<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Charlie's Blog</title>
    <link>https://www.chiply.dev</link>
    <description>A personal blog built with Emacs Org and SvelteKit</description>
    <language>en-us</language>
    <lastBuildDate>Thu, 07 May 2026 19:35:26 GMT</lastBuildDate>
    <atom:link href="https://www.chiply.dev/rss.xml" rel="self" type="application/rss+xml"/>

    <item>
      <title>My Dotfiles: macOS Bootstrap and an Emacs Distribution</title>
      <link>https://www.chiply.dev/post-my-dotfiles</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-my-dotfiles</guid>
      <pubDate>Thu, 07 May 2026 15:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>My dotfiles for my MacOS rice and Emacs configuration live in two public repositories. Both repos are shared as a reference; clone, fork, or just lift the bits that look useful to you!</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dotfiles">dotfiles</span>&#xa0;<span class="macos">macos</span>&#xa0;<span class="emacs">emacs</span>&#xa0;<span class="setup">setup</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgd9cf010" class="figure">
<p><img src="https://www.chiply.dev/images/dotfiles-banner.jpeg" alt="dotfiles-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
My dotfiles for my MacOS rice and Emacs configuration live in two public repositories.  Both repos are shared as a reference; clone, fork, or just lift the bits that look useful to you!
</p>

<p>
This post is a thin entry point, and the READMEs in each repo carry the actual detail.
</p>
</div>
</div>
<div id="outline-container-files" class="outline-2">
<h2 id="files"><span class="section-number-2">2.</span> <a href="https://github.com/chiply/.files">chiply/.files</a>&#xa0;&#xa0;&#xa0;<span class="tag"><span class="shell">shell</span>&#xa0;<span class="tmux">tmux</span>&#xa0;<span class="macos">macos</span>&#xa0;<span class="bootstrap">bootstrap</span></span></h2>
<div class="outline-text-2" id="text-files">
<p>
A single <code>bootstrap.sh</code> that takes a clean macOS install to a fully provisioned development machine in roughly thirty minutes.  It installs Xcode CLI tools, Homebrew, and a long list of CLI utilities and language toolchains, then symlinks every config in <code>files/</code> into the matching path under <code>$HOME</code>.
</p>

<p>
See repo for installation instructions.
</p>

<p>
What gets installed:
</p>

<ul class="org-ul">
<li><b>Shell</b>: zsh + zinit, <a href="https://starship.rs/">starship</a> prompt, <a href="https://atuin.sh/">atuin</a> shared history</li>
<li><b>Terminal</b>: <a href="https://ghostty.org/">Ghostty</a> with cursor shaders, plus Nerd Fonts</li>
<li><b>Multiplexer</b>: tmux with tmux-powerline, tmuxinator, TPM</li>
<li><b>Window manager</b>: <a href="https://github.com/nikitabobko/AeroSpace">AeroSpace</a>, <a href="https://www.jeantinland.com/toolbox/simple-bar/">simple-bar</a>, <a href="https://github.com/FelixKratz/JankyBorders">JankyBorders</a></li>
<li><b>Languages</b>: pyenv, Poetry, uv, nvm + Node, language servers (json, eslint, copilot, svelte)</li>
<li><b>CLI</b>: <code>gh</code>, <code>k9s</code>, <code>bat</code>, <code>fzf</code>, <code>ripgrep</code>, <code>eza</code>, <code>jq</code>, <code>lazygit</code>, AWS CLI v2, and more</li>
</ul>

<p>
The full package list lives in the repo's <a href="https://github.com/chiply/.files/blob/main/files/.config/Brewfile">Brewfile</a>.  <code>bootstrap.sh</code> also clones <a href="https://www.chiply.dev/#zetta">.zetta.d</a> to <code>~/.zetta.d</code> as part of the Emacs setup; if you only want the shell side, comment out the emacs section.
</p>
</div>
</div>
<div id="outline-container-zetta" class="outline-2">
<h2 id="zetta"><span class="section-number-2">3.</span> <a href="https://github.com/chiply/.zetta.d">chiply/.zetta.d</a>&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="zetta">zetta</span>&#xa0;<span class="distribution">distribution</span></span></h2>
<div class="outline-text-2" id="text-zetta">
<p>
My Emacs configuration, packaged as a small distribution.  Around 320 packages wired up via a module DSL, with an Elpaca lockfile pinning every package to an exact commit, and byte- and native-compilation done up front so the first launch is clean.
</p>

<p>
The name is a cheeky play on how we name certain minimalist text editors after <a href="https://en.wikipedia.org/wiki/Metric_prefix">Metric Prefixes</a> <code>nano</code> (10^-9) or <code>pico</code> (10^-12).  This maximalist editor config is named after <code>zetta</code> (10^21).
</p>

<p>
See repo for installation instructions.
</p>

<p>
Notable parts:
</p>

<ul class="org-ul">
<li><b>Triple-modal editing</b>: Evil, Meow, and vanilla side-by-side, switchable on the fly</li>
<li><b>Module DSL</b>: enable categories or individual packages via <code>zetta-modules!</code> in <code>~/.zetta.el</code></li>
<li><b>Reproducible</b>: lockfile + compile-by-default install</li>
<li><b>CI-tested</b>: Emacs 29.4, 30.2, and the 31 snapshot</li>
</ul>

<p>
Zetta also hosts several small packages I've written that live in their own public repos.  See the README's "Bundled custom packages" section for the list.  None of these are released or publicized yet, so bring a pinch of salt if you choose to try them.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Emacs Completion Showcase with VOMPECCC (video)</title>
      <link>https://www.chiply.dev/post-vompeccc-showcase</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-vompeccc-showcase</guid>
      <pubDate>Tue, 05 May 2026 18:15:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>This is the fifth post in my series on Emacs completion. The first, Incremental Completing Read (ICR), explains what modern completion actually is, and how Emacs exposes it as a programmable substrate...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="completion">completion</span>&#xa0;<span class="workflows">workflows</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org4d9857b" class="figure">
<p><img src="https://www.chiply.dev/images/vompeccc-showcase-banner.jpeg" alt="vompeccc-showcase-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/dall-e-3">DALL-E 3</a></p>
</div>

<p>
This is the fifth post in my series on Emacs completion.  The <a href="https://www.chiply.dev/post-icr-primer">first</a>, <a href="https://www.chiply.dev/post-icr-primer#what-is-incremental-completing-read-">Incremental Completing Read (ICR)</a>, explains what modern completion actually is, and how Emacs exposes it as a programmable <i>substrate</i> rather than a closed UI.  The <a href="https://www.chiply.dev/post-vompeccc">second</a> introduced the <a href="https://www.chiply.dev/post-vompeccc#vompeccc-framework">VOMPECCC</a> stack of eight packages covering the <a href="https://www.chiply.dev/post-vompeccc#hidden-complexity">six orthogonal concerns</a> of a complete completion system.  The <a href="https://www.chiply.dev/post-vompeccc-spot">third</a> toured <code>spot</code>, a Spotify client built as a thin <a href="https://www.chiply.dev/post-vompeccc-spot#candidates-as-currency">shim</a> on top of those packages.  And the <a href="https://www.chiply.dev/post-vompeccc-fruits">fourth</a> built a produce picker from scratch, demonstrating the specific features that each VOMPECCC package provides.
</p>

<p>
This post is the practical complement to all the other posts.  Here, we showcase over a dozen workflows I use every day.  Most are powered entirely by features that ship in the box with the VOMPECCC packages, and there are 'Bonuses' which demonstrate workflows enable by 3rd party packages that build on top of VOMPECCC.  The prose is deliberately thin, and you will find most of the demonstration is in the video below.
</p>
</div>
</div>
<div id="outline-container-video" class="outline-2">
<h2 id="video"><span class="section-number-2">2.</span> The Video&#xa0;&#xa0;&#xa0;<span class="tag"><span class="demo">demo</span></span></h2>
<div class="outline-text-2" id="text-video">
<p>
<div class="youtube-container">
<iframe src="https://www.youtube-nocookie.com/embed/wyzUXScErjE"
        title="A dozen high-impact VOMPECCC workflows &#x2014; search, refactor, navigate, batch-act, and pivot mid-prompt, all using built-in features of the eight packages."
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
</iframe>
</div>
</p>

<p>
As in the previous posts in this series, the upper-right of my Emacs (in the <code>tab-bar</code>) shows the keybindings and command names I am invoking, so you can map what you see onto your own configuration.
</p>
</div>
</div>
<div id="outline-container-configuration" class="outline-2">
<h2 id="configuration"><span class="section-number-2">3.</span> A Note on My Configuration&#xa0;&#xa0;&#xa0;<span class="tag"><span class="setup">setup</span></span></h2>
<div class="outline-text-2" id="text-configuration">
<p>
Two configuration choices show up repeatedly and are worth naming once upfront so the keystrokes are intelligible.
</p>

<p>
<b>Async split character.</b>  My <code>consult-async-split-style</code> is <code>comma</code>, not the default <code>#</code>.  In Consult commands like <code>consult-ripgrep</code>, everything before the first <code>,</code> is sent to the external tool as the search pattern, and everything after is filtered locally with my completion style.
</p>

<p>
<b>Orderless dispatchers.</b>  My <code>orderless-style-dispatchers</code> bind affix characters to matching styles: <code>@</code> for <a href="https://www.chiply.dev/post-vompeccc#marginalia">Marginalia</a>-annotation matching, <code>~</code> for flex, <code>`</code> for initialism, <code>!</code> for negation.  Each can be a prefix or suffix on a component.  My <code>orderless-component-separator</code> is also <code>,</code>, so a single comma serves double duty depending on context.
</p>
</div>
</div>
<div id="outline-container-ripgrep-export-wgrep" class="outline-2">
<h2 id="ripgrep-export-wgrep"><span class="section-number-2">4.</span> Multi-File Refactor&#xa0;&#xa0;&#xa0;<span class="tag"><span class="ripgrep">ripgrep</span>&#xa0;<span class="embark">embark</span>&#xa0;<span class="wgrep">wgrep</span></span></h2>
<div class="outline-text-2" id="text-ripgrep-export-wgrep">
<p>
<code>consult-ripgrep</code> → input your search term → <code>embark-export</code> → <code>wgrep-change-to-wgrep-mode</code> → edit as you like → <code>C-&lt;return&gt;</code>
</p>
</div>
</div>
<div id="outline-container-ripgrep-two-stage" class="outline-2">
<h2 id="ripgrep-two-stage"><span class="section-number-2">5.</span> Async + Local Two-Stage Search&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="async">async</span>&#xa0;<span class="orderless">orderless</span></span></h2>
<div class="outline-text-2" id="text-ripgrep-two-stage">
<p>
<code>consult-ripgrep</code> with <code>,</code> splitting external (ripgrep) from local (Orderless): for example, <code>error,handler,~retry,!test</code>.
</p>
</div>
</div>
<div id="outline-container-consult-buffer" class="outline-2">
<h2 id="consult-buffer"><span class="section-number-2">6.</span> Unified Buffer / File / Bookmark Switcher&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="narrowing">narrowing</span></span></h2>
<div class="outline-text-2" id="text-consult-buffer">
<p>
<code>consult-buffer</code> with narrowing keys: <code>b SPC</code> for buffers, <code>f SPC</code> for recent files, <code>m SPC</code> for bookmarks, <code>p SPC</code> for project items.
</p>
</div>
</div>
<div id="outline-container-consult-line" class="outline-2">
<h2 id="consult-line"><span class="section-number-2">7.</span> Buffer and Project-Wide Line Search&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="preview">preview</span></span></h2>
<div class="outline-text-2" id="text-consult-line">
<p>
<code>consult-line</code> within the current buffer; <code>consult-line-multi</code> across all buffers (or the project, with a prefix argument).
</p>
</div>
</div>
<div id="outline-container-consult-imenu" class="outline-2">
<h2 id="consult-imenu"><span class="section-number-2">8.</span> Code Symbol Navigation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="imenu">imenu</span>&#xa0;<span class="navigation">navigation</span></span></h2>
<div class="outline-text-2" id="text-consult-imenu">
<p>
<code>consult-imenu</code> within the current buffer; <code>consult-imenu-multi</code> across every buffer of the same major mode.
</p>
</div>
</div>
<div id="outline-container-consult-info" class="outline-2">
<h2 id="consult-info"><span class="section-number-2">9.</span> Documentation Search&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="docs">docs</span></span></h2>
<div class="outline-text-2" id="text-consult-info">
<p>
<code>consult-info</code> for Info manuals (Emacs, Elisp, Org, plus every package that ships its own <code>.info</code> file); <code>consult-man</code> for system man pages.
</p>
</div>
</div>
<div id="outline-container-mx-by-annotation" class="outline-2">
<h2 id="mx-by-annotation"><span class="section-number-2">10.</span> Find Commands by Docstring&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marginalia">marginalia</span>&#xa0;<span class="orderless">orderless</span></span></h2>
<div class="outline-text-2" id="text-mx-by-annotation">
<p>
<code>M-x window @frame</code>.  The <code>@</code> dispatcher routes a component through <code>orderless-annotation</code> to match against <a href="https://www.chiply.dev/post-vompeccc#marginalia">Marginalia</a>'s docstring text rather than the candidate name.  This lets you query for commands by what they <i>do</i> rather than what they are called.
</p>
</div>
</div>
<div id="outline-container-embark-act-all" class="outline-2">
<h2 id="embark-act-all"><span class="section-number-2">11.</span> Mass Action Across Candidates&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="batch">batch</span></span></h2>
<div class="outline-text-2" id="text-embark-act-all">
<p>
<code>C-&gt;</code> (<code>embark-act-all</code>) runs a single Embark action on every candidate currently surviving in the prompt.
</p>

<p>
You can also <code>embark-select</code> to create a subset of displayed candidates and use <code>embark-act-all</code> to act on only those selected candidates.
</p>
</div>
</div>
<div id="outline-container-embark-become" class="outline-2">
<h2 id="embark-become"><span class="section-number-2">12.</span> Pivot Mid-Prompt&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="flow">flow</span></span></h2>
<div class="outline-text-2" id="text-embark-become">
<p>
<code>embark-become</code> switches the active command (e.g. <code>find-file</code> → <code>switch-to-buffer</code>) without losing the input I have already typed.
</p>
</div>
</div>
<div id="outline-container-xref-export" class="outline-2">
<h2 id="xref-export"><span class="section-number-2">13.</span> Symbol-Aware Multi-File Refactor&#xa0;&#xa0;&#xa0;<span class="tag"><span class="xref">xref</span>&#xa0;<span class="embark">embark</span>&#xa0;<span class="wgrep">wgrep</span></span></h2>
<div class="outline-text-2" id="text-xref-export">
<p>
<code>xref-find-references</code> (<code>M-?</code>, with <code>xref-show-xrefs-function</code> set to <code>consult-xref</code>) → <code>embark-export</code> → <code>wgrep-change-to-wgrep-mode</code> → edit → <code>C-&lt;return&gt;</code> to write your changes.  This is very similar to the <a href="https://www.chiply.dev/#ripgrep-export-wgrep">ripgrep version above</a> but driven by the language server, so <code>foo</code> the variable and <code>foo</code> the unrelated comment stay separate.
</p>
</div>
</div>
<div id="outline-container-recent-files-dired" class="outline-2">
<h2 id="recent-files-dired"><span class="section-number-2">14.</span> Recent Files as a Filesystem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dired">dired</span>&#xa0;<span class="embark">embark</span></span></h2>
<div class="outline-text-2" id="text-recent-files-dired">
<p>
<code>consult-recent-file</code> → <code>embark-export</code> produces a <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html">Dired</a> buffer, putting every Dired operation (mark, copy, rename, chmod, batch shell command) on the recent-files set.
</p>
</div>
</div>
<div id="outline-container-vertico-quick" class="outline-2">
<h2 id="vertico-quick"><span class="section-number-2">15.</span> Avy-Style Jump Then Act&#xa0;&#xa0;&#xa0;<span class="tag"><span class="vertico">vertico</span>&#xa0;<span class="embark">embark</span></span></h2>
<div class="outline-text-2" id="text-vertico-quick">
<p>
In any Vertico session, <code>C-'</code> jumps to a labeled candidate (<a href="https://github.com/abo-abo/avy">Avy</a>-style); <code>C-"</code> does the same jump and hands the candidate to Embark.
</p>
</div>
</div>
<div id="outline-container-vertico-repeat" class="outline-2">
<h2 id="vertico-repeat"><span class="section-number-2">16.</span> Resume the Last Session&#xa0;&#xa0;&#xa0;<span class="tag"><span class="vertico">vertico</span>&#xa0;<span class="repeat">repeat</span></span></h2>
<div class="outline-text-2" id="text-vertico-repeat">
<p>
<code>s-V</code> (<code>vertico-repeat</code>) reopens the last completion session with its prompt, input, and selected candidate intact.
</p>
</div>
</div>
<div id="outline-container-consult-ls-git" class="outline-2">
<h2 id="consult-ls-git"><span class="section-number-2">17.</span> Bonus: Magit-Style Working Copy as Completion :consult-ls-git&#xa0;&#xa0;&#xa0;<span class="tag"><span class="git">git</span></span></h2>
<div class="outline-text-2" id="text-consult-ls-git">
<p>
<a href="https://github.com/rcj/consult-ls-git"><code>consult-ls-git</code></a> surfaces working-copy status, tracked files, and branches in a single multi-source prompt with narrowing keys.  A nice on-the-fly alternative to the Magit status buffer.
</p>
</div>
</div>
<div id="outline-container-consult-gh-repos" class="outline-2">
<h2 id="consult-gh-repos"><span class="section-number-2">18.</span> Bonus: Browse GitHub Repos from the Minibuffer :consult-gh&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span></span></h2>
<div class="outline-text-2" id="text-consult-gh-repos">
<p>
<a href="https://github.com/armindarvish/consult-gh"><code>consult-gh-search-repos</code></a> streams GitHub repos as candidates; <code>C-=</code> previews the README; <code>M-S</code> (<code>vertico-suspend</code>) (or simply moving your cursor out of the minibuffer) detaches the minibuffer for free reading; <code>s-V</code> (<code>vertico-repeat</code>) (or simply moving your cursor back into the minibuffer) resumes; <code>C-.</code> exposes Embark actions (clone, browse, view issues, view PRs, view files, fork).
</p>
</div>
</div>
<div id="outline-container-consult-gh-code" class="outline-2">
<h2 id="consult-gh-code"><span class="section-number-2">19.</span> Bonus: Search All Public Code on GitHub :consult-gh:code-search:</h2>
<div class="outline-text-2" id="text-consult-gh-code">
<p>
<code>consult-gh-search-code</code> against the contents of every public repository on GitHub.  You get the same VOMPECCC features, but with the search space expanded to "all open source code in the world".
</p>
</div>
</div>
<div id="outline-container-consult-omni-web" class="outline-2">
<h2 id="consult-omni-web"><span class="section-number-2">20.</span> Bonus: Multi-Source Web Search :consult-omni&#xa0;&#xa0;&#xa0;<span class="tag"><span class="web">web</span></span></h2>
<div class="outline-text-2" id="text-consult-omni-web">
<p>
<a href="https://github.com/armindarvish/consult-omni"><code>consult-omni-web</code></a> fans one query out across Google, Brave, Wikipedia, StackOverflow, YouTube, and a <a href="https://github.com/karthink/gptel"><code>gptel</code></a>-backed LLM source simultaneously; <code>s-j</code> / <code>s-k</code> jumps between source groups; <code>C-=</code> previews; <code>C-.</code> surfaces Embark alternates (open in EWW, copy URL, etc.).
</p>
</div>
</div>
<div id="outline-container-closing" class="outline-2">
<h2 id="closing"><span class="section-number-2">21.</span> Closing&#xa0;&#xa0;&#xa0;<span class="tag"><span class="closing">closing</span></span></h2>
<div class="outline-text-2" id="text-closing">
<p>
What makes this so cool is that none of these workflows required a single line of custom code.  Each is built entirely out of the features that ship with one or more of the <a href="https://www.chiply.dev/post-vompeccc#vompeccc-framework">VOMPECCC</a> packages.  Pick the two or three that map onto frictions you already feel, and the rest will reveal themselves ad-hoc as you encounter new frictions.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">22.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Over a dozen high-impact Emacs workflows are demonstrated in this post:  <a href="https://www.chiply.dev/#ripgrep-export-wgrep">multi-file refactor</a>, <a href="https://www.chiply.dev/#ripgrep-two-stage">two-stage ripgrep</a>, <a href="https://www.chiply.dev/#consult-buffer">unified buffer switching</a>, <a href="https://www.chiply.dev/#consult-line">line search with preview</a>, <a href="https://www.chiply.dev/#consult-imenu">symbol navigation</a>, <a href="https://www.chiply.dev/#consult-info">docs search</a>, <a href="https://www.chiply.dev/#mx-by-annotation">M-x by docstring</a>, <a href="https://www.chiply.dev/#embark-act-all">batch action</a>, <a href="https://www.chiply.dev/#embark-become">mid-prompt pivot</a>, <a href="https://www.chiply.dev/#xref-export">symbol-aware refactor</a>, <a href="https://www.chiply.dev/#recent-files-dired">recent files as Dired</a>, and <a href="https://www.chiply.dev/#vertico-quick">quick jump + act</a>, <a href="https://www.chiply.dev/#vertico-repeat">session resume</a>.  Each of these workflows is composed entirely from features that ship in the box with the <a href="https://www.chiply.dev/post-vompeccc#vompeccc-framework">VOMPECCC</a> packages.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>David Foster Wallace&apos;s Hidden Names: an Anagram Atlas of Infinite Jest</title>
      <link>https://www.chiply.dev/post-ij-anagrams</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-ij-anagrams</guid>
      <pubDate>Sat, 02 May 2026 09:07:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>DFW famously embedded wordplay in his character names. The surname Pemulis is an anagram of impulse. Otis P. Lord rearranges to Sport Idol, the absurd authority figure of the Eschaton game he runs. Th...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="dfw">dfw</span>&#xa0;<span class="anagrams">anagrams</span></span></h2>
<div class="outline-text-2" id="text-about">
<p>
DFW famously embedded wordplay in his character names. The surname <i>Pemulis</i> is an anagram of <i>impulse</i>. <i>Otis P. Lord</i> rearranges to <i>Sport Idol</i>, the absurd authority figure of the Eschaton game he runs. This essay catalogues every named character in <i>Infinite Jest</i>, computes letter-multiset anagrams of their full names, and asks of each: did DFW intend it?
</p>

<p>
Each character section can show up to three registers of name-play, depending on what the name yields. <b>Name analysis</b> covers non-anagram wordplay — etymology (Incandenza ← <i>incandescere</i>), substring derivations (Madame Psychosis ← <i>metempsychosis</i>), phonetic puns, initialisms (J.O.I. = <i>joy</i>), translations, allusions, double entendres. <b>Possible anagrams</b> are curated rearrangements selected for thematic resonance with the character. <b>Algorithmic candidates</b> are the raw output of a subset-sum search over the top 30K English words.
</p>

<p>
Three caveats. First, the only externally confirmed anagrams in the novel's character names are <i>Pemulis</i> (surname) and <i>Otis P. Lord</i> (full name) — everything else here is speculative. Second, every anagram displayed has been deterministically verified to use exactly the letters of the source name (no leftovers, no extras). Third, the name-analysis observations are interpretive readings, not claims about authorial intent.
</p>

<p>
Each section below is one character. Sections are ordered by mention count in the novel — Don Gately first, the long tail of named cleaners and Eschaton combatants last. Use the left-side table of contents to navigate.
</p>
</div>
</div>
<div id="outline-container-methodology" class="outline-2">
<h2 id="methodology"><span class="section-number-2">2.</span> Methodology&#xa0;&#xa0;&#xa0;<span class="tag"><span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-methodology">
<p>
Pipeline: character extraction (web sources + spaCy NER on the full PDF) → algorithmic anagram search (subset-sum-on-multisets over the top 30K English words by frequency, max 3 words per anagram) → deterministic letter-multiset verification. <i>Possible anagrams</i> come from a separate curatorial pass: candidate rearrangements are selected for thematic resonance with the character, then run through the same letter-multiset check.
</p>

<p>
For each character, the catalogue shows two name forms where they differ — a <b>short</b> form (first + last) and a <b>full</b> form (with middle initials/names). The two forms have different letter multisets and therefore different anagram opportunities.
</p>

<p>
Each interpretation is tagged <i>[strong]</i> (external corroboration), <i>[suggestive]</i> (internally coherent with character traits), or <i>[speculative]</i> (plausible but possibly Texas-sharpshooter). Default is speculative; confidence climbs only with evidence.
</p>
</div>
</div>
<div id="outline-container-don-gately" class="outline-2">
<h2 id="don-gately"><span class="section-number-2">3.</span> Don Gately&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="hugeness">hugeness</span>&#xa0;<span class="sobriety">sobriety</span>&#xa0;<span class="physicalendurance">physicalendurance</span></span></h2>
<div class="outline-text-2" id="text-don-gately">
<p>
A 6'6" former Demerol addict and burglar serving as live-in staffer at Ennet House Drug and Alcohol Recovery House, defined by gentle physical menace and an embodied, unsentimental relationship to AA-style sobriety. After a gun-shot wound from defending residents, he spends the novel's late stretches feverish and pre-verbal in a hospital bed, refusing narcotic painkillers as a last act of will.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>gately ← Old English geatlic, 'belonging to a gate'</b> — As Ennet House's live-in staffer and bouncer, Gately is literally the gatekeeper — the man at the door deciding who enters and who is removed. The surname performs the role.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>gate / gately (Old English geatlic, 'belonging to a gate')</b> — As Ennet House's live-in staffer and bouncer, Gately is literally the gatekeeper — the man at the door who decides who enters and who is removed. The surname performs the role.</li>
<li><i>[Phonetic]</i> <b>gait-ly</b> — The name evokes 'gait' — fitting for a 6'6" character whose lumbering physical presence and walk are a constant feature of the prose.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Don Gately</b>
</p>
<ul class="org-ul">
<li><b>any old get</b> — Ennet House staffer Gately accepts any old derelict who comes through the door (suggestive).</li>
<li><b>only get ad</b> — His sober life as one long advertisement for the AA program (loose).</li>
<li><b>today glen</b> — An anonymous everyman name for the silent giant (loose).</li>
<li><b>dog late ny</b> — Lenz's late dog-killing on New England streets triggers Gately's gunshot wound (loose).</li>
<li><b>any dog let</b> — He'd let any dog into Ennet House — judgement-free, animal-shelter style (suggestive).</li>
</ul>

<p>
<b>Donald W. Gately</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>along teddy law</b> — His backstory under the law alongside his junkie pal Teddy (suggestive).</li>
<li><b>dental gold way</b> — His gap-toothed grin and golden-hearted way (loose).</li>
<li><b>deadly long wat</b> — His deadly long Watertown wait for the ambulance after being shot (suggestive).</li>
<li><b>delay want gold</b> — AA's delay-want lesson: the gold of sobriety lies past the want (suggestive).</li>
<li><b>all god went day</b> — His deathbed sense that all god went out of the day (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Don Gately</b>
</p>
<ul class="org-ul">
<li>today glen</li>
<li>doyle tang</li>
<li>dayton leg</li>
<li>dayton gel</li>
<li>neatly god</li>
<li>neatly dog</li>
<li>tangled yo</li>
<li>dealt yong</li>
<li>delta yong</li>
<li>get any old</li>
<li>angled toy</li>
<li>godly neat</li>
<li>godly nate</li>
<li>godly ante</li>
<li>delay tong</li>
<li>gated only</li>
<li>gated lyon</li>
<li>any let god</li>
<li>gently ado</li>
<li>daley tong</li>
<li>alton edgy</li>
<li>gayle dont</li>
<li>golden tay</li>
<li>tangled oy</li>
<li>tonal edgy</li>
<li>gently oda</li>
<li>longed tay</li>
<li>gently dao</li>
<li>land yet go</li>
<li>gold yet an</li>
<li>any got led</li>
<li>any let dog</li>
<li>lady get on</li>
<li>lady get no</li>
<li>lady ten go</li>
<li>glad yet on</li>
<li>glad yet no</li>
<li>not led gay</li>
<li>old ten gay</li>
<li>end lot gay</li>
<li>only get ad</li>
<li>long yet ad</li>
<li>lady net go</li>
<li>old gay net</li>
<li>gold ten ya</li>
<li>gold net ya</li>
<li>long day et</li>
<li>gold any et</li>
<li>dont leg ya</li>
<li>not day leg</li>
</ul>

<p>
<b>Donald W. Gately</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>wanted gold lay</li>
<li>golden away ltd</li>
<li>newly data gold</li>
<li>delay want gold</li>
<li>delay town glad</li>
<li>wanted lady log</li>
<li>wayne told glad</li>
<li>lately dawn god</li>
<li>lately dawn dog</li>
<li>deadly want log</li>
<li>wallet andy god</li>
<li>wallet andy dog</li>
<li>dental gold way</li>
<li>wanted ally god</li>
<li>wanted ally dog</li>
<li>delay glad wont</li>
<li>lodge want lady</li>
<li>wanted doll gay</li>
<li>dylan told wage</li>
<li>wanted lloyd ga</li>
<li>lloyd want aged</li>
<li>lloyd gate dawn</li>
<li>deadly lawn got</li>
<li>wanted lloyd ag</li>
<li>lately wang odd</li>
<li>deadly wang lot</li>
<li>delay told wang</li>
<li>lloyd date wang</li>
<li>towel glad andy</li>
<li>dodge want ally</li>
<li>deadly glow tan</li>
<li>wallet yang odd</li>
<li>dental glow day</li>
<li>dealt andy glow</li>
<li>delta andy glow</li>
<li>dylan date glow</li>
<li>golden walt day</li>
<li>deadly wagon lt</li>
<li>deadly wagon tl</li>
<li>delay wagon ltd</li>
<li>lodge andy walt</li>
<li>allowed yang td</li>
<li>along teddy law</li>
<li>logan teddy law</li>
<li>wagon teddy all</li>
<li>teddy goal lawn</li>
<li>teddy alan glow</li>
<li>teddy anal glow</li>
<li>alley todd wang</li>
<li>deadly glow ant</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-james-orin-incandenza" class="outline-2">
<h2 id="james-orin-incandenza"><span class="section-number-2">4.</span> James Orin Incandenza&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="father">father</span>&#xa0;<span class="filmmaker">filmmaker</span>&#xa0;<span class="suicide">suicide</span></span></h2>
<div class="outline-text-2" id="text-james-orin-incandenza">
<p>
Optical physicist turned avant-garde filmmaker and founder of Enfield Tennis Academy, known as Himself or The Mad Stork, whose final film 'Infinite Jest' is so entertaining it kills viewers. A near-mute alcoholic father obsessed with annulment, communication failure, and his muse Joelle, he kills himself by inserting his head into a modified microwave.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>J.O.I. = 'joy'</b> — His initials spell 'joy' — cruelly ironic for a depressive alcoholic who microwaves his head and whose final film is named Infinite Jest, the entertainment so pleasurable it kills.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · initialism · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Initialism]</i> <b>J.O.I. = 'joy'</b> — His initials read as 'joy', cruelly ironic for a depressive alcoholic who kills himself by microwaving his head and whose final film is named 'Infinite Jest.'</li>
<li><i>[Etymology]</i> <b>incandescent (Latin incandescere, 'to glow with heat')</b> — The surname encodes 'incandesce' — apt for an optical physicist obsessed with lenses and light, and for a man who literally cooks his own brain in a microwave.</li>
<li><i>[Substring]</i> <b>candenza / cadenza</b> — A 'cadenza' is a virtuosic solo flourish at the end of a movement; James's auteur career and final film function as a terminal cadenza, his solo before the silence.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>James Incandenza</b>
</p>
<ul class="org-ul">
<li><b>ascend ninja maze</b> — Himself ascends silently like a ninja through his maze of optical films (suggestive).</li>
<li><b>jamaicans zen den</b> — His late-life solitary microwave den has a Jamaican-zen aspect (loose).</li>
<li><b>jasmine cadenza n</b> — His final film as a fragrant cadenza cut off mid-note (loose).</li>
<li><b>damascene ninja z</b> — His Damascene conversion to filmmaking, silent as a ninja (loose).</li>
<li><b>cinema jansen ad z</b> — His cinema career filtered through a Jansen-style optical ad (loose).</li>
</ul>

<p>
<b>James Orin Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>incandenza senior jam</b> — He is the senior Incandenza, in a creative jam over his unfinished anti-anhedonia film (strong).</li>
<li><b>incandenza reason jim</b> — Jim's reason for the Entertainment: cure his son's anhedonia (strong).</li>
<li><b>americans dozen ninja</b> — His samizdat films killed dozens of Americans like silent ninjas (loose).</li>
<li><b>jamaican dozens inner</b> — The inner-dozens of Jamaican-spy distribution channels his film travels (loose).</li>
<li><b>ninja amazed corinne s</b> — His silent ninja-camera amazed any 'Corinne' he filmed (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>James Incandenza</b>
</p>
<ul class="org-ul">
<li>dances ninja maze</li>
<li>canned james nazi</li>
<li>jasmine canned az</li>
<li>jamaican needs nz</li>
<li>jamaican dense nz</li>
<li>jamaican send zen</li>
<li>jamaican ends zen</li>
<li>camden jeans nazi</li>
<li>camden nazis jane</li>
<li>camden nazis jean</li>
<li>amazed cannes jin</li>
<li>canadians jeez nm</li>
<li>canadians jeez mn</li>
<li>jeanne adams zinc</li>
<li>jensen damian zac</li>
<li>scanned maize jan</li>
<li>jensen mazda cain</li>
<li>since jenna mazda</li>
<li>scene ninja mazda</li>
<li>incense mazda jan</li>
<li>ascend ninja maze</li>
<li>jennie mazda scan</li>
<li>jennie mazda cans</li>
<li>scanned amaze jin</li>
<li>dances minaj zane</li>
<li>ascend minaj zane</li>
<li>canned nazism jae</li>
<li>jeanne nazism cad</li>
<li>nazism dance jane</li>
<li>nazism dance jean</li>
<li>jasmine canned za</li>
<li>scanned maze jain</li>
<li>manned nazis jace</li>
<li>canned amazes jin</li>
<li>canadiens maze nj</li>
<li>canadiens zane mj</li>
<li>canadiens zane jm</li>
<li>canadiens jam zen</li>
<li>canadiens zen maj</li>
<li>eczema ninja sand</li>
<li>eczema ninja dans</li>
<li>amazed jenna ncis</li>
<li>canned janis maze</li>
<li>camden janis zane</li>
<li>jeanne mazda ncis</li>
<li>amazed ninjas nec</li>
<li>eczema ninjas and</li>
<li>eczema ninjas dan</li>
<li>eczema ninjas dna</li>
<li>ninjas dance maze</li>
</ul>

<p>
<b>James Orin Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>americans dozen ninja</li>
<li>american dozens ninja</li>
<li>jamaican dinner zones</li>
<li>jamaican dozens inner</li>
<li>jamaican dinners zone</li>
<li>jamaican sinner dozen</li>
<li>ordnance jasmine nazi</li>
<li>mandarin janice zones</li>
<li>dinners amazon janice</li>
<li>dominican jensen zara</li>
<li>mendoza arsenic ninja</li>
<li>jamaican dinners enzo</li>
<li>jordanian cinemas zen</li>
<li>americans ninja zoned</li>
<li>jamaican sinner zoned</li>
<li>ordinances ninja maze</li>
<li>insomnia crazed jenna</li>
<li>ordinances minaj zane</li>
<li>ordinance nazism jane</li>
<li>ordinance nazism jean</li>
<li>janeiro canned nazism</li>
<li>zionism canada jenner</li>
<li>canadians mornin jeez</li>
<li>jordanian eczema sinn</li>
<li>jordanian eczema inns</li>
<li>endocrine nazism jana</li>
<li>dominance ninjas ezra</li>
<li>dominance ninjas reza</li>
<li>ordinance ninjas maze</li>
<li>american ninjas dozen</li>
<li>american ninjas zoned</li>
<li>ordnance ninjas maize</li>
<li>modernize ninjas ncaa</li>
<li>corinne amazed ninjas</li>
<li>jordanian menzies can</li>
<li>jordanian menzies anc</li>
<li>mansion crazed janine</li>
<li>manners zodiac janine</li>
<li>mendoza cairns janine</li>
<li>discern amazon janine</li>
<li>dominance jansen ariz</li>
<li>dominican jansen ezra</li>
<li>dominican jansen reza</li>
<li>arizona minced jansen
<i>Search timed out at 45s with 44 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-michael-pemulis" class="outline-2">
<h2 id="michael-pemulis"><span class="section-number-2">5.</span> Michael Pemulis&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="scheming">scheming</span>&#xa0;<span class="drugdealing">drugdealing</span>&#xa0;<span class="mathematics">mathematics</span></span></h2>
<div class="outline-text-2" id="text-michael-pemulis">
<p>
Hal's wisecracking best friend and ETA's resident drug dealer and statistician, a working-class Allston kid with a yachting cap and a mathematical mind for probability and Eschaton. He is the primary source of the DMZ that haunts the novel's drug subplot, and is eventually expelled from the Academy.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>michael mathew impulse</b> — His full name unpacks cleanly to 'Michael Mathew impulse' — a fitting tag for ETA's reckless drug-dealing schemer who lives by tactical impulse and eventually gets expelled for it.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Michael Pemulis</b>
</p>
<ul class="org-ul">
<li><b>muslim peach lie</b> — His working-class Allston bravado conceals a peachy lie (loose).</li>
<li><b>michelle aim ups</b> — His mathematical aim aimed up high (Eschaton trajectories), behind a 'Michelle' alias (loose).</li>
<li><b>pemulis each mil</b> — Pemulis is each mil(itary) tactician of ETA's Eschaton war game (suggestive).</li>
<li><b>muscle email hip</b> — His hip muscle of email and rumor inside ETA (loose).</li>
<li><b>eclipse mail hum</b> — He eclipses his peers and the mailroom hums with his deals (loose).</li>
</ul>

<p>
<b>Michael Mathew Pemulis</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>michael mathew impulse</b> — Pemulis is named-and-impulsed: dealing always on impulse (strong).</li>
<li><b>michael mathew limp sue</b> — His limping legal hangover after the DMZ scandal — sue Michael (loose).</li>
<li><b>michael wimples ate hum</b> — The wimples (head-wraps) of his ETA conspiracies, eating the hum of school (loose).</li>
<li><b>michael temples wham iu</b> — Wham of his Eschaton temple-attack on Hal/IU-style targets (loose).</li>
<li><b>wimples michael math eu</b> — Wimples of math and his Eu-style class consciousness (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Michael Pemulis</b>
</p>
<ul class="org-ul">
<li>michael impulse</li>
<li>michael miles up</li>
<li>music email help</li>
<li>muslim cheap lie</li>
<li>michael smile up</li>
<li>muscle email hip</li>
<li>michael mile ups</li>
<li>samuel mile chip</li>
<li>helps miami clue</li>
<li>michelle aims up</li>
<li>michelle aim ups</li>
<li>michael pile sum</li>
<li>muslim each pile</li>
<li>emails much pile</li>
<li>muslim epic heal</li>
<li>samuel phil mice</li>
<li>michael pulse im</li>
<li>michael pulse mi</li>
<li>implies clue ham</li>
<li>implies heal cum</li>
<li>muslim chile epa</li>
<li>memphis clue ali</li>
<li>simple mice haul</li>
<li>michelle ups mia</li>
<li>michael lime ups</li>
<li>samuel chip lime</li>
<li>miami clues help</li>
<li>eclipse hail mum</li>
<li>musical peel him</li>
<li>muslim chapel ie</li>
<li>muslim peach lie</li>
<li>memphis ucla lie</li>
<li>memphis lace liu</li>
<li>memphis ucla eli</li>
<li>muslim cheap eli</li>
<li>muslim peach eli</li>
<li>please chili mum</li>
<li>asleep chili mum</li>
<li>pulse chili emma</li>
<li>memes chili paul</li>
<li>impulse michel a</li>
<li>impulse claim he</li>
<li>impulse claim eh</li>
<li>impulse email ch</li>
<li>impulse alice hm</li>
<li>impulse chile am</li>
<li>impulse chile ma</li>
<li>impulse each mil</li>
<li>impulse male chi</li>
<li>impulse meal chi</li>
</ul>

<p>
<b>Michael Mathew Pemulis</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>michelle mathieu swamp</li>
<li>michael impulse mathew</li>
<li>whimsical emulate hemp</li>
<li>emphatic wilhelm amuse
<i>Search timed out at 45s with 4 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-mario-incandenza" class="outline-2">
<h2 id="mario-incandenza"><span class="section-number-2">6.</span> Mario Incandenza&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="disability">disability</span>&#xa0;<span class="sweetness">sweetness</span>&#xa0;<span class="filmmaking">filmmaking</span></span></h2>
<div class="outline-text-2" id="text-mario-incandenza">
<p>
The middle Incandenza son, born severely physically disabled — undersized, bradykinetic, with a deformed body that requires a head-mounted camera rig to film — and possessed of an unbroken sweetness that makes him the novel's moral center. He is the only person Hal can really talk to, and the only one who loves Avril without complication.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Boo / Boo Radley</b> — His nickname 'Boo' echoes Harper Lee's Boo Radley — another physically marked, gentle, near-mute innocent who functions as the moral touchstone of his novel.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Boo / Boo Radley</b> — His nickname 'Boo' echoes Boo Radley of To Kill a Mockingbird — another physically marked, gentle, near-mute innocent who functions as a moral touchstone in his novel.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Mario Incandenza</b>
</p>
<ul class="org-ul">
<li><b>amazon inner acid</b> — Mario carries the Incandenza inner-amazon acid of love without the corrosion (suggestive).</li>
<li><b>indiana corn maze</b> — His pure-hearted center is a midwestern corn-maze — simple, finite, knowable (suggestive).</li>
<li><b>amazonic dinner a</b> — Mario presides over the family Amazonic dinners with grace (loose).</li>
<li><b>marina canon zed i</b> — Mario as a marina canon — anchored, named-by-trait (loose).</li>
<li><b>amazon nice ndira</b> — His amazon-Indira (matriarch-orbiting) niceness (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Mario Incandenza</b>
</p>
<ul class="org-ul">
<li>amazon inner acid</li>
<li>canadian zero min</li>
<li>arizona named inc</li>
<li>arizona dance min</li>
<li>arizona nice damn</li>
<li>american nazi don</li>
<li>arizona media cnn</li>
<li>arizona aimed cnn</li>
<li>dinner amazon cia</li>
<li>iranian dozen mac</li>
<li>iranian dozen cam</li>
<li>amazon drain nice</li>
<li>arizona median nc</li>
<li>indiana zone marc</li>
<li>admire canon nazi</li>
<li>comedian nazi ran</li>
<li>domain crane nazi</li>
<li>canadian zone rim</li>
<li>amazon cried nina</li>
<li>monica andre nazi</li>
<li>miranda once nazi</li>
<li>dominance nazi ar</li>
<li>dominance nazi ra</li>
<li>miranda nazi cone</li>
<li>arizona maiden nc</li>
<li>amazed ironic ann</li>
<li>marion dance nazi</li>
<li>arizona mind cane</li>
<li>american nazi nod</li>
<li>arizona dice mann</li>
<li>madonna rice nazi</li>
<li>madonna eric nazi</li>
<li>dancer amino nazi</li>
<li>arizona cinema nd</li>
<li>indiana marco zen</li>
<li>indiana macro zen</li>
<li>amanda ironic zen</li>
<li>monica adrian zen</li>
<li>dominance rain az</li>
<li>dominance iran az</li>
<li>comedian nazi rna</li>
<li>cinnamon aired az</li>
<li>romance indian az</li>
<li>cameron indian az</li>
<li>arizona mind acne</li>
<li>dominican area nz</li>
<li>dominican near az</li>
<li>dominican earn az</li>
<li>indiana corn maze</li>
<li>romania dean zinc</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-orin-incandenza" class="outline-2">
<h2 id="orin-incandenza"><span class="section-number-2">7.</span> Orin Incandenza&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="seduction">seduction</span>&#xa0;<span class="punting">punting</span>&#xa0;<span class="narcissism">narcissism</span></span></h2>
<div class="outline-text-2" id="text-orin-incandenza">
<p>
The eldest Incandenza son, a professional football punter for the Phoenix Cardinals who left tennis and the family in revolt against Avril, compulsively seducing young mothers he calls 'Subjects.' Vain, charming, and emotionally hollow, he is eventually captured and tortured by the A.F.R. for information about his father's film.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>O. = 'zero' / 'the One'</b> — DFW explicitly nicknames him 'O.' and 'The One' — the digit-letter ambiguity of zero/one mirrors his hollow narcissism, a zero who insists on being The One.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · initialism · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Initialism]</i> <b>O. = 'zero' / 'the One'</b> — DFW explicitly nicknames him 'O.' and 'The One' — the digit-letter ambiguity of zero/one mirrors his hollow narcissism (a zero who insists on being The One).</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Orin Incandenza</b>
</p>
<ul class="org-ul">
<li><b>indiana corn zen</b> — Orin punts in stadium fields; the corn-zen of Sunday football (loose).</li>
<li><b>ordinance ian nz</b> — He's a one-man ordinance against the Moms (loose).</li>
<li><b>zodiac inner ann</b> — Orin's seductions stalk an inner zodiac of Anns (Subjects) (suggestive).</li>
<li><b>dancer nina zion</b> — The dancing Nina-figures (Subjects) he pursues out of the Zion of family (loose).</li>
<li><b>connie nazi rand</b> — His seductions reduce Subjects to a Connie/Rand objectivist transaction (loose).</li>
</ul>

<p>
<b>Orin James Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>americans dozen ninja</b> — Orin among the silent-ninja Americans of pro football (loose).</li>
<li><b>jamaican dinner zones</b> — His Phoenix Cardinals nightlife of Jamaican-dinner zones (loose).</li>
<li><b>dominican jansen ezra</b> — The Subjects' Dominican/Jansen/Ezra anonymity in his ledger (loose).</li>
<li><b>manners zodiac janine</b> — His mannered zodiac of Janine-style Subjects (loose).</li>
<li><b>mansion crazed janine</b> — The Phoenix Cardinals mansion crazed with a Janine after every game (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Orin Incandenza</b>
</p>
<ul class="org-ul">
<li>indiana zero cnn</li>
<li>iranian dozen nc</li>
<li>cannon ride nazi</li>
<li>cannon nazi reid</li>
<li>indiana corn zen</li>
<li>cannon nazi dire</li>
<li>adrian none zinc</li>
<li>ronnie dana zinc</li>
<li>donna nicer nazi</li>
<li>adrian zinc neon</li>
<li>conrad nine nazi</li>
<li>canned iron nazi</li>
<li>ordinance ian nz</li>
<li>ordinance inn az</li>
<li>arizona nine dnc</li>
<li>iranian zone dnc</li>
<li>cannon erin diaz</li>
<li>inner canon diaz</li>
<li>nordic anne nazi</li>
<li>arizona dine cnn</li>
<li>canned nazi noir</li>
<li>canon diner nazi</li>
<li>adrian connie nz</li>
<li>connie nazi rand</li>
<li>connie nazi darn</li>
<li>ordinance ain nz</li>
<li>dinner ncaa zion</li>
<li>dancer nina zion</li>
<li>canned rain zion</li>
<li>canned iran zion</li>
<li>dozen rican nina</li>
<li>zodiac inner ann</li>
<li>zodiac inner nan</li>
<li>inner conan diaz</li>
<li>diner conan nazi</li>
<li>iranian dozen cn</li>
<li>indian ezra conn</li>
<li>ordnance nazi in</li>
<li>ordnance nazi ni</li>
<li>cannon diaz rein</li>
<li>indian craze non</li>
<li>canine rand zion</li>
<li>canine darn zion</li>
<li>rained nazi conn</li>
<li>rained zinc nano</li>
<li>rained zinc anon</li>
<li>dancer nazi nino</li>
<li>andrea zinc nino</li>
<li>nadine nazi corn</li>
<li>nadine zinc nora</li>
</ul>

<p>
<b>Orin James Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>americans dozen ninja</li>
<li>american dozens ninja</li>
<li>jamaican dinner zones</li>
<li>jamaican dozens inner</li>
<li>jamaican dinners zone</li>
<li>jamaican sinner dozen</li>
<li>ordnance jasmine nazi</li>
<li>mandarin janice zones</li>
<li>dinners amazon janice</li>
<li>dominican jensen zara</li>
<li>mendoza arsenic ninja</li>
<li>jamaican dinners enzo</li>
<li>jordanian cinemas zen</li>
<li>americans ninja zoned</li>
<li>jamaican sinner zoned</li>
<li>ordinances ninja maze</li>
<li>insomnia crazed jenna</li>
<li>ordinances minaj zane</li>
<li>ordinance nazism jane</li>
<li>ordinance nazism jean</li>
<li>janeiro canned nazism</li>
<li>zionism canada jenner</li>
<li>canadians mornin jeez</li>
<li>jordanian eczema sinn</li>
<li>jordanian eczema inns</li>
<li>endocrine nazism jana</li>
<li>dominance ninjas ezra</li>
<li>dominance ninjas reza</li>
<li>ordinance ninjas maze</li>
<li>american ninjas dozen</li>
<li>american ninjas zoned</li>
<li>ordnance ninjas maize</li>
<li>modernize ninjas ncaa</li>
<li>corinne amazed ninjas</li>
<li>jordanian menzies can</li>
<li>jordanian menzies anc</li>
<li>mansion crazed janine</li>
<li>manners zodiac janine</li>
<li>mendoza cairns janine</li>
<li>discern amazon janine</li>
<li>dominance jansen ariz</li>
<li>dominican jansen ezra</li>
<li>dominican jansen reza</li>
<li>arizona minced jansen
<i>Search timed out at 45s with 44 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-rémy-marathe" class="outline-2">
<h2 id="rémy-marathe"><span class="section-number-2">8.</span> Rémy Marathe&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="wheelchair">wheelchair</span>&#xa0;<span class="quadrupleagent">quadrupleagent</span>&#xa0;<span class="loyaltytowife">loyaltytowife</span></span></h2>
<div class="outline-text-2" id="text-rémy-marathe">
<p>
A wheelchair-bound member of Les Assassins des Fauteuils Rollents, a Québécois separatist cell, working as a quadruple agent who betrays the A.F.R. to U.S. operatives in exchange for medical care for his skull-less wife Gertraude. His long mountainside debate with Steeply about freedom, choice, and what one is willing to die for is the novel's philosophical spine.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Rémy ← Latin Remigius, 'oarsman / rower'</b> — The wheelchair-bound Marathe propels himself with his arms — a literal oarsman of his own chair — while his surname 'Marathe' simultaneously evokes the marathon of his all-night philosophical debate with Steeply.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Rémy (French, from Latin Remigius, 'oarsman / rower')</b> — The wheelchair-bound Marathe propels himself with his arms — a literal oarsman of his own chair. The Latin root remex/remigium ('oar') sits inside his given name.</li>
<li><i>[Allusion]</i> <b>Marathon</b> — Marathe's surname evokes the marathon — the endurance of his mountainside debate with Steeply, which spans the entire night, makes the name a quiet joke about a wheelchair-bound man running a philosophical marathon.</li>
<li><i>[Translation]</i> <b>Rémy ← Latin Remigius, 'oarsman / rower'</b> — The wheelchair-bound Marathe propels himself with his arms — a literal oarsman of his own chair — while his surname 'Marathe' simultaneously evokes the marathon of his all-night philosophical debate with Steeply.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Rémy Marathe</b>
</p>
<ul class="org-ul">
<li><b>martha emery</b> — His debate with Steeply has Martha-Emery domestic intimacy (loose).</li>
<li><b>harry team em</b> — Hurry/Harry — quadruple-agent urgency teaming with anyone who'll help his wife (loose).</li>
<li><b>hammer ear ty</b> — His hammered-ear AFR interrogations and his loyalty (loose).</li>
<li><b>rhyme team ar</b> — His rhyming reasoning team of AFR moles, suggestive of his choice rhetoric (loose).</li>
<li><b>may rather em</b> — His paralytic conditional: rather may em (himself) sacrifice than betray his wife (suggestive).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Rémy Marathe</b>
</p>
<ul class="org-ul">
<li>martha meyer</li>
<li>mayhem terra</li>
<li>martha emery</li>
<li>mather mayer</li>
<li>rather may me</li>
<li>hear term may</li>
<li>there army am</li>
<li>three army am</li>
<li>heart army me</li>
<li>earth army me</li>
<li>them army are</li>
<li>team army her</li>
<li>hear army met</li>
<li>there mary am</li>
<li>three mary am</li>
<li>heart mary me</li>
<li>earth mary me</li>
<li>them mary are</li>
<li>team mary her</li>
<li>hear mary met</li>
<li>them rare may</li>
<li>there may arm</li>
<li>three may arm</li>
<li>them year arm</li>
<li>yeah term arm</li>
<li>harry team me</li>
<li>harry meet am</li>
<li>them army era</li>
<li>them mary era</li>
<li>heart year mm</li>
<li>earth year mm</li>
<li>there army ma</li>
<li>there mary ma</li>
<li>three army ma</li>
<li>three mary ma</li>
<li>harry meet ma</li>
<li>harry meat me</li>
<li>army meat her</li>
<li>mary meat her</li>
<li>theme arm ray</li>
<li>rather may em</li>
<li>heart army em</li>
<li>heart mary em</li>
<li>earth army em</li>
<li>earth mary em</li>
<li>harry team em</li>
<li>harry meat em</li>
<li>harry mate me</li>
<li>harry mate em</li>
<li>army mate her</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-joelle-van-dyne" class="outline-2">
<h2 id="joelle-van-dyne"><span class="section-number-2">9.</span> Joelle van Dyne&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="veil">veil</span>&#xa0;<span class="beautydisfigurement">beautydisfigurement</span>&#xa0;<span class="voice">voice</span></span></h2>
<div class="outline-text-2" id="text-joelle-van-dyne">
<p>
A Kentucky-bred radio host on MIT's pirate station as the veiled Madame Psychosis, formerly known among the Incandenzas as the P.G.O.A.T. — Prettiest Girl Of All Time — Orin's ex, James's muse and final film's star. Her veil hides either disfigurement from acid or an unbearable beauty, and she enters Ennet House after a near-fatal freebase overdose.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Madame Psychosis ⊂ metempsychosis</b> — Her radio name is embedded inside 'metempsychosis' (Greek: transmigration of souls) — apt for a veiled figure who moves between identities (Joelle / Madame Psychosis / Lucille Duquette) and whose voice transmigrates across the airwaves.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · substring · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>metempsychosis</b> — Her radio name 'Madame Psychosis' is embedded in 'metempsychosis' (Greek: transmigration of souls) — fitting for a veiled figure who moves between identities (Joelle / Madame Psychosis / Lucille Duquette) and whose voice transmits across the airwaves.</li>
<li><i>[Initialism]</i> <b>P.G.O.A.T. = 'pee-goat' / scapegoat</b> — The acronym 'Prettiest Girl Of All Time' is pronounced as a near-homophone of 'goat'/'scapegoat' — apt for a woman whose extreme beauty makes her the object of disfiguring acid (or unbearable visibility) and who carries the symbolic weight of the men around her.</li>
<li><i>[Allusion]</i> <b>Anthony van Dyck (the painter known for portraiture and beauty)</b> — Her surname echoes the great portraitist of glamorous faces — a wry choice for a character whose face is precisely what cannot be looked at.</li>
<li><i>[Substring]</i> <b>Madame Psychosis ⊂ metempsychosis</b> — Her radio name is embedded inside 'metempsychosis' (Greek: transmigration of souls) — apt for a veiled figure who moves between identities (Joelle / Madame Psychosis / Lucille Duquette) and whose voice transmigrates across the airwaves.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Joelle Dyne</b>
</p>
<ul class="org-ul">
<li><b>joelle deny</b> — Joelle's veil is one long denial of her face (strong).</li>
<li><b>eyed jen lol</b> — Veiled Joelle (Jen) eyed but unseen — lol of a dark joke (loose).</li>
<li><b>joel den lye</b> — Joel/Joelle in the den, with lye (the freebase chemistry) (suggestive).</li>
<li><b>lonely de je</b> — Lonely 'de je' — French diminutive; her Kentucky-French radio persona (loose).</li>
<li><b>yelled on je</b> — Madame Psychosis yelled on the air on the 'je' (I) of MIT pirate radio (loose).</li>
</ul>

<p>
<b>Joelle van Dyne</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>lovely jane den</b> — The lovely Jane (P.G.O.A.T.) in her den under the veil (suggestive).</li>
<li><b>eleven dylan jo</b> — Eleven (her radio hour) and Dylan-folk Jo at the mic (loose).</li>
<li><b>anyone level dj</b> — An 'anyone-level DJ' is exactly Madame Psychosis's egalitarian radio voice (suggestive).</li>
<li><b>level deny joan</b> — Veiled denial at the level of 'Joan' — every name now anonymous (loose).</li>
<li><b>jenny love deal</b> — Her freebase love-deal with Jenny-style chemistry (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Joelle Dyne</b>
</p>
<ul class="org-ul">
<li>enjoyed ell</li>
<li>enjoy led el</li>
<li>enjoy led le</li>
<li>enjoy del el</li>
<li>enjoy del le</li>
<li>ellen joy de</li>
<li>ellen joy ed</li>
<li>deny joel el</li>
<li>deny joel le</li>
<li>need joey ll</li>
<li>joel lend ye</li>
<li>joey lend el</li>
<li>joey lend le</li>
<li>lend lee joy</li>
<li>need yell jo</li>
<li>yell end joe</li>
<li>yell joe den</li>
<li>enjoy dee ll</li>
<li>yell jon dee</li>
<li>ellen dye jo</li>
<li>joey eden ll</li>
<li>yell eden jo</li>
<li>jelly one de</li>
<li>jelly one ed</li>
<li>jelly neo de</li>
<li>jelly neo ed</li>
<li>jelly dee on</li>
<li>jelly dee no</li>
<li>jelly doe en</li>
<li>jelly doe ne</li>
<li>yelled en jo</li>
<li>yelled ne jo</li>
<li>doyle lee nj</li>
<li>joel eyed ln</li>
<li>joel led yen</li>
<li>joel del yen</li>
<li>yell joe ned</li>
<li>enjoy lee dl</li>
<li>joey dell en</li>
<li>joey dell ne</li>
<li>dell eye jon</li>
<li>dell joe yen</li>
<li>lonely de je</li>
<li>lonely ed je</li>
<li>yelled on je</li>
<li>yelled no je</li>
<li>done yell je</li>
<li>node yell je</li>
<li>lonely dj ee</li>
<li>jelly don ee</li>
</ul>

<p>
<b>Joelle van Dyne</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>level enjoy and</li>
<li>lovely need jan</li>
<li>level enjoy dan</li>
<li>level enjoy dna</li>
<li>lovely jane end</li>
<li>lovely jean end</li>
<li>valley need jon</li>
<li>level danny joe</li>
<li>anyone level dj</li>
<li>eleven land joy</li>
<li>eleven lady jon</li>
<li>eleven dylan jo</li>
<li>lloyd even jane</li>
<li>lloyd even jean</li>
<li>lovely jane den</li>
<li>lovely jean den</li>
<li>level deny joan</li>
<li>dylan even joel</li>
<li>leave jenny old</li>
<li>jenny love deal</li>
<li>jenny love lead</li>
<li>jenny love dale</li>
<li>ellen devon jay</li>
<li>devon jane yell</li>
<li>devon jean yell</li>
<li>lovely eden jan</li>
<li>valley eden jon</li>
<li>enjoyed evan ll</li>
<li>donna jelly eve</li>
<li>jelly need nova</li>
<li>jelly done evan</li>
<li>jelly none dave</li>
<li>jelly dean oven</li>
<li>jelly nova eden</li>
<li>jelly node evan</li>
<li>lonely jane dev</li>
<li>lonely jean dev</li>
<li>yelled jane nov</li>
<li>yelled jane von</li>
<li>yelled jean nov</li>
<li>yelled jean von</li>
<li>yelled oven jan</li>
<li>yelled evan jon</li>
<li>enjoy allen dev</li>
<li>laden joel envy</li>
<li>lovely jane ned</li>
<li>lovely jean ned</li>
<li>loved jenny ale</li>
<li>enjoy evan dell</li>
<li>jelly anne dove</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-hal-incandenza" class="outline-2">
<h2 id="hal-incandenza"><span class="section-number-2">10.</span> Hal Incandenza&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="intellectualprecocity">intellectualprecocity</span>&#xa0;<span class="interiority">interiority</span>&#xa0;<span class="cannabis">cannabis</span></span></h2>
<div class="outline-text-2" id="text-hal-incandenza">
<p>
Youngest Incandenza son and ETA's top junior, a verbally precocious 17-year-old whose interior life unravels under the combined pressures of his father's death, his mother's psychological domination, and a secret cannabis dependency. The novel opens with him in total mind-body breakdown — speaking what he hears as articulate sentences but emerges from his mouth as inhuman noise.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>HAL 9000 (2001: A Space Odyssey)</b> — Hal evokes Kubrick's HAL — an artificial intelligence whose mind catastrophically breaks down. The novel opens with Hal's own mind-language interface failing in exactly this way: lucid inside, garbled out.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>HAL 9000 (2001: A Space Odyssey)</b> — Hal evokes Kubrick's HAL — an artificial intelligence whose mind catastrophically breaks down. The novel opens with Hal's own mind-language interface failing in exactly this way: lucid inside, garbled out.</li>
<li><i>[Allusion]</i> <b>Prince Hal (Shakespeare's Henry IV / Henry V)</b> — Hal shares his name with Shakespeare's Prince Hal, the brilliant heir who carouses with bad influences before his expected ascension — the structural template for a precocious prince of ETA living a secret druggie life under his father's shadow.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Hal Incandenza</b>
</p>
<ul class="org-ul">
<li><b>zealand ahi cnn</b> — Hal as a New-Zealand ahi (rare fish) on the CNN of his own collapsing mind (loose).</li>
<li><b>daniel zach ann</b> — Names of any anonymous teen — Hal among them, dispossessed of his own (loose).</li>
<li><b>annie land zach</b> — Same all-American teen-name banality (loose).</li>
<li><b>hanna deal zinc</b> — Hannah's deal of zinc lozenges; Hal's mother-bound illness (loose).</li>
<li><b>china dana lenz</b> — China-Dana-Lenz blur of late-night ETA roommate noise (loose).</li>
</ul>

<p>
<b>Harold James Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>hernandez jamaican sold</b> — His mother's Hernandez-Jamaican lover-network sold him out (loose).</li>
<li><b>zealand jericho sandman</b> — Sandman comes for him from a New-Zealand Jericho of his subconscious (suggestive).</li>
<li><b>jamaican handlers dozen</b> — A dozen Jamaican handlers (Avril's lovers) circle him (loose).</li>
<li><b>alejandro sanchez admin</b> — ETA admin staff (Alejandro/Sanchez stand-ins) administer his collapse (loose).</li>
<li><b>childrens amazed joanna</b> — The children's-amazed-Joanna refrain: Hal stays a frozen child (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Hal Incandenza</b>
</p>
<ul class="org-ul">
<li>zealand inch an</li>
<li>zealand inch na</li>
<li>zealand inc nah</li>
<li>channel nazi ad</li>
<li>channel nazi da</li>
<li>handle nazi can</li>
<li>clean hand nazi</li>
<li>zealand chan in</li>
<li>zealand chin an</li>
<li>zealand chin na</li>
<li>zealand ann chi</li>
<li>lance hand nazi</li>
<li>zealand chan ni</li>
<li>zealand inc han</li>
<li>candle nazi nah</li>
<li>candle nazi han</li>
<li>zealand nina ch</li>
<li>daniel zach ann</li>
<li>denial zach ann</li>
<li>nailed zach ann</li>
<li>lined anna zach</li>
<li>linda anne zach</li>
<li>annie land zach</li>
<li>laden nazi chan</li>
<li>laden nina zach</li>
<li>zealand inn cha</li>
<li>zealand nina hc</li>
<li>handle zinc ana</li>
<li>canned nazi hal</li>
<li>linen dana zach</li>
<li>dance hanna liz</li>
<li>hanna deal zinc</li>
<li>hanna lead zinc</li>
<li>hanna dale zinc</li>
<li>channel diaz an</li>
<li>channel diaz na</li>
<li>zealand cnn hai</li>
<li>china zelda ann</li>
<li>chain zelda ann</li>
<li>hanna zelda inc</li>
<li>zelda inch anna</li>
<li>zelda anna chin</li>
<li>zelda chan nina</li>
<li>zealand cain nh</li>
<li>lined zach nana</li>
<li>diana hazel cnn</li>
<li>zelda inch nana</li>
<li>zelda chin nana</li>
<li>chennai land az</li>
<li>zealand chi nan</li>
</ul>

<p>
<b>Harold James Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>machines zealand jordan</li>
<li>hernandez jamaican sold</li>
<li>hernandez jamaican olds</li>
<li>jamaican handler dozens</li>
<li>alejandro sanchez admin</li>
<li>jamaican handlers dozen</li>
<li>jordanian sanchez medal</li>
<li>jamaican handlers zoned</li>
<li>calendars johnnie mazda</li>
<li>declared johanna nazism</li>
<li>alejandro canadiens mhz</li>
<li>childrens amazed joanna</li>
<li>zealand jericho sandman
<i>Search timed out at 45s with 13 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-randy-lenz" class="outline-2">
<h2 id="randy-lenz"><span class="section-number-2">11.</span> Randy Lenz&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="sadism">sadism</span>&#xa0;<span class="cocaine">cocaine</span>&#xa0;<span class="animalcruelty">animalcruelty</span></span></h2>
<div class="outline-text-2" id="text-randy-lenz">
<p>
A cocaine-addicted Ennet House resident with a pathological need for control, who relieves his anxiety by torturing and killing animals on his nightly walks. His killing of a Nuck's dog triggers the novel's climactic Ennet House siege.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>randy (lustful, aggressive) + Lenz (Büchner's schizophrenic poet)</b> — His given name reads as the slang adjective 'randy' — a sour joke for a man whose aggression is displaced from sex onto animal-killing — while 'Lenz' invokes Büchner's archetype of paranoid disintegration.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>randy (English slang: 'lustful, aggressive')</b> — His given name reads as the common adjective 'randy' — a small joke that fits his pathological aggression and need for control, displaced from sex onto the killing of animals.</li>
<li><i>[Allusion]</i> <b>Lenz (Georg Büchner's 1836 novella about a schizophrenic poet)</b> — Büchner's 'Lenz' is the archetypal portrait of paranoid mental disintegration in German literature — a fitting namesake for DFW's most floridly paranoid, dissociating addict.</li>
<li><i>[Etymology]</i> <b>randy (lustful, aggressive) + Lenz (Büchner's schizophrenic poet)</b> — His given name reads as the slang adjective 'randy' — a sour joke for a man whose aggression is displaced from sex onto animal-killing — while 'Lenz' invokes Büchner's archetype of paranoid disintegration.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Randy Lenz</b>
</p>
<ul class="org-ul">
<li><b>landry zen</b> — Lenz's coke-fuelled false serenity — a 'Landry zen' (loose).</li>
<li><b>lazy nerd n</b> — His lazy-nerd alibi for nightly walks (loose).</li>
<li><b>zelda yr nn</b> — Zelda-Year-NN: Lenz hides among NYC nurses (loose).</li>
<li><b>ryan zen ld</b> — Anonymous Ryan-zen-LD on Boston streets (loose).</li>
<li><b>dn rely zan</b> — DN-rely-zan: paranoid cocaine telegraphing (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Randy Lenz</b>
</p>
<ul class="org-ul">
<li>landry zen</li>
<li>dylan re nz</li>
<li>dylan er nz</li>
<li>randy el nz</li>
<li>randy le nz</li>
<li>land rey nz</li>
<li>ryan led nz</li>
<li>ryan del nz</li>
<li>lane dry nz</li>
<li>rely and nz</li>
<li>rely dan nz</li>
<li>rely dna nz</li>
<li>lean dry nz</li>
<li>lend ray nz</li>
<li>laden nz yr</li>
<li>ready nz ln</li>
<li>early nz nd</li>
<li>delay nz rn</li>
<li>layer nz nd</li>
<li>relay nz nd</li>
<li>lazy end rn</li>
<li>lazy den rn</li>
<li>lazy ned rn</li>
<li>land zen yr</li>
<li>lady zen rn</li>
<li>yard zen ln</li>
<li>lynn red az</li>
<li>lynn der az</li>
<li>ryan zen dl</li>
<li>nerd lay nz</li>
<li>neal dry nz</li>
<li>dry zen lan</li>
<li>lena dry nz</li>
<li>yarn led nz</li>
<li>yarn del nz</li>
<li>yarn zen dl</li>
<li>zelda ny rn</li>
<li>ready nz nl</li>
<li>land rye nz</li>
<li>yard zen nl</li>
<li>lady ren nz</li>
<li>lazy ren nd</li>
<li>daly zen rn</li>
<li>daly ren nz</li>
<li>delay nz nr</li>
<li>zelda ny nr</li>
<li>daryl en nz</li>
<li>daryl ne nz</li>
<li>lady zen nr</li>
<li>yard len nz</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-hugh-steeply" class="outline-2">
<h2 id="hugh-steeply"><span class="section-number-2">12.</span> Hugh Steeply&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="disguise">disguise</span>&#xa0;<span class="crossdressing">crossdressing</span>&#xa0;<span class="surveillance">surveillance</span></span></h2>
<div class="outline-text-2" id="text-hugh-steeply">
<p>
An undercover operative for the Office of Unspecified Services who appears throughout in unconvincing drag as 'Helen Steeply,' a soft-profile journalist, while pursuing the master copy of 'the Entertainment.' His mountainside dialogue with Marathe pits American pleasure-freedom against Québécois conviction.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Hugh Steeply ≈ 'hugely steep'</b> — The name phonetically encodes 'hugely steep' — the towering, badly-balanced cross-dressing operative whose disguise is always on the verge of toppling over its own height.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Suggestive]</i> <b>steeply / steep</b> — Steeply is described as towering and badly disguised in heels; the surname 'Steeply' literalizes his vertical, off-balance physical presence — a man whose disguise is steeply pitched and always about to topple.</li>
<li><i>[Phonetic]</i> <b>Hugh / hue (as in 'cry hue and cry') and 'huge'</b> — 'Hugh Steeply' sounds like 'huge-ly steep' — encoding the character's exaggerated, ungainly height in his very name.</li>
<li><i>[Phonetic]</i> <b>Hugh Steeply ≈ 'hugely steep'</b> — The name phonetically encodes 'hugely steep' — the towering, badly-balanced cross-dressing operative whose disguise is always on the verge of toppling over its own height.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Hugh Steeply</b>
</p>
<ul class="org-ul">
<li><b>hugely steph</b> — His drag persona Steph(anie) as 'hugely Steph' — too big to pass (strong).</li>
<li><b>plug seth hey</b> — Steeply plugging his cover-name like a Seth-hey suburban dad (loose).</li>
<li><b>they push leg</b> — His shaved leg pushed under a too-tight skirt (loose).</li>
<li><b>style hugh ep</b> — Style/Hugh/EP: Hugh's stylish episode of cross-dressing failure (loose).</li>
<li><b>sheep ugly th</b> — An ugly-sheep-disguise of an OUS operative (suggestive).</li>
</ul>

<p>
<b>Helen P. Steeply</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>plenty sheep le</b> — Soft-profile journalist who herds plenty of sheep le-stories (loose).</li>
<li><b>sleepy helen pt</b> — Sleepy 'Helen' part-time disguise (loose).</li>
<li><b>plenty help see</b> — He's there to help see what the AFR can't (loose).</li>
<li><b>ellen step hype</b> — Ellen-style step into journalist hype (loose).</li>
<li><b>phelps lent eye</b> — Phelps-tall figure who lent an eye to surveillance (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Hugh Steeply</b>
</p>
<ul class="org-ul">
<li>hugely steph</li>
<li>help guys the</li>
<li>helps the guy</li>
<li>they push leg</li>
<li>types leg huh</li>
<li>type legs huh</li>
<li>helps yet ugh</li>
<li>sheep ugly th</li>
<li>slept hey ugh</li>
<li>they plug she</li>
<li>these ugly hp</li>
<li>style huge hp</li>
<li>sheet ugly hp</li>
<li>sleep ugh thy</li>
<li>egypt huh les</li>
<li>sleep thy hug</li>
<li>helps yet hug</li>
<li>slept hey hug</li>
<li>hughes yep lt</li>
<li>plug thee shy</li>
<li>thus hype leg</li>
<li>shut hype leg</li>
<li>lets hype ugh</li>
<li>lets hype hug</li>
<li>hughes yep tl</li>
<li>helps hey gut</li>
<li>style hugh ep</li>
<li>types hugh el</li>
<li>types hugh le</li>
<li>slept hugh ye</li>
<li>type hugh les</li>
<li>lets hugh yep</li>
<li>these ugly ph</li>
<li>style huge ph</li>
<li>sheet ugly ph</li>
<li>they plug hes</li>
<li>help seth guy</li>
<li>plug seth hey</li>
<li>hughes yet pl</li>
<li>hughes yet lp</li>
<li>types huh gel</li>
<li>they push gel</li>
<li>thus hype gel</li>
<li>shut hype gel</li>
<li>legs hype hut</li>
<li>help guts hey</li>
<li>sleep hugh ty</li>
<li>helps huge ty</li>
<li>style hugh pe</li>
<li>hustle hey gp</li>
</ul>

<p>
<b>Helen P. Steeply</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>plenty sleep he</li>
<li>plenty help see</li>
<li>plenty sleep eh</li>
<li>plenty sheep el</li>
<li>plenty sheep le</li>
<li>helen slept yep</li>
<li>plenty heels ep</li>
<li>spell teen hype</li>
<li>ellen step hype</li>
<li>ellen sept hype</li>
<li>ellen pets hype</li>
<li>stephen yell ep</li>
<li>plenty heel sep</li>
<li>plenty peel she</li>
<li>plenty peel hes</li>
<li>sleep hype lent</li>
<li>stephen yell pe</li>
<li>plenty heels pe</li>
<li>sleepy helen pt</li>
<li>sleepy help ten</li>
<li>sleepy help net</li>
<li>sleepy pete nhl</li>
<li>ellen hype pest</li>
<li>plenty helps ee</li>
<li>plenty heel eps</li>
<li>plenty heel esp</li>
<li>phelps lent eye</li>
<li>shelley teen pp</li>
<li>shelley pete np</li>
<li>shelley pet pen</li>
<li>shelley ten pep</li>
<li>shelley net pep</li>
<li>style helen pep</li>
<li>ellen steph yep</li>
<li>sleepy helen tp</li>
<li>spent hype elle</li>
<li>shelley pepe nt</li>
<li>shelley pepe tn</li>
<li>yells then pepe</li>
<li>shelley peep nt</li>
<li>shelley peep tn</li>
<li>yells then peep</li>
<li>stephen peel ly</li>
<li>synth elle pepe</li>
<li>synth elle peep</li>
<li>sleepy ethel np</li>
<li>shelley pep ent</li>
<li>sleepy help ent</li>
<li>shelly teen pep</li>
<li>shelly pete pen</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-ortho-stice" class="outline-2">
<h2 id="ortho-stice"><span class="section-number-2">13.</span> Ortho Stice&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="blackclothing">blackclothing</span>&#xa0;<span class="stoicism">stoicism</span>&#xa0;<span class="tennis">tennis</span></span></h2>
<div class="outline-text-2" id="text-ortho-stice">
<p>
A top-ranked ETA player nicknamed 'The Darkness' for dressing entirely in black, a flat-affect Kansan and Hal's closest tennis peer. Late in the novel his bedroom furniture and his own forehead start mysteriously moving and sticking to walls, suggesting a wraith's interference.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>ortho- ('straight') + -stice (Latin sistere, 'to stand')</b> — The name literally means 'standing straight' — perfect for the rigidly upright, black-clad Kansan stoic whose bedroom furniture and forehead inexplicably begin to stand fixed against walls.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>ortho- (Greek orthos, 'straight, upright, correct')</b> — Ortho's flat-affect Kansan stoicism and rigid black uniform make him the ETA player most defined by uprightness. The Greek prefix 'ortho-' (straight) is the foundation of his character.</li>
<li><i>[Etymology]</i> <b>-stice (as in interstice, solstice; from Latin sistere/stare, 'to stand')</b> — The suffix '-stice' carries the Latin 'to stand still' — combined with ortho- ('straight'), the name literally means 'standing straight,' which is also exactly what his bedroom furniture and forehead inexplicably do, sticking to walls.</li>
<li><i>[Etymology]</i> <b>ortho- ('straight') + -stice (Latin sistere, 'to stand')</b> — The name literally means 'standing straight' — perfect for the rigidly upright, black-clad Kansan stoic whose bedroom furniture and forehead inexplicably begin to stand fixed against walls.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Ortho Stice</b>
</p>
<ul class="org-ul">
<li><b>ethics root</b> — His monkish black-clad ethics rooted in stoicism (strong).</li>
<li><b>ostrich toe</b> — Ortho's ostrich-stance with face-against-window-toe-out (loose).</li>
<li><b>erotic shot</b> — His tennis erotic-shot precision (loose).</li>
<li><b>hector otis</b> — Hector/Otis everyman pair — Stice as background figure (loose).</li>
<li><b>riches otto</b> — Stice as Otto, the lone richer-than-life Darkness (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Ortho Stice</b>
</p>
<ul class="org-ul">
<li>ethics root</li>
<li>tooth cries</li>
<li>erotic shot</li>
<li>erotic host</li>
<li>richest too</li>
<li>scooter hit</li>
<li>riches otto</li>
<li>roots ethic</li>
<li>hector otis</li>
<li>ethic torso</li>
<li>cohort site</li>
<li>cohort ties</li>
<li>shooter ict</li>
<li>chores tito</li>
<li>tortoise ch</li>
<li>tortoise hc</li>
<li>hitters coo</li>
<li>ethics toro</li>
<li>shooter tic</li>
<li>sitter choo</li>
<li>theorist co</li>
<li>theorist oc</li>
<li>scooter thi</li>
<li>torches iot</li>
<li>torches ito</li>
<li>riches toto</li>
<li>other stoic</li>
<li>otters choi</li>
<li>ostrich toe</li>
<li>otter sochi</li>
<li>riches toot</li>
<li>thrice soot</li>
<li>thrice soto</li>
<li>cohorts tie</li>
<li>heroic tots</li>
<li>hottie orcs</li>
<li>ethic roost</li>
<li>socio rhett</li>
<li>tesco rohit</li>
<li>other cost i</li>
<li>others it co</li>
<li>other its co</li>
<li>store hit co</li>
<li>short ice to</li>
<li>sort hot ice</li>
<li>rich too set</li>
<li>other sit co</li>
<li>score hit to</li>
<li>score hot it</li>
<li>this core to</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-charles-tavis" class="outline-2">
<h2 id="charles-tavis"><span class="section-number-2">14.</span> Charles Tavis&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="administration">administration</span>&#xa0;<span class="selfeffacement">selfeffacement</span>&#xa0;<span class="anxiety">anxiety</span></span></h2>
<div class="outline-text-2" id="text-charles-tavis">
<p>
ETA's Headmaster and Avril's adoptive half-brother (rumored possibly her biological half-brother and the true father of Mario), a relentlessly self-effacing administrator whose anxiety expresses itself as compulsive over-explanation. He runs the Academy in James's absence with bureaucratic competence and zero charisma.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>C.T. = CT (cat-scan)</b> — Pervasively known by his initials, which double as the medical acronym for a brain scan — apt for a character whose entire interiority is anxious self-examination and compulsive over-explanation.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · initialism · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Initialism]</i> <b>C.T. = 'see-tee' / cat-scan abbreviation</b> — C.T. is pervasively known by his initials, which double as the medical acronym for a brain scan — apt for a character whose interiority is all anxious self-examination and compulsive over-explanation.</li>
<li><i>[Initialism]</i> <b>C.T. = CT (cat-scan)</b> — Pervasively known by his initials, which double as the medical acronym for a brain scan — apt for a character whose entire interiority is anxious self-examination and compulsive over-explanation.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Charles Tavis</b>
</p>
<ul class="org-ul">
<li><b>archives last</b> — Tavis the archivist who lasts — bureaucracy outliving James (suggestive).</li>
<li><b>charles vista</b> — Tavis's bureaucratic vista (Windows-Vista mediocrity) (loose).</li>
<li><b>racist halves</b> — His anxious half-self-effacement — racist halves (loose).</li>
<li><b>vertical sash</b> — Vertical sash of his administrator's robe (loose).</li>
<li><b>rivals cheats</b> — His position rivals/cheats Avril's authority (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Charles Tavis</b>
</p>
<ul class="org-ul">
<li>archives last</li>
<li>archives salt</li>
<li>archive lasts</li>
<li>crashes vital</li>
<li>charles vista</li>
<li>racist halves</li>
<li>rivals cheats</li>
<li>traces lavish</li>
<li>reacts lavish</li>
<li>archive salts</li>
<li>scarlet shiva</li>
<li>archival sets</li>
<li>lavish crates</li>
<li>visceral hats</li>
<li>vertical sash</li>
<li>archival tess</li>
<li>cartels shiva</li>
<li>visceral hast</li>
<li>hearts slavic</li>
<li>haters slavic</li>
<li>lavish caster</li>
<li>lavish caters</li>
<li>slavic hearst</li>
<li>slavic earths</li>
<li>thrives scala</li>
<li>travel cash is</li>
<li>lives cash art</li>
<li>live star cash</li>
<li>last save rich</li>
<li>have list cars</li>
<li>article has vs</li>
<li>silver cash at</li>
<li>silver has act</li>
<li>christ save al</li>
<li>christ save la</li>
<li>silver has cat</li>
<li>charles via st</li>
<li>israel cash tv</li>
<li>star cash evil</li>
<li>live cash arts</li>
<li>cash evil arts</li>
<li>least chair vs</li>
<li>sales chair tv</li>
<li>articles vs ah</li>
<li>silver cast ah</li>
<li>silver acts ah</li>
<li>save rich salt</li>
<li>articles vs ha</li>
<li>silver cast ha</li>
<li>silver acts ha</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-john-wayne" class="outline-2">
<h2 id="john-wayne"><span class="section-number-2">15.</span> John Wayne&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="numberone">numberone</span>&#xa0;<span class="qubcois">qubcois</span>&#xa0;<span class="silence">silence</span></span></h2>
<div class="outline-text-2" id="text-john-wayne">
<p>
ETA's top-ranked player, a stoic Québécois from the asbestos-mining town of Saint-Rémi-d'Esquermes whose surname earns him the nickname 'No Relation.' Quietly involved with Avril, he is later revealed to be an A.F.R. plant inside the Academy.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>John Wayne (the American film star)</b> — The Québécois infiltrator who is ETA's #1 player carries the most archetypically American name imaginable — a covert nationality joke whose nickname 'No Relation' winks at the irony.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>John Wayne (the American film star)</b> — His name is the iconic American cowboy actor's, hence his nickname 'No Relation' — but the irony is doubled: the Québécois infiltrator who is ETA's #1 player carries the most archetypically American name imaginable, a covert nationality joke.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>John Wayne</b>
</p>
<ul class="org-ul">
<li><b>johnny awe</b> — Johnny No-Relation's silent awe at ETA (suggestive).</li>
<li><b>anyhow jen</b> — His off-handed 'anyhow' affect with Jen-girl Avril (loose).</li>
<li><b>jenny whoa</b> — Jenny-whoa effect — quiet astonishment at Wayne's unflappable game (loose).</li>
<li><b>joanne why</b> — AFR plant: a Joanne-why riddle of motive (loose).</li>
<li><b>wayne john</b> — The man himself, but inverted — name as anagram (strong).</li>
</ul>

<p>
<b>John "No Relation" Wayne</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>anywhere joint loo nan</b> — His on-court anywhere-joint precision; loo/nan as Quebecois fragments (loose).</li>
<li><b>entirely anna john woo</b> — Entirely Anna(Avril)/John woo — Avril and Wayne entangled (suggestive).</li>
<li><b>jonathan owner ion lye</b> — Jonathan-owner of ion/lye (asbestos chemistry of his hometown) (loose).</li>
<li><b>jonathan newly ion ore</b> — His asbestos-ore hometown made newly nylon (loose).</li>
<li><b>internal yahoo jen now</b> — An internal Yahoo (asset) named Jen, now an A.F.R. plant (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>John Wayne</b>
</p>
<ul class="org-ul">
<li>wayne john</li>
<li>jenny whoa</li>
<li>johnny awe</li>
<li>jenny woah</li>
<li>anyhow jen</li>
<li>joanne why</li>
<li>joanne hwy</li>
<li>johann yew</li>
<li>john any we</li>
<li>john new ya</li>
<li>when joy an</li>
<li>one why jan</li>
<li>now hey jan</li>
<li>own hey jan</li>
<li>won hey jan</li>
<li>john way en</li>
<li>when jan yo</li>
<li>when joy na</li>
<li>jane who ny</li>
<li>jane how ny</li>
<li>jane why on</li>
<li>jane why no</li>
<li>new joy nah</li>
<li>when jay on</li>
<li>when jay no</li>
<li>jean who ny</li>
<li>jean how ny</li>
<li>jean why on</li>
<li>jean why no</li>
<li>johnny we a</li>
<li>why joe ann</li>
<li>when jon ya</li>
<li>john way ne</li>
<li>when any jo</li>
<li>anne why jo</li>
<li>joan why en</li>
<li>joan why ne</li>
<li>why jan neo</li>
<li>non hey jaw</li>
<li>honey wa nj</li>
<li>honey aw nj</li>
<li>wayne oh nj</li>
<li>wayne ho nj</li>
<li>yeah now nj</li>
<li>yeah own nj</li>
<li>yeah won nj</li>
<li>new joy han</li>
<li>owen hay nj</li>
<li>new jon hay</li>
<li>jenny who a</li>
</ul>

<p>
<b>John "No Relation" Wayne</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>rationale johnny owen</li>
<li>inherently joanna woo</li>
<li>waterloo johnny annie</li>
<li>hotline norway joanne</li>
<li>antoine johann rowley</li>
<li>nonlinear wheaton joy</li>
<li>janeiro wheaton nylon</li>
<li>jonathan looney erwin</li>
<li>johanna winter looney</li>
<li>wharton looney janine
<i>Search timed out at 45s with 10 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-president-johnny-gentle" class="outline-2">
<h2 id="president-johnny-gentle"><span class="section-number-2">16.</span> President Johnny Gentle&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="crooner">crooner</span>&#xa0;<span class="germaphobia">germaphobia</span>&#xa0;<span class="cleanliness">cleanliness</span></span></h2>
<div class="outline-text-2" id="text-president-johnny-gentle">
<p>
Former lounge singer turned U.S. President, founder of the Clean U.S. Party, a germaphobe whose solution to American waste is to gift the toxic northeast to Canada as the Great Concavity. His regime institutes Subsidized Time and triggers the Québécois separatist crisis.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>pretending steel</b> — 'President Gentle' anagrams to 'pretending steel' — the perfect tag for a kitsch crooner-turned-strongman whose germaphobic Clean Party regime is authoritarian theatre dressed up as gentleness.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>Gentle (cleanliness/manners) vs. authoritarian regime</b> — His surname 'Gentle' performs the kitsch of his crooner persona and his germaphobic obsession with everything being clean and pleasant — set against an authoritarian government that gifts toxic land to Canada. The name is the brand.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>President Gentle</b>
</p>
<ul class="org-ul">
<li><b>pretending steel</b> — Gentle's regime is pretending-steel — a kitsch crooner doing strongman (strong).</li>
<li><b>sterling pete den</b> — His sterling-Pete den of crooners-turned-cabinet (loose).</li>
<li><b>entering dept les</b> — His new-Department-style entering dept (loose).</li>
<li><b>stringent peeled</b> — Stringent-peeled, germaphobic president (suggestive).</li>
<li><b>pertinent sledge</b> — His sledge-hammer pertinent solution: gift territory to Canada (loose).</li>
</ul>

<p>
<b>Johnny Gentle Famous Crooner</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>technology reforms juan neon</b> — His tech-reforms by which Juan/neon (anonymous Americans) get Subsidized Time (loose).</li>
<li><b>encourages johnny lemon fort</b> — Encourages Johnny's lemon-fresh-fort cleanliness regime (loose).</li>
<li><b>manchester journey golf noon</b> — Manchester-journey-golf-noon banality of his administration (loose).</li>
<li><b>technology farmers june noon</b> — Tech and farmers under his June-noon Subsidized Time (loose).</li>
<li><b>enforcement johnson gary lou</b> — Enforcement under Johnson/Gary/Lou — a Reagan-cabinet pastiche (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>President Gentle</b>
</p>
<ul class="org-ul">
<li>president gentle</li>
<li>pretending steel</li>
<li>deleting present</li>
<li>pinterest legend</li>
<li>deleting serpent</li>
<li>internet pledges</li>
<li>stringent peeled</li>
<li>pertinent sledge</li>
<li>pretends gentile</li>
<li>gentiles pretend</li>
<li>green title spend</li>
<li>spending tree let</li>
<li>president ten leg</li>
<li>president net leg</li>
<li>sending peter let</li>
<li>single peter tend</li>
<li>letting needs per</li>
<li>letting needs pre</li>
<li>sleeping trend et</li>
<li>ending peter lets</li>
<li>engineer step ltd</li>
<li>listen gender pet</li>
<li>gender silent pet</li>
<li>present legend it</li>
<li>legend enter tips</li>
<li>entering step led</li>
<li>engineers ltd pet</li>
<li>design letter pen</li>
<li>letter signed pen</li>
<li>gender titles pen</li>
<li>ending settle per</li>
<li>ending settle pre</li>
<li>printed lets gene</li>
<li>engineer sept ltd</li>
<li>entering sept led</li>
<li>letting needs rep</li>
<li>engines peter ltd</li>
<li>ending settle rep</li>
<li>president let gen</li>
<li>entered split gen</li>
<li>printed steel gen</li>
<li>printed legs teen</li>
<li>pretend single et</li>
<li>pretend lines get</li>
<li>pretend line gets</li>
<li>pretend list gene</li>
<li>gender split teen</li>
<li>legend priest ten</li>
<li>legend priest net</li>
<li>legend strip teen</li>
</ul>

<p>
<b>Johnny Gentle Famous Crooner</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>encouragement johnny floors</li>
<li>nanotechnology reforms june
<i>Search timed out at 45s with 2 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-tiny-ewell" class="outline-2">
<h2 id="tiny-ewell"><span class="section-number-2">17.</span> Tiny Ewell&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="smallness">smallness</span>&#xa0;<span class="legalprecision">legalprecision</span>&#xa0;<span class="cataloguing">cataloguing</span></span></h2>
<div class="outline-text-2" id="text-tiny-ewell">
<p>
A diminutive disbarred attorney drying out at Ennet House, who copes with sobriety by cataloguing the tattoos of his fellow residents with lawyerly precision. His smallness and fastidiousness mark him among the House's bigger, rougher inmates.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Bob Ewell (To Kill a Mockingbird)</b> — He shares a surname with Harper Lee's loathsome Bob Ewell — a fittingly seedy literary echo for a disbarred attorney drying out in a halfway house.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Suggestive]</i> <b>Ewell / 'ewe-ell' (small, contracting sound)</b> — The surname is short, vowel-tight, and visually small on the page; combined with the nickname 'Tiny' the whole name onomatopoeically contracts, matching his diminutive stature.</li>
<li><i>[Allusion]</i> <b>Bob Ewell (To Kill a Mockingbird)</b> — Ewell shares a surname with Harper Lee's loathsome Bob Ewell — a fittingly seedy literary echo for a disbarred attorney drying out in a halfway house.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Tiny Ewell</b>
</p>
<ul class="org-ul">
<li><b>newly tile</b> — Newly-tiled bathroom precision — lawyerly Ewell catalogues every grout-line (loose).</li>
<li><b>willy teen</b> — A willy-teen smallness; Ewell as overgrown boy-attorney (loose).</li>
<li><b>wiley lent</b> — Wiley-Coyote-lent shrewdness (loose).</li>
<li><b>telly wine</b> — Telly-and-wine alcoholism that landed him in Ennet (loose).</li>
<li><b>teeny will</b> — Teeny-Will: Ewell's diminished name and small frame (suggestive).</li>
</ul>

<p>
<b>Eldred K. Ewell Jr.</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>elder dwell jerk</b> — Disbarred elder dwelling among Ennet's jerks (suggestive).</li>
<li><b>keller welded jr</b> — Helen-Keller-style sense-deprived Jr. welded into AA (loose).</li>
<li><b>derek ewell jr ld</b> — A 'Derek Ewell Jr.' anonymizing alias for Tiny (loose).</li>
<li><b>derek well led jr</b> — His derek-well-led legal precision (loose).</li>
<li><b>elder jewel dk rl</b> — Elder-jewel: his preserved fastidiousness inside Ennet (loose). NOTE: 14-letter form is consonant-heavy and dominated by E (×4) and L (×3); abbreviation-laden combos are unavoidable.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Tiny Ewell</b>
</p>
<ul class="org-ul">
<li>newly tile</li>
<li>willy teen</li>
<li>newly lite</li>
<li>wiley lent</li>
<li>telly wine</li>
<li>well yet in</li>
<li>teeny will</li>
<li>tellin yew</li>
<li>new yet ill</li>
<li>wine yet ll</li>
<li>will yet en</li>
<li>well tie ny</li>
<li>newly let i</li>
<li>newly it el</li>
<li>newly it le</li>
<li>twin eye ll</li>
<li>newly et il</li>
<li>newly et li</li>
<li>will ten ye</li>
<li>will net ye</li>
<li>tell win ye</li>
<li>went ill ye</li>
<li>till new ye</li>
<li>newly il te</li>
<li>newly li te</li>
<li>will yet ne</li>
<li>well tin ye</li>
<li>lily new et</li>
<li>lily new te</li>
<li>lily ten we</li>
<li>lily net we</li>
<li>lily wet en</li>
<li>lily wet ne</li>
<li>well yet ni</li>
<li>newly tel i</li>
<li>went lil ye</li>
<li>new yet lil</li>
<li>till wee ny</li>
<li>tiny wee ll</li>
<li>newly el ti</li>
<li>newly le ti</li>
<li>went yell i</li>
<li>yell new it</li>
<li>yell new ti</li>
<li>yell win et</li>
<li>yell win te</li>
<li>yell ten wi</li>
<li>yell net wi</li>
<li>yell wet in</li>
<li>yell wet ni</li>
</ul>

<p>
<b>Eldred K. Ewell Jr.</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>elder dwell jerk</li>
<li>keller welded jr</li>
<li>keller welded rj</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-poor-tony-krause" class="outline-2">
<h2 id="poor-tony-krause"><span class="section-number-2">18.</span> Poor Tony Krause&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="crossdressing">crossdressing</span>&#xa0;<span class="heroin">heroin</span>&#xa0;<span class="glamour">glamour</span></span></h2>
<div class="outline-text-2" id="text-poor-tony-krause">
<p>
A flamboyant cross-dressing heroin addict and thief who steals a heart in a portable cooler and later suffers a horrific seizure on a Cambridge subway platform after going cold-turkey. His glamour-in-squalor and abjection make him one of the novel's purest portraits of street addiction.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Krause ← German 'kraus' (curly, frilled)</b> — The German root 'kraus' means curly or ruffled — apt for a flamboyant cross-dresser whose entire presentation is texture, frill, and theatrical fabric.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Krause (German: 'curly, frizzy'; also evokes 'Krauss/krausen' textures)</b> — The surname's German root 'kraus' means curly/frilled — apt for a flamboyant cross-dresser whose presentation is all texture and ruffles. (DFW often uses German surnames diagnostically.)</li>
<li><i>[Translation]</i> <b>Krause ← German 'kraus' (curly, frilled)</b> — The German root 'kraus' means curly or ruffled — apt for a flamboyant cross-dresser whose entire presentation is texture, frill, and theatrical fabric.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Poor Krause</b>
</p>
<ul class="org-ul">
<li><b>kapoor ruse</b> — His glamour-ruse of an exotic Kapoor name (loose).</li>
<li><b>rope our ask</b> — His 'rope-our-ask' street panhandling lifestyle (loose).</li>
<li><b>oaks pro rue</b> — Cambridge oaks rue his presence; pro-rue self-pity (loose).</li>
<li><b>pork are sou</b> — Pork-are-sou: literal abjection of street-life food (loose).</li>
<li><b>pour oak res</b> — Pour-oak-residue of street alcohol (loose).</li>
</ul>

<p>
<b>Poor Tony Krause</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>operators yukon</b> — Operators of the Yukon-cold subway platform where his seizure hits (loose).</li>
<li><b>preston your oak</b> — Preston-station-style your-oak-bench seizure scene (loose).</li>
<li><b>korean soup tory</b> — Korean-soup-tory: pure abjection-cum-glamour (loose).</li>
<li><b>nature oops york</b> — Nature-oops-york: cosmic accident on a New-York-bound train (loose).</li>
<li><b>turkey porno sao</b> — Turkey/porno/Sao kitsch-glamour palette of his street wardrobe (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Poor Krause</b>
</p>
<ul class="org-ul">
<li>porous rake</li>
<li>kapoor sure</li>
<li>kapoor user</li>
<li>korea pours</li>
<li>kapoor ruse</li>
<li>europa kors</li>
<li>speak our or</li>
<li>korea pro us</li>
<li>korea ups or</li>
<li>sake our pro</li>
<li>poor ears uk</li>
<li>spare our ok</li>
<li>super oak or</li>
<li>sure pro oak</li>
<li>user pro oak</li>
<li>peak ours or</li>
<li>rape ours ok</li>
<li>ours per oak</li>
<li>ours pre oak</li>
<li>ours rep oak</li>
<li>rare soup ok</li>
<li>rear soup ok</li>
<li>sake pour or</li>
<li>ears pour ok</li>
<li>pour ask ore</li>
<li>park euro so</li>
<li>park euro os</li>
<li>euro ask pro</li>
<li>spoke our ar</li>
<li>rope our ask</li>
<li>korea our sp</li>
<li>euro pork as</li>
<li>euro pork sa</li>
<li>pork our sea</li>
<li>pork usa ore</li>
<li>korea our ps</li>
<li>rose pork au</li>
<li>park our seo</li>
<li>pork sore au</li>
<li>poker ours a</li>
<li>poker our as</li>
<li>poker our sa</li>
<li>poker usa or</li>
<li>purse oak or</li>
<li>korea pro su</li>
<li>poor sake ur</li>
<li>spoke our ra</li>
<li>poker sour a</li>
<li>peak sour or</li>
<li>rape sour ok</li>
</ul>

<p>
<b>Poor Tony Krause</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>operators yukon</li>
<li>person okay tour</li>
<li>taken yours poor</li>
<li>story korea upon</li>
<li>korean yours top</li>
<li>korean sport you</li>
<li>korean your stop</li>
<li>korean your post</li>
<li>korean your spot</li>
<li>person york auto</li>
<li>korean yours pot</li>
<li>resort upon okay</li>
<li>opera tokyo runs</li>
<li>operator sony uk</li>
<li>routes okay porn</li>
<li>troops kenya our</li>
<li>tours kenya poor</li>
<li>korean toys pour</li>
<li>roots kenya pour</li>
<li>troops karen you</li>
<li>roster upon okay</li>
<li>korean ports you</li>
<li>korean your tops</li>
<li>korean roots yup</li>
<li>senator pork you</li>
<li>yours poker nato</li>
<li>troops your kane</li>
<li>korean soup troy</li>
<li>tours prone okay</li>
<li>troops korean yu</li>
<li>korean yours opt</li>
<li>korean soup tory</li>
<li>parks youre onto</li>
<li>tanks youre poor</li>
<li>spark youre onto</li>
<li>spoon kerry auto</li>
<li>treason pork you</li>
<li>korea tours pony</li>
<li>preston your oak</li>
<li>preston okay our</li>
<li>troops kanye our</li>
<li>roots kanye pour</li>
<li>tours kanye poor</li>
<li>korean syrup too</li>
<li>korean your pots</li>
<li>korea syrup onto</li>
<li>operator sunk yo</li>
<li>pearson tokyo ur</li>
<li>pearson york out</li>
<li>return okay oops</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-gene-fackelmann" class="outline-2">
<h2 id="gene-fackelmann"><span class="section-number-2">19.</span> Gene Fackelmann&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="betrayal">betrayal</span>&#xa0;<span class="dilaudid">dilaudid</span>&#xa0;<span class="bookie">bookie</span></span></h2>
<div class="outline-text-2" id="text-gene-fackelmann">
<p>
A grossly tall bookie's runner who, in Gately's pre-sobriety backstory, embezzles from the loanshark Whitey Sorkin and goes on a Dilaudid bender with Gately. He is captured and his eyelids are sewn open while he is forced to watch endless cartridges before being killed.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Fackel ← German 'torch'</b> — Fackelmann is German for 'torch-man' — and his death is precisely that, eyelids sewn open as he is forced to stare at burning images on screen until he dies, a literal torch-bearer of forced visual illumination.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Fackel (German: 'torch')</b> — Fackelmann is German for 'torch-man' — his eyelids are sewn open and he is forced to stare at burning images on screen until he dies, making him a literal torch-bearer of forced visual illumination.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Gene Fackelmann</b>
</p>
<ul class="org-ul">
<li><b>flank mcgee anne</b> — His flanking by McGee/Anne-style enforcers in the bookie scene (loose).</li>
<li><b>menace flank gen</b> — Menace-flank-Gene: the menace flanking his late days (loose).</li>
<li><b>mclean knee fang</b> — McLean-knee-fang: the brutal eyelid-sewing punishment (loose).</li>
<li><b>kennel megan afc</b> — Kennel-Megan-AFC: his betrayal trapped him (loose).</li>
<li><b>manage kneel nfc</b> — Manage-kneel-NFC: forced to kneel before the bookie (loose).</li>
</ul>

<p>
<b>Gene "Fax" Fackelmann</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>affleck megan annex</b> — Affleck/Megan/annex: a Hollywood-banality cast for his last act (loose).</li>
<li><b>annex fame flag neck</b> — Annex-fame-flag-neck: the bookie's pilgrimage of brutality (loose).</li>
<li><b>manage flex neck fan</b> — Manage-flex-neck-fan: he tries to manage the bookie and fails (loose).</li>
<li><b>manage fake flex cnn</b> — Manage-fake-flex-CNN: his bookie's fake-flex evening news (loose).</li>
<li><b>annex meg fleck a fan</b> — Annex-Meg-fleck: the bookie's annex where he bleeds (loose). NOTE: 17-letter form has the rare X — almost no clean phrases exist.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Gene Fackelmann</b>
</p>
<ul class="org-ul">
<li>menace flank gen</li>
<li>menace flank eng</li>
<li>mclean kane feng</li>
<li>flank mcgee anne</li>
<li>fleece mann kang</li>
<li>mclean knee fang</li>
<li>mclean keen fang</li>
<li>enamel neck fang</li>
<li>lankan fence gem</li>
<li>lankan fence meg</li>
<li>mckenna angel fe</li>
<li>mckenna angle fe</li>
<li>mckenna eagle fn</li>
<li>mckenna elena gf</li>
<li>mckenna elena fg</li>
<li>mckenna feel ang</li>
<li>mckenna gene afl</li>
<li>mckenna gene fla</li>
<li>mckenna leaf gen</li>
<li>mckenna leaf eng</li>
<li>mckenna lang fee</li>
<li>mckenna flee ang</li>
<li>mckenna glee fan</li>
<li>mckenna flea gen</li>
<li>mckenna flea eng</li>
<li>mckenna feng ale</li>
<li>mckenna feng lea</li>
<li>mckenna fang lee</li>
<li>mckenna angel ef</li>
<li>mckenna angle ef</li>
<li>mckenna eagle nf</li>
<li>manage kneel nfc</li>
<li>mckenna fang eel</li>
<li>mckenna flag nee</li>
<li>mckenna lange fe</li>
<li>mckenna lange ef</li>
<li>mckenna feel nag</li>
<li>mckenna flee nag</li>
<li>menace feng klan</li>
<li>fence megan klan</li>
<li>manage kennel fc</li>
<li>manage kennel cf</li>
<li>kennel megan afc</li>
<li>kennel megan cfa</li>
<li>kennel came fang</li>
<li>kennel fang mace</li>
<li>fence lanka meng</li>
<li>mckenna egan elf</li>
<li>maclean feng ken</li>
<li>mckenna gene alf</li>
</ul>

<p>
<b>Gene "Fax" Fackelmann</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>affleck megan annex</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-gerhardt-schtitt" class="outline-2">
<h2 id="gerhardt-schtitt"><span class="section-number-2">20.</span> Gerhardt Schtitt&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="germandiscipline">germandiscipline</span>&#xa0;<span class="kantianmetaphysics">kantianmetaphysics</span>&#xa0;<span class="tennisasphilosophy">tennisasphilosophy</span></span></h2>
<div class="outline-text-2" id="text-gerhardt-schtitt">
<p>
ETA's Head Coach and Athletic Director, an old Bavarian disciplinarian shaped by pre-war Kantian schooling who teaches tennis as a metaphysics of self-transcendence through constraint. He sidecars around campus with the Moms's dog and lectures Mario on the geometry of will.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Gerhardt ← Germanic ger ('spear') + hart ('hard/strong')</b> — His given name compounds 'spear' and 'hard' — a martial Germanic name for the old-world Bavarian disciplinarian who teaches tennis as a metaphysics of constraint and will.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>Schtitt / 'shit'</b> — His surname is one phoneme away from the German pronunciation of 'shit' — DFW playing with the Bavarian disciplinarian's harshness as scatological bark, the kind of name a teenage tennis player would mock under his breath.</li>
<li><i>[Etymology]</i> <b>Gerhardt (Germanic ger 'spear' + hart 'hard/strong')</b> — His given name compounds 'spear' and 'hard' — a martial Germanic name for the old-world disciplinarian who teaches tennis as a metaphysics of constraint and will.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Gerhardt Schtitt</b>
</p>
<ul class="org-ul">
<li><b>stretch tight rad</b> — Stretch-tight-rad: Schtitt's Kantian tennis discipline (suggestive).</li>
<li><b>stretch third tag</b> — Stretch-third-tag: third-act discipline tag-line (loose).</li>
<li><b>charged thirst tt</b> — Charged thirst (will-to-power) (loose).</li>
<li><b>thatcher dirt sgt</b> — Thatcher-dirt-Sgt: old-world strongman pedagogy (loose).</li>
<li><b>garrett stitch hd</b> — Garrett-stitch-HD: stitching the Academy together (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Gerhardt Schtitt</b>
</p>
<ul class="org-ul">
<li>stretch third tag</li>
<li>stretch that grid</li>
<li>garrett stitch hd</li>
<li>charged thirst tt</li>
<li>stretch third gta</li>
<li>tighter charts td</li>
<li>tighter charts dt</li>
<li>stretch right dat</li>
<li>stretch right tad</li>
<li>stretch tight rad</li>
<li>thatcher dirt sgt</li>
<li>started right htc</li>
<li>targets third htc</li>
<li>traders tight htc</li>
<li>starred tight htc</li>
<li>target thirds htc</li>
<li>thatcher grit std</li>
<li>charter tight std</li>
<li>garrett hitch std</li>
<li>tighter chart std</li>
<li>tighter darts htc</li>
<li>thatcher dirt gst</li>
<li>chatter rights td</li>
<li>chatter rights dt</li>
<li>chatter thirds gt</li>
<li>chatter right std</li>
<li>chatter third sgt</li>
<li>chatter third gst</li>
<li>garrett stitch dh</li>
<li>thatcher grit tds</li>
<li>charter tight tds</li>
<li>garrett hitch tds</li>
<li>tighter chart tds</li>
<li>chatter right tds</li>
<li>stretch tight dar</li>
<li>tight tract shred</li>
<li>started right thc</li>
<li>targets third thc</li>
<li>traders tight thc</li>
<li>starred tight thc</li>
<li>tighter darts thc</li>
<li>target thirds thc</li>
<li>tighter starch td</li>
<li>tighter starch dt</li>
<li>charter tights td</li>
<li>charter tights dt</li>
<li>chatter tights dr</li>
<li>chatter tights rd</li>
<li>trader tights htc</li>
<li>trader tights thc</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-robert-f" class="outline-2">
<h2 id="robert-f"><span class="section-number-2">21.</span> Robert F.&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="aaelder">aaelder</span>&#xa0;<span class="parable">parable</span>&#xa0;<span class="water">water</span></span></h2>
<div class="outline-text-2" id="text-robert-f">
<p>
A leather-clad biker AA old-timer called 'Bob Death' who tells Gately the parable of the two young fish who don't know what water is. He embodies the AA truth that the program's deepest insights are corny, anonymous, and easy to miss.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>'Bob Death' — most ordinary name + least ordinary noun</b> — The 'Bob Death' epithet inverts AA anonymity into near-mythic moniker — 'Bob' the most ordinary American name, 'Death' the least, capturing the AA paradox that the deepest wisdom comes wrapped in the most banal package.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>'Bob Death' / Bob Dylan's 'Death is Not the End'</b> — The 'Bob Death' epithet inverts AA anonymity (first name + initial) into a near-mythic moniker — 'Bob' is the most ordinary American name and 'Death' the least, capturing the AA paradox that the deepest wisdom comes wrapped in the most banal package.</li>
<li><i>[Allusion]</i> <b>'Bob Death' — most ordinary name + least ordinary noun</b> — The 'Bob Death' epithet inverts AA anonymity into near-mythic moniker — 'Bob' the most ordinary American name, 'Death' the least, capturing the AA paradox that the deepest wisdom comes wrapped in the most banal package.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Robert F.</b>
</p>
<ul class="org-ul">
<li><b>bert for</b> — Bert-for: anonymous AA Bert (suggestive).</li>
<li><b>fret bro</b> — Fret-bro: the fish-parable lesson of stop-fretting (loose).</li>
<li><b>forte br</b> — Forte-br: his AA forte and biker-bro identity (loose).</li>
<li><b>bret fro</b> — Bret-fro: anonymous AA elder (loose).</li>
<li><b>fret rob</b> — Fret-Rob: don't fret, Rob (anonymous AA-er) (loose). NOTE: this 7-letter form has only one vowel (e) plus two o's vs two r's — combos collapse to 2-3 letter fragments.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Robert F.</b>
</p>
<ul class="org-ul">
<li>retro bf</li>
<li>retro fb</li>
<li>bert for</li>
<li>bert fro</li>
<li>bret for</li>
<li>bret fro</li>
<li>fret bro</li>
<li>fret rob</li>
<li>fret orb</li>
<li>forte br</li>
<li>forte rb</li>
<li>fret bor</li>
<li>bro re ft</li>
<li>rob re ft</li>
<li>bro ft er</li>
<li>rob ft er</li>
<li>bet or fr</li>
<li>bro et fr</li>
<li>rob et fr</li>
<li>bro fr te</li>
<li>rob fr te</li>
<li>for be rt</li>
<li>feb or rt</li>
<li>ref rt bo</li>
<li>bro rt fe</li>
<li>rob rt fe</li>
<li>for et br</li>
<li>for te br</li>
<li>ore ft br</li>
<li>toe fr br</li>
<li>ref to br</li>
<li>for re bt</li>
<li>for er bt</li>
<li>ore fr bt</li>
<li>ref or bt</li>
<li>ref br ot</li>
<li>bot re fr</li>
<li>bot er fr</li>
<li>ore rt bf</li>
<li>ore rt fb</li>
<li>for be tr</li>
<li>bro fe tr</li>
<li>feb or tr</li>
<li>rob fe tr</li>
<li>ore bf tr</li>
<li>ore fb tr</li>
<li>ref bo tr</li>
<li>for re tb</li>
<li>for er tb</li>
<li>ore fr tb</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-avril-mondragon-incandenza" class="outline-2">
<h2 id="avril-mondragon-incandenza"><span class="section-number-2">22.</span> Avril Mondragon Incandenza&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="domination">domination</span>&#xa0;<span class="perfectionism">perfectionism</span>&#xa0;<span class="qubcois">qubcois</span></span></h2>
<div class="outline-text-2" id="text-avril-mondragon-incandenza">
<p>
The towering Québécois matriarch known to her sons as the Moms, ETA's Dean of Academic Affairs, a Grammar Network founder and serial seductress whose perfectionism and fragility psychologically dominate her children. Her sexual entanglements — possibly with Wayne, possibly with Tavis, possibly with Orin — radiate suspicion through the novel.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Mondragon ← 'mountain-dragon' / 'world-dragon'</b> — Her maiden name reads as 'mountain-dragon' or 'world-dragon' — fitting for a towering matriarch whose perfectionism and sexual menace dominate the novel's domestic plane.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Avril (French: 'April')</b> — Her name is the French word for April — literalizing her Québécois identity and serving as the maternal/spring figure ('the Moms') around whom her sons orbit.</li>
<li><i>[Translation]</i> <b>Mondragon (Spanish/Romance: 'world-dragon' or mont-dragon, 'dragon mountain')</b> — Her middle/maiden name reads as 'mountain-dragon' or 'world-dragon' — fitting for a towering matriarch whose perfectionism and sexual menace dominate the novel's domestic plane.</li>
<li><i>[Translation]</i> <b>Mondragon ← 'mountain-dragon' / 'world-dragon'</b> — Her maiden name reads as 'mountain-dragon' or 'world-dragon' — fitting for a towering matriarch whose perfectionism and sexual menace dominate the novel's domestic plane.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Avril Incandenza</b>
</p>
<ul class="org-ul">
<li><b>nirvana acid lenz</b> — Avril's nirvana-acid Lenz-side of motherhood: dazzling and corrosive (suggestive).</li>
<li><b>calvin drain zane</b> — Her seductions drain Calvin/Zane-style ETA boys (loose).</li>
<li><b>invalid craze ann</b> — Invalid (Mario)-craze and an Ann-anonymity in her wake (loose).</li>
<li><b>nirvana dance liz</b> — Her nirvana-dance with Liz (Joelle)-figures (loose).</li>
<li><b>carnival nina zed</b> — Carnival of Nina-figures; zed for the alphabet's end of seductions (loose).</li>
</ul>

<p>
<b>Avril Mondragon Tavis Incandenza</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>incandenza mondragon travails iv</b> — Avril's matriarchal travails through Books I-V of the novel (suggestive).</li>
<li><b>incandenza mondragon rivals vita</b> — Her seductions rival her own vita (life-story) (suggestive).</li>
<li><b>incandenza mandarins volta vigor</b> — She rules her mandarin (advisor)-court with voltaic vigor (loose).</li>
<li><b>dominance invading salvor tzar an</b> — Her dominance, invading and tsar-like; salvor as 'salvager' of her sons (loose).</li>
<li><b>incandenza mondragon avril tavis</b> — Direct nominal rearrangement — every part of her name accounted for (strong, but trivial).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Avril Incandenza</b>
</p>
<ul class="org-ul">
<li>driven canal nazi</li>
<li>carnival nazi end</li>
<li>carnival nazi den</li>
<li>calvin andre nazi</li>
<li>canadian liver nz</li>
<li>carnival diane nz</li>
<li>radical vienna nz</li>
<li>cardinal naive nz</li>
<li>carnival nazi ned</li>
<li>cardinal ivan zen</li>
<li>cardinal vain zen</li>
<li>adrian calvin zen</li>
<li>vienna lizard can</li>
<li>lizard vince anna</li>
<li>valencia drain nz</li>
<li>canned rival nazi</li>
<li>canned viral nazi</li>
<li>lizard vince nana</li>
<li>inland crave nazi</li>
<li>variance linda nz</li>
<li>vienna lizard anc</li>
<li>carnival dani zen</li>
<li>lizard vance nina</li>
<li>calendar nazi vin</li>
<li>zealand rican vin</li>
<li>inland carve nazi</li>
<li>alvarez indian nc</li>
<li>alvarez india cnn</li>
<li>dancer alvin nazi</li>
<li>andrea alvin zinc</li>
<li>alvarez indian cn</li>
<li>zealand vicar inn</li>
<li>nirvana dance liz</li>
<li>nirvana lined zac</li>
<li>nirvana zelda inc</li>
<li>nirvana zelda nic</li>
<li>nirvana eliza dnc</li>
<li>nirvana deal zinc</li>
<li>nirvana lead zinc</li>
<li>nirvana dale zinc</li>
<li>invalid craze ann</li>
<li>invalid craze nan</li>
<li>inland craze ivan</li>
<li>inland craze vain</li>
<li>lizard canine van</li>
<li>rained naval zinc</li>
<li>inclined zara van</li>
<li>inland vince zara</li>
<li>zealand vinci ran</li>
<li>zealand vinci rna</li>
</ul>

<p>
<b>Avril Mondragon Tavis Incandenza</b> <i>(full form)</i>
  <i>No anagrams of these letters using common English vocabulary.</i>
</p>

</details>
</div>
</div>
<div id="outline-container-rodney-tine-sr" class="outline-2">
<h2 id="rodney-tine-sr"><span class="section-number-2">23.</span> Rodney Tine Sr.&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="spymaster">spymaster</span>&#xa0;<span class="powerbehindthethrone">powerbehindthethrone</span>&#xa0;<span class="manipulation">manipulation</span></span></h2>
<div class="outline-text-2" id="text-rodney-tine-sr">
<p>
Senior Chief of the Office of Unspecified Services, called 'Rod the God,' the actual operational power behind President Gentle and lover of the Québécois assassin Luria P—. He is the spider at the center of the novel's intelligence web.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>tine (a fork's prong, a sharp point)</b> — A 'tine' is the spike on a fork — apt for the spymaster called 'Rod the God,' the sharp, prong-like instrument behind President Gentle who skewers his targets from the shadows.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · substring · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>tine (a prong of a fork; sharp point)</b> — A 'tine' is the spike on a fork — apt for a spymaster who skewers his targets and operates as the sharp instrument behind President Gentle. 'Rod the God' Tine = the rod with the prong.</li>
<li><i>[Phonetic]</i> <b>Tine / 'tine' as in serpent's tooth</b> — His nickname 'Rod the God' rhymes deliberately and the surname pricks — a small, sharp man who is the actual operational power behind the throne.</li>
<li><i>[Substring]</i> <b>tine (a fork's prong, a sharp point)</b> — A 'tine' is the spike on a fork — apt for the spymaster called 'Rod the God,' the sharp, prong-like instrument behind President Gentle who skewers his targets from the shadows.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Rodney Sr.</b>
</p>
<ul class="org-ul">
<li><b>dryer ons</b> — Dryer-ons: the spymaster who keeps the heat on (loose).</li>
<li><b>sorry den</b> — His sorry-den of OUS operations (loose).</li>
<li><b>rory send</b> — Rory-send: covert send-orders (loose).</li>
<li><b>snyder or</b> — Snyder-or: a Snyder-style covert operator (loose).</li>
<li><b>ends rory</b> — Ends-Rory: how Tine handles compromised assets (loose). NOTE: this 8-letter form is consonant-heavy with only e/o as vowels — most combos resolve to short fragments.</li>
</ul>

<p>
<b>"Rod the God" Tine</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>tonight eroded</b> — Tonight-eroded: his late-night intelligence work erodes him (suggestive).</li>
<li><b>tightened door</b> — Tightened-door: OUS's tightened-door secrecy (suggestive).</li>
<li><b>hidden root get</b> — Hidden-root-get: how he gets to the hidden root of every plot (suggestive).</li>
<li><b>editor then god</b> — Editor-then-god: the man who edits the world from behind Gentle (loose).</li>
<li><b>either dont god</b> — Either-dont-god: theological-style ambivalence of his power (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Rodney Sr.</b>
</p>
<ul class="org-ul">
<li>sorry end</li>
<li>orders ny</li>
<li>sorry den</li>
<li>drones yr</li>
<li>send rory</li>
<li>ends rory</li>
<li>sorry ned</li>
<li>rodney rs</li>
<li>rodney sr</li>
<li>snyder or</li>
<li>snyder ro</li>
<li>dryer son</li>
<li>dryer ons</li>
<li>drone yrs</li>
<li>dryer nos</li>
<li>ryder son</li>
<li>ryder ons</li>
<li>ryder nos</li>
<li>rosen dry</li>
<li>nerds roy</li>
<li>synod err</li>
<li>nerd rosy</li>
<li>dyson err</li>
<li>norse dry</li>
<li>derry son</li>
<li>derry ons</li>
<li>derry nos</li>
<li>nerdy ros</li>
<li>dorsey rn</li>
<li>dorsey nr</li>
<li>order syn</li>
<li>yes nor dr</li>
<li>son dry re</li>
<li>rose dr ny</li>
<li>nor dry se</li>
<li>son dry er</li>
<li>yes ron dr</li>
<li>dry ron se</li>
<li>sony dr re</li>
<li>sony dr er</li>
<li>deny or rs</li>
<li>one dry rs</li>
<li>end roy rs</li>
<li>rose ny rd</li>
<li>sony re rd</li>
<li>sony er rd</li>
<li>yes nor rd</li>
<li>yes ron rd</li>
<li>deny or sr</li>
<li>one dry sr</li>
</ul>

<p>
<b>"Rod the God" Tine</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>tonight eroded</li>
<li>tightened door</li>
<li>tightened odor</li>
<li>together did on</li>
<li>together did no</li>
<li>tried good then</li>
<li>right ended too</li>
<li>either dont god</li>
<li>either dont dog</li>
<li>third onto edge</li>
<li>tired good then</li>
<li>their noted god</li>
<li>their noted dog</li>
<li>editor then god</li>
<li>editor then dog</li>
<li>together don id</li>
<li>their good tend</li>
<li>eight door tend</li>
<li>tight need door</li>
<li>gotten hero did</li>
<li>together odd in</li>
<li>neither got odd</li>
<li>hidden root get</li>
<li>gotten hired do</li>
<li>noted hired got</li>
<li>gotten hire odd</li>
<li>gordon teeth id</li>
<li>gordon diet the</li>
<li>gordon tied the</li>
<li>trend diego hot</li>
<li>trend diego tho</li>
<li>detroit hong de</li>
<li>detroit hong ed</li>
<li>together don di</li>
<li>gordon teeth di</li>
<li>third good teen</li>
<li>edited north go</li>
<li>gordon edit the</li>
<li>detroit gone hd</li>
<li>ignored hot ted</li>
<li>ignored tho ted</li>
<li>editor hong ted</li>
<li>other doing ted</li>
<li>north diego ted</li>
<li>other noted dig</li>
<li>tiger tend hood</li>
<li>gotten hide rod</li>
<li>edited horn got</li>
<li>doing teeth rod</li>
<li>north eddie got</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-jim-troeltsch" class="outline-2">
<h2 id="jim-troeltsch"><span class="section-number-2">24.</span> Jim Troeltsch&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="broadcastercosplay">broadcastercosplay</span>&#xa0;<span class="mediocrity">mediocrity</span>&#xa0;<span class="narration">narration</span></span></h2>
<div class="outline-text-2" id="text-jim-troeltsch">
<p>
An ETA student of middling tennis ability who fancies himself a future sports broadcaster and constantly narrates matches in announcer-voice. He spends much of the novel sick in bed, where Pemulis raids his stash.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Ernst Troeltsch (theologian/sociologist who classified religious traditions)</b> — He shares a surname with the early-20th-century theologian known for systematizing and classifying — a wry namesake for a student who can only experience tennis through narrating, classifying, and broadcasting it.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Ernst Troeltsch (German theologian/sociologist of religion)</b> — Troeltsch shares a surname with the early-20th-century theologian Ernst Troeltsch, known for systematizing and classifying religious traditions — a wry namesake for a student who can only experience tennis through narrating, classifying, and broadcasting it.</li>
<li><i>[Allusion]</i> <b>Ernst Troeltsch (theologian/sociologist who classified religious traditions)</b> — He shares a surname with the early-20th-century theologian known for systematizing and classifying — a wry namesake for a student who can only experience tennis through narrating, classifying, and broadcasting it.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Jim Troeltsch</b>
</p>
<ul class="org-ul">
<li><b>clothes jim rt</b> — Clothes-Jim-RT: the broadcast-cosplay (suggestive).</li>
<li><b>christ melt jo</b> — Christ-melt-Jo: feverish bedridden delirium (loose).</li>
<li><b>ctrl josh time</b> — Ctrl-Josh-time: he ctrl-narrates each match in real-time (loose).</li>
<li><b>strict joel hm</b> — Strict-Joel-hm: his ESPN-style strict-broadcast affect (loose).</li>
<li><b>torch lets jim</b> — Torch-lets-Jim: Pemulis torches Jim's stash while he sleeps (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Jim Troeltsch</b>
</p>
<ul class="org-ul">
<li>clothes tim jr</li>
<li>stretch jim lo</li>
<li>clothes jim rt</li>
<li>times cloth jr</li>
<li>items cloth jr</li>
<li>cloth rest jim</li>
<li>stretch jim ol</li>
<li>christ joel mt</li>
<li>christ melt jo</li>
<li>stole mitch jr</li>
<li>terms cloth ji</li>
<li>metric josh lt</li>
<li>metric josh tl</li>
<li>cloth jets rim</li>
<li>strict joel hm</li>
<li>clothes mit jr</li>
<li>stretch mil jo</li>
<li>torch lets jim</li>
<li>torch jets mil</li>
<li>torch slim jet</li>
<li>clothes jim tr</li>
<li>litter josh cm</li>
<li>litter josh mc</li>
<li>sector html ji</li>
<li>strict helm jo</li>
<li>escort html ji</li>
<li>crest holt jim</li>
<li>jets rico html</li>
<li>stitch joel mr</li>
<li>stitch joel rm</li>
<li>stitch mole jr</li>
<li>thirst joel cm</li>
<li>thirst joel mc</li>
<li>christ joel tm</li>
<li>cloth jets mri</li>
<li>chrome tilt js</li>
<li>metric holt js</li>
<li>erotic html js</li>
<li>merit cloth js</li>
<li>cloth timer js</li>
<li>torch lest jim</li>
<li>chemist lot jr</li>
<li>mothers lit cj</li>
<li>mothers til cj</li>
<li>mother list cj</li>
<li>mother slit cj</li>
<li>hotels trim cj</li>
<li>hitler most cj</li>
<li>tories html cj</li>
<li>merits holt cj</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-aubrey-delint" class="outline-2">
<h2 id="aubrey-delint"><span class="section-number-2">25.</span> Aubrey deLint&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="enforcer">enforcer</span>&#xa0;<span class="humorlessness">humorlessness</span>&#xa0;<span class="observation">observation</span></span></h2>
<div class="outline-text-2" id="text-aubrey-delint">
<p>
An ETA Prorector and assistant coach, a humorless functionary who serves as Schtitt's enforcer and watches the students with bureaucratic suspicion. He is mostly defined by his dour, observational presence on the sidelines.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>de Lint ← Dutch/Flemish for 'of the ribbon/strip/sideline'</b> — 'Lint' in Dutch means ribbon, strip, or band — and deLint is the man permanently positioned along the sideline of the courts, Schtitt's grey enforcer at the edge of the play.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>lint (small fibers, accumulated debris)</b> — The surname embeds 'lint' — a humble, accumulating, watchful nothing. Apt for a dour functionary who serves as Schtitt's grey enforcer on the sidelines.</li>
<li><i>[Translation]</i> <b>de Lint (Dutch/Flemish: 'of the ribbon/strip/sideline')</b> — 'Lint' in Dutch means ribbon, strip, or band — fittingly, deLint is the man permanently positioned along the sideline of the courts.</li>
<li><i>[Translation]</i> <b>de Lint ← Dutch/Flemish for 'of the ribbon/strip/sideline'</b> — 'Lint' in Dutch means ribbon, strip, or band — and deLint is the man permanently positioned along the sideline of the courts, Schtitt's grey enforcer at the edge of the play.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Aubrey deLint</b>
</p>
<ul class="org-ul">
<li><b>united barely</b> — Barely-united with the players he watches; alone in his enforcer role (suggestive).</li>
<li><b>bradley unite</b> — Bradley-unite: anonymous everyman-coach (loose).</li>
<li><b>turned bailey</b> — Turned-Bailey: a turned-bailey-style enforcer (loose).</li>
<li><b>buried neatly</b> — Buried-neatly: his subordinate's life buried under Schtitt's shadow (suggestive).</li>
<li><b>audible entry</b> — Audible-entry: he is the audible entry recording every infraction (suggestive).</li>
</ul>

<p>
<b>Aubrey F. deLint</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>inflated buyer</b> — Inflated-buyer: ETA enforcer who buys into Schtitt's program too much (loose).</li>
<li><b>britney feudal</b> — Britney-feudal: his feudal subordination cast in pop terms (loose).</li>
<li><b>defiantly uber</b> — Defiantly-uber: defiantly above-it-all enforcer (loose).</li>
<li><b>refuted libyan</b> — Refuted-Libyan: a deLint refutation of any rebellion (loose).</li>
<li><b>enabled fruity</b> — Enabled-fruity: enables Schtitt's fruity Kantian metaphors (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Aubrey deLint</b>
</p>
<ul class="org-ul">
<li>united barely</li>
<li>turned bailey</li>
<li>bradley unite</li>
<li>tribunal eyed</li>
<li>tribune delay</li>
<li>turbine delay</li>
<li>united barley</li>
<li>buried neatly</li>
<li>audible entry</li>
<li>reunite badly</li>
<li>liberated yun</li>
<li>adultery bein</li>
<li>liberty duane</li>
<li>tribune daley</li>
<li>turbine daley</li>
<li>nearby dilute</li>
<li>bayern dilute</li>
<li>barney dilute</li>
<li>liberated nyu</li>
<li>united real by</li>
<li>adultery bien</li>
<li>interlude bay</li>
<li>unitary bleed</li>
<li>uterine badly</li>
<li>britney laude</li>
<li>until ready be</li>
<li>ready line but</li>
<li>tried blue any</li>
<li>trade line buy</li>
<li>related buy in</li>
<li>until year bed</li>
<li>built year end</li>
<li>build year ten</li>
<li>build near yet</li>
<li>nearly but die</li>
<li>nearly bit due</li>
<li>leader unit by</li>
<li>early unit bed</li>
<li>trial need buy</li>
<li>learned buy it</li>
<li>build tree any</li>
<li>build year net</li>
<li>beauty line dr</li>
<li>truly been aid</li>
<li>turned lie bay</li>
<li>brain duty lee</li>
<li>tired blue any</li>
<li>line bear duty</li>
<li>until beer day</li>
<li>lady unit beer</li>
</ul>

<p>
<b>Aubrey F. deLint</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>inflated buyer</li>
<li>britney feudal</li>
<li>enabled fruity</li>
<li>refuted libyan</li>
<li>defiantly uber</li>
<li>friend late buy</li>
<li>nature field by</li>
<li>friday blue ten</li>
<li>federal unit by</li>
<li>reality fun bed</li>
<li>learned buy fit</li>
<li>reality fund be</li>
<li>friday blue net</li>
<li>turned life bay</li>
<li>turned file bay</li>
<li>united bear fly</li>
<li>brain feel duty</li>
<li>final duty beer</li>
<li>italy fund beer</li>
<li>field urban yet</li>
<li>built feed ryan</li>
<li>build feet ryan</li>
<li>trained fuel by</li>
<li>failed rent buy</li>
<li>failure tend by</li>
<li>fruit been lady</li>
<li>relief duty ban</li>
<li>beauty ride nfl</li>
<li>failed burn yet</li>
<li>daily feet burn</li>
<li>italy feed burn</li>
<li>nearly debut if</li>
<li>brief duty lane</li>
<li>debut life ryan</li>
<li>debut file ryan</li>
<li>earned lift buy</li>
<li>nearby lift due</li>
<li>fairly debut en</li>
<li>fairly need but</li>
<li>bread fuel tiny</li>
<li>nature filed by</li>
<li>barely unit fed</li>
<li>barely fund tie</li>
<li>barely diet fun</li>
<li>urban filed yet</li>
<li>barely tied fun</li>
<li>brian feel duty</li>
<li>belief turn day</li>
<li>belief duty ran</li>
<li>badly free unit</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-jim-struck" class="outline-2">
<h2 id="jim-struck"><span class="section-number-2">26.</span> Jim Struck&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="plagiarism">plagiarism</span>&#xa0;<span class="laziness">laziness</span>&#xa0;<span class="mediocrity">mediocrity</span></span></h2>
<div class="outline-text-2" id="text-jim-struck">
<p>
An academically lazy ETA student best known for plagiarizing his term paper on Québécois separatism wholesale from a scholarly journal, badly. He embodies the Academy's intellectual underclass.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>struck (past tense of 'strike'; 'struck through', 'struck out')</b> — His surname is the past tense of 'strike' — apt for a tennis player who literally strikes the ball, and for a plagiarist whose stolen prose deserves to be struck through and crossed off.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>struck (as in 'struck out' / 'struck through' / 'struck down')</b> — His surname is the past tense of 'strike' — apt for a tennis player who literally strikes the ball, and for a plagiarist who deserves to be struck through and crossed off.</li>
<li><i>[Entendre]</i> <b>struck (past tense of 'strike'; 'struck through', 'struck out')</b> — His surname is the past tense of 'strike' — apt for a tennis player who literally strikes the ball, and for a plagiarist whose stolen prose deserves to be struck through and crossed off.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Jim Struck</b>
</p>
<ul class="org-ul">
<li><b>trucks jim</b> — Trucks-Jim: he plagiarizes by the truckload (loose).</li>
<li><b>struck jim</b> — His name as identity — already an anagram of itself (strong).</li>
<li><b>stuck im jr</b> — Stuck-im-Jr: stuck Jr. plagiarist (loose).</li>
<li><b>crm jut ski</b> — CRM-jut-ski: his middle-American leisure-class glide (loose).</li>
<li><b>jut kim src</b> — Jut-Kim-src: jutting Kim/source-stealing (loose). NOTE: J/M/K/S/T/R/U/I/C is vowel-poor; most fits are abbreviation-heavy.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Jim Struck</b>
</p>
<ul class="org-ul">
<li>struck jim</li>
<li>trucks jim</li>
<li>stuck im jr</li>
<li>cuts kim jr</li>
<li>suck tim jr</li>
<li>stuck jr mi</li>
<li>stick jr um</li>
<li>suck jim rt</li>
<li>just kim cr</li>
<li>stuck mr ji</li>
<li>truck ms ji</li>
<li>kurt jim sc</li>
<li>kurt jim cs</li>
<li>tick sum jr</li>
<li>kits cum jr</li>
<li>stuck ji rm</li>
<li>suck mit jr</li>
<li>suck jim tr</li>
<li>truck ji sm</li>
<li>scum kit jr</li>
<li>crust km ji</li>
<li>stick jr mu</li>
<li>turks cm ji</li>
<li>turks mc ji</li>
<li>just kim rc</li>
<li>crust ji mk</li>
<li>stick mr ju</li>
<li>stick rm ju</li>
<li>trick ms ju</li>
<li>trick sm ju</li>
<li>skirt cm ju</li>
<li>skirt mc ju</li>
<li>tick mrs ju</li>
<li>just rim kc</li>
<li>just mri kc</li>
<li>rust jim kc</li>
<li>tuck mrs ji</li>
<li>tuck jim rs</li>
<li>tuck jim sr</li>
<li>tuck sim jr</li>
<li>tuck mis jr</li>
<li>truck im js</li>
<li>truck mi js</li>
<li>trick um js</li>
<li>trick mu js</li>
<li>kurt mic js</li>
<li>tick rum js</li>
<li>tuck rim js</li>
<li>tuck mri js</li>
<li>music rt jk</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-ted-schacht" class="outline-2">
<h2 id="ted-schacht"><span class="section-number-2">27.</span> Ted Schacht&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="dentistry">dentistry</span>&#xa0;<span class="resignation">resignation</span>&#xa0;<span class="illness">illness</span></span></h2>
<div class="outline-text-2" id="text-ted-schacht">
<p>
An ETA tennis player who has largely given up on a pro career and plans to become a dentist, suffering quietly from Crohn's disease and a wrecked knee. His ordinariness and acceptance set him apart from his more driven peers.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Schacht ← German 'shaft / mineshaft / pit'</b> — Schacht is German for 'shaft' or 'pit' — apt for a player resigned to descending into dentistry (drilling shafts in teeth) and quietly suffering Crohn's, a disease of the gut's interior pit.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Schacht (German: 'shaft, mineshaft, pit')</b> — Schacht is German for 'shaft' or 'pit' — an apt surname for a player resigned to descending into dentistry (drilling shafts in teeth) and quietly suffering from Crohn's, a disease of the gut's interior pit.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Ted Schacht</b>
</p>
<ul class="org-ul">
<li><b>hatched cst</b> — Hatched-cst: hatched as a future dentist (loose).</li>
<li><b>chatted sch</b> — Chatted-sch: he chats up the school dentist's office (loose).</li>
<li><b>catch the sd</b> — Catch-the-SD: catching his calmness amid ETA chaos (loose).</li>
<li><b>chest hat dc</b> — Chest-hat-DC: chest-and-hat anonymity (loose).</li>
<li><b>cast tech hd</b> — Cast-tech-HD: cast as the tech-of-teeth (dental tech) (loose). NOTE: this 10-letter form is consonant-heavy (only one E and one A) — abbreviations dominate.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Ted Schacht</b>
</p>
<ul class="org-ul">
<li>hatchet cds</li>
<li>chatted sch</li>
<li>hatched cst</li>
<li>hatched cts</li>
<li>hatches cdt</li>
<li>chest hat dc</li>
<li>chest hat cd</li>
<li>catch set hd</li>
<li>chest act hd</li>
<li>chest cat hd</li>
<li>cast tech hd</li>
<li>tech acts hd</li>
<li>tech cats hd</li>
<li>catch des th</li>
<li>deaths th cc</li>
<li>that shed cc</li>
<li>catch est hd</li>
<li>chest had ct</li>
<li>chat shed ct</li>
<li>tech hats dc</li>
<li>tech hats cd</li>
<li>tech dash ct</li>
<li>deaths ct ch</li>
<li>thats dec ch</li>
<li>test chad ch</li>
<li>tech chad st</li>
<li>chat seth dc</li>
<li>chat seth cd</li>
<li>chad seth ct</li>
<li>catch the sd</li>
<li>catch she td</li>
<li>catch hes td</li>
<li>cash tech td</li>
<li>tech chat sd</li>
<li>catch ted sh</li>
<li>hatch set dc</li>
<li>hatch set cd</li>
<li>hatch etc sd</li>
<li>hatch sec td</li>
<li>hatch dec st</li>
<li>hatch ted sc</li>
<li>hatch ted cs</li>
<li>hatch des ct</li>
<li>hatch est dc</li>
<li>hatch est cd</li>
<li>catch the ds</li>
<li>catch eds th</li>
<li>hatch etc ds</li>
<li>hatch eds ct</li>
<li>tech chat ds</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-rodney-tine-jr" class="outline-2">
<h2 id="rodney-tine-jr"><span class="section-number-2">28.</span> Rodney Tine Jr.&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="son">son</span>&#xa0;<span class="minor">minor</span>&#xa0;<span class="intelligenceadjacent">intelligenceadjacent</span></span></h2>
<div class="outline-text-2" id="text-rodney-tine-jr">
<p>
The estranged son of the U.S. spymaster Rodney Tine Sr., a minor presence in the novel's intelligence subplot. His existence largely registers as a complication in his father's life rather than as a developed character.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>tine (the lesser prong)</b> — He inherits his father's fork-prong surname but, as a minor estranged figure, registers only as the lesser tine — a small spike off the main rod.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · substring · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>tine (prong)</b> — Inherits his father's fork-prong surname, but as a minor, estranged figure he is the lesser tine — a spike off the main rod.</li>
<li><i>[Substring]</i> <b>tine (the lesser prong)</b> — He inherits his father's fork-prong surname but, as a minor estranged figure, registers only as the lesser tine — a small spike off the main rod.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Rodney Jr.</b>
</p>
<ul class="org-ul">
<li><b>dryer jon</b> — Dryer-Jon: dried-up Jr. version of his father (loose).</li>
<li><b>jerry don</b> — Jerry-Don: anonymous Jr.-pair (loose).</li>
<li><b>jerry nod</b> — Jerry-nod: Jr.'s passive nod to his father's plots (loose).</li>
<li><b>rodney rj</b> — Rodney-RJ: his initials embed in his father's name (suggestive).</li>
<li><b>end roy jr</b> — End-Roy-Jr: the end of the Roy/Tine dynasty in him (loose).</li>
</ul>

<p>
<b>Rodney Tine Jr.</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>retired jonny</b> — Retired-Jonny: he retires from the family business (suggestive).</li>
<li><b>trendy rejoin</b> — Trendy-rejoin: he tries to trendily rejoin his father's circle (loose).</li>
<li><b>entire dry jon</b> — Entire-dry-Jon: an entire dry Jon — colorless heir (loose).</li>
<li><b>jenny tier rod</b> — Jenny-tier-rod: pop-tier echo of Rod Sr. (loose).</li>
<li><b>retired ny jon</b> — Retired-NY-Jon: retires to NY estrangement (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Rodney Jr.</b>
</p>
<ul class="org-ul">
<li>jerry don</li>
<li>jerry nod</li>
<li>rodney jr</li>
<li>dryer jon</li>
<li>ryder jon</li>
<li>rodney rj</li>
<li>derry jon</li>
<li>one dry jr</li>
<li>deny or jr</li>
<li>dry jon re</li>
<li>end roy jr</li>
<li>dry jon er</li>
<li>roy den jr</li>
<li>dry neo jr</li>
<li>red roy nj</li>
<li>dry ore nj</li>
<li>roy der nj</li>
<li>rode jr ny</li>
<li>nor dye jr</li>
<li>ron dye jr</li>
<li>done jr yr</li>
<li>rode nj yr</li>
<li>node jr yr</li>
<li>red jon yr</li>
<li>nor rey dj</li>
<li>don rey jr</li>
<li>jon der yr</li>
<li>jon rey dr</li>
<li>jon rey rd</li>
<li>ron rey dj</li>
<li>rod rey nj</li>
<li>rey nod jr</li>
<li>rod yen jr</li>
<li>rory de nj</li>
<li>rory ed nj</li>
<li>rory en dj</li>
<li>rory dj ne</li>
<li>joey dr rn</li>
<li>joey rd rn</li>
<li>red joy rn</li>
<li>joe dry rn</li>
<li>joy der rn</li>
<li>roy ned jr</li>
<li>nerd jr yo</li>
<li>nerd jo yr</li>
<li>rory nd je</li>
<li>nor dry je</li>
<li>dry ron je</li>
<li>deny jr ro</li>
<li>dry jen or</li>
</ul>

<p>
<b>Rodney Tine Jr.</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>retired jonny</li>
<li>trendy rejoin</li>
<li>dinner try joe</li>
<li>enter join dry</li>
<li>entry join red</li>
<li>enjoy rent rid</li>
<li>retired jon ny</li>
<li>entire dry jon</li>
<li>entry ride jon</li>
<li>noted jerry in</li>
<li>jerry into end</li>
<li>jerry diet non</li>
<li>jerry tied non</li>
<li>jerry edit non</li>
<li>dinner jet roy</li>
<li>terry join end</li>
<li>enjoy inter dr</li>
<li>entry join der</li>
<li>jerry tend ion</li>
<li>oriented jr ny</li>
<li>enjoy inter rd</li>
<li>jerry nine dot</li>
<li>jerry tide non</li>
<li>jerry done tin</li>
<li>jerry into den</li>
<li>terry join den</li>
<li>entry reid jon</li>
<li>tender roy jin</li>
<li>terry done jin</li>
<li>noted jerry ni</li>
<li>dinner joey rt</li>
<li>entry rode jin</li>
<li>order jenny it</li>
<li>tried jenny or</li>
<li>tired jenny or</li>
<li>entry drone ji</li>
<li>rider jenny to</li>
<li>jenny dirt ore</li>
<li>jenny tier rod</li>
<li>jenny riot red</li>
<li>jenny riot der</li>
<li>jenny tire rod</li>
<li>jenny trio red</li>
<li>jenny trio der</li>
<li>rented roy jin</li>
<li>jerry node tin</li>
<li>terry node jin</li>
<li>enter irony dj</li>
<li>order jenny ti</li>
<li>rider jenny ot</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-tall-paul-shaw" class="outline-2">
<h2 id="tall-paul-shaw"><span class="section-number-2">29.</span> Tall Paul Shaw&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="height">height</span>&#xa0;<span class="peripheral">peripheral</span>&#xa0;<span class="etastudent">etastudent</span></span></h2>
<div class="outline-text-2" id="text-tall-paul-shaw">
<p>
An ETA student notable mainly for his height, a peripheral figure in the Academy's social ecosystem. He appears in passing among the upperclassmen.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Tall Paul Shaw — sing-song rhyme</b> — The whole name is constructed as a nursery-rhyme tag, reducing the character to a peripheral sing-song epithet — appropriate for a figure defined entirely by a single physical trait.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>Tall Paul Shaw / rhyming epithet</b> — The name is constructed as a sing-song rhyme ('Tall Paul Shaw'), reducing the character to a peripheral nursery-rhyme tag — appropriate for a figure defined entirely by a single physical trait.</li>
<li><i>[Phonetic]</i> <b>Tall Paul Shaw — sing-song rhyme</b> — The whole name is constructed as a nursery-rhyme tag, reducing the character to a peripheral sing-song epithet — appropriate for a figure defined entirely by a single physical trait.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Tall Shaw</b>
</p>
<ul class="org-ul">
<li><b>walls hat</b> — Walls-hat: he stands tall against ETA walls (loose).</li>
<li><b>tall wash</b> — Tall-wash: a tall-and-wash anonymity (loose).</li>
<li><b>hats wall</b> — Hats-wall: hats hang on the wall at Tall-Paul height (loose).</li>
<li><b>shall twa</b> — Shall-TWA: he shall fly TWA (tall passenger) (loose).</li>
<li><b>whats all</b> — Whats-all: tall and that's all (named-by-trait) (suggestive).</li>
</ul>

<p>
<b>Tall Paul Shaw</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>paul shaw tall</b> — Direct nominative re-arrangement (strong).</li>
<li><b>walls paul hat</b> — Walls-Paul-hat: he stands above the walls (loose).</li>
<li><b>walls utah lap</b> — Walls-Utah-lap: out-west-tall imagery (loose).</li>
<li><b>paul tall wash</b> — Direct rearrangement — tall-Paul washes out as background (loose).</li>
<li><b>alpha wall stu</b> — Alpha-wall-Stu: tall alpha against the wall (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Tall Shaw</b>
</p>
<ul class="org-ul">
<li>walls hat</li>
<li>tall wash</li>
<li>tall shaw</li>
<li>wall hats</li>
<li>laws halt</li>
<li>whats all</li>
<li>walsh alt</li>
<li>walls tha</li>
<li>hall swat</li>
<li>stall wah</li>
<li>shall wat</li>
<li>halls wat</li>
<li>walt lash</li>
<li>shalt law</li>
<li>shalt wal</li>
<li>stall wha</li>
<li>walsh tal</li>
<li>shawl alt</li>
<li>shawl tal</li>
<li>walsh atl</li>
<li>shawl atl</li>
<li>wall hast</li>
<li>walsh lat</li>
<li>shawl lat</li>
<li>stall haw</li>
<li>whats lal</li>
<li>hall twas</li>
<li>what as ll</li>
<li>wall st ah</li>
<li>wall st ha</li>
<li>was hat ll</li>
<li>saw hat ll</li>
<li>wash at ll</li>
<li>what ll sa</li>
<li>walls th a</li>
<li>wall as th</li>
<li>wall sa th</li>
<li>laws al th</li>
<li>laws la th</li>
<li>was all th</li>
<li>all saw th</li>
<li>law las th</li>
<li>hall st wa</li>
<li>laws ah lt</li>
<li>laws ha lt</li>
<li>wash al lt</li>
<li>wash la lt</li>
<li>has law lt</li>
<li>law ash lt</li>
<li>hall st aw</li>
</ul>

<p>
<b>Tall Paul Shaw</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>walls paul hat</li>
<li>paul tall wash</li>
<li>walls utah lap</li>
<li>paul tall shaw</li>
<li>wall paul hats</li>
<li>walls utah pal</li>
<li>wall utah slap</li>
<li>paul laws halt</li>
<li>pasta hull law</li>
<li>allah laws put</li>
<li>allah puts law</li>
<li>allah walt ups</li>
<li>walls haul tap</li>
<li>walls haul pat</li>
<li>whats paul all</li>
<li>past wall haul</li>
<li>tall swap haul</li>
<li>slap walt haul</li>
<li>walsh paul alt</li>
<li>hull walt asap</li>
<li>alpha stall wu</li>
<li>walls alpha tu</li>
<li>alpha slut law</li>
<li>walls paula th</li>
<li>whats paula ll</li>
<li>walsh paula lt</li>
<li>walsh paula tl</li>
<li>stall paula wh</li>
<li>walls alpha ut</li>
<li>alpha lust law</li>
<li>walls haul apt</li>
<li>what pull alas</li>
<li>wall utah laps</li>
<li>walt haul laps</li>
<li>wall path saul</li>
<li>walls paul tha</li>
<li>pulls walt aha</li>
<li>allah walt sup</li>
<li>wall utah alps</li>
<li>wall utah pals</li>
<li>wall haul taps</li>
<li>walt haul alps</li>
<li>walt haul pals</li>
<li>asphalt all wu</li>
<li>asphalt law lu</li>
<li>asphalt law ul</li>
<li>pulls what ala</li>
<li>whats pull ala</li>
<li>asphalt wal lu</li>
<li>asphalt wal ul</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-trevor-axford" class="outline-2">
<h2 id="trevor-axford"><span class="section-number-2">30.</span> Trevor Axford&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="sidekick">sidekick</span>&#xa0;<span class="missingfinger">missingfinger</span>&#xa0;<span class="drugaccomplice">drugaccomplice</span></span></h2>
<div class="outline-text-2" id="text-trevor-axford">
<p>
An ETA tennis player nicknamed 'Axhandle,' Pemulis's close friend and accomplice in drug schemes, missing part of a finger from a childhood firecracker accident. He is implicated alongside Pemulis in the DMZ-related troubles.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Ax(e) → 'Axhandle'</b> — His surname contains 'ax', generating the nickname 'Axhandle' — a grim joke since he is missing part of a finger, an ax-handle being precisely the thing one grips with all one's fingers.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · substring · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>Ax(e) — 'Axhandle'</b> — His surname contains 'ax', which generates his nickname 'Axhandle' — itself a grim joke since he is missing part of a finger (an ax-handle being precisely the thing one grips with all one's fingers). The name and injury collide.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Trevor Axford</b>
</p>
<ul class="org-ul">
<li><b>oxford rate vr</b> — Oxford-rate-VR: aristocratic-Oxford-rate accomplice (loose).</li>
<li><b>oxford tar ver</b> — Oxford-tar-ver: tar-and-Oxford-verisimilitude (loose).</li>
<li><b>rover ford tax</b> — Rover-Ford-tax: his missing-finger automotive imagery (suggestive).</li>
<li><b>vortex ford ar</b> — Vortex-Ford-AR: vortex of trouble he's drawn into with Pemulis (loose).</li>
<li><b>trader for vox</b> — Trader-for-vox: drug-trader voice (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Trevor Axford</b>
</p>
<ul class="org-ul">
<li>oxford rare tv</li>
<li>oxford rear tv</li>
<li>oxford art rev</li>
<li>oxford rat rev</li>
<li>order favor tx</li>
<li>oxford rate vr</li>
<li>oxford tear vr</li>
<li>rover ford tax</li>
<li>favor retro xd</li>
<li>oxford vera rt</li>
<li>oxford vera tr</li>
<li>oxford rev tar</li>
<li>trevor rod fax</li>
<li>trevor road fx</li>
<li>oxford rate rv</li>
<li>oxford tear rv</li>
<li>oxford rave rt</li>
<li>oxford rave tr</li>
<li>rover dart fox</li>
<li>trevor fox rad</li>
<li>trader roof xv</li>
<li>vortex road fr</li>
<li>vortex road rf</li>
<li>vortex ford ar</li>
<li>vortex ford ra</li>
<li>vortex for rad</li>
<li>vortex far rod</li>
<li>vortex rod raf</li>
<li>draft rover ox</li>
<li>oxford rare vt</li>
<li>oxford rear vt</li>
<li>trevor afro xd</li>
<li>vortex afro dr</li>
<li>vortex afro rd</li>
<li>oxford art ver</li>
<li>oxford rat ver</li>
<li>oxford tar ver</li>
<li>trevor dora fx</li>
<li>vortex dora fr</li>
<li>vortex dora rf</li>
<li>oxford vat err</li>
<li>retard roof xv</li>
<li>trevor ford ax</li>
<li>trevor fox dar</li>
<li>vortex roar df</li>
<li>vortex for dar</li>
<li>vortex rad fro</li>
<li>vortex dar fro</li>
<li>favored rot rx</li>
<li>favored tor rx</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-roy-tony" class="outline-2">
<h2 id="roy-tony"><span class="section-number-2">31.</span> Roy Tony&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="streetviolence">streetviolence</span>&#xa0;<span class="menace">menace</span>&#xa0;<span class="aa">aa</span></span></h2>
<div class="outline-text-2" id="text-roy-tony">
<p>
A volatile, violent Black street figure encountered in Ennet House and AA contexts, who once nearly killed Poor Tony over a botched drug deal. He represents the unromanticized danger of the Boston street-addict world.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Roy ← French roi, 'king'</b> — 'Roy' derives from the French for 'king' — a sardonic crown for a violent street figure who rules his Boston AA/addict patch by fear.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Roy (French roi, 'king')</b> — 'Roy' derives from the French/Old English for 'king' — a sardonic crown for a violent street figure who rules by fear in his Boston AA/addict patch.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Roy Tony</b>
</p>
<ul class="org-ul">
<li><b>tony roy</b> — Inverted name — same letters, opposite order (strong).</li>
<li><b>troy yon</b> — Troy-yon: classical-violence Troy in his rage (loose).</li>
<li><b>yon tory</b> — Yon-tory: Roy as a yon-tory street figure (loose).</li>
<li><b>tory yon</b> — Tory-yon: his Boston-tory street menace (loose).</li>
<li><b>yo tony r</b> — Yo-Tony-R: street-call addressing Tony with violence implied (loose). NOTE: 7-letter form with R/N/T as only consonants and 4 vowels (o/y) — combos collapse to short fragments.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Roy Tony</b>
</p>
<ul class="org-ul">
<li>tony roy</li>
<li>yoon try</li>
<li>try on yo</li>
<li>try no yo</li>
<li>toy or ny</li>
<li>roy to ny</li>
<li>roy ny ot</li>
<li>not yo yr</li>
<li>too ny yr</li>
<li>toy on yr</li>
<li>toy no yr</li>
<li>ton yo yr</li>
<li>roy yo nt</li>
<li>nor yo ty</li>
<li>roy on ty</li>
<li>roy no ty</li>
<li>ron yo ty</li>
<li>toy yo rn</li>
<li>roy yo tn</li>
<li>rot yo ny</li>
<li>toy ny ro</li>
<li>try ny oo</li>
<li>tor yo ny</li>
<li>yoo ny rt</li>
<li>yoo ny tr</li>
<li>yoo yr nt</li>
<li>yoo yr tn</li>
<li>yoo ty rn</li>
<li>toy yo nr</li>
<li>yoo ty nr</li>
<li>nyt or yo</li>
<li>nyt yo ro</li>
<li>nyt yr oo</li>
<li>nor yo yt</li>
<li>roy on yt</li>
<li>roy no yt</li>
<li>ron yo yt</li>
<li>yoo rn yt</li>
<li>yoo nr yt</li>
<li>ono yr ty</li>
<li>ono yr yt</li>
<li>ont yo yr</li>
<li>not yr oy</li>
<li>try on oy</li>
<li>try no oy</li>
<li>nor ty oy</li>
<li>nor yt oy</li>
<li>toy rn oy</li>
<li>toy nr oy</li>
<li>ton yr oy</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-kate-gompert" class="outline-2">
<h2 id="kate-gompert"><span class="section-number-2">32.</span> Kate Gompert&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="depression">depression</span>&#xa0;<span class="suicidality">suicidality</span>&#xa0;<span class="marijuana">marijuana</span></span></h2>
<div class="outline-text-2" id="text-kate-gompert">
<p>
A young marijuana addict hospitalized for suicidal depression, whose interview with a clueless psych resident produces the novel's most clinically precise account of clinical depression as a pain distinct from sadness. She drifts through Ennet House and the Boston AA scene as a study in anhedonia.
</p>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Kate Gompert</b>
</p>
<ul class="org-ul">
<li><b>greek apt tom</b> — Greek-apt-tom: Greek-tragedy-apt anhedonic Tom of mind (loose).</li>
<li><b>metro keg pat</b> — Metro-keg-pat: Boston-metro keg-and-pat youth (loose).</li>
<li><b>maker get pot</b> — Maker-get-pot: marijuana-addict directly named (suggestive).</li>
<li><b>poker met tag</b> — Poker-met-tag: poker-faced depression met by tag-line diagnosis (loose).</li>
<li><b>treat keg mop</b> — Treat-keg-mop: untreated-keg of mop-up depression (loose).</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Kate Gompert</b>
</p>
<ul class="org-ul">
<li>meet park got</li>
<li>term kept ago</li>
<li>gotta keep mr</li>
<li>make port get</li>
<li>term peak got</li>
<li>kept gear tom</li>
<li>market pet go</li>
<li>more kept tag</li>
<li>kept rome tag</li>
<li>market get op</li>
<li>greek matt op</li>
<li>great kept mo</li>
<li>repeat got km</li>
<li>greek tom tap</li>
<li>greek tom pat</li>
<li>maker get top</li>
<li>maker get pot</li>
<li>maker got pet</li>
<li>market gop et</li>
<li>take term gop</li>
<li>term kate gop</li>
<li>market got ep</li>
<li>great poet km</li>
<li>mark poet get</li>
<li>gotta peer km</li>
<li>metro kept ga</li>
<li>mark pete got</li>
<li>potter age km</li>
<li>kept rage tom</li>
<li>operate mt kg</li>
<li>repeat tom kg</li>
<li>remote tap kg</li>
<li>remote pat kg</li>
<li>treat poem kg</li>
<li>metro tape kg</li>
<li>market gop te</li>
<li>rate kept omg</li>
<li>kept tear omg</li>
<li>team pork get</li>
<li>meet pork tag</li>
<li>meat pork get</li>
<li>mate pork get</li>
<li>gate pork met</li>
<li>market ego pt</li>
<li>peter goat km</li>
<li>market get po</li>
<li>greek matt po</li>
<li>poker gate mt</li>
<li>poker met tag</li>
<li>game trek top</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-kevin-bain" class="outline-2">
<h2 id="kevin-bain"><span class="section-number-2">33.</span> Kevin Bain&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="addict">addict</span>&#xa0;<span class="sibling">sibling</span>&#xa0;<span class="peripheral">peripheral</span></span></h2>
<div class="outline-text-2" id="text-kevin-bain">
<p>
Marlon Bain's older brother and a hard-drug addict glimpsed in Ennet House proximity.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Bain ~ bane</b> — The surname homophones 'bane' — a cause of ruin — exactly fitting a hard-drug addict whose life is being destroyed.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>bane</b> — Surname is homophonous with 'bane' (a cause of misery or ruin), fitting for a hard-drug addict whose life is being destroyed.</li>
<li><i>[Phonetic]</i> <b>Bain ~ bane</b> — The surname homophones 'bane' — a cause of ruin — exactly fitting a hard-drug addict whose life is being destroyed.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Kevin Bain</b>
</p>
<ul class="org-ul">
<li><b>knave bin i</b> — the addict-sibling cast as a knave hiding in a bin; loose.</li>
<li><b>naive bink</b> — the peripheral naivete of a junkie casualty (bink as nonword filler); loose.</li>
<li><b>vein bank i</b> — junkie's vein as a bank he keeps drawing on; suggestive.</li>
<li><b>ink a vein b</b> — imperative shorthand for needle use plus stray b; loose.</li>
<li><b>bin via ken</b> — discarded out of the brother's awareness; suggestive.</li>
<li><b>bake inn vi</b> — Ennet House (the inn) where he cooks/uses; loose.</li>
<li><b>ken via bin</b> — Marlon's brother known via the dumpster scene; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Kevin Bain</b>
</p>
<ul class="org-ul">
<li>kevin bain</li>
<li>kevin ban i</li>
<li>bike van in</li>
<li>kevin nba i</li>
<li>bike ann iv</li>
<li>kevin bin a</li>
<li>via bin ken</li>
<li>kevin in ba</li>
<li>bike ann vi</li>
<li>bike inn va</li>
<li>via ben ink</li>
<li>kevin in ab</li>
<li>kevin an bi</li>
<li>kevin na bi</li>
<li>bin ink ave</li>
<li>bank ive in</li>
<li>ban ink ive</li>
<li>nba ink ive</li>
<li>bean ink iv</li>
<li>bean ink vi</li>
<li>kane bin iv</li>
<li>kane bin vi</li>
<li>vibe ink an</li>
<li>vibe ink na</li>
<li>kevin ba ni</li>
<li>kevin ab ni</li>
<li>bank ive ni</li>
<li>bike van ni</li>
<li>bake inn iv</li>
<li>bake inn vi</li>
<li>bin ink eva</li>
<li>vibe ann ki</li>
<li>nike van bi</li>
<li>nike ban iv</li>
<li>nike ban vi</li>
<li>nike nba iv</li>
<li>nike nba vi</li>
<li>nike bin va</li>
<li>ivan ben ki</li>
<li>ivan ken bi</li>
<li>ivan ink be</li>
<li>bank vii en</li>
<li>bank vii ne</li>
<li>ban ken vii</li>
<li>nba ken vii</li>
<li>vibe inn ka</li>
<li>evan bin ki</li>
<li>evan ink bi</li>
<li>bank vein i</li>
<li>vein ban ki</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-lyle" class="outline-2">
<h2 id="lyle"><span class="section-number-2">34.</span> Lyle&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="guru">guru</span>&#xa0;<span class="sweat">sweat</span>&#xa0;<span class="wisdom">wisdom</span></span></h2>
<div class="outline-text-2" id="text-lyle">
<p>
The barefoot guru who lives in the ETA weight room, subsisting on the perspiration of student athletes who come to him for koan-like advice. He functions as the academy's resident sage, dispensing aphoristic wisdom on suffering, identity, and acceptance.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>yell</b> — The silent, sweat-eating guru is the inverse of a 'yell' — the only single English word in this letter set, and a tidy negation of his ascetic quietism.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Lyell (Charles Lyell)</b> — Echoes the geologist Charles Lyell, whose principle of uniformitarianism — slow, steady processes shaping the world — mirrors the guru's quietist, accept-and-endure ethic.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Lyle</b>
</p>
<ul class="org-ul">
<li><b>yell</b> — the silent guru is the inverse of a yell; suggestive (and the only single English word in this letter set).</li>
<li><b>ye L L</b> — addressing the sage by initials; loose.</li>
<li><b>ell y</b> — an ell (measuring unit) plus query — the guru as cryptic measure; very loose.</li>
<li><b>ley l</b> — a 'ley' (sacred line) for the resident mystic; loose.</li>
<li><b>lye l</b> — lye, the caustic, mirrors his sweat-eating asceticism; loose.</li>
<li><b>ly e l</b> — fragmentary; this 4-letter form (l, l, y, e) is essentially intractable for English. Confidence: very loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Lyle</b>
</p>
<ul class="org-ul">
<li>yell</li>
<li>lyle</li>
<li>ll ye</li>
<li>el ly</li>
<li>le ly</li>
<li>ll ey</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-evan-ingersoll" class="outline-2">
<h2 id="evan-ingersoll"><span class="section-number-2">35.</span> Evan Ingersoll&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="junior">junior</span>&#xa0;<span class="eschaton">eschaton</span>&#xa0;<span class="disliked">disliked</span></span></h2>
<div class="outline-text-2" id="text-evan-ingersoll">
<p>
A younger ETA student and Eschaton participant, generally disliked by his peers and prone to whining.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>snivel longer a</b> — Captures exactly the trait that defines him — the peer-rejected, perpetually whining junior who outlasts everyone's patience by sniveling.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Robert G. Ingersoll</b> — Shares a surname with the 19th-century 'Great Agnostic' orator Robert Ingersoll, an ironic match for a whiny junior whose voice annoys everyone around him.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Evan Ingersoll</b>
</p>
<ul class="org-ul">
<li><b>slavering lone</b> — the disliked junior salivating on the sidelines of Eschaton; suggestive.</li>
<li><b>overline slang</b> — an Eschaton player overlining the field with bratty argot; loose.</li>
<li><b>leveling arson</b> — Eschaton ends in his levelling/arson chaos; suggestive.</li>
<li><b>allergen vison</b> — junior cast as the allergen the older boys can't tolerate (vison as filler); loose.</li>
<li><b>longer lives an</b> — the whining one who outlasts; loose.</li>
<li><b>single love ran</b> — a single, unloved boy ran; loose.</li>
<li><b>loner livens ag</b> — the loner livens (in the bad sense) Eschaton; loose.</li>
<li><b>snivel longer a</b> — captures his peer-rejected whining; suggestive.</li>
<li><b>learning solve</b> — (in algorithmic list); the junior learns/solves under fire; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Evan Ingersoll</b>
</p>
<ul class="org-ul">
<li>learning loves</li>
<li>learning solve</li>
<li>leaning lovers</li>
<li>villagers none</li>
<li>villagers neon</li>
<li>neville organs</li>
<li>resolving lane</li>
<li>resolving lean</li>
<li>resolving neal</li>
<li>resolving lena</li>
<li>inverse gallon</li>
<li>rollins geneva</li>
<li>overall ensign</li>
<li>rollins avenge</li>
<li>leveling arson</li>
<li>leveling sonar</li>
<li>generals lovin</li>
<li>enrolling save</li>
<li>enrolling vase</li>
<li>selling verona</li>
<li>granville ones</li>
<li>granville nose</li>
<li>novella singer</li>
<li>novella resign</li>
<li>novella reigns</li>
<li>several long in</li>
<li>leave girls non</li>
<li>learn live song</li>
<li>longer lives an</li>
<li>large lives non</li>
<li>lives long near</li>
<li>learn love sign</li>
<li>level gonna sir</li>
<li>longer live san</li>
<li>single love ran</li>
<li>online large vs</li>
<li>losing never al</li>
<li>losing never la</li>
<li>selling over an</li>
<li>leaves girl non</li>
<li>single role van</li>
<li>region sell van</li>
<li>levels gain nor</li>
<li>given learn los</li>
<li>evening roll as</li>
<li>seeing roll van</li>
<li>seven gain roll</li>
<li>longer evil san</li>
<li>learn song evil</li>
<li>longer lies van</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-otis-p-lord" class="outline-2">
<h2 id="otis-p-lord"><span class="section-number-2">36.</span> Otis P. Lord&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="statistician">statistician</span>&#xa0;<span class="gamemaster">gamemaster</span>&#xa0;<span class="eschaton">eschaton</span></span></h2>
<div class="outline-text-2" id="text-otis-p-lord">
<p>
A 13-year-old ETA baseliner and calculus phenom from Wilmington, Delaware, who serves as Eschaton's gamemaster and statistician — wearing the Beanie that designates the role and adjudicating the nuclear-war simulation with mock-judicial gravity. He is run over by his own monitor mid-game in the chaos that ends Eschaton, and is later seen with the monitor frozen around his head.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>Sport Idol</b> — The Beanie-wearing, mock-judicial gamemaster of Eschaton is literally the sport's small god — an unmissable rearrangement of the full name.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>lordosis</b> — The full name 'Otis Lord' phonetically suggests 'lordosis,' the inward spinal curvature — apt for a kid who ends up immobilized with a CRT monitor frozen around his head and neck.</li>
<li><i>[Entendre]</i> <b>Lord (judge/deity)</b> — He adjudicates Eschaton with 'mock-judicial gravity' wearing the Beanie of office, so 'Lord' literalizes his role as the game's appointed god-judge.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Otis Lord</b>
</p>
<ul class="org-ul">
<li><b>old riots</b> — his Eschaton ends in riot; suggestive.</li>
<li><b>doris lot</b> — casts him as one of life's losers (a Doris's lot); loose.</li>
<li><b>drool sit</b> — the monitor-stunned, drool-on-chin tableau after his accident; suggestive.</li>
<li><b>solid rot</b> — the gamemaster crushed under his own monitor as a solid rot; suggestive.</li>
<li><b>lord otis</b> — self-reference to the title-bestowing god of Eschaton; suggestive.</li>
<li><b>list door</b> — statistician at the gate of the game; loose.</li>
<li><b>rid stool</b> — the toppled stool of his Eschaton mishap, ridding him of authority; loose.</li>
</ul>

<p>
<b>Otis P. Lord</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>sport idol</b> — (in algorithmic list); Eschaton's Beanie-wearer is the sport's small god; strong.</li>
<li><b>spoilt rod</b> — the spoilt rod (sceptre) of the Adjudicator-Beanie king felled by his own monitor; suggestive.</li>
<li><b>stolid pro</b> — the gravely judicial 13-year-old; suggestive.</li>
<li><b>drool tips</b> — (in algorithmic list); the post-monitor catatonic drool; suggestive.</li>
<li><b>solid port</b> — the gamemaster as the solid port the war-game (briefly) puts in to; suggestive.</li>
<li><b>torpid sol</b> — the torpid sun-god of Eschaton, knocked out by his own monitor; suggestive.</li>
<li><b>tripod sol</b> — the tripod-mounted sun-king; loose.</li>
<li><b>pistol rod</b> — the absurd authority of the 13-year-old gamemaster as pistol-rod; suggestive.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Otis Lord</b>
</p>
<ul class="org-ul">
<li>list door</li>
<li>tools rid</li>
<li>solo dirt</li>
<li>doors lit</li>
<li>sold riot</li>
<li>doors til</li>
<li>riot olds</li>
<li>sort idol</li>
<li>sold trio</li>
<li>olds trio</li>
<li>riots old</li>
<li>roots lid</li>
<li>solid rot</li>
<li>doris lot</li>
<li>solid tor</li>
<li>tools dir</li>
<li>root slid</li>
<li>stool rid</li>
<li>stool dir</li>
<li>lords iot</li>
<li>dirt oslo</li>
<li>idols rot</li>
<li>idols tor</li>
<li>list odor</li>
<li>door slit</li>
<li>odor slit</li>
<li>stood irl</li>
<li>torso lid</li>
<li>lord otis</li>
<li>dots lori</li>
<li>its old or</li>
<li>slid toro</li>
<li>sold tori</li>
<li>olds tori</li>
<li>told is or</li>
<li>lords ito</li>
<li>lost dior</li>
<li>lots dior</li>
<li>slot dior</li>
<li>root lids</li>
<li>toro lids</li>
<li>drool its</li>
<li>drool sit</li>
<li>drool tis</li>
<li>drool ist</li>
<li>drool sti</li>
<li>roots dil</li>
<li>torso dil</li>
<li>rods toil</li>
<li>roost lid</li>
</ul>

<p>
<b>Otis P. Lord</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>solid port</li>
<li>split door</li>
<li>pilots rod</li>
<li>sport idol</li>
<li>ports idol</li>
<li>pistol rod</li>
<li>pools dirt</li>
<li>troops lid</li>
<li>loops dirt</li>
<li>pilot rods</li>
<li>tools drip</li>
<li>doris plot</li>
<li>troop slid</li>
<li>stool drip</li>
<li>porto slid</li>
<li>split odor</li>
<li>idols port</li>
<li>proto slid</li>
<li>tripod los</li>
<li>tripod sol</li>
<li>plots dior</li>
<li>troop lids</li>
<li>porto lids</li>
<li>proto lids</li>
<li>spoilt rod</li>
<li>drool tips</li>
<li>drool spit</li>
<li>drool pits</li>
<li>troops dil</li>
<li>drops toil</li>
<li>stop lord i</li>
<li>stop oil dr</li>
<li>post lord i</li>
<li>post oil dr</li>
<li>lord top is</li>
<li>old top sir</li>
<li>lost drop i</li>
<li>drop lot is</li>
<li>drop oil st</li>
<li>told pro is</li>
<li>list pro do</li>
<li>sold pro it</li>
<li>its old pro</li>
<li>trip old so</li>
<li>old pro sit</li>
<li>lord spot i</li>
<li>spot oil dr</li>
<li>drop lots i</li>
<li>drop los it</li>
<li>trip los do</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-ferocious-francis-gehaney" class="outline-2">
<h2 id="ferocious-francis-gehaney"><span class="section-number-2">37.</span> Ferocious Francis Gehaney&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="aa">aa</span>&#xa0;<span class="sponsor">sponsor</span>&#xa0;<span class="crocodile">crocodile</span></span></h2>
<div class="outline-text-2" id="text-ferocious-francis-gehaney">
<p>
Don Gately's AA sponsor, a crusty Boston AA elder known as the Crocodile, whose gravelly aphorisms anchor Gately's recovery. His no-bullshit Boston AA style embodies the program's anti-intellectual, do-the-next-right-thing pragmatism.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Francis = 'free man' / Franciscan</b> — The Franciscan vow of humility and stripped-down spirituality maps onto AA's anti-intellectual pragmatism, and is comically undercut by the 'Ferocious' Crocodile epithet.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>Francis = Franciscan / 'free man'</b> — The given name evokes the Franciscan vow of humility and poverty — the AA program's stripped-down ascetic spirituality — undercut comically by the epithet 'Ferocious.'</li>
<li><i>[Etymology]</i> <b>Francis = 'free man' / Franciscan</b> — The Franciscan vow of humility and stripped-down spirituality maps onto AA's anti-intellectual pragmatism, and is comically undercut by the 'Ferocious' Crocodile epithet.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Ferocious Gehaney</b>
</p>
<ul class="org-ul">
<li><b>house of carney gie</b> — captures the AA-as-circus house of Boston meetings (carney + 'gie' = give); suggestive.</li>
<li><b>feracious gone hey</b> — his crusty gone-too-soon mentor energy; loose.</li>
<li><b>coniferous hag yee</b> — the Crocodile elder as coniferous hag (the gnarled-tree image); loose.</li>
<li><b>facinorous hey gee</b> — his colorful, faintly criminal sponsorial bark; loose.</li>
<li><b>feracious gene hoy</b> — AA gene of the recovery-elder; loose.</li>
<li><b>geyserine ouch oaf</b> — his geyser-blast aphorisms calling oafs out; suggestive.</li>
<li><b>forechase ione guy</b> — the chasing-down of the alcoholic guy; loose.</li>
<li><b>gaucherie nose foy</b> — his gaucherie (no-bullshit roughness) on the nose; loose.</li>
</ul>

<p>
<b>Ferocious Francis Gehaney</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>concessionaire huffy gear</b> — AA elder-as-concessionaire of the program, with huffy 'do the next right thing' gear; suggestive.</li>
<li><b>concessionaire huffy rage</b> — his huffy Crocodile rage at intellectualizing newcomers; suggestive.</li>
<li><b>concessionaire gruff yeah</b> — his gruff yeah-style assent that anchors Gately's recovery; strong.</li>
<li><b>sacchariferous fogey nine</b> — the sweet-talking AA fogey at the nine-o'clock meeting; loose.</li>
<li><b>infraconscious faery ghee</b> — AA as below-conscious sponsorship + folkish ghee/faery seasoning; loose.</li>
<li><b>concessionary figure heaf</b> — the program's concessionary mentor figure (heaf as filler); loose.</li>
<li><b>nonefficacious sheer gary</b> — the program's outwardly non-efficacious bluntness reading like sheer Gary-the-elder talk; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Ferocious Gehaney</b>
</p>
<ul class="org-ul">
<li>agency refuse ohio</li>
<li>fashion greece you</li>
<li>refugees yahoo inc</li>
<li>securing yahoo fee</li>
<li>figure scene yahoo</li>
<li>refuge since yahoo</li>
<li>hygiene cause roof</li>
<li>hygiene sauce roof</li>
<li>fierce yahoo genus</li>
<li>rooney chief usage</li>
<li>generic yahoo fuse</li>
<li>fierce house agony</li>
<li>gracious honey fee</li>
<li>fearing echoes you</li>
<li>guinea cheesy roof</li>
<li>cheesy rogue fiona</li>
<li>cheesy rouge fiona</li>
<li>surface hygiene oo</li>
<li>herein cause goofy</li>
<li>herein sauce goofy</li>
<li>energies yahoo ufc</li>
<li>gunfire choose yea</li>
<li>gunfire choose aye</li>
<li>encourage shoe fyi</li>
<li>encourage hose fyi</li>
<li>increase goofy hue</li>
<li>recognise yeah ufo</li>
<li>refugees china yoo</li>
<li>refugees chain yoo</li>
<li>refugee chains yoo</li>
<li>hygiene coarse ufo</li>
<li>refugees yahoo nic</li>
<li>freeing yahoo cues</li>
<li>echoing youre safe</li>
<li>cheney rogue sofia</li>
<li>cheney sofia rouge</li>
<li>rescuing yahoo fee</li>
<li>hygiene focus aero</li>
<li>ceasefire young oh</li>
<li>ceasefire young ho</li>
<li>ceasefire hong you</li>
<li>ceasefire hung yoo</li>
<li>ceasefire hyun goo</li>
<li>refugee hanoi cosy</li>
<li>younger fiasco hee</li>
<li>greenhouse cio fay</li>
<li>cohesion refuge ya</li>
<li>cohesion refuge ay</li>
<li>cohesion grey uefa</li>
<li>refugee sayin choo
<i>Search timed out at 45s with 195 partial candidates.</i></li>
</ul>

<p>
<b>Ferocious Francis Gehaney</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>agencies honorary suffice</li>
<li>confusion fishery acreage</li>
<li>ferocious fencing hearsay</li>
<li>sufficiency orange ashore</li>
<li>scenario geoffrey sichuan
<i>Search timed out at 45s with 5 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-keith-freer" class="outline-2">
<h2 id="keith-freer"><span class="section-number-2">38.</span> Keith Freer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="tennis">tennis</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="crude">crude</span></span></h2>
<div class="outline-text-2" id="text-keith-freer">
<p>
An older ETA tennis player, sexually preoccupied and something of a locker-room blowhard.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Freer ~ 'more free'</b> — The surname reads as a comparative of 'free,' ironic for a locker-room blowhard whose obsessive sexual preoccupation makes him anything but liberated.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>freer (more free)</b> — The surname reads as a comparative of 'free,' ironic for a locker-room blowhard whose obsessive sexual preoccupation makes him anything but liberated.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Keith Freer</b>
</p>
<ul class="org-ul">
<li><b>ferret hike</b> — the locker-room blowhard ferreting/hiking around the academy; suggestive.</li>
<li><b>heifer trek</b> — his ETA tennis trek with adolescent leering ('heifer' echoing his sexual preoccupation); suggestive.</li>
<li><b>either kerf</b> — either-or kerf (a cut), the locker-room edge; loose.</li>
<li><b>refer keith</b> — (in algorithmic list); the self-referring blowhard speech; suggestive.</li>
<li><b>kithe refer</b> — kithe (archaic: 'to make known') for the locker-room exhibitionist; loose.</li>
<li><b>freer kithe</b> — the freer kithe (display) of his locker-room boasts; loose.</li>
<li><b>reefer kith</b> — his reefer-kith (kin) crude bonhomie; loose.</li>
<li><b>three kefir</b> — drop in cohort; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Keith Freer</b>
</p>
<ul class="org-ul">
<li>refer keith</li>
<li>ferret hike</li>
<li>keith freer</li>
<li>refer kit he</li>
<li>free her kit</li>
<li>refer kit eh</li>
<li>here trek if</li>
<li>here trek fi</li>
<li>free trek hi</li>
<li>fire trek he</li>
<li>fire trek eh</li>
<li>keith ref re</li>
<li>keith ref er</li>
<li>here kit ref</li>
<li>free hike rt</li>
<li>tree hike fr</li>
<li>hire trek fe</li>
<li>trek heir fe</li>
<li>there ref ki</li>
<li>three ref ki</li>
<li>refer the ki</li>
<li>trek reef hi</li>
<li>hike reef rt</li>
<li>reef her kit</li>
<li>free hike tr</li>
<li>hike reef tr</li>
<li>here erik ft</li>
<li>free erik th</li>
<li>feet erik hr</li>
<li>thee erik fr</li>
<li>reef erik th</li>
<li>erik the ref</li>
<li>feet kerr hi</li>
<li>thee kerr if</li>
<li>thee kerr fi</li>
<li>kerr hit fee</li>
<li>here erik tf</li>
<li>here kite fr</li>
<li>here kite rf</li>
<li>free kite hr</li>
<li>tree hike rf</li>
<li>thee erik rf</li>
<li>reef kite hr</li>
<li>kite her ref</li>
<li>retire fe hk</li>
<li>refer tie hk</li>
<li>free tier hk</li>
<li>free tire hk</li>
<li>free rite hk</li>
<li>fire tree hk</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-calvin-thrust" class="outline-2">
<h2 id="calvin-thrust"><span class="section-number-2">39.</span> Calvin Thrust&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="counselor">counselor</span>&#xa0;<span class="exporn">exporn</span>&#xa0;<span class="corvette">corvette</span></span></h2>
<div class="outline-text-2" id="text-calvin-thrust">
<p>
An Ennet House counselor and former porn actor in recovery, vain about his physique and his Corvette. His backstory as ex-talent gives him a particular line on shame and self-presentation in sobriety.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>thrust (sexual)</b> — A transparent porn-industry aptronym — pelvic 'thrust' literalized as a surname for the ex-talent recovering counselor.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>thrust (sexual)</b> — The surname is a transparent porn-industry pun on pelvic 'thrust,' literalizing his ex-talent backstory.</li>
<li><i>[Allusion]</i> <b>John Calvin</b> — First name evokes the Calvinist doctrine of total depravity and predestined shame, ironically yoked to a recovering porn actor preoccupied with self-presentation.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Calvin Thrust</b>
</p>
<ul class="org-ul">
<li><b>thrust calvin</b> — (in algorithmic list); blunt restatement of his porn-derived aptronym; strong.</li>
<li><b>thirst vulcan</b> — (in algorithmic list); the Corvette-vain ex-actor as thirsty Vulcan; strong.</li>
<li><b>vulcan thirst</b> — variant; the recovering body-vain Vulcan; suggestive.</li>
<li><b>calvin truths</b> — (in algorithmic list); the calvinist truths of recovery applied to a porn name; suggestive.</li>
<li><b>calvin struth</b> — 'struth' (god's truth) — religious-confessional wail under the porn name; loose.</li>
<li><b>thrust van cli</b> — the porn-talent's van-thrust travel from set to set; loose.</li>
<li><b>thrust vain cl</b> — his vainglorious recovery preening; suggestive.</li>
<li><b>intrust vlach</b> — obscure 'vlach'; loose.</li>
<li><b>scarth nut liv</b> — drop — obscure.</li>
<li><b>stunt rich val</b> — the stuntwork-as-porn body of Calvin; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Calvin Thrust</b>
</p>
<ul class="org-ul">
<li>thrust calvin</li>
<li>calvin truths</li>
<li>thirst vulcan</li>
<li>until crash tv</li>
<li>until chart vs</li>
<li>start lunch iv</li>
<li>latin crush tv</li>
<li>start lunch vi</li>
<li>shirt cult van</li>
<li>trans cult hiv</li>
<li>vital hurts nc</li>
<li>travis cut nhl</li>
<li>virtual nhs ct</li>
<li>insult arch tv</li>
<li>trust clan hiv</li>
<li>launch stir tv</li>
<li>rivals hunt ct</li>
<li>turns vital ch</li>
<li>shirt vault nc</li>
<li>rivals cunt th</li>
<li>thrust clan iv</li>
<li>thrust clan vi</li>
<li>rival stunt ch</li>
<li>viral stunt ch</li>
<li>stunt carl hiv</li>
<li>travis hunt cl</li>
<li>thrust ivan cl</li>
<li>vault hints cr</li>
<li>stuart nhl vic</li>
<li>turns halt vic</li>
<li>launch tits vr</li>
<li>insult chat vr</li>
<li>calvin hurt st</li>
<li>calvin thus rt</li>
<li>calvin shut rt</li>
<li>calvin ruth st</li>
<li>calvin thru st</li>
<li>truth silva nc</li>
<li>chris vault nt</li>
<li>vital crush nt</li>
<li>thrust vain cl</li>
<li>calvin rust th</li>
<li>calvin thus tr</li>
<li>calvin shut tr</li>
<li>christ nut val</li>
<li>thrust inc val</li>
<li>trust inch val</li>
<li>trust chin val</li>
<li>chris vault tn</li>
<li>lunch vista rt</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-m-fortier" class="outline-2">
<h2 id="m-fortier"><span class="section-number-2">40.</span> M. Fortier&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="afr">afr</span>&#xa0;<span class="quebecois">quebecois</span>&#xa0;<span class="terrorist">terrorist</span></span></h2>
<div class="outline-text-2" id="text-m-fortier">
<p>
A senior cell leader of the Assassins des Fauteuils Rollents (A.F.R.), the Quebecois separatist wheelchair terrorists hunting the master copy of the Entertainment. He coordinates the Antitoi raid and the broader hunt for the lethal cartridge.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>fortier ~ fort (French: 'strong/fortress')</b> — The French surname is built on 'fort' — strength, fortress — exactly suiting a hardened cell leader of the armed AFR coordinating raids.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>fortier ~ fort (French: 'strong')</b> — The French surname is built on 'fort' ('strong/fortress'), suiting a hardened cell leader of an armed separatist movement.</li>
<li><i>[Etymology]</i> <b>Latin fortis 'strong'</b> — Ultimately from Latin 'fortis,' reinforcing the militant fortitude of an A.F.R. commander coordinating raids.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>M. Fortier</b>
</p>
<ul class="org-ul">
<li><b>fire mort</b> — Fortier as instrument of fiery death (mort) hunting the Entertainment; strong.</li>
<li><b>tremor if</b> — the wheelchair AFR cell-leader as tremor; suggestive.</li>
<li><b>fort emir</b> — Fortier as the fort's emir, leader of the AFR cell; suggestive.</li>
<li><b>form tier</b> — (in algorithmic list); rank-tier of the AFR; loose.</li>
<li><b>merit for</b> — (in algorithmic list); ironic — the terror-merit of a wheelchair cell; loose.</li>
<li><b>more rift</b> — (in algorithmic list); the Quebec/U.S. rift the AFR exploits; suggestive.</li>
<li><b>remit for</b> — (in algorithmic list); the cartridge-hunter's remit; loose.</li>
<li><b>former it</b> — (in algorithmic list); the former (creator) of the Entertainment chase; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>M. Fortier</b>
</p>
<ul class="org-ul">
<li>former it</li>
<li>reform it</li>
<li>from tier</li>
<li>form tier</li>
<li>merit for</li>
<li>from tire</li>
<li>form tire</li>
<li>former ti</li>
<li>reform ti</li>
<li>firm tore</li>
<li>timer for</li>
<li>more rift</li>
<li>rome rift</li>
<li>from rite</li>
<li>form rite</li>
<li>retro imf</li>
<li>trim fore</li>
<li>metro fir</li>
<li>metro fri</li>
<li>motif err</li>
<li>merit fro</li>
<li>timer fro</li>
<li>timor ref</li>
<li>forte rim</li>
<li>forte mri</li>
<li>forte mir</li>
<li>fret mori</li>
<li>remit for</li>
<li>remit fro</li>
<li>fire mort</li>
<li>rife mort</li>
<li>timor fer</li>
<li>tremor if</li>
<li>tremor fi</li>
<li>fort emir</li>
<li>from teri</li>
<li>form teri</li>
<li>fire to mr</li>
<li>from it re</li>
<li>form it re</li>
<li>term for i</li>
<li>term or if</li>
<li>firm to re</li>
<li>firm or et</li>
<li>for tie mr</li>
<li>for tim re</li>
<li>fort re im</li>
<li>fort re mi</li>
<li>term or fi</li>
<li>from it er</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-kent-blott" class="outline-2">
<h2 id="kent-blott"><span class="section-number-2">41.</span> Kent Blott&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="junior">junior</span>&#xa0;<span class="eschaton">eschaton</span>&#xa0;<span class="small">small</span></span></h2>
<div class="outline-text-2" id="text-kent-blott">
<p>
One of the youngest ETA students, an Eschaton participant noted for his small stature and bodily frailties.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Blott ~ blot</b> — The surname sounds like 'blot' — a small smudge or stain — fitting one of the smallest, most physically frail Eschaton juniors, a barely-there mark on the page.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>blot</b> — Surname sounds like 'blot' — a small smudge or stain — fitting for one of the smallest, most physically frail Eschaton participants, a barely-there mark on the page.</li>
<li><i>[Phonetic]</i> <b>Blott ~ blot</b> — The surname sounds like 'blot' — a small smudge or stain — fitting one of the smallest, most physically frail Eschaton juniors, a barely-there mark on the page.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Kent Blott</b>
</p>
<ul class="org-ul">
<li><b>knelt bott</b> — the smallest junior knelt during Eschaton (with 'bott' as nonword filler); loose.</li>
<li><b>kent lot bt</b> — (in algorithmic list); loose.</li>
<li><b>bolt ken tt</b> — (in algorithmic list); the small one bolting; loose.</li>
<li><b>knot belt t</b> — the small junior knot/belt of Eschaton; loose.</li>
<li><b>bent lot kt</b> — (in algorithmic list); the small bent figure; loose.</li>
<li><b>tent lot kb</b> — (in algorithmic list); his tent-lot at the war-game; loose. Form (3 t's + b/k/n/o/l/e) is largely intractable for English.</li>
<li><b>elton tt kb</b> — (in algorithmic list); loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Kent Blott</b>
</p>
<ul class="org-ul">
<li>kent lot bt</li>
<li>token lt bt</li>
<li>token tl bt</li>
<li>kent bot lt</li>
<li>kent bot tl</li>
<li>knot let bt</li>
<li>knot bet lt</li>
<li>knot bet tl</li>
<li>knot tel bt</li>
<li>token lt tb</li>
<li>token tl tb</li>
<li>kent lot tb</li>
<li>knot let tb</li>
<li>knot tel tb</li>
<li>token lb tt</li>
<li>bolt ken tt</li>
<li>bloke nt tt</li>
<li>bloke tn tt</li>
<li>knob let tt</li>
<li>knob tel tt</li>
<li>tent lot kb</li>
<li>tent lot bk</li>
<li>knot bel tt</li>
<li>elton tt kb</li>
<li>elton tt bk</li>
<li>knot lte bt</li>
<li>knot lte tb</li>
<li>knob lte tt</li>
<li>token tt bl</li>
<li>belt tnt ok</li>
<li>belt tnt ko</li>
<li>bolt tnt ke</li>
<li>kobe tnt lt</li>
<li>kobe tnt tl</li>
<li>bot elk tnt</li>
<li>noble tt kt</li>
<li>nobel tt kt</li>
<li>elton bt kt</li>
<li>elton tb kt</li>
<li>belt not kt</li>
<li>belt ton kt</li>
<li>bolt ten kt</li>
<li>bolt net kt</li>
<li>bolt ent kt</li>
<li>bent lot kt</li>
<li>lent bot kt</li>
<li>lobe tnt kt</li>
<li>bolt tnt ek</li>
<li>noble tt tk</li>
<li>nobel tt tk</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-near-eastern-medical-attaché" class="outline-2">
<h2 id="near-eastern-medical-attaché"><span class="section-number-2">42.</span> Near-Eastern Medical Attaché&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="saudi">saudi</span>&#xa0;<span class="attach">attach</span>&#xa0;<span class="entertainment">entertainment</span></span></h2>
<div class="outline-text-2" id="text-near-eastern-medical-attaché">
<p>
A medical attaché to a Saudi prince, fastidious about his sinuses and his evening unwinding ritual, who receives an unmarked cartridge of the Entertainment and is found catatonic in front of his viewer along with everyone who comes to check on him. His fate is the novel's first demonstration of the lethal Infinite Jest cartridge.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>attaché (French: 'attached/bound')</b> — From French 'attacher,' grimly literal for a viewer who becomes physically fixed — bound — to his entertainment screen until he dies.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>attaché (French: 'attached')</b> — From French 'attacher' ('to attach/bind'), grimly literal for a viewer who becomes physically fixed — bound — to his entertainment screen until he dies.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Near-Eastern Attaché</b>
</p>
<ul class="org-ul">
<li><b>rethreaten castanea</b> — the catatonic viewer 'rethreatened' by the lethal cartridge (castanea = chestnut, a brittle dead thing); suggestive.</li>
<li><b>anarcestean theater</b> — anarcestean (anarchic) home-theater that kills him; suggestive.</li>
<li><b>transcreate ethane a</b> — the cartridge as a trans-created ethane vapor that paralyzes; loose.</li>
<li><b>aceanthrene astarte</b> — obscure; the lethal-female Astarte image (echoing the rumored Joelle figure on screen); loose.</li>
<li><b>thereanent caesar at</b> — his nightly Caesarian unwinding ritual locked there at the screen; loose.</li>
<li><b>transcreate taheen a</b> — drop — obscure; suggestive.</li>
<li><b>aceanthrene taster a</b> — the discerning attaché as taster of the Entertainment; suggestive.</li>
</ul>

<p>
<b>Near-Eastern Medical Attaché</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>tetrasaccharide ameen leant</b> — the lethal sugar (tetrasaccharide) of the Entertainment + 'leant' echoes catatonic posture; loose.</li>
<li><b>sacramentarian delete teach</b> — the cartridge that 'deletes' the teacher and his sacrament-like nightly ritual; suggestive.</li>
<li><b>reascertainment teach dalea</b> — the cartridge's lethal reascertainment of cinema's grip; loose.</li>
<li><b>sacramentarian decate lethe</b> — Lethe (river of forgetting) for the catatonic viewer; suggestive.</li>
<li><b>reascertainment ahead cleat</b> — the cleat-fixed viewer ahead of his screen; loose.</li>
<li><b>tetrasaccharide enate leman</b> — the catatonic 'enate leman' (fixed lover) of the Entertainment; loose.</li>
<li><b>tetrasaccharide ameen ental</b> — drop — obscure; loose.</li>
<li><b>sacramentarian decate ethel</b> — loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Near-Eastern Attaché</b>
</p>
<ul class="org-ul">
<li>theatre creates anna</li>
<li>theater creates anna</li>
<li>career estate nathan</li>
<li>create nathan easter</li>
<li>theatre stance arena</li>
<li>theater stance arena</li>
<li>retreat nathan cease</li>
<li>retreat teaches anna</li>
<li>threaten create nasa</li>
<li>threaten stance area</li>
<li>threaten creates ana</li>
<li>threaten easter ncaa</li>
<li>teacher tenants area</li>
<li>eastern theatre ncaa</li>
<li>eastern theater ncaa</li>
<li>theatre nearest ncaa</li>
<li>theater nearest ncaa</li>
<li>entrance theaters aa</li>
<li>theaters create anna</li>
<li>threaten caesar neat</li>
<li>threaten caesar nate</li>
<li>teachers tenant area</li>
<li>teacher tenant areas</li>
<li>threaten centers aaa</li>
<li>threaten centres aaa</li>
<li>theatre nascar eaten</li>
<li>theater nascar eaten</li>
<li>threatens center aaa</li>
<li>threatens recent aaa</li>
<li>threatens create ana</li>
<li>threatens centre aaa</li>
<li>threaten teresa ncaa</li>
<li>create nathan teresa</li>
<li>theatre earnest ncaa</li>
<li>theater earnest ncaa</li>
<li>threaten arena caste</li>
<li>terrace estate hanna</li>
<li>terrace nathan tease</li>
<li>caesar tenant heater</li>
<li>entrance theatres aa</li>
<li>theaters create nana</li>
<li>theatres create anna</li>
<li>theatres create nana</li>
<li>theatre creates nana</li>
<li>theater creates nana</li>
<li>retreat teaches nana</li>
<li>recreate tenants aha</li>
<li>recreate nathan east</li>
<li>recreate nathan seat</li>
<li>recreate nathan eats</li>
</ul>

<p>
<b>Near-Eastern Medical Attaché</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>attendance material reaches</li>
<li>americans alternate cheated</li>
<li>clearance animated theaters</li>
<li>accelerated samantha entire</li>
<li>attendance americas leather</li>
<li>accelerated emirates nathan</li>
<li>accelerate manhattan desire</li>
<li>accelerate manhattan reside</li>
<li>accelerated entertain hamas</li>
<li>accelerate attained sherman</li>
<li>mediterranean atleast cache</li>
<li>clearance animated theatres</li>
<li>antarctica released methane</li>
<li>alternate academia trenches</li>
<li>accelerated threatens mania</li>
<li>accelerate threatens damian</li>
<li>catherine cleaners metadata</li>
<li>mediterranean escalate chat</li>
<li>accelerated manhattan serie</li>
<li>mercenaries atlanta cheated</li>
<li>accelerated methane artisan</li>
<li>accelerated methane sinatra</li>
<li>accelerated tasmanian there</li>
<li>accelerated tasmanian three</li>
<li>accelerated tasmanian ether</li>
<li>accelerate meredith santana</li>
<li>accelerated terminate hasan</li>
<li>threatened americana castle</li>
<li>terminated accelerate hasan</li>
<li>incarcerated athlete seaman</li>
<li>lancashire metadata terence</li>
<li>mediterranean acetate clash</li>
<li>cheerleaders attainment aca</li>
<li>netherlands america acetate</li>
<li>cheerleader attainment casa</li>
<li>attendance emirates rachael</li>
<li>attainment decrease rachael</li>
<li>accelerated armenians theta</li>
<li>clearance shattered animate</li>
<li>mediterranean attaches lace</li>
<li>mediterranean attaches alec</li>
<li>headmaster catalina terence</li>
<li>clearances animated theatre</li>
<li>clearances animated theater</li>
<li>clearances metadata neither</li>
<li>clearances metadata therein</li>
<li>catherine metadata cleanser</li>
<li>accelerate mistreated hanna</li>
<li>mistreated clearance athena</li>
<li>accelerated marietta hansen
<i>Search timed out at 45s with 73 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-lucien-antitoi" class="outline-2">
<h2 id="lucien-antitoi"><span class="section-number-2">43.</span> Lucien Antitoi&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="antitoi">antitoi</span>&#xa0;<span class="mute">mute</span>&#xa0;<span class="videoshop">videoshop</span></span></h2>
<div class="outline-text-2" id="text-lucien-antitoi">
<p>
The mute, simple-minded younger Antitoi brother who runs the Cambridge video-rental front with Bertraund. He is murdered by the AFR during their raid on the shop, impaled on the handle of his own broom in a famously baroque death scene.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Antitoi (French: 'against-you')</b> — The Quebecois surname literally parses as 'anti-you' in French, marking the brothers as oppositional separatist figures hostile to the O.N.A.N. order.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Antitoi (French: 'anti-you')</b> — The Quebecois surname literally parses as 'against-you' in French, marking the brothers as oppositional separatist figures hostile to the O.N.A.N. order.</li>
<li><i>[Etymology]</i> <b>Lucien from Latin lux 'light'</b> — His given name derives from 'lux' (light), an ironic gloss on a mute, simple-minded character whose inner life is obscure and whose fate is darkness.</li>
<li><i>[Translation]</i> <b>Antitoi (French: 'against-you')</b> — The Quebecois surname literally parses as 'anti-you' in French, marking the brothers as oppositional separatist figures hostile to the O.N.A.N. order.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Lucien Antitoi</b>
</p>
<ul class="org-ul">
<li><b>intuition lace</b> — the simple-minded younger Antitoi's pure intuition under the lace of muteness; suggestive.</li>
<li><b>incaution tile</b> — his incautious stewardship of the video shop the AFR raids; suggestive.</li>
<li><b>tuition clean i</b> — captures the simple-minded mute as 'tuition clean'; loose.</li>
<li><b>inutile cation</b> — the inutile (useless) cation, his impotent fate at AFR hands; suggestive.</li>
<li><b>inutile action</b> — the futile action of the broom-impaled mute; suggestive.</li>
<li><b>auction line it</b> — the video-rental shop as auction-line; loose.</li>
<li><b>intuition alec</b> — his Alec-like (innocent) intuition; loose.</li>
<li><b>uniciliate ton</b> — the uni-ciliate (single-minded) ton of muteness; loose.</li>
<li><b>tunicate liin o</b> — drop — gibberish.</li>
<li><b>ciliation tune</b> — the simple ciliation/tune of his daily routine; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Lucien Antitoi</b>
</p>
<ul class="org-ul">
<li>intuition lace</li>
<li>intuition alec</li>
<li>action unit lie</li>
<li>initial one cut</li>
<li>initial cute on</li>
<li>initial cute no</li>
<li>continue tail i</li>
<li>continue ali it</li>
<li>notice unit ali</li>
<li>initial tune co</li>
<li>auction line it</li>
<li>alice into unit</li>
<li>tunnel coat iii</li>
<li>notice until ai</li>
<li>title union cia</li>
<li>continue lit ai</li>
<li>auction neil it</li>
<li>cattle union ii</li>
<li>auction intel i</li>
<li>initial ceo nut</li>
<li>continue til ai</li>
<li>auction lie tin</li>
<li>initial cut neo</li>
<li>caution intel i</li>
<li>caution line it</li>
<li>caution neil it</li>
<li>caution lie tin</li>
<li>tuition clean i</li>
<li>tuition line ca</li>
<li>tuition line ac</li>
<li>tuition nice al</li>
<li>tuition nice la</li>
<li>tuition neil ca</li>
<li>tuition neil ac</li>
<li>tuition can lie</li>
<li>tuition lance i</li>
<li>outline cant ii</li>
<li>outline cia tin</li>
<li>action unite il</li>
<li>action unite li</li>
<li>unite tail coin</li>
<li>unite tail icon</li>
<li>continue ali ti</li>
<li>auction line ti</li>
<li>auction neil ti</li>
<li>caution line ti</li>
<li>caution neil ti</li>
<li>initial unto ce</li>
<li>tuition nail ce</li>
<li>citation lie un</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-eric-r-clipperton" class="outline-2">
<h2 id="eric-r-clipperton"><span class="section-number-2">44.</span> Eric R. Clipperton&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="tennis">tennis</span>&#xa0;<span class="glock">glock</span>&#xa0;<span class="suicide">suicide</span></span></h2>
<div class="outline-text-2" id="text-eric-r-clipperton">
<p>
A junior tennis player who wins matches by entering every tournament with a Glock pressed to his own temple, having sworn to pull the trigger if he loses, which forces opponents to throw their matches. After finally being granted an official ranking he kills himself in the headmaster's office at ETA.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>clip (magazine of bullets / to shoot)</b> — The surname embeds 'clip' — both a Glock magazine and the verb meaning to shoot/kill — directly foreshadowing the gun he holds to his temple every match.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · substring · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>clip</b> — Surname embeds 'clip' — both a magazine of bullets (his Glock) and to 'clip' someone (shoot/kill), foreshadowing the gun he holds to his temple in every match.</li>
<li><i>[Substring]</i> <b>clip (magazine of bullets / to shoot)</b> — The surname embeds 'clip' — both a Glock magazine and the verb meaning to shoot/kill — directly foreshadowing the gun he holds to his temple every match.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Eric Clipperton</b>
</p>
<ul class="org-ul">
<li><b>principle recto</b> — the upright/recto principle he hides behind to extort wins; suggestive.</li>
<li><b>petrolic pincer</b> — petrol/pincer captures his coercive Glock-pincer pressure; suggestive.</li>
<li><b>petrolic prince</b> — petrol-prince — the explosive coercive ranking; suggestive.</li>
<li><b>ecliptic perron</b> — ecliptic perron (a stair) — ascending ranking by suicide-threat; loose.</li>
<li><b>triple price con</b> — his ranking-extortion racket as a triple-price con; suggestive.</li>
<li><b>prince triple co</b> — the triple-threat (Glock, ranking, suicide) prince; loose.</li>
<li><b>proclitic preen</b> — the proclitic (depending on opponent's flinch) preening of the gun-to-head boy; loose.</li>
<li><b>triplice crepon</b> — drop — obscure.</li>
</ul>

<p>
<b>Eric R. Clipperton</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>principle rector</b> — (in algorithmic list); the principle-bound rector who finally shoots himself in the headmaster's office; strong.</li>
<li><b>incorrect ripple</b> — (in algorithmic list); the morally incorrect ripple effect of his extortion; suggestive.</li>
<li><b>circle print rope</b> — the circular round-robin print + the rope of his self-noose; suggestive.</li>
<li><b>principle cot err</b> — the cot-bound erring principle; loose.</li>
<li><b>principle roc ret</b> — the principle-bound 'roc' (predator) returning to his rec; loose.</li>
<li><b>rector clip pier n</b> — rector + clip + pier; loose.</li>
<li><b>concreter rip lip</b> — the concreter-of-rankings ripping the lip; loose.</li>
<li><b>incorrect lipper</b> — the morally incorrect Glock-lipper; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Eric Clipperton</b>
</p>
<ul class="org-ul">
<li>prince price lot</li>
<li>prince plot rice</li>
<li>circle point per</li>
<li>circle point pre</li>
<li>circle open trip</li>
<li>client price pro</li>
<li>prince plot eric</li>
<li>electric iron pp</li>
<li>circle point rep</li>
<li>principle etc or</li>
<li>principle oct re</li>
<li>prince triple co</li>
<li>electric pro pin</li>
<li>triple price con</li>
<li>triple nice corp</li>
<li>percent clip rio</li>
<li>report nice clip</li>
<li>entire corp clip</li>
<li>circle trip nope</li>
<li>principle oct er</li>
<li>report clinic ep</li>
<li>proper clinic et</li>
<li>clinic peter pro</li>
<li>triple epic corn</li>
<li>prince trip cole</li>
<li>price print cole</li>
<li>circle inter pop</li>
<li>entire clip crop</li>
<li>triple nice crop</li>
<li>correct pile pin</li>
<li>concert pile rip</li>
<li>clinic port peer</li>
<li>concrete rip lip</li>
<li>principle ore ct</li>
<li>correct pine lip</li>
<li>circle port pine</li>
<li>circle pipe torn</li>
<li>recipe clip torn</li>
<li>proper clinic te</li>
<li>price intel corp</li>
<li>price intel crop</li>
<li>electronic pr ip</li>
<li>principle ceo rt</li>
<li>electric porn ip</li>
<li>police prince rt</li>
<li>proper celtic in</li>
<li>celtic prior pen</li>
<li>perception il cr</li>
<li>perception li cr</li>
<li>principle toe cr</li>
</ul>

<p>
<b>Eric R. Clipperton</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>incorrect ripple</li>
<li>principle rector</li>
<li>report circle pin</li>
<li>electric porn rip</li>
<li>reporter clip inc</li>
<li>center prior clip</li>
<li>recent prior clip</li>
<li>centre prior clip</li>
<li>report clinic per</li>
<li>report clinic pre</li>
<li>report clinic rep</li>
<li>corner triple pic</li>
<li>electronic rip pr</li>
<li>protein circle pr</li>
<li>triple price corn</li>
<li>circle print rope</li>
<li>principle core rt</li>
<li>proper circle tin</li>
<li>circle porter pin</li>
<li>clinic porter per</li>
<li>clinic porter pre</li>
<li>clinic porter rep</li>
<li>terror pencil pic</li>
<li>incorrect pile pr</li>
<li>incorrect prep il</li>
<li>incorrect prep li</li>
<li>incorrect per lip</li>
<li>incorrect pre lip</li>
<li>incorrect rep lip</li>
<li>printer police cr</li>
<li>printer circle op</li>
<li>printer circle po</li>
<li>printer price col</li>
<li>printer core clip</li>
<li>circle prone trip</li>
<li>incorrect pier pl</li>
<li>incorrect pier lp</li>
<li>circle intro prep</li>
<li>proper circle int</li>
<li>circle inter prop</li>
<li>principle core tr</li>
<li>prince retro clip</li>
<li>clinic retro prep</li>
<li>principle tore cr</li>
<li>correct piper lin</li>
<li>circle piper torn</li>
<li>electronic rip rp</li>
<li>incorrect pile rp</li>
<li>incorrect ripe pl</li>
<li>incorrect ripe lp</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-lady-delphina" class="outline-2">
<h2 id="lady-delphina"><span class="section-number-2">45.</span> Lady Delphina&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="dealer">dealer</span>&#xa0;<span class="supplier">supplier</span>&#xa0;<span class="peripheral">peripheral</span></span></h2>
<div class="outline-text-2" id="text-lady-delphina">
<p>
The drug supplier from whom Ken Erdedy's marijuana hookup is supposed to acquire the pound he's waiting for.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Delphi / Pythia</b> — 'Delphina' evokes the Delphic oracle whose pronouncements were anxiously awaited by supplicants — exactly the position of the unseen drug supplier on whose word Erdedy hangs.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Delphi / Pythia</b> — 'Delphina' evokes the Delphic oracle, whose pronouncements were sought by supplicants — fitting for an unseen drug supplier on whose word everyone anxiously waits.</li>
<li><i>[Etymology]</i> <b>Greek delphis 'dolphin' / delphys 'womb'</b> — The root 'delph-' means both dolphin and womb, suggesting a maternal source-figure who delivers (or withholds) sustenance to her dependents.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Delphina</b>
</p>
<ul class="org-ul">
<li><b>delphian</b> — the dealer cast as Delphic oracle of the pot supply chain; strong.</li>
<li><b>hand pile</b> — the pound-of-pot pile changing hands; suggestive.</li>
<li><b>plan hide</b> — her supplier's plan-and-hide modus; suggestive.</li>
<li><b>panel hid</b> — the hidden panel of the dealer network; loose.</li>
<li><b>phial den</b> — the dealer's phial in her den; loose.</li>
<li><b>held pain</b> — the supplier holds the pain of the addict's wait; suggestive.</li>
<li><b>hide plan</b> — her literal hide-the-stash plan; loose.</li>
<li><b>pedal hin</b> — drop — obscure; loose.</li>
<li><b>lined hap</b> — drop — obscure.</li>
<li><b>plead hin</b> — the addict's plea aimed her way; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Delphina</b>
</p>
<ul class="org-ul">
<li>held pain</li>
<li>plan hide</li>
<li>dean phil</li>
<li>delhi pan</li>
<li>hand pile</li>
<li>daniel hp</li>
<li>alien phd</li>
<li>handle ip</li>
<li>denial hp</li>
<li>daniel ph</li>
<li>denial ph</li>
<li>handle pi</li>
<li>delhi nap</li>
<li>plane hid</li>
<li>panel hid</li>
<li>nepal hid</li>
<li>nailed hp</li>
<li>nailed ph</li>
<li>laden hip</li>
<li>alpine hd</li>
<li>laden phi</li>
<li>pale hind</li>
<li>plea hind</li>
<li>leap hind</li>
<li>hailed np</li>
<li>phil dane</li>
<li>penal hid</li>
<li>help dani</li>
<li>piled nah</li>
<li>piled han</li>
<li>help and i</li>
<li>alpine dh</li>
<li>pedal nih</li>
<li>plead nih</li>
<li>plaid hen</li>
<li>phil edna</li>
<li>daphne il</li>
<li>daphne li</li>
<li>inhale pd</li>
<li>inhale dp</li>
<li>alden hip</li>
<li>alden phi</li>
<li>hilda pen</li>
<li>dinah epl</li>
<li>pine dahl</li>
<li>phil aden</li>
<li>help dina</li>
<li>piled ahn</li>
<li>lined hap</li>
<li>help andi</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-ken-erdedy" class="outline-2">
<h2 id="ken-erdedy"><span class="section-number-2">46.</span> Ken Erdedy&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="marijuana">marijuana</span>&#xa0;<span class="adexec">adexec</span>&#xa0;<span class="waiting">waiting</span></span></h2>
<div class="outline-text-2" id="text-ken-erdedy">
<p>
A marijuana-addicted advertising executive whose opening sequence has him waiting in agonized obsessive paralysis for a woman to deliver a pound of pot, planning the binge that will finally cure him of pot. He later turns up at Ennet House as a resident, a study in the bourgeois face of addiction.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>keyed nerd</b> — Captures the bourgeois ad-exec's keyed-up obsessive paralysis as he waits for the pound of pot — both nerd and amped-up.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>ur-deedy / ur-deedee</b> — The surname's phonetic shape suggests a stuttering, repetitive 'deedy-deedy' — apt for a character paralyzed in an obsessive loop of waiting and planning.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Ken Erdedy</b>
</p>
<ul class="org-ul">
<li><b>derek deny</b> — (in algorithmic list); his obsessive denial of his pot habit, projected onto a 'Derek'; suggestive.</li>
<li><b>keyed nerd</b> — the keyed-up bourgeois nerd-junkie waiting for the pot; strong.</li>
<li><b>eyed nerd k</b> — the eyed-the-clock nerd Erdedy; suggestive.</li>
<li><b>redden key</b> — the addict's reddening key-up before his binge; suggestive.</li>
<li><b>kneed dyer</b> — (in algorithmic list); kneed (humbled) dyer waiting for delivery; loose.</li>
<li><b>eyed ked rn</b> — the eyed/cataloguing ad-exec; loose.</li>
<li><b>deeny k red</b> — loose; the named-Derek/Deeny catalogue of waits; loose.</li>
<li><b>dyed reek n</b> — the dyed-reek of his ad-exec apartment; loose.</li>
<li><b>derek dy ne</b> — loose; Derek-cataloguing self-naming; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Ken Erdedy</b>
</p>
<ul class="org-ul">
<li>derek deny</li>
<li>reddy knee</li>
<li>reddy keen</li>
<li>need key dr</li>
<li>end red key</li>
<li>knee dry de</li>
<li>knee dry ed</li>
<li>end key der</li>
<li>keen dry de</li>
<li>keen dry ed</li>
<li>need key rd</li>
<li>derek de ny</li>
<li>derek ed ny</li>
<li>red key den</li>
<li>key der den</li>
<li>eyed ken dr</li>
<li>eyed ken rd</li>
<li>dry ken dee</li>
<li>knee dye dr</li>
<li>knee dye rd</li>
<li>keen dye dr</li>
<li>keen dye rd</li>
<li>eden key dr</li>
<li>eden key rd</li>
<li>red ken dye</li>
<li>ken der dye</li>
<li>ended re ky</li>
<li>ended er ky</li>
<li>need red ky</li>
<li>need der ky</li>
<li>deer end ky</li>
<li>deer den ky</li>
<li>reed end ky</li>
<li>reed den ky</li>
<li>eden red ky</li>
<li>eden der ky</li>
<li>deed ken yr</li>
<li>derek ye nd</li>
<li>deer key nd</li>
<li>deer ned ky</li>
<li>reed key nd</li>
<li>reed ned ky</li>
<li>deed key rn</li>
<li>red key ned</li>
<li>key der ned</li>
<li>knee rey dd</li>
<li>keen rey dd</li>
<li>nerd key de</li>
<li>nerd key ed</li>
<li>nerd dee ky</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-kyle-d-coyle" class="outline-2">
<h2 id="kyle-d-coyle"><span class="section-number-2">47.</span> Kyle D. Coyle&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="eschaton">eschaton</span>&#xa0;<span class="junior">junior</span></span></h2>
<div class="outline-text-2" id="text-kyle-d-coyle">
<p>
An ETA student and Eschaton participant, generally part of the older-junior cohort.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Kyle / Coyle rhyme</b> — Given name and surname rhyme so tightly that the doubling flattens him into a sing-songy, easily-overlooked junior — the form enacts his minor status.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · suggestive · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Suggestive]</i> <b>Kyle / Coyle rhyme</b> — Given name and surname rhyme tightly, producing a sing-songy doubling that flattens him into a minor, easily-overlooked junior in the cohort.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Kyle Coyle</b>
</p>
<ul class="org-ul">
<li><b>coyly leek</b> — the coyly leeking (sneaking) junior ETA player; loose.</li>
<li><b>coyly keel</b> — the coyly keeling (toppling) Eschaton-cohort kid; loose.</li>
<li><b>leeky cloy</b> — the cloying leeky cohort comedy; loose.</li>
<li><b>yockel ley</b> — the cohort yokel on the ley-line of Eschaton; loose.</li>
<li><b>key col lye</b> — fragmentary cohort filler; very loose.</li>
<li><b>elk ley coy</b> — (in algorithmic list); the coy ETA-elk; loose. Form (Y/L/E heavy with K and C) is largely intractable.</li>
<li><b>lock eye ly</b> — (in algorithmic list); his locked-eye junior demeanor; loose.</li>
<li><b>cleeky loy</b> — (in algorithmic list); a cleeky-loy junior; loose.</li>
</ul>

<p>
<b>Kyle Dempsy Coyle</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>comedy skype yell</b> — (in algorithmic list); juvenile cohort's comedy-Skype yell; strong.</li>
<li><b>comedy skype lyle</b> — (in algorithmic list); his cohort's comedy aimed at Lyle; loose.</li>
<li><b>comedy kelley spy</b> — (in algorithmic list); a Kelley-spy comedy among the juniors; loose.</li>
<li><b>comedy pesky yell</b> — (in algorithmic list); the pesky-yell comedy of the older-junior cohort; suggestive.</li>
<li><b>skelly comedy yep</b> — (in algorithmic list); the skelly comedy-assent of a junior; loose.</li>
<li><b>comply seedy kyle</b> — (in algorithmic list); his cohort's seedy compliance with Eschaton rules; loose.</li>
<li><b>employ clyde keys</b> — (in algorithmic list); his cohort employs Clyde-style keys to the game; loose.</li>
<li><b>employ clyde skye</b> — (in algorithmic list); the Clyde-and-Skye Scotch image for a cohort joke; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Kyle Coyle</b>
</p>
<ul class="org-ul">
<li>kelley coy</li>
<li>cell key yo</li>
<li>kelly co ye</li>
<li>kyle col ye</li>
<li>yell key co</li>
<li>kelly yo ce</li>
<li>yell ceo ky</li>
<li>yell eco ky</li>
<li>kelly yo ec</li>
<li>kelly ye oc</li>
<li>yell key oc</li>
<li>locke ye ly</li>
<li>lock eye ly</li>
<li>cole key ly</li>
<li>kyle ceo ly</li>
<li>kyle eco ly</li>
<li>kyle leo cy</li>
<li>kyle ole cy</li>
<li>lyle key co</li>
<li>lyle key oc</li>
<li>lyle ceo ky</li>
<li>lyle eco ky</li>
<li>kelly co ey</li>
<li>kelly oc ey</li>
<li>locke ly ey</li>
<li>cello ye ky</li>
<li>cello ky ey</li>
<li>kyle col ey</li>
<li>lock yee ly</li>
<li>kyle loc ye</li>
<li>kyle loc ey</li>
<li>lock ely ye</li>
<li>lock ely ey</li>
<li>cole ely ky</li>
<li>kyle ely co</li>
<li>kyle ely oc</li>
<li>coke ely ly</li>
<li>key col ely</li>
<li>key loc ely</li>
<li>kyle coe ly</li>
<li>yell coe ky</li>
<li>lyle coe ky</li>
<li>kelly ce oy</li>
<li>kelly ec oy</li>
<li>cell key oy</li>
<li>kelly cy eo</li>
<li>kyle coy el</li>
<li>kyle coy le</li>
<li>yell coy ke</li>
<li>yell coy ek</li>
</ul>

<p>
<b>Kyle Dempsy Coyle</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>employs clyde key</li>
<li>employ clyde keys</li>
<li>comedy skype yell</li>
<li>comedy kelley spy</li>
<li>employ clyde skye</li>
<li>comedy skype lyle</li>
<li>comply kelsey dye</li>
<li>smokey clyde yelp</li>
<li>comedy kelsey ply</li>
<li>dempsey kelly coy</li>
<li>comedy pesky yell</li>
<li>comedy pesky lyle</li>
<li>comply kelsey dey</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-barry-loach" class="outline-2">
<h2 id="barry-loach"><span class="section-number-2">48.</span> Barry Loach&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="trainer">trainer</span>&#xa0;<span class="handshake">handshake</span>&#xa0;<span class="backstory">backstory</span></span></h2>
<div class="outline-text-2" id="text-barry-loach">
<p>
ETA's Head Trainer, whose backstory involves a crisis-of-faith Jesuit brother and a long stint of begging strangers to touch his hand on a Boston subway platform until Mario Incandenza stopped and shook it. The story explains his loyalty to the Incandenzas and his low-key sanctity.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>loach (bottom-feeder fish) / leech</b> — The surname names a bottom-dwelling fish and echoes 'leech,' fitting his backstory of literally reaching out from the bottom — a subway-platform supplicant begging for human touch.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>loach (bottom-feeder fish) / leech</b> — The surname names a bottom-dwelling fish and echoes 'leech,' fitting his story of literally reaching out from the bottom — a subway-platform supplicant begging for human touch.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Barry Loach</b>
</p>
<ul class="org-ul">
<li><b>harbor clay</b> — (in algorithmic list); Loach as harbor of clay (kid trainees); his sanctifying patience; suggestive.</li>
<li><b>harbor lacy</b> — (in algorithmic list); his lacy harbor of broken bodies; loose.</li>
<li><b>broach lyra</b> — broaches the lyric of his own backstory; loose.</li>
<li><b>archly boar</b> — his Jesuit-brother archly Boston-blue-collar persona; loose.</li>
<li><b>choral bray</b> — (in algorithmic list); the choral bray of his subway-platform plea for a handshake; suggestive.</li>
<li><b>labor cry ah</b> — (in algorithmic list); the trainer's labor-cry; loose.</li>
<li><b>abhor acryl</b> — drop — gibberish.</li>
<li><b>carboy harl</b> — drop — obscure.</li>
<li><b>chola barry</b> — (in algorithmic list); his chola/Barry duality; loose.</li>
<li><b>royal brach</b> — drop — obscure 'brach'.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Barry Loach</b>
</p>
<ul class="org-ul">
<li>harbor clay</li>
<li>baylor arch</li>
<li>baylor char</li>
<li>harbor lacy</li>
<li>choral bray</li>
<li>holy car bar</li>
<li>labor cry ah</li>
<li>labor cry ha</li>
<li>carry lab oh</li>
<li>harry lab co</li>
<li>larry abc oh</li>
<li>carry lab ho</li>
<li>larry abc ho</li>
<li>holy bar arc</li>
<li>royal abc hr</li>
<li>harry abc lo</li>
<li>carol bay hr</li>
<li>oral arch by</li>
<li>arch lay bro</li>
<li>arch lay rob</li>
<li>arch lab roy</li>
<li>barry cal oh</li>
<li>barry cal ho</li>
<li>harry cal bo</li>
<li>holy arab cr</li>
<li>harry abc ol</li>
<li>royal bar ch</li>
<li>royal cab hr</li>
<li>labor ray ch</li>
<li>harry cab lo</li>
<li>harry cab ol</li>
<li>harry col ba</li>
<li>harry col ab</li>
<li>larry cab oh</li>
<li>larry cab ho</li>
<li>barry col ah</li>
<li>barry col ha</li>
<li>labor hay cr</li>
<li>carl bro hay</li>
<li>carl rob hay</li>
<li>carol hay br</li>
<li>blah car roy</li>
<li>blah roy arc</li>
<li>coral bay hr</li>
<li>coral hay br</li>
<li>harbor ya cl</li>
<li>royal bra ch</li>
<li>holy car bra</li>
<li>holy arc bra</li>
<li>clara boy hr</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-trent-kite" class="outline-2">
<h2 id="trent-kite"><span class="section-number-2">49.</span> Trent Kite&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="minor">minor</span>&#xa0;<span class="nickname">nickname</span></span></h2>
<div class="outline-text-2" id="text-trent-kite">
<p>
A minor ETA-adjacent figure nicknamed Quo Vadis, sparsely sketched in the novel.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Quo Vadis (Peter to the risen Christ)</b> — His nickname references the Latin 'Quo vadis?' — 'where are you going?' — a grandly literary tag comically inflating an otherwise barely-sketched ETA figure.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Quo Vadis (Henryk Sienkiewicz / Peter)</b> — His nickname references the Latin 'Quo vadis?' ('Where are you going?'), the question Peter asks the risen Christ — a grandly literary tag for an otherwise obscure ETA figure, comically inflating his minor presence.</li>
<li><i>[Allusion]</i> <b>Quo Vadis (Peter to the risen Christ)</b> — His nickname references the Latin 'Quo vadis?' — 'where are you going?' — a grandly literary tag comically inflating an otherwise barely-sketched ETA figure.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Trent Kite</b>
</p>
<ul class="org-ul">
<li><b>kitten ret</b> — (in algorithmic list); a kitten retreating — minor sketched figure; loose.</li>
<li><b>trike tent</b> — (in algorithmic list); kid-vehicle (trike) under the Eschaton tent; loose.</li>
<li><b>kitten tre</b> — (in algorithmic list); junior cohort kitten; loose.</li>
<li><b>trent kite</b> — self-referential — the 'Quo Vadis' Kite is barely sketched; loose.</li>
<li><b>tier ken tt</b> — (in algorithmic list); a tier-of-ranks Ken; loose.</li>
<li><b>knit tee rt</b> — his minor figure 'knit tee' on the periphery; loose.</li>
<li><b>rent kit et</b> — (in algorithmic list); his rented kit; loose.</li>
<li><b>tent trek i</b> — (in algorithmic list); his Eschaton tent trek; loose.</li>
</ul>

<p>
<b>Trent "Quo Vadis" Kite</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>advertise knot quit</b> — (in algorithmic list); minor figure's quit/knot existence + the 'Quo vadis?' echo; loose.</li>
<li><b>totaquine kits verd</b> — drop — obscure.</li>
<li><b>quartette void skin</b> — the Quo Vadis ('whither?') captured as quartette in a void; suggestive.</li>
<li><b>tetraonid quet skiv</b> — drop — obscure.</li>
<li><b>advertise tonk quit</b> — drop — obscure.</li>
<li><b>quintetto vade kris</b> — the Latin nickname Quo Vadis + 'vade' (go); his 'where are you going' shading; suggestive.</li>
<li><b>drakonite vest quit</b> — drop — obscure.</li>
<li><b>varietist kend quot</b> — drop — obscure.</li>
<li><b>diversion takt quet</b> — drop — obscure.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Trent Kite</b>
</p>
<ul class="org-ul">
<li>trent kite</li>
<li>kitten ter</li>
<li>kitten ret</li>
<li>kitten tre</li>
<li>rent kit et</li>
<li>rent kit te</li>
<li>tent kit re</li>
<li>tent kit er</li>
<li>teen kit rt</li>
<li>kent tie rt</li>
<li>tent trek i</li>
<li>trek ten it</li>
<li>trek net it</li>
<li>trek tin et</li>
<li>trek tin te</li>
<li>trek ten ti</li>
<li>trek net ti</li>
<li>trent et ki</li>
<li>trent te ki</li>
<li>trek int et</li>
<li>trek int te</li>
<li>tree kit nt</li>
<li>trek tie nt</li>
<li>teen kit tr</li>
<li>kent tie tr</li>
<li>tree kit tn</li>
<li>kent tri et</li>
<li>kent tri te</li>
<li>trek tie tn</li>
<li>knit tee rt</li>
<li>knit tee tr</li>
<li>enter ki tt</li>
<li>tree ink tt</li>
<li>tree kin tt</li>
<li>knee tri tt</li>
<li>tier ken tt</li>
<li>keen tri tt</li>
<li>tire ken tt</li>
<li>erik ten tt</li>
<li>erik net tt</li>
<li>rite ken tt</li>
<li>kite ten rt</li>
<li>kite ten tr</li>
<li>kite net rt</li>
<li>kite net tr</li>
<li>inter tt ke</li>
<li>trent it ke</li>
<li>trent ti ke</li>
<li>tent tri ke</li>
<li>rent tit ke</li>
</ul>

<p>
<b>Trent "Quo Vadis" Kite</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>artist quoted kevin</li>
<li>invited quote stark</li>
<li>invite quoted stark</li>
<li>native quoted skirt</li>
<li>quoted traits kevin</li>
<li>antique voted skirt</li>
<li>interest vodka quit</li>
<li>quoted knives trait</li>
<li>advertise quit knot</li>
<li>invited torque task</li>
<li>visited torque tank</li>
<li>torque kitten davis</li>
<li>quoted strait kevin</li>
<li>renovated quit kits</li>
<li>quartet videos knit</li>
<li>quartet soviet kind</li>
<li>evident skirt quota</li>
<li>drives kitten quota</li>
<li>kitten divers quota</li>
<li>kristen quoted vita</li>
<li>revoked titans quit</li>
<li>quantities voter kd</li>
<li>antiques viktor ted</li>
<li>antiques viktor edt</li>
<li>detroit kevin squat</li>
<li>transit quoted kiev</li>
<li>distant torque kiev</li>
<li>kittens torque avid</li>
<li>kittens drive quota</li>
<li>question advert kit</li>
<li>quartet video stink</li>
<li>quoted tanker visit</li>
<li>question divert kat</li>
<li>inverted quota kits</li>
<li>dentist quake vitro</li>
<li>kittens torque diva</li>
<li>kittens quota diver</li>
<li>renovated quits kit</li>
<li>revoked titan quits</li>
<li>antiques viktor det</li>
<li>tristan quoted kiev</li>
<li>revisit quoted tank</li>
<li>quantities voter dk</li>
<li>kittens devout iraq</li>
<li>quieter vodka stint</li>
<li>inverted quotas kit</li>
<li>started invoke quit</li>
<li>invited quotas trek</li>
<li>quartet invoked its</li>
<li>quartet invoked sit
<i>Search timed out at 45s with 218 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-lateral-alice-moore" class="outline-2">
<h2 id="lateral-alice-moore"><span class="section-number-2">50.</span> Lateral Alice Moore&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="secretary">secretary</span>&#xa0;<span class="lateral">lateral</span>&#xa0;<span class="wheeled">wheeled</span></span></h2>
<div class="outline-text-2" id="text-lateral-alice-moore">
<p>
ETA's administrative secretary, who navigates the office exclusively in lateral motion due to a back injury, gliding sideways on a wheeled chair. Her sideways locomotion is one of the academy's running visual gags.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>lateral (Latin lateralis 'of the side')</b> — Her epithet is precisely literal — from Latin 'latus' (side) — describing a secretary who only moves sideways on her wheeled chair.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · etymology · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>lateral (Latin lateralis 'of the side')</b> — Her epithet 'Lateral' is precisely literal — from Latin 'latus' ('side') — describing a secretary who only moves sideways on her wheeled chair.</li>
<li><i>[Phonetic]</i> <b>Moore ~ 'more'</b> — The surname puns on 'more,' so 'Alice Moore' suggests the always-more administrative paperwork that flows through her sideways-gliding station.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Lateral Moore</b>
</p>
<ul class="org-ul">
<li><b>lateral romeo</b> — (in algorithmic list); her sideways gliding gives a lateral-Romeo cameo; suggestive.</li>
<li><b>lateral moore</b> — self-referential, but the algorithmic match captures her aptronym; strong.</li>
<li><b>morella orate</b> — her cherry-pit (morella) orations from the wheeled chair; loose.</li>
<li><b>morello rate a</b> — drop — too cherry-shop.</li>
<li><b>real room late</b> — (in algorithmic list); her real-room-late office life; loose.</li>
<li><b>molar relate o</b> — the secretary as the office's molar (grinding tooth) of relating; loose.</li>
<li><b>remote oral al</b> — (in algorithmic list); her remote-oral office persona; loose.</li>
<li><b>alarm root lee</b> — (in algorithmic list); her root-alarm filing system; loose.</li>
</ul>

<p>
<b>"Lateral" Alice Moore</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>ameliorate cellar o</b> — the secretary who ameliorates callers' inquiries while gliding sideways; strong.</li>
<li><b>ameliorate recall o</b> — the secretary's recall function — she ameliorates recall; suggestive.</li>
<li><b>lateral alice romeo</b> — (in algorithmic list); restated aptronym + Romeo for her glide; suggestive.</li>
<li><b>morcellate aerial o</b> — drop — obscure.</li>
<li><b>reallocate mailer o</b> — the secretary as a re-allocator and mailer; suggestive.</li>
<li><b>metacoelia roller a</b> — the wheeled-chair (roller) office secretary as 'metacoelia'; loose.</li>
<li><b>lamellaria cerote o</b> — drop — obscure.</li>
<li><b>reallocate moiler a</b> — drop — obscure 'moiler'.</li>
<li><b>ameliorate caller o</b> — the secretary's ameliorating reception of callers; strong.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Lateral Moore</b>
</p>
<ul class="org-ul">
<li>lateral moore</li>
<li>lateral romeo</li>
<li>tell area room</li>
<li>real room late</li>
<li>male rare tool</li>
<li>rare tool meal</li>
<li>moral role eat</li>
<li>moral role tea</li>
<li>male tool rear</li>
<li>tool meal rear</li>
<li>real male root</li>
<li>real meal root</li>
<li>moral role ate</li>
<li>real room tale</li>
<li>later moore al</li>
<li>later moore la</li>
<li>alert moore al</li>
<li>alert moore la</li>
<li>alarm root lee</li>
<li>moore rate all</li>
<li>moore tall are</li>
<li>moore tall era</li>
<li>moore tall ear</li>
<li>moore tear all</li>
<li>role rate lmao</li>
<li>role tear lmao</li>
<li>remote oral al</li>
<li>remote oral la</li>
<li>more late oral</li>
<li>more tale oral</li>
<li>team role oral</li>
<li>late rome oral</li>
<li>role meat oral</li>
<li>role mate oral</li>
<li>tree lmao oral</li>
<li>rome tale oral</li>
<li>relate room al</li>
<li>relate room la</li>
<li>relate lmao or</li>
<li>relate oral mo</li>
<li>moral rate leo</li>
<li>moral tear leo</li>
<li>metro area lol</li>
<li>room late earl</li>
<li>room tale earl</li>
<li>male root earl</li>
<li>meal root earl</li>
<li>later lmao ore</li>
<li>metal oral ore</li>
<li>moral late ore</li>
</ul>

<p>
<b>"Lateral" Alice Moore</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>electoral maria leo</li>
<li>earlier locate lmao</li>
<li>electoral aerial mo</li>
<li>remote aerial local</li>
<li>cooler aerial metal</li>
<li>lateral moore alice</li>
<li>electoral maria ole</li>
<li>lateral alice romeo</li>
<li>material oracle leo</li>
<li>material oracle ole</li>
<li>electoral mario ale</li>
<li>material cooler ale</li>
<li>electoral morale ai</li>
<li>electoral morale ia</li>
<li>collateral mario ee</li>
<li>aerial oracle motel</li>
<li>calorie relate lmao</li>
<li>calorie morale late</li>
<li>calorie morale tale</li>
<li>malaria electro leo</li>
<li>malaria electro ole</li>
<li>electro aerial lmao</li>
<li>telecom aerial oral</li>
<li>collateral erie mao</li>
<li>electoral mario lea</li>
<li>material cooler lea</li>
<li>electoral area milo</li>
<li>electoral aerial om</li>
<li>electoral amelia or</li>
<li>electoral amelia ro</li>
<li>clearer amelia tool</li>
<li>electro amelia oral</li>
<li>relate amelia color</li>
<li>cooler amelia later</li>
<li>cooler amelia alert</li>
<li>cooler amelia alter</li>
<li>aerial meteor local</li>
<li>electoral ariel mao</li>
<li>locate morale ariel</li>
<li>lateral calorie moe</li>
<li>clearer amelia loot</li>
<li>retailer cameo lola</li>
<li>relocate morale ali</li>
<li>relocate email oral</li>
<li>relocate mario ella</li>
<li>relocate marie lola</li>
<li>relocate ariel lmao</li>
<li>croatia morale elle</li>
<li>electoral mole aria</li>
<li>earlier oatmeal col</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-thierry-poutrincourt" class="outline-2">
<h2 id="thierry-poutrincourt"><span class="section-number-2">51.</span> Thierry Poutrincourt&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="prorector">prorector</span>&#xa0;<span class="quebecois">quebecois</span>&#xa0;<span class="history">history</span></span></h2>
<div class="outline-text-2" id="text-thierry-poutrincourt">
<p>
A Quebecois prorector at ETA who teaches Canadian history and quietly observes the geopolitics of Subsidized Time. She has a confidential conversation with Avril about the political stakes around the Incandenza family.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Jean de Poutrincourt (Acadian colonist)</b> — She shares a surname with a 17th-century French colonist of Acadia — an apt pedigree for a Quebecois history teacher quietly tracking the Canadian–American political stakes.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Jean de Poutrincourt</b> — Shares a surname with Jean de Biencourt de Poutrincourt et de Saint-Just, a 17th-century French colonist of Acadia — an apt pedigree for a Quebecois history teacher quietly tracking the Canadian–American political stakes.</li>
<li><i>[Allusion]</i> <b>Jean de Poutrincourt (Acadian colonist)</b> — She shares a surname with a 17th-century French colonist of Acadia — an apt pedigree for a Quebecois history teacher quietly tracking the Canadian–American political stakes.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Thierry Poutrincourt</b>
</p>
<ul class="org-ul">
<li><b>interrupt cohort yuri</b> — (in algorithmic list); the interrupting cohort (her Quebec colleagues) — suggestive of her geopolitical role; suggestive.</li>
<li><b>irruption couth retry</b> — her couth/quiet retry on Quebec geopolitics inside ETA; suggestive.</li>
<li><b>irruption corey truth</b> — the irrupting truth of the Subsidized-Time politics; loose.</li>
<li><b>irruption recto hurty</b> — drop — obscure.</li>
<li><b>neotropic hurri rutty</b> — drop — obscure.</li>
<li><b>irruption thuoc retry</b> — drop — gibberish 'thuoc'.</li>
<li><b>nutritory pouch tirer</b> — drop — obscure.</li>
<li><b>nutritory route chirp</b> — the prorector's nourishing route + chirp aside on geopolitics; loose.</li>
</ul>

<p>
<b>Mlle. Thierry Poutrincourt</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>premonitorily lurch utter</b> — her premonitory utterance to Avril about the Incandenza geopolitics; suggestive.</li>
<li><b>premonitorily truth ulcer</b> — her quiet premonitory truths that ulcerate the Incandenza family stakes; suggestive.</li>
<li><b>premonitorily truth cruel</b> — her cruel premonitory truths about Subsidized Time politics; suggestive.</li>
<li><b>premonitorily lucre truth</b> — the lucre-truth of geopolitics she sees clearly; suggestive.</li>
<li><b>lithotriptor murrey uncle</b> — drop — names.</li>
<li><b>interlocutory thurm plier</b> — drop — obscure.</li>
<li><b>lithotriptor lyceum runer</b> — drop — obscure.</li>
<li><b>competitioner hurry trull</b> — drop — obscure.</li>
<li><b>interlocutory mirth puler</b> — the interlocutory mirth + puler image of her quietly-observing role; suggestive.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Thierry Poutrincourt</b>
</p>
<ul class="org-ul">
<li>priority counter hurt</li>
<li>priority return touch</li>
<li>priority hunter court</li>
<li>priority turner touch</li>
<li>priority current thou</li>
<li>priority counter ruth</li>
<li>priority counter thru</li>
<li>corruption thirty rue</li>
<li>corruption thirty eur</li>
<li>priority untrue torch</li>
<li>interrupt cohort yuri</li>
<li>priority turnout cher</li>
<li>recruit purity horton</li>
<li>priority torture chun</li>
<li>territory pouch turin</li>
<li>priority recount hurt</li>
<li>priority recount ruth</li>
<li>priority recount thru</li>
<li>corruption hitter ryu</li>
<li>priority cutter huron</li>
<li>turnout ritchie pryor</li>
<li>corruption thierry tu</li>
<li>corruption thierry ut</li>
<li>neurotic priory truth</li>
<li>recruit hutton priory</li>
<li>turnout thrice priory</li>
<li>corruption rhett yuri</li>
<li>corinth purity router</li>
</ul>

<p>
<b>Mlle. Thierry Poutrincourt</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>corruption mueller thirty</li>
<li>currently lithium trooper</li>
<li>hypocrite turnout merrill</li>
<li>recruiter multiply horton</li>
<li>horticulture milton perry</li>
<li>horticulture morley print</li>
<li>horticulture importer lyn
<i>Search timed out at 45s with 7 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-charlotte-treat" class="outline-2">
<h2 id="charlotte-treat"><span class="section-number-2">52.</span> Charlotte Treat&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="ennet">ennet</span>&#xa0;<span class="exprostitute">exprostitute</span>&#xa0;<span class="knitting">knitting</span></span></h2>
<div class="outline-text-2" id="text-charlotte-treat">
<p>
An Ennet House resident, a reformed prostitute and recovering IV drug user who knits compulsively and bears the marks of her past on her body. She is part of the chorus of residents whose sharings populate the house's group life.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>treat (sexual favor / sweet)</b> — The surname doubles as sex-worker slang and innocent candy/reward, capturing exactly the gap between her past as a prostitute and her present in recovery.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>treat (sexual favor / sweet)</b> — The surname doubles as the slang 'treat' for a sex-worker's service and the innocent 'treat' (candy/reward), capturing the gap between her past as a prostitute and her present in recovery.</li>
<li><i>[Phonetic]</i> <b>treat (medical)</b> — 'Treat' also evokes medical treatment — fitting for an IV drug user in recovery whose body bears the marks of disease and whose presence in the house is itself a course of treatment.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Charlotte Treat</b>
</p>
<ul class="org-ul">
<li><b>throatlet cater</b> — the IV-scarred ex-prostitute's throatlet, suggestive of body marks; suggestive.</li>
<li><b>throatlet crate</b> — the throatlet/crate image of her past; loose.</li>
<li><b>trochlea tatter</b> — her tattered (knitted) trochlea — knitting compulsively; suggestive.</li>
<li><b>chatterer lotta</b> — the chattering knitter of group meetings; suggestive.</li>
<li><b>chatterer total</b> — her total chatter at Ennet meetings; suggestive.</li>
<li><b>altercate troth</b> — her altercating troth (vow) at the meetings; suggestive.</li>
<li><b>throatlet react</b> — her reactive throatlet/voice; loose.</li>
<li><b>charlotte treat</b> — self-restated; the algorithmic identity reading; loose.</li>
<li><b>throatlet creat</b> — drop — obscure.</li>
<li><b>chatter loretta</b> — (in algorithmic list); her chatter under the name Loretta; suggestive.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Charlotte Treat</b>
</p>
<ul class="org-ul">
<li>charlotte treat</li>
<li>athlete tractor</li>
<li>chatter loretta</li>
<li>loretta ratchet</li>
<li>rather total etc</li>
<li>threat actor let</li>
<li>threat latter co</li>
<li>latter other act</li>
<li>latter other cat</li>
<li>latter actor the</li>
<li>latter that core</li>
<li>letter actor hat</li>
<li>total chart tree</li>
<li>threat later oct</li>
<li>latter heart oct</li>
<li>latter earth oct</li>
<li>carter total the</li>
<li>threat alert oct</li>
<li>letter throat ca</li>
<li>throat later etc</li>
<li>throat alert etc</li>
<li>letter throat ac</li>
<li>charter total et</li>
<li>charlotte art et</li>
<li>creator that let</li>
<li>attract other el</li>
<li>attract other le</li>
<li>attract hotel re</li>
<li>attract hotel er</li>
<li>attract here lot</li>
<li>attract role the</li>
<li>attract hero let</li>
<li>threat trace lot</li>
<li>latter trace hot</li>
<li>latter trace tho</li>
<li>throat trace let</li>
<li>charlotte rat et</li>
<li>threat react lot</li>
<li>latter react hot</li>
<li>latter react tho</li>
<li>throat react let</li>
<li>theatre actor lt</li>
<li>theater actor lt</li>
<li>rather cattle to</li>
<li>create throat lt</li>
<li>threat cattle or</li>
<li>throat cattle re</li>
<li>throat cattle er</li>
<li>cattle other art</li>
<li>cattle other rat</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-p-tom-veals" class="outline-2">
<h2 id="p-tom-veals"><span class="section-number-2">53.</span> P. Tom Veals&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="adman">adman</span>&#xa0;<span class="vineyandveals">vineyandveals</span>&#xa0;<span class="cynic">cynic</span></span></h2>
<div class="outline-text-2" id="text-p-tom-veals">
<p>
The harder-edged partner in the Viney and Veals advertising firm, a cynical adman whose work surfaces in the novel's ad-industry margins.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>P. Tom ~ Peeping Tom</b> — Initial-plus-name reads as 'P. Tom,' glancing at 'Peeping Tom' — apt for an adman whose trade is voyeuristic surveillance of consumer desire.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>veal (young calf slaughtered for meat)</b> — The surname evokes 'veal' — flesh of a confined young animal — a fittingly cynical signature for a hard-edged adman who treats audiences as livestock to be processed.</li>
<li><i>[Entendre]</i> <b>P. Tom (peeping Tom)</b> — Initial-plus-name reads as 'P. Tom,' glancing at 'Peeping Tom' — apt for an adman whose trade is voyeuristic surveillance of consumer desire.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>P. Veals</b>
</p>
<ul class="org-ul">
<li><b>vespal</b> — single-word vespal echoes V&amp;V firm and wasp-stinging cynicism; loose.</li>
<li><b>sale vp</b> — the cynical adman as VP of Sales; suggestive.</li>
<li><b>save lp</b> — the adman as ad-jingle LP saver; loose.</li>
<li><b>slap ve</b> — his cynical adman slap; loose.</li>
<li><b>leap vs</b> — his leap-vs-look adman MO; loose.</li>
<li><b>pale vs</b> — (in algorithmic list); the cynical adman cast as 'pale versus'; loose. Form (6 letters, V/P heavy) is largely intractable.</li>
<li><b>vase pl</b> — (in algorithmic list); ad-vase placement; loose.</li>
<li><b>alps ve</b> — (in algorithmic list); loose.</li>
</ul>

<p>
<b>P. Tom Veals</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>stamp love</b> — (in algorithmic list); ad-stamp on love — his cynical commodifier MO; strong.</li>
<li><b>stove palm</b> — (in algorithmic list); the adman's palm-on-stove burning Madison Avenue cynicism; loose.</li>
<li><b>psalm vote</b> — (in algorithmic list); the ad-as-psalm vote-getter; suggestive.</li>
<li><b>lamps veto</b> — (in algorithmic list); his veto of authentic light, all lamps; loose.</li>
<li><b>votes lamp</b> — (in algorithmic list); the votes-lamp of campaign-ad cynicism; loose.</li>
<li><b>stomp vale</b> — (in algorithmic list); the adman stomps the vale of authentic feeling; suggestive.</li>
<li><b>moves plat</b> — (in algorithmic list); his ad-moves on the plat (plot of land); loose.</li>
<li><b>palms vote</b> — (in algorithmic list); palms-up vote-getting via ads; loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>P. Veals</b>
</p>
<ul class="org-ul">
<li>pale vs</li>
<li>slap ve</li>
<li>plea vs</li>
<li>leap vs</li>
<li>save pl</li>
<li>save lp</li>
<li>sale vp</li>
<li>seal vp</li>
<li>sep val</li>
<li>ave pls</li>
<li>eva pls</li>
<li>laps ve</li>
<li>slap ev</li>
<li>laps ev</li>
<li>vale sp</li>
<li>vale ps</li>
<li>val eps</li>
<li>alps ve</li>
<li>alps ev</li>
<li>pals ve</li>
<li>pals ev</li>
<li>val esp</li>
<li>vase pl</li>
<li>vase lp</li>
<li>elsa vp</li>
<li>sale pv</li>
<li>seal pv</li>
<li>elsa pv</li>
<li>peas lv</li>
<li>apes lv</li>
<li>vape ls</li>
<li>vape sl</li>
<li>pave ls</li>
<li>pave sl</li>
<li>pale sv</li>
<li>plea sv</li>
<li>leap sv</li>
<li>spa lev</li>
<li>sap lev</li>
<li>pas lev</li>
<li>psa lev</li>
<li>lev aps</li>
<li>ave lps</li>
<li>eva lps</li>
<li>veal sp</li>
<li>veal ps</li>
<li>lev asp</li>
<li>epl vas</li>
<li>val pes</li>
<li>vs el pa</li>
</ul>

<p>
<b>P. Tom Veals</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>votes palm</li>
<li>stamp love</li>
<li>votes lamp</li>
<li>lamps vote</li>
<li>stove palm</li>
<li>stove lamp</li>
<li>lamps veto</li>
<li>psalm vote</li>
<li>psalm veto</li>
<li>palms vote</li>
<li>palms veto</li>
<li>apostle mv</li>
<li>temps oval</li>
<li>metals pov</li>
<li>stomp vale</li>
<li>apostle vm</li>
<li>pavel most</li>
<li>moves plat</li>
<li>stomp veal</li>
<li>pavel toms</li>
<li>volta meps</li>
<li>male top vs</li>
<li>love map st</li>
<li>lose map tv</li>
<li>save lot pm</li>
<li>loves at pm</li>
<li>votes al pm</li>
<li>votes la pm</li>
<li>love sat pm</li>
<li>lost map ve</li>
<li>lots map ve</li>
<li>plot sam ve</li>
<li>meal top vs</li>
<li>metal vs op</li>
<li>solve at pm</li>
<li>maps lot ve</li>
<li>past vol me</li>
<li>past vol em</li>
<li>east vol pm</li>
<li>step vol am</li>
<li>step vol ma</li>
<li>seat vol pm</li>
<li>tape vol ms</li>
<li>maps vol et</li>
<li>set map vol</li>
<li>sam pet vol</li>
<li>male pot vs</li>
<li>meal pot vs</li>
<li>plate vs mo</li>
<li>sept vol am</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-dwayne-glynn" class="outline-2">
<h2 id="dwayne-glynn"><span class="section-number-2">54.</span> Dwayne Glynn&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="ennet">ennet</span>&#xa0;<span class="doony">doony</span>&#xa0;<span class="gastric">gastric</span></span></h2>
<div class="outline-text-2" id="text-dwayne-glynn">
<p>
An Ennet House resident known as Doony, whose chronic gastric distress and crude affect make him part of the house's grimmer comedy.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Doony ~ doody</b> — His nickname phonetically nudges toward childish 'doody' (excrement), aligning with his chronic gastric distress and crude affect.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>Doony ~ doody</b> — His nickname 'Doony' phonetically nudges toward childish 'doody' (excrement), aligning with his chronic gastric distress and crude affect.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Dwayne Glynn</b>
</p>
<ul class="org-ul">
<li><b>lynn edgy wan</b> — (in algorithmic list); Doony's edgy/wan gastric-distress portrait; suggestive.</li>
<li><b>lynn dawg yen</b> — (in algorithmic list); his street-talk yen; loose.</li>
<li><b>wanly gyn end</b> — (in algorithmic list); his wanly-end gastric distress; loose.</li>
<li><b>lynn yang dew</b> — (in algorithmic list); his Doony-yang at dawn; loose.</li>
<li><b>newly andy ng</b> — (in algorithmic list); newly-Andy-Ng character peripheral; loose.</li>
<li><b>dwayne glynn</b> — self-restated; loose.</li>
<li><b>wendy lang ny</b> — (in algorithmic list); loose.</li>
<li><b>danny gel wyn</b> — (in algorithmic list); the 'Danny gel' grimace of his gastric upsets; loose.</li>
</ul>

<p>
<b>Dwayne R. Glynn</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>grandly wenny</b> — (in algorithmic list); 'grandly wenny' — his crudely grand stage of distress; loose.</li>
<li><b>grew andy lynn</b> — (in algorithmic list); the residents grew up around Doony; loose.</li>
<li><b>grey dawn lynn</b> — (in algorithmic list); the grey-dawn Ennet routine; suggestive.</li>
<li><b>drew yang lynn</b> — (in algorithmic list); the residents drew around his crude yang; loose.</li>
<li><b>grand newly ny</b> — (in algorithmic list); his grand-newly Ennet residency; loose.</li>
<li><b>newly randy ng</b> — (in algorithmic list); newly-randy Ennet inmate; suggestive.</li>
<li><b>wandery glynn</b> — (in algorithmic list); the wandery (wandering) Glynn at Ennet; suggestive.</li>
<li><b>angry wendy ln</b> — (in algorithmic list); his angry-Wendy facade of crudeness; loose.</li>
<li><b>lynn wary deng</b> — (in algorithmic list); loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Dwayne Glynn</b>
</p>
<ul class="org-ul">
<li>newly andy ng</li>
<li>wendy lynn ga</li>
<li>wendy lynn ag</li>
<li>wendy lang ny</li>
<li>wang lynn dye</li>
<li>wendy yang ln</li>
<li>newly yang nd</li>
<li>yang lynn wed</li>
<li>newly dang ny</li>
<li>wendy yang nl</li>
<li>yang lynn dew</li>
<li>dylan gwen ny</li>
<li>lynn gwen day</li>
<li>lynn edgy wan</li>
<li>danny gwen ly</li>
<li>denny wang ly</li>
<li>lenny dawg ny</li>
<li>lynn dawg yen</li>
<li>lynn dawg nye</li>
<li>lenny andy gw</li>
<li>lenny yang dw</li>
<li>lynn edgy naw</li>
<li>lynne andy gw</li>
<li>lynne yang dw</li>
<li>lynne dawg ny</li>
<li>wayne lynn gd</li>
<li>wayne lynn dg</li>
<li>waged lynn ny</li>
<li>lynn deng way</li>
<li>dwayne lng ny</li>
<li>wendy any lng</li>
<li>wendy yan lng</li>
<li>wendy nay lng</li>
<li>denny way lng</li>
<li>lenny yang wd</li>
<li>lynne yang wd</li>
<li>delay wynn ng</li>
<li>dylan wynn ge</li>
<li>dylan wynn eg</li>
<li>gland wynn ye</li>
<li>gland wynn ey</li>
<li>daley wynn ng</li>
<li>gayle wynn nd</li>
<li>lady wynn gen</li>
<li>lady wynn eng</li>
<li>glad wynn yen</li>
<li>glad wynn nye</li>
<li>andy wynn leg</li>
<li>andy wynn gel</li>
<li>deny wynn gal</li>
</ul>

<p>
<b>Dwayne R. Glynn</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>grand newly ny</li>
<li>newly randy ng</li>
<li>grew andy lynn</li>
<li>grey dawn lynn</li>
<li>drew yang lynn</li>
<li>angry wendy ln</li>
<li>angry newly nd</li>
<li>angry lynn wed</li>
<li>wendy lynn rag</li>
<li>angry wendy nl</li>
<li>angry lynn dew</li>
<li>newly danny gr</li>
<li>yard lynn gwen</li>
<li>grey lynn wand</li>
<li>warn lynn edgy</li>
<li>wendy yang nrl</li>
<li>lenny wang dry</li>
<li>dwayne lynn gr</li>
<li>wang lynn dyer</li>
<li>granny weld ny</li>
<li>randy lenny gw</li>
<li>angry lenny dw</li>
<li>angry lynne dw</li>
<li>randy lynne gw</li>
<li>lynne wang dry</li>
<li>lynn wary deng</li>
<li>granny lewd ny</li>
<li>wendy ryan lng</li>
<li>wendy yarn lng</li>
<li>denny wary lng</li>
<li>dwayne lynn rg</li>
<li>angry lenny wd</li>
<li>angry lynne wd</li>
<li>newly danny rg</li>
<li>lenny grady nw</li>
<li>lynne grady nw</li>
<li>grady lynn new</li>
<li>grady lynn wen</li>
<li>nearly wynn gd</li>
<li>nearly wynn dg</li>
<li>garden wynn ly</li>
<li>danger wynn ly</li>
<li>grande wynn ly</li>
<li>gerald wynn ny</li>
<li>ranged wynn ly</li>
<li>dearly wynn ng</li>
<li>angled wynn yr</li>
<li>ready wynn lng</li>
<li>grand wynn ely</li>
<li>angry wynn led</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-wardine" class="outline-2">
<h2 id="wardine"><span class="section-number-2">55.</span> Wardine&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="wardine">wardine</span>&#xa0;<span class="abuse">abuse</span>&#xa0;<span class="vernacular">vernacular</span></span></h2>
<div class="outline-text-2" id="text-wardine">
<p>
A young Black girl in the novel's vernacular Roxbury section, suffering sexual abuse at the hands of her mother's boyfriend Roy Tony. Her chapter, written in a controversial dialect, is one of the book's bleakest depictions of domestic violence.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>ward (minor under guardianship / hospital ward)</b> — Her name embeds 'ward' — both a minor under guardianship and a hospital ward — apt for an abused child nominally under the 'protection' of her mother and her boyfriend.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · substring · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Substring]</i> <b>ward</b> — Her name embeds 'ward' — both a minor under guardianship and a hospital ward — apt for an abused child who is nominally under the 'protection' of her mother and her boyfriend.</li>
<li><i>[Substring]</i> <b>ward (minor under guardianship / hospital ward)</b> — Her name embeds 'ward' — both a minor under guardianship and a hospital ward — apt for an abused child nominally under the 'protection' of her mother and her boyfriend.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Wardine</b>
</p>
<ul class="org-ul">
<li><b>warden i</b> — (in algorithmic list); the abuser-as-warden of the Roxbury house; suggestive.</li>
<li><b>warned i</b> — (in algorithmic list); the unheeded warning of her abuse; suggestive.</li>
<li><b>wander i</b> — (in algorithmic list); her wandering, isolated dialect chapter; loose.</li>
<li><b>rewind a</b> — (in algorithmic list); the trauma-rewind of her chapter; suggestive.</li>
<li><b>winder a</b> — (in algorithmic list); winder of her vernacular chapter; loose.</li>
<li><b>redawn i</b> — (in algorithmic list); the bleak redawn of each abusive day; suggestive.</li>
<li><b>weird an</b> — (in algorithmic list); her chapter's controversial 'weird' dialect; loose.</li>
<li><b>drain we</b> — (in algorithmic list); her chapter as drain — bleakest; loose.</li>
<li><b>andrew i</b> — (in algorithmic list); her abuser's name folded in (a roy-tony stand-in); loose.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Wardine</b>
</p>
<ul class="org-ul">
<li>read win</li>
<li>wide ran</li>
<li>wind are</li>
<li>weird an</li>
<li>dear win</li>
<li>wind era</li>
<li>andrew i</li>
<li>wind ear</li>
<li>wire and</li>
<li>wire dan</li>
<li>wire dna</li>
<li>weird na</li>
<li>drew ian</li>
<li>dare win</li>
<li>wider an</li>
<li>wider na</li>
<li>raid new</li>
<li>warned i</li>
<li>drain we</li>
<li>warn die</li>
<li>andre wi</li>
<li>drawn ie</li>
<li>wired an</li>
<li>wired na</li>
<li>ride wan</li>
<li>reid wan</li>
<li>aired nw</li>
<li>wide rna</li>
<li>wander i</li>
<li>diane wr</li>
<li>rain wed</li>
<li>iran wed</li>
<li>dire wan</li>
<li>edwin ar</li>
<li>edwin ra</li>
<li>warden i</li>
<li>drain ew</li>
<li>wear ind</li>
<li>wear din</li>
<li>ware ind</li>
<li>ware din</li>
<li>rain dew</li>
<li>iran dew</li>
<li>weir and</li>
<li>weir dan</li>
<li>weir dna</li>
<li>wine rad</li>
<li>raid wen</li>
<li>dine war</li>
<li>dine raw</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-bruce-green" class="outline-2">
<h2 id="bruce-green"><span class="section-number-2">56.</span> Bruce Green&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="ennet">ennet</span>&#xa0;<span class="mild">mild</span>&#xa0;<span class="workingclass">workingclass</span></span></h2>
<div class="outline-text-2" id="text-bruce-green">
<p>
A mild-mannered Ennet House resident from a working-class background, formerly partnered with Mildred Bonk, drawn quietly toward Joelle van Dyne. He represents the inarticulate, decent end of the residents' spectrum.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>green (naive / inexperienced)</b> — The surname's slang sense of 'green' — naive, unsophisticated — fits a mild, inarticulate working-class resident drawn quietly toward a woman well above his romantic experience.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>green (naive / inexperienced)</b> — The surname's slang sense of 'green' (naive, unsophisticated) fits a mild, inarticulate working-class resident drawn quietly toward a woman well above his romantic experience.</li>
<li><i>[Phonetic]</i> <b>Bruce ~ bruise</b> — First name nearly homophones 'bruise,' and paired with 'Green' suggests a green-and-purple bruise — the quiet, soft-tissue damage of a working-class life.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Bruce Green</b>
</p>
<ul class="org-ul">
<li><b>greener cub</b> — (in algorithmic list); the gentle greener cub of Ennet House; suggestive.</li>
<li><b>renege curb</b> — (in algorithmic list); his quiet renege/curb of the old life; suggestive.</li>
<li><b>begun rec re</b> — (in algorithmic list); begun recovery; loose.</li>
<li><b>cure ben reg</b> — (in algorithmic list); cure-ben (good) regimen of Ennet; loose.</li>
<li><b>bergen cure</b> — (in algorithmic list); a Bergen cure metaphor for his quiet recovery; loose.</li>
<li><b>burgee cern</b> — drop — obscure.</li>
<li><b>green bruce</b> — self-restated; loose.</li>
<li><b>begreen cur</b> — (in algorithmic list); to be-green (envious) cur — captures his quiet love for Joelle; suggestive.</li>
<li><b>regreen cub</b> — (in algorithmic list); the Mildred-Bonk-era cub re-greened in Ennet; suggestive.</li>
<li><b>burr cee gen</b> — drop — gibberish.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Bruce Green</b>
</p>
<ul class="org-ul">
<li>green bruce</li>
<li>greece burn</li>
<li>bruce genre</li>
<li>greene curb</li>
<li>bergen cure</li>
<li>greener cub</li>
<li>bruce gen re</li>
<li>bruce gen er</li>
<li>beer urge nc</li>
<li>been urge cr</li>
<li>gene uber cr</li>
<li>beer cure ng</li>
<li>greece un br</li>
<li>gene cure br</li>
<li>burger en ce</li>
<li>burger ne ce</li>
<li>green rub ce</li>
<li>genre rub ce</li>
<li>eugene cr br</li>
<li>gene curb re</li>
<li>gene curb er</li>
<li>greene bc ur</li>
<li>green cue br</li>
<li>genre cue br</li>
<li>greene br uc</li>
<li>greene ur cb</li>
<li>greece un rb</li>
<li>burger en ec</li>
<li>burger ne ec</li>
<li>eugene cr rb</li>
<li>greene uc rb</li>
<li>green rub ec</li>
<li>green cue rb</li>
<li>genre rub ec</li>
<li>genre cue rb</li>
<li>gene cure rb</li>
<li>bruce reg en</li>
<li>bruce reg ne</li>
<li>cure ben reg</li>
<li>bruce eng re</li>
<li>bruce eng er</li>
<li>eugene br rc</li>
<li>eugene rb rc</li>
<li>been urge rc</li>
<li>gene uber rc</li>
<li>bruce gee rn</li>
<li>begun rec re</li>
<li>begun rec er</li>
<li>been rug rec</li>
<li>beer gun rec</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-molly-notkin" class="outline-2">
<h2 id="molly-notkin"><span class="section-number-2">57.</span> Molly Notkin&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="filmstudent">filmstudent</span>&#xa0;<span class="mit">mit</span>&#xa0;<span class="joellesfriend">joellesfriend</span></span></h2>
<div class="outline-text-2" id="text-molly-notkin">
<p>
A film-studies graduate student at MIT and Joelle van Dyne's closest friend, who hosts the party where Joelle attempts to overdose on cocaine. Her dissertation gossip about James Incandenza supplies key exposition about the Entertainment.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Notkin = 'not kin'</b> — The surname parses as 'not kin' — fitting for a confidante whose betrayal of Joelle's intimate secrets exposes the limit of friendship that isn't blood.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>not kin</b> — Surname parses as 'not kin' — fitting for a gossip who is intimate-but-not-family with Joelle, and whose betrayal of confidences exposes the limit of that bond.</li>
<li><i>[Phonetic]</i> <b>Notkin = 'not kin'</b> — The surname parses as 'not kin' — fitting for a confidante whose betrayal of Joelle's intimate secrets exposes the limit of friendship that isn't blood.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Molly Notkin</b>
</p>
<ul class="org-ul">
<li>only link tom</li>
<li>only milk not</li>
<li>only milk ton</li>
<li>only monk lit</li>
<li>only monk til</li>
<li>tony monk ill</li>
<li>tiny monk lol</li>
<li>lily monk not</li>
<li>lily monk ton</li>
<li>tony monk lil</li>
<li>molly not ink</li>
<li>molly non kit</li>
<li>molly ton ink</li>
<li>look lynn tim</li>
<li>kill tony mon</li>
<li>milk lynn too</li>
<li>tool lynn kim</li>
<li>link lyon tom</li>
<li>milk lyon not</li>
<li>milk lyon ton</li>
<li>monk lyon lit</li>
<li>monk lyon til</li>
<li>look lynn mit</li>
<li>took lynn mil</li>
<li>molly knot in</li>
<li>molly knot ni</li>
<li>only knot mil</li>
<li>lily knot mon</li>
<li>lyon knot mil</li>
<li>molly knit on</li>
<li>molly knit no</li>
<li>molly not kin</li>
<li>molly ton kin</li>
<li>monty kill on</li>
<li>monty kill no</li>
<li>monty link lo</li>
<li>monty link ol</li>
<li>monty lol ink</li>
<li>monty lol kin</li>
<li>milky onto ln</li>
<li>milky onto nl</li>
<li>milky noon lt</li>
<li>milky noon tl</li>
<li>milky lot non</li>
<li>lynn loot kim</li>
<li>molly into nk</li>
<li>molly toni nk</li>
<li>nylon milk to</li>
<li>nylon milk ot</li>
<li>nylon lot kim</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-cosgrove-watt" class="outline-2">
<h2 id="cosgrove-watt"><span class="section-number-2">58.</span> Cosgrove Watt&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="actor">actor</span>&#xa0;<span class="joifilms">joifilms</span>&#xa0;<span class="fatherfigure">fatherfigure</span></span></h2>
<div class="outline-text-2" id="text-cosgrove-watt">
<p>
A character actor who appears in several of James Incandenza's films, often cast as a father figure or as JOI's stand-in.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Watt = SI unit of power/light</b> — The surname is the unit of electrical power, a quiet entry in the Incandenza-family lighting motif (incandesce, lamp, lens) — apt for an actor who serves as JOI's on-screen stand-in.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>watt</b> — Surname is the SI unit of power, a quiet electrical pun in a novel saturated with Incandenza-family lighting/optics imagery (Incandenza, lamp, lens) — fitting for JOI's on-screen stand-in.</li>
<li><i>[Phonetic]</i> <b>Watt = SI unit of power/light</b> — The surname is the unit of electrical power, a quiet entry in the Incandenza-family lighting motif (incandesce, lamp, lens) — apt for an actor who serves as JOI's on-screen stand-in.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Cosgrove Watt</b>
</p>
<ul class="org-ul">
<li><b>covet star wog</b> — loose, not idiomatic English (loose)</li>
<li><b>vector goats w</b> — loose (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Cosgrove Watt</b>
</p>
<ul class="org-ul">
<li>vote grow cast</li>
<li>votes grow act</li>
<li>votes grow cat</li>
<li>vote grow acts</li>
<li>scott gave row</li>
<li>vote grow cats</li>
<li>covers two tag</li>
<li>worst gave oct</li>
<li>grave cost two</li>
<li>worst cave got</li>
<li>grows vote act</li>
<li>grows vote cat</li>
<li>storage cow tv</li>
<li>voters tag cow</li>
<li>scott grow ave</li>
<li>stewart gov co</li>
<li>waters oct gov</li>
<li>actors wet gov</li>
<li>treats cow gov</li>
<li>towers act gov</li>
<li>towers cat gov</li>
<li>water cost gov</li>
<li>wrote cast gov</li>
<li>wrote acts gov</li>
<li>wrote cats gov</li>
<li>scott wear gov</li>
<li>actor west gov</li>
<li>tower cast gov</li>
<li>tower acts gov</li>
<li>tower cats gov</li>
<li>cottage row vs</li>
<li>vector was got</li>
<li>vector two gas</li>
<li>vector got saw</li>
<li>toast crew gov</li>
<li>scott grove wa</li>
<li>scott grove aw</li>
<li>grove cast two</li>
<li>grove acts two</li>
<li>grove cats two</li>
<li>grove stat cow</li>
<li>gotta cows rev</li>
<li>treat cows gov</li>
<li>voter cows tag</li>
<li>coast grow vet</li>
<li>scott grow eva</li>
<li>grows coat vet</li>
<li>costa grow vet</li>
<li>traces two gov</li>
<li>graves two oct</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-susan-t-cheese" class="outline-2">
<h2 id="susan-t-cheese"><span class="section-number-2">59.</span> Susan T. Cheese&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="street">street</span>&#xa0;<span class="peripheral">peripheral</span>&#xa0;<span class="boston">boston</span></span></h2>
<div class="outline-text-2" id="text-susan-t-cheese">
<p>
A street figure on the periphery of the novel's Boston substance-scene chorus.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Cheese = cheap, processed, slightly rotten</b> — The surname carries street-slang connotations of the cheap and faintly putrid — apt for a peripheral figure in the novel's Boston substance chorus.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · suggestive · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Suggestive]</i> <b>cheese</b> — Surname evokes street slang for something cheap, processed, and slightly rotten — apt for a peripheral street figure.</li>
<li><i>[Suggestive]</i> <b>Cheese = cheap, processed, slightly rotten</b> — The surname carries street-slang connotations of the cheap and faintly putrid — apt for a peripheral figure in the novel's Boston substance chorus.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Susan Cheese</b>
</p>
<ul class="org-ul">
<li>cheese susan</li>
<li>causes sheen</li>
<li>cheeses anus</li>
<li>sauces sheen</li>
<li>ensues chase</li>
<li>ensues aches</li>
<li>sense each us</li>
<li>such seen sea</li>
<li>scene has use</li>
<li>seen cash use</li>
<li>scene she usa</li>
<li>scene uses ah</li>
<li>sense cash eu</li>
<li>each sees sun</li>
<li>scenes has eu</li>
<li>scenes use ah</li>
<li>scenes use ha</li>
<li>scenes usa he</li>
<li>scene uses ha</li>
<li>cheese ass un</li>
<li>cheese san us</li>
<li>cheese sun as</li>
<li>causes she en</li>
<li>chase seen us</li>
<li>chase uses en</li>
<li>chase sees un</li>
<li>chase see sun</li>
<li>hence uses as</li>
<li>hence use ass</li>
<li>such sean see</li>
<li>cheese sun sa</li>
<li>hence uses sa</li>
<li>scenes sea uh</li>
<li>scenes usa eh</li>
<li>sense case uh</li>
<li>cases seen uh</li>
<li>scenes sue ah</li>
<li>scenes sue ha</li>
<li>scene has sue</li>
<li>hence ass sue</li>
<li>seen cash sue</li>
<li>census see ah</li>
<li>census see ha</li>
<li>census sea he</li>
<li>census sea eh</li>
<li>scenes ash eu</li>
<li>scene use ash</li>
<li>scene sue ash</li>
<li>causes she ne</li>
<li>chase uses ne</li>
</ul>

<p>
<b>Susan T. Cheese</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>cheeses aunts</li>
<li>cheats ensues</li>
<li>cause sent she</li>
<li>such seen east</li>
<li>sense such eat</li>
<li>cases then use</li>
<li>these case sun</li>
<li>scene thus sea</li>
<li>case seen thus</li>
<li>these uses can</li>
<li>then case uses</li>
<li>each sent uses</li>
<li>such seen seat</li>
<li>scene shut sea</li>
<li>case seen shut</li>
<li>sense such tea</li>
<li>sense tech usa</li>
<li>sense teach us</li>
<li>sense cute has</li>
<li>causes sent he</li>
<li>causes she ten</li>
<li>causes she net</li>
<li>these cases un</li>
<li>teach sees sun</li>
<li>scenes hate us</li>
<li>scenes heat us</li>
<li>scenes the usa</li>
<li>cheese sun sat</li>
<li>sense chat use</li>
<li>seen uses chat</li>
<li>scenes use hat</li>
<li>scene uses hat</li>
<li>chest seen usa</li>
<li>cases hunt see</li>
<li>case sees hunt</li>
<li>chase sent use</li>
<li>chase uses ten</li>
<li>chase uses net</li>
<li>cases sheet un</li>
<li>sheet case sun</li>
<li>sheet uses can</li>
<li>seats hence us</li>
<li>hence uses sat</li>
<li>hence sets usa</li>
<li>sense such ate</li>
<li>chest ease sun</li>
<li>such sent ease</li>
<li>cash uses teen</li>
<li>chest sean use</li>
<li>uses tech sean</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-todd-possalthwaite" class="outline-2">
<h2 id="todd-possalthwaite"><span class="section-number-2">60.</span> Todd Possalthwaite&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="junior">junior</span>&#xa0;<span class="nose">nose</span></span></h2>
<div class="outline-text-2" id="text-todd-possalthwaite">
<p>
A young ETA student notable for breaking his nose and wearing a protective splint, part of the academy's junior cohort.
</p>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Todd Possalthwaite</b>
</p>
<ul class="org-ul">
<li><b>those plait dad swot</b> — loose; the splinted junior swotting through drills (suggestive)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Todd Possalthwaite</b>
</p>
<ul class="org-ul">
<li>stated shadow pilot</li>
<li>hospitals dated two</li>
<li>swedish total adopt</li>
<li>spotted shadow tail</li>
<li>shadow titles adopt</li>
<li>hospital waste todd</li>
<li>hospital sweat todd</li>
<li>potatoes whilst add</li>
<li>potatoes whilst dad</li>
<li>shadows title adopt</li>
<li>hospital wasted dot</li>
<li>twisted photo salad</li>
<li>twisted stood alpha</li>
<li>whistle potato adds</li>
<li>whistle stood adapt</li>
<li>potatoes width lads</li>
<li>tattoos shaped wild</li>
<li>ottawa depths solid</li>
<li>tattoos saddle whip</li>
<li>hospitals dated tow</li>
<li>photos wasted tidal</li>
<li>shadow tasted pilot</li>
<li>hospital stated dow</li>
<li>hospital tasted dow</li>
<li>spotted holds await</li>
<li>shadows titled atop</li>
<li>whistle potato dads</li>
<li>disposed total what</li>
<li>spotted awaits hold</li>
<li>twisted asphalt doo</li>
<li>adopted twists halo</li>
<li>tattoos paddle wish</li>
<li>ottawa depths idols</li>
<li>adopted totals wish</li>
<li>wished totals adopt</li>
<li>deposit oswald that</li>
<li>spotted oswald thai</li>
<li>hottest oswald paid</li>
<li>hottest oswald ipad</li>
<li>stated whoops tidal</li>
<li>tasted whoops tidal</li>
<li>polished ottawa std</li>
<li>hospitals dotted wa</li>
<li>hospitals dotted aw</li>
<li>hospital dotted was</li>
<li>hospital dotted saw</li>
<li>spatial dotted show</li>
<li>adopted whilst oats</li>
<li>hospitals towed dat</li>
<li>hospitals towed tad
<i>Search timed out at 45s with 229 partial candidates.</i></li>
</ul>

</details>
</div>
</div>
<div id="outline-container-ann-kittenplan" class="outline-2">
<h2 id="ann-kittenplan"><span class="section-number-2">61.</span> Ann Kittenplan&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="eschaton">eschaton</span>&#xa0;<span class="muscular">muscular</span></span></h2>
<div class="outline-text-2" id="text-ann-kittenplan">
<p>
A muscular, aggressive female ETA student rumored to be on steroids, and a combatant in the Eschaton game whose physicality contributes to the match's collapse into actual violence.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Kittenplan = 'kitten' + 'plan'</b> — The diminutive, cuddly 'kitten' planted in her surname clashes pointedly with her steroidal aggression and her role in escalating Eschaton into actual violence.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>kitten</b> — The diminutive, cuddly 'kitten' clashes ironically with her steroidal aggression — the name plans (or schemes) a kitten she emphatically isn't.</li>
<li><i>[Entendre]</i> <b>Kittenplan = 'kitten' + 'plan'</b> — The diminutive, cuddly 'kitten' planted in her surname clashes pointedly with her steroidal aggression and her role in escalating Eschaton into actual violence.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Ann Kittenplan</b>
</p>
<ul class="org-ul">
<li><b>innate plant nk</b> — her innate aggression plants the Eschaton riot (suggestive)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Ann Kittenplan</b>
</p>
<ul class="org-ul">
<li>plant nine tank</li>
<li>talent pink ann</li>
<li>patent link ann</li>
<li>planet tank inn</li>
<li>taken plant inn</li>
<li>latin tank penn</li>
<li>plant kent nina</li>
<li>tenant plan ink</li>
<li>tenant link pan</li>
<li>tenant link nap</li>
<li>antenna link pt</li>
<li>antenna pink lt</li>
<li>antenna pink tl</li>
<li>talkin penn tan</li>
<li>talkin penn ant</li>
<li>talkin penn nat</li>
<li>antenna knit pl</li>
<li>antenna knit lp</li>
<li>planet knit ann</li>
<li>plant anne knit</li>
<li>tenant plan kin</li>
<li>tenant pink lan</li>
<li>kitten plan ann</li>
<li>intent lanka np</li>
<li>planet knit nan</li>
<li>talent pink nan</li>
<li>patent link nan</li>
<li>kitten plan nan</li>
<li>intent plank an</li>
<li>intent plank na</li>
<li>tenant plain nk</li>
<li>tenant plank in</li>
<li>tenant plank ni</li>
<li>innate plant nk</li>
<li>innate plank nt</li>
<li>innate plank tn</li>
<li>plank tent nina</li>
<li>antenna link tp</li>
<li>natal penn knit</li>
<li>intent plan kan</li>
<li>kaplan tent inn</li>
<li>lankan tent pin</li>
<li>lankan penn tit</li>
<li>lankan pint ten</li>
<li>lankan pint net</li>
<li>lankan tent nip</li>
<li>lankan pint ent</li>
<li>kaplan nine tnt</li>
<li>lankan pine tnt</li>
<li>annie plank tnt</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-dolores-rusk" class="outline-2">
<h2 id="dolores-rusk"><span class="section-number-2">62.</span> Dolores Rusk&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="counselor">counselor</span>&#xa0;<span class="psychologist">psychologist</span>&#xa0;<span class="eta">eta</span></span></h2>
<div class="outline-text-2" id="text-dolores-rusk">
<p>
ETA's staff psychologist and counselor, whose jargon-laden therapeutic style is mocked by the students.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Dolores = Spanish/Latin for 'sorrows, pains'</b> — Her given name literally means 'pains/sorrows' — pointed for a staff psychologist whose jargon-laden therapy spectacularly fails to address actual suffering.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>dolores (Spanish/Latin: 'sorrows, pains')</b> — Her given name literally means 'pains/sorrows' — pointed for a psychologist whose jargon-laden style fails to actually address suffering.</li>
<li><i>[Translation]</i> <b>Dolores = Spanish/Latin for 'sorrows, pains'</b> — Her given name literally means 'pains/sorrows' — pointed for a staff psychologist whose jargon-laden therapy spectacularly fails to address actual suffering.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Dolores Rusk</b>
</p>
<ul class="org-ul">
<li>looser kurds</li>
<li>looks sure dr</li>
<li>order loss uk</li>
<li>users look dr</li>
<li>users lord ok</li>
<li>orders los uk</li>
<li>looks user dr</li>
<li>older ross uk</li>
<li>ruled ross ok</li>
<li>looks rude rs</li>
<li>doors luke rs</li>
<li>luke ross rod</li>
<li>looks sure rd</li>
<li>looks user rd</li>
<li>users look rd</li>
<li>lords sure ok</li>
<li>lords rose uk</li>
<li>lords user ok</li>
<li>looks rude sr</li>
<li>doors luke sr</li>
<li>roses lord uk</li>
<li>lords sore uk</li>
<li>looks reds ur</li>
<li>dress look ur</li>
<li>losers rod uk</li>
<li>older ussr ok</li>
<li>look ussr red</li>
<li>look ussr der</li>
<li>older ussr ko</li>
<li>users lord ko</li>
<li>ruled ross ko</li>
<li>lords sure ko</li>
<li>lords user ko</li>
<li>looked sur rs</li>
<li>looked sur sr</li>
<li>looks red sur</li>
<li>looks der sur</li>
<li>look reds sur</li>
<li>orders sol uk</li>
<li>rulers dos ok</li>
<li>rulers dos ko</li>
<li>older russ ok</li>
<li>older russ ko</li>
<li>souls kerr do</li>
<li>look russ red</li>
<li>look russ der</li>
<li>loss kerr duo</li>
<li>soul kerr dos</li>
<li>solo kerr usd</li>
<li>rules rods ok</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-otto-brandt" class="outline-2">
<h2 id="otto-brandt"><span class="section-number-2">63.</span> Otto Brandt&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="custodial">custodial</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="background">background</span></span></h2>
<div class="outline-text-2" id="text-otto-brandt">
<p>
A member of ETA's custodial staff, a peripheral background presence on the grounds.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Brandt = German for 'fire, blaze, burning'</b> — The German surname meaning 'fire' is another quiet entry in the novel's pyre/incandescence motif, here attached to the man who tends the academy grounds.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · translation · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Translation]</i> <b>Brandt (German: 'fire, blaze, burning')</b> — German 'Brand' means fire or burning — another quiet entry in the novel's pyre/incandescence motif, attached here to a custodian of the grounds.</li>
<li><i>[Translation]</i> <b>Brandt = German for 'fire, blaze, burning'</b> — The German surname meaning 'fire' is another quiet entry in the novel's pyre/incandescence motif, here attached to the man who tends the academy grounds.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Otto Brandt</b>
</p>
<ul class="org-ul">
<li>brandt otto</li>
<li>brandt toto</li>
<li>brandt toot</li>
<li>torn bat dot</li>
<li>dont boat rt</li>
<li>torn dot tab</li>
<li>robot tan td</li>
<li>boat torn td</li>
<li>robot ant td</li>
<li>dont art bot</li>
<li>dont rat bot</li>
<li>robot nat td</li>
<li>dont boat tr</li>
<li>tattoo br nd</li>
<li>tattoo db rn</li>
<li>tattoo nd rb</li>
<li>dont bat rot</li>
<li>dont tab rot</li>
<li>band otto rt</li>
<li>band otto tr</li>
<li>onto bart td</li>
<li>dont bart to</li>
<li>dont bart ot</li>
<li>boot rant td</li>
<li>brad otto nt</li>
<li>brad otto tn</li>
<li>barn otto td</li>
<li>rand otto bt</li>
<li>rand otto tb</li>
<li>bart otto nd</li>
<li>bart not dot</li>
<li>bart ton dot</li>
<li>otto rant db</li>
<li>rant dot bot</li>
<li>baton dot rt</li>
<li>baton dot tr</li>
<li>baton rot td</li>
<li>dont bot tar</li>
<li>barton to td</li>
<li>barton do tt</li>
<li>barton td ot</li>
<li>board not tt</li>
<li>board ton tt</li>
<li>brand too tt</li>
<li>broad not tt</li>
<li>broad ton tt</li>
<li>robot and tt</li>
<li>robot dan tt</li>
<li>robot dna tt</li>
<li>donor bat tt</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-uss-millicent-kent" class="outline-2">
<h2 id="uss-millicent-kent"><span class="section-number-2">64.</span> U.S.S. Millicent Kent&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="tall">tall</span>&#xa0;<span class="mario">mario</span></span></h2>
<div class="outline-text-2" id="text-uss-millicent-kent">
<p>
An unusually tall female ETA student whose nickname plays on her battleship-like stature, romantically interested in Mario Incandenza in one episode.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>U.S.S. = 'United States Ship'</b> — The Navy hull-number prefix replaces a personal title, making the joke explicit: she is so battleship-tall she gets designated like a vessel rather than addressed like a person.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · initialism · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Initialism]</i> <b>U.S.S.</b> — The prefix is the U.S. Navy's 'United States Ship' designation, the joke explicit in the text: her battleship-like stature earns her a hull number rather than a person's title.</li>
<li><i>[Initialism]</i> <b>U.S.S. = 'United States Ship'</b> — The Navy hull-number prefix replaces a personal title, making the joke explicit: she is so battleship-tall she gets designated like a vessel rather than addressed like a person.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>U.S.S. Kent</b>
</p>
<ul class="org-ul">
<li><b>uses knt</b> — loose; 'knt' not a word (loose)</li>
<li><b>stuns ek</b> — loose; the tall student 'stuns' onlookers (loose)</li>
<li><b>knuts es</b> — loose; Harry-Potter-ish currency, not English (loose)</li>
<li><b>tusks en</b> — loose; battleship-tall student bears 'tusks' (suggestive — single noun)</li>
<li><b>skunt es</b> — non-word (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>U.S.S. Kent</b>
</p>
<ul class="org-ul">
<li>sunk set</li>
<li>sunk est</li>
<li>tunes ks</li>
<li>tunes sk</li>
<li>kent uss</li>
<li>nests uk</li>
<li>nests ku</li>
<li>sets kun</li>
<li>nuke sts</li>
<li>tess kun</li>
<li>sunk ste</li>
<li>nukes st</li>
<li>nukes ts</li>
<li>kent sus</li>
<li>sent suk</li>
<li>nest suk</li>
<li>tens suk</li>
<li>nets suk</li>
<li>ken us st</li>
<li>ten uk ss</li>
<li>net uk ss</li>
<li>sen st uk</li>
<li>ken st su</li>
<li>ken ss tu</li>
<li>ken ss ut</li>
<li>ken us ts</li>
<li>ken su ts</li>
<li>sen uk ts</li>
<li>ten ss ku</li>
<li>net ss ku</li>
<li>sen st ku</li>
<li>sen ts ku</li>
<li>use nt ks</li>
<li>use tn ks</li>
<li>set un ks</li>
<li>set nu ks</li>
<li>ten us ks</li>
<li>ten su ks</li>
<li>sun et ks</li>
<li>sun te ks</li>
<li>net us ks</li>
<li>net su ks</li>
<li>sue nt ks</li>
<li>sue tn ks</li>
<li>est un ks</li>
<li>est nu ks</li>
<li>nut se ks</li>
<li>nut es ks</li>
<li>sen tu ks</li>
<li>sen ut ks</li>
</ul>

<p>
<b>U.S.S. Millicent Kent</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>minute skills cent</li>
<li>minutes still neck</li>
<li>minutes tells nick</li>
<li>minutes skill cent</li>
<li>intense stuck mill</li>
<li>minutes cents kill</li>
<li>minute skill cents</li>
<li>minutes kills cent</li>
<li>minute cents kills</li>
<li>tunnel miles stick</li>
<li>tunnel stick smile</li>
<li>tunnel sticks mile</li>
<li>muscle intent silk</li>
<li>client sunset milk</li>
<li>sentiment suck ill</li>
<li>skills cement unit</li>
<li>insult cement silk</li>
<li>cement units skill</li>
<li>cement units kills</li>
<li>intent smiles luck</li>
<li>sentiment suck lil</li>
<li>license stunt milk</li>
<li>silence stunt milk</li>
<li>tunnel sticks lime</li>
<li>minutes scent kill</li>
<li>clients tunes milk</li>
<li>tunnels stick mile</li>
<li>tunnels stick lime</li>
<li>listen nickel must</li>
<li>minute skill scent</li>
<li>minute kills scent</li>
<li>silent nickel must</li>
<li>insult nickel stem</li>
<li>nickel until stems</li>
<li>nickel miles stunt</li>
<li>nickel smile stunt</li>
<li>tunnels miles tick</li>
<li>tunnels smile tick</li>
<li>tunnel smiles tick</li>
<li>tunnels times lick</li>
<li>tunnels items lick</li>
<li>sentiment kill usc</li>
<li>tickets mines null</li>
<li>sentiment skill uc</li>
<li>sentiment kills uc</li>
<li>minutes slick lent</li>
<li>clients klein must</li>
<li>tunnels slick time</li>
<li>tunnels slick item</li>
<li>tunnels tiles mick</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-lamont-chu" class="outline-2">
<h2 id="lamont-chu"><span class="section-number-2">65.</span> LaMont Chu&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="famelust">famelust</span>&#xa0;<span class="lyle">lyle</span></span></h2>
<div class="outline-text-2" id="text-lamont-chu">
<p>
An ETA junior tennis player consumed by a craving for tennis fame and magazine-cover recognition, who brings his obsession to Lyle in the weight room and receives a koan about the relationship between fame and fear.
</p>

<aside class="ij-pick ij-pick-speculative ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>munch a lot</b> — The fame-hungry junior 'munches a lot' on the dream of magazine-cover recognition, the very appetite Lyle's koan diagnoses.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · speculative]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>LaMont Chu</b>
</p>
<ul class="org-ul">
<li><b>much on alt</b> — loose; he wants 'much' fame, gets the 'alt' koan from Lyle (loose)</li>
<li><b>macho lunt</b> — loose; 'lunt' is archaic for slow-burn match — fame as slow burn (loose)</li>
<li><b>Lon, mat chu</b> — loose proper-noun salad (loose)</li>
<li><b>calm not uh</b> — Lyle's koan response: be calm, not uhh-ing for fame (suggestive)</li>
<li><b>munch a lot</b> — the junior ETA player feeding his fame-hunger (suggestive)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>LaMont Chu</b>
</p>
<ul class="org-ul">
<li>launch tom</li>
<li>column hat</li>
<li>mouth clan</li>
<li>month ucla</li>
<li>lunch atom</li>
<li>column tha</li>
<li>human colt</li>
<li>month luca</li>
<li>cloth manu</li>
<li>much lot an</li>
<li>notch alum</li>
<li>month aclu</li>
<li>calhoun mt</li>
<li>calhoun tm</li>
<li>human clot</li>
<li>alton much</li>
<li>lamont chu</li>
<li>luton mach</li>
<li>lunch moat</li>
<li>launch mot</li>
<li>tonal much</li>
<li>munch alot</li>
<li>munch alto</li>
<li>alton chum</li>
<li>tonal chum</li>
<li>much not al</li>
<li>much not la</li>
<li>lunch tom a</li>
<li>lunch to am</li>
<li>lunch to ma</li>
<li>calm hot un</li>
<li>much lot na</li>
<li>calm tho un</li>
<li>lunch at mo</li>
<li>much ton al</li>
<li>much ton la</li>
<li>calm not uh</li>
<li>calm ton uh</li>
<li>hunt com al</li>
<li>hunt com la</li>
<li>column th a</li>
<li>human co lt</li>
<li>cult man oh</li>
<li>cult man ho</li>
<li>cult nah mo</li>
<li>cult ham on</li>
<li>cult ham no</li>
<li>human ct lo</li>
<li>match un lo</li>
<li>hunt mac lo</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-tex-watson" class="outline-2">
<h2 id="tex-watson"><span class="section-number-2">66.</span> Tex Watson&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="prorector">prorector</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="staff">staff</span></span></h2>
<div class="outline-text-2" id="text-tex-watson">
<p>
A prorector at ETA, part of the academy's adult coaching and supervisory cohort, sharing a name with the Manson follower.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Tex Watson = Charles 'Tex' Watson, Manson Family killer</b> — He shares the name of the Manson follower who led the Tate murders — a darkly incongruous label for a tennis prorector charged with supervising children.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Tex Watson</b> — Shares the name of Charles 'Tex' Watson, the Manson Family member who led the Tate murders — a darkly incongruous name for a tennis prorector charged with supervising children.</li>
<li><i>[Allusion]</i> <b>Tex Watson = Charles 'Tex' Watson, Manson Family killer</b> — He shares the name of the Manson follower who led the Tate murders — a darkly incongruous label for a tennis prorector charged with supervising children.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Tex Watson</b>
</p>
<ul class="org-ul">
<li>texas town</li>
<li>taxes town</li>
<li>texas wont</li>
<li>taxes wont</li>
<li>texans two</li>
<li>texans tow</li>
<li>watson tex</li>
<li>weston tax</li>
<li>next was to</li>
<li>next two as</li>
<li>extant sow</li>
<li>texans wto</li>
<li>watson ext</li>
<li>sexton wat</li>
<li>texan stow</li>
<li>texans wot</li>
<li>next saw to</li>
<li>want sex to</li>
<li>town sex at</li>
<li>went tax so</li>
<li>news tax to</li>
<li>west tax on</li>
<li>west tax no</li>
<li>now set tax</li>
<li>own set tax</li>
<li>set won tax</li>
<li>text was on</li>
<li>text was no</li>
<li>text now as</li>
<li>text own as</li>
<li>text saw on</li>
<li>text saw no</li>
<li>text won as</li>
<li>wants to ex</li>
<li>text snow a</li>
<li>snow tax et</li>
<li>town sat ex</li>
<li>son tax wet</li>
<li>tons tax we</li>
<li>towns at ex</li>
<li>texts now a</li>
<li>texts own a</li>
<li>texts won a</li>
<li>next two sa</li>
<li>text now sa</li>
<li>text own sa</li>
<li>text won sa</li>
<li>town tax se</li>
<li>text owns a</li>
<li>owns tax et</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-wade-mcdade" class="outline-2">
<h2 id="wade-mcdade"><span class="section-number-2">67.</span> Wade McDade&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="ennet">ennet</span>&#xa0;<span class="troublemaker">troublemaker</span>&#xa0;<span class="resident">resident</span></span></h2>
<div class="outline-text-2" id="text-wade-mcdade">
<p>
An Ennet House resident with a troublemaking streak, part of the house's roster of difficult residents.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Wade / McDade internal rhyme</b> — The sing-song Wade/McDade rhyme gives the name a juvenile-delinquent jingle suited to a troublemaking Ennet House resident.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · suggestive · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Suggestive]</i> <b>McDade rhyme</b> — The internal rhyme Wade/McDade gives the name a sing-song, juvenile-delinquent ring suited to a troublemaking resident.</li>
<li><i>[Suggestive]</i> <b>Wade / McDade internal rhyme</b> — The sing-song Wade/McDade rhyme gives the name a juvenile-delinquent jingle suited to a troublemaking Ennet House resident.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Wade McDade</b>
</p>
<ul class="org-ul">
<li><b>we dad mac de</b> — loose (loose)</li>
<li><b>addmce wed a</b> — loose nonsense (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Wade McDade</b>
</p>
<ul class="org-ul">
<li>added mac we</li>
<li>adam weed dc</li>
<li>adam weed cd</li>
<li>weed add mac</li>
<li>weed dad mac</li>
<li>decade md wa</li>
<li>decade md aw</li>
<li>added cam we</li>
<li>weed add cam</li>
<li>weed dad cam</li>
<li>made wade dc</li>
<li>made wade cd</li>
<li>dead wade cm</li>
<li>dead wade mc</li>
<li>wade mad dec</li>
<li>wade dec dam</li>
<li>wade dame dc</li>
<li>wade dame cd</li>
<li>decade wa dm</li>
<li>decade aw dm</li>
<li>added awe cm</li>
<li>added awe mc</li>
<li>came wade dd</li>
<li>came add wed</li>
<li>came dad wed</li>
<li>dead mac wed</li>
<li>dead cam wed</li>
<li>adam dec wed</li>
<li>decade ad mw</li>
<li>decade da mw</li>
<li>added mac ew</li>
<li>added ace mw</li>
<li>added cam ew</li>
<li>added mae cw</li>
<li>made dead cw</li>
<li>dead dame cw</li>
<li>adam deed cw</li>
<li>came add dew</li>
<li>came dad dew</li>
<li>dead mac dew</li>
<li>dead cam dew</li>
<li>adam dec dew</li>
<li>wade dec amd</li>
<li>dead mead cw</li>
<li>wade mead dc</li>
<li>wade mead cd</li>
<li>added mae wc</li>
<li>made dead wc</li>
<li>made wed cad</li>
<li>made dew cad</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-eighties-bill" class="outline-2">
<h2 id="eighties-bill"><span class="section-number-2">68.</span> Eighties Bill&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="aa">aa</span>&#xa0;<span class="nickname">nickname</span>&#xa0;<span class="decade">decade</span></span></h2>
<div class="outline-text-2" id="text-eighties-bill">
<p>
An AA regular on the Boston commitment circuit, identified by the decade he got stuck in.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Eighties = the decade he's stuck in</b> — The nickname is doubly motivated — the AA regular is aesthetically and psychologically marooned in the 1980s, the decade naming his arrested development.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>Eighties</b> — Nickname is doubly motivated in-text: he is stuck in the 1980s aesthetically/psychologically, the decade naming his arrested development.</li>
<li><i>[Entendre]</i> <b>Eighties = the decade he's stuck in</b> — The nickname is doubly motivated — the AA regular is aesthetically and psychologically marooned in the 1980s, the decade naming his arrested development.</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Eighties Bill</b>
</p>
<ul class="org-ul">
<li>eligible this</li>
<li>eligible shit</li>
<li>eligible hits</li>
<li>eighties bill</li>
<li>eligible st hi</li>
<li>eligible is th</li>
<li>eligible th si</li>
<li>heels lgbt iii</li>
<li>eight ellis bi</li>
<li>siege bill hit</li>
<li>siege hill bit</li>
<li>leslie big hit</li>
<li>eight bells ii</li>
<li>eligible it sh</li>
<li>eligible sh ti</li>
<li>eight bills ie</li>
<li>sight belle ii</li>
<li>eight lies lib</li>
<li>eight isle lib</li>
<li>elite sigh lib</li>
<li>bites leigh il</li>
<li>bites leigh li</li>
<li>leigh site lib</li>
<li>leigh lies bit</li>
<li>leigh ties lib</li>
<li>leigh bits lie</li>
<li>leigh isle bit</li>
<li>leigh belts ii</li>
<li>leigh bits eli</li>
<li>leigh tiles bi</li>
<li>eligible it hs</li>
<li>eligible ti hs</li>
<li>eligible hi ts</li>
<li>sight ellie bi</li>
<li>ellie this big</li>
<li>ellie shit big</li>
<li>ellie hits big</li>
<li>ellie sigh bit</li>
<li>billie this ge</li>
<li>billie this eg</li>
<li>billie shit ge</li>
<li>billie shit eg</li>
<li>billie gets hi</li>
<li>billie hits ge</li>
<li>billie hits eg</li>
<li>billie sigh et</li>
<li>billie sigh te</li>
<li>billie seth gi</li>
<li>billie seth ig</li>
<li>billie his get</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-bridget-boone" class="outline-2">
<h2 id="bridget-boone"><span class="section-number-2">69.</span> Bridget Boone&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="female">female</span>&#xa0;<span class="tennis">tennis</span></span></h2>
<div class="outline-text-2" id="text-bridget-boone">
<p>
An ETA female student, part of the girls' tennis cohort.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Boone ~ Daniel Boone</b> — The surname echoes the frontier-icon Daniel Boone — the kind of Americana-flavored roster filler DFW favors for ETA's cohort.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Boone</b> — Surname echoes frontier-icon Daniel Boone, an Americana surname DFW favors for ETA roster filler.</li>
<li><i>[Allusion]</i> <b>Boone ~ Daniel Boone</b> — The surname echoes the frontier-icon Daniel Boone — the kind of Americana-flavored roster filler DFW favors for ETA's cohort.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Bridget Boone</b>
</p>
<ul class="org-ul">
<li><b>ironed bog bet</b> — loose (loose)</li>
<li><b>obedient brog</b> — loose; 'brog' not a word (loose)</li>
<li><b>boring be toed</b> — loose; her tennis 'toed' baseline play (loose)</li>
</ul>

<p>
<b>Bridget C. Boone</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>credo bigot bn e</b> — loose (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Bridget Boone</b>
</p>
<ul class="org-ul">
<li>breeding boot</li>
<li>bridget boone</li>
<li>obedient borg</li>
<li>boeing debtor</li>
<li>brooding beet</li>
<li>bigoted borne</li>
<li>bigoted boner</li>
<li>being door bet</li>
<li>begin door bet</li>
<li>bridge onto be</li>
<li>bridge too ben</li>
<li>entire god bob</li>
<li>entire dog bob</li>
<li>doing tree bob</li>
<li>tried gone bob</li>
<li>tired gone bob</li>
<li>bridge bone to</li>
<li>being root bed</li>
<li>begin root bed</li>
<li>diego born bet</li>
<li>diego rent bob</li>
<li>editor bob gen</li>
<li>gordon bite be</li>
<li>oregon bit bed</li>
<li>oregon bet bid</li>
<li>tiger done bob</li>
<li>ignored bob et</li>
<li>region bob ted</li>
<li>ignore bob ted</li>
<li>being robot de</li>
<li>being robot ed</li>
<li>begin robot de</li>
<li>begin robot ed</li>
<li>robot need big</li>
<li>robot gene bid</li>
<li>robot been dig</li>
<li>robot edge bin</li>
<li>being bored to</li>
<li>begin bored to</li>
<li>bored gone bit</li>
<li>bored note big</li>
<li>bored tone big</li>
<li>bridge boot en</li>
<li>being boot red</li>
<li>green boot bid</li>
<li>begin boot red</li>
<li>bird gene boot</li>
<li>been boot grid</li>
<li>genre boot bid</li>
<li>being boot der</li>
</ul>

<p>
<b>Bridget C. Boone</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>october end big</li>
<li>doctor being be</li>
<li>doctor been big</li>
<li>doctor begin be</li>
<li>credit gone bob</li>
<li>direct gone bob</li>
<li>entire good bbc</li>
<li>editor gone bbc</li>
<li>boring code bet</li>
<li>boring debt ceo</li>
<li>bridge bone oct</li>
<li>october bid gen</li>
<li>oregon diet bbc</li>
<li>oregon tied bbc</li>
<li>oregon edit bbc</li>
<li>ignored etc bob</li>
<li>being robot dec</li>
<li>begin robot dec</li>
<li>october ben dig</li>
<li>being bored oct</li>
<li>begin bored oct</li>
<li>genetic bob rod</li>
<li>oriented bbc go</li>
<li>breeding too bc</li>
<li>ignored bbc toe</li>
<li>noticed bro beg</li>
<li>noticed rob beg</li>
<li>generic bob dot</li>
<li>oregon tide bbc</li>
<li>october big den</li>
<li>breeding oct bo</li>
<li>doctrine beg bo</li>
<li>october bed ing</li>
<li>breeding boo ct</li>
<li>bridge cent boo</li>
<li>genocide bob rt</li>
<li>noticed bore gb</li>
<li>notice bored gb</li>
<li>genocide bro bt</li>
<li>genocide rob bt</li>
<li>robbed nice got</li>
<li>robbed coin get</li>
<li>robbed icon get</li>
<li>robbed goin etc</li>
<li>doctrine ego bb</li>
<li>genetic door bb</li>
<li>oregon cited bb</li>
<li>robbie cent god</li>
<li>robbie cent dog</li>
<li>doctrine bob ge</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-mary-esther-thode" class="outline-2">
<h2 id="mary-esther-thode"><span class="section-number-2">70.</span> Mary Esther Thode&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="eta">eta</span>&#xa0;<span class="prorector">prorector</span>&#xa0;<span class="politics">politics</span></span></h2>
<div class="outline-text-2" id="text-mary-esther-thode">
<p>
An ETA prorector who teaches a notoriously baroque political-philosophy course on the lyrics of post-Soviet protest music.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Thode contains 'ode'</b> — Her surname embeds 'ode,' the lyric-poetry form — apt for a prorector whose signature course is built entirely around the lyrics of post-Soviet protest songs.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · phonetic · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Phonetic]</i> <b>Thode ~ 'ode'</b> — Surname contains 'ode,' the lyric-poetry form — apt for a prorector whose signature course is built around the lyrics of protest songs.</li>
<li><i>[Phonetic]</i> <b>Thode contains 'ode'</b> — Her surname embeds 'ode,' the lyric-poetry form — apt for a prorector whose signature course is built entirely around the lyrics of post-Soviet protest songs.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Mary Thode</b>
</p>
<ul class="org-ul">
<li><b>myth read o</b> — loose (loose)</li>
</ul>

<p>
<b>Mary Esther Thode</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>they smothered ar</b> — loose; her baroque smothering of student curiosity (suggestive)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Mary Thode</b>
</p>
<ul class="org-ul">
<li>mother day</li>
<li>theory mad</li>
<li>method ray</li>
<li>theory dam</li>
<li>ahmed troy</li>
<li>ahmed tory</li>
<li>adore myth</li>
<li>hydro team</li>
<li>hydro meat</li>
<li>hydro mate</li>
<li>hydro meta</li>
<li>them day or</li>
<li>hydro tame</li>
<li>theory amd</li>
<li>ready moth</li>
<li>harmed toy</li>
<li>hearty mod</li>
<li>hearty dom</li>
<li>rhyme toad</li>
<li>dreamy hot</li>
<li>dreamy tho</li>
<li>doherty am</li>
<li>doherty ma</li>
<li>hardy tome</li>
<li>hydra tome</li>
<li>ready thom</li>
<li>today he mr</li>
<li>theory adm</li>
<li>mayer doth</li>
<li>morty head</li>
<li>dreamt hoy</li>
<li>made try oh</li>
<li>thyme road</li>
<li>thyme dora</li>
<li>death my or</li>
<li>dream hoyt</li>
<li>armed hoyt</li>
<li>hydrate mo</li>
<li>hydrate om</li>
<li>earthy mod</li>
<li>earthy dom</li>
<li>method ayr</li>
<li>heart my do</li>
<li>heard to my</li>
<li>road the my</li>
<li>term day oh</li>
<li>read hot my</li>
<li>may red hot</li>
<li>trade my oh</li>
<li>earth my do</li>
</ul>

<p>
<b>Mary Esther Thode</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>smothered hearty</li>
<li>smothered earthy</li>
<li>others ready them</li>
<li>mother there days</li>
<li>mother three days</li>
<li>others dream they</li>
<li>theory terms head</li>
<li>rather method yes</li>
<li>methods there ray</li>
<li>methods three ray</li>
<li>methods they rare</li>
<li>mother shared yet</li>
<li>theory shared met</li>
<li>theory heads term</li>
<li>story heard theme</li>
<li>death sorry theme</li>
<li>short ready theme</li>
<li>others armed they</li>
<li>theory dreams the</li>
<li>dreams other they</li>
<li>others theme yard</li>
<li>mother these yard</li>
<li>method arrest hey</li>
<li>mother stayed her</li>
<li>destroy them hear</li>
<li>theatre homes dry</li>
<li>other theme yards</li>
<li>tested harry home</li>
<li>destroy here math</li>
<li>mother thread yes</li>
<li>methods they rear</li>
<li>mother sheet yard</li>
<li>theory meets hard</li>
<li>theater homes dry</li>
<li>mothers there day</li>
<li>mothers three day</li>
<li>mothers ready the</li>
<li>mothers heard yet</li>
<li>mothers trade hey</li>
<li>mothers they read</li>
<li>mothers they dear</li>
<li>mothers they dare</li>
<li>mother steady her</li>
<li>mothers rated hey</li>
<li>theory heard stem</li>
<li>methods rather ye</li>
<li>mothers thread ye</li>
<li>mother reads they</li>
<li>theory reads them</li>
<li>harder theme toys</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-disney-r-leith" class="outline-2">
<h2 id="disney-r-leith"><span class="section-number-2">71.</span> Disney R. Leith&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="professor">professor</span>&#xa0;<span class="oldcolleagueofhimself">oldcolleagueofhimself</span>&#xa0;<span class="historian">historian</span></span></h2>
<div class="outline-text-2" id="text-disney-r-leith">
<p>
ETA's resident entertainment-history and optics professor, a former associate of Himself who lectures the boys on Incandenza's filmography and the technical lineage of annular fusion and lens craft. He is one of the few adults at the academy who remembers Himself with anything like affection.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Disney = the Disney brand</b> — His given name is the world's defining entertainment brand — pointedly ironic for an entertainment-history professor in a novel whose central artifact is a lethally compelling piece of entertainment.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · allusion · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Allusion]</i> <b>Disney</b> — His given name is the Disney brand — ironic for an entertainment-history professor in a novel whose central artifact is a lethally compelling piece of entertainment, and whose subject is the medium Disney epitomizes.</li>
<li><i>[Allusion]</i> <b>Disney = the Disney brand</b> — His given name is the world's defining entertainment brand — pointedly ironic for an entertainment-history professor in a novel whose central artifact is a lethally compelling piece of entertainment.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Disney Leith</b>
</p>
<ul class="org-ul">
<li><b>lithe denis y</b> — the lithe optics-professor (suggestive)</li>
<li><b>thinly seedi</b> — letter mismatch (extra letters); disregard (loose)</li>
<li><b>tidy lens hie</b> — the lens-historian's tidy lecture (suggestive)</li>
<li><b>they idle sin</b> — loose (loose)</li>
<li><b>this elide yn</b> — loose (loose)</li>
</ul>

<p>
<b>Disney R. Leith</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>nerdy lithesi</b> — loose (loose)</li>
<li><b>nerdy histlie</b> — loose (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Disney Leith</b>
</p>
<ul class="org-ul">
<li>sideline thy</li>
<li>yields thine</li>
<li>eyelids thin</li>
<li>eyelids hint</li>
<li>disney leith</li>
<li>sidney leith</li>
<li>eyelid hints</li>
<li>listed hey in</li>
<li>listen hey id</li>
<li>lines they id</li>
<li>style hide in</li>
<li>silent hey id</li>
<li>disney let hi</li>
<li>disney hit el</li>
<li>disney hit le</li>
<li>this deny lie</li>
<li>shit deny lie</li>
<li>lies deny hit</li>
<li>hits deny lie</li>
<li>density hi el</li>
<li>density hi le</li>
<li>listen hey di</li>
<li>silent hey di</li>
<li>lines they di</li>
<li>density he il</li>
<li>disney the il</li>
<li>shield yet in</li>
<li>shield tie ny</li>
<li>slide they in</li>
<li>slide tiny he</li>
<li>delhi site ny</li>
<li>delhi tiny se</li>
<li>delhi ties ny</li>
<li>delhi yet sin</li>
<li>density il eh</li>
<li>slide tiny eh</li>
<li>density he li</li>
<li>density eh li</li>
<li>disney the li</li>
<li>slide thin ye</li>
<li>tiny shed lie</li>
<li>elite dish ny</li>
<li>they lied sin</li>
<li>line dish yet</li>
<li>tiny dish lee</li>
<li>tiny lied she</li>
<li>thin lied yes</li>
<li>disney lie th</li>
<li>destiny he il</li>
<li>destiny he li</li>
</ul>

<p>
<b>Disney R. Leith</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>entirely dish</li>
<li>disney hitler</li>
<li>hitler sidney</li>
<li>lindsey their</li>
<li>inherited sly</li>
<li>desire thinly</li>
<li>reside thinly</li>
<li>thirdly seine</li>
<li>lindsey thier</li>
<li>third line yes</li>
<li>henry list die</li>
<li>listed henry i</li>
<li>held rise tiny</li>
<li>lines dirty he</li>
<li>dirty line she</li>
<li>lines hide try</li>
<li>listen hey rid</li>
<li>lines they rid</li>
<li>silent hey rid</li>
<li>style hired in</li>
<li>listed hire ny</li>
<li>shirt deny lie</li>
<li>irish deny let</li>
<li>list hire deny</li>
<li>density her il</li>
<li>shield entry i</li>
<li>shield tiny re</li>
<li>their slide ny</li>
<li>henry slide it</li>
<li>entry slide hi</li>
<li>slide tiny her</li>
<li>entry delhi is</li>
<li>tries delhi ny</li>
<li>side thin rely</li>
<li>thin dies rely</li>
<li>entirely is hd</li>
<li>lines dirty eh</li>
<li>density her li</li>
<li>third lines ye</li>
<li>shield tiny er</li>
<li>lines dirt hey</li>
<li>henry lied its</li>
<li>henry lied sit</li>
<li>entry dish lie</li>
<li>entry lied his</li>
<li>density lie hr</li>
<li>inside rely th</li>
<li>destiny her il</li>
<li>destiny her li</li>
<li>destiny lie hr</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-burt-f-smith" class="outline-2">
<h2 id="burt-f-smith"><span class="section-number-2">72.</span> Burt F. Smith&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="amputee">amputee</span>&#xa0;<span class="devoutcatholic">devoutcatholic</span>&#xa0;<span class="elderly">elderly</span></span></h2>
<div class="outline-text-2" id="text-burt-f-smith">
<p>
An elderly, profoundly devout Catholic Ennet House resident whose hands and feet were amputated after he passed out drunk in a snowbank, leaving him with stumps and a particularly Job-like posture toward his suffering. He is one of the House's quieter, more dignified residents.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>thumb first</b> — His full name rearranges exactly to 'thumb first' — a grim pun on the thumbless amputee whose Job-like faith puts him first in the House's order of devotion.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Burt Smith</b>
</p>
<ul class="org-ul">
<li><b>stub mirth</b> — his stumps and stoic mirth-with-Job (suggestive)</li>
</ul>

<p>
<b>Burt F. Smith</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li><b>thumb first</b> — the thumb-less first-resident with the deepest faith (suggestive — letters check: thumb=t,h,u,m,b; first=f,i,r,s,t — yes uses t,h,u,m,b,f,i,r,s,t — exactly the set)</li>
<li><b>frith stumb</b> — loose; 'frith' is archaic for peace (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Burt Smith</b>
</p>
<ul class="org-ul">
<li>birth must</li>
<li>thumb stir</li>
<li>thrust ibm</li>
<li>thumbs tri</li>
<li>truths ibm</li>
<li>thirst bum</li>
<li>thrust bmi</li>
<li>truths bmi</li>
<li>smith burt</li>
<li>this but mr</li>
<li>brush mitt</li>
<li>shrub mitt</li>
<li>shit but mr</li>
<li>births tum</li>
<li>thumb tris</li>
<li>britt mush</li>
<li>thus bit mr</li>
<li>but hit mrs</li>
<li>shut bit mr</li>
<li>hurt bit ms</li>
<li>hits but mr</li>
<li>butt his mr</li>
<li>butt mrs hi</li>
<li>must bit hr</li>
<li>suit tbh mr</li>
<li>butt him rs</li>
<li>burst im th</li>
<li>burst mi th</li>
<li>birth us mt</li>
<li>birth st um</li>
<li>brush it mt</li>
<li>burst hi mt</li>
<li>rush bit mt</li>
<li>ruth bit ms</li>
<li>truth ms bi</li>
<li>hurts mt bi</li>
<li>bush tim rt</li>
<li>bust him rt</li>
<li>bust hit mr</li>
<li>bust tim hr</li>
<li>butt him sr</li>
<li>truth im bs</li>
<li>truth mi bs</li>
<li>hurt tim bs</li>
<li>ruth tim bs</li>
<li>thumb is rt</li>
<li>thumb it rs</li>
<li>thumb it sr</li>
<li>thumb si rt</li>
<li>tits hub mr</li>
</ul>

<p>
<b>Burt F. Smith</b> <i>(full form)</i>
</p>
<ul class="org-ul">
<li>first thumb</li>
<li>thumbs rift</li>
<li>thrift bums</li>
<li>shift but mr</li>
<li>truth fbi ms</li>
<li>birth sum ft</li>
<li>fish butt mr</li>
<li>brush tim ft</li>
<li>submit ft hr</li>
<li>firms but th</li>
<li>fruit tbh ms</li>
<li>burst him ft</li>
<li>first tbh um</li>
<li>hurts fbi mt</li>
<li>brush fit mt</li>
<li>submit th fr</li>
<li>smith but fr</li>
<li>first hub mt</li>
<li>firm bust th</li>
<li>shirt but fm</li>
<li>hurts bit fm</li>
<li>burst hit fm</li>
<li>hurt bits fm</li>
<li>bits ruth fm</li>
<li>thumb its fr</li>
<li>thumb sir ft</li>
<li>thumb fit rs</li>
<li>thumb fit sr</li>
<li>thumb sit fr</li>
<li>thumb sri ft</li>
<li>smith rub ft</li>
<li>shift rub mt</li>
<li>hurts ibm ft</li>
<li>thumb irs ft</li>
<li>bush trim ft</li>
<li>truth ibm sf</li>
<li>smith fur bt</li>
<li>thus firm bt</li>
<li>firm shut bt</li>
<li>thrust bi fm</li>
<li>firm butt sh</li>
<li>surf tim tbh</li>
<li>smith tub fr</li>
<li>shirt tub fm</li>
<li>shift tub mr</li>
<li>firms tub th</li>
<li>firms hut bt</li>
<li>bits thru fm</li>
<li>fruits bt hm</li>
<li>first but hm</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-corbett-thorp" class="outline-2">
<h2 id="corbett-thorp"><span class="section-number-2">73.</span> Corbett Thorp&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="prorector">prorector</span>&#xa0;<span class="coach">coach</span>&#xa0;<span class="staff">staff</span></span></h2>
<div class="outline-text-2" id="text-corbett-thorp">
<p>
An ETA prorector — one of the post-graduate coaching assistants who run drills, supervise matches, and shepherd the junior players through the academy's grueling daily schedule. He surfaces mostly as a name on the prorector roster.
</p>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Corbett Thorp</b>
</p>
<ul class="org-ul">
<li><b>bother corp tt</b> — loose; 'bother' fits the prorector who hectors juniors (suggestive)</li>
<li><b>trot the proc b</b> — loose; the prorector's trot through procedure (loose)</li>
<li><b>the brort copt</b> — loose nonsense (loose)</li>
<li><b>perch trot bot</b> — letter mismatch (no extra letter check); disregard (loose)</li>
<li><b>bot perch trot</b> — loose; coach robotically perched, repeating trot drills (suggestive)</li>
<li><b>trotter chop b</b> — letter mismatch; disregard (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Corbett Thorp</b>
</p>
<ul class="org-ul">
<li>protector tbh</li>
<li>protect broth</li>
<li>robert http co</li>
<li>protect bro th</li>
<li>protect rob th</li>
<li>protect tbh or</li>
<li>report oct tbh</li>
<li>brother top ct</li>
<li>brother pot ct</li>
<li>report both ct</li>
<li>bother port ct</li>
<li>brother oct pt</li>
<li>potter both cr</li>
<li>porter both ct</li>
<li>porter oct tbh</li>
<li>robot perth ct</li>
<li>brother opt ct</li>
<li>protect hot br</li>
<li>protect tho br</li>
<li>photo brett cr</li>
<li>brett corp hot</li>
<li>brett corp tho</li>
<li>brett crop hot</li>
<li>brett crop tho</li>
<li>brett porch to</li>
<li>brett porch ot</li>
<li>protect bot hr</li>
<li>protect hbo rt</li>
<li>brett torch op</li>
<li>brett torch po</li>
<li>torch port bet</li>
<li>protect hbo tr</li>
<li>protect hot rb</li>
<li>protect tho rb</li>
<li>brett chop rot</li>
<li>potter thor bc</li>
<li>potter thor cb</li>
<li>brett troop ch</li>
<li>brett thor cop</li>
<li>potter both rc</li>
<li>photo brett rc</li>
<li>brett troop hc</li>
<li>protect tbh ro</li>
<li>robot http rec</li>
<li>brother cop tt</li>
<li>robert chop tt</li>
<li>bother corp tt</li>
<li>bother crop tt</li>
<li>probe torch tt</li>
<li>hotter port bc</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-emil-minty" class="outline-2">
<h2 id="emil-minty"><span class="section-number-2">74.</span> Emil Minty&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="punk">punk</span>&#xa0;<span class="heroinaddict">heroinaddict</span>&#xa0;<span class="nihilistic">nihilistic</span></span></h2>
<div class="outline-text-2" id="text-emil-minty">
<p>
A young punk-rock heroin addict at Ennet House, sullen and aggressively unrepentant, who sports the ruined teeth and theatrical nihilism of the late-stage street junkie. He is one of the residents least convinced by the Program.
</p>

<aside class="ij-pick ij-pick-strong ij-pick-name-analysis">

<p>
<b><b>Most likely reading:</b></b> <b>Minty = fresh breath, clean teeth</b> — The surname's connotation of mint-fresh dental hygiene is a pointed counter-image to a punk junkie whose ruined teeth are part of his signature description.
</p>

<p class="ij-pick-meta">

<p>
<i>[name analysis · entendre · strong]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Entendre]</i> <b>Minty</b> — Surname evokes fresh breath and clean teeth — a pointed counter-image to a punk junkie described as having ruined teeth.</li>
<li><i>[Entendre]</i> <b>Minty = fresh breath, clean teeth</b> — The surname's connotation of mint-fresh dental hygiene is a pointed counter-image to a punk junkie whose ruined teeth are part of his signature description.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Emil Minty</b>
</p>
<ul class="org-ul">
<li><b>limit men y</b> — loose; the punk's nihilistic limit on men's recovery talk (loose)</li>
<li><b>mint my lie</b> — loose; mint-fresh teeth he doesn't have, the lie of the punk pose (suggestive)</li>
<li><b>Emily mint</b> — loose; cross-gender pun on his name (loose)</li>
<li><b>in lime my t</b> — loose (loose)</li>
<li><b>my mint lie</b> — the addict's denial as a minted lie (suggestive)</li>
<li><b>limit nmey</b> — loose (loose)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Emil Minty</b>
</p>
<ul class="org-ul">
<li>emily mint</li>
<li>timely min</li>
<li>miley mint</li>
<li>timmy line</li>
<li>timmy neil</li>
<li>timmy nile</li>
<li>timmy lien</li>
<li>enmity mil</li>
<li>enmity lim</li>
<li>tiny lie mm</li>
<li>line tim my</li>
<li>limit my en</li>
<li>mini let my</li>
<li>limit me ny</li>
<li>limit em ny</li>
<li>mile tim ny</li>
<li>mine lit my</li>
<li>neil tim my</li>
<li>emily in mt</li>
<li>intel my im</li>
<li>intel my mi</li>
<li>limit my ne</li>
<li>mine til my</li>
<li>mile tin my</li>
<li>emily mt ni</li>
<li>mint lie my</li>
<li>mini tel my</li>
<li>lime tim ny</li>
<li>lime tin my</li>
<li>mini yet ml</li>
<li>timely nm i</li>
<li>limit ye nm</li>
<li>emily it nm</li>
<li>emily ti nm</li>
<li>time lin my</li>
<li>tiny mel im</li>
<li>tiny mel mi</li>
<li>item lin my</li>
<li>mile int my</li>
<li>lime int my</li>
<li>emily im nt</li>
<li>emily mi nt</li>
<li>time mil ny</li>
<li>line mit my</li>
<li>tiny eli mm</li>
<li>tiny mil me</li>
<li>tiny mil em</li>
<li>mile mit ny</li>
<li>item mil ny</li>
<li>neil mit my</li>
</ul>

</details>
</div>
</div>
<div id="outline-container-reginald" class="outline-2">
<h2 id="reginald"><span class="section-number-2">75.</span> Reginald&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infinitejest">infinitejest</span>&#xa0;<span class="wardinesection">wardinesection</span>&#xa0;<span class="menacing">menacing</span>&#xa0;<span class="abusive">abusive</span></span></h2>
<div class="outline-text-2" id="text-reginald">
<p>
A figure in the Wardine vignette — the menacing son (or step-figure) in the household whose looming presence is part of the chain of abuse Wardine endures. He appears only in that ebonics-rendered set piece narrated by Clenette.
</p>

<aside class="ij-pick ij-pick-suggestive ij-pick-llm-anagram">

<p>
<b><b>Most likely reading:</b></b> <b>danger li</b> — The name rearranges almost cleanly to 'danger,' naming the menacing presence that looms over Wardine in Clenette's vignette.
</p>

<p class="ij-pick-meta">

<p>
<i>[anagram · suggestive]</i>
</p>
</p>

</aside>

<section class="ij-register ij-register-name-analysis">

<p>
<b><b>Name analysis</b></b> <i>(non-anagram wordplay: etymology, puns, allusions)</i>
</p>

<ul class="org-ul">
<li><i>[Etymology]</i> <b>Reginald (Germanic: 'ruler's counsel/power')</b> — The name derives from Germanic 'ragin' (counsel) + 'wald' (rule/power) — a regal, dominating etymology that aligns with his menacing, authoritarian role in the Wardine household.</li>
</ul>

</section>

<section class="ij-register ij-register-possible-anagram">

<p>
<b><b>Possible anagrams</b></b> <i>(thematically chosen)</i>
</p>

<p>
<b>Reginald</b>
</p>
<ul class="org-ul">
<li><b>darling e</b> — ironic — the menacing 'darling' of the abusive household (suggestive)</li>
<li><b>danger li</b> — the menacing presence in Wardine's home (strong)</li>
<li><b>denial rg</b> — loose; the household's denial as it churns abuse (loose)</li>
<li><b>engrail d</b> — loose; 'engrail' is archaic for indent — the indent he leaves on Wardine (loose)</li>
<li><b>garden li</b> — ironic — the sealed-off garden of the projects, locked-in violence (suggestive)</li>
</ul>

</section>

<details class="ij-register ij-register-algorithmic">

<summary>

<p>
<b><b>Algorithmic candidates</b></b> <i>(subset-sum search over 30K-word vocab)</i>
</p>
</summary>

<p>
<b>Reginald</b>
</p>
<ul class="org-ul">
<li>reginald</li>
<li>grand lie</li>
<li>deal ring</li>
<li>lead ring</li>
<li>angel rid</li>
<li>girl dean</li>
<li>angle rid</li>
<li>garden il</li>
<li>danger il</li>
<li>line drag</li>
<li>garden li</li>
<li>danger li</li>
<li>learn dig</li>
<li>grain led</li>
<li>grain del</li>
<li>lane grid</li>
<li>drag neil</li>
<li>grid lean</li>
<li>drain leg</li>
<li>ring dale</li>
<li>grande il</li>
<li>grande li</li>
<li>reign lad</li>
<li>raid glen</li>
<li>ride lang</li>
<li>reid lang</li>
<li>drain gel</li>
<li>range lid</li>
<li>anger lid</li>
<li>grade lin</li>
<li>edgar lin</li>
<li>gerald in</li>
<li>gerald ni</li>
<li>ranged il</li>
<li>ranged li</li>
<li>grand eli</li>
<li>laden rig</li>
<li>lied rang</li>
<li>daring el</li>
<li>daring le</li>
<li>rang idle</li>
<li>linda reg</li>
<li>grind ale</li>
<li>glad erin</li>
<li>regain dl</li>
<li>lang dire</li>
<li>ridge lan</li>
<li>drag nile</li>
<li>grid neal</li>
<li>align red</li>
</ul>

</details>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>VOMPECCC from Scratch: Picking Produce with ICR in Emacs</title>
      <link>https://www.chiply.dev/post-vompeccc-fruits</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-vompeccc-fruits</guid>
      <pubDate>Tue, 28 Apr 2026 09:14:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>This is the fourth post in a series on Emacs completion. The first argued that Incremental Completing Read (ICR) is a structural property of an interface rather than a convenience feature. The second ...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="completion">completion</span>&#xa0;<span class="walkthrough">walkthrough</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org84a0e4e" class="figure">
<p><img src="https://www.chiply.dev/images/vompeccc-fruits-banner.jpeg" alt="vompeccc-fruits-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/dall-e-3">DALL-E 3</a></p>
</div>

<p>
This is the fourth post in a series on Emacs completion.  The <a href="https://www.chiply.dev/post-icr-primer">first</a> argued that <a href="https://www.chiply.dev/post-icr-primer#what-is-incremental-completing-read-">Incremental Completing Read (ICR)</a> is a <i>structural</i> property of an interface rather than a convenience feature.  The <a href="https://www.chiply.dev/post-vompeccc">second</a> broke the Emacs substrate into eight packages (collectively <a href="https://www.chiply.dev/post-vompeccc#vompeccc-framework">VOMPECCC</a>) each solving one of the <a href="https://www.chiply.dev/post-vompeccc#hidden-complexity">six orthogonal concerns</a> of a complete completion system.  The <a href="https://www.chiply.dev/post-vompeccc-spot">third</a> walked through <code>spot</code>, a ~1,100-line Spotify client built as a little <a href="https://www.chiply.dev/post-vompeccc-spot#candidates-as-currency">shim</a> on top of those packages.
</p>

<p>
This post is the hands-on complement to the <code>spot</code> post.  Where the <code>spot</code> case study reviewed a finished codebase from the outside, this one builds a tiny produce picker tool from scratch, one VOMPECCC package at a time.  The use case is deliberately trivial: we have a list of produce items (twenty fruits and ten vegetables) with some metadata, and we want to pick one and do something with it.  
</p>

<p>
Every piece of interesting behavior; the display control, multi-component matching, metadata columns, narrowing keys, contextual actions, transformer-driven type refinement, frecency-based sorting; will be layered on by adding one VOMPECCC package at a time, in order to make it clear exactly what each package provides.  By the end, we will have a ~90-line produce picker whose entire UI was composed from six packages that don't know about each other.
</p>

<blockquote class="pull-quote pull-right">
<p>
We will build a ~90-line produce picker whose entire UI was composed from six packages that don't know about each other.
</p>
</blockquote>

<p>
A caveat: two of the eight VOMPECCC packages are out of scope here.  <a href="https://www.chiply.dev/post-vompeccc#corfu">Corfu</a> and <a href="https://www.chiply.dev/post-vompeccc#cape">Cape</a> target in-buffer completion, and the produce picker in this post is focused on minibuffer interaction.  So this post focuses on the six packages that <i>do</i> show up visibly in a live walkthrough: <b>Vertico</b>, <b>Orderless</b>, <b>Marginalia</b>, <b>Prescient</b>, <b>Embark</b>, and <b>Consult</b>.
</p>

<p>
<b>A note on format.</b>  You can open this webpage in EWW and execute the code from within there (you can see my video for an example of how to do this).
</p>
</div>
</div>
<div id="outline-container-walkthrough" class="outline-2">
<h2 id="walkthrough"><span class="section-number-2">2.</span> The Walkthrough&#xa0;&#xa0;&#xa0;<span class="tag"><span class="demo">demo</span></span></h2>
<div class="outline-text-2" id="text-walkthrough">
<p>
This video demo walks through the code in this post live.  I have also included prose in each section explaining what each piece of code does.  The article is long, so the video will likely be a quicker digestion of this post's message.
</p>

<p>
<b>Note</b>: wherever you see video demos, you will see, in the upper right hand side (in the <code>tab-bar</code>), the keybindings and associated commands that I am invoking to execute each command.  This is relevant because you may have things configured differently on your side.  By providing both the kbd and command name, my invocation of behaviors you see in the video should unambiguous.
</p>

<p>
<div class="youtube-container">
<iframe src="https://www.youtube-nocookie.com/embed/0NeQ6xgRGkA"
        title="Building a ~90-line produce picker from scratch &#x2014; Vertico, Orderless, Marginalia, Consult, Embark, and Prescient layered one phase at a time."
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
</iframe>
</div>
</p>
</div>
</div>
<div id="outline-container-setup" class="outline-2">
<h2 id="setup"><span class="section-number-2">3.</span> The Produce Corpus&#xa0;&#xa0;&#xa0;<span class="tag"><span class="setup">setup</span>&#xa0;<span class="data">data</span></span></h2>
<div class="outline-text-2" id="text-setup">
<p>
The data is a list of <i>candidates</i>, which in this implementation are propertized strings, one per produce item.  Each candidate is the produce item's name, with five text properties riding along: <code>category</code>, <code>type</code>, <code>color</code>, <code>season</code>, and <code>price</code>.  Two of those properties are load-bearing for later phases.  <code>category</code> is the <i>completion category</i>, the symbol Marginalia and Embark dispatch on.
</p>

<p>
A word on the name <code>category</code>, because it is doing more work than it looks like.  Of the five property names above, four are arbitrary: <code>type</code>, <code>color</code>, <code>season</code>, <code>price</code> are labels we chose, nothing in Emacs reserves them, and you could rename them all and only have to update our own code.  <code>category</code> is the exception because it is a <i>reserved text-property name</i> in Emacs.  The <a href="https://www.gnu.org/software/emacs/manual/html_mono/elisp.html#Special-Properties">Elisp manual</a> specifies that when a character has a <code>category</code> text property whose value is a symbol, that symbol's property list serves as defaults for the character's other text properties.  In other words, <code>category</code> is Emacs's standard hook for stamping a <i>typed symbol</i> onto a piece of text.  Emacs's completion ecosystem overloads the same name with a second, related meaning, which is that every prompt has a <i>completion category</i> (a symbol like <code>file</code>, <code>buffer</code>, or in our case <code>fruit</code> or <code>vegetable</code>), which Marginalia, Embark, etc&#x2026; consult through the <a href="https://www.gnu.org/software/emacs/manual/html_mono/elisp.html#Completion-Variables">completion metadata</a> to decide which annotator, keymap, or exporter to dispatch.
</p>

<p>
There is one important subtlety worth surfacing now, since it explains a lot of what happens later.  <i>The framework never reads our <code>category</code> text property directly.</i>  Marginalia and Embark dispatch off the prompt-level <i>completion metadata</i> (a separate channel from text properties), and Consult, when we add it in Phase 5, communicates per-candidate categories through a different text property called <code>multi-category</code> rather than ours.  So our <code>category</code> property is read only by <i>our own</i> code: a corpus-filtering helper in Phase 4, an Embark exporter in Phase 6, etc&#x2026;.  The framework dispatches because we set the Consult source's <code>:category</code> key to match the data's <code>category</code> property by hand.  The two stay in sync because we keep them in sync, not because anyone cross-checks.  This is the candidate-as-currency convention being load-bearing for <i>us</i>, with the framework reading a parallel, framework-owned slot for its own dispatch.
</p>

<p>
<code>type</code> is the finer classification (botanical: <code>pome</code>, <code>berry</code>, <code>citrus</code>, <code>stone</code>, <code>tropical</code>, <code>melon</code>; vegetable: <code>root</code>, <code>leafy</code>, <code>cruciferous</code>, <code>nightshade</code>) used in Phase 6 to drive an Embark transformer that gives citrus its own action set.  Both classifications live on the candidate itself.  No framework code in any phase below ever invents a category or hardcodes a type, and the routing keys are pulled directly off the candidates.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-produce</span>
  <span style="color: #4f54aa;">(</span>list
   <span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">Fruits
</span>   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"apple"</span>      'category 'fruit     'type 'pome        'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>    'season <span style="color: #4250ef;">"fall"</span>       'price 1.29<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"pear"</span>       'category 'fruit     'type 'pome        'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"fall"</span>       'price 1.79<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"strawberry"</span> 'category 'fruit     'type 'berry       'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>    'season <span style="color: #4250ef;">"spring"</span>     'price 3.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"blueberry"</span>  'category 'fruit     'type 'berry       'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #0000ff;">blue</span><span style="color: #4250ef;">"</span>   'season <span style="color: #4250ef;">"summer"</span>     'price 4.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"raspberry"</span>  'category 'fruit     'type 'berry       'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>    'season <span style="color: #4250ef;">"summer"</span>     'price 5.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"blackberry"</span> 'category 'fruit     'type 'berry       'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #000000;">black</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"summer"</span>     'price 5.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"lemon"</span>      'category 'fruit     'type 'citrus      'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #ffff00;">yellow</span><span style="color: #4250ef;">"</span> 'season <span style="color: #4250ef;">"year-round"</span> 'price 0.79<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"lime"</span>       'category 'fruit     'type 'citrus      'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"year-round"</span> 'price 0.39<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"orange"</span>     'category 'fruit     'type 'citrus      'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"winter"</span>     'price 0.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"grapefruit"</span> 'category 'fruit     'type 'citrus      'color <span style="color: #4250ef;">"pink"</span>   'season <span style="color: #4250ef;">"winter"</span>     'price 1.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"peach"</span>      'category 'fruit     'type 'stone       'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"summer"</span>     'price 2.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"plum"</span>       'category 'fruit     'type 'stone       'color <span style="color: #4250ef;">"purple"</span> 'season <span style="color: #4250ef;">"summer"</span>     'price 2.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"cherry"</span>     'category 'fruit     'type 'stone       'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>    'season <span style="color: #4250ef;">"summer"</span>     'price 6.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"apricot"</span>    'category 'fruit     'type 'stone       'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"summer"</span>     'price 3.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"mango"</span>      'category 'fruit     'type 'tropical    'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"summer"</span>     'price 1.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"pineapple"</span>  'category 'fruit     'type 'tropical    'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #ffff00;">yellow</span><span style="color: #4250ef;">"</span> 'season <span style="color: #4250ef;">"year-round"</span> 'price 3.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"banana"</span>     'category 'fruit     'type 'tropical    'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #ffff00;">yellow</span><span style="color: #4250ef;">"</span> 'season <span style="color: #4250ef;">"year-round"</span> 'price 0.59<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"papaya"</span>     'category 'fruit     'type 'tropical    'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"year-round"</span> 'price 2.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"watermelon"</span> 'category 'fruit     'type 'melon       'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"summer"</span>     'price 0.59<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"cantaloupe"</span> 'category 'fruit     'type 'melon       'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"summer"</span>     'price 2.99<span style="color: #ba35af;">)</span>
   <span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">Vegetables
</span>   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"carrot"</span>     'category 'vegetable 'type 'root        'color <span style="color: #4250ef;">"orange"</span> 'season <span style="color: #4250ef;">"year-round"</span> 'price 0.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"beet"</span>       'category 'vegetable 'type 'root        'color <span style="color: #4250ef;">"purple"</span> 'season <span style="color: #4250ef;">"fall"</span>       'price 1.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"radish"</span>     'category 'vegetable 'type 'root        'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>    'season <span style="color: #4250ef;">"spring"</span>     'price 0.79<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"spinach"</span>    'category 'vegetable 'type 'leafy       'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"spring"</span>     'price 2.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"kale"</span>       'category 'vegetable 'type 'leafy       'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"winter"</span>     'price 2.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"lettuce"</span>    'category 'vegetable 'type 'leafy       'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"summer"</span>     'price 1.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"broccoli"</span>   'category 'vegetable 'type 'cruciferous 'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #00ff00;">green</span><span style="color: #4250ef;">"</span>  'season <span style="color: #4250ef;">"year-round"</span> 'price 2.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"cauliflower"</span> 'category 'vegetable 'type 'cruciferous 'color <span style="color: #4250ef;">"</span><span style="color: #000000; background-color: #ffffff;">white</span><span style="color: #4250ef;">"</span> 'season <span style="color: #4250ef;">"fall"</span>       'price 3.99<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"tomato"</span>     'category 'vegetable 'type 'nightshade  'color <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>    'season <span style="color: #4250ef;">"summer"</span>     'price 2.49<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span>propertize <span style="color: #4250ef;">"eggplant"</span>   'category 'vegetable 'type 'nightshade  'color <span style="color: #4250ef;">"purple"</span> 'season <span style="color: #4250ef;">"summer"</span>     'price 3.49<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Produce candidates (fruits + vegetables) for the VOMPECCC walkthrough."</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Thirty propertized strings, two completion categories (<code>fruit</code>, <code>vegetable</code>), ten types (six botanical + four vegetable), and four additional properties.  For the rest of this post, <i>produce candidate</i> means one of these propertized strings: a plain Emacs string at the surface (the produce name, if you will) with its remaining fields as text properties.
</p>
</div>
<div id="outline-container-candidate-shape" class="outline-3">
<h3 id="candidate-shape"><span class="section-number-3">3.1.</span> Why a propertized string?</h3>
<div class="outline-text-3" id="text-candidate-shape">
<p>
The post is going to lean on one specific claim: <i>the candidate is the unit of currency that flows between every layer of the substrate</i>.  When we say "every package consumes the same currency without knowing about each other," we mean the propertized string above is what gets used by the built-in Emacs substrate and the VOMPECCC packages.  The shape we chose for <code>my-produce</code> is what makes the thesis cash out.
</p>

<p>
It is worth pausing on, because the same data could plausibly have been a list of plists, an alist of cons cells, a hash table from name to record, or a programmed completion function, etc&#x2026;. and <code>completing-read</code> accepts <i>all</i> of those as collections.  No VOMPECCC package constrains the shape further, so strictly speaking, <i>any</i> shape that produces strings would work.  The question is what each shape costs the consumer code, and the answer is relevant to the rest of this post.
</p>

<p>
<b>1. Properties survive the round trip.</b>  When <code>completing-read</code> returns the chosen candidate, it returns the <i>exact propertized string you put in</i>.  Properties intact.  Your Embark action receives <code>"apple"</code> with all its text properties; your transformer receives <code>"apple"</code> with <code>type pome</code> still attached; the exporter receives the full set.  The entire candidate-as-currency story rests on this: domain data and candidate identity are <i>the same object</i>.  You hand the framework a propertized string, and you get a propertized string back, and you can read whatever properties you stamped on without ever leaving the candidate.
</p>

<p>
<b>2. Text properties are the framework's integration channel.</b>  Three concrete examples from the packages below:
</p>

<ul class="org-ul">
<li>Vertico applies <code>face</code> text properties for display.</li>
<li>Consult writes a <code>multi-category</code> text property to thread per-candidate types through Embark's dispatcher.</li>
<li>Embark's built-in transformer reads <code>multi-category</code> off the candidate.</li>
</ul>

<p>
These packages were <i>designed</i> assuming candidates are strings with rich text properties.  The two framework-owned properties (<code>face</code> and <code>multi-category</code>) coexist on the same string object alongside <i>our</i> <code>category</code>, <code>type</code>, <code>color</code>, <code>season</code>, <code>price</code>, because a propertized string supports arbitrary properties without conflict.  An alist or hash-table shape would force you to translate to strings somewhere on the way into the framework, and that breaks the integration, or at least makes it more difficult.
</p>

<p>
<b>3. No sidecar state.</b>  The plausible alternative is plain strings plus a hash table mapping <code>name → record</code>.  It works, but introduces:
</p>

<ul class="org-ul">
<li>A second piece of state to keep in sync with the candidate list.</li>
<li>A <code>(gethash cand my-records)</code> step in every annotator, action, transformer, and exporter.  This is 'lookup' or 'rehydration'.  Best avoided, and I can only think of this being useful if a candidate has a huge number of mostly unneeded properties, which is rare in practice.</li>
<li>A bug class around duplicate names: the hash table can't disambiguate, and the candidate string by itself carries no other identifying information.</li>
</ul>

<p>
Propertized strings collapse this into "the candidate <i>is</i> the record."  Each <code>propertize</code> call mints a fresh string object whose properties are bound to that exact instance.
</p>

<p>
<b>What is <i>not</i> uniquely required.</b>  Some things look more constrained than they are.
</p>

<ul class="org-ul">
<li>The property name <code>category</code> is <i>not</i> required by the framework.  No VOMPECCC package reads our <code>category</code> text property; we discussed this above, and our filter helper and exporter are the only consumers.  We could rename it to <code>kind</code> or <code>class</code> and only our own code would change.  <code>category</code> was chosen for alignment with Emacs's vocabulary and the completion ecosystem's conventions, not for compatibility.</li>
<li>Flat properties vs. one <code>multi-data</code> bag is <i>not</i> required either.  This corpus uses individual properties because the records are shallow and the property bag stays small, so every consumer asks one <code>get-text-property</code> question and gets one answer.  In codebases like <a href="https://www.chiply.dev/post-vompeccc-spot"><code>spot</code></a>, where each candidate is a Spotify track or playlist with dozens of nested fields, the convention is to attach the full record under a single <code>multi-data</code> property and let consumers reach into the plist for deep fields.  Both routes meet the substrate at the same hook.</li>
<li>Human readability is <i>not</i> required.  The candidate string is just an identifier as far as <code>completing-read</code> is concerned.  You could put a UUID and have the annotator render the human name as a prefix.  Most packages use the string as the visible name because it is simpler, not because they have to.</li>
</ul>

<p>
<b>The shape, in one sentence.</b>  A list of propertized strings is the shape that lets every VOMPECCC layer participate without your code translating between a "candidate" representation (what the framework sees) and a "record" representation (what your annotator, action, and transformer need).  Every other shape forces that translation somewhere.  Candidate-as-currency means: don't translate.  Pass the same object end to end.
</p>
</div>
</div>
</div>
<div id="outline-container-reset" class="outline-2">
<h2 id="reset"><span class="section-number-2">4.</span> Phase 0: Resetting to Stock&#xa0;&#xa0;&#xa0;<span class="tag"><span class="reset">reset</span>&#xa0;<span class="setup">setup</span></span></h2>
<div class="outline-text-2" id="text-reset">
<p>
If you are reading this post in your own Emacs, your config may already have VOMPECCC packages enabled, which would muddy this demo of built-in completion.  Run this block first to peel them off so the baseline really is the baseline.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">Reset completion-styles and category configuration to Emacs defaults.
</span><span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> completion-styles '<span style="color: #4f54aa;">(</span>basic partial-completion emacs22<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> completion-category-defaults nil<span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> completion-category-overrides nil<span style="color: #008858;">)</span>

<span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">Drop any custom Orderless wiring so dispatchers and matching styles don't leak in.
</span><span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span>boundp 'orderless-style-dispatchers<span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">setq</span> orderless-style-dispatchers nil<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span>boundp 'orderless-component-separator<span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">setq</span> orderless-component-separator <span style="color: #4250ef;">" "</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span>boundp 'orderless-matching-styles<span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">setq</span> orderless-matching-styles '<span style="color: #ba35af;">(</span>orderless-literal orderless-regexp<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">Disable every VOMPECCC mode the post will switch back on layer by layer.
</span><span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">bound-and-true-p</span> vertico-prescient-mode<span style="color: #4f54aa;">)</span> <span style="color: #4f54aa;">(</span>vertico-prescient-mode -1<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">bound-and-true-p</span> prescient-persist-mode<span style="color: #4f54aa;">)</span> <span style="color: #4f54aa;">(</span>prescient-persist-mode -1<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">bound-and-true-p</span> vertico-mode<span style="color: #4f54aa;">)</span> <span style="color: #4f54aa;">(</span>vertico-mode -1<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">when</span> <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">bound-and-true-p</span> marginalia-mode<span style="color: #4f54aa;">)</span> <span style="color: #4f54aa;">(</span>marginalia-mode -1<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
After this block, <code>completing-read</code> should behave the way Emacs ships out of the box: a <code>*Completions*</code> buffer instead of a vertical list, prefix-only and partial-completion matching, no annotations, no contextual actions, and no 'frecency'.  Phase 1 below demonstrates exactly that, and every subsequent phase adds one layer back.
</p>
</div>
</div>
<div id="outline-container-baseline" class="outline-2">
<h2 id="baseline"><span class="section-number-2">5.</span> Phase 1: The Baseline (Stock <code>completing-read</code>)&#xa0;&#xa0;&#xa0;<span class="tag"><span class="baseline">baseline</span></span></h2>
<div class="outline-text-2" id="text-baseline">
<p>
Before we pull in a single VOMPECCC package, it is worth seeing what the built-in Emacs completion already gives us.  <code>completing-read</code> is a function in the Emacs standard library: it prompts the user for an input string, filters candidates based on that string, and then returns the chosen string.  That is the entire contract.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>completing-read <span style="color: #4250ef;">"Pick something: "</span> my-produce<span style="color: #008858;">)</span>
</pre>
</div>

<p>
When this block runs, the minibuffer opens with the prompt <code>Pick something: = and a blinking cursor.  Nothing else is visible at first.  Press =TAB</code> and a <code>*Completions*</code> buffer pops open above the minibuffer, laying all thirty produce names across columns.  Type a prefix like <code>pe</code> and press <code>TAB</code> again; the <code>*Completions*</code> buffer shrinks to the two produce items whose names start with those letters (<code>peach</code> and <code>pear</code>).  Once you pick an item (either by arrowing to it in <code>*Completions*</code> or typing its full name), you accept it with <code>RET</code> and the chosen string echoes into the message area.
</p>

<p>
This works, but it is visually and ergonomically primitive.  Right now we are lacking display control for our candidate list, fuzzy matching, metadata display and filtering, preview, and any way of doing anything <i>with</i> the chosen item except receive its name.
</p>

<blockquote class="pull-quote pull-left">
<p>
Every phase that follows places layers of the VOMPECCC stack <i>around</i> this function without changing the function itself.
</p>
</blockquote>

<p>
Every phase that follows places layers of the VOMPECCC stack <i>around</i> this function without changing the function itself.
</p>
</div>
</div>
<div id="outline-container-vertico" class="outline-2">
<h2 id="vertico"><span class="section-number-2">6.</span> Phase 2: Vertico &#x2014;  Display Control&#xa0;&#xa0;&#xa0;<span class="tag"><span class="vertico">vertico</span>&#xa0;<span class="display">display</span></span></h2>
<div class="outline-text-2" id="text-vertico">
<p>
<a href="https://www.chiply.dev/post-vompeccc#vertico">Vertico</a> only does one thing; it just gives us control over how candidates are displayed in the minibuffer.  It does not filter, sort, annotate, or act on anything.  Any code that calls <code>completing-read</code>; whether it's yours, Emacs's, a third-party package's; is rendered according Vertico's display settings if it is enabled.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>vertico-mode 1<span style="color: #008858;">)</span>
</pre>
</div>

<p>
That is the entire integration!  Re-evaluate the Phase 1 block:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>completing-read <span style="color: #4250ef;">"Pick something: "</span> my-produce<span style="color: #008858;">)</span>
</pre>
</div>

<p>
This time the <code>*Completions*</code> buffer never appears.  Instead, the minibuffer lays out candidates horizontally (my default display style for Vertico).  The prompt still sits at the bottom, but to it's right is a flat array of all thirty produce items, one per line, with the first one (<code>apple</code>) highlighted as the current selection.  <code>C-n</code> (or <code>C-j</code> in my config) walks the highlight to the right through <code>pear</code>, <code>strawberry</code>, <code>blueberry</code>, and so on through the list; <code>C-p</code> (or <code>C-k</code> in my config) walks it left.  <code>RET</code> accepts whichever candidate is currently highlighted.  The candidate list filters incrementally whenever we type a letter: typing a single <code>p</code> on the empty prompt collapses it to the 12 items whose names contains <code>p</code>, and each additional character narrows further in real time, with the selection snapping to the first surviving candidate.
</p>

<p>
Notice our <code>completing-read</code> function call did not change, and, critically, we did not pass Vertico the candidates.  Vertico hooked into the minibuffer-setup pipeline at a lower level, and Emacs routes candidates to it.  This is critical because this means Vertico now gives us candidate display control <i>everywhere <code>completing-read</code> is used</i>, <b>without us havign to anything except enable vertico mode</b>!
</p>
</div>
</div>
<div id="outline-container-orderless" class="outline-2">
<h2 id="orderless"><span class="section-number-2">7.</span> Phase 3: Orderless &#x2014; Multi-Component Matching&#xa0;&#xa0;&#xa0;<span class="tag"><span class="orderless">orderless</span>&#xa0;<span class="filtering">filtering</span></span></h2>
<div class="outline-text-2" id="text-orderless">
<p>
Vertico gave us a better <i>view</i>, but the <i>matching</i> is still the default combination of <code>basic</code>, <code>partial-completion</code>, and <code>emacs22</code>.  These styles handle prefix and hyphen-segmented matches, but we lack a way to type more than one independent fragment, do any flex matching, negation, or any way to filter against candidate metadata.
</p>

<p>
<a href="https://www.chiply.dev/post-vompeccc#orderless">Orderless</a> is a <i>completion style</i>: it plugs into the <code>completion-styles</code> variable and changes how the input string is matched against candidates during <code>completing-read</code>.  Orderless in practice reveals its namesake: it lets us split the input on a separator character, and return candidates that contain <i>every</i> component, in <i>any</i> <b>order</b>.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> orderless-component-separator <span style="color: #4250ef;">","</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> orderless-matching-styles '<span style="color: #4f54aa;">(</span>orderless-regexp<span style="color: #4f54aa;">)</span>
      completion-styles '<span style="color: #4f54aa;">(</span>orderless basic<span style="color: #4f54aa;">)</span>
      completion-category-defaults nil
      completion-category-overrides '<span style="color: #4f54aa;">(</span><span style="color: #ba35af;">(</span>file <span style="color: #1f77bb;">(</span>styles basic partial-completion<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">A small `</span><span style="color: #065fff;">tab</span><span style="color: #7fff7fff7fff;">' shim style: TAB completion only, no fall-through.
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">In-buffer completion (Corfu, the in-buffer counterpart from Post 2)
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">uses it; minibuffer prompts ignore it and pass straight through to Orderless.
</span><span style="color: #008858;">(</span>add-to-list 'completion-styles-alist
             '<span style="color: #4f54aa;">(</span>tab completion-basic-try-completion ignore
                   <span style="color: #4250ef;">"Completion style which provides TAB completion only."</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> completion-styles '<span style="color: #4f54aa;">(</span>tab orderless basic<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Note: The <code>basic</code> fallback matters because a handful of Emacs features (TRAMP host completion, for example) require prefix-style matching that Orderless does not handle; <code>basic</code> catches those.  
</p>

<p>
A few notes on the configuration we set up: 
</p>
<ul class="org-ul">
<li><code>orderless-matching-styles</code> is the chain Orderless uses against any input component that no dispatcher has claimed, and setting it to <code>orderless-regexp</code> is my personal recommendation.</li>
<li>The file-category override (defined in <code>completion-category-overrides</code>) is an idiomatic exception so that <code>~/d/s</code> expands to <code>~/Documents/source/</code>.</li>
<li>The separator is set to a comma because Orderless's default is a space, and spaces are valuable inside candidate strings because search candidates (and their annotations) often contain whitespace.  Reassigning the separator to a rarely-occurring character (<code>,</code>) keeps spaces available as part of any component you might want to match.</li>
<li>The <code>tab</code> style at the head of <code>completion-styles</code> is a one-line concession to in-buffer completion, because Corfu uses it for plain TAB completion in code buffers, but know that the minibuffer prompts ignore it and fall through to Orderless.</li>
</ul>

<p>
Re-run the produce prompt:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>completing-read <span style="color: #4250ef;">"Pick something: "</span> my-produce<span style="color: #008858;">)</span>
</pre>
</div>

<p>
You see Orderless in action when you type more than one component separated by commas.  Typing <code>a,e</code> narrows the list to every item containing both an <code>a</code> <i>and</i> an <code>e</code> somewhere, in either order (Order-<b>less</b>, remember? 😜).  Each comma-separated component is an independent filter, and the <b>intersection</b> (in the set algebra sense) of their matches is what survives in the candidate list display.
</p>

<p>
Individual components can override the default matching style through Orderless's <i>style dispatchers</i>, which are single-character affixes that tell Orderless to treat that component in a special way.  Out of the box, Orderless ships <code>orderless-affix-dispatch</code> as the default dispatcher, mapping <code>~</code> to flex, <code>!</code> to negation, <code>&amp;</code> to annotation matching, <code>,</code> to initialism, and a few others.  Two of those defaults are awkward in our setup: <code>,</code> is already serving as our component separator, so it can't double as a dispatcher prefix; and we want the annotation prefix to support a flex variant, which the affix-alist can't express because each entry maps a single character to a single style.  Both reasons motivate replacing the defaults with hand-rolled versions tuned to our preferred prefix vocabulary.
</p>

<p>
We'll use a customized dispatcher set for the rest of this post.  
</p>

<p>
Each dispatcher is a function of three arguments (pattern, index, total) that inspects a component and, if its dispatcher character appears at either end, returns <code>(STYLE . PATTERN-WITHOUT-AFFIX)</code> as so Orderless knows which matching style to apply.  Returning <code>nil</code> passes the component to the next dispatcher in the chain, or to the default style if none match.  Accepting the character as either prefix <i>or</i> suffix mirrors the built-in <code>orderless-affix-dispatch</code> and means you don't have to remember which end the trigger goes on, or preempt the matching style before you type out a component.  <code>~bna</code> and <code>bna~</code> both flex-match.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">Each dispatcher accepts the dispatcher character as either a
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">PREFIX or a SUFFIX of the component, mirroring the built-in
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">`</span><span style="color: #065fff;">orderless-affix-dispatch</span><span style="color: #7fff7fff7fff;">'.
</span>
<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my/orderless-dispatcher-initialism</span> <span style="color: #4f54aa;">(</span>pattern _index _total<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Initialism-match a component starting OR ending with a backtick."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">cond</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>string-prefix-p <span style="color: #4250ef;">"`"</span> pattern<span style="color: #1f77bb;">)</span>
    `<span style="color: #1f77bb;">(</span>orderless-initialism . ,<span style="color: #b65050;">(</span>substring pattern 1<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>string-suffix-p <span style="color: #4250ef;">"`"</span> pattern<span style="color: #1f77bb;">)</span>
    `<span style="color: #1f77bb;">(</span>orderless-initialism . ,<span style="color: #b65050;">(</span>substring pattern 0 -1<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">flex-if-twiddle</span> <span style="color: #4f54aa;">(</span>pattern _index _total<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Flex-match a component starting OR ending with `</span><span style="color: #065fff;">~</span><span style="color: #7fff7fff7fff;">'."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">cond</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>string-prefix-p <span style="color: #4250ef;">"~"</span> pattern<span style="color: #1f77bb;">)</span>
    `<span style="color: #1f77bb;">(</span>orderless-flex . ,<span style="color: #b65050;">(</span>substring pattern 1<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>string-suffix-p <span style="color: #4250ef;">"~"</span> pattern<span style="color: #1f77bb;">)</span>
    `<span style="color: #1f77bb;">(</span>orderless-flex . ,<span style="color: #b65050;">(</span>substring pattern 0 -1<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">annotation-if-at</span> <span style="color: #4f54aa;">(</span>pattern _index _total<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Annotation-match `</span><span style="color: #065fff;">@P</span><span style="color: #7fff7fff7fff;">' or `</span><span style="color: #065fff;">P@</span><span style="color: #7fff7fff7fff;">'.
Flex-annotation-match `</span><span style="color: #065fff;">@~P</span><span style="color: #7fff7fff7fff;">', `</span><span style="color: #065fff;">~P@</span><span style="color: #7fff7fff7fff;">', `</span><span style="color: #065fff;">@P~</span><span style="color: #7fff7fff7fff;">', or `</span><span style="color: #065fff;">P~@</span><span style="color: #7fff7fff7fff;">' --- the inner
`</span><span style="color: #065fff;">~</span><span style="color: #7fff7fff7fff;">' may sit at either end of the inner pattern."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>rest
         <span style="color: #b65050;">(</span><span style="color: #6052cf;">cond</span>
          <span style="color: #4250ef;">(</span><span style="color: #008858;">(</span>string-prefix-p <span style="color: #4250ef;">"@"</span> pattern<span style="color: #008858;">)</span> <span style="color: #008858;">(</span>substring pattern 1<span style="color: #008858;">)</span><span style="color: #4250ef;">)</span>
          <span style="color: #4250ef;">(</span><span style="color: #008858;">(</span>string-suffix-p <span style="color: #4250ef;">"@"</span> pattern<span style="color: #008858;">)</span> <span style="color: #008858;">(</span>substring pattern 0 -1<span style="color: #008858;">)</span><span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span><span style="color: #6052cf;">when</span> rest
      <span style="color: #1f77bb;">(</span><span style="color: #6052cf;">cond</span>
       <span style="color: #b65050;">(</span><span style="color: #4250ef;">(</span>string-prefix-p <span style="color: #4250ef;">"~"</span> rest<span style="color: #4250ef;">)</span>
        <span style="color: #4250ef;">(</span><span style="color: #6052cf;">let</span> <span style="color: #008858;">(</span><span style="color: #6052cf;">(</span>re <span style="color: #a45f22;">(</span>mapconcat <span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #4f54aa;">(</span>c<span style="color: #4f54aa;">)</span> <span style="color: #4f54aa;">(</span>regexp-quote <span style="color: #ba35af;">(</span>char-to-string c<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
                             <span style="color: #008858;">(</span>string-to-list <span style="color: #4f54aa;">(</span>substring rest 1<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span> <span style="color: #4250ef;">".*"</span><span style="color: #a45f22;">)</span><span style="color: #6052cf;">)</span><span style="color: #008858;">)</span>
          `<span style="color: #008858;">(</span>orderless-annotation . ,re<span style="color: #008858;">)</span><span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span>
       <span style="color: #b65050;">(</span><span style="color: #4250ef;">(</span>string-suffix-p <span style="color: #4250ef;">"~"</span> rest<span style="color: #4250ef;">)</span>
        <span style="color: #4250ef;">(</span><span style="color: #6052cf;">let</span> <span style="color: #008858;">(</span><span style="color: #6052cf;">(</span>re <span style="color: #a45f22;">(</span>mapconcat <span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #4f54aa;">(</span>c<span style="color: #4f54aa;">)</span> <span style="color: #4f54aa;">(</span>regexp-quote <span style="color: #ba35af;">(</span>char-to-string c<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
                             <span style="color: #008858;">(</span>string-to-list <span style="color: #4f54aa;">(</span>substring rest 0 -1<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span> <span style="color: #4250ef;">".*"</span><span style="color: #a45f22;">)</span><span style="color: #6052cf;">)</span><span style="color: #008858;">)</span>
          `<span style="color: #008858;">(</span>orderless-annotation . ,re<span style="color: #008858;">)</span><span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span>
       <span style="color: #b65050;">(</span>t `<span style="color: #4250ef;">(</span>orderless-annotation . ,rest<span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">without-if-bang</span> <span style="color: #4f54aa;">(</span>pattern _index _total<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Exclude a literal match for a component starting OR ending with `</span><span style="color: #065fff;">!</span><span style="color: #7fff7fff7fff;">'.
A bare `</span><span style="color: #065fff;">!</span><span style="color: #7fff7fff7fff;">' is a no-op (matches every candidate)."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">cond</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>equal <span style="color: #4250ef;">"!"</span> pattern<span style="color: #1f77bb;">)</span> '<span style="color: #1f77bb;">(</span>orderless-literal . <span style="color: #4250ef;">""</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>string-prefix-p <span style="color: #4250ef;">"!"</span> pattern<span style="color: #1f77bb;">)</span>
    `<span style="color: #1f77bb;">(</span>orderless-without-literal . ,<span style="color: #b65050;">(</span>substring pattern 1<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>string-suffix-p <span style="color: #4250ef;">"!"</span> pattern<span style="color: #1f77bb;">)</span>
    `<span style="color: #1f77bb;">(</span>orderless-without-literal . ,<span style="color: #b65050;">(</span>substring pattern 0 -1<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">`</span><span style="color: #065fff;">annotation-if-at</span><span style="color: #7fff7fff7fff;">' precedes `</span><span style="color: #065fff;">flex-if-twiddle</span><span style="color: #7fff7fff7fff;">' on purpose:
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">with suffix support, a compound like `</span><span style="color: #065fff;">@PATTERN~</span><span style="color: #7fff7fff7fff;">' would otherwise
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">fire flex on the trailing `</span><span style="color: #065fff;">~</span><span style="color: #7fff7fff7fff;">' before annotation could claim the
</span><span style="color: #a65f6a;">;; </span><span style="color: #7fff7fff7fff;">leading `</span><span style="color: #065fff;">@</span><span style="color: #7fff7fff7fff;">'.  Annotation must get first crack.
</span><span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> orderless-style-dispatchers
      '<span style="color: #4f54aa;">(</span>my/orderless-dispatcher-initialism
        annotation-if-at
        flex-if-twiddle
        without-if-bang<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Four dispatchers, each demonstrated against the candidate list:
</p>

<p>
Try the prompt again with these style dispatchers in place.
</p>
<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>completing-read <span style="color: #4250ef;">"Pick something: "</span> my-produce<span style="color: #008858;">)</span>
</pre>
</div>

<p>
The annotation dispatcher is the most interesting of the four because it shows that some dispatchers <i>compose</i>.  The leading <code>@</code> picks a slot (annotation rather than name); the inner <code>~</code> switches the match style on whatever it's already pointing at.  Nothing in Orderless's library knows about <code>@~</code> as a compound prefix.  We wrote that composition ourselves, in a few lines (<code>annotation-if-at</code>).
</p>

<p>
Note we won't be able to see the annotation dispatcher in action until we add annotations in the Marginalia section, so bear with me!
</p>

<p>
Orderless stays out of the way.  The only thing that changed about our completion setup by introducing Orderless is <i>how</i> Emacs matches your input against the candidate list changed, because we effectively swapped in a different completion style through <code>completion-styles</code>.
</p>

<blockquote class="pull-quote pull-right">
<p>
Vertico didn't need to know about Orderless.  Orderless didn't need to know about Vertico.  They compose because Emacs routes their concerns through separate hooks.
</p>
</blockquote>

<p>
To drive the point home, what we did <i>not</i> do matters architecturally: Vertico and Orderless are agnostic to one another.  They leverage independent Emacs built-ins (<code>completing-read</code> rendering and <code>completion-styles</code> respectively), and they compose because Emacs natively routes their concerns through these separate channels.  You can swap in any other minibuffer candidate display UI (Icomplete-vertical, Mct, fido-vertical-mode), or drop it entirely, and Orderless will still work.  You can swap in any other completion style and Vertico will still work.
</p>
</div>
</div>
<div id="outline-container-marginalia" class="outline-2">
<h2 id="marginalia"><span class="section-number-2">8.</span> Phase 4: Marginalia &#x2014; Annotations&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marginalia">marginalia</span>&#xa0;<span class="annotations">annotations</span></span></h2>
<div class="outline-text-2" id="text-marginalia">
<p>
We can filter tightly now, but in spite of the rich metadata attached to each produce candidate, we can't <i>see</i> anything about a candidate except its name.  If you're deciding between <code>peach</code> and <code>apricot</code>, relevant information like price, color, season, etc&#x2026; is already on the candidate's text properties, but Vertico is only showing us the string itself.  <a href="https://www.chiply.dev/post-vompeccc#marginalia">Marginalia</a> is the package that promotes candidates into <i>informed choices</i> by displaying their metadata as right-aligned columns next to each candidate name.
</p>

<p>
The trick that makes Marginalia (and every subsequent VOMPECCC package) possible is the convention already established by the list of produce items: <i>the candidate is its name as a string, with its full record's fields stamped on as text properties.</i>  <code>completing-read</code> is satisfied because the candidate is a plain string; the rest of the substrate is satisfied because the properties are right there.
</p>

<p>
A note on scope before we start: plain <code>completing-read</code> can only carry <i>one</i> completion category at a time, and Marginalia dispatches its annotator off that prompt-level category.  The corpus has two categories (<code>fruit</code> and <code>vegetable</code>); to keep this phase honest, we narrow the picker to <i>just fruits</i> for now.  Phase 5 will lift this restriction with Consult's multi-source mechanism.
</p>

<p>
That narrowing is one line of Lisp, but worth pausing on.  Every later phase (the Consult sources in Phase 5, the async variant in Phase 8) will partition the corpus by category in the same way, so we factor it out once and read the routing key off the candidate itself rather than off of some separate lookup table.  This is the candidate-as-currency convention in miniature: a routing question is answered by reading a text property:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-produce-of-category</span> <span style="color: #4f54aa;">(</span>cat<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Return candidates from `</span><span style="color: #065fff;">my-produce</span><span style="color: #7fff7fff7fff;">' whose `</span><span style="color: #065fff;">category</span><span style="color: #7fff7fff7fff;">' is CAT."</span>
  <span style="color: #4f54aa;">(</span>cl-remove-if-not
   <span style="color: #ba35af;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #1f77bb;">(</span>cand<span style="color: #1f77bb;">)</span> <span style="color: #1f77bb;">(</span>eq <span style="color: #b65050;">(</span>get-text-property 0 'category cand<span style="color: #b65050;">)</span> cat<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
   my-produce<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
We still need to tell the prompt itself what completion category this is, because Marginalia dispatches its annotator off the <b>prompt's</b> metadata, not off per-candidate text properties.  The candidate stamping we did in the corpus is for <i>our own</i> code (the actions, the transformer, the exporter) to read; the framework looks at prompt metadata.  The lightest way to set the metadata is <code>completion-extra-properties</code>, a property list that overrides the completion metadata for the duration of one <code>completing-read</code> call:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-pick-fruit</span> <span style="color: #4f54aa;">()</span>
  <span style="color: #7fff7fff7fff;">"Pick a fruit by name."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>completion-extra-properties '<span style="color: #b65050;">(</span><span style="color: #ba35af;">:category</span> fruit<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>message <span style="color: #4250ef;">"You picked: %s"</span>
             <span style="color: #1f77bb;">(</span>completing-read <span style="color: #4250ef;">"Fruit: "</span> <span style="color: #b65050;">(</span>my-produce-of-category 'fruit<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
The foundational pattern &#x2014; a completion function that responds to <code>(action 'metadata)</code> with <code>(metadata (category . fruit))</code> &#x2014; is still available and is what libraries like Consult build on.  However, for a one-off picker without Consult, <code>completion-extra-properties</code> is equivalent and cleaner.  Why is this even needed?  Because plain <code>completing-read</code> over a list of strings has no way to communicate a category to Marginalia: the framework reads the prompt's completion metadata, and our list of strings doesn't ship any.  As soon as we move to Consult in Phase 5, the source-level <code>:category</code> key takes over and the extra-properties step disappears entirely, which is exactly why packages like <code>spot</code> never need this dance.  Every <code>spot</code> prompt is a Consult prompt.  Our Phase 4 picker is the awkward case precisely because it is deliberately the barest possible call, and a demonstration of a Consult-less completion setup for our produce picker. 
</p>

<p>
The annotator itself is the heart of this phase.  It takes a candidate string, reads its text properties directly, and hands a list of columns to <code>marginalia--fields</code>, which does the alignment, per-field truncation, and face application:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-annotate-produce</span> <span style="color: #4f54aa;">(</span>cand<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Annotate a produce candidate CAND with type, color, season, and price."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">marginalia--fields</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>symbol-name <span style="color: #b65050;">(</span>get-text-property 0 'type cand<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
    <span style="color: #ba35af;">:truncate</span> 13 <span style="color: #ba35af;">:face</span> 'marginalia-type<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>get-text-property 0 'color cand<span style="color: #1f77bb;">)</span>  <span style="color: #ba35af;">:truncate</span> 10 <span style="color: #ba35af;">:face</span> 'marginalia-string<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>get-text-property 0 'season cand<span style="color: #1f77bb;">)</span> <span style="color: #ba35af;">:truncate</span> 12 <span style="color: #ba35af;">:face</span> 'marginalia-date<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>format <span style="color: #4250ef;">"$%.2f"</span> <span style="color: #b65050;">(</span>get-text-property 0 'price cand<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
    <span style="color: #ba35af;">:truncate</span> 8  <span style="color: #ba35af;">:face</span> 'marginalia-number<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Note that I am not writing any layout code at all.  <code>marginalia--fields</code> handles padding, alignment, and face application; my job is only to declare which <i>fields</i> go in which <i>columns</i>.  Annotating the candidates in this way enables Orderless's <code>@</code> dispatcher to filter by our produce's metadata, so <code>@berry</code>, <code>@citrus</code>, <code>@root</code> become legitimate filter prefixes.
</p>

<p>
Registration for the <code>fruit</code> category is one <code>add-to-list</code> against Marginalia's annotator registry, plus enabling the <code>marginalia-mode</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>add-to-list 'marginalia-annotators
             '<span style="color: #4f54aa;">(</span>fruit my-annotate-produce none<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>marginalia-mode 1<span style="color: #008858;">)</span>
</pre>
</div>

<p>
The registry entry is <code>(CATEGORY ANNOTATOR1 ANNOTATOR2 ... none)</code>, where each tail symbol is an annotator <code>marginalia-cycle</code> (<code>M-A</code>) can rotate to in-session.  We list two states for our category: our custom annotator, then <code>none</code> (annotations off).  This matches <code>spot</code>'s convention and gives a clean toggle on <code>M-A</code>.  Marginalia's own built-in entries also include a <code>builtin</code> symbol in the chain (which is a fallback that defers to whatever <code>annotation-function</code> the prompt's metadata declares natively) but for a category nobody else knows about (like <code>fruit</code>), there is no built-in to defer to, so leaving it out keeps the cycle to two visibly-different states instead of three.
</p>

<p>
Run the picker:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>my-pick-fruit<span style="color: #008858;">)</span>
</pre>
</div>

<p>
When <code>(my-pick-fruit)</code> runs, the prompt opens as it did before, but every one of the fruit candidates is now followed by a horizontally aligned set of columns: a type word (<code>pome</code>, <code>berry</code>, <code>citrus</code>, &#x2026;), a color word (<code>red</code>, <code>yellow</code>, <code>blue</code>, <code>green</code>, &#x2026;), a season (<code>spring</code>, <code>summer</code>, <code>winter</code>, <code>year-round</code>), and a dollar-formatted price (<code>$0.59</code>, <code>$3.99</code>, <code>$6.99</code>).  
</p>

<p>
Stylistically, each column is padded to a fixed width and rendered in its own face, so the four fields read as distinct groups rather than running together with a delimiter.  Where Phase 3 showed <code>strawberry</code>, this phase shows:
</p>

<p>
<code>strawberry   berry         red       spring      $3.99</code>  
</p>

<p>
The list is legible at a glance, and what's more, you can usefully compare <code>peach</code> against <code>apricot</code> on price and season without typing anything.  Scanning for cheap in-season fruit, for example, is made easy in this way.
</p>

<p>
Typing against annotation text is where Marginalia crosses from cosmetic to compositional.  Typing <code>yellow</code> at the prompt matches nothing, because <code>yellow</code> is in the <i>annotation</i> column, not the candidate <i>name</i>, and Orderless is still matching against names only.  But prefix that same component with <code>@</code>, as in <code>@yellow</code>, and the annotation dispatcher we wired up in Phase 3 tells Orderless to match this particular component against the annotation text instead of the candidate string.  The list snaps to exactly the three yellow-colored fruits in the corpus.
</p>

<p>
To drive home the point that the VOMPECCC packages work independently of one another, Orderless knew nothing about Marginalia, and vice-versa.  The <code>@</code> dispatcher simply matches against whatever is in the "annotation slot" of the current candidate, and that slot happens to contain the words Marginalia stamped there.
</p>

<p>
The compound variant from Phase 3 cashes in here too: <code>@~sm</code> triggers the flex branch of <code>annotation-if-at</code>, flex-matching the characters <code>s</code> and <code>m</code> against annotation text.  Only <code>summer</code> contains them in order, so the list collapses to the summer fruits.
</p>

<p>
Annotation components compose the same way regular components do.  Typing <code>@summer,@red</code> on the empty prompt narrows first to summer fruits, then to the subset of those that are also red.  The list collapses to <code>raspberry</code> and <code>cherry</code>, the two red summer fruits in the corpus.  You can reach for a fruit by its <i>properties</i> without ever remembering its name.  <a href="https://www.chiply.dev/post-vompeccc#marginalia">Post 2 called this "an unusually large leverage gain for what feels like a cosmetic layer"</a>.
</p>

<p>
The architectural observation here is that the <code>@</code> dispatcher is not a Marginalia feature.  It is an Orderless feature (a dispatcher <i>we wrote</i>) that <i>happens to work</i> because Marginalia exposes annotations through the same completion-metadata slot Orderless reads from.  Swap Marginalia for any other annotator (say, a leaner one you write yourself that only shows <code>price</code>) and the <code>@</code> filter still works.  With an alternative annotation provider, Orderless would just filter against whatever that other annotator produces.
</p>
</div>
<div id="outline-container-inline-annotations" class="outline-3">
<h3 id="inline-annotations"><span class="section-number-3">8.1.</span> The recommended alternative to Marginalia: inline annotations</h3>
<div class="outline-text-3" id="text-inline-annotations">
<p>
The Marginalia readme is explicit on a point worth surfacing before we lock this pattern in.  For completion commands <i>you control</i>, the author recommends putting the annotator directly in the completion metadata via <code>affixation-function</code>, <i>not</i> in <code>marginalia-annotators</code>.  Marginalia exists primarily to annotate <i>other people's</i> commands, and most of the time this is the built-in Emacs prompts and third-party packages whose authors didn't ship annotations themselves.  When you control the call site, as we are here with our fruit picker, you can attach the annotator alongside the category in <code>completion-extra-properties</code>, and that is the recommended default.
</p>

<p>
The replacement is one function and one extra property.  <code>affixation-function</code> receives the list of currently visible candidates and returns <code>(CANDIDATE PREFIX SUFFIX)</code> triples, which Vertico (or any <code>completing-read</code> UI) renders.  We have to do our own column padding now: <code>marginalia--fields</code> was the convenience that absorbed it, but the upside of doing things the recommended way is that we eliminate the Marginalia dependency for this prompt:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-affixation-produce</span> <span style="color: #4f54aa;">(</span>cands<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Return (CAND PREFIX SUFFIX) per candidate, columns padded for alignment."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>width <span style="color: #b65050;">(</span>apply #'max 0 <span style="color: #4250ef;">(</span>mapcar #'length cands<span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>mapcar
     <span style="color: #1f77bb;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #b65050;">(</span>cand<span style="color: #b65050;">)</span>
       <span style="color: #b65050;">(</span><span style="color: #6052cf;">let*</span> <span style="color: #4250ef;">(</span><span style="color: #008858;">(</span>pad  <span style="color: #6052cf;">(</span>make-string <span style="color: #a45f22;">(</span>- <span style="color: #008858;">(</span>1+ width<span style="color: #008858;">)</span> <span style="color: #008858;">(</span>length cand<span style="color: #008858;">)</span><span style="color: #a45f22;">)</span> ?\s<span style="color: #6052cf;">)</span><span style="color: #008858;">)</span>
              <span style="color: #008858;">(</span>cols <span style="color: #6052cf;">(</span>concat
                     <span style="color: #a45f22;">(</span>propertize <span style="color: #008858;">(</span>format <span style="color: #4250ef;">"%-13s"</span> <span style="color: #4f54aa;">(</span>symbol-name <span style="color: #ba35af;">(</span>get-text-property 0 'type cand<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
                                 'face 'completions-annotations<span style="color: #a45f22;">)</span>
                     <span style="color: #a45f22;">(</span>propertize <span style="color: #008858;">(</span>format <span style="color: #4250ef;">"%-10s"</span> <span style="color: #4f54aa;">(</span>get-text-property 0 'color cand<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
                                 'face 'completions-annotations<span style="color: #a45f22;">)</span>
                     <span style="color: #a45f22;">(</span>propertize <span style="color: #008858;">(</span>format <span style="color: #4250ef;">"%-12s"</span> <span style="color: #4f54aa;">(</span>get-text-property 0 'season cand<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
                                 'face 'completions-annotations<span style="color: #a45f22;">)</span>
                     <span style="color: #a45f22;">(</span>propertize <span style="color: #008858;">(</span>format <span style="color: #4250ef;">"$%-7.2f"</span> <span style="color: #4f54aa;">(</span>get-text-property 0 'price cand<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
                                 'face 'completions-annotations<span style="color: #a45f22;">)</span><span style="color: #6052cf;">)</span><span style="color: #008858;">)</span><span style="color: #4250ef;">)</span>
         <span style="color: #4250ef;">(</span>list cand <span style="color: #4250ef;">""</span> <span style="color: #008858;">(</span>concat pad cols<span style="color: #008858;">)</span><span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
     cands<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-pick-fruit-builtin</span> <span style="color: #4f54aa;">()</span>
  <span style="color: #7fff7fff7fff;">"Pick a fruit using only built-in annotation machinery."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>completion-extra-properties
         <span style="color: #b65050;">(</span>list <span style="color: #ba35af;">:category</span> 'fruit
               <span style="color: #ba35af;">:affixation-function</span> #'my-affixation-produce<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>message <span style="color: #4250ef;">"You picked: %s"</span>
             <span style="color: #1f77bb;">(</span>completing-read <span style="color: #4250ef;">"Fruit: "</span> <span style="color: #b65050;">(</span>my-produce-of-category 'fruit<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
The shape is the same as the Marginalia version (propertized candidates and a property list bound around the call) but with one extra entry.  <code>:affixation-function</code> delivers the annotator directly, where the Marginalia version had only <code>:category</code> and let the marginalia-annotators registry lookup pick the annotator.
</p>

<p>
To prove this is independent of Marginalia, turn the mode off and run the new picker:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>marginalia-mode -1<span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>my-pick-fruit-builtin<span style="color: #008858;">)</span>
</pre>
</div>

<p>
The four columns still appear.  Vertico renders, Orderless filters, and the annotation slot is filled by <code>my-affixation-produce</code>.  <code>@yellow</code> still works, too, because Orderless's annotation dispatcher reads from whatever populates that slot!
</p>

<p>
This actually <i>strengthens</i> the unix-style nature of the VOMPECCC set rather than diluting it.  <code>affixation-function</code> in the completion metadata is an Emacs primitive.  Marginalia is one consumer of that slot: a registry that picks an annotator per <i>category</i> and writes it into the slot at <code>completing-read</code> time.  Our hand-rolled <code>affixation-function</code> is a different consumer of the same slot, delivering the annotator inline.  
</p>

<p>
Re-enable Marginalia and re-run the original picker:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>marginalia-mode 1<span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>my-pick-fruit<span style="color: #008858;">)</span>
</pre>
</div>

<p>
Annotations are back, served by Marginalia's registry-driven version.  The two implementations are interchangeable from the prompt's point of view, and only the <i>provenance</i> of the annotator differs.  When to reach for which becomes a question of who controls the command.  For built-in Emacs prompts and third-party commands you can't edit, Marginalia is your only entry point.  For your own commands, the inline <code>affixation-function</code> is closer to the data, has zero dependency, and is the route the Marginalia author recommends.
</p>

<p>
The rest of this post continues with Marginalia in place.  Both routes (the Marginalia registry and the inline <code>affixation-function</code>) can be threaded through the Consult sources in Phase 5.  The takeaway from this aside is that the inline route <i>exists</i>, that it is the recommended default for a completion command you fully control, and that it composes with Vertico, Orderless, and the rest of the completion substrate exactly the way the other VOMPECCC packages do.  
</p>

<p>
That being said, I will use the Marginalia registry approach for the rest of this post because you will most often see Marginalia style annotations if you adopt VOMPECCC, and they look nicer with Marginalia's formatting.
</p>
</div>
</div>
</div>
<div id="outline-container-consult" class="outline-2">
<h2 id="consult"><span class="section-number-2">9.</span> Phase 5: Consult &#x2014; Multi-Source with Narrowing&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="sources">sources</span>&#xa0;<span class="narrow">narrow</span></span></h2>
<div class="outline-text-2" id="text-consult">
<p>
Phase 4 was a single prompt over a fruit-only subset, which was enough to demo Marginalia, but it left the corpus's vegetables out and gave us no way to <i>scope</i> the prompt without typing.  What if we want one prompt that holds <i>everything</i> (all categories) in <code>my-produce</code> and lets us flip between fruits and vegetables (and back to all of it) with a single keystroke.
</p>

<p>
<a href="https://www.chiply.dev/post-vompeccc#consult">Consult</a> fixes this through its multi-source mechanism: <code>consult--multi</code> takes a list of <i>sources</i>, unifies their candidates into a single prompt, and sets up per-source narrowing keys.  Each source declares its own name, its own narrow key, its own completion category, and its own items.
</p>

<p>
The dispatch story in Phase 5 is <i>the same</i> as in Phase 4: the candidate's completion category is whatever the candidate <i>carries</i>.  In Phase 4 we read <code>category</code> off the only candidate type the picker held (a fruit).  In Phase 5 each source produces candidates of one category (<code>fruit</code> for the fruit source, <code>vegetable</code> for the vegetable source) and the source-level <code>:category</code> matches the value the data already declared on every candidate.  The framework reads the value out of the candidate.  <a href="https://www.chiply.dev/post-vompeccc-spot#candidates-as-currency">The <code>spot</code> post</a> uses the same double-stamping pattern, and the per-candidate text property is the authoritative truth, and the source-level <code>:category</code> keeps Consult's metadata in sync.
</p>

<p>
Each candidate carries five text properties: <code>category</code> (<code>fruit</code> or <code>vegetable</code>), <code>type</code>, <code>color</code>, <code>season</code>, and <code>price</code>, and is read directly off the candidate by Marginalia, Embark, the annotator, and every action we're about to write.
</p>

<p>
Here we define two Consult sources (1 per category) each with its own scoped <code>:history</code> variable so previously-selected fruits and previously-selected vegetables don't get mixed together in any one history list:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-consult-history-fruit</span> nil
  <span style="color: #7fff7fff7fff;">"History scoped to the fruit Consult source."</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-consult-history-vegetable</span> nil
  <span style="color: #7fff7fff7fff;">"History scoped to the vegetable Consult source."</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-consult-source-fruit</span>
  `<span style="color: #4f54aa;">(</span><span style="color: #ba35af;">:name</span>     <span style="color: #4250ef;">"Fruits"</span>
    <span style="color: #ba35af;">:narrow</span>   ?f
    <span style="color: #ba35af;">:category</span> fruit
    <span style="color: #ba35af;">:history</span>  my-consult-history-fruit
    <span style="color: #ba35af;">:items</span>    ,<span style="color: #ba35af;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #1f77bb;">()</span> <span style="color: #1f77bb;">(</span>my-produce-of-category 'fruit<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Consult source for fruits."</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-consult-source-vegetable</span>
  `<span style="color: #4f54aa;">(</span><span style="color: #ba35af;">:name</span>     <span style="color: #4250ef;">"Vegetables"</span>
    <span style="color: #ba35af;">:narrow</span>   ?v
    <span style="color: #ba35af;">:category</span> vegetable
    <span style="color: #ba35af;">:history</span>  my-consult-history-vegetable
    <span style="color: #ba35af;">:items</span>    ,<span style="color: #ba35af;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #1f77bb;">()</span> <span style="color: #1f77bb;">(</span>my-produce-of-category 'vegetable<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Consult source for vegetables."</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
A Consult source plist's most important keys:
</p>

<ul class="org-ul">
<li><code>:items</code> is the candidate stream.  A function that returns a list of candidates, called lazily when the prompt opens.  Ours is synchronous because the produce list is local, but for a remote API you would swap <code>:items</code> for <code>:async</code> and <code>consult--dynamic-collection</code> instead.  <a href="https://www.chiply.dev/#async">Phase 8</a> walks through that variant against a faithfully-mocked produce API, and the <a href="https://www.chiply.dev/post-vompeccc-spot#cache"><code>spot</code> post</a> discusses the consumer-side cost when multiple sources share one endpoint.</li>
<li><code>:narrow ?f</code> binds the character that scopes the prompt to <i>just this source</i>.  Pressing <code>f SPC</code> at the prompt hides every non-fruit candidate.</li>
<li><code>:category fruit</code> is the completion category that propagates onto every candidate from this source, not via our <code>category</code> text property but via Consult's own <code>multi-category</code> text property, which Consult writes from this very key.  Marginalia and Embark consult that <code>multi-category</code> channel to pick the right annotator and the right keymap per candidate.  <i>We set this key to match the data's <code>category</code> property by hand, so the two declarations stay aligned.</i></li>
<li><code>:history my-consult-history-fruit</code> is a <i>per-source</i> history list.  Selecting a candidate from this source appends it to this variable (and only this variable), which means the fruit picker can keep its own history and Vertico's <code>M-p</code> / <code>M-n</code> cycle through a list that is meaningfully scoped instead of polluted with every minibuffer interaction in the session.  This per-source scoping is also what makes a per-prompt history-recall command (something like <code>consult-history</code>) useful, which we discuss right after.</li>
<li><code>:name</code> is the header shown above this source's candidates in the unified list.</li>
</ul>

<p>
Two Marginalia registry entries, one per category:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">dolist</span> <span style="color: #4f54aa;">(</span>cat '<span style="color: #ba35af;">(</span>fruit vegetable<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>add-to-list 'marginalia-annotators
               `<span style="color: #ba35af;">(</span>,cat my-annotate-produce none<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Then the consult command:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">consult-produce-picker</span> <span style="color: #4f54aa;">()</span>
  <span style="color: #7fff7fff7fff;">"Pick produce with multi-source Consult completion."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>consult--multi
   <span style="color: #ba35af;">(</span>list my-consult-source-fruit
         my-consult-source-vegetable<span style="color: #ba35af;">)</span>
   <span style="color: #ba35af;">:prompt</span> <span style="color: #4250ef;">"Produce: "</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Run it:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>consult-produce-picker<span style="color: #008858;">)</span>
</pre>
</div>

<p>
When <code>(consult-produce-picker)</code> runs, the prompt opens with the thirty produce items grouped under two bold header lines in the vertical list: <code>Fruits</code> and <code>Vegetables</code>, each followed by the candidates belonging to that source.  
Every annotation column from Phase 4 is preserved because the annotator is registered against both categories and Consult has tagged each candidate with the right one.
</p>

<p>
The new affordance by consult is "<i>narrowing</i>", the ability to scope the prompt to a single source with one keystroke.  Pressing <code>f</code> followed by <code>SPC</code> hides every non-fruit candidate: the twenty fruits become the entire list, and a small indicator in the prompt header shows the narrow state.  Pressing <code>DEL</code> clears the narrow and restores the full thirty-item view.  <code>v SPC</code> narrows to vegetables.
</p>

<p>
The features from earlier phases still apply inside a narrowed view, and this is where the independence of VOMPECCC's constituents really shows.  With the list narrowed to fruits, typing <code>@berry</code> further filters to the four berries via the type column we added to the annotator; <code>@red</code> filters to red fruits; <code>~bry</code> flex-matches the <code>berry</code> suffix of the remaining candidates; <code>!blue</code> excludes anything containing <code>blue</code>.  Orderless, Vertico, and Marginalia are all still doing exactly what they did in Phase 4, and they could care less that the candidate source is now two Consult sources instead of one list.
</p>

<p>
Note we have another classifying property in each candidate.  The <code>type</code> column in the annotator means <code>@berry</code>, <code>@citrus</code>, <code>@root</code>, <code>@cruciferous</code> are all valid one-component filters on the prompt, and they <i>compose</i> with the source narrow, so <code>f SPC @citrus</code> gives you exactly the four citrus fruits.  The framework keeps <code>type</code> as a <i>searchable</i> axis instead of a <i>narrowing</i> axis.
</p>

<p>
The amount of code we wrote for the narrowing, grouping, and per-source history is pretty close to zero.  I like Consult's declarative API in this way: we declared; Consult did.
</p>
</div>
<div id="outline-container-consult-history" class="outline-3">
<h3 id="consult-history"><span class="section-number-3">9.1.</span> Recalling prior queries with <code>consult-history</code></h3>
<div class="outline-text-3" id="text-consult-history">
<p>
Now that fruit selections land in <code>my-consult-history-fruit</code> and vegetable selections land in <code>my-consult-history-vegetable</code>, two further capabilities become available almost for free.
</p>

<p>
The first is the built-in cycle: <code>M-p</code> and <code>M-n</code> walk through the active prompt's history, and because we scoped per source, those cycles only contain entries you actually picked <i>from this source</i>.
</p>

<p>
The second is more powerful: <code>consult-history</code>, a Consult command that reads the active history variable, presents its entries in a <i>recursive</i> minibuffer with full Orderless+Vertico+Marginalia goodness, and inserts the chosen entry into the original prompt.  The conventional binding is in <code>minibuffer-local-map</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> enable-recursive-minibuffers t<span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>keymap-set minibuffer-local-map <span style="color: #4250ef;">"C-r"</span> #'consult-history<span style="color: #008858;">)</span>
</pre>
</div>

<p>
The <code>enable-recursive-minibuffers</code> setting is a prerequisite: <code>consult-history</code> opens a <i>new</i> minibuffer prompt <i>while another is already active</i>, and Emacs's default is to refuse that.  Most VOMPECCC configurations turn it on already.
</p>

<p>
<b>Demo 1: recall a previous selection.</b>  Run <code>(consult-produce-picker)</code> and pick <code>cherry</code>.  Run it again, and at the empty prompt press <code>C-r</code>.  A second minibuffer opens listing the active source's history; <code>cherry</code> is at the top.  Pick it (or flex-match <code>~chy</code>) and <code>cherry</code> is inserted into the original prompt &#x2014; <code>RET</code> confirms it.
</p>

<p>
This is the everyday case: the items you reach for repeatedly are one keystroke and one history-pick away on every subsequent run.  Since the history is per-source, narrowing first with <code>f SPC</code> or <code>v SPC</code> scopes <code>C-r</code> to <i>just</i> that source's history.
</p>

<p>
<b>Demo 2: recall a complex Orderless query.</b>  Selections in history are useful, but the bigger win is recalling <i>queries</i>, for example, something like the complex multi-component Orderless filters you spent twenty seconds composing.
</p>

<p>
This is a behavior of consult history that is not so obvious.  The trick is that, by default, <code>completing-read</code> adds whatever it <i>returns</i> to the history variable, and what it returns is the selected candidate.  To make the typed query the return value instead, press <code>M-RET</code> (<code>vertico-exit-input</code>) at the prompt.  This commits the literal minibuffer contents rather than the highlighted candidate, regardless of whether <code>:require-match</code> is on.
</p>

<p>
Run the picker, narrow with <code>f SPC</code>, and type <code>summer,!blue,@red</code>.  Instead of pressing <code>RET</code> to pick a fruit, press <code>M-RET</code>.  The minibuffer closes and the returned string is the literal <code>summer,!blue,@red</code>, and that is what lands in <code>my-consult-history-fruit</code>.
</p>

<p>
Run the picker again, narrow with <code>f SPC</code>, and hit <code>C-r</code>.  <code>summer,!blue,@red</code> is at the top of history.  Pick it, and the query is restored in the prompt, ready to be tweaked or <code>RET</code>-confirmed against the same filtered set.
</p>

<p>
<b>Demo 3: save typed input on every exit.</b>  If you'd rather not have to remember <code>M-RET</code>, and you're willing to accept some history pollution, a <code>minibuffer-exit-hook</code> can capture the typed input alongside the selection on every exit:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-save-typed-input-to-history</span> <span style="color: #4f54aa;">()</span>
  <span style="color: #7fff7fff7fff;">"Add minibuffer input to history before exit, alongside the selection."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">when-let*</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>hist minibuffer-history-variable<span style="color: #1f77bb;">)</span>
              <span style="color: #1f77bb;">(</span><span style="color: #b65050;">(</span>symbolp hist<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
              <span style="color: #1f77bb;">(</span><span style="color: #b65050;">(</span>not <span style="color: #4250ef;">(</span>eq hist t<span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
              <span style="color: #1f77bb;">(</span>input <span style="color: #b65050;">(</span>minibuffer-contents-no-properties<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
              <span style="color: #1f77bb;">(</span><span style="color: #b65050;">(</span>not <span style="color: #4250ef;">(</span>string-empty-p input<span style="color: #4250ef;">)</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>add-to-history hist input<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span>add-hook 'minibuffer-exit-hook #'my-save-typed-input-to-history<span style="color: #008858;">)</span>
</pre>
</div>

<p>
With the hook in place, every exit pushes the <i>current contents of the minibuffer</i> to history.  <code>RET</code> on <code>raspberry</code> after typing <code>summer,!blue,@red</code> now adds <i>both</i> <code>summer,!blue,@red</code> and <code>raspberry</code> to history.  <code>C-r</code> shows both.  The cost is that history gets longer, including incomplete queries you abandoned mid-typing.  But consider you also have the fully feature ICR experience in <code>consult-history</code>, so in my opinion, this is a non-issue.
</p>

<p>
Indeed, architecturally, history is one more consumer of the same Emacs primitive every other phase has been consuming.  <code>completing-read</code> takes a HIST argument and updates that variable on selection, and Consult's per-source <code>:history</code> key just threads that argument per source.  <code>consult-history</code> reads the variable.  The <code>minibuffer-exit-hook</code> reads the buffer's typed contents.  <code>M-RET</code> changes <i>what gets returned</i> from the prompt and therefore <i>what gets stored</i>.
</p>
</div>
</div>
</div>
<div id="outline-container-embark" class="outline-2">
<h2 id="embark"><span class="section-number-2">10.</span> Phase 6: Embark &#x2014; Actions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="actions">actions</span></span></h2>
<div class="outline-text-2" id="text-embark">
<p>
We can <i>select</i> a produce item, but we still can't <i>do</i> anything with it.  
</p>

<p>
So far the picker's job has ended at returning a string.  <a href="https://www.chiply.dev/post-vompeccc#embark">Embark</a> is the package that adds a layer of category-contextual actions on top of any completion prompt and provides a keyboard-driven context menu whose contents depend on what kind of candidate is highlighted.
</p>

<p>
It's useful to think of Embark as similar to a left-click or 'context menu' in traditional UIs.
</p>

<p>
We'll define the minimum set of actions needed to demonstrate Embark's three headline features:
</p>

<ol class="org-ol">
<li><b><code>embark-act</code> on a single candidate (the basic case)</b>: an <code>inspect</code> action that pops a buffer with the item's full plist.</li>
<li><b><code>embark-act-all</code> on every visible candidate (multi-cardinality)</b>: an <code>add-to-cart</code> action that appends to a <code>*Cart*</code> buffer, which we will invoke against every currently visible item at once.</li>
<li><b>Per-category keymap dispatch (context-sensitivity)</b>: vegetables get a <code>roast</code> action that fruits don't; citrus fruits get a <code>juice</code> action that other fruits don't.  These are specializations side by side, by two different mechanisms in Embark: the vegetable specialization rides on the Consult source's <code>:category</code> key (forwarded to Embark via Consult's <code>multi-category</code> channel), while the citrus specialization rides on an Embark <i>transformer</i> <i>we</i> write that reads the candidate's <code>type</code> property.</li>
</ol>
</div>
<div id="outline-container-embark-actions" class="outline-3">
<h3 id="embark-actions"><span class="section-number-3">10.1.</span> The Actions</h3>
<div class="outline-text-3" id="text-embark-actions">
<p>
Each action is a regular interactive command that takes the candidate string as its argument.  Embark passes the current candidate when the user triggers the action.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-inspect-produce</span> <span style="color: #4f54aa;">(</span>cand<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Pop a buffer showing the text properties carried by produce CAND."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span> <span style="color: #4250ef;">"sProduce: "</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>props <span style="color: #b65050;">(</span>text-properties-at 0 cand<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
        <span style="color: #1f77bb;">(</span>buf <span style="color: #b65050;">(</span>get-buffer-create <span style="color: #4250ef;">"*Produce Inspect*"</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span><span style="color: #6052cf;">with-current-buffer</span> buf
      <span style="color: #1f77bb;">(</span>erase-buffer<span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>insert <span style="color: #b65050;">(</span>format <span style="color: #4250ef;">";; %s\n"</span> cand<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>pp props <span style="color: #b65050;">(</span>current-buffer<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>goto-char <span style="color: #b65050;">(</span>point-min<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>emacs-lisp-mode<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>display-buffer buf<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-add-to-cart</span> <span style="color: #4f54aa;">(</span>cand<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Append produce CAND to the *Cart* buffer."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span> <span style="color: #4250ef;">"sProduce: "</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>price <span style="color: #b65050;">(</span>get-text-property 0 'price cand<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span><span style="color: #6052cf;">with-current-buffer</span> <span style="color: #1f77bb;">(</span>get-buffer-create <span style="color: #4250ef;">"*Cart*"</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>goto-char <span style="color: #b65050;">(</span>point-max<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>insert <span style="color: #b65050;">(</span>format <span style="color: #4250ef;">"- %-12s ($%.2f)\n"</span> cand price<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>message <span style="color: #4250ef;">"Added %s to cart"</span> cand<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-make-juice</span> <span style="color: #4f54aa;">(</span>cand<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Juice the citrus fruit CAND."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span> <span style="color: #4250ef;">"sFruit: "</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>message <span style="color: #4250ef;">"&#127865; Juicing %s!"</span> cand<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-roast-vegetable</span> <span style="color: #4f54aa;">(</span>cand<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Roast the vegetable CAND."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span> <span style="color: #4250ef;">"sVegetable: "</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>message <span style="color: #4250ef;">"&#128293; Roasting %s!"</span> cand<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Each action reads whatever properties it needs off the candidate, and then does something domain-specific.  No Embark machinery leaks into the action body, and Embark's only job is to <i>invoke</i> the action with the right candidate.
</p>
</div>
</div>
<div id="outline-container-embark-keymaps" class="outline-3">
<h3 id="embark-keymaps"><span class="section-number-3">10.2.</span> The Keymaps</h3>
<div class="outline-text-3" id="text-embark-keymaps">
<p>
Embark actions are bound in per-category keymaps.  We define one general map per top-level <code>category</code> (one for fruits, one for vegetables), and then we define a specialized citrus map that inherits from the fruit map and adds <code>j</code> (for juice 🍹):
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defvar-keymap</span> my-fruit-general-map
  <span style="color: #ba35af;">:parent</span> embark-general-map
  <span style="color: #ba35af;">:doc</span> <span style="color: #7fff7fff7fff;">"Embark actions applicable to any fruit."</span>
  <span style="color: #4250ef;">"i"</span> #'my-inspect-produce
  <span style="color: #4250ef;">"a"</span> #'my-add-to-cart<span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar-keymap</span> my-vegetable-general-map
  <span style="color: #ba35af;">:parent</span> embark-general-map
  <span style="color: #ba35af;">:doc</span> <span style="color: #7fff7fff7fff;">"Embark actions applicable to any vegetable."</span>
  <span style="color: #4250ef;">"i"</span> #'my-inspect-produce
  <span style="color: #4250ef;">"a"</span> #'my-add-to-cart
  <span style="color: #4250ef;">"r"</span> #'my-roast-vegetable<span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar-keymap</span> my-fruit-citrus-map
  <span style="color: #ba35af;">:parent</span> my-fruit-general-map
  <span style="color: #ba35af;">:doc</span> <span style="color: #7fff7fff7fff;">"Embark actions for citrus (fruit actions + juicing)."</span>
  <span style="color: #4250ef;">"j"</span> #'my-make-juice<span style="color: #008858;">)</span>
</pre>
</div>

<p>
<code>:parent embark-general-map</code> gives us the built-in defaults (copy-as-kill, describe, insert, etc.) on top of our domain-specific actions.  <code>:parent my-fruit-general-map</code> on the citrus map means citrus candidates inherit <code>i</code> and <code>a</code> from the fruit general map and add <code>j</code> on top.
</p>
</div>
</div>
<div id="outline-container-embark-transformer" class="outline-3">
<h3 id="embark-transformer"><span class="section-number-3">10.3.</span> The transformer: refining <code>fruit</code> to <code>fruit-citrus</code></h3>
<div class="outline-text-3" id="text-embark-transformer">
<p>
We have two dispatch problems in this phase, and they look symmetric on the surface:
</p>

<ol class="org-ol">
<li><b>Fruit vs. vegetable.</b>  Each candidate already knows whether it is a fruit or a vegetable &#x2014; the source declared it, and Consult propagated that source-level <code>:category</code> onto each candidate via the <code>multi-category</code> text property.  We want Embark to pick <code>my-fruit-general-map</code> for fruits and <code>my-vegetable-general-map</code> for vegetables.</li>

<li><b>Citrus refinement.</b>  Within fruits, we want citrus candidates to additionally get the <code>j</code> juice action via <code>my-fruit-citrus-map</code>.  This is a refinement <i>below</i> the source category.</li>
</ol>

<p>
The corpus already keeps two clean axes for these: <code>category</code> for top-level kind (<code>fruit</code> or <code>vegetable</code>) and <code>type</code> for fine-grained classification (<code>pome</code>, <code>berry</code>, <code>citrus</code>, &#x2026;).  We don't <i>want</i> to declare <code>citrus</code> as a third completion category &#x2014; that would collapse the two axes back into one, force the data's <code>category</code> field to mean both "fruit-vs-vegetable" <i>and</i> "this particular kind of fruit", and the next refinement (say, expensive citrus) would force yet another category symbol.  The right tool is Embark's <i>transformer</i> mechanism: keep the framework's category vocabulary flat (just <code>fruit</code> and <code>vegetable</code>), and specialize <i>below</i> that vocabulary by reading additional fields off the candidate.
</p>

<p>
A transformer is a function attached to a category in <code>embark-transformer-alist</code>.  When Embark is about to dispatch on a candidate, it looks up the prompt's category in that alist; if it finds a transformer, it calls the transformer once with the original type and target.  The transformer returns a refined <code>(TYPE . TARGET)</code> cons, and Embark dispatches on that refined type.
</p>

<p>
Now the load-bearing detail: <i>Embark applies the transformer exactly once.</i>  Look at <code>embark.el</code>'s dispatch path and you'll see <code>(if-let (transform (alist-get type embark-transformer-alist)) ...)</code> &#x2014; a single <code>if-let</code>, no recursion, no chain.  The transformer for the original prompt type fires, returns a refined type, and Embark dispatches on that refined type without consulting the alist a second time.
</p>

<p>
This is consequential when Consult is in the picture.  <code>consult--multi</code> sets the prompt's completion-metadata category to <code>multi-category</code> (not <code>fruit</code> or <code>vegetable</code>) and stamps each candidate with a <code>multi-category</code> text property holding <code>(SOURCE-CATEGORY . CAND)</code>.  Embark ships a built-in transformer registered on <code>multi-category</code> (<code>embark--refine-multi-category</code>) that extracts the source category from that property and returns <code>(fruit . CAND)</code> or <code>(vegetable . CAND)</code>.  But that's the only transformer Embark will run.  If we naively wrote a <code>fruit</code> transformer and registered it on <code>fruit</code>, <i>it would never fire</i> in our prompt, because the prompt's category is <code>multi-category</code> and Embark only consults the alist once, against that original key.
</p>

<p>
So in a multi-category prompt, the <code>multi-category</code> transformer is the only integration point for any refinement below the source level.  We replace Embark's built-in with our own that does both jobs in a single pass: the same source-category extraction (delegated to the built-in helper), plus our citrus refinement on top.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-multi-category-transformer</span> <span style="color: #4f54aa;">(</span>type cand<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Refine `</span><span style="color: #065fff;">multi-category</span><span style="color: #7fff7fff7fff;">' candidates with citrus refinement layered on.
Embark applies the prompt-category transformer exactly once, so any
per-source refinement must happen inside this single function.  We
delegate to Embark's built-in `</span><span style="color: #065fff;">embark--refine-multi-category</span><span style="color: #7fff7fff7fff;">' for the
source-category extraction, then refine `</span><span style="color: #065fff;">fruit</span><span style="color: #7fff7fff7fff;">' to `</span><span style="color: #065fff;">fruit-citrus</span><span style="color: #7fff7fff7fff;">' when
the candidate's `</span><span style="color: #065fff;">type</span><span style="color: #7fff7fff7fff;">' property is `</span><span style="color: #065fff;">citrus</span><span style="color: #7fff7fff7fff;">'."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>refined <span style="color: #b65050;">(</span>embark--refine-multi-category type cand<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span><span style="color: #6052cf;">if</span> <span style="color: #1f77bb;">(</span><span style="color: #6052cf;">and</span> <span style="color: #b65050;">(</span>eq <span style="color: #4250ef;">(</span>car refined<span style="color: #4250ef;">)</span> 'fruit<span style="color: #b65050;">)</span>
             <span style="color: #b65050;">(</span>eq <span style="color: #4250ef;">(</span>get-text-property 0 'type cand<span style="color: #4250ef;">)</span> 'citrus<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
        <span style="color: #1f77bb;">(</span>cons 'fruit-citrus <span style="color: #b65050;">(</span>cdr refined<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
      refined<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">setf</span> <span style="color: #4f54aa;">(</span>alist-get 'multi-category embark-transformer-alist<span style="color: #4f54aa;">)</span>
      #'my-multi-category-transformer<span style="color: #008858;">)</span>
</pre>
</div>

<p>
Then we register one keymap per target type.  Every fruit gets the general fruit map, citrus gets the specialized one (via our transformer's refinement), and every vegetable gets the vegetable map:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>add-to-list 'embark-keymap-alist '<span style="color: #4f54aa;">(</span>fruit        . my-fruit-general-map<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>add-to-list 'embark-keymap-alist '<span style="color: #4f54aa;">(</span>fruit-citrus . my-fruit-citrus-map<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>add-to-list 'embark-keymap-alist '<span style="color: #4f54aa;">(</span>vegetable    . my-vegetable-general-map<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
What about <code>embark-act-all</code> over a candidate set that spans both sources?  When all candidates share a refined type (after our transformer runs), Embark dispatches on that unified type &#x2014; so a fruits-only narrowing dispatches on <code>fruit</code>, and a vegetables-only narrowing dispatches on <code>vegetable</code>.  When the set spans sources (e.g., a cross-cutting <code>@summer</code> filter), Embark falls back to the original prompt category, <code>multi-category</code>.  We register a fallback keymap so cross-source <code>embark-act-all</code> still has <code>i</code> and <code>a</code> available:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>add-to-list 'embark-keymap-alist '<span style="color: #4f54aa;">(</span>multi-category . my-fruit-general-map<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
This is the integration, end to end.  <code>consult--multi</code> sets the prompt category to <code>multi-category</code> and stamps each candidate with a <code>multi-category</code> text property holding the source category.  Embark looks up <code>multi-category</code> in <code>embark-transformer-alist</code> and runs our transformer, which extracts the source category (via the built-in helper), then refines <code>fruit</code> to <code>fruit-citrus</code> when the candidate's <code>type</code> property is <code>citrus</code>.  Embark dispatches on the refined type, finds the matching keymap in <code>embark-keymap-alist</code>, and opens it.  One transformer, three keymap entries plus a fallback &#x2014; all riding on the per-source <code>:category</code> we declared in Phase 5 and the <code>type</code> field we put on every candidate at the top of the post.
</p>
</div>
</div>
<div id="outline-container-embark-demo" class="outline-3">
<h3 id="embark-demo"><span class="section-number-3">10.4.</span> The Demo</h3>
<div class="outline-text-3" id="text-embark-demo">
<p>
Assuming you have <code>embark-act</code> bound somewhere (the canonical binding is <code>C-.</code>; if you don't, evaluate <code>(keymap-global-set "C-." #'embark-act)</code> first):
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>consult-produce-picker<span style="color: #008858;">)</span>
</pre>
</div>

<p>
Let's start by acting on a single candidate.  Run the picker, narrow or scroll until <code>apple</code> is highlighted, and press <code>C-.</code>.  A small keymap-hint popup appears, listing the action keys available for this candidate: <code>i</code> for inspect, <code>a</code> for add-to-cart, plus everything <code>embark-general-map</code> inherits.  Pressing <code>i</code> closes the popup, closes the minibuffer, and pops open a new buffer called <code>*Produce Inspect*</code> showing the apple's text properties pretty-printed: a comment line <code>;; apple</code> followed by <code>(category fruit type pome color "red" season "fall" price 1.29)</code>.  Embark invoked <code>my-inspect-produce</code> with the candidate string; our action grabbed the candidate's text-property plist via <code>text-properties-at</code> and handed it to <code>pp</code>.
</p>

<p>
Now clear the prompt and scroll to <code>lemon</code>.  Press <code>C-.</code>.  The menu is different this time!  <code>i</code> and <code>a</code> are still there, but there is a third entry: <i><code>j</code> juice</i>.  Inside our <code>multi-category</code> transformer, the source-category extraction returns <code>fruit</code>, and then the citrus check sees <code>(get-text-property 0 'type cand)</code> = <code>citrus</code> and refines the dispatch type to <code>fruit-citrus</code>.  Embark looks up <code>fruit-citrus</code> in the keymap alist, finds <code>my-fruit-citrus-map</code> (which inherits the <code>i</code> and <code>a</code> bindings from the general map and adds <code>j</code> on top), and presents it after <code>embark-act</code>.  Press <code>j</code> and <code>🍹 Juicing lemon!</code> flashes into the echo area.
</p>

<p>
Try <code>embark-act</code> on <code>strawberry</code>.  The popup is back to just <code>i</code> and <code>a</code>, with no <code>j</code> in sight.  A strawberry's <code>type</code> is <code>berry</code>, so the citrus branch of our transformer doesn't fire, the refined type stays <code>fruit</code>, and Embark picks <code>my-fruit-general-map</code>.  Now try <code>carrot</code>: the popup is <code>i a r</code>.  <i><code>r</code> roast</i> is there because <code>carrot</code> came from the vegetable source &#x2014; our transformer extracts <code>vegetable</code> from <code>multi-category</code>, the citrus branch is irrelevant (the source category is not <code>fruit</code>), and Embark dispatches on <code>vegetable</code>.
</p>

<p>
For the multi-candidate case, start the picker again and narrow to fruits with <code>f SPC</code>, then type <code>@summer</code> to filter to summer fruits within fruits.  The list collapses to the summer fruits.  Press <code>embark-act-all</code> (the canonical binding is <code>A</code> from the <code>embark-act</code> popup, or <code>C-u C-.</code> directly).  Embark prompts for a single action to apply to <i>every visible candidate</i>; press <code>a</code> for add-to-cart.  In one keystroke, all ten summer fruits are appended to the <code>*Cart*</code> buffer, one per line, each with its price.  You can also select individual candidates with <code>embark-select</code> (which I have bound to <code>C-SPC</code>).
</p>

<p>
Now clear the narrow and try <code>@summer</code> across the full produce corpus.  Hit <code>embark-act-all</code>, press <code>a</code> for add-to-cart.  All thirteen go into <code>*Cart*</code>.  This is where the <code>multi-category</code> fallback we registered earlier comes in handy, because the candidate set spans both sources, embark dispatches against the <code>multi-category</code> type, and the fallback gives us <code>i</code> and <code>a</code> (the actions defined on <b>every</b> produce item).  Type-specific actions like <code>j</code> and <code>r</code> aren't available in this state, which makes logical sense, because a single keystroke can't simultaneously juice a citrus and roast a vegetable.
</p>

<p>
The same text properties that Marginalia was reading in Phase 4 are what the action functions and the transformer are reading now.  Three different packages are cooperating, on three different hooks, off the <i>same</i> candidate, without ever knowing about each other.
</p>
</div>
</div>
<div id="outline-container-embark-collect-export" class="outline-3">
<h3 id="embark-collect-export"><span class="section-number-3">10.5.</span> Collect and export</h3>
<div class="outline-text-3" id="text-embark-collect-export">
<p>
So far the picker has been a <i>selection</i> surface: pick something and act on it.  
</p>

<p>
Embark provides two further commands that promote the candidate list itself into a first-class buffer.
</p>

<p>
<b><code>embark-collect</code></b> snapshots the current candidate set into a fresh <code>*Embark Collect*</code> buffer with the original keymap dispatch still attached to each row.  Run the picker, type <code>@summer</code> to narrow to summer items across both sources, then <code>M-x embark-collect</code> (or invoke <code>embark-act</code> with a prefix arg and pick <code>embark-collect</code>).  The thirteen summer items land in a buffer that outlives the minibuffer; press <code>embark-act</code> on <code>cherry</code> and the fruit map dispatches; press <code>embark-act</code> on <code>tomato</code> and the vegetable map dispatches.  No registration is needed; <code>embark-collect</code> works on any candidate set automatically.
</p>

<p>
<b><code>embark-export</code></b> goes one step further.  Where <code>collect</code> produces a generic buffer of strings, <code>export</code> <i>materializes</i> the candidates into the major mode appropriate to their type.  File candidates become a Dired buffer; buffer candidates become an Ibuffer; grep results become a wgrep-editable buffer; etc&#x2026;.  For our produce, the natural target is Emacs's built-in <code>tabulated-list-mode</code>.
</p>

<p>
We register a per-category exporter the same way we registered keymaps in <a href="https://www.chiply.dev/#embark-keymaps">The Keymaps</a>.  The exporter takes the list of candidate strings and produces the target buffer:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-export-produce-tabulated</span> <span style="color: #4f54aa;">(</span>candidates<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Export produce CANDIDATES into a tabulated-list buffer."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>buf <span style="color: #b65050;">(</span>get-buffer-create <span style="color: #4250ef;">"*Produce*"</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span><span style="color: #6052cf;">with-current-buffer</span> buf
      <span style="color: #1f77bb;">(</span>tabulated-list-mode<span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span><span style="color: #6052cf;">setq</span> tabulated-list-format
            <span style="color: #b65050;">[</span><span style="color: #4250ef;">(</span><span style="color: #4250ef;">"Name"</span>   14 t<span style="color: #4250ef;">)</span>
             <span style="color: #4250ef;">(</span><span style="color: #4250ef;">"Cat"</span>     10 t<span style="color: #4250ef;">)</span>
             <span style="color: #4250ef;">(</span><span style="color: #4250ef;">"Type"</span>   13 t<span style="color: #4250ef;">)</span>
             <span style="color: #4250ef;">(</span><span style="color: #4250ef;">"Color"</span>  10 t<span style="color: #4250ef;">)</span>
             <span style="color: #4250ef;">(</span><span style="color: #4250ef;">"Season"</span> 12 t<span style="color: #4250ef;">)</span>
             <span style="color: #4250ef;">(</span><span style="color: #4250ef;">"Price"</span>   8 t<span style="color: #4250ef;">)</span><span style="color: #b65050;">]</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span><span style="color: #6052cf;">setq</span> tabulated-list-entries
            <span style="color: #b65050;">(</span>mapcar
             <span style="color: #4250ef;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #008858;">(</span>cand<span style="color: #008858;">)</span>
               <span style="color: #008858;">(</span>list cand
                     <span style="color: #6052cf;">(</span>vector <span style="color: #a45f22;">(</span>substring-no-properties cand<span style="color: #a45f22;">)</span>
                             <span style="color: #a45f22;">(</span>symbol-name <span style="color: #008858;">(</span>get-text-property 0 'category cand<span style="color: #008858;">)</span><span style="color: #a45f22;">)</span>
                             <span style="color: #a45f22;">(</span>symbol-name <span style="color: #008858;">(</span>get-text-property 0 'type cand<span style="color: #008858;">)</span><span style="color: #a45f22;">)</span>
                             <span style="color: #a45f22;">(</span>get-text-property 0 'color cand<span style="color: #a45f22;">)</span>
                             <span style="color: #a45f22;">(</span>get-text-property 0 'season cand<span style="color: #a45f22;">)</span>
                             <span style="color: #a45f22;">(</span>format <span style="color: #4250ef;">"$%.2f"</span> <span style="color: #008858;">(</span>get-text-property 0 'price cand<span style="color: #008858;">)</span><span style="color: #a45f22;">)</span><span style="color: #6052cf;">)</span><span style="color: #008858;">)</span><span style="color: #4250ef;">)</span>
             candidates<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>tabulated-list-init-header<span style="color: #1f77bb;">)</span>
      <span style="color: #1f77bb;">(</span>tabulated-list-print<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>pop-to-buffer buf<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">dolist</span> <span style="color: #4f54aa;">(</span>cat '<span style="color: #ba35af;">(</span>fruit fruit-citrus vegetable multi-category<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>add-to-list 'embark-exporters-alist
               <span style="color: #ba35af;">(</span>cons cat #'my-export-produce-tabulated<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
The exporter reaches for the same text properties that the annotator and the actions read from.  Once again, the candidate-as-currency convention is being leveraged.
</p>

<p>
The four registrations are worth a moment.  <code>embark-export</code> unifies the candidate set's type by calling our transformer on every candidate and checking that they all produce the same refined type (<code>(cl-every ...)</code> in <code>embark--maybe-transform-candidates</code>).  When all candidates are vegetables, our transformer returns <code>vegetable</code> for each, unification succeeds, and Embark dispatches on <code>vegetable</code>.  Same for an all-citrus filter (<code>f SPC @citrus</code> → all <code>fruit-citrus</code>) or an all-non-citrus filter.  But the obvious case (narrow to fruits with <code>f SPC</code>) gives a <i>mix</i> of citrus and non-citrus, which our transformer refines to two different types (<code>fruit-citrus</code> and <code>fruit</code>).  Unification fails, and Embark falls back to the prompt's original category: <code>multi-category</code>.  Registering our exporter on <code>multi-category</code> as well makes the export work in that mixed-but-still-produce case.
</p>

<p>
A trade-off worth being honest about: that <code>multi-category</code> registration is <i>shared</i> with every other <code>consult--multi</code> prompt in the session.  In a real package, you'd want to guard the exporter against non-produce candidates, or scope the registration more tightly.  For this walkthrough we accept the broad registration and move on.
</p>

<p>
Run the picker, narrow to fruits with <code>f SPC</code>, then <code>M-x embark-export</code> (or <code>embark-act</code> with prefix arg, then <code>E</code>).  A <code>*Produce*</code> buffer pops open in tabulated-list mode:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>consult-produce-picker<span style="color: #008858;">)</span>
</pre>
</div>

<pre class="example" id="orgc470412">
Name           Cat       Type          Color      Season       Price
apple          fruit     pome          red        fall        $1.29
pear           fruit     pome          green      fall        $1.79
strawberry     fruit     berry         red        spring      $3.99
blueberry      fruit     berry         blue       summer      $4.99
raspberry      fruit     berry         red        summer      $5.99
...
</pre>

<p>
Every column is sortable from its header (the trailing <code>t</code> flag in <code>tabulated-list-format</code> turns sortability on per-column); <code>n</code> and <code>p</code> move between rows.  Since each row's <code>tabulated-list-id</code> is the original propertized candidate, <code>embark-act</code> on a row still dispatches to the right keymap based on category, including the citrus juicer via the transformer.  <code>embark-act</code> on the <code>cherry</code> row offers <code>i a</code> (general fruit); <code>embark-act</code> on <code>lemon</code> offers <code>i a j</code> (citrus, refined by the transformer); <code>embark-act</code> on a row exported from a vegetable narrowing would offer <code>i a r</code> (vegetable).  Nothing changes about the per-candidate dispatch just because the candidates moved into a major mode, which is very convenient!  This is one of my favourite things about Embark.  It works on minibuffer candidates in the all-too-common ICR setting, but it also works on any arbitrary piece of text in Emacs.  With Embark, literally everything in Emacs becomes left-clickable in this way.
</p>

<p>
Cross-cutting filters (e.g., <code>@summer</code> spanning fruits and vegetables) also fall back to <code>multi-category</code> for the same reason (the candidates' refined types differ) so the same registration carries them through.  <code>embark-collect</code> remains available as a finer-grained alternative when you want a generic candidate-set buffer rather than a tabulated table.
</p>

<p>
The architectural take: <code>embark-export</code> is <i>not</i> a Consult feature, <i>not</i> a Marginalia feature, <i>not</i> part of any pipeline our other phases set up.  It is an Embark feature that works with any <code>completing-read</code>-driven prompt that has a category and a registered exporter.  And the exporter we wrote is one more consumer of the candidate's text properties alongside the annotator (Phase 4), the action functions (this phase), the transformer (this phase), and the per-category keymap dispatch (also this phase).  If you ever decide a tabulated list isn't the right export target (maybe a csv-mode or org-mode would be better) swap the exporter function and every other layer of the picker stays exactly where it is.
</p>
</div>
</div>
</div>
<div id="outline-container-prescient" class="outline-2">
<h2 id="prescient"><span class="section-number-2">11.</span> Phase 7: Prescient &#x2014; Recency and Frequency Sorting&#xa0;&#xa0;&#xa0;<span class="tag"><span class="prescient">prescient</span>&#xa0;<span class="sorting">sorting</span></span></h2>
<div class="outline-text-2" id="text-prescient">
<p>
The last concern from <a href="https://www.chiply.dev/post-vompeccc#hidden-complexity">the orthogonal six</a> is <i>sorting</i>.
</p>

<p>
So far, sorting has been incidental.  Vertico ships a default sort that draws on minibuffer history, but it ignores frequency entirely and resets when Emacs restarts.  <a href="https://www.chiply.dev/post-vompeccc#prescient">Prescient</a> is the dedicated package for this concern: it computes a combined <i>frecency</i> score (recency + frequency) per candidate and hands it to Vertico as the sort function.  With <code>prescient-persist-mode</code> the score survives Emacs restarts; even without it, the score updates from the very first selection.
</p>

<p>
That last point is what makes Prescient demonstrable in a from-scratch walkthrough.  The score for a never-selected candidate is zero.  Pick a candidate once and its score jumps above zero, putting it at the top of the next prompt.  One selection is enough to see the effect.
</p>

<p>
One wrinkle to acknowledge before the setup.  Of the six VOMPECCC packages we are using, Prescient is the only one that bundles two concerns: sorting (the part we want here) <i>and</i> its own filtering style.  <code>vertico-prescient-mode</code> toggles both by default &#x2014; which would replace the Orderless setup from Phase 3 and silently break the custom dispatchers (<code>~</code>, <code>!</code>, backtick, <code>@</code>) we have been relying on for the last four phases.  <a href="https://www.chiply.dev/post-vompeccc#prescient">Post 2</a> flags this same two-concern split and notes the common workaround: keep Orderless for filtering, take Prescient's sort, and disable the filter takeover.  That is what we do here:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">setq</span> vertico-prescient-enable-filtering nil<span style="color: #008858;">)</span>
<span style="color: #008858;">(</span>vertico-prescient-mode 1<span style="color: #008858;">)</span>
</pre>
</div>

<p>
This is the entire prescient integration.  (Adding <code>(prescient-persist-mode 1)</code> would persist scores across Emacs sessions; we leave it off here so the demo's state stays bounded to this single Emacs session.)
</p>

<p>
This demo is simple.  Run the picker once, select an item, then re-run.  The item we just picked will be the first in the list. 
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>consult-produce-picker<span style="color: #008858;">)</span>
</pre>
</div>

<p>
All five layers continue untouched, while a sixth package quietly improves the order in which their candidates land on screen.
</p>
</div>
</div>
<div id="outline-container-async" class="outline-2">
<h2 id="async"><span class="section-number-2">12.</span> Phase 8: Async &#x2014; Going Remote with <code>consult--dynamic-collection</code>&#xa0;&#xa0;&#xa0;<span class="tag"><span class="async">async</span>&#xa0;<span class="consult">consult</span>&#xa0;<span class="remote">remote</span></span></h2>
<div class="outline-text-2" id="text-async">
<p>
The picker we have works because the entire produce corpus is local: <code>my-produce</code> is a literal thirty-item list, and each Consult source's <code>:items</code> callback returns its slice synchronously.  Real-world sources are usually remote, and the <code>spot</code> post demonstrates this w/r/t the Spotify API.  Filtering at the Emacs side stops being viable once the corpus has thousands or millions of entries, and you have to push the query to the server and let the server tell you which candidates match.
</p>

<p>
Consult ships <code>consult--dynamic-collection</code> for exactly this case.  It wraps a completion function so that each keystroke fires a debounced, cancellable query.  The function you write only has to answer <i>"given this query string, what are the candidates?"</i>  Consult handles the debouncing (default ~200ms via <code>consult-async-input-debounce</code>), the cancellation of stale responses (via <code>while-no-input</code>), and the display refresh.
</p>
</div>
<div id="outline-container-async-mock" class="outline-3">
<h3 id="async-mock"><span class="section-number-3">12.1.</span> Mocking the API</h3>
<div class="outline-text-3" id="text-async-mock">
<p>
We don't have a real produce service to hit, but we can mock one faithfully enough to demonstrate the pattern.  Real search backends (Spotify, GitHub, Algolia, Slack, &#x2026;) almost always do something <i>fuzzier</i> than literal substring matching, so the mock matches that style.  Each character of the query must appear in the candidate's name in order, possibly with gaps in between, case-insensitively.  The mock takes a query string, sleeps 😴 to simulate network round-trip, and returns the matches:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-mock-api-latency</span> 0.1
  <span style="color: #7fff7fff7fff;">"Simulated API latency in seconds.  Set to 0 to disable the sleep."</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-mock-api-call-count</span> 0
  <span style="color: #7fff7fff7fff;">"Counter so we can watch the mock API being hit."</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-mock-fuzzy-match-p</span> <span style="color: #4f54aa;">(</span>query name<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Return non-nil if NAME flex-matches QUERY (chars in order, case-insensitive)."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">let</span> <span style="color: #ba35af;">(</span><span style="color: #1f77bb;">(</span>case-fold-search t<span style="color: #1f77bb;">)</span>
        <span style="color: #1f77bb;">(</span>re <span style="color: #b65050;">(</span>mapconcat <span style="color: #4250ef;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #008858;">(</span>c<span style="color: #008858;">)</span> <span style="color: #008858;">(</span>regexp-quote <span style="color: #6052cf;">(</span>char-to-string c<span style="color: #6052cf;">)</span><span style="color: #008858;">)</span><span style="color: #4250ef;">)</span>
                       <span style="color: #4250ef;">(</span>string-to-list query<span style="color: #4250ef;">)</span>
                       <span style="color: #4250ef;">".*"</span><span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>string-match-p re name<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-mock-produce-api</span> <span style="color: #4f54aa;">(</span>query<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Pretend to fetch produce from a remote API matching QUERY.
Sleeps for `</span><span style="color: #065fff;">my-mock-api-latency</span><span style="color: #7fff7fff7fff;">' seconds to simulate network round-trip,
then returns the subset of `</span><span style="color: #065fff;">my-produce</span><span style="color: #7fff7fff7fff;">' whose names fuzzy-match QUERY."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">cl-incf</span> my-mock-api-call-count<span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">when</span> <span style="color: #ba35af;">(</span>&gt; my-mock-api-latency 0<span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>sleep-for my-mock-api-latency<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>cl-remove-if-not
   <span style="color: #ba35af;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #1f77bb;">(</span>cand<span style="color: #1f77bb;">)</span> <span style="color: #1f77bb;">(</span>my-mock-fuzzy-match-p query cand<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span>
   my-produce<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
A real client would issue an HTTP request, parse JSON, and propertize the response.  Ours is a fuzzy regex match over the corpus, gated by <code>sleep-for</code> so latency is observable.  The call counter lets us watch the API being hit (or not) as we type.
</p>
</div>
</div>
<div id="outline-container-async-single" class="outline-3">
<h3 id="async-single"><span class="section-number-3">12.2.</span> A single async source</h3>
<div class="outline-text-3" id="text-async-single">
<p>
The completion function takes a query and returns a list of propertized candidates, the same text properties as the synchronous version (<code>category</code>, <code>type</code>, <code>color</code>, <code>season</code>, <code>price</code> all carried directly on each candidate).  Only the source has changed.  Because <code>my-produce</code> is <i>already</i> a list of propertized strings, the completion function reduces to a category filter on whatever the API returns.  We close over <code>cat</code> so each completion category gets its own completion function:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-async-produce-candidates</span> <span style="color: #4f54aa;">(</span>cat<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Return a completion function that yields CAT-category produce matching QUERY."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #ba35af;">(</span>query<span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">(</span>cl-remove-if-not
     <span style="color: #1f77bb;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #b65050;">(</span>cand<span style="color: #b65050;">)</span> <span style="color: #b65050;">(</span>eq <span style="color: #4250ef;">(</span>get-text-property 0 'category cand<span style="color: #4250ef;">)</span> cat<span style="color: #b65050;">)</span><span style="color: #1f77bb;">)</span>
     <span style="color: #1f77bb;">(</span>my-mock-produce-api query<span style="color: #1f77bb;">)</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
The Consult source uses <code>:async</code> rather than <code>:items</code>, wrapping the completion function with <code>consult--dynamic-collection</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-consult-history-fruit-async</span> nil<span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defvar</span> <span style="color: #1f77bb;">my-consult-source-fruit-async</span>
  `<span style="color: #4f54aa;">(</span><span style="color: #ba35af;">:name</span>     <span style="color: #4250ef;">"Fruits (async)"</span>
    <span style="color: #ba35af;">:narrow</span>   ?f
    <span style="color: #ba35af;">:category</span> fruit
    <span style="color: #ba35af;">:async</span>    ,<span style="color: #ba35af;">(</span>consult--dynamic-collection
                <span style="color: #1f77bb;">(</span>my-async-produce-candidates 'fruit<span style="color: #1f77bb;">)</span>
                <span style="color: #ba35af;">:min-input</span> 1<span style="color: #ba35af;">)</span>
    <span style="color: #ba35af;">:history</span>  my-consult-history-fruit-async<span style="color: #4f54aa;">)</span>
  <span style="color: #7fff7fff7fff;">"Async Consult source for fruits."</span><span style="color: #008858;">)</span>

<span style="color: #008858;">(</span><span style="color: #6052cf;">defun</span> <span style="color: #cf25aa;">my-async-produce-picker</span> <span style="color: #4f54aa;">()</span>
  <span style="color: #7fff7fff7fff;">"Pick a fruit using an async Consult source."</span>
  <span style="color: #4f54aa;">(</span><span style="color: #6052cf;">interactive</span><span style="color: #4f54aa;">)</span>
  <span style="color: #4f54aa;">(</span>consult--multi <span style="color: #ba35af;">(</span>list my-consult-source-fruit-async<span style="color: #ba35af;">)</span>
                  <span style="color: #ba35af;">:prompt</span> <span style="color: #4250ef;">"Fruit: "</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
Run it:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>my-async-produce-picker<span style="color: #008858;">)</span>
</pre>
</div>

<p>
The prompt opens empty; <code>:min-input 1</code> keeps Consult from firing until you type at least one character.  Type <code>b</code> and you will see a moment of latency, and then five fruits whose names contain <code>b</code> (the four berries and <code>banana</code>).  Type <code>e</code> more (<code>be</code>), and you will see another fetch, the four berries appear because each one has a <code>b</code> followed by an <code>e</code> somewhere in its name, while <code>banana</code> has no <code>e</code> and drops out.  
</p>

<p>
Empty the prompt and type rapidly: <code>papaya</code>, holding no key for long.  Consult's debouncer drops intermediate queries, and in a resource-sparing manner, only the queries you <i>paused on</i> reach the API.  When a slow query is mid-flight and your input changes, <code>while-no-input</code> aborts the in-flight computation and restarts on the new input, so stale results never make it to the candidate list.  The completion function we wrote is plain synchronous Lisp; all the cancellation, debouncing, and refresh logic are <code>consult--dynamic-collection</code>'s problem, not ours.
</p>

<p>
A small architectural symmetry worth flagging.  For Consult's "async command" family of commands (<code>consult-grep</code>, <code>consult-ripgrep</code>, <code>consult-find</code>, and friends) input is additionally <i>split</i> between a backend portion and a local-filter portion via <code>consult-async-split-style</code>.  Setting it to <code>'comma</code> aligns the split character with our <code>orderless-component-separator</code>, so the same <code>,</code> delimits "send this to the server" and "filter the result locally with Orderless."  <code>consult--dynamic-collection</code> (the lighter wrapper we used here) skips the split and forwards the whole input to the completion function, but the harmonisation is right there for prompts that want both.
</p>
</div>
</div>
</div>
<div id="outline-container-substrate" class="outline-2">
<h2 id="substrate"><span class="section-number-2">13.</span> The Payoff: Candidates as Shared Currency&#xa0;&#xa0;&#xa0;<span class="tag"><span class="substrate">substrate</span></span></h2>
<div class="outline-text-2" id="text-substrate">
<p>
Step back and count what each package contributed:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Package</th>
<th scope="col" class="org-left">What it gave us</th>
<th scope="col" class="org-left">Lines we wrote</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Vertico</td>
<td class="org-left">Display control of candidates</td>
<td class="org-left">1</td>
</tr>

<tr>
<td class="org-left">Orderless</td>
<td class="org-left">Multi-component matching, flex, negation, annotation filter dispatch</td>
<td class="org-left">~3</td>
</tr>

<tr>
<td class="org-left">Marginalia</td>
<td class="org-left">Four right-aligned metadata columns + annotation-aware matching</td>
<td class="org-left">~12</td>
</tr>

<tr>
<td class="org-left">Consult</td>
<td class="org-left">Two sources, unified prompt, per-source narrow keys</td>
<td class="org-left">~20</td>
</tr>

<tr>
<td class="org-left">Embark</td>
<td class="org-left">Per-category action keymaps + transformer for citrus refinement + tabular export</td>
<td class="org-left">~50</td>
</tr>

<tr>
<td class="org-left">Prescient</td>
<td class="org-left">Frecency sort that promotes recently/frequently picked candidates</td>
<td class="org-left">2</td>
</tr>
</tbody>
</table>

<p>
Roughly 90 lines of VOMPECCC-facing <code>emacs-lisp</code>, and we have a categorised, narrowable, annotated, annotation-matchable, action-bindable, transformer-refined, exportable, frecency-sorted produce picker (that's a mouthful!).  There is no UI code anywhere in it!  Every line fell into one of two buckets: <i>configuring a VOMPECCC package</i> (<code>add-to-list</code>, <code>setq</code>, <code>defvar-keymap</code>) or <i>declaring our data</i> (the propertized corpus, the annotator, the action bodies, the transformer).
</p>

<p>
The <i>how</i> of the composition, the question <a href="https://www.chiply.dev/post-vompeccc">Post 2</a> gestured at and <a href="https://www.chiply.dev/post-vompeccc-spot">Post 3</a> answered for a larger application, is a single convention repeated across the six packages.  Each candidate is its name as a string, with its full record's fields stamped on as text properties:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span>propertize <span style="color: #4250ef;">"apple"</span>
            'category 'fruit       <span style="color: #a65f6a;">; </span><span style="color: #7fff7fff7fff;">our internal routing key (filter, exporter)
</span>            'type     'pome        <span style="color: #a65f6a;">; </span><span style="color: #7fff7fff7fff;">read by our Embark transformer
</span>            'color    <span style="color: #4250ef;">"</span><span style="color: #ffffff; background-color: #ff0000;">red</span><span style="color: #4250ef;">"</span>        <span style="color: #a65f6a;">; </span><span style="color: #7fff7fff7fff;">annotation column
</span>            'season   <span style="color: #4250ef;">"fall"</span>       <span style="color: #a65f6a;">; </span><span style="color: #7fff7fff7fff;">annotation column
</span>            'price    1.29<span style="color: #008858;">)</span>        <span style="color: #a65f6a;">; </span><span style="color: #7fff7fff7fff;">annotation column</span>
</pre>
</div>

<p>
That is the entire integration surface.  The exporter and our filter helper read the candidate's <code>category</code> property directly; the annotator reads <code>type</code>, <code>color</code>, <code>season</code>, <code>price</code>; the Embark transformer reads <code>type</code>.  Vertico, Orderless, and Prescient do their work on the string itself.  Marginalia and Embark dispatch off the prompt's completion-metadata category, which Consult populates from each source's <code>:category</code> key (in turn set to match the data's <code>category</code> property by hand &#x2014; the candidate-as-currency convention paying its dues by keeping our routing key and Consult's source key in lockstep).  When Embark wants to dispatch <i>below</i> the granularity of <code>category</code>, e.g.\ with citrus-only juicing inside the broader <code>fruit</code> category, our transformer reads <code>type</code> off the candidate and refines accordingly.  These six packages communicate interoperably through Emacs's completion substrate, with one shared currency: the propertized candidate.
</p>

<p>
A note on shape.  We carry the produce fields as <i>individual</i> text properties because the records are shallow and the property bag stays small.  Codebases like <a href="https://www.chiply.dev/post-vompeccc-spot"><code>spot</code></a>, where each candidate is a Spotify track or playlist with dozens of nested fields, take a different approach and attach the entire record under a single <code>multi-data</code> property and let consumers reach into the plist for deep fields.  Both shapes meet the substrate at the same hook (a propertized candidate whose properties happen to encode a typed record) and the choice between them is purely local to the corpus.
</p>

<p>
Replace <code>my-produce</code> with GitHub issues, S3 buckets, Slack channels, ten thousand Org headings, a music library, or your ticketing system.  The integration surface is unchanged.  The corpus reshapes; the annotator reads different fields; the Consult sources partition differently; the Embark keymaps and transformer do different things.  Critically, none of the <i>framework</i> code moves.  This is the architecture behind <a href="https://www.chiply.dev/post-vompeccc-spot"><code>spot</code></a>, <code>consult-gh</code>, <code>consult-notes</code>, <code>consult-omni</code>, and the growing ecosystem of ICR-driven packages that <a href="https://www.chiply.dev/post-vompeccc#subset-property">can be assembled from the same substrate</a> without rebuilding any of its layers.
</p>
</div>
</div>
<div id="outline-container-further" class="outline-2">
<h2 id="further"><span class="section-number-2">14.</span> What We Didn't Cover&#xa0;&#xa0;&#xa0;<span class="tag"><span class="further">further</span></span></h2>
<div class="outline-text-2" id="text-further">
<p>
A handful of VOMPECCC features are worth pointing at, because you <i>will</i> want them eventually and they are all continuations of the same substrate pattern you just saw:
</p>

<ul class="org-ul">
<li><b>Consult preview.</b>  A source's <code>:state</code> key accepts a function that fires on candidate navigation, not just selection.  <code>consult-line</code> uses it to scroll the current buffer to the highlighted line; <code>consult-theme</code> applies the theme as you scroll.  For a produce picker, <code>:state</code> could pop a "tasting notes" buffer that updates as you move through the list.</li>
<li><b>More Embark transformers.</b>  Phase 6 used a transformer to refine <code>fruit</code> → <code>fruit-citrus</code> based on <code>type</code>.  Transformers can refine on <i>any</i> predicate over the candidate's properties &#x2014; e.g., turn every produce item into <code>produce-expensive</code> if <code>price &gt; 3.00</code>, so you can layer arbitrary fine-grained dispatch on top of the coarse <code>category</code> without inventing new completion categories.  There is a huge amount of power there, especially when you understand that Embark's utility expands greatly when you are willing to start typing entities in Emacs, whether they are in the minibuffer or regular buffers.</li>
<li><b><code>prescient-persist-mode</code>.</b>  Phase 7 enabled <code>vertico-prescient-mode</code> without persistence to keep the demo bounded; in a real configuration, <code>(prescient-persist-mode 1)</code> writes the frecency table to disk so your most-picked candidates stay at the top across Emacs restarts.</li>
<li><b>Corfu and Cape.</b>  In-buffer completion, out of scope for a picker.  If you have a programming-language buffer open and you want to complete symbols with the same matching style and annotation columns, Corfu and Cape are how.</li>
</ul>

<p>
Each of these is an incremental add on the same foundational Emacs completion substrate.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">15.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
This post built <a href="https://www.chiply.dev/#setup">a 30-item produce picker in Emacs</a> by layering six VOMPECCC packages one at a time on top of <a href="https://www.chiply.dev/#baseline">stock <code>completing-read</code></a>.  <a href="https://www.chiply.dev/#vertico">Vertico</a> rendered the candidate list vertically with a one-line mode activation; <a href="https://www.chiply.dev/#orderless">Orderless</a> added multi-component matching via the <code>completion-styles</code> hook plus four custom style dispatchers (<code>~</code> flex, backtick initialism, <code>!</code> negation, <code>@</code> annotation with a <code>@~</code> flex variant), each accepting its trigger character at either prefix or suffix position; <a href="https://www.chiply.dev/#marginalia">Marginalia</a> added four metadata columns per candidate (type, color, season, price) by reading text properties attached to each candidate string with <code>propertize</code>, and along the way made the annotation text <i>itself</i> searchable through Orderless's <code>@</code> dispatcher &#x2014; including the type column, so <code>@berry</code> and <code>@citrus</code> become valid one-component filters; <a href="https://www.chiply.dev/#consult">Consult</a>'s <code>consult--multi</code> unified the corpus into two categorised sources (fruit, vegetable) under one prompt with per-source narrow keys and per-source <code>:history</code> variables, with each source's <code>:category</code> <i>matching the value the data declares</i>, and <a href="https://www.chiply.dev/#consult-history"><code>consult-history</code> bound to <code>C-r</code></a> turning those scoped histories into a recall surface for both prior selections and (via <code>M-RET</code> or a <code>minibuffer-exit-hook</code>) prior Orderless queries; <a href="https://www.chiply.dev/#embark">Embark</a> attached contextual actions via per-category keymaps (one for fruits, one for vegetables, one specialized for citrus) plus an Embark <i>transformer</i> registered on <code>multi-category</code> that does both jobs in a single pass &#x2014; because Embark fires the prompt-category transformer exactly once, the <code>multi-category</code> transformer is the one place to put both source-category extraction (delegating to the built-in helper) and our citrus refinement (reading <code>type</code> off the candidate and refining <code>fruit</code> to <code>fruit-citrus</code>) &#x2014; and an <code>embark-export</code> path that materialises candidates into a sortable <code>tabulated-list-mode</code> buffer; <a href="https://www.chiply.dev/#prescient">Prescient</a> then hooked Vertico's sort slot to surface recently and frequently picked candidates first, with the rank visibly shifting after a single selection; finally <a href="https://www.chiply.dev/#async">an async variant</a> swapped <code>:items</code> for <code>consult--dynamic-collection</code> over a faithfully-mocked produce API, demonstrating debounced/cancellable remote queries and discussing when the <code>spot</code>-style cache+mutex pattern is load-bearing (multiple sources sharing one endpoint) versus defensive ceremony (single thread, single source).  The entire integration surface across the six packages was <a href="https://www.chiply.dev/#substrate">one propertized candidate carrying its fields as text properties &#x2014; <code>category</code> as our internal routing key, kept aligned with each Consult source's <code>:category</code> so the framework's own <code>multi-category</code> channel matches; <code>type</code> read by our Embark transformer; the rest for annotation columns</a> &#x2014; no package calls another's API, none imports another's internals, and swapping <code>my-produce</code> for any other typed corpus would leave every line of the framework configuration unchanged.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Gödel, Escher, Bach, Wallace: the &quot;o&apos;s, d&apos;s and p&apos;s&quot; in Infinite Jest</title>
      <link>https://www.chiply.dev/post-reperspectivizing-orin</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-reperspectivizing-orin</guid>
      <pubDate>Sat, 25 Apr 2026 22:53:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Content note: This essay necessarily discusses child sexual abuse and incest as implicit elements of Infinite Jest, and includes excerpts from the novel containing explicit language. The argument is s...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="dfw">dfw</span>&#xa0;<span class="literature">literature</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org3b700c8" class="figure">
<p><img src="https://www.chiply.dev/images/reperspectivizing-orin-banner.png" alt="reperspectivizing-orin-banner.png" />
</p>
<p><span class="figure-number">Figure 1: </span>Figure 64 from Douglas Hofstadter's <i>Gödel, Escher, Bach: An Eternal Golden Braid</i> (1979), p. 335.</p>
</div>

<blockquote>
<p>
<b>Content note:</b> This essay necessarily discusses child sexual abuse and incest as implicit elements of <i>Infinite Jest</i>, and includes excerpts from the novel containing explicit language.  The argument is speculative and text-based, but engages the subject matter directly.  Reader discretion is advised.
</p>
</blockquote>

<p>
Orin Incandenza looks, on first read, like the villain of <i>Infinite Jest</i>.  He is cruel to animals and to his own brother; he serially seduces married mothers and abandons them before breakfast; and he retrieves his father's lethal Entertainment cartridge from the corpse's skull, disperses it to his mother's former lovers, and eventually hands the master copy to the terrorists trying to weaponize it.  But <i>Infinite Jest</i> constantly dares its readers to subvert first impressions, and character roles blur along with everything else in a novel without a traditional plot arc.  
</p>

<p>
A closer look at Orin's past, and at the last words he utters in the novel, recasts him as something closer to a victim than a villain.  His apparent evil, this essay argues, is the recursive (self-feeding) product of childhood trauma, specifically the sexual abuse by his mother, Avril Incandenza.  The argument is offered as an explanation, not exoneration: the cruelty Orin inflicts on others remains real, and the people he harms (Mario, his Subjects, Joelle, the recipients of the Entertainment) are not made less harmed by the fact that Orin himself was harmed first.
</p>

<p>
This essay's novel contribution to the critical literature is a typographic close-reading of one moment in Orin's morning chapter, where Wallace describes a peculiar feature of a Subject's handwritten note: "every single circle &#x2013; o's, d's, p's, the #s 6 and 8 &#x2013; is darkened in" (pg. 43).  The argument is that the three darkened letters (O, D, P) spell, in Orin's perception, the name <i>Oedipus</i>.  This may seem like a reach, but the encoding becomes the smoking gun in the case against Avril Incandenza when you appreciate Wallace's intellectual debt to Douglas Hofstadter and <i>Gödel, Escher, Bach</i> &#x2013; a debt the essay documents in detail below.  <i>GEB</i> is the foundational text in which typographic position carries semantic weight, and Wallace, per his biographer, "actually shoved this book excitedly at people in the eighties."  The figure at the top of this essay is <i>GEB</i>'s own demonstration of the move: the outer words HOLISM and IONISM are formed of twelve letters whose constituent shapes spell REDUCTIONISM, a word hidden across the morphology of two others.  And the figure deepens once one sees the dialogue it sits inside.  In <i>GEB</i>'s "&#x2026;Ant Fugue," Hofstadter has the Crab, the Anteater, Achilles, and the Tortoise each look at the same picture and each see a different word:
</p>

<blockquote>
<p>
<b>Crab:</b> Well, what have we here?  Oh, I see: it's 'HOLISMIONISM,' written in large letters that first shrink and then grow back to their original size.
</p>

<p>
<b>Anteater:</b> Well, what have we here?  Oh, I see: it's 'REDUCTHOLISM,' written in small letters that first grow and then shrink back to their original size.
</p>
</blockquote>

<p>
Achilles, in turn, sees HOLISM written twice; the Tortoise sees REDUCTIONISM written once.  The picture is the same; the perception supplies the word.  That is the o/d/p situation in miniature.  The Subject's note is the picture; Orin is one of the characters; "Oedipus" is what he sees that nobody else can.  Without that key, the o/d/p observation reads as coincidence, but with it, the encoding reads as Wallace marking the page with the signature of an event he did not put on it directly.
</p>
</div>
</div>
<div id="outline-container-orin-the-apparent-antagonist" class="outline-2">
<h2 id="orin-the-apparent-antagonist"><span class="section-number-2">2.</span> Orin the Apparent Antagonist&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span></span></h2>
<div class="outline-text-2" id="text-orin-the-apparent-antagonist">
<p>
Orin is presented as a deplorable character throughout the novel.  His introduction sees him being cruel to cockroaches, torturing them by suffocating them under glass tumblers (pg. 45).  This is not the only time Orin is seen being remorselessly cruel to animals.  Indeed, in his accident with the family dog, S. Johnson, he was unapologetic (n. 269 pg. 1050).  
</p>

<p>
His cruelty to animals is reflected in a general lack of human compassion.  Mario, Orin's physically disabled younger brother, often fell victim to Orin's harsh verbal cruelty (pg. 589).  He chooses to liaise sexually with only married mothers, an interaction he knows harms not only the women, whom he coldly calls "Subjects", but also their children, who (especially in literature) tend to epitomize innocence (n. 110 pg. 1014-1015).
</p>

<p>
Even within the realm of this conscious act of familial corruption, Orin maintains cold detachment towards his Subjects, actively hoping they are gone by the time he awakens (pg. 46).  Even before this documented mission in pursuing Subjects (see "Speedy Seduction Strategy Number [X]" n. 110 pg. 1007), Orin displayed vanity in his selection of a woman who was of "brainlocking beauty" (pg. 295).  Confirming a vain motivation for this relationship, Orin cruelly relinquished Joelle around the time of her disfigurement, the precise circumstances of which the novel leaves deliberately ambiguous (pg. 795).  
</p>

<p>
Finally, in perhaps the epitome of his list of crimes, Orin was the one who retrieved the lethal entertainment master copy from his father's skull, the motivation for this desecration being personal revenge (see Aaron Swartz's <i>What Happened in Infinite Jest?</i>).  In perhaps a worse offense, he traded the Entertainment to the AFR for his life after a considerably mild technical interview (pg. 971-972).
</p>

<p>
Taken together, this evidence would make Orin the villain of Infinite Jest &#x2013; cruel, vain, vengeful, reckless, and implicated in the book's central catastrophe.  But read against his past, and against the specific moment near the novel's end when he breaks, these traits change shape.  Two events in his life perform the shift: an abuse buried in Orin's childhood, and a technical interview by the AFR in which that past finally surfaces.
</p>
</div>
</div>
<div id="outline-container-the-first-event" class="outline-2">
<h2 id="the-first-event"><span class="section-number-2">3.</span> The First Event: Avril's Abuse of Orin&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="avril">avril</span></span></h2>
<div class="outline-text-2" id="text-the-first-event">
<p>
The first of these events is Avril Incandenza's sexual abuse of Orin.  The actual occurrence of this event is implicit, and requires significant analysis to deduce.  Given its fringe nature, this accusation against Avril comes with a large burden of proof.  Plenty of evidence supports it.
</p>

<p>
The implication of incest between Avril and Orin first arises when Himself and Joelle's potential relationship is being discussed.  Himself realizes that Avril was unbothered by the rumored affair between him and Joelle for a specific reason: she was having one of her own.  The evidence which led Jim to believe this was the presence of a name written in his car window's steam (n. 80 pg. 999).  Himself goes no further with this evidence than to conclude an affair was taking place between Avril and some unnamed partner.  The natural curiosity to the reader, then, is the identity of the person in the car with Avril.
</p>

<p>
The text implies that the partner is Orin, and that Orin has perhaps suppressed the memory due to its traumatic nature.  It comes to light (pg. 899) that "Orin alleged in YTMP that when he took the Moms's car in the morning, he sometimes observed the smeared prints of nude human feet on the inside of the windshield."  At first glance, it would seem as though Orin is a third party in a potential affair, as he is 'observ[ing]' evidence of its occurrence.  But his own behavior when pressed about the affair complicates this reading: n. 80 notes that Orin "would not say who or whether he knew who" was in the Volvo with Avril (n. 80 pg. 999), a conspicuous evasion for a purportedly disinterested observer.  Alternative explanations exist &#x2013; Orin might be protecting the family from a less lurid but still embarrassing revelation &#x2013; but few of them account for the specific intensity of his avoidance.  And Marlon Bain, Orin's closest friend, characterizes him to Hugh Steeply this way: "It is not that Orin Incandenza is a liar, but that I think he has come to regard the truth as constructed instead of reported" (n. 269 pg. 1048).
</p>

<p>
One might reasonably ask why the Volvo's partner must be Orin rather than John Wayne, Avril's documented student lover, and the subject of established scholarship on her sexual irregularity.  The text pushes the answer toward Orin by the specific fact of how Jim learned of the affair: the Volvo anecdote is Orin's own.  Orin is the source of the story about the name in the steam; and Orin, uniquely, "would not say who or whether he knew who" was in the car.  Orin reporting on his mother's affair with a student teammate has no particular reason to withhold the name, while Orin reporting on his own abuse by his mother has every reason.  The Wayne affair is the well-documented surface &#x2013; the visible scene of Avril with Wayne.  The Volvo is a different scene underneath it, and the evidence requires Orin to be the boy in the car with her.
</p>

<p>
Indeed, if one cross-references the imagery (fogged windows and footprints) used in describing the affair with the imagery used to describe Orin elsewhere, the text invites the identification of Orin as Avril's victim.  The evidence is circumstantial, but it accumulates in a specific direction.  The identification is a reading the text invites, not a conclusion it hands us.  
</p>

<p>
What follows proceeds on the working hypothesis that Orin was in the Volvo and asks what else in the novel lines up with that reading.
</p>
</div>
</div>
<div id="outline-container-steam-and-footprints" class="outline-2">
<h2 id="steam-and-footprints"><span class="section-number-2">4.</span> Steam and Footprints&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="imagery">imagery</span></span></h2>
<div class="outline-text-2" id="text-steam-and-footprints">
<p>
First, in Orin's introduction, steam is an almost gratuitous motif.  The general description of dampness and sweat coupled with repeated references to glass windows echoes the image of the fogged car window.  His coffee's steam thinning, a sweat-soaked mustache, the chugging of a Jacuzzi, shimmering heat, mirages, a perfume spritzer, etc&#x2026; all contribute to the general atmosphere of humidity seen in the car (pg. 43).  Orin's technique of killing cockroaches involves inverted glass tumblers, which "gradually steam up with roach-dioxide.  The whole thing makes Orin sick" (pg. 45).  The image of steam is enhanced by the description of extremely hot water which immediately follows (pg. 45).  The section closes with a poignant image: Orin shaving in the shower "wreathed in steam, by feel, shaving upward, with south-to-north strokes, as he was taught" (pg. 49).  The closing image also nods implicitly at Himself, the teacher of against-grain shaving.  The atmosphere of steam likely brings Orin back to that same atmosphere in the car with Avril, and conjures a misplaced sense of guilt towards his father.
</p>

<p>
Second, when Orin is the subject in a technical interview, he is placed under a sort of glass tumbler, like the cockroaches he kills.  The glass tumbler itself echoes the car: "steam on the sides," and, in possibly this essay's most robust connection to the car scene, smeared footprints.  "The glass was too thick to break or to kick his way out, and it felt like he might have possibly broken the leg's foot already trying&#x2026; There were now smeared footprints on the glass" (pg. 972).  These footprints on the glass walls of the tumbler parallel strikingly, in both content and diction, with the footprints Orin "alleged" earlier in the novel.
</p>

<p>
A skeptical reader will note that steam, glass, and enclosure are motifs <i>Infinite Jest</i> uses broadly.  They saturate the Phoenix condo, fill Ennet House, and recur across the Quebec arc.  The claim here is not that the imagery is unique to Orin but that its specific conjunction in his introduction, with fog plus footprints plus glass plus the mother-as-Jacuzzi-chugger and father-as-teacher-of-shaving pairing, recapitulates the car scene with a density that reads as patterning rather than accident.
</p>
</div>
</div>
<div id="outline-container-other-evidence-for-the-affair" class="outline-2">
<h2 id="other-evidence-for-the-affair"><span class="section-number-2">5.</span> Other Evidence for the Affair&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="avril">avril</span></span></h2>
<div class="outline-text-2" id="text-other-evidence-for-the-affair">
<p>
Other evidence supports the incest hypothesis.  Pemulis walks in on John Wayne and Avril mid-encounter in Charles Tavis's office:
</p>

<blockquote>
<p>
John Wayne wore a football helmet and light shoulderpads and a Russell athletic supporter and socks and shoes and nothing else.  He was down in the classic three-point stance of U.S. football.  Inc's incredibly tall and well-preserved mother Dr. Avril Incandenza wore a little green-and-white cheerleader's outfit and had one of deLint's big brass whistles hanging around her neck (pg. 553).
</p>
</blockquote>

<p>
The scene demonstrates that Avril has engaged in sexual contact with a teenage student at the academy she co-founded.  It also, once the staging is on the page, reads as a fantasy re-enactment in which Wayne plays Orin (a football player) and Avril plays Joelle (Orin's former cheerleader girlfriend).  The football helmet, the brass whistle, the green-and-white outfit, the paper pompoms on the seminar table are too specific a constellation to be coincidence.
</p>

<p>
The incestuous connection is encouraged by the narrative.  Immediately before the scene Pemulis walks in on, Dolores Rusk is in her office down the hall delivering an extended psychoanalytic monologue to Ortho Stice that turns out, for several pages, to be a gloss on the Oedipus complex: "GI Joe typically being cathected as an image of the potent but antagonistic father&#x2026; the Oedipal phase's desire to control the bowels in order to impress or quote 'win' the mother, of whom the Barbie might be seen as the most obviously reductive and phallocentric reduction of the mother to an archetype of sexual function and availability" (pg. 550).  Wallace has placed his most explicit Oedipal exegesis a few feet from the room where Avril and Wayne are role-playing it.
</p>

<p>
Wayne's exact age during the YDAU narrative-present is one of the novel's deliberate ambiguities.  The text places him at 17 or just-18 &#x2013; he was "the top-ranked junior male in Canada at sixteen" the year before, and the recruitment plan has him going pro "at nineteen" (pg. 259) &#x2013; and the novel never pins the date down.  That ambiguity is itself part of <i>Infinite Jest</i>'s broader practice: the text leaves Joelle's disfigurement deliberately ambiguous, leaves the Volvo partner unnamed, leaves Wayne's age on the cusp.  If Wayne is 17 the Avril-Wayne affair is statutory rape; if 18, it is institutional grooming by a co-founder of the academy he is enrolled at.  Both are harsh indictments of Avril's sexual irregularity.  The novel withholds the difference, and the withholding does literary work: what it leaves visible is that Wayne is a teenager under Avril's authority, sexually entangled with her, his relation to consent unresolved.  On this reading, Wayne in the Wayne-Avril incident is not the documented adult counterpart to a hidden Orin abuse, but instead is a parallel case.  Wayne and Orin are two instances of the same predation, treated by the novel with the same studied ambiguity that keeps either from being unmistakably named.
</p>

<p>
Avril's note to Orin offers another piece of evidence, which contains obviously sexual diction: "Every floral unit on the grounds has its pistil aprick and petals atremble in a truly shameless fashion, for the bees are about."  She closes the letter with the phrase "Proud, as ever, to know you." (n. 110 pg. 1006).  A militant grammarian, Avril chooses her phrasing with care, and as a result, the second (sexual) entendre is difficult to read as accidental.  Marlon Bain, in a correspondence with Hugh Steeply, revealed that Avril, towards her children, was "the most consummate mind-fucker" (n. 269 pg. 1048).  The phrase is Bain's characterization of Avril's psychological dominion over her children, which is not, in itself, evidence of sexual abuse, but it does corroborate the broader pattern of maternal enmeshment on which this reading rests.
</p>

<p>
Finally, after Himself sees the name written in the window, presumably 'Orin' or simply 'O', he immediately films the scene in the Entertainment which depicts a mother emphatically apologizing to her son (n. 80 pg. 999).  Orin's habit of leaving signature marks on partners makes him a more natural candidate for the name-writer than a hypothetical third party.  One Subject is characterized in passing as "Not real bright &#x2013; she thought the figure he'd trace without thinking on her bare flank after sex was the numeral 8, to give you an idea" (n. 110 pg. 1015).  The trait characterological: Orin signs his work.
</p>
</div>
</div>
<div id="outline-container-os-ds-and-ps" class="outline-2">
<h2 id="os-ds-and-ps"><span class="section-number-2">6.</span> O's, D's, and P's: The Oedipus Encoding&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="oedipus">oedipus</span></span></h2>
<div class="outline-text-2" id="text-os-ds-and-ps">
<p>
The most direct textual figure for what happened to Orin appears in a description of his nightmares.  In a paragraph which opens with the phrase "As a nod to Orin's own unhappy youth&#x2026;", a dream is described in which "&#x2026;Mrs. Avril M. T. Incandenza's, the Moms's disconnected head attached face-to-face to his own fine head, strapped tight to his face somehow by a wrap-around system of VS HiPro top-shelf lamb-gut string from his Academy racquet's own face.  So that no matter how frantically Orin tries to move his head or shake it side to side or twist up his face or roll his eyes he's still staring at, into, and somehow through his mother's face.  As if the Moms's head was some sort of overtight helmet Orin can't wrestle his way out of" (pg. 46-47).  The face-to-face contact is unmistakably sexual in charge.
</p>

<p>
A few pages earlier in the same chapter, the narration had turned to the Subject's note, on which Orin believed "The only interesting thing about the script, but also depressing, is that every single circle &#x2013; o's, d's, p's, the #s 6 and 8 &#x2013; is darkened in, while the i's are dotted not with circles but with tiny little Valentine hearts, which are not darkened in" (pg. 43).  The note, in the particular circles Orin fixates on, suggests the name "Oedipus" not as something the Subject has encoded, but as something Orin is projecting onto a neutral graphic.  The morphological hook is real: O, D, and P are the circle-bearing letters of the Oedipal name, and they are precisely the letter-circles Orin's eye lands on.  The darkening also includes the numerals 6 and 8, which belong to no word at all, meaning the pattern cannot be the Subject's coded spelling.  It is Orin's pattern-matching, his repressed memory surfacing through a visual trigger.  
</p>

<p>
A more parsimonious reading is available: the Subject simply darkens all closed glyphs as a handwriting tic, with no projection required.  The projective reading depends on those numerals doing semantic work no tic explains, an argument the essay develops below for the 8 in particular.  The Valentine hearts dotting the i's (not darkened in) compound the creepiness of the near-spelling: the hearts occupy the "i" positions of the word, so that a post-coital note from a maternal-figure Subject comes to Orin as "Oedipus" rendered with a heart over the self-referential "I" character.  The geometry is more precise than that.  "Oedipus" has seven letters, and the single <i>i</i> sits at position four, the exact center of the word.  The valentine heart that replaces the i's dot lands at the very middle of the name Orin is reading.  This is <i>GEB</i>'s foundational move: typographic position doing semantic work, the way Gödel-numbering makes a symbol's place in a string inseparable from its meaning.  The sign of romance sits at the center of the Oedipal word, just as Orin's trauma sits at the center of his romantic engagements.  This is "depressing" because Orin understands, at whatever level of awareness, that he and the Greek mythological figure Oedipus have the same secret.
</p>

<p>
A reader might reasonably object at this point that the essay is forcing a physical reading onto a pathology the novel establishes as emotional.  <i>Infinite Jest</i> characterizes Avril as controlling, enmeshing, "consummately" manipulative.  Every behavior the essay has cited (the smothering, the helmet dream, the Feeding My Man passage, the bees letter) could be read as severe psychological enmeshment without any physical component.  The objection is legitimate.  The answer here is not that physical abuse is proven but that it is the reading which accounts for the specific shape of the evidence: the fogged-window / nude-feet parallel is a physical detail belonging to a physical event; the "Do it to her!" collapse at the technical interview is a breaking-under-torture response that protects a specific embodied memory rather than a generalized emotional wound; and Orin's repeated evasion about who was in the Volvo points at a particular person rather than an abstract pattern of maternal damage.  Emotional enmeshment is a necessary condition for this reading, not a competing one.  The essay argues that the enmeshment has a physical dimension the novel suggests rather than confirms.
</p>
</div>
</div>
<div id="outline-container-godel-escher-bach-wallace" class="outline-2">
<h2 id="godel-escher-bach-wallace"><span class="section-number-2">7.</span> Gödel, Escher, Bach, Wallace&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="hofstadter">hofstadter</span></span></h2>
<div class="outline-text-2" id="text-godel-escher-bach-wallace">
<p>
The o/d/p reading looks, on its own, like a particular kind of writerly trick: a word hidden in the morphology of other letters, a signified laundered through the shape of the signifier.  That trick is not idiosyncratic to Wallace.  It belongs to a specific intellectual inheritance, one Wallace took up from Douglas Hofstadter's 1979 <i>Gödel, Escher, Bach: An Eternal Golden Braid</i> (<i>GEB</i>), a book he read with enough zeal that, per his biographer D. T. Max, he "actually shoved this book excitedly at people in the eighties."
</p>

<p>
Wallace was on record about <i>GEB</i> in his own voice.  In a 2003 interview with Dave Eggers for <i>The Believer</i>, he called it "a great book, but it's hard," and added that "I personally don't think Hofstadter does enough teaching of the basic concepts to make his riffs and dialogues come alive for people who didn't have a lot of basic logic and recursion-theory in college" (qtd. in Burn).  He was treating <i>GEB</i> as a peer text &#x2013; engaged enough with its argument to fault its pedagogy, fluent enough in its idiom to use "recursion-theory" as a term of art.  The same year, he published <i>Everything and More: A Compact History of Infinity</i>, his pop-math book on Cantor's set theory; and Atlas Books had originally commissioned Wallace to write the volume on Gödel and the Incompleteness Theorems before the topic was reassigned.  The publisher, in other words, saw Wallace as the natural author to popularize Gödel, which was exactly the territory <i>GEB</i> had defined a generation earlier.
</p>

<p>
The provenance stretches across two decades of Wallace's career.  D. T. Max's authorized biography records that Wallace borrowed his father's copy of <i>GEB</i>; that Mark Costello, Wallace's Amherst roommate, remembered Wallace while composing <i>Infinite Jest</i> "going on about the 'braid' or 'fugue' shape &#x2013; disparate elements making a whole"; and that Max himself treats GEB as "a predecessor to <i>Infinite Jest</i>, at least structurally" (Max 312 n. 6).  In his 1993 interview with Larry McCaffery, Wallace was already using "recursive mechanism" as his master-term for evaluating self-referential fiction, three years before <i>Infinite Jest</i>'s publication (McCaffery, <i>Review of Contemporary Fiction</i> 13.2).  Three years later, in a 1996 conversation with Michael Silverblatt on KCRW's <i>Bookworm</i>, he confirmed the Sierpinski gasket (a self-similar fractal) as <i>Infinite Jest</i>'s structural model.  By 2003, his vocabulary had hardened: in a <i>Boston Globe</i> interview with Caleb Crain, he called Gödel "the devil, for math," described "Cantor's paradox" as the moment that "starts the wheel of self-reference," and tied the character of Pemulis to Gödelian incompleteness as one of the novel's "Antichrists" (Crain 2003).  And in his 2004 conversation with Steve Paulson, he looked back on the philosophical terrain of his graduate years as "full of recursion, and involution, and things bending back on themselves, and various incarnations of Gödel's proof, and I think some of that kind of affected me at a spinal level" (qtd. in Burn 133).
</p>

<p>
The structural connection has support beyond the biographical record.  N. Katherine Hayles, in "The Illusion of Autonomy and the Fact of Recursivity" (<i>New Literary History</i> 30.3, 1999), reads <i>Infinite Jest</i> as governed by recursive feedback loops in which entertainment systems entrap characters who cannot achieve autonomy precisely because they are inside the loop.  Roberto Natalini's chapter "David Foster Wallace and the Mathematics of Infinity" in <i>A Companion to David Foster Wallace Studies</i> (eds. Boswell and Burn, Palgrave 2013) argues that Wallace drew on Cantorian infinity and Gödelian self-reference as structural and thematic resources throughout his career.  Ryan David Mullins, in "Theories of Everything and More: Infinity Is Not the End" (<i>Gesturing Toward Reality</i>, eds. Bolger and Korb, Bloomsbury 2014), develops a "bad infinity / good infinity" reading explicitly indebted to Hofstadter's distinction between vicious regress and productive recursion.  Most directly: Ugo Panzani, writing in <i>Lettera Matematica International</i> (Springer 2015), observes that the recursive iteration in <i>Infinite Jest</i> "proceeds to the level of typography", which is the precise level at which the o/d/p reading operates.  Cory M. Hudson's 2015 MTSU thesis <i>Gödel, Hofstadter, Wallace: The Gödelian Metalogical Narrative Structure of David Foster Wallace's Infinite Jest</i> then develops the structural isomorphism in detail (Hudson 56-86).  The braid, the fugue, and the strange loop are <i>GEB</i>'s operative figures, and they become Wallace's formal vocabulary.
</p>

<p>
But <i>GEB</i> is not only a book about structural recursion; it is, persistently, a book about typography.  Hofstadter's dialogues between Achilles and the Tortoise are riddled with letters encoding other letters, words hidden inside numerals, ambigrams, and Gödel-numbering (the technique by which any written statement can be translated into a single integer, so that a formal system can refer to itself through a different surface grammar).  The Subject's note in <i>Infinite Jest</i> works in exactly that register.  Orin does not <i>read</i> "Oedipus" on the page; he <i>projects</i> it, out of the morphology of circle-letters the Subject wrote for other reasons entirely.  That is a Hofstadterian move: the move of finding a meaningful string inside an indifferent one, the move of self-reference laundered through surface innocuousness.  When Orin registers the note as "depressing," he is doing the reader's work for us: he is experiencing, as a character, the kind of strange loop <i>GEB</i> taught Wallace to construct.
</p>


<div id="org038069c" class="figure">
<p><img src="https://www.chiply.dev/images/geb-trip-let.jpeg" alt="geb-trip-let.jpeg" />
</p>
<p><span class="figure-number">Figure 2: </span>Hofstadter's "trip-let" construction from <i>Gödel, Escher, Bach</i> (1979) &#x2013; a single wooden form whose shadows spell G, E, and B when lit from three orthogonal directions.  The wood itself is neutral; the letters live in the projection.  Orin reads "Oedipus" onto the Subject's circle-letters by the same light: the morphology on the page is indifferent, and the meaning is supplied by the angle from which Orin is looking.</p>
</div>

<p>
What the record does not yield is direct evidence that Wallace and Hofstadter ever met or corresponded; Hofstadter, for his part, has never publicly commented on Wallace's work.  Wallace's father, James Donald Wallace, was a virtue ethicist (Cornell PhD under Norman Malcolm &#x2013; a prominent Wittgenstein pupil), not a formal-systems philosopher; the household intellectual lineage ran Wittgenstein-via-Malcolm rather than through Hofstadter directly.  <i>GEB</i> reached Wallace as a pop-sci stimulus, sitting beside but not displacing the formal-philosophy training he had through his Amherst thesis on modal logic.  Nor has any prior scholarship identified the o/d/p encoding specifically.  The argument here is original, but grounded in well-prepared soil.
</p>
</div>
</div>
<div id="outline-container-letters-inside-letters" class="outline-2">
<h2 id="letters-inside-letters"><span class="section-number-2">8.</span> Letters Inside Letters&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="hofstadter">hofstadter</span>&#xa0;<span class="typography">typography</span></span></h2>
<div class="outline-text-2" id="text-letters-inside-letters">
<p>
None of this would matter if the o/d/p encoding were the only such moment in <i>Infinite Jest</i>.  It isn't.  Wallace is a typographic novelist in the strictest sense: across his thousand pages he encodes meaning in the morphology of letters, in acronyms that spell hidden words, in anagrammed proper names, in phonetic decomposition, and in self-reflexive footnote architecture.  The Subject's note is one instance in a consistent practice.
</p>

<ul class="org-ul">
<li>Consider James Incandenza's filmography, listed in n. 24.  Wallace gives one of the films the title <i>Möbiu$ Strips</i>, substituting a dollar sign for the <i>s</i>, a single glyph that visually resembles the letter while importing a second register (commerce, pornography) into a topological loop.  The technique is the same as the one on the Subject's note: a character whose shape is doing typographic work beyond the level of legibility.</li>
<li>Or consider "O.N.A.N." &#x2013; the Organization of North American Nations, the supranational body whose acronym, deployed relentlessly throughout the novel, spells "Onanism" (the biblical figure for spilled seed and the term for self-gratification).  The reader is being asked to assemble a hidden word from morphological units, which is the o/d/p mechanism applied at the scale of a nation-name.</li>
<li>Or "Pemulis," the drug-dealing E.T.A. student whose name is a perfect anagram of <i>IMPULSE</i> &#x2013; letters rearranged to encode the character's pathology inside his own name.</li>
<li>Or a credited filmography character named "Hugh G. Rection" &#x2013; a phonetic decomposition that hides "huge erection" inside an innocuous-looking proper name.</li>
</ul>

<p>
Each of these moves is the o/d/p move, played in a different register.
</p>

<p>
The practice scales from individual words to the architecture of the text.  <i>Infinite Jest</i>, the novel &#x2013; contains a film called <i>Infinite Jest</i>, the lethal Entertainment whose existence drives the plot (listed in James Incandenza's filmography as versions I-VI, n. 24); the book is named after the artifact at the center of its own world, a structural strange loop in Hofstadter's exact sense.  
</p>

<p>
Pemulis's answering-machine message extends the move into prose: "This is Mike Pemulis's answering machine's answering machine; Mike Pemulis's answering machine regrets being unavailable to take a first-order message&#x2026; if you'll leave a second-order message&#x2026;".  This is <i>GEB</i>'s technical vocabulary deployed inside a character's voicemail.  An internal footnote in n. 304 reads "Q.v. Note 304 sub", which is a footnote pointing back at its own note, a bibliographic strange loop.  Another Incandenza film is titled <i>The Machine in the Ghost</i>, inverting the Cartesian/Rylean "ghost in the machine"; the narration later quotes its own inversion verbatim ("A machine in the ghost, to quote a phrase"), so that the filmography and the body text cite each other across formal levels.
</p>

<p>
At the level of proper names, the practice continues.  James O. Incandenza's initials spell J.O.I. (joy), the very feeling his lethal Entertainment was intended to deliver and instead poisons.  "Orin" is an anagram of "NOIR," and the chapters Orin appears in are the novel's most paranoid, surveillance-soaked, genre-marked sections.  Other instances are scattered throughout: Hugh Steeply's drag persona "Helen Steeply" encodes the "Paris and Helen" mythology the Marathe-Steeply dialogues explicitly invoke; Madame Psychosis's call sign "WYYY-109" is named for the fact that 109 is "the largest whole prime on the FM band" (the triple Y also reading as a question &#x2013; <i>why? why? why?</i>); James Incandenza's production company "Latrodectus Mactans Productions" takes its name from the Latin for the black widow spider, naming his marriage's fate.  In each case, <i>Infinite Jest</i> is doing what the Subject's note does: hiding a word inside a word, or a word inside a structure, asking the reader to recognize the embedded meaning by stepping back from the surface text.  The Oedipal reading is one instance of a continuous authorial practice.
</p>
</div>
</div>
<div id="outline-container-orin-in-translation" class="outline-2">
<h2 id="orin-in-translation"><span class="section-number-2">9.</span> Orin in Translation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="hofstadter">hofstadter</span>&#xa0;<span class="typography">typography</span></span></h2>
<div class="outline-text-2" id="text-orin-in-translation">
<p>
Orin's name itself does similar work, at the level of translation.  The strongest reading is Hebrew: <i>Oren</i> (אֹרֶן) means "pine tree," and appears in Isaiah 44:14 in one of the prophet's central attacks on idolatry.  The passage describes a man who plants a tree, waters it, and from the same tree carves both fuel for his fire and an idol he proceeds to worship.  The prophet's argument is that idolatry consists in worshipping what one has oneself constructed.  That is the precise architecture of Orin's life.  The football helmet, the amniotic stadium and the Subject-protocol are all built from his own trauma and then submitted to as if external.  The Hebrew root names the pattern.
</p>

<p>
A second derivation, the Irish <i>Odhrán</i>, may supplement the reading.  The name belonged to a 6th-century Irish saint, one of Columba's companions at Iona; an apocryphal but persistent strand of his hagiography holds that he was buried alive at the abbey's foundation to consecrate the ground.  Whether Wallace had this particular legend in mind is unknowable, but the loose resonance is hard to miss.  The Irish derivation, even taken at the level of legend, places a buried boy at the foundation of an institution, which is exactly what the essay has been arguing the Incandenza family is: an institution erected over the buried fact of what was done to its eldest son.
</p>

<p>
<i>GEB</i> devotes substantial attention to translation across languages, formal systems, and levels of representation.  Hofstadter's recurring argument is that meaning lives at the level where these translations preserve structure, not at the level of the surface symbols.  The etymology of "Orin" is exactly that kind of cross-system encoding: a name innocuous in English unfolds, under translation, into a thematic key, which visible to the reader who knows the language, and hidden to the one who does not.  Wallace, characteristically, has placed the meaning where it can be missed.
</p>
</div>
</div>
<div id="outline-container-eight-sideways" class="outline-2">
<h2 id="eight-sideways"><span class="section-number-2">10.</span> Eight, Sideways&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="hofstadter">hofstadter</span>&#xa0;<span class="typography">typography</span></span></h2>
<div class="outline-text-2" id="text-eight-sideways">
<p>
There is one more typographic observation worth pausing on, and it returns the essay to the same passage we began with.  The Subject's note has its darkened circles &#x2013; "o's, d's, p's, the #s 6 and 8" &#x2013; and the essay has so far read those as the morphology of "Oedipus."  But the 8 is doing additional work the other glyphs are not.  Rotated ninety degrees, "8" becomes "∞."  It is the only glyph in the darkened set that is also an ambigram of the symbol for infinity.
</p>

<p>
The Subject's note, in other words, is doing the same kind of encoding work twice.  The letter-circles spell Oedipus.  The numerical circle, rotated, spells infinity.  Together, the note encodes the Oedipal trauma in its recursive form: not only Orin's mythological status, but the strange-loop structure that keeps him traversing it.  <i>GEB</i> is the founding text on strange loops (closed curves that cross themselves, traversed forever, never escaping).  <i>∞</i> is the strange loop drawn and 8 is the strange loop hidden inside an innocuous numeral.
</p>

<p>
The same figure shows up on Orin's body.  His specific signature mark, "the figure he'd trace without thinking on her bare flank after sex was the numeral 8" (n. 110 pg. 1015), is, in this light, the symbol for infinity inscribed on a Subject's body.  Each new Subject receives the shape of recursion.  Orin's pursuit of married mothers is, the essay has argued, a recursive trauma, a return to the original maternal scene that can never quite arrive there.  The 8 is what that pursuit looks like, drawn idly, on skin, after sex.  Wallace's own <i>Everything and More: A Compact History of Infinity</i> (2003) is his sustained engagement with Cantor and Gödel, the mathematics of recursive structures ascending into hierarchies of infinity without ever closing.  When Orin traces 8 on a Subject's flank, he is drawing a structure Wallace had spent two years writing a book about.  Wallace signs the page, Orin signs the body, and the signature in both cases is the same: the strange-loop figure traced on a stand-in for the original.
</p>
</div>
</div>
<div id="outline-container-recursive-anxiety" class="outline-2">
<h2 id="recursive-anxiety"><span class="section-number-2">11.</span> Recursive Anxiety and Redirected Hostility&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="psychopathology">psychopathology</span></span></h2>
<div class="outline-text-2" id="text-recursive-anxiety">
<p>
The event of Orin's sexual abuse recasts his villainy in an intuitive way: Orin is a victim of a traumatic childhood experience, and his current and past actions are products of the damage that experience instilled.  The humid atmosphere of his introduction instills dread.
</p>

<blockquote>
<p>
For Orin Incandenza, #71, morning is the soul's night.  The day's worst time, psychically.  He cranks the condo's AC way down at night and still most mornings wakes up soaked, fetally curled, entombed in that kind of psychic darkness where you're dreading whatever you think of (pg. 42).
</p>
</blockquote>

<p>
"Dread" and "entombment" here describe Orin's psychological reaction to trauma.  The fact that he "dread[s] whatever [he] think[s] of" speaks to his reasons for repressing, on some level, the entire car incident (this repression is seen when Orin is "alleg[ing]" the "smeared prints of nude human feet" distantly, in the 3rd person).  The damage from his childhood abuse manifests as anxiety about being entrapped, under control, suffocated (it is no accident that Avril herself defensively self-describes as "un-smothering").  This is perhaps why Orin gravitates towards football, a sport in which he can escape "&#x2026; as he'd never escaped himself on the court&#x2026;".  Football gives Orin "&#x2026; a sense of a presence in the sky&#x2026;" (pg. 295-296).  Football, "the sound of the womb, the roar gathering, tidal, amniotic&#x2026;", offers him a rebirth (pg. 295).
</p>

<p>
The anxiety dominates him.  The morning passages give the imagery directly: "It's the mornings after the spider-and-heights dreams that are the most painful, that it takes sometimes three coffees and two showers and sometimes a run to loosen the grip on his soul's throat" (pg. 46).  The cockroaches in the humid shower symbolize his rekindled entrapment anxiety.  To subvert these feelings, he attempts to nullify them by exerting some cruel, god-like, torturous control over them.
</p>

<p>
What makes the anxiety <i>recursive</i> &#x2013; in the precise Hofstadterian sense Wallace was already using by 1993, when he called metafiction a "recursive mechanism" that "spirals in on itself" (McCaffery, <i>Review of Contemporary Fiction</i> 13.2) &#x2013; is that <b>every</b> defense Orin builds against it reproduces the conditions of the original trauma.  The avoidance-behavior is itself the trigger.  The cockroach scene traces the loop step by step.  He tries to suppress the cockroaches (the memory) by running the shower water hot.  But, alas, hot water generates steam, and steam is the texture of the fogged-up car window where the abuse occurred.  He tries to trap the cockroaches under glass tumblers, but the trapped insects produce "roach-dioxide" that fogs the tumbler walls.  This is the same fogged-glass signature.  Every escape route returns him to the trauma.  "The whole thing makes Orin sick" (pg. 45) is not surprise; it is the recognition that the trap is closed.
</p>

<p>
The same loop runs through Orin's fear of heights.  Every elevation he achieves; whether in career, public visibility, or his punter's contract with Arizona; raises the height from which he will eventually tumble into the morning dread.  Climbing is escape, but with Shakespearian tragicness, the higher he climbs, the more terrifying is his descent.
</p>

<blockquote>
<p>
These worst mornings with cold floors and hot windows and merciless light &#x2013; the soul's certainty that the day will have to be not traversed, but sort of climbed, vertically, and then that going to sleep at the end of it will be like falling, again, off something tall and sheer (pg. 46).
</p>
</blockquote>

<p>
Orin also fights his anxiety more directly, whether consciously or not.  His abuse of Mario is probably an arrow thrown obliquely towards his abuser, Avril, given how close Mario and Avril are; and then the S. Johnson incident was probably driven by that same logic, given how close Avril was to the dog.  Presumably, the reason Orin resented Avril for forgiving the S. Johnson incident (pg. 1014) was that he had hoped it would crush her.  Here, Avril reveals how deftly she 'fucks the mind'.
</p>

<p>
Orin's destructive relationship with the married-mother Subjects is another example of his attempt to control what is controlling him.  That is, he targets maternal figures in an attempt to silence the control his own mother exerted over him.  His callousness towards them (his desire that they be gone by the morning) is issued because they remind him directly of Avril: they ask "what exactly is the story with the foggy inverted tumblers on the bathroom floor, commenting on his night-sweats&#x2026; the ones who have this thing about they call it Feeding My Man&#x2026;" (pg. 46).  This reminiscence of Avril is likely the reason Orin deludedly sees things like "Oedipus" ("o's, d's, p's") encoded into his Subject's note.
</p>

<p>
Perhaps Orin's biggest crime was the retrieval and dispersal of the Entertainment to his mother's previous sexual partners and his ensuing grant of the master copy to the AFR.  Orin didn't do this to avenge his father's suicide; Jim and Avril "hadn't been intimate with each other, i.e. conjugally, for quite some time" (n. 80 pg. 999), so an affair wouldn't have driven Jim to kill himself.  Rather, his targeting of his mother's previous sexual partners was probably an attempt to seek retribution for his own imagined complicity in what his mother had done to him.  This reading departs from Swartz's, which posits the father as the target of Orin's revenge, but it better accounts for the specificity of the recipients: not Avril's enemies or Himself's rivals generally, but the men Avril slept with.  Although this was reckless &#x2013; catastrophically, if not apocalyptically, reckless, given the Entertainment's nuclear properties &#x2013; it is psychologically intelligible from someone plagued by an intense and recursive anxiety whose source (the abuse by his mother) is the very thing at which the recklessness was aimed.  Intelligibility is not absolution; the people the Entertainment was meant to reach were endangered regardless of why.
</p>
</div>
</div>
<div id="outline-container-the-helmet" class="outline-2">
<h2 id="the-helmet"><span class="section-number-2">12.</span> The Helmet&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="psychopathology">psychopathology</span></span></h2>
<div class="outline-text-2" id="text-the-helmet">
<p>
There is a puzzle in Orin's psyche the essay should pause on.  Orin's central trauma (the one his nightmares stage and his syntax disowns) is the experience of being enclosed by his mother.  The helmet dream is the explicit version: "Mrs. Avril M. T. Incandenza's, the Moms's disconnected head attached face-to-face to his own fine head, strapped tight to his face&#x2026; As if the Moms's head was some sort of overtight helmet Orin can't wrestle his way out of" (pg. 46-47).  And yet Orin, every game, voluntarily straps on a football helmet.  The trauma's exact structure, a tight enclosure of his own head, is the structure of his profession.
</p>

<p>
The puzzle resolves itself when you consider who is doing the strapping.  The Avril-helmet of the dream is something done to him: he cannot wrestle his way out, the verb <i>attached</i> is passive, and the leather string around his face is the work of an external agent.  The football helmet is the opposite kind of object: he puts it on, by his own hand, before each game, and removes it when he chooses.  Counterphobia, in the vocabulary Dolores Rusk uses with Ortho Stice, is the defense by which one reproduces the structure of the feared thing under conditions of personal control, and then repeating the trauma to master it.  Orin's helmet is counterphobic in exactly that sense because he has built a career out of the gesture of voluntarily closing the maternal enclosure around his head, and unbuckling it when he is done.
</p>

<p>
The football helmet sits, moreover, inside a larger chosen womb.  The essay has already cited DFW's word for the stadium: "the sound of the womb, the roar gathering, tidal, amniotic" (pg. 295).  Football is a controlled re-entry into the maternal space.  Helmet, stadium, crowd, and team are nested enclosures, each of which is chosen, and each of which is protective.  Orin has constructed a counter-mother out of professional infrastructure.  The original mother smothered him; the new one is amniotic.
</p>

<p>
Then comes the technical interview.  The AFR's glass tumbler is the same shape as the helmet, a tight enclosure of Orin's head and body.  But it is unchosen, he cannot remove it, and he can break neither it nor the "leg's foot" trying.  What is devastating about the Room 101 (read below) scene is not only that Orin breaks; it is that his entire defensive architecture has been reverse-engineered.  The novel has been showing us this counterphobic apparatus all along (the helmet, the stadium, the controlled descent into womb-shape) and the AFR's interrogation chamber is the moment when the apparatus is turned back on him.  The helmet that protected him for years is welded shut, and he is inside it, terrified to the point that he is willing to get relief by any means necessary, even if that means the apocalypse.
</p>
</div>
</div>
<div id="outline-container-the-second-event" class="outline-2">
<h2 id="the-second-event"><span class="section-number-2">13.</span> The Second Event: Room 101&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="orwell">orwell</span></span></h2>
<div class="outline-text-2" id="text-the-second-event">
<p>
Relinquishing the master copy to the AFR, which the reader is led to believe could result in the deaths of millions of innocent people, after a seemingly mild technical interview (see Luria P-&#x2014;'s eye rolling, pg. 972), seems weak of Orin.  However, this is the second event which shifts the perspective of Orin away from him as a villain.  A close look at Orin's final words in the novel, "Do it to her!  Do it to her!", reveals an echo of what Winston shouts in George Orwell's <i>Nineteen Eighty-Four</i> whilst being tortured in Room 101, the infamous torture chamber from which no one emerges unbroken.  This allusion suggests that Orin's interview was not mild at all but calculated; it tapped into his most visceral fears and anxieties.
</p>

<p>
Being entombed and suffocated in steamed glass brought back vivid memories of his abuse, and the flood of cockroaches signifies a total inundation of his mind with abuse-related anxiety.  Orin, like the people in Room 101, is helpless against the technical interviewer's technique.  The text marks the moment his repressed memory breaks through: "When Orin had tried to kick his way out was when he'd recognized that the Subject was looking at his eyes rather than into them as previously.  There were now smeared footprints on the glass" (pg. 972).  Orin realizes he has lost control of the Subject and that he has become the Subject.  The footprints on the glass follow immediately, marking the moment his childhood abuse surges back into conscious awareness.  Under such duress, Orin's giving in is psychologically intelligible &#x2013; though intelligibility is not the same as forgiveness.
</p>

<p>
Like Winston's betrayal of Julia in <i>Nineteen Eighty-Four</i>, his (presumed) betrayal of Joelle is the response of an ordinary person to extraordinary torture, not the act of a super-human evil.  That is an explanation, not an exoneration.  Joelle is endangered regardless of why he named her.  The parallel is not airtight because IJ never specifies who Orin names under torture, or who "her" refers to, but <i>Nineteen Eighty-Four</i>'s echo resonant: the man who spent the novel fleeing his mother's abuse breaks, at last, into betraying the one woman who had loved him outside that shadow.
</p>
</div>
</div>
<div id="outline-container-subject-not-object" class="outline-2">
<h2 id="subject-not-object"><span class="section-number-2">14.</span> Subject, Not Object&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="language">language</span></span></h2>
<div class="outline-text-2" id="text-subject-not-object">
<p>
DFW was hyper-aware of the difference a word makes.  He was trained in analytic philosophy, wrote an undergraduate thesis on modal logic, and taught prose style for a living.  His choice of words is deliberate.  So it is worth sitting with the word Orin uses for the married women he seduces: not <i>Object</i>, but <i>Subject</i>, always capitalized &#x2013; "last night's Subject," "the Subject's note," "the Subject," and, when he is on the phone giving step-by-step seduction protocols to a friend, "Picture this.  Obtain a ring.  As in a wedding band.  So you present yourself to the Subject as visibly married" (n. 110 pg. 1007).  The standard feminist critique of male sexual behavior is that men <i>objectify</i> women, that they flatten women into their use-value, evacuating their interiority.  Orin is not doing that.  He is doing something much more strange.
</p>

<p>
The word "Subject" carries several resonances, each of which inflects what he is doing in a different way.  
</p>

<ul class="org-ul">
<li>In research, "the Subject" is the person in an experiment, or someone whose behavior is studied under controlled conditions, and about whom data is gathered.</li>
<li>In grammar, "the Subject" is the agent of a sentence, the entity performing the action.  The Object is what is acted upon.</li>
<li>In philosophy (Descartes, Kant, Hegel, phenomenology), "the Subject" is the experiencing self, the locus of consciousness, the counterpart of the Object out there in the world.</li>
<li>In political theory, "subjects" are those under the authority of a sovereign, and are subordinates to a power.</li>
</ul>

<p>
DFW's capitalized "Subject" does all four jobs at once.
</p>

<ul class="org-ul">
<li>Grammatically, Orin makes the women agents, but agents of his own ongoing narrative.</li>
<li>Philosophically, he acknowledges their subjecthood, but drafts it into the service of his own.</li>
<li>Experimentally, he runs a long, iterated study, with each seduction being a fresh run of the same protocol (see the "Speedy Seduction Strategy" series, n. 110), with data gathered and discarded at dawn.</li>
<li>Politically, the women are subjects of his self-reproducing pathology &#x2013; subordinates to a sovereign whose only real project is working out the shape of his own trauma.</li>
</ul>

<p>
<i>Subjectification</i>, in Orin's hands, turns out to be a subtler and more invasive violation than objectification.  To objectify is to deny interiority.  To <i>subjectify</i>, in Orin's sense, is to borrow interiority, or to enlist the other person's subjecthood as a prop for your own.  The women are treated as full persons, but only insofar as being treated as full persons is useful to the work Orin's psyche is doing on itself.
</p>

<p>
The logic shows itself most clearly on the note.  The Subject's note is a real artifact of her subjecthood (her script, her violet bond, her Valentine hearts) and yet what Orin reads in it is not her at all.  He reads <i>Oedipus</i>.  He reads himself.  The Subject has been drafted, unknowingly, into the mirror he is using to see his own buried memory.  The "depressing" feeling he registers is not about her, but rather about what her handwriting has made him recognize in himself.  Her full subjecthood is exactly what makes the mirror work.  An Object could not reflect him back; only a Subject could.
</p>

<p>
Then, in the technical interview by the AFR, the direction reverses.  Orin, who has spent the novel using women as Subjects, becomes the Subject himself, in the capital-S, technical sense, interrogated behind glass.  "When Orin had tried to kick his way out was when he'd recognized that the Subject was looking at his eyes rather than into them as previously" (pg. 972).  The Subject on the other side of the glass is Luria P-&#x2014;, who was once his Subject in the sexual sense; now the surface between them has flipped, and Orin is the one being studied, squeezed for data, held under conditions he did not choose.  He has been the Subject the whole time, the subject of his own trauma working itself out through the bodies of other women.  The technical interview is merely the scene in which the novel makes the inversion explicit.  Every "Subject" Orin ever slept with was, in a sense, a surrogate Orin:  the self he could not look at directly, looked at through someone else.  The strange loop, in the Hofstadter sense, is that the grammar was right.  He was always the Subject of the sentence and the women were the Objects.  He just preferred to call them otherwise.
</p>
</div>
</div>
<div id="outline-container-the-legs-foot" class="outline-2">
<h2 id="the-legs-foot"><span class="section-number-2">15.</span> The Leg's Foot&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span>&#xa0;<span class="language">language</span></span></h2>
<div class="outline-text-2" id="text-the-legs-foot">
<p>
DFW's grammar of Orin's body is also marked, and the technical-interview passage is the place where the marking becomes most visible.  Look again at the sentence the essay has been quoting:
</p>

<blockquote>
<p>
The glass was too thick to break or to kick his way out, and it felt like he might have possibly broken the leg's foot already trying (pg. 972).
</p>
</blockquote>

<p>
Two things are doing trauma-vocabulary work in this sentence.  The first is the modal scaffolding:  <i>it felt like</i> plus <i>might have</i> plus <i>possibly</i> is a triple hedge that puts maximum distance between Orin's reporting consciousness and the embodied event.  He is not reporting that he hurt his foot, but rather is reporting that it-felt-like-may-have-possibly happened to a foot.  The second is the noun phrase "the leg's foot."  Not "his leg" or "his foot."  Not even "the leg" with "the foot" as a separate noun.  The leg owns the foot, and Orin owns nothing.  The chain of dispossession runs two removes deep because the leg is alienated from Orin, and the foot belongs to that already-alienated leg.
</p>

<p>
This is the language of dissociation.  Survivors of childhood sexual abuse frequently describe a trauma response in which the body is experienced as not-theirs, watched from outside, and owned by something other than the experiencing self.  Wallace renders the dissociation clinically, at the level of syntax.  There is no clean <i>I</i> connected to this body in this sentence at all.  The pronoun has retreated entirely.
</p>

<p>
The grammatical retreat is sharper given who Orin is.  Orin is a punter.  The leg the syntax disowns is the body part on which his entire adult identity is built, the instrument of his career, the locus of his "amniotic" rebirth fantasy in the football passages, and the means by which he had organized his post-trauma life around an escape from his early-morning dread.  To watch the novel disown that leg in this scene is to watch Orin lose access to the one part of himself he had organized everything around.
</p>

<p>
The same syntactic move runs through the morning chapters.  He describes "his soul's throat" being under grip (pg. 46).  This throat is not owned not by Orin but by his soul, with Orin reporting on the relationship as a third party.  <i>The soul's throat, the soul's certainty, the soul's night, the leg's foot</i>: in moments of acute pressure, the grammar reaches for an intermediate possessor (like a soul or a leg) to insert between Orin and the part of him that is suffering.  The intermediate noun is the grammatical residue of dissociation; it is the chain of inanimate relations that the body becomes when the self has fled it.
</p>

<p>
This is also, finally, a problem in the philosophy of mind that Wallace, via Hofstadter, was already preoccupied with.  <i>GEB</i> takes its energy from the question of how a self inhabits a physical substrate, or the ghost in the machine, in Ryle's coinage that Wallace inverted in <i>The Machine in the Ghost</i> on the Incandenza filmography.  The leg's foot is what the inversion looks like inside Orin's body: a chain of mechanical relations with no clean ghost left to own them.  By the technical interview, Orin's strategy of interposing intermediate nouns has run out of room.  There is no further owner.  He is the Subject at last, but he no longer owns the body the Subject is in.  The trauma is what made the leg into "the leg" to begin with.  Orin's body alienated itself a long time ago, in another room, with another woman who needed not to be there.  The technical interview only forces him to watch the alienation happen in real time.
</p>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">16.</span> Conclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="infiniteJest">infiniteJest</span>&#xa0;<span class="orin">orin</span></span></h2>
<div class="outline-text-2" id="text-conclusion">
<p>
Orin's deplorable behavior, on this reading, is the product of his traumatic past and its recursive anxiety.  Each of the cruelties the novel attributes to him admits an explanation that does not require inherent evil.  The cockroaches are a memory he can neither suppress nor face.  S. Johnson and Mario are plausibly indirect targets of hostility he cannot aim at Avril directly.  The married-mother Subjects are both a search for his mother and a revenge upon her, which is why he is cold to them at breakfast.  Leaving Joelle after her disfigurement was overdetermined &#x2013; superficially a vanity collapse, but plausibly compounded by the familial echoes her relationship with her father stirred up that day.  Dispersing the Entertainment targeted the men Avril slept with, not, as Swartz argues, an avenging of Jim.
</p>

<p>
Taken together, these behaviors describe a victim as well as an antagonist.  The thesis does not exonerate Orin &#x2013; he does real harm, and the harm is real.  It argues only that his role as the catalyst of the post-text conflict is the aftermath of childhood sexual abuse, not the expression of any super-human evil.
</p>

<p>
What makes the reading possible at all is the o/d/p trick, and what makes the trick legible is <i>GEB</i>.  Without Hofstadter, the darkened circles read as coincidence; with him, they read as Wallace doing exactly the kind of work <i>GEB</i> taught him &#x2013; meaning encoded in the morphology of marks rather than in their assigned content, recoverable only by a reader who knows where to look.  The Subject's note is the place where the novel marks its own page with the signature of an event it cannot otherwise put on it.  Every other piece of evidence the essay has assembled &#x2013; the Volvo, the Wayne parallel, the helmet dream, the leg's foot, the Room-101 betrayal &#x2013; is corroboration of what the o/d/p trick names directly.
</p>
</div>
</div>
<div id="outline-container-glossary" class="outline-2">
<h2 id="glossary"><span class="section-number-2">17.</span> Glossary&#xa0;&#xa0;&#xa0;<span class="tag"><span class="glossary">glossary</span></span></h2>
<div class="outline-text-2" id="text-glossary">
<dl class="org-dl">
<dt>Aaron Swartz</dt><dd>Programmer, activist, and early <i>Infinite Jest</i> critic.  His 2009 essay <i>What Happened in Infinite Jest?</i> proposes a reconstruction of the novel's post-text plot.  This essay's Event-Two reading explicitly departs from Swartz's father-as-revenge-target account.</dd>
<dt>AFR</dt><dd>Les Assassins des Fauteuils Rollents ("the Wheelchair Assassins"), the Québécois-separatist terrorist cell pursuing the Entertainment.</dd>
<dt>Avril</dt><dd>Orin's mother, formally Avril Incandenza (also called "the Moms" or "Mrs. M. T. Incandenza" in the novel).  Co-founder of E.T.A., Québécois Catholic immigrant, prescriptivist grammarian.  Confirmed in a sexual relationship with E.T.A.'s John Wayne &#x2013; a relationship established scholarship treats as the primary textual evidence for her sexual irregularity, beneath which this essay argues a deeper layer.</dd>
<dt>DFW</dt><dd>David Foster Wallace, the author of <i>Infinite Jest</i> and the subject of this essay.</dd>
<dt>Dolores Rusk</dt><dd>E.T.A.'s staff counselor and on-site psychologist.  Conducts therapy sessions in which she expounds, sometimes comically, on psychoanalytic theory; her discussion of the Oedipus complex with Ortho Stice immediately precedes the Pemulis/Avril/Wayne scene.</dd>
<dt>Entertainment</dt><dd>Shorthand in <i>Infinite Jest</i> for James Incandenza's final, fatally compelling film cartridge &#x2013; the film whose viewers cannot stop watching.</dd>
<dt>E.T.A.</dt><dd>Enfield Tennis Academy, the elite tennis school co-founded by Avril and James Incandenza, where most of the novel's E.T.A.-side action takes place.</dd>
<dt>GEB</dt><dd><i>Gödel, Escher, Bach: An Eternal Golden Braid</i>, Douglas Hofstadter's 1979 Pulitzer-winning book on self-reference, recursion, and strange loops &#x2013; a foundational influence on Wallace.</dd>
<dt>Himself</dt><dd>Family nickname for James O. Incandenza, Orin's father (also "Jim").  Optical physicist, experimental filmmaker, and founder of E.T.A.; creator of the Entertainment cartridge whose effect drives the novel's plot.  Kills himself by microwaving his own head before the main narrative opens; the Entertainment's master copy is subsequently retrieved from his skull.</dd>
<dt>Hofstadter</dt><dd>Douglas Hofstadter (b. 1945), American cognitive scientist and author of <i>Gödel, Escher, Bach</i> (1979).  His work on self-reference, recursion, and strange loops is the formal-system framework this essay reads Wallace against.</dd>
<dt>Hugh Steeply</dt><dd>Office of Unspecified Services operative investigating the Entertainment.  Typically works in (unconvincing) female disguise.  His correspondence with Marlon Bain supplies the Bain letters this essay quotes.</dd>
<dt>IJ</dt><dd><i>Infinite Jest</i>, David Foster Wallace's 1996 novel.</dd>
<dt>Joelle</dt><dd>Orin's girlfriend of twenty-six months, formally Joelle van Dyne; the so-called "Prettiest Girl Of All Time" (P.G.O.A.T.).  After Orin leaves her she becomes muse and lead for James Incandenza's final, lethal film.  Disfigured under circumstances the novel leaves deliberately ambiguous; later wears a veil and broadcasts as "Madame Psychosis" on MIT student radio.  This essay frames her as Orin's Julia in the Room-101 betrayal structure.</dd>
<dt>John Wayne</dt><dd>E.T.A.'s top-ranked male tennis player, formerly of Montcerf, Quebec; Avril's secret student lover.  In the YDAU narrative-present he is 17 or just-18 &#x2013; an age the novel leaves deliberately ambiguous.  This essay reads him as plausibly a parallel victim to Orin rather than Avril's adult co-conspirator.</dd>
<dt>Julia</dt><dd>Winston Smith's lover in <i>Nineteen Eighty-Four</i>, betrayed by him under Room-101 torture.  This essay casts Joelle as Julia's structural analog in Orin's collapse.</dd>
<dt>Luria P----</dt><dd>Member of the AFR (full name Luria Perec, of Lamartine, county L'Islet, Quebec).  Appears female-presenting as one of Orin's Subjects and then as co-operative at his technical interview; her eye-rolling at the AFR leader's theatrics is the gesture Orin misreads as the interview being "mild."</dd>
<dt>Mario</dt><dd>The middle Incandenza brother (Mario Incandenza), born with severe congenital disabilities and a documentary-filmmaker's temperament.  Avril's favorite &#x2013; which this essay argues is exactly why Orin's verbal cruelty toward him functions as displaced hostility toward their mother.</dd>
<dt>Marlon Bain</dt><dd>Orin's closest friend from the E.T.A. tennis years, later debilitated by a psychosomatic hyper-perspiration disorder.  His correspondence with Office of Unspecified Services agent Hugh Steeply is the source of the "most consummate mind-fucker" characterization of Avril and the claim that Orin "constructs rather than reports the truth."</dd>
<dt>Oedipus</dt><dd>Mythological king of Thebes who, fulfilling an oracle, unknowingly killed his father Laius and married his mother Jocasta; source of Freud's "Oedipus complex."  This essay argues Orin's fixation on the note's darkened circle-letters o/d/p reflects an inadvertent Oedipal self-recognition.</dd>
<dt>O.N.A.N.</dt><dd>Organization of North American Nations, the supranational confederation <i>Infinite Jest</i> invents.  The acronym, deployed relentlessly throughout the novel, spells "Onanism" &#x2013; one of Wallace's typographic encodings.</dd>
<dt>Orin</dt><dd>Orin Incandenza, the eldest of the three Incandenza brothers, a former E.T.A. tennis prodigy who switched to football at Boston University and became a punter (#71) for the Arizona Cardinals.  The subject of this essay.</dd>
<dt>Ortho Stice</dt><dd>("The Darkness") E.T.A. tennis player ranked #3, from rural Kansas, dressed exclusively in black.  Therapy-session interlocutor of Dolores Rusk.</dd>
<dt>Pemulis</dt><dd>Michael Pemulis, E.T.A. student, mathematical and navigational savant, and the Academy's de facto pharmacist.  Hal Incandenza's closest friend.  His accidental discovery of John Wayne and Avril mid-encounter in Charles Tavis's office is one of this essay's pieces of evidence.</dd>
<dt>P.G.O.A.T.</dt><dd>Prettiest Girl Of All Time, Joelle van Dyne's nickname.</dd>
<dt>S. Johnson</dt><dd>The Incandenza family dog, named after the English lexicographer Samuel Johnson.  Killed in an "accident" involving Orin; the circumstances surface in n. 269.  This essay reads Orin's indifference to the dog's death as an indirect attack on Avril.</dd>
<dt>Subjects</dt><dd>Orin's chosen term for the married women he seduces (always capitalized, "the Subject," "last night's Subject").  This essay reads the terminology as deliberately marked: not <i>objectifying</i> but <i>subjectifying</i> &#x2013; borrowing each woman's interiority as a prop for his own.</dd>
<dt>Winston</dt><dd>Winston Smith, protagonist of George Orwell's <i>Nineteen Eighty-Four</i> (1949).  Broken in Room 101 under the threat of having rats set on his face, he shouts "Do it to her!  Do it to her!" &#x2013; the same phrase Orin shouts at his technical interview, anchoring this essay's Event-Two allusion.</dd>
<dt>YDAU</dt><dd>Year of the Depend Adult Undergarment, the principal year of <i>Infinite Jest</i>'s narrative-present.  In the novel's near-future, calendar years are subsidized to corporate sponsors.</dd>
<dt>YTMP</dt><dd>Year of the Tucks Medicated Pad, one of the subsidized years preceding YDAU.</dd>
</dl>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>A VOMPECCC Case Study: Spotify as Pure ICR in Emacs</title>
      <link>https://www.chiply.dev/post-vompeccc-spot</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-vompeccc-spot</guid>
      <pubDate>Fri, 24 Apr 2026 04:26:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>This is the third post in a series on Emacs completion. The first post argued that Incremental Completing Read (ICR) is not merely a UI convenience but a structural property of an interface, and that ...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="completion">completion</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org11ebfbb" class="figure">
<p><img src="https://www.chiply.dev/images/vompeccc-spot-banner.jpeg" alt="vompeccc-spot-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/dall-e-3">DALL-E 3</a></p>
</div>

<p>
This is the third post in a series on Emacs completion.  The <a href="https://www.chiply.dev/post-icr-primer">first post</a> argued that <a href="https://www.chiply.dev/post-icr-primer#what-is-incremental-completing-read-">Incremental Completing Read</a> (ICR) is not merely a UI convenience but a <i>structural</i> property of an interface, and that Emacs is one of the few environments where completion is exposed as a programmable <a href="https://www.chiply.dev/post-icr-primer#why-icr-matters-more-in-emacs-than-anywhere-else">substrate</a> rather than a sealed UI.  The <a href="https://www.chiply.dev/post-vompeccc">second post</a> broke the substrate into eight packages (collectively <a href="https://www.chiply.dev/post-vompeccc#vompeccc-framework">VOMPECCC</a>), each solving one of the <a href="https://www.chiply.dev/post-vompeccc#hidden-complexity">six orthogonal concerns</a> of a complete completion system.
</p>

<p>
In this post, I show, concretely, what it looks like when you <i>build</i> with VOMPECCC, by walking through the code of <a href="https://github.com/chiply/spot"><code>spot</code></a>, a Spotify client I implemented as a pure ICR application in Emacs.
</p>

<p>
A word I'll use throughout this post to refer to the use of VOMPECCC in <code>spot</code> is <i>shim</i>, and it is worth qualifying that.  The whole package is about 1,100 non-blank, non-comment lines of Lisp<sup><a id="fnr.loc" class="footref" href="https://www.chiply.dev/#fn.loc" role="doc-backlink">1</a></sup>.  Roughly 635 of those is infrastructure any Spotify client would need regardless of its UI choices: OAuth with refresh, HTTP transport with error surfacing, a cached search layer, a currently-playing mode-line, a config surface, player-control commands, blah blah blah.  <i>The shim is the rest</i>: 493 lines across exactly three files (<code>spot-consult.el</code>, <code>spot-marginalia.el</code>, <code>spot-embark.el</code>) whose entire job is to feed candidates into <a href="https://www.chiply.dev/post-vompeccc#consult">Consult</a> (<a href="https://github.com/minad/consult">source</a>), annotate them with <a href="https://www.chiply.dev/post-vompeccc#marginalia">Marginalia</a> (<a href="https://github.com/minad/marginalia">source</a>), and attach actions to them through <a href="https://www.chiply.dev/post-vompeccc#embark">Embark</a> (<a href="https://github.com/oantolin/embark">source</a>).  When I say <code>spot</code> is a shim, I mean those three files, and I'm emphasizing the fact that there is relatively little code.  The rest of <code>spot</code> is plumbing that has nothing to do with the completion substrate.
</p>

<p>
<code>spot</code> implements no custom UI.  It has no tabulated-list buffer, no custom keymap for navigation, no rendering code.  Every interaction surface; the search prompt, the candidate display, the annotations, and the action menu; is rented from the completion substrate by the 493-line shim.
</p>

<p>
This post is about the code.  Instead of cataloging <code>spot</code>'s features (I'll do that when I publish the package to Melpa), I want to show <i>how the code actually hangs together on top of VOMPECCC</i>, with verbatim snippets mapped onto the interaction they produce.  If the previous two posts were the <i>why</i> and the <i>what</i>, this one is the <i>how</i>, with a working application to ground the pattern.
</p>
</div>
</div>
<div id="outline-container-demonstration" class="outline-2">
<h2 id="demonstration"><span class="section-number-2">2.</span> The Demonstration&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="marginalia">marginalia</span>&#xa0;<span class="embark">embark</span></span></h2>
<div class="outline-text-2" id="text-demonstration">
<p>
Before any code, here is the concrete task the video is solving: I am trying to find a <a href="https://en.wikipedia.org/wiki/J_Dilla">J Dilla</a> song whose title I can't remember; all I recall is that the word <i>don't</i> is somewhere in the track name.  The entire post revolves around this one video, so it is worth watching before reading on.  Everything that follows is a line-by-line breakdown of the code that produces what you are about to see.  In the upper right hand side of my emacs (in the tab-bar), you'll see the key-bindings and, more importantly, the commands I am invoking to drive <code>spot</code>.  (To make this clip easier to digest, you can play, pause, scrub, view in full screen, or view as "Picture in Picture" use the video controls).
</p>

<video src="https://www.chiply.dev/videos/vompeccc-spot-demo.mp4" data-interactive-video>
</video>

<p>
Here is what happens in the clip:
</p>

<ol class="org-ol">
<li><b>I invoke <code>spot-consult-search</code> and type <code>j dilla</code>.</b>  Each keystroke fires an async query against the Spotify Web API, and the result set is streamed into the minibuffer.  That is <a href="https://www.chiply.dev/post-vompeccc#consult">Consult</a>.  In my emacs config, <code>Vertico</code><sup><a id="fnr.vertico" class="footref" href="https://www.chiply.dev/#fn.vertico" role="doc-backlink">2</a></sup> renders the candidate set vertically so the per-row metadata is legible.</li>
<li><b>I use Spotify's query parameters to widen the result set per type.</b>  Spotify's search endpoint caps results per content type, so I append parameter flags (<code>--type=track --limit=50</code>, etc.) to ask for a fatter haul across tracks, albums, and artists.  The candidates are streamed back through <code>Consult</code> exactly as before, just more of them.</li>
<li><b>I type <code>,</code>, the <code>consult-async-split-style</code> character, to switch from remote search to local ICR.</b>  Everything before the comma continues to be the API query; everything after is a local narrowing pattern that matches against the candidate set already in hand.  No further Spotify requests are issued, and each incremental keystroke only filters the rows <code>Consult</code> is already holding.</li>
<li><b>I type <code>dont</code> (no apostrophe) looking for the song.</b>  The default matching is literal, so "dont" doesn't match "Don't".  Zero candidates.  The corpus contains the song; my pattern just doesn't.  (You thought I did this by mistake didn't you 😜?  It actually highlights why fuzzy matching is so important.)</li>
<li><b>I backspace and prefix the query with <code>~</code></b>, the <code>Orderless</code><sup><a id="fnr.orderless" class="footref" href="https://www.chiply.dev/#fn.orderless" role="doc-backlink">3</a></sup> dispatcher for fuzzy matching.  <code>~dont</code> now matches "Don't Cry" (and others) because fuzzy matching tolerates the missing apostrophe.  The search set is unchanged; I swapped matching styles without re-querying Spotify.  This may sound like a small feature, but consider how much a little fuzz widens the match space of your input strings.  This is espacially important in an application like Spotify where entity names can be long and difficult to remember.</li>
<li><b>I append <code>@donuts</code></b>, the <code>Orderless</code> dispatcher for matching against the <code>Marginalia</code> annotation column rather than the candidate name.  That narrows the surviving candidates to tracks whose <i>annotation</i> mentions "donuts" (i.e., tracks on Dilla's <i>Donuts</i> album, my personal favourite), even though the word "donuts" never appears in any track <i>title</i>.  The song I was looking for is right there. (note my <code>orderless-component-separator</code> is also ",")</li>
<li><b>With the track selected, I invoke <code>Embark</code> (<code>embark-act</code>) and press <code>P</code> to play.</b>  The <code>P</code> binding dispatches to <code>spot-action--generic-play-uri</code>, which pulls the track's URI off the candidate's <code>multi-data</code> property and sends a PUT to the Spotify player.  The song starts playing; no further navigation required.</li>
</ol>

<p>
Three VOMPECCC packages are doing the work: <a href="https://www.chiply.dev/post-vompeccc#consult">Consult</a> (the async streaming + the split-character handoff to local ICR), <a href="https://www.chiply.dev/post-vompeccc#marginalia">Marginalia</a> (the metadata column the <code>@</code> dispatcher just narrowed against), and <a href="https://www.chiply.dev/post-vompeccc#embark">Embark</a> (the action menu that allows you to play the track, list the album's other tracks, or add it to a playlist).  The whole rest of this post is an argument that the code required to make this happen is pleasantly concise, because none of those capabilities (asynchronous search with narrowing, metadata annotation, annotation-aware fuzzy filtering, or contextual actions) needed to be <i>built</i>.  They already exist in the VOMPECCC framework, and <code>spot</code>'s only job is to feed them data.
</p>
</div>
</div>
<div id="outline-container-anatomy" class="outline-2">
<h2 id="anatomy"><span class="section-number-2">3.</span> Anatomy of spot&#xa0;&#xa0;&#xa0;<span class="tag"><span class="structure">structure</span>&#xa0;<span class="modularity">modularity</span></span></h2>
<div class="outline-text-2" id="text-anatomy">
<p>
<code>spot</code> is organized so that each file corresponds to one concern.  This is deliberate: the architecture mirrors the <a href="https://www.chiply.dev/post-vompeccc#vompeccc-framework">modularity</a> of VOMPECCC itself, not because I was trying to be cute (I'm cute enough 👺), but because when your substrate is modular, consuming it modularly is the lowest-friction path.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-right" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">File</th>
<th scope="col" class="org-left">Responsibility</th>
<th scope="col" class="org-left">Substrate package</th>
<th scope="col" class="org-right">LoC</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>spot-auth.el</code></td>
<td class="org-left">OAuth2 authorization + automatic token refresh timer</td>
<td class="org-left">(none)</td>
<td class="org-right">65</td>
</tr>

<tr>
<td class="org-left"><code>spot-generic-query.el</code></td>
<td class="org-left">HTTP request plumbing (sync + async, error surfacing)</td>
<td class="org-left">(none)</td>
<td class="org-right">88</td>
</tr>

<tr>
<td class="org-left"><code>spot-search.el</code></td>
<td class="org-left">Cached search against the Spotify API</td>
<td class="org-left">(none)</td>
<td class="org-right">100</td>
</tr>

<tr>
<td class="org-left"><code>spot-generic-action.el</code></td>
<td class="org-left">Player control commands (play/pause/next/previous)</td>
<td class="org-left">(none)</td>
<td class="org-right">51</td>
</tr>

<tr>
<td class="org-left"><code>spot-mode-line.el</code></td>
<td class="org-left">Currently-playing display</td>
<td class="org-left">(none)</td>
<td class="org-right">115</td>
</tr>

<tr>
<td class="org-left"><code>spot-var.el</code></td>
<td class="org-left">Configuration variables (endpoints, credentials, etc.)</td>
<td class="org-left">(none)</td>
<td class="org-right">127</td>
</tr>

<tr>
<td class="org-left"><code>spot-util.el</code></td>
<td class="org-left">Alist/hash-table conversions, candidate propertize</td>
<td class="org-left">(the glue)</td>
<td class="org-right">52</td>
</tr>

<tr>
<td class="org-left"><code>spot-consult.el</code></td>
<td class="org-left">Seven async Consult sources + <code>consult--multi</code> entry</td>
<td class="org-left">Consult</td>
<td class="org-right">194</td>
</tr>

<tr>
<td class="org-left"><code>spot-marginalia.el</code></td>
<td class="org-left">Annotation functions per content type</td>
<td class="org-left">Marginalia</td>
<td class="org-right">159</td>
</tr>

<tr>
<td class="org-left"><code>spot-embark.el</code></td>
<td class="org-left">Keymaps and actions per content type</td>
<td class="org-left">Embark</td>
<td class="org-right">140</td>
</tr>

<tr>
<td class="org-left"><code>spot.el</code></td>
<td class="org-left"><code>spot-mode</code>: wires registries + timers in and out</td>
<td class="org-left">(integration)</td>
<td class="org-right">37</td>
</tr>

<tr>
<td class="org-left"><b>Total</b></td>
<td class="org-left">&#xa0;</td>
<td class="org-left">&#xa0;</td>
<td class="org-right"><b>1128</b></td>
</tr>
</tbody>
</table>

<p>
<br />
</p>

<p>
The breakdown is the whole point of the <i>shim</i> framing.  The three substrate-facing files (194 + 159 + 140 = 493 lines) are the part that actually integrates with VOMPECCC.  None of that is UI code; there is no UI code in <code>spot</code>.  Every pixel the user sees comes from <code>Consult</code>, <code>Marginalia</code>, <code>Embark</code>, or whatever the user has slotted in below them.
</p>

<p>
One caveat on the 194-line figure for <code>spot-consult.el</code>: roughly 105 of those lines are a 7-way parallel triplet (one source definition, one history variable, and one completion function per Spotify content type), varying only in the narrow key and the <code>:category</code> symbol.  A small macro (<code>spot-define-consult-source</code>) would collapse the 105 lines into 7 invocations plus a ~25-line definition, for 30-35 lines total.  The honest <code>Consult</code>-facing line count, with redundancy factored out, is closer to 115 than 194, and the whole shim closer to 420 than 493.  
</p>

<p>
The reason I didn't write this macro is because it would muddy the concrete depiction of the VOMPECCC APIs here, and honestly, I tend to avoid over-macroizing as it creates new and confusing APIs over well-established and intuitive APIs.
</p>
</div>
</div>
<div id="outline-container-candidates-as-currency" class="outline-2">
<h2 id="candidates-as-currency"><span class="section-number-2">4.</span> Candidates as Shared Currency&#xa0;&#xa0;&#xa0;<span class="tag"><span class="candidates">candidates</span></span></h2>
<div class="outline-text-2" id="text-candidates-as-currency">
<p>
Before looking at any of the three VOMPECCC layers individually, there is one piece of code that makes the entire integration possible.  It is a short function, and if you understand it, you understand how <code>Consult</code>, <code>Marginalia</code>, and <code>Embark</code> cooperate without knowing anything about each other.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defun spot--propertize-items (tables)
  "Propertize a list of hash TABLES for display in completion.
Each table is expected to have `name' and `type' keys.  Names are
truncated for display per `spot-candidate-max-width'; the full
name remains accessible via `multi-data'."
  (-map
   (lambda (table)
     (propertize
      (spot--truncate-name (ht-get table 'name))
      'category (intern (ht-get table 'type))
      'multi-data table))
   tables))
</pre>
</div>

<p>
Every candidate that <code>spot</code> hands to <code>Consult</code> is a <b>string</b> (the Spotify item's name) carrying two <b>text properties</b>:
</p>

<ul class="org-ul">
<li><code>category</code> is one of <code>album</code>, <code>artist</code>, <code>track</code>, <code>playlist</code>, <code>show</code>, <code>episode</code>, or <code>audiobook</code>.  Emacs's completion metadata protocol uses this property to route candidates to the right annotator and the right action keymap.  <code>Marginalia</code> reads it to pick an annotator; <code>Embark</code> reads it to pick a keymap.  <i>The two packages never talk to each other, and yet they agree on every candidate's type</i>, because both are reading the same Emacs-standard property.</li>
<li><code>multi-data</code> is the raw hash table the Spotify API returned for this item: the full JSON response with every field the API exposes.  <code>Marginalia</code>'s annotator reads from it to format the margin; <code>Embark</code>'s actions read from it to execute playback, to navigate to an album's tracks, to add to a playlist.  The candidate <i>is</i> the full record; the name is just the visible handle.  The name <code>multi-data</code> is <code>spot</code>'s own designation, not a <code>Consult</code> or <code>Marginalia</code> convention (the <code>multi-</code> prefix is unrelated to <code>consult--multi</code>); any symbol would have worked.  What <i>is</i> conventional is attaching the domain record to the candidate via <code>propertize</code> in the first place.</li>
</ul>

<blockquote class="pull-quote pull-right">
<p>
<code>Marginalia</code> and <code>Embark</code> never talk to each other.  They both read the same text property on the same candidate, and that is enough.
</p>
</blockquote>

<p>
That is the entire integration surface: <b>One string</b> (display name) and <b>two props</b> (category and metadata).  Everything else (the async fetching, the narrowing, the annotation columns, the action menu) is handled by VOMPECCC, keyed on those two properties.  This is a key take away for those looking to build with VOMPECCC: build your candidates like this and you will have a good time on the mountain.
</p>

<p>
This is what I meant in the <a href="https://www.chiply.dev/post-icr-primer">first post</a> when I called completion a substrate rather than a UI.  A UI would be "here is a widget, bind data to it."  A substrate is "here is a common currency (candidates with standard properties); tools that speak the currency can be mixed freely."
</p>
</div>
</div>
<div id="outline-container-consult" class="outline-2">
<h2 id="consult"><span class="section-number-2">5.</span> Consult: Defining the Search Surface&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="async">async</span>&#xa0;<span class="narrowing">narrowing</span></span></h2>
<div class="outline-text-2" id="text-consult">
<p>
<a href="https://www.chiply.dev/post-vompeccc#consult">Consult</a> is <code>spot</code>'s frontdoor.  It gives me three things I would otherwise have had to build from scratch: async candidate streaming, multi-source unification with narrowing keys, history, and probably other things I'm forgetting.  Here is one of the seven source definitions <code>spot</code> uses:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defvar spot--consult-source-track
  `(:async ,(consult--dynamic-collection
             #'spot--consult-completion-function-consult-track
             :min-input 1)
    :name "Track"
    :narrow ?t
    :category track
    :history spot--history-source-track)
  "Consult source for Spotify tracks.")
</pre>
</div>

<p>
A <code>Consult</code> source is just a plist.  The interesting keys are:
</p>

<ul class="org-ul">
<li><code>:async</code> is the candidate stream.  <code>consult--dynamic-collection</code> is the de-facto extension point third-party packages have settled on for async sources, despite the double-dash that conventionally marks it internal<sup><a id="fnr.consult-extension" class="footref" href="https://www.chiply.dev/#fn.consult-extension" role="doc-backlink">4</a></sup>.  It wraps a function that takes the current minibuffer input and returns a list of candidates.  <code>Consult</code> handles the debouncing and the "only recompute when the input changes" logic on its side; my code just has to produce candidates for a given query.  <code>:min-input 1</code> prevents a search on an empty query.  This is the <a href="https://www.chiply.dev/post-vompeccc#consult">two-level async filtering</a> that <code>Consult</code> is designed around: the external tool (Spotify's API, in this case) handles the expensive filtering against its own corpus, and my completion style (<code>Orderless</code>, if I have it) narrows the returned set locally.</li>
<li><code>:narrow ?t</code> binds the narrowing key.  In the video, I could have pressed <code>t SPC</code> when running <code>spot-consult-search</code>, and the session would have been scoped to tracks only, and would have avoided querying the other sources.  I didn't implement narrowing; <code>Consult</code> did.  I just declared which character maps to which source!</li>
<li><code>:category track</code> is the property that will propagate onto every candidate from this source.  This is the <i>same</i> <code>category</code> property that <code>spot--propertize-items</code> stamps on individual candidates, and it is the hinge that <code>Marginalia</code> and <code>Embark</code> both key off.</li>
<li><code>:history</code> gives me free persistent search history for this source, isolated from the other sources.</li>
</ul>

<p>
The completion function itself is trivial because all the work happens in <code>spot-search.el</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defun spot--consult-completion-function-consult-track (query)
  "Return track candidates for QUERY."
  (spot--search-cached-and-locked query spot--mutex spot--cache)
  spot--candidates-track)
</pre>
</div>

<p>
Seven of these functions exist, one per content type, all identical except for which global they return.  The heavy lifting (the HTTP call, the cache, the propertization) is shared.  Each source is effectively a <i>view</i> onto a single search result split by type.
</p>

<p>
Putting all seven sources together into one interface is also trivial:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defvar spot--search-sources
  '(spot--consult-source-album spot--consult-source-artist
    spot--consult-source-playlist spot--consult-source-track
    spot--consult-source-show spot--consult-source-episode
    spot--consult-source-audiobook)
  "List of consult sources for Spotify search.")

;;;###autoload
(defun spot-consult-search (&amp;optional initial)
  "Search Spotify with consult multi-source completion.
Optional INITIAL provides initial input."
  (interactive)
  (consult--multi
   spot--search-sources
   :history '(:input spot--consult-search-search-history)
   :initial initial))
</pre>
</div>

<p>
This is the command you saw in the video.  <code>consult--multi</code> takes the list of sources, unifies their candidates into a single list, and wires the narrowing keys.  Seven heterogeneous content types, one prompt, one keystroke to filter to any subset, async throughout, with per-source history.
</p>

<blockquote class="pull-quote pull-left">
<p>
Without <code>Consult</code> I would need: a separate candidate display, an async debouncer, a narrowing mechanism, per-source history buffers, and some way to visually distinguish content types in a single list.
</p>
</blockquote>

<p>
Compare this to the counterfactual.  Without <code>Consult</code> I would need: a separate candidate display, an async debouncer, a narrowing mechanism, per-source history buffers, and some way to visually distinguish content types in a single list.  And because <code>Consult</code> uses the standard <code>completing-read</code> contract, <i>every</i> minibuffer feature my Emacs already has (<code>Vertico</code>'s display, <code>Orderless</code>'s matching, <code>Prescient</code>'s sorting) applies to <code>spot</code> with zero integration code.
</p>
</div>
</div>
<div id="outline-container-cache" class="outline-2">
<h2 id="cache"><span class="section-number-2">6.</span> Why the Cache?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="async">async</span>&#xa0;<span class="ratelimits">ratelimits</span></span></h2>
<div class="outline-text-2" id="text-cache">
<p>
I have been brushing past a detail of <code>spot-consult.el</code> that deserves its own section, because it is the honest <i>cost</i> of building on an async-on-every-keystroke substrate.  <code>consult--dynamic-collection</code> wires the completion function to the minibuffer such that it is invoked on (a debounced version of) every keystroke the user types.  For <code>spot</code>, each invocation issues an HTTP request to Spotify's Web API, receives a mixed-type result set, splits it across the seven global candidate lists, and returns the slice relevant to the calling source.  That is the hot path.  And the hot path is a rate-limited network call.
</p>

<p>
Spotify's Web API is rate-limited 🙃.  Exact limits are dynamic and not publicly documented in detail, but the envelope is small enough that a rapid-typing ICR session can hit it quickly.  Consider the baseline: typing <code>radiohead</code> fires a completion call for each prefix the user's typing pauses on (<code>Consult</code>'s <code>consult-async-input-debounce</code> and <code>consult-async-input-throttle</code> collapse runs of keystrokes into a smaller set of actually-issued calls, but realistically that still leaves several distinct prefixes per word).  Now add the common real-world pattern of typing too far, backspacing a few characters, and retyping: the same query string is re-issued within the same search session.  Without a cache, each repetition burns a request, but with a cache keyed on the raw query string, repeats are actually free (or at least as cheap as a cache hit):
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defun spot--search-cached (query cache)
  "Search for QUERY, using CACHE to avoid duplicate requests."
  (when (not (ht-get cache query))
    (let ((results (spot--propertize-items
                    (spot--union-search-items
                     (spot--search-items query)))))
      (ht-set cache query results)))
  (let ((results (ht-get cache query)))
    (spot--set-search-candidates results)))
</pre>
</div>

<p>
The cache is a hash table from query strings to propertized candidate lists.  It lives for the life of the Emacs session, so not only backspace-and-retype within one search but also the <i>next</i> search session that hits the same prefix is instant.  The memory cost is negligible (a few hundred candidates per query, small hash tables for each) and the request-budget win is real.  And if you find yourself listening to the same music over and over, then you'll have snappier results when you go down familiar paths.
</p>

<blockquote class="pull-quote pull-left">
<p>
Async-on-every-keystroke against a remote corpus is the feature.  A query-string cache is the bill.
</p>
</blockquote>

<p>
This is the honest <i>consumer</i> tax of the substrate.  The <a href="https://www.chiply.dev/post-icr-primer">first post</a> sold you on ICR by promising that <a href="https://www.chiply.dev/post-icr-primer#the-cognitive-cost-argument">the interaction scales constantly regardless of how big the underlying corpus gets</a>.  That claim depends on async sources that fire on every keystroke against a remote corpus, and that in turn means you as package author inherit rate-limit pressure your users never see.  <code>Consult</code> gives you the debouncer, the display, the narrowing keys, and the stale-response discarding on its side of the protocol.  The cache is what you owe back on your side when your candidate source is a rate-limited network API rather than a local list, and it is <i>exactly</i> the kind of infrastructure that does not belong in <code>Consult</code> itself (because <code>Consult</code> has no way to know your backend is rate-limited, or which queries are equivalent enough to cache together).
</p>
</div>
</div>
<div id="outline-container-marginalia" class="outline-2">
<h2 id="marginalia"><span class="section-number-2">7.</span> Marginalia: Promoting Candidates into Informed Choices&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marginalia">marginalia</span></span></h2>
<div class="outline-text-2" id="text-marginalia">
<p>
If you watch the video carefully, each track in the candidate list is followed by a horizontally aligned column of fields: <code>#&lt;track-number&gt;</code>, artist, a <code>M:SS</code> duration, album name, album type, release date.  Each field is rendered to a fixed width in its own face, so numbers and dates and names land as visually distinct columns rather than getting mashed together with a delimiter.  Small glyph prefixes (<code>#</code> for counts, <code>★</code> for popularity, <code>♥</code> for followers) disambiguate otherwise bare numbers.  That column is provided by <a href="https://www.chiply.dev/post-vompeccc#marginalia">Marginalia</a>, and it comes from one function:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defun spot--annotate-track (cand)
  "Annotate track CAND with number, artist, duration, album, type, and date.
The track number is prefixed with `#' and duration rendered as M:SS."
  (let ((data (get-text-property 0 'multi-data cand)))
    (marginalia--fields
     ((spot--format-count (ht-get data 'track_number))
      :format "#%s" :truncate 5 :face 'spot-marginalia-number)
     ((spot--annotation-field (spot--first-name (ht-get data 'artists)))
      :truncate 25 :face 'spot-marginalia-artist)
     ((spot--format-duration (ht-get data 'duration_ms))
      :truncate 7 :face 'spot-marginalia-number)
     ((spot--annotation-field (ht-get* data 'album 'name))
      :truncate 30 :face 'spot-marginalia-album)
     ((spot--annotation-field (ht-get* data 'album 'album_type))
      :truncate 8 :face 'spot-marginalia-type)
     ((spot--annotation-field (ht-get* data 'album 'release_date))
      :truncate 10 :face 'spot-marginalia-date))))
</pre>
</div>

<p>
The first line is the only plumbing: <code>(get-text-property 0 'multi-data cand)</code> pulls the full Spotify API response off the candidate (exactly the hash table <code>spot--propertize-items</code> stashed earlier), and everything after it is <code>Marginalia</code>'s own <code>marginalia--fields</code> macro doing the formatting.  <code>marginalia--fields</code> handles the alignment, the per-field truncation, and the face application.  The only thing my code does is declare which <i>fields</i> of the Spotify payload go in which <i>columns</i> with which <i>faces</i>.  This is another substrate borrow hiding in plain sight: <code>Marginalia</code> registers the annotator <i>and</i> formats its output.  I never wrote a single character of alignment, padding, or colourisation logic.  The annotator reached into <code>multi-data</code> for its fields, <code>Marginalia</code>'s macro did the cosmetic work, and <code>Marginalia</code> never had to know about Spotify's data model.
</p>

<p>
<code>spot</code> ships seven annotators.  Each one is a domain-specific projection of a single Spotify response type onto a display string.  Albums surface artist, release date, and track count, artists surface popularity and follower count, shows surface publisher, media type, and episode count; and all this context is really important, especially if you are 'browsing'.  The annotators are independent of the search code, independent of the actions code, and independent of each other.
</p>

<p>
Registering them with <code>Marginalia</code> is three lines of bookkeeping:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defvar spot--marginalia-annotator-entries
  '((album spot--annotate-album none)
    (artist spot--annotate-artist none)
    (playlist spot--annotate-playlist none)
    (track spot--annotate-track none)
    (show spot--annotate-show none)
    (episode spot--annotate-episode none)
    (audiobook spot--annotate-audiobook none))
  "List of marginalia annotator entries registered by spot.")

(defun spot--setup-marginalia ()
  "Register spot annotators with marginalia."
  (dolist (entry spot--marginalia-annotator-entries)
    (add-to-list 'marginalia-annotators entry)))
</pre>
</div>

<p>
The <code>spot--marginalia-annotator-entries</code> list keys on the <code>category</code> symbol (<code>album</code>, <code>artist</code>, and so on), the very same symbols the <code>Consult</code> sources stamp onto their candidates.  <code>Marginalia</code> looks up the category of the current candidate in <code>marginalia-annotators</code>, finds the entry, and runs the annotator.  No <code>spot</code> code is in that path.  I only had to declare the mapping.
</p>

<p>
This is where one of the most interesting benefits of the second post shows up concretely.  <a href="https://www.chiply.dev/post-vompeccc#marginalia">That post mentioned</a> that because <code>Marginalia</code> annotations are themselves searchable, <code>Orderless</code>'s <code>@</code> dispatcher lets you match against annotation text.  <code>spot</code> did not ship this feature.  <code>Orderless</code> and <code>Marginalia</code> did, for free, because I stamped the annotation onto the candidate in the right way.
</p>
</div>
</div>
<div id="outline-container-four-levels-of-narrowing" class="outline-2">
<h2 id="four-levels-of-narrowing"><span class="section-number-2">8.</span> The Four Levels of Narrowing&#xa0;&#xa0;&#xa0;<span class="tag"><span class="narrowing">narrowing</span>&#xa0;<span class="orderless">orderless</span>&#xa0;<span class="consult">consult</span></span></h2>
<div class="outline-text-2" id="text-four-levels-of-narrowing">
<p>
The walkthrough at the top of this post shows three different characters doing three superficially similar jobs: <code>,</code> (narrow the Spotify result set without issuing a new remote request), <code>~</code> (switch to fuzzy matching), and <code>@</code> (match against the <code>Marginalia</code> annotation column rather than the candidate name).  To a reader who has internalized the modularity argument of the series, these are <i>three different packages each owning one lever</i>, but when you see them one after another in a single search prompt, it is easy to assume they are variations on the same theme.
</p>

<p>
<a href="https://www.reddit.com/r/emacs/comments/1srni92/a_vompeccc_case_study_spotify_as_pure_icr_in_emacs/ohrx5k6/"><code>u/Strickinato</code> pointed out on Reddit</a> that step 3 of the walkthrough broke their mental model in a useful way.  Their initial read was that <code>,</code> would match against the annotations; which is actually what <code>Orderless</code>'s <code>&amp;</code> dispatcher does (remapped to <code>@</code> in my config).  Reasoning through <i>why</i> that expectation was wrong led them to a crisp taxonomy worth reproducing here, lightly adapted, because it names something the walkthrough shows but never quite spells out.
</p>

<ol class="org-ol">
<li><i>Remote search.</i>  Everything <i>before</i> the <code>consult-async-split-style</code> character is the string sent to the external backend; Spotify's Web API, in this case.  The backend runs its own search, over its own corpus, under its own matching rules.  Nothing in Emacs has <i>seen</i> this corpus; <code>Consult</code> is just the transport.  (Step 2 of the demo showed a sub-variant of this: Spotify-specific <i>flags</i> like <code>--type=track --limit=50</code> are appended to the remote query to reshape <i>how</i> the backend searches, rather than <i>what</i> it searches against.  Same level; different lever on the same backend.)</li>
<li><i>Candidate narrowing.</i>  Everything <i>after</i> the split character is matched against the candidates <code>Consult</code> is already holding.  No more remote requests fire.  Each keystroke filters the in-hand set by candidate <i>name</i> under whatever completion style you have configured (<code>Orderless</code>, <code>flex</code>, <code>basic</code>, whatever).  This is the level at which your everyday completion-style intuitions apply; step 3 of the demo switches to this mode the moment I type the comma.</li>
<li><i>Annotation narrowing.</i>  <code>Orderless</code>'s <code>orderless-annotation</code> dispatcher (the <code>&amp;</code> character by default, remapped to <code>@</code> in my config via <code>orderless-affix-dispatch-alist</code>) matches against the <code>Marginalia</code> column rather than the candidate name.  Step 6 of the demo is exactly this: <code>@donuts</code> narrows to tracks whose annotation mentions "donuts", even though no track <i>title</i> contains the word.  This is the level at which <code>Orderless</code> and <code>Marginalia</code> interoperate without either knowing about the other.</li>
<li><i>Hidden-metadata narrowing.</i>  <a href="https://www.reddit.com/r/emacs/comments/1srni92/a_vompeccc_case_study_spotify_as_pure_icr_in_emacs/ohtcn2b/"><code>u/JDRiverRun</code> added</a> a fourth: <code>orderless-kwd</code> exposes keyword-prefixed filters that match against metadata which is <i>not even displayed</i>.  Their example: in <code>M-x</code>, type <code>org :doc:table</code> and the candidate list narrows to commands whose <i>docstring</i> mentions "table", even when the docstring never appears on screen.  This is the most interesting level to me, because it clarifies that "the candidate" (the visible text) and "what is searchable about the candidate" are two different things, and the substrate lets a package declare <i>which facets of a record are filterable</i> without committing to <i>which facets are visible</i>.</li>
</ol>

<blockquote class="pull-quote pull-right">
<p>
Four levels, four packages cooperating.  No one of them implements more than one.
</p>
</blockquote>

<p>
<code>Consult</code> owns the split that separates 1 from 2; the active completion style (<code>Orderless</code>, <code>flex</code>, and so on) owns the matching in 2; <code>Marginalia</code> supplies the content that 3 matches against; <code>orderless-kwd</code> (shipped with <code>Orderless</code>) owns 4.  The original confusion (<i>"surely the comma filters annotations too?"</i>) is, in hindsight, the sharpest demonstration of the modularity on offer: if one character <i>had</i> been responsible for two of these levels, the substrate would be more tangled, and VOMPECCC's central claim would be weaker.
</p>
</div>
</div>
<div id="outline-container-embark" class="outline-2">
<h2 id="embark"><span class="section-number-2">9.</span> Embark: The Action Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="composition">composition</span></span></h2>
<div class="outline-text-2" id="text-embark">
<p>
The third leg of <code>spot</code>'s tripod is <a href="https://www.chiply.dev/post-vompeccc#embark">Embark</a>.  In the video, pressing the <code>Embark</code> action key on any candidate surfaces a menu of single-letter actions appropriate to <i>that kind</i> of candidate: <code>P</code> plays it, <code>s</code> shows its raw data, <code>t</code> lists its tracks (on albums and artists), <code>+</code> adds it to a playlist (on tracks).  Each of those actions is a one-function definition in <code>spot-embark.el</code>, and their binding to candidates is declarative.
</p>

<p>
The simplest action is <code>play</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defun spot-action--generic-play-uri (item)
  "Play the Spotify item represented by ITEM."
  (let* ((table (get-text-property 0 'multi-data item))
         (type (ht-get table 'type))
         (offset (cond
                  ((string= type "track") `(("uri" . ,(ht-get* table 'uri))))
                  ((string= type "playlist") '(("position" . 0)))
                  ((string= type "album") '(("position" . 0)))
                  ((string= type "artist") nil)))
         (context_uri (cond
                       ((string= type "track") (ht-get* table 'album 'uri))
                       ((string= type "playlist") (ht-get* table 'uri))
                       ((string= type "album") (ht-get* table 'uri))
                       ((string= type "artist") (ht-get* table 'uri))))
         ...
         (spot-request-async
          :method "PUT"
          :url spot-player-play-url ...))))
</pre>
</div>

<p>
Same pattern as the annotators: <code>(get-text-property 0 'multi-data item)</code> pulls the full hash table off the candidate, and the rest is Spotify domain logic.  <code>Embark</code> invokes my action with the candidate that was highlighted; my action handles the HTTP.
</p>

<p>
The keymap wiring is also just bookkeeping:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defvar-keymap spot-embark-track-keymap
  :parent embark-general-map
  :doc "Keymap for Spotify track actions.")

;; ... one keymap per content type ...

(defvar spot--embark-keymap-entries
  '((album . spot-embark-album-keymap)
    (artist . spot-embark-artist-keymap)
    (playlist . spot-embark-playlist-keymap)
    (track . spot-embark-track-keymap)
    (show . spot-embark-show-keymap)
    (episode . spot-embark-episode-keymap)
    (audiobook . spot-embark-audiobook-keymap)
    ...))

(dolist (map (list spot-embark-artist-keymap spot-embark-album-keymap
                   spot-embark-playlist-keymap spot-embark-track-keymap
                   ...))
  (define-key map "s" #'spot-action--generic-show-data)
  (define-key map "P" #'spot-action--generic-play-uri))

(define-key spot-embark-track-keymap "+" #'spot-action--add-track-to-playlist)
(define-key spot-embark-album-keymap  "t" #'spot-action--list-album-tracks)
(define-key spot-embark-artist-keymap "t" #'spot-action--list-artist-tracks)
(define-key spot-embark-playlist-keymap "t" #'spot-action--list-playlist-tracks)
</pre>
</div>

<p>
Again, the key keys off <code>category</code>.  <code>Embark</code> looks up the current candidate's <code>category</code> in <code>embark-keymap-alist</code>, finds the matching keymap, opens it.  Every layer of this integration is the same trick: a candidate carries a <code>category</code> property, and the substrate routes based on it.  All three VOMPECCC packages, working on the same candidates, sharing the same category convention, never importing each other.
</p>
</div>
<div id="outline-container-composition" class="outline-3">
<h3 id="composition"><span class="section-number-3">9.1.</span> Composition: When an Action Opens Another Search&#xa0;&#xa0;&#xa0;<span class="tag"><span class="composition">composition</span>&#xa0;<span class="chaining">chaining</span></span></h3>
<div class="outline-text-3" id="text-composition">
<p>
One action in particular is worth reading slowly, because it closes the loop the <a href="https://www.chiply.dev/post-icr-primer#a-thought-exercise--how-much-of-computing-fits-inside-icr-">thought exercise</a> in the first post opened:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(defun spot-action--list-album-tracks (item)
  "Search for tracks on the album represented by ITEM."
  (let* ((table (get-text-property 0 'multi-data item))
         (album-name (ht-get* table 'name))
         (artist-name (ht-get* (nth 0 (ht-get* table 'artists)) 'name)))
    (spot-consult-search
     (concat
      "album:" album-name
      " "
      "artist:" artist-name " -- --type=track"))))
</pre>
</div>

<p>
This action runs when I am in a completion session, run <code>Embark</code> on an album candidate, and press <code>t</code>.  It extracts the album name and artist from the <code>multi-data</code>, builds a Spotify query using Spotify's field-filter syntax (<code>album:X artist:Y</code>), and calls <code>spot-consult-search</code> again: the same entry point the user invoked initially.
</p>

<blockquote class="pull-quote pull-left">
<p>
<code>Embark</code> action on a <code>Consult</code> candidate launches a new <code>Consult</code> session, scoped to that candidate.  Three lines of Lisp.  The whole "chain ICRs to compose workflows" argument from Post 1, made concrete.
</p>
</blockquote>

<p>
Nice!!!  What just happened?  An <code>Embark</code> action on a candidate produced by a <code>Consult</code> source launched a new <code>Consult</code> session, scoped to the selected candidate, in the same substrate, with the same annotators, and the same available actions.  The <a href="https://www.chiply.dev/post-icr-primer#a-thought-exercise--how-much-of-computing-fits-inside-icr-">chaining pattern from the first post</a> ("ICR to pick a thing, which scopes the candidate set for the next ICR") is literally three lines of <code>spot</code> code, because the substrate composes oh so cleanly with itself.
</p>

<p>
The first post described this as the shell's <code>git branch | fzf | xargs git checkout</code> pattern in miniature.  In <code>spot</code>, the pipe is <code>embark-act</code>, and the downstream command is another <code>consult--multi</code>.  It is the same compositional shape; the surface it runs on is different.
</p>
</div>
</div>
</div>
<div id="outline-container-spot-mode" class="outline-2">
<h2 id="spot-mode"><span class="section-number-2">10.</span> The Integration Point: spot-mode&#xa0;&#xa0;&#xa0;<span class="tag"><span class="modularity">modularity</span>&#xa0;<span class="hooks">hooks</span></span></h2>
<div class="outline-text-2" id="text-spot-mode">
<p>
Both registries (<code>Marginalia</code>'s annotator alist and <code>Embark</code>'s keymap alist) plus the two background timers (mode-line updates and access-token refresh) get installed and uninstalled in one place:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">;;;###autoload
(define-minor-mode spot-mode
  "Global minor mode for the spot Spotify client.
Registers embark keymaps, marginalia annotators, starts the
mode-line update timer, and starts a periodic access-token
refresh timer when enabled.  Cleanly removes all integrations
when disabled."
  :global t
  :group 'spot
  (if spot-mode
      (progn
        (spot--setup-embark)
        (spot--setup-marginalia)
        (spot--start-update-timer)
        (spot--start-refresh-timer))
    (spot--teardown-embark)
    (spot--teardown-marginalia)
    (spot--stop-update-timer)
    (spot--stop-refresh-timer)))
</pre>
</div>

<p>
This is the entire integration layer.  Toggle the mode, <code>spot</code>'s categories appear in <code>Marginalia</code> and <code>Embark</code> and the two timers begin ticking.  Toggle it off, they all disappear.  No global state mutation escapes the teardown path.
</p>

<p>
And by the way, a user who never installs <code>Marginalia</code> or <code>Embark</code> still gets a working <code>spot</code>; the setup functions no-op gracefully (all they do is <code>add-to-list</code> against someone else's variable), that user just doesn't get annotations or actions.  The "stack what you want, subset what you don't need" <a href="https://www.chiply.dev/post-vompeccc#subset-property">property of VOMPECCC</a> propagates through to <code>spot</code> as a consumer: the package is graceful under any subset of VOMPECCC.
</p>
</div>
</div>
<div id="outline-container-counterfactual" class="outline-2">
<h2 id="counterfactual"><span class="section-number-2">11.</span> The Counterfactual: What spot Would Look Like Without VOMPECCC</h2>
<div class="outline-text-2" id="text-counterfactual">
<p>
To see what <code>spot</code> isn't building, look at the negative space.
</p>

<p>
A pre-VOMPECCC Spotify client (see <a href="https://github.com/danielfm/smudge">smudge</a> for an example that predates the modern completion ecosystem) has to build the UI itself: a <code>tabulated-list-mode</code> buffer with its own keymap, its own rendering code, its own pagination, its own selection logic.  That approach works and can work well.  But the cost is structural: a bespoke UI is a parallel universe of interaction that does not benefit from any completion infrastructure the user has already invested in.  You have to learn its bindings, and frustratingly, these don't carry over to any other Emacs tool.
</p>

<p>
The architecture was entirely reasonable when there was nothing else to build on.  The point here is purely structural: once the substrate exists, reinventing the UI on top of it is a strictly larger codebase that delivers a strictly less interoperable experience.  <code>spot</code> is about 1,100 lines of Lisp, and its interface, as we've shown, is closer to 420 lines of Lisp.  A pre-substrate equivalent is many times that, and much of the delta is code implementing things (display, filtering, selection, action menus) that <code>Consult</code>, <code>Marginalia</code>, and <code>Embark</code> implement once, centrally, for every completion-driven command in the user's Emacs.
</p>

<p>
This is the gap the <a href="https://www.chiply.dev/post-icr-primer#why-icr-matters-more-in-emacs-than-anywhere-else">first post</a> was pointing at when it distinguished <i>using</i> completion from <i>building on</i> completion.  A package that uses completion is a consumer of <code>completing-read</code>.  A package that builds on completion assumes the existence of a richer substrate (async sources, categorized candidates, annotator hooks, action keymaps) and contributes into that substrate rather than rebuilding around it.
</p>
</div>
</div>
<div id="outline-container-substrate-takeaway" class="outline-2">
<h2 id="substrate-takeaway"><span class="section-number-2">12.</span> What This Says About the Substrate&#xa0;&#xa0;&#xa0;<span class="tag"><span class="substrate">substrate</span>&#xa0;<span class="platform">platform</span></span></h2>
<div class="outline-text-2" id="text-substrate-takeaway">
<p>
Three things follow.
</p>

<p>
<b>First, the cost of building an ICR-driven app collapses once the substrate exists.</b>  <code>spot</code> is about 1,100 lines including OAuth, token refresh, HTTP, caching, the mode-line, and the integration glue.  The three VOMPECCC files (<code>spot-consult.el</code>, <code>spot-marginalia.el</code>, <code>spot-embark.el</code>) are together under 500 lines, much of it boilerplate per content type.  A feature-competitive pre-VOMPECCC Spotify client would easily have been several thousand lines larger.
</p>

<p>
<b>Second, composition is the feature, not the packages.</b>  The <code>list-album-tracks</code> action is the most important ten lines in the repository, not because of what it does (a Spotify query), but because of what it demonstrates: an <code>Embark</code> action on a <code>Consult</code> candidate launching a new <code>Consult</code> session in the same substrate.  Every ICR-driven package in your Emacs configuration that shares this substrate composes with every other one.  <code>embark-export</code> on a <code>spot</code> result set could, in principle, produce a native mode for Spotify results, the same way it produces Dired from file candidates or wgrep from ripgrep hits.  The composability is a property of the substrate, not of any individual package.
</p>

<p>
<b>Third, the category property is doing an enormous amount of load-bearing work.</b>  Three different packages, each knowing nothing about the others, all agree on the right behavior for every candidate because they are keying off the same standardized property <code>'category</code>.  The "text" in the protocol is <code>(candidate . (category . metadata))</code>, and every tool that speaks the protocol interoperates for free.
</p>
</div>
</div>
<div id="outline-container-generalization" class="outline-2">
<h2 id="generalization"><span class="section-number-2">13.</span> Generalizing the Pattern Beyond Spotify&#xa0;&#xa0;&#xa0;<span class="tag"><span class="generalization">generalization</span>&#xa0;<span class="pattern">pattern</span></span></h2>
<div class="outline-text-2" id="text-generalization">
<p>
<code>spot</code> is specifically a Spotify client, but nothing about the recipe it follows is Spotify-specific.  Strip the domain out and what remains is a six-step shape that applies to an enormous fraction of the services and data sources you interact with daily:
</p>

<ol class="org-ol">
<li>An API or backend that returns <i>typed items</i>: each item has a <code>type</code> discriminator and a bag of metadata.</li>
<li>A candidate-constructor (the <code>spot--propertize-items</code> analogue) that turns those items into completion candidates with a <code>category</code> text property and a <code>multi-data</code> payload.</li>
<li>A <code>Consult</code> source per type, async, with a narrow key, all unified under a <code>consult--multi</code> entry point.</li>
<li>A <code>Marginalia</code> annotator per type, keyed on <code>category</code>, reading the <code>multi-data</code> payload for its domain metadata.</li>
<li>An <code>Embark</code> keymap per type, keyed on <code>category</code>, binding single-letter actions that operate on the <code>multi-data</code> payload.</li>
<li>A minor mode that installs and uninstalls the three registries together.  This one can even be optional, but I recommend doing it.</li>
</ol>

<p>
Any domain that fits that shape can be built the same way.  The <a href="https://www.chiply.dev/post-icr-primer#a-thought-exercise--how-much-of-computing-fits-inside-icr-">thought exercise from the first post</a> (<i>which of your daily tools reduces to "pick a thing, act on it" over a typed corpus?</i>) has a lot of concrete answers: issue trackers, cloud consoles, email, chat, package managers, news feeds, knowledge bases, code hosting.  Two worked examples are enough to sketch the altitude:
</p>

<ul class="org-ul">
<li><b>Issue trackers.</b>  Types are issue / epic / comment / user, metadata is status / assignee / priority / labels, actions are transition / assign / comment / close.</li>
<li><b>Code hosting.</b>  <code>consult-gh</code> already does the GitHub version.  Types are repo / PR / issue / branch / release / user, metadata is state / author / date / counts, actions are clone / checkout / review / merge / close.</li>
</ul>

<p>
Several domains already ship as working packages: <code>consult-gh</code>, <code>consult-notes</code>, <code>consult-omni</code>, <code>consult-tramp</code>, <code>consult-dir</code>, and many others.  None of these packages ships a UI; they all (roughly) follow the same six-step recipe <code>spot</code> follows, and each one composes with every other one automatically.
</p>

<p>
The more interesting exercise is the shape of domains that <i>don't</i> cleanly fit.  The pattern starts to strain when items aren't naturally enumerable, or when the right interaction is a canvas rather than a list (a map, a timeline, a dependency graph).  Those cases need something more than ICR.  What I find remarkable is how often even <i>those</i> interfaces still have an ICR-shaped core (pick a location on the map, pick a node on the graph, pick a frame on the timeline), which could be delegated to the substrate while the custom-UI parts focus on what genuinely needs rendering.
</p>

<p>
The concrete-enough test I apply to any new Emacs workflow I'm considering building: <i>can I express it as a <code>Consult</code> source, a <code>Marginalia</code> annotator, and an <code>Embark</code> keymap?</i>  If yes, the package will be mostly a client of the VOMPECCC API.  If no, the package needs custom UI, and I should be deliberate about which parts genuinely do and which parts could still be delegated.  <code>spot</code> is the case where the answer is a clean "yes across the board", but I've found that more often than not, the answer is yes for the first draft.
</p>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">14.</span> Conclusion</h2>
<div class="outline-text-2" id="text-conclusion">
<p>
This post took a working application and showed what the argument looks like when you cash it in.
</p>

<p>
If there is one thing I want a reader to take away from the series, it is the reframe.  Completion is not a convenience feature you turn on and forget about.  It is the <i>primitive</i> on which a surprising fraction of your Emacs interaction either already runs or could run, if you let it.  Packages that treat it that way end up smaller, more interoperable, and more amenable to composition than packages that treat it as one feature among many.  <code>spot</code> is one example.
</p>

<p>
The broader claim, which I will leave you with, is that "packages that do one thing" is the lazy reading of the Unix philosophy.  The sharper reading is "packages that <i>contribute into</i> a shared substrate."  Unix pipes were never interesting because each command was small; they were interesting because every command produced and consumed plain text.  VOMPECCC is interesting for the same reason, with candidates-with-properties instead of plain text.  <code>spot</code> was easy to write because the substrate is good.  Many things in your Emacs configuration could be rewritten today as "ICR applications on the substrate" and would be smaller, cleaner, and more composable as a result.
</p>

<p>
When you next find yourself thinking <i>"I wish there were a better way to browse X"</i>, ask whether it could just be a <code>Consult</code> source, a <code>Marginalia</code> annotator, and an <code>Embark</code> keymap.  Surprisingly often, that is the entire package, and all you have to do is feed it data.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">15.</span> TLDR</h2>
<div class="outline-text-2" id="text-tldr">
<p>
<a href="https://github.com/chiply/spot"><code>spot</code></a> is a Spotify client for Emacs that implements <a href="https://www.chiply.dev/#demonstration">no custom UI</a>.  About <a href="https://www.chiply.dev/#anatomy">493 of its ~1,100 lines are the "shim"</a> that feeds candidates into <code>Consult</code>, <code>Marginalia</code>, and <code>Embark</code> via a single text-property pattern (<a href="https://www.chiply.dev/#candidates-as-currency"><code>category</code> plus <code>multi-data</code></a>); the remaining ~635 are plumbing any Spotify client would need regardless of UI.  The <a href="https://www.chiply.dev/#generalization">six-step recipe</a> (typed items → propertize → <code>Consult</code> source per type → <code>Marginalia</code> annotator per type → <code>Embark</code> keymap per type → minor mode) generalizes to issue trackers, cloud consoles, email, chat, knowledge bases, and more, many of which already ship as working packages (<code>consult-gh</code>, <code>consult-notes</code>, <code>consult-omni</code>).  The claim the series has been building toward: <i>when the substrate is good, ICR applications collapse to their domain logic, and "packages that contribute into a shared substrate" is the sharper reading of the Unix philosophy</i>.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.loc" class="footnum" href="https://www.chiply.dev/#fnr.loc" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
As of the version being discussed, the eleven <code>.el</code> files in the repository total about 1,128 non-blank, non-comment lines.  Not a large package by any measure.
</p></div></div>

<div class="footdef"><sup><a id="fn.vertico" class="footnum" href="https://www.chiply.dev/#fnr.vertico" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
<a href="https://www.chiply.dev/post-vompeccc#vertico">Vertico</a> is the vertical minibuffer UI you see in the video.  It is <i>not</i> part of the <code>spot</code> package; it is a piece of my personal Emacs configuration, one of the VOMPECCC packages the user slots in underneath a consumer like <code>spot</code>.  A different user could run <code>spot</code> with <code>fido-vertical-mode</code>, Helm, Ivy, or plain default <code>completing-read</code>; the candidates and their annotations would be unchanged, only the rendering would differ.
</p></div></div>

<div class="footdef"><sup><a id="fn.orderless" class="footnum" href="https://www.chiply.dev/#fnr.orderless" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
<a href="https://www.chiply.dev/post-vompeccc#orderless">Orderless</a> is the completion style that powers the <code>~</code> (fuzzy) and <code>@</code> (annotation) dispatchers in the video.  Like Vertico, it is configured in my personal Emacs setup, not shipped with spot.  One detail worth calling out: Orderless's default annotation dispatcher is <code>&amp;</code>, not <code>@</code>.  I remap it to <code>@</code> in my own config, so the <code>@donuts</code> you see in the video is specific to my setup; out of the box you would type <code>&amp;donuts</code> to get the same behavior.  The dispatcher characters are fully user-configurable, and users on an entirely different completion style (<code>flex</code>, <code>substring</code>, <code>basic</code>) will see different filtering behavior.
</p></div></div>

<div class="footdef"><sup><a id="fn.consult-extension" class="footnum" href="https://www.chiply.dev/#fnr.consult-extension" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The double-dash convention in Elisp marks a symbol as internal to its package.  <code>consult--dynamic-collection</code> is formally one of those.  In practice it is the extension point third-party async <code>Consult</code> sources have all settled on, and Daniel Mendler has been careful about signalling breaking changes in the <code>Consult</code> changelog when its shape does shift.  <code>spot</code> pins <code>consult &gt;</code> 1.0= for this reason.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>VOMPECCC: A Modular Completion Framework for Emacs</title>
      <link>https://www.chiply.dev/post-vompeccc</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-vompeccc</guid>
      <pubDate>Fri, 17 Apr 2026 11:17:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Completion is not a feature or UI, but instead it is a system composed of at least half a dozen orthogonal concerns that most users never think about separately. The previous post in this series argue...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="completion">completion</span>&#xa0;<span class="modularity">modularity</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org740ab49" class="figure">
<p><img src="https://www.chiply.dev/images/vompeccc-banner.jpeg" alt="vompeccc-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/dall-e-3">DALL-E 3</a></p>
</div>

<p>
Completion is not a feature or UI, but instead it is a <i>system</i> composed of at least half a dozen orthogonal concerns that most users never think about separately.  The <a href="https://www.chiply.dev/post-icr-primer">previous post in this series</a> argued that Emacs uniquely exposes completion as a programmable <i>substrate</i> rather than a sealed UI, and that this substrate is what makes <a href="https://www.chiply.dev/post-icr-primer#what-is-incremental-completing-read-">Incremental Completing Read (ICR)</a> viable as a primary interaction pattern in Emacs.  This post is about the packages that build on that substrate in practice.
</p>

<p>
VOMPECCC is a loose acronym for eight of them that, together, form a complete, modular, Unix-philosophy-aligned completion framework for Emacs: <a href="https://github.com/minad/vertico"><b>V</b>​ertico</a>, <a href="https://github.com/oantolin/orderless"><b>O</b>​rderless</a>, <a href="https://github.com/minad/marginalia"><b>M</b>​arginalia</a>, <a href="https://github.com/radian-software/prescient.el"><b>P</b>​rescient</a>, <a href="https://github.com/oantolin/embark"><b>E</b>​mbark</a>, <a href="https://github.com/minad/consult"><b>C</b>​onsult</a>, <a href="https://github.com/minad/corfu"><b>C</b>​orfu</a>, and <a href="https://github.com/minad/cape"><b>C</b>​ape</a>.  Each package does one thing, and the key attribute of all eight is that they compose through Emacs's standard completion APIs, meaning any subset works without the others.
</p>

<p>
I'm writing this post because these packages have recently taken the Emacs community by storm, but I rarely see discussions on how they relate or how they compose together to provide a feature complete ICR system in emacs.  These packages implement concretely what the antecedent post argues in the abstract: completion is a substrate, or set of primitives, on top of which users can build rich interfaces for effortlessly interacting with your machine to do almost <i>anything</i>.
</p>
</div>
</div>
<div id="outline-container-hidden-complexity" class="outline-2">
<h2 id="hidden-complexity"><span class="section-number-2">2.</span> The Hidden Complexity of Completion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="complexity">complexity</span>&#xa0;<span class="design">design</span></span></h2>
<div class="outline-text-2" id="text-hidden-complexity">
<p>
Even if you've only used Emacs once, you've likely seen its completion features in action. When you press <code>M-x</code> and start typing, a list appears, you pick something, and it runs.  But beneath that interaction lies a system of surprising depth.  Consider what a fully featured completion experience actually requires:
</p>

<p>
<b>Candidate display.</b>  Where do completion candidates appear?  In the minibuffer, vertically?  Horizontally?  In a separate buffer?  In a popup at point?  The display layer determines how you scan and navigate candidates, and of course the optimal display is context dependent.  Switching buffers might want a vertical list; completing a symbol in code might want a popup near the cursor.
</p>

<p>
<b>Filtering.</b>  You can also think of this as 'matching': how does your input <b>match</b> against candidates?  Literal prefix matching is the simplest: <code>find-f</code> matches <code>find-file</code>.  But if we want to add some flexibilty (or 'fuzzy matching'), where for examplke <code>ff</code> matches <code>find-file</code>?  What about splitting your input into multiple components and matching all of them in any order?  What about mixing strategies, for example, where one component matches as a regexp, and another matches as an initialism?  Candidate lists can be huge, so we need this set of features as a sort of query language for filtering the candidate list to find what we're looking for.
</p>

<p>
<b>Sorting.</b>  Once you have your filtered candidates, in what order do you see them?  Alphabetically?  By string length?  By how recently you selected them?  By frequency of use?  A good sorting strategy means the candidate you want is almost always within the first few results.  A bad one means scrolling every time.
</p>

<p>
<b>Annotation.</b>  A bare list of candidate names is often insufficient or unhelpful.  Often, candidates are of a certain 'type' or 'category' and have rich metadata associated with them.  In the <code>M-x</code> example, when selecting a command, you likely want to see its keybinding and docstring.  When selecting a file, you likely want to see its size and modification date.  When selecting a buffer, you want to see its major mode and file path.  Annotations transform a list of strings into a list of <i>informed choices</i>.
</p>

<p>
<b>Actions.</b>  Selecting a candidate (and running some default action) is the most common interaction, but not the only one.  In the <code>find-file</code> example, what if you want to <i>delete</i> the file instead of opening it?  In the <code>M-x</code> example, what if you want to <i>describe</i> the function instead of running it?  A completion system without contextual actions forces you out of the flow: complete, exit, invoke a separate command, etc&#x2026;.
</p>

<p>
<b>In-buffer completion.</b>  Everything above applies to the minibuffer (the prompt at the bottom of the screen).  But completion also happens <i>inside</i> buffers: symbol completion while writing code, dictionary words while writing prose, file paths while editing configuration.  In-buffer completion has its own display requirements (a popup near the cursor, not the minibuffer) and its own backend requirements (language servers, dynamic abbreviations, file system paths).  A truly complete completion system must handle both contexts well.
</p>

<blockquote class="pull-quote pull-right">
<p>
Completion is not one problem.  It is at least six, and most frameworks pretend otherwise.
</p>
</blockquote>


<p>
These six concerns are <i>orthogonal</i>.  The way you display candidates has nothing to do with how you filter them; the way you sort them has nothing to do with what actions you can take, etc&#x2026;.  It's actually a useful thought exercise to go through each of the six concerns and appreciate how each is independent from the others.  A single-package system can deliver an excellent out-of-the-box experience across all of these, and many have (see Ivy and Helm below).  The trade-off is usually that the boundaries between concerns become harder to see, and harder to swap one concern's implementation without disturbing the others.
</p>
</div>
</div>
<div id="outline-container-monolith-problem" class="outline-2">
<h2 id="monolith-problem"><span class="section-number-2">3.</span> The Monolith Era: Helm and Ivy&#xa0;&#xa0;&#xa0;<span class="tag"><span class="helm">helm</span>&#xa0;<span class="ivy">ivy</span>&#xa0;<span class="legacy">legacy</span></span></h2>
<div class="outline-text-2" id="text-monolith-problem">
<p>
For the better part of a decade, two incredible frameworks dominated Emacs completion: Helm and Ivy.  Both were genuinely transformative, because in my opinion, they proved that Emacs's built-in completion experience was inadequate, and they inspired everything that followed.  But both, in retrospect, made the same architectural trade-off: they bundled every concern into a single package with a single API.  I have used both packages extensively, as both a package author and a consumer.  The benefits were immediate for me, but the costs emerged over time.
</p>
</div>
<div id="outline-container-helm-section" class="outline-3">
<h3 id="helm-section"><span class="section-number-3">3.1.</span> Helm: The Kitchen Sink</h3>
<div class="outline-text-3" id="text-helm-section">
<p>
<a href="https://github.com/emacs-helm/helm">Helm</a> traces its lineage to <code>anything.el</code>, created by Tamas Patrovics in 2007.  Thierry Volpiatto, a French alpine guide who taught himself programming after discovering Linux in 2006<sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>, forked it as Helm in 2011 and contributed nearly 7,000 commits over the following decade.  Helm became the most downloaded package on MELPA<sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup> and the default completion framework in Spacemacs, which drove massive adoption during 2013&#x2013;2018.
</p>

<p>
Helm's ambition was impressive but all-encompassing.  It provided its own candidate display, filtering, action system, source API (via EIEIO classes), and dozens of built-in commands for things like file finding, buffer switching, grep, and more&#x2026;.  The actions system was comprehensive too &#x2014; it offered 44+ file actions alone.
</p>

<blockquote class="pull-quote pull-left">
<p>
Helm showed what great completion could feel like.  Its architecture showed what happens when a single maintainer carries every concern alone.
</p>
</blockquote>

<p>
The cost was proportional to Thierry's ambition.  Users reported <a href="https://github.com/emacs-helm/helm/issues/936">multi-second delays</a> on basic operations after extended use, <a href="https://github.com/emacs-helm/helm/issues/1976">100&#x2013;500ms lag</a> on window popups, and CPU-intensive fuzzy matching that required disabling for large projects.  Samuel Barreto's widely cited <a href="https://sam217pa.github.io/2016/09/13/from-helm-to-ivy/">"From Helm to Ivy"</a> essay called Helm "a big behemoth size package" and reported using only a third of its capabilities.
</p>

<p>
Most critically, Helm replaced Emacs's <code>completing-read</code> entirely with its own proprietary <code>helm-source</code> API.  Every Helm extension was written against this API.  <i>None of them could be reused with any other completion system</i>.  That was the helm killer for me: if Helm's development stalled &#x2014; and it did, twice, in <a href="https://github.com/emacs-helm/helm/issues/2083">2018</a> and <a href="https://github.com/emacs-helm/helm/issues/2386">2020</a><sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup> &#x2014; every downstream package would be stranded.
</p>
</div>
</div>
<div id="outline-container-ivy-section" class="outline-3">
<h3 id="ivy-section"><span class="section-number-3">3.2.</span> Ivy: The Lighter Monolith</h3>
<div class="outline-text-3" id="text-ivy-section">
<p>
<a href="https://github.com/abo-abo/swiper">Ivy</a> emerged in 2015 as Oleh Krehel's direct reaction to Helm's complexity.  Where Helm tried to do everything, Oleh aimed to be more minimalist or at least better factored.  The package split its concerns into three logical components: Ivy (the completion UI), Swiper (an isearch replacement), and Counsel (enhanced commands).
</p>

<p>
In practice, the split was cosmetic.  All three lived in a single repository.  Counsel was coupled to Ivy's internals.  And the core architectural choice was the same as Helm's: Ivy defined its own completion API, <code>ivy-read</code>, and Counsel commands called <code>ivy-read</code> directly rather than <code>completing-read</code>.  <b>Code written for Ivy worked only with Ivy.</b>
</p>

<p>
The <code>ivy-read</code> function grew organically to accept roughly 20 arguments with multiple special cases<sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup>.  As the <a href="https://github.com/radian-software/selectrum">Selectrum</a> developers noted: "When Ivy became a more general-purpose interactive selection package, more and more special cases were added to make various commands work properly."  Users reported <a href="https://github.com/abo-abo/swiper/issues/1571">performance degradation</a> after extended use, and Ivy <a href="https://github.com/abo-abo/swiper/issues/3001">broke with Emacs 28 and again with Emacs 30</a>, forcing compatibility polyfills.  This is stressful for not only the consumers of Ivy, but also for the maintainers.
</p>

<p>
When Ivy's original maintainer stepped back, the project entered a period of reduced maintenance.  A new maintainer has since taken over and released version 0.15.1, but active feature development has slowed considerably from the 2016&#x2013;2020 peak.
</p>
</div>
</div>
<div id="outline-container-unix-violation" class="outline-3">
<h3 id="unix-violation"><span class="section-number-3">3.3.</span> The Unix Philosophy Lens</h3>
<div class="outline-text-3" id="text-unix-violation">
<p>
The Unix philosophy, as articulated by Doug McIlroy<sup><a id="fnr.5" class="footref" href="https://www.chiply.dev/#fn.5" role="doc-backlink">5</a></sup>, is straightforward: "Write programs that do one thing and do it well.  Write programs to work together."  Viewed through this lens, both Helm and Ivy bundle too many concerns into packages that communicate through proprietary APIs (<code>helm-source</code>, <code>ivy-read</code>) rather than Emacs's native <code>completing-read</code> contract.  The result is that extensions and backends written for one framework cannot be reused with another, making an investment in either tool non-transferable.
</p>

<p>
None of this diminishes what they achieved, by the way.  I'm personally a huge Helm and Ivy fan and I've build with them and consumed them directly for years.  In my opinion, the legacy of Helm and Ivy is that they showed the community what great completion <i>felt</i> like, and gave a taste of what a fully featured completion system built on the Emacs substrate <i>could be</i>.  The question is whether the architecture that delivered those features is the one we want to build on going forward.
</p>

<p>
The irony is that Emacs already provides the right abstraction.  
</p>

<ul class="org-ul">
<li><code>completing-read</code> is a stable, well-specified API that any UI can render<sup><a id="fnr.6" class="footref" href="https://www.chiply.dev/#fn.6" role="doc-backlink">6</a></sup>.</li>
<li><code>completion-styles</code> is a pluggable system for controlling how input matches candidates<sup><a id="fnr.7" class="footref" href="https://www.chiply.dev/#fn.7" role="doc-backlink">7</a></sup>.</li>
<li><code>completion-at-point-functions</code> is a standard hook for in-buffer completion backends.</li>
</ul>

<p>
The infrastructure for composable completion has existed for years.  It just needed packages that actually used it.
</p>
</div>
</div>
</div>
<div id="outline-container-vompeccc-framework" class="outline-2">
<h2 id="vompeccc-framework"><span class="section-number-2">4.</span> The VOMPECCC Framework&#xa0;&#xa0;&#xa0;<span class="tag"><span class="vompeccc">vompeccc</span>&#xa0;<span class="framework">framework</span></span></h2>
<div class="outline-text-2" id="text-vompeccc-framework">
<p>
VOMPECCC is not a framework in the traditional sense.  There is no single repository, no shared dependency, and no coordinating package.  It is eight independent packages, maintained by three different developers, that compose through Emacs's standard APIs to cover <i>every</i> concern of a complete completion system.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Package</th>
<th scope="col" class="org-left">Concern</th>
<th scope="col" class="org-left">Author</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Vertico</td>
<td class="org-left">Minibuffer display</td>
<td class="org-left"><a href="https://github.com/minad">Daniel Mendler</a></td>
</tr>

<tr>
<td class="org-left">Orderless</td>
<td class="org-left">Filtering / matching</td>
<td class="org-left"><a href="https://github.com/oantolin">Omar Antolin Camarena</a> &amp; Daniel Mendler</td>
</tr>

<tr>
<td class="org-left">Marginalia</td>
<td class="org-left">Candidate annotations</td>
<td class="org-left">Omar Antolin Camarena &amp; Daniel Mendler</td>
</tr>

<tr>
<td class="org-left">Prescient</td>
<td class="org-left">Sorting / ranking</td>
<td class="org-left"><a href="https://github.com/radian-software">Radon Rosborough</a></td>
</tr>

<tr>
<td class="org-left">Embark</td>
<td class="org-left">Contextual actions</td>
<td class="org-left">Omar Antolin Camarena</td>
</tr>

<tr>
<td class="org-left">Consult</td>
<td class="org-left">Enhanced commands</td>
<td class="org-left">Daniel Mendler</td>
</tr>

<tr>
<td class="org-left">Corfu</td>
<td class="org-left">In-buffer display</td>
<td class="org-left">Daniel Mendler</td>
</tr>

<tr>
<td class="org-left">Cape</td>
<td class="org-left">In-buffer backends</td>
<td class="org-left">Daniel Mendler</td>
</tr>
</tbody>
</table>

<p>
The architecture maps cleanly onto the six concerns identified earlier:
</p>

<pre>
                Minibuffer                     Buffer
                ----------                     ------
Display:        Vertico                        Corfu
Filtering:      Orderless          (shared across both)
Sorting:        Prescient          (shared across both)
Annotations:    Marginalia         (shared across both)
Actions:        Embark             (shared across both)
Backends:       Consult                        Cape
</pre>

<p>
Each package targets a single layer, and they all communicate through standard Emacs APIs: <code>completing-read</code>, <code>completion-styles</code>, <code>completion-at-point-functions</code>, annotation functions, and keymaps.  No package knows about the others' internals ‼️, and because of this all of them can be replaced without affecting the rest.
</p>
</div>
</div>
<div id="outline-container-vertico" class="outline-2">
<h2 id="vertico"><span class="section-number-2">5.</span> Vertico: The Display Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="vertico">vertico</span>&#xa0;<span class="display">display</span></span></h2>
<div class="outline-text-2" id="text-vertico">
<p>
<a href="https://github.com/minad/vertico">Vertico</a> (VERTical Interactive COmpletion) provides a vertical candidate list in the minibuffer.  It is roughly 600 lines of code, excluding its extensions.
</p>

<p>
Vertico's defining characteristic is strict adherence to the <code>completing-read</code> contract.  It doesn't filter candidates (that's your completion style's job).  It doesn't sort them (that's your sorting function's job).  It doesn't annotate them (that's your annotation function's job).  It <i>just displays</i> them.  Any command that calls <code>completing-read</code>, whether built-in or third-party, automatically gets Vertico's UI with zero configuration.
</p>

<p>
If you think 1 package for display is overkill, like I originally did before migrating to VOMPECCC, keep reading.
</p>

<p>
Vertico ships with <i>13</i> built-in extensions that modify the display behavior:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Extension</th>
<th scope="col" class="org-left">Effect</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>vertico-buffer</code></td>
<td class="org-left">Display in a regular buffer instead of the minibuffer</td>
</tr>

<tr>
<td class="org-left"><code>vertico-directory</code></td>
<td class="org-left">Ido-like directory navigation (backspace deletes path components)</td>
</tr>

<tr>
<td class="org-left"><code>vertico-flat</code></td>
<td class="org-left">Horizontal, flat display</td>
</tr>

<tr>
<td class="org-left"><code>vertico-grid</code></td>
<td class="org-left">Grid layout</td>
</tr>

<tr>
<td class="org-left"><code>vertico-indexed</code></td>
<td class="org-left">Select candidates by numeric prefix argument</td>
</tr>

<tr>
<td class="org-left"><code>vertico-mouse</code></td>
<td class="org-left">Mouse scrolling and selection</td>
</tr>

<tr>
<td class="org-left"><code>vertico-multiform</code></td>
<td class="org-left">Per-command or per-category display configuration</td>
</tr>

<tr>
<td class="org-left"><code>vertico-quick</code></td>
<td class="org-left">Avy-style quick selection keys</td>
</tr>

<tr>
<td class="org-left"><code>vertico-repeat</code></td>
<td class="org-left">Repeat last completion session</td>
</tr>

<tr>
<td class="org-left"><code>vertico-reverse</code></td>
<td class="org-left">Bottom-to-top display</td>
</tr>

<tr>
<td class="org-left"><code>vertico-suspend</code></td>
<td class="org-left">Suspend and restore completion sessions</td>
</tr>

<tr>
<td class="org-left"><code>vertico-unobtrusive</code></td>
<td class="org-left">Show only a single candidate</td>
</tr>
</tbody>
</table>

<p>
The <code>vertico-multiform</code> extension is particularly worth configuring: it lets you set per-command display modes, so <code>consult-line</code> can open in a full buffer while <code>M-x</code> stays in the minibuffer.
</p>

<p>
<b>Created:</b> April 2021.  <b>Stars:</b> ~1,800.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-orderless" class="outline-2">
<h2 id="orderless"><span class="section-number-2">6.</span> Orderless: The Filtering Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="orderless">orderless</span>&#xa0;<span class="filtering">filtering</span></span></h2>
<div class="outline-text-2" id="text-orderless">
<p>
<a href="https://github.com/oantolin/orderless">Orderless</a> is a <i>completion style</i> &#x2014; it plugs into Emacs's <code>completion-styles</code> variable, the standard mechanism for controlling how user input is matched against candidates.  Where built-in styles like <code>basic</code> require prefix matching and <code>flex</code> does single-pattern fuzzy matching, Orderless splits your input into space-separated components and matches candidates that contain <i>all</i> components in <i>any</i> order (Orderless reveals its namesake 😜).
</p>

<p>
Each component can independently use a different matching method:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Style</th>
<th scope="col" class="org-left">Example</th>
<th scope="col" class="org-left">Matches</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>orderless-literal</code></td>
<td class="org-left"><code>buffer</code></td>
<td class="org-left"><code>switch-to-buffer</code></td>
</tr>

<tr>
<td class="org-left"><code>orderless-regexp</code></td>
<td class="org-left"><code>^con.*mode$</code></td>
<td class="org-left"><code>conf-mode</code></td>
</tr>

<tr>
<td class="org-left"><code>orderless-initialism</code></td>
<td class="org-left"><code>stb</code></td>
<td class="org-left"><code>switch-to-buffer</code></td>
</tr>

<tr>
<td class="org-left"><code>orderless-flex</code></td>
<td class="org-left"><code>stbf</code></td>
<td class="org-left"><code>switch-to-buffer</code></td>
</tr>

<tr>
<td class="org-left"><code>orderless-prefixes</code></td>
<td class="org-left"><code>s-t-b</code></td>
<td class="org-left"><code>switch-to-buffer</code></td>
</tr>

<tr>
<td class="org-left"><code>orderless-literal-prefix</code></td>
<td class="org-left"><code>swi</code></td>
<td class="org-left"><code>switch-to-buffer</code></td>
</tr>
</tbody>
</table>

<p>
Style dispatchers let you select a matching method per component using affix characters: <code>=</code>​= for literal, <code>~</code> for flex, <code>,</code> for initialism, <code>!</code> for negation, <code>&amp;</code> to match annotations.  The system is fully extensible.
</p>

<p>
The typical configuration sets <code>completion-styles</code> to <code>'(orderless basic)</code>, with <code>partial-completion</code> for the <code>file</code> category so that <code>~/d/s</code> expands path components like <code>~/Documents/src</code>.  The fallback to <code>basic</code> is deliberate: some Emacs features (TRAMP hostname completion, dynamic completion tables) require a prefix-matching style.
</p>

<p>
Let's keep beating the dead horse of this post's theme: because Orderless is a standard completion style, it works with <i>any</i> completion UI that uses Emacs's <code>completing-read</code> API: Vertico, Icomplete, the default <code>*Completions*</code> buffer, and even the minibuffer in Emacs's stock configuration.
</p>

<p>
<b>Quick timeout: for readers getting to this point thinking "Wow Vertico plus Orderless is a power stack, let's keep stacking", you certainly can see things this way, but instead, I encourage you to consider what it would be like to use each package <i>without</i> the others.  That will give you a better understanding of how the consituent stars in the VOMPECCC constellation behave independently.  And that's the long term ROI you'll get from VOMPECCC.  The independence is what makes stacking safe and fortuitous, but it doesn't make it necessary.</b>
</p>

<p>
<b>Created:</b> April 2020.  <b>Stars:</b> ~979.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-marginalia" class="outline-2">
<h2 id="marginalia"><span class="section-number-2">7.</span> Marginalia: The Annotation Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marginalia">marginalia</span>&#xa0;<span class="annotations">annotations</span></span></h2>
<div class="outline-text-2" id="text-marginalia">
<p>
<a href="https://github.com/minad/marginalia">Marginalia</a> adds contextual annotations to minibuffer completion candidates.  The name refers to notes written in the margins of books, and here it means metadata displayed alongside each candidate.
</p>

<p>
Marginalia detects the <i>category</i> of the current completion (files, commands, variables, faces, buffers, bookmarks, packages, etc.) and selects an appropriate annotator function.  The detection works through two mechanisms: <code>marginalia-classify-by-command-name</code> (lookup table keyed by calling command) and <code>marginalia-classify-by-prompt</code> (regex matching against the minibuffer prompt text).
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Category</th>
<th scope="col" class="org-left">Annotations shown</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Command</td>
<td class="org-left">Keybinding, docstring summary</td>
</tr>

<tr>
<td class="org-left">File</td>
<td class="org-left">Size, modification date, permissions</td>
</tr>

<tr>
<td class="org-left">Variable</td>
<td class="org-left">Current value, docstring</td>
</tr>

<tr>
<td class="org-left">Face</td>
<td class="org-left">Preview of the face styling</td>
</tr>

<tr>
<td class="org-left">Symbol</td>
<td class="org-left">Class indicator (v/f/c), docstring</td>
</tr>

<tr>
<td class="org-left">Buffer</td>
<td class="org-left">Mode, size, file path</td>
</tr>

<tr>
<td class="org-left">Bookmark</td>
<td class="org-left">Type, target location</td>
</tr>

<tr>
<td class="org-left">Package</td>
<td class="org-left">Version, status, description</td>
</tr>
</tbody>
</table>

<p>
<code>marginalia-cycle</code> (typically bound to <code>M-A</code>) lets you cycle between annotation levels: detailed, abbreviated, or disabled entirely.  This is useful when annotations are consuming screen space during narrow completions.
</p>

<p>
Marginalia hooks into Emacs's <code>annotation-function</code> and <code>affixation-function</code> properties in completion metadata.  Sorry again to the dead horse I've been wailing on, but yes, this means Marginalia works with any completion UI that respects these properties.  It is the framework-agnostic successor to <a href="https://github.com/Yevgnen/ivy-rich">ivy-rich</a><sup><a id="fnr.8" class="footref" href="https://www.chiply.dev/#fn.8" role="doc-backlink">8</a></sup>, which provided similar annotations but was Ivy-specific.  It's cool to see Oleh and Thierry's visions carry on in these packages!
</p>

<p>
This was a mind blower to me when I discovered it - one subtle but consequential effect of using Marginalia is that the annotations themselves become <i>searchable</i>.  Combined with Orderless's <code>&amp;</code> style dispatcher, your input can match against annotation text as well as candidate names: running <code>M-x</code> and typing <code>window &amp;frame</code> narrows to commands whose name contains "window" <i>and</i> whose docstring contains "frame".  The search/matching space extends beyond candidate identifiers into candidate metadata, which is an <i>unusually large leverage gain for what feels like a cosmetic layer</i>.  You are no longer constrained to remembering exact names (🤯); you can reach for commands, files, or buffers by properties that were previously invisible to your completion input.  This helps to solve for cases where you have a ICR UI, but you don't know exactly what you're looking for.  It can also be used to help you 'browse' candidates based on their characteristics as opposed to their names.  Honestly my favorite feature of any of the VOMPECCC packages.
</p>

<p>
<b>Created:</b> December 2020.  <b>Stars:</b> ~919.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-prescient" class="outline-2">
<h2 id="prescient"><span class="section-number-2">8.</span> Prescient: The Sorting Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="prescient">prescient</span>&#xa0;<span class="sorting">sorting</span></span></h2>
<div class="outline-text-2" id="text-prescient">
<p>
<a href="https://github.com/radian-software/prescient.el">Prescient</a> provides intelligent sorting and filtering of completion candidates based on recency and frequency of use.  The portmanteau <i>frecency</i> captures the combined metric that drives the ranking.  
</p>

<p>
Orderless and Prescient are often confused with one another: the difference is that while Orderless answers "which candidates match?", Prescient answers "in what order should they appear?"
</p>

<p>
The sorting is hierarchical:
</p>

<ol class="org-ol">
<li><b>Recency</b> &#x2014; most recently selected candidates appear first</li>
<li><b>Frequency</b> &#x2014; frequently selected candidates next, with scores that decay over time</li>
<li><b>Length</b> &#x2014; remaining candidates sorted by string length (shorter first)</li>
</ol>

<p>
Usage statistics persist across Emacs sessions via <code>prescient-persist-mode</code>, which writes to a save file.  This means Prescient learns your habits: if you frequently run <code>magit-status</code> from <code>M-x</code>, it surfaces near the top after a few uses, regardless of where it falls alphabetically.
</p>

<p>
Prescient ships as a core library plus framework-specific adapters &#x2014; <code>vertico-prescient</code> and <code>corfu-prescient</code> being the relevant ones for VOMPECCC.  A key architectural insight is that both Vertico and Corfu work seamlessly with Prescient.
</p>

<p>
A common and powerful configuration combines Orderless for filtering with Prescient for sorting.  Among the candidates filtered by Orderless, the most recent and frequent ones get promoted to the top.
</p>

<p>
Prescient also provides its own filtering methods (literal, regexp, initialism, fuzzy, prefix, anchored) with on-the-fly toggling via <code>M-s</code> prefix commands.  However, I personally prefer Orderless for filtering and use Prescient purely for its sorting intelligence.  I sort of act as if Prescient was cohesive in this way, rather than giving it responsibility for 2 orthogonal features.
</p>

<p>
<b>Created:</b> August 2017.  <b>Stars:</b> ~695.  <b>Available on:</b> MELPA.
</p>
</div>
</div>
<div id="outline-container-embark" class="outline-2">
<h2 id="embark"><span class="section-number-2">9.</span> Embark: The Action Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="actions">actions</span></span></h2>
<div class="outline-text-2" id="text-embark">
<p>
<a href="https://github.com/oantolin/embark">Embark</a> provides a framework for performing context-aware actions on "targets" &#x2014; the thing at point or the current completion candidate.  Think of it as a keyboard-driven right-click context menu that works everywhere in Emacs: in the minibuffer and in normal buffers.
</p>

<p>
The core command is <code>embark-act</code>.  When invoked, Embark determines the type of the target (file, buffer, URL, symbol, command, etc.) and opens a keymap of single-letter actions appropriate to that type:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Target</th>
<th scope="col" class="org-left">Example actions</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">File</td>
<td class="org-left">Open, delete, copy, rename, byte-compile, open as root</td>
</tr>

<tr>
<td class="org-left">Buffer</td>
<td class="org-left">Switch to, kill, bury, open in other window</td>
</tr>

<tr>
<td class="org-left">URL</td>
<td class="org-left">Browse, download, copy</td>
</tr>

<tr>
<td class="org-left">Symbol</td>
<td class="org-left">Describe, find definition, find references</td>
</tr>

<tr>
<td class="org-left">Package</td>
<td class="org-left">Install, delete, describe, browse homepage</td>
</tr>
</tbody>
</table>

<p>
There are over 100 preconfigured actions across all target types.
</p>

<p>
Beyond <code>embark-act</code>, Embark provides several other capabilities:
</p>

<ul class="org-ul">
<li><b><code>embark-dwim</code></b> runs the default action without showing the menu</li>
<li><b><code>embark-act-all</code></b> applies the same action to every current candidate (e.g., kill all matching buffers)</li>
<li><b><code>embark-collect</code></b> snapshots current candidates into a persistent buffer</li>
<li><b><code>embark-live</code></b> creates a live-updating collection that refreshes as you type</li>
<li><b><code>embark-export</code></b> exports candidates into the appropriate Emacs major mode: file candidates become a Dired buffer, grep results become a grep-mode buffer (editable with <code>wgrep</code>), buffer candidates become an Ibuffer buffer</li>
<li><b><code>embark-become</code></b> switches to a different command mid-stream, transferring your input</li>
</ul>

<p>
Two of these deserve special attention, because they change what a completion session <i>is</i>.
</p>

<p>
<b><code>embark-collect</code></b> freezes the current candidate set into a standalone buffer that persists after the minibuffer exits.  This converts an ephemeral interaction (browse, pick, leave) into something durable (collect, hand off, revisit later).  The collected buffer remains an Embark target, so the same keymap of actions applies to each entry.  It is the right tool when the candidate list <i>itself</i> is the useful artifact: a shortlist of files to process, a set of buffers you want to act on later, a reference you want to keep open on the side.
</p>

<p>
<b><code>embark-export</code></b> goes one step further: instead of a generic candidate buffer, it materializes a buffer in the <i>native major mode appropriate to the candidate type</i>.  File candidates become a Dired buffer, with Dired's decades of filesystem operations available.  Grep-style candidates become a grep-mode buffer that <code>wgrep</code> can turn into a multi-file editing session, buffer candidates become Ibuffer, package candidates become the package menu, etc&#x2026;.  Each export targets a major mode purpose-built for the candidate type, so you end up inside the tool that was already the best one for the job, arrived at on demand, from a completion prompt, with no navigation overhead.  Few interaction patterns in computing convert <i>generic</i> into <i>specialized</i> this cleanly.
</p>

<blockquote class="pull-quote pull-right">
<p>
Embark is a difference of kind, not quantity, compared to Helm and Ivy's action systems &#x2014; because it works everywhere, across all types of objects<sup><a id="fnr.9" class="footref" href="https://www.chiply.dev/#fn.9" role="doc-backlink">9</a></sup>.
</p>
</blockquote>

<p>
Using embark and consult together we can see a canonical example of this pattern: exporting <code>consult-ripgrep</code> results gives you a <code>wgrep</code>-editable grep buffer, so the workflow &#x2014; search with Consult, export with Embark, edit with wgrep &#x2014; compounds three independent packages into a multi-file refactor tool without any of them knowing about the others.
</p>

<p>
<b>Created:</b> May 2020.  <b>Stars:</b> ~1,200.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-consult" class="outline-2">
<h2 id="consult"><span class="section-number-2">10.</span> Consult: The Command Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="consult">consult</span>&#xa0;<span class="commands">commands</span></span></h2>
<div class="outline-text-2" id="text-consult">
<p>
<a href="https://github.com/minad/consult">Consult</a> provides 50+ enhanced commands built on <code>completing-read</code>.  It is the spiritual successor to Counsel (from the Ivy ecosystem) but designed to work with <i>any</i> completion UI.  Where Counsel called <code>ivy-read</code> directly, Consult uses the native contract, which means its commands work with Vertico, Icomplete, fido-mode, or even Emacs's default completion buffer.
</p>

<p>
Consult's commands span several categories:
</p>

<p>
<b>Search:</b>
</p>
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Command</th>
<th scope="col" class="org-left">Purpose</th>
<th scope="col" class="org-left">Replaces</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>consult-line</code></td>
<td class="org-left">Search lines in current buffer</td>
<td class="org-left">Swiper</td>
</tr>

<tr>
<td class="org-left"><code>consult-line-multi</code></td>
<td class="org-left">Search across multiple buffers</td>
<td class="org-left">Swiper-all</td>
</tr>

<tr>
<td class="org-left"><code>consult-ripgrep</code></td>
<td class="org-left">Async ripgrep search</td>
<td class="org-left"><code>counsel-rg</code></td>
</tr>

<tr>
<td class="org-left"><code>consult-grep</code></td>
<td class="org-left">Async grep search</td>
<td class="org-left"><code>counsel-grep</code></td>
</tr>

<tr>
<td class="org-left"><code>consult-git-grep</code></td>
<td class="org-left">Git-aware grep</td>
<td class="org-left"><code>counsel-git-grep</code></td>
</tr>

<tr>
<td class="org-left"><code>consult-find</code></td>
<td class="org-left">Async file finding</td>
<td class="org-left"><code>counsel-find</code></td>
</tr>
</tbody>
</table>

<p>
<b>Navigation:</b>
</p>
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Command</th>
<th scope="col" class="org-left">Purpose</th>
<th scope="col" class="org-left">Replaces</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>consult-buffer</code></td>
<td class="org-left">Enhanced buffer switching</td>
<td class="org-left"><code>helm-mini</code></td>
</tr>

<tr>
<td class="org-left"><code>consult-imenu</code></td>
<td class="org-left">Flat imenu with grouping</td>
<td class="org-left"><code>helm-imenu</code></td>
</tr>

<tr>
<td class="org-left"><code>consult-outline</code></td>
<td class="org-left">Navigate headings with preview</td>
<td class="org-left">Built-in</td>
</tr>

<tr>
<td class="org-left"><code>consult-goto-line</code></td>
<td class="org-left">Goto line with live preview</td>
<td class="org-left">Built-in</td>
</tr>

<tr>
<td class="org-left"><code>consult-bookmark</code></td>
<td class="org-left">Enhanced bookmark selection</td>
<td class="org-left">Built-in</td>
</tr>

<tr>
<td class="org-left"><code>consult-recent-file</code></td>
<td class="org-left">Recent file selection with preview</td>
<td class="org-left"><code>counsel-recentf</code></td>
</tr>
</tbody>
</table>

<p>
<b>Editing and miscellaneous:</b>
</p>
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Command</th>
<th scope="col" class="org-left">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>consult-yank-from-kill-ring</code></td>
<td class="org-left">Browse kill ring interactively</td>
</tr>

<tr>
<td class="org-left"><code>consult-theme</code></td>
<td class="org-left">Preview themes before applying</td>
</tr>

<tr>
<td class="org-left"><code>consult-man</code></td>
<td class="org-left">Async man page lookup</td>
</tr>

<tr>
<td class="org-left"><code>consult-flymake</code></td>
<td class="org-left">Navigate Flymake diagnostics</td>
</tr>

<tr>
<td class="org-left"><code>consult-org-heading</code></td>
<td class="org-left">Navigate org headings</td>
</tr>
</tbody>
</table>

<p>
Three features make Consult particularly powerful:
</p>

<p>
<b>Live preview:</b> Most commands show a real-time preview as you navigate candidates.  <code>consult-line</code> highlights the matching line in the buffer.  <code>consult-theme</code> applies the theme before you select it.  <code>consult-goto-line</code> scrolls to the line as you type the number.
</p>

<p>
<b>Narrowing and grouping:</b> <code>consult-buffer</code> combines buffers, recent files, bookmarks, and project items into a single unified list.  Narrowing keys filter to a single source: <code>b</code> SPC for buffers, <code>f</code> SPC for files, <code>m</code> SPC for bookmarks.  Custom sources can be added via <code>consult-buffer-sources</code>.
</p>

<p>
<b>Two-level async filtering:</b> Commands like <code>consult-ripgrep</code> split input at <code>#</code>: everything before it goes to the external tool as the search pattern, everything after it filters the results locally with your completion style.  <code>error#handler</code> searches for "error" with ripgrep, then narrows to results containing "handler" using Orderless.  Async support is an enormously important feature, because it makes the cognitive cost of search roughly constant with respect to the size of the search space.
</p>

<p>
<b>Created:</b> November 2020.  <b>Stars:</b> ~1,600.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-corfu" class="outline-2">
<h2 id="corfu"><span class="section-number-2">11.</span> Corfu: The In-Buffer Display Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="corfu">corfu</span>&#xa0;<span class="completion">completion</span></span></h2>
<div class="outline-text-2" id="text-corfu">
<p>
<a href="https://github.com/minad/corfu">Corfu</a> (COmpletion in Region FUnction) is simply the in-buffer counterpart to Vertico.  Where Vertico handles minibuffer completion display, Corfu handles the popup that appears at point when you complete a symbol while writing code or text.  It is roughly 1,220 lines of code.
</p>

<p>
Corfu's defining architectural choice mirrors Vertico's: it hooks into Emacs's built-in <code>completion-in-region</code> mechanism rather than inventing its own backend system.  Any mode that provides a <code>completion-at-point-function</code> (Eglot, Tree-sitter, elisp-mode, etc.) works with Corfu automatically.  Any <code>completion-style</code> (basic, partial-completion, orderless) can be used for filtering.
</p>

<p>
This is the fundamental difference from <a href="https://github.com/company-mode/company-mode">Company</a>, the incumbent in-buffer completion framework<sup><a id="fnr.10" class="footref" href="https://www.chiply.dev/#fn.10" role="doc-backlink">10</a></sup>.  Company uses its own proprietary <code>company-backends</code> API.  Company backends don't work with <code>completion-at-point</code>, and Capfs don't work with Company (without an adapter).  Anecdotally, I've had many wrestling matches with Company and always found it incredibly difficult to set up properly.  Corfu eliminates this split.  Doom Emacs recognized this: Company is now deprecated in Doom in favor of Corfu, with plans to remove it post-v3<sup><a id="fnr.11" class="footref" href="https://www.chiply.dev/#fn.11" role="doc-backlink">11</a></sup>.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Aspect</th>
<th scope="col" class="org-left">Company</th>
<th scope="col" class="org-left">Corfu</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Backend system</td>
<td class="org-left">Proprietary</td>
<td class="org-left">Emacs-native Capfs</td>
</tr>

<tr>
<td class="org-left">Popup technology</td>
<td class="org-left">Overlays</td>
<td class="org-left">Child frames</td>
</tr>

<tr>
<td class="org-left">Completion styles</td>
<td class="org-left">Limited</td>
<td class="org-left">Any Emacs style</td>
</tr>

<tr>
<td class="org-left">Codebase size</td>
<td class="org-left">Many files, 3,900+ LOC in main file</td>
<td class="org-left">Single file, ~1,220 LOC</td>
</tr>

<tr>
<td class="org-left">Created</td>
<td class="org-left">2009</td>
<td class="org-left">2021</td>
</tr>
</tbody>
</table>

<p>
Corfu ships with seven built-in extensions:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Extension</th>
<th scope="col" class="org-left">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>corfu-echo</code></td>
<td class="org-left">Brief candidate documentation in the echo area</td>
</tr>

<tr>
<td class="org-left"><code>corfu-history</code></td>
<td class="org-left">Sort by selection history/frequency</td>
</tr>

<tr>
<td class="org-left"><code>corfu-indexed</code></td>
<td class="org-left">Select candidates by numeric prefix argument</td>
</tr>

<tr>
<td class="org-left"><code>corfu-info</code></td>
<td class="org-left">Access candidate location and documentation</td>
</tr>

<tr>
<td class="org-left"><code>corfu-popupinfo</code></td>
<td class="org-left">Documentation popup adjacent to the completion menu</td>
</tr>

<tr>
<td class="org-left"><code>corfu-quick</code></td>
<td class="org-left">Avy-style quick key selection</td>
</tr>
</tbody>
</table>

<p>
<b>Created:</b> April 2021.  <b>Stars:</b> ~1,400.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-cape" class="outline-2">
<h2 id="cape"><span class="section-number-2">12.</span> Cape: The In-Buffer Backend Layer&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cape">cape</span>&#xa0;<span class="backends">backends</span></span></h2>
<div class="outline-text-2" id="text-cape">
<p>
<a href="https://github.com/minad/cape">Cape</a> (Completion At Point Extensions) provides a collection of modular completion backends (Capfs) and a powerful set of Capf transformers for composing and adapting them.  If Corfu is the frontend (how completions are displayed), Cape is the backend toolkit (what completions are available).
</p>

<p>
Cape provides 13 completion backends, here are some highlights:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Capf</th>
<th scope="col" class="org-left">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>cape-dabbrev</code></td>
<td class="org-left">Dynamic abbreviation from current buffers</td>
</tr>

<tr>
<td class="org-left"><code>cape-file</code></td>
<td class="org-left">File path completion</td>
</tr>

<tr>
<td class="org-left"><code>cape-elisp-block</code></td>
<td class="org-left">Elisp completion inside Org/Markdown blocks</td>
</tr>

<tr>
<td class="org-left"><code>cape-keyword</code></td>
<td class="org-left">Programming language keyword completion</td>
</tr>

<tr>
<td class="org-left"><code>cape-history</code></td>
<td class="org-left">History completion in Eshell/Comint</td>
</tr>
</tbody>
</table>

<p>
The remaining backends cover dictionary words, emoji, abbreviations, line completion, and Unicode input via TeX, SGML, and RFC 1345 mnemonics.
</p>

<p>
Cape's Capf transformers are higher-order functions that wrap and modify backends:
</p>

<ul class="org-ul">
<li><b><code>cape-capf-super</code></b> merges multiple Capfs into a single unified source</li>
<li><b><code>cape-capf-case-fold</code></b> adds case-insensitive matching</li>
<li><b><code>cape-capf-inside-code</code></b> / <b><code>cape-capf-inside-string</code></b> / <b><code>cape-capf-inside-comment</code></b> restrict activation to specific syntactic regions</li>
<li><b><code>cape-capf-prefix-length</code></b> requires a minimum prefix before activating</li>
<li><b><code>cape-capf-predicate</code></b> filters candidates with a custom predicate</li>
<li><b><code>cape-capf-sort</code></b> applies custom sorting</li>
</ul>

<p>
The <b><code>cape-company-to-capf</code></b> adapter converts any Company backend into a standard Capf, without requiring Company to be installed.  This bridges the two ecosystems: you can use Company-era backends (like <code>company-yasnippet</code>) with Corfu.  I don't personally do this, but you can if you want!
</p>

<p>
<b>Created:</b> November 2021.  <b>Stars:</b> ~760.  <b>Available on:</b> GNU ELPA.
</p>
</div>
</div>
<div id="outline-container-subset-property" class="outline-2">
<h2 id="subset-property"><span class="section-number-2">13.</span> The Subset Property: Use What You Want&#xa0;&#xa0;&#xa0;<span class="tag"><span class="modularity">modularity</span>&#xa0;<span class="flexibility">flexibility</span></span></h2>
<div class="outline-text-2" id="text-subset-property">
<p>
The most important property of VOMPECCC is that you don't need to buy into all eight packages.  You can start with one, add another when you feel a gap, and swap any component for an alternative without breaking anything else.  
</p>

<p>
If you're into inverting dependencies, VOMPECCC is your bag, man.
</p>

<p>
This works because every package communicates through the native Emacs APIs rather than depending on each other's internals.  There are no hard dependencies between any of the eight packages.  Here is a map of what each package can be replaced with &#x2014; or simply omitted:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Package</th>
<th scope="col" class="org-left">Alternative</th>
<th scope="col" class="org-left">Or simply&#x2026;</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Vertico</td>
<td class="org-left">Icomplete-vertical, Mct, Ido, fido-mode</td>
<td class="org-left">Default <code>*Completions*</code> buffer</td>
</tr>

<tr>
<td class="org-left">Orderless</td>
<td class="org-left">Hotfuzz, Fussy, Prescient (filtering mode)</td>
<td class="org-left">Built-in <code>flex</code> or <code>substring</code></td>
</tr>

<tr>
<td class="org-left">Marginalia</td>
<td class="org-left">(none equivalent)</td>
<td class="org-left">No annotations (still works)</td>
</tr>

<tr>
<td class="org-left">Prescient</td>
<td class="org-left"><code>savehist-mode</code> + <code>vertico-sort-override</code></td>
<td class="org-left">Alphabetical sorting</td>
</tr>

<tr>
<td class="org-left">Embark</td>
<td class="org-left">(none equivalent)</td>
<td class="org-left">Direct command invocation</td>
</tr>

<tr>
<td class="org-left">Consult</td>
<td class="org-left">Built-in <code>switch-to-buffer</code>, <code>grep</code>, etc.</td>
<td class="org-left">Standard Emacs commands</td>
</tr>

<tr>
<td class="org-left">Corfu</td>
<td class="org-left">Company, <code>completion-preview-mode</code></td>
<td class="org-left">Default <code>*Completions*</code> buffer</td>
</tr>

<tr>
<td class="org-left">Cape</td>
<td class="org-left">Company backends, <code>hippie-expand</code></td>
<td class="org-left">Mode-provided Capfs</td>
</tr>
</tbody>
</table>

<p>
Some practical subset configurations:
</p>

<p>
<b>Minimal (2 packages):</b> Vertico + Orderless.  You get a vertical candidate list with multi-component matching.  No annotations, no actions, no enhanced commands &#x2014; but a dramatically better <code>M-x</code> and <code>find-file</code> experience than stock Emacs.
</p>

<p>
<b>Comfortable (4 packages):</b> Vertico + Orderless + Marginalia + Consult.  Now you have annotations on every candidate and enhanced commands with live preview.  This is probably the sweet spot for most users.
</p>

<p>
<b>Full stack (8 packages):</b> All of VOMPECCC.  Complete coverage of both minibuffer and in-buffer completion, with intelligent sorting, contextual actions, and modular backends.
</p>

<p>
For concrete configuration, the most reliable starting point is each package's own repository &#x2014; every package linked in the opener ships a comprehensive README with example <code>use-package</code> snippets, and most also provide wikis or info manuals covering more specialized use cases (Vertico's per-command <code>vertico-multiform</code> patterns, Cape's Capf transformer recipes, Embark's keymap customization examples, Consult's custom sources, and so on).  Reading those directly is faster than copying a consolidated configuration and then reverse-engineering what each line does, and it scales better as the packages themselves evolve.
</p>
</div>
</div>
<div id="outline-container-timeline" class="outline-2">
<h2 id="timeline"><span class="section-number-2">14.</span> Growth and Adoption Timeline&#xa0;&#xa0;&#xa0;<span class="tag"><span class="timeline">timeline</span>&#xa0;<span class="adoption">adoption</span></span></h2>
<div class="outline-text-2" id="text-timeline">
<p>
The history of Emacs completion frameworks is a progression from monolithic solutions toward composable ones.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-right" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Year</th>
<th scope="col" class="org-left">Event</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">1996</td>
<td class="org-left">Kim F. Storm begins Ido</td>
</tr>

<tr>
<td class="org-right">2007</td>
<td class="org-left">Ido included in Emacs 22; <code>anything.el</code> created (Helm's ancestor)</td>
</tr>

<tr>
<td class="org-right">2011</td>
<td class="org-left">Volpiatto forks <code>anything.el</code> as Helm</td>
</tr>

<tr>
<td class="org-right">2013&#x2013;2018</td>
<td class="org-left"><b>Helm's golden era:</b> most-downloaded MELPA package, default in Spacemacs</td>
</tr>

<tr>
<td class="org-right">2015</td>
<td class="org-left">Krehel creates Ivy/Swiper/Counsel</td>
</tr>

<tr>
<td class="org-right">2016</td>
<td class="org-left">"From Helm to Ivy" blog post sparks migration; <b>Ivy peaks ~2016&#x2013;2020</b></td>
</tr>

<tr>
<td class="org-right">2017</td>
<td class="org-left">Rosborough creates Prescient</td>
</tr>

<tr>
<td class="org-right">2018</td>
<td class="org-left">Helm enters bug-fix-only mode (maintainer burnout)</td>
</tr>

<tr>
<td class="org-right">2019</td>
<td class="org-left">Rosborough creates Selectrum (first <code>completing-read</code>​-native UI)</td>
</tr>

<tr>
<td class="org-right">2020 Apr</td>
<td class="org-left">Antolin Camarena creates Orderless</td>
</tr>

<tr>
<td class="org-right">2020 May</td>
<td class="org-left">Antolin Camarena creates Embark</td>
</tr>

<tr>
<td class="org-right">2020 Sep</td>
<td class="org-left">Helm development officially stopped</td>
</tr>

<tr>
<td class="org-right">2020 Nov</td>
<td class="org-left">Mendler creates Consult</td>
</tr>

<tr>
<td class="org-right">2020 Dec</td>
<td class="org-left">Antolin Camarena &amp; Mendler create Marginalia</td>
</tr>

<tr>
<td class="org-right">2021 Apr</td>
<td class="org-left">Mendler creates Vertico and Corfu</td>
</tr>

<tr>
<td class="org-right">2021 May</td>
<td class="org-left">"Replacing Ivy and Counsel with Vertico and Consult" (System Crafters)</td>
</tr>

<tr>
<td class="org-right">2021</td>
<td class="org-left">Selectrum deprecated in favor of Vertico; Doom Emacs adds Vertico module</td>
</tr>

<tr>
<td class="org-right">2021 Nov</td>
<td class="org-left">Mendler creates Cape</td>
</tr>

<tr>
<td class="org-right">2022</td>
<td class="org-left">Doom Emacs switches default completion from Ivy to Vertico</td>
</tr>

<tr>
<td class="org-right">2024</td>
<td class="org-left">Ivy breaks with Emacs 30; Company deprecated in Doom in favor of Corfu</td>
</tr>
</tbody>
</table>

<p>
Helm and Ivy accumulated stars over a longer period; the newer packages are growing faster relative to their age (counts as of early 2026):
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Package</th>
<th scope="col" class="org-left">Stars</th>
<th scope="col" class="org-left">Created</th>
<th scope="col" class="org-left">Approx. age</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Helm</td>
<td class="org-left">~3,500</td>
<td class="org-left">2011</td>
<td class="org-left">15 years</td>
</tr>

<tr>
<td class="org-left">Ivy/Swiper</td>
<td class="org-left">~2,400</td>
<td class="org-left">2015</td>
<td class="org-left">11 years</td>
</tr>

<tr>
<td class="org-left">Vertico</td>
<td class="org-left">~1,800</td>
<td class="org-left">April 2021</td>
<td class="org-left">5 years</td>
</tr>

<tr>
<td class="org-left">Consult</td>
<td class="org-left">~1,600</td>
<td class="org-left">Nov 2020</td>
<td class="org-left">5 years</td>
</tr>

<tr>
<td class="org-left">Corfu</td>
<td class="org-left">~1,400</td>
<td class="org-left">April 2021</td>
<td class="org-left">5 years</td>
</tr>

<tr>
<td class="org-left">Embark</td>
<td class="org-left">~1,200</td>
<td class="org-left">May 2020</td>
<td class="org-left">6 years</td>
</tr>

<tr>
<td class="org-left">Orderless</td>
<td class="org-left">~979</td>
<td class="org-left">April 2020</td>
<td class="org-left">6 years</td>
</tr>

<tr>
<td class="org-left">Marginalia</td>
<td class="org-left">~919</td>
<td class="org-left">Dec 2020</td>
<td class="org-left">5 years</td>
</tr>

<tr>
<td class="org-left">Cape</td>
<td class="org-left">~760</td>
<td class="org-left">Nov 2021</td>
<td class="org-left">4 years</td>
</tr>

<tr>
<td class="org-left">Prescient</td>
<td class="org-left">~695</td>
<td class="org-left">Aug 2017</td>
<td class="org-left">9 years</td>
</tr>
</tbody>
</table>

<p>
The community momentum is clear.  Doom Emacs, one of the most popular Emacs distributions, has moved to Vertico + Corfu as its defaults<sup><a id="fnr.12" class="footref" href="https://www.chiply.dev/#fn.12" role="doc-backlink">12</a></sup>.  Modern configuration guides almost universally recommend the modular stack<sup><a id="fnr.13" class="footref" href="https://www.chiply.dev/#fn.13" role="doc-backlink">13</a></sup>.  And the upstream Emacs project itself has been integrating ideas from this ecosystem: Emacs 30 added <code>completion-preview-mode</code>, and Emacs 31 is incorporating Mct-inspired features (they love Prot, and for good reason, lol).
</p>
</div>
</div>
<div id="outline-container-tradeoffs" class="outline-2">
<h2 id="tradeoffs"><span class="section-number-2">15.</span> The Trade-Off: Monolith vs. Composition&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tradeoffs">tradeoffs</span>&#xa0;<span class="analysis">analysis</span></span></h2>
<div class="outline-text-2" id="text-tradeoffs">
<p>
Engineering is about trade-offs.  The modular approach has real advantages, but it does have costs, so I want to be honest about them:
</p>
</div>
<div id="outline-container-advantages" class="outline-3">
<h3 id="advantages"><span class="section-number-3">15.1.</span> Advantages of VOMPECCC</h3>
<div class="outline-text-3" id="text-advantages">
<p>
<b>No vendor lock-in.</b>  Every package builds on the same native contracts.  If any one of the eight packages is abandoned, you replace it.  Your other packages continue to work.  Contrast this with Helm, where the maintainer's burnout announcement stranded an entire ecosystem of downstream packages.
</p>

<p>
<b>Independent maintenance.</b>  Three different developers maintain the eight packages.  Daniel Mendler maintains five (Vertico, Consult, Corfu, Cape, and co-maintains Marginalia), so the overall bus factor is not dramatically higher than a monolith.  But the key difference is structural: if Mendler stepped away, the remaining packages would continue to function independently.  Omar Antolin Camarena's Embark and Orderless would keep working.  Radon Rosborough's Prescient would keep working.  Nobody's contribution is stranded by someone else's absence.
</p>

<p>
<b>Incremental adoption.</b>  You start with one package and add more as you discover needs.  There is no cliff of initial configuration.  You never need to understand all eight before getting value from any one.
</p>

<p>
<b>Smaller, auditable codebases.</b>  Vertico is ~600 lines.  Corfu is ~1,220 lines.  These are packages you can actually read end to end.  Bugs are easier to find and fix in small, focused codebases.
</p>

<p>
<b>Automatic ecosystem benefits.</b>  Because everything uses the native completion protocol, third-party packages benefit for free.  Any command that calls <code>completing-read</code> gets your chosen UI, filtering, sorting, annotations, and actions without any integration code.
</p>

<p>
<b>Future compatibility.</b>  Emacs itself continues to improve its built-in completion system.  Packages built on the native protocol benefit from those improvements automatically.  Packages built on proprietary APIs do not.
</p>
</div>
</div>
<div id="outline-container-disadvantages" class="outline-3">
<h3 id="disadvantages"><span class="section-number-3">15.2.</span> Disadvantages of VOMPECCC</h3>
<div class="outline-text-3" id="text-disadvantages">
<p>
<b>Higher initial discovery cost.</b>  A newcomer searching "Emacs completion" finds eight packages instead of one.  Understanding the role of each, and which subset to start with, requires more research than "install Helm" or "install Ivy."  The conceptual overhead is non-trivial.
</p>

<p>
<b>Configuration across packages.</b>  Eight packages means eight <code>use-package</code> declarations, eight sets of configuration variables, and eight places where something could be misconfigured.  Helm's all-in-one approach means one declaration, one set of variables, one source of truth.
</p>

<p>
<b>Interaction effects.</b>  While the packages are independent, some combinations require awareness of how they interact.  Combining Orderless with Prescient requires understanding that Orderless handles filtering while Prescient handles sorting.  The <code>embark-consult</code> integration package exists because the two packages benefit from knowing about each other in specific workflows.
</p>

<p>
<b>Less out-of-the-box polish.</b>  Helm ships with dozens of purpose-built commands.  With VOMPECCC, you compose those workflows yourself.  The result is often more powerful, but you build it rather than unwrap it.
</p>

<p>
<b>Documentation is distributed.</b>  Each package has its own README, its own issue tracker, its own wiki.  There is no single "VOMPECCC manual."  Cross-cutting workflows (search with Consult, export with Embark, edit with wgrep) are documented across multiple repositories.
</p>
</div>
</div>
<div id="outline-container-when-to-choose" class="outline-3">
<h3 id="when-to-choose"><span class="section-number-3">15.3.</span> When to Choose What</h3>
<div class="outline-text-3" id="text-when-to-choose">
<p>
<b>Choose VOMPECCC if:</b>
</p>
<ul class="org-ul">
<li>You value understanding your tools and want to read the source code</li>
<li>You want completion that works identically with built-in and third-party commands</li>
<li>You want to invest incrementally rather than all at once</li>
<li>You care about long-term maintainability and Emacs version compatibility</li>
<li>You want to mix and match components as your needs evolve</li>
</ul>

<p>
<b>Consider Helm if:</b>
</p>
<ul class="org-ul">
<li>You want maximum out-of-the-box functionality with minimal configuration</li>
<li>You prefer a single point of documentation and support</li>
<li>You are comfortable depending on a single package and its API</li>
<li>You need one of Helm's highly specific, purpose-built features (like <code>helm-top</code> or <code>helm-colors</code>) and don't want to replicate them</li>
<li>You think Thierry is a cool dude (he is)</li>
</ul>

<p>
<b>Consider Ivy if:</b>
</p>
<ul class="org-ul">
<li>You are already invested in the Ivy ecosystem with custom <code>ivy-read</code> code</li>
<li>You prefer Ivy's action selection UX</li>
<li>You need Spacemacs's Ivy layer specifically</li>
<li>You think Oleh is a cool dude (he is)</li>
</ul>

<p>
For new configurations today, the community consensus points strongly toward the modular stack.  Doom Emacs's switch to Vertico and Corfu, the deprecation of Selectrum, and the ongoing maintenance challenges of both Helm and Ivy have made the direction clear.  The question is no longer whether to use the modular approach, but which subset to start with.
</p>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">16.</span> Conclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="conclusion">conclusion</span></span></h2>
<div class="outline-text-2" id="text-conclusion">
<p>
I came to this stack the way most people probably do: one package at a time, over the course of a year or so.  I started with Vertico and Orderless because my Ivy config had started fighting with Emacs 28 upgrades and I was tired of debugging someone else's <code>ivy-read</code> edge cases.  Two packages, ten minutes of configuration, and <code>M-x</code> already felt better.  Marginalia came next for me.  Once you've seen keybindings and docstrings next to every command, you can't unsee their absence.  Consult replaced Counsel, Embark replaced the "type search string, exit completion, run a different command" waltz, and Corfu replaced Company when I realized the same Orderless filtering I'd grown to depend on in the minibuffer wasn't available in my code buffers.
</p>

<p>
The whole migration happened very incrementally, which was incidental for me, but is the point of this post.  I never sat down to "install VOMPECCC."  I solved one friction at a time, and each solution composed with the ones I already had.  That's the experience the architecture is designed to produce.
</p>

<p>
Nobody really calls it VOMPECCC in Emacs circles, it is a mnemonic used here for the sake of an article rather than an established term.  But the packages it describes have quietly become the default recommendation for modern Emacs completion, adopted by Doom Emacs, recommended by Protesilaos Stavrou<sup><a id="fnr.14" class="footref" href="https://www.chiply.dev/#fn.14" role="doc-backlink">14</a></sup>, documented by System Crafters<sup><a id="fnr.15" class="footref" href="https://www.chiply.dev/#fn.15" role="doc-backlink">15</a></sup>, and built on by a growing ecosystem of third-party packages.
</p>

<p>
The shift from Helm to Ivy to the modular stack follows a familiar pattern in software: monoliths are convenient until they aren't.  Composable tools with clear interfaces outlast the frameworks that try to be everything<sup><a id="fnr.16" class="footref" href="https://www.chiply.dev/#fn.16" role="doc-backlink">16</a></sup>.  Emacs figured this out forty years ago, and the modular stack described here is what completion looks like once you treat it as a <i>substrate</i>, the raw material on top of which you build Incremental Completing Read interactions, rather than as a finished product the vendor hands you.  Its completion ecosystem just needed a few years to catch up.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">17.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Emacs completion is not one problem but <a href="https://www.chiply.dev/#hidden-complexity">at least six orthogonal concerns</a>: display, filtering, sorting, annotation, actions, and in-buffer completion. For a decade, <a href="https://www.chiply.dev/#monolith-problem">Helm and Ivy</a> delivered excellent experiences but bundled everything behind proprietary APIs, creating vendor lock-in and maintenance fragility. VOMPECCC names <a href="https://www.chiply.dev/#vompeccc-framework">eight independent packages</a> — <a href="https://www.chiply.dev/#vertico">Vertico</a>, <a href="https://www.chiply.dev/#orderless">Orderless</a>, <a href="https://www.chiply.dev/#marginalia">Marginalia</a>, <a href="https://www.chiply.dev/#prescient">Prescient</a>, <a href="https://www.chiply.dev/#embark">Embark</a>, <a href="https://www.chiply.dev/#consult">Consult</a>, <a href="https://www.chiply.dev/#corfu">Corfu</a>, and <a href="https://www.chiply.dev/#cape">Cape</a> — that each address a single concern and compose through Emacs's native <code>completing-read</code> contract rather than custom APIs. Because no package depends on another's internals, <a href="https://www.chiply.dev/#subset-property">any subset works on its own</a> and any component can be replaced without breaking the rest. The <a href="https://www.chiply.dev/#timeline">community has moved decisively</a> toward this modular stack, with Doom Emacs switching its defaults to Vertico and Corfu. There are <a href="https://www.chiply.dev/#tradeoffs">real trade-offs</a> — higher discovery cost and distributed configuration — but the architecture pays off in durability, auditability, and incremental adoption.
</p>
</div>
</div>
<div id="outline-container-socials" class="outline-2">
<h2 id="socials"><span class="section-number-2">18.</span> socials</h2>
<div class="outline-text-2" id="text-socials">
<ul class="org-ul">
<li><a href="https://www.reddit.com/r/softwarearchitecture/comments/1sn4gnj/vompeccc_a_modular_completion_framework_for_emacs/">r/softwarearchitecture VOMPECCC: A Modular Completion Framework for Emacs</a></li>
<li><a href="https://www.reddit.com/r/programming/comments/1sn4e7z/vompeccc_a_modular_completion_framework_for_emacs/">r/programming VOMPECCC: A Modular Completion Framework for Emacs</a></li>
<li><a href="https://www.reddit.com/r/emacs/comments/1sn4d2d/vompeccc_a_modular_completion_framework_for_emacs/">r/emacs VOMPECCC: A Modular Completion Framework for Emacs</a></li>
<li><a href="https://news.ycombinator.com/item?id=47793138">VOMPECCC: A Modular Completion Framework for Emacs</a></li>
<li><a href="https://www.linkedin.com/feed/update/urn:li:activity:7450544846848708609/">VOMPECCC: A Modular Completion Framework for Emacs</a></li>
</ul>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Sacha Chua's <a href="https://sachachua.com/blog/2018/09/interview-with-thierry-volpiatto/">interview with Thierry Volpiatto</a> (2018) provides a candid account of Helm's history.  Volpiatto describes being a mountain guide with no programming background, discovering Linux in 2006, and gradually becoming Helm's sole maintainer.  He also discusses the financial unsustainability of maintaining a package used by hundreds of thousands of users as a volunteer.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Helm accumulated over 640,000 downloads on MELPA, making it the most downloaded package on the archive at its peak.  MELPA download counts are visible on the <a href="https://melpa.org/#/helm">MELPA package page</a>.  The figure is cumulative since MELPA began tracking downloads in 2013.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Volpiatto's 2020 announcement (<a href="https://github.com/emacs-helm/helm/issues/2386">GitHub Issue #2386</a>) was definitive: "Helm development is now stopped, please don't send bug reports or feature request, you will have no answers."  The issue was locked to collaborators.  The <a href="https://news.ycombinator.com/item?id=24449883">Hacker News discussion</a> that followed highlights the difficulty of sustaining large open-source projects without institutional support.
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The <code>ivy-read</code> signature can be inspected in <a href="https://github.com/abo-abo/swiper/blob/master/ivy.el">ivy.el</a> on GitHub.  The Selectrum README (<a href="https://github.com/radian-software/selectrum">radian-software/selectrum</a>) provides a detailed comparison of <code>ivy-read</code> with <code>completing-read</code> and explains why the deviation from the standard API created long-term maintainability problems.
</p></div></div>

<div class="footdef"><sup><a id="fn.5" class="footnum" href="https://www.chiply.dev/#fnr.5" role="doc-backlink">5</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
McIlroy's articulation of the Unix philosophy appears in the Bell System Technical Journal's 1978 special issue on Unix (<a href="https://archive.org/details/bstj57-6-1899">available at archive.org</a>).  The full quote is: "Make each program do one thing well.  To do a new job, build afresh rather than complicate old programs by adding new 'features'."  See also Eric S. Raymond's <a href="http://www.catb.org/~esr/writings/taoup/html/ch01s06.html">The Art of Unix Programming</a>, Chapter 1, which elaborates on the philosophy's implications for software design.
</p></div></div>

<div class="footdef"><sup><a id="fn.6" class="footnum" href="https://www.chiply.dev/#fnr.6" role="doc-backlink">6</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The <code>completing-read</code> API is documented in the <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Minibuffer-Completion.html">Emacs Lisp Reference Manual</a>.  The key design insight is that <code>completing-read</code> supports <i>programmatic completion tables</i> &#x2014; functions that can compute candidates lazily based on the current input &#x2014; which is essential for large or dynamic candidate sets like TRAMP hosts or LSP symbols.
</p></div></div>

<div class="footdef"><sup><a id="fn.7" class="footnum" href="https://www.chiply.dev/#fnr.7" role="doc-backlink">7</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Emacs's completion styles system is documented in the <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Completion-Styles.html">GNU Emacs Manual</a>.  The variable <code>completion-styles</code> controls which matching strategies are tried, in order, until one produces results.  The <code>completion-category-overrides</code> variable allows per-category customization, so file completion can use <code>partial-completion</code> while <code>M-x</code> uses <code>orderless</code>.
</p></div></div>

<div class="footdef"><sup><a id="fn.8" class="footnum" href="https://www.chiply.dev/#fnr.8" role="doc-backlink">8</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
ivy-rich (<a href="https://github.com/Yevgnen/ivy-rich">Yevgnen/ivy-rich</a>) was a popular Ivy extension that added columns of information to Ivy completion candidates &#x2014; essentially the same concept as Marginalia.  The key limitation was that it was structurally coupled to Ivy: if you switched away from Ivy, you lost your annotations.  Marginalia solves the same problem through the standard <code>annotation-function</code> API, making it framework-agnostic.
</p></div></div>

<div class="footdef"><sup><a id="fn.9" class="footnum" href="https://www.chiply.dev/#fnr.9" role="doc-backlink">9</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This characterization comes from Karthinks's <a href="https://karthinks.com/software/fifteen-ways-to-use-embark/">"Fifteen Ways to Use Embark"</a>, one of the most comprehensive third-party guides to the package.  The post demonstrates workflows that were impossible or impractical before Embark: acting on multiple candidates simultaneously, exporting completion results into native Emacs modes, and switching commands mid-stream without losing context.
</p></div></div>

<div class="footdef"><sup><a id="fn.10" class="footnum" href="https://www.chiply.dev/#fnr.10" role="doc-backlink">10</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Company-mode (<a href="https://github.com/company-mode/company-mode">company-mode/company-mode</a>) was created by Nikolaj Schumacher in 2009 and has been maintained by Dmitry Gutov since 2013.  It remains actively maintained with ~2,300 GitHub stars.  The architectural critique here is specific to the backend API: <code>company-backends</code> is a separate protocol from <code>completion-at-point-functions</code>, which means backends written for Company don't work with other completion UIs, and vice versa.
</p></div></div>

<div class="footdef"><sup><a id="fn.11" class="footnum" href="https://www.chiply.dev/#fnr.11" role="doc-backlink">11</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The Doom Emacs Corfu module was merged in <a href="https://github.com/doomemacs/doomemacs/pull/7002">PR #7002</a> in March 2024.  The <a href="https://discourse.doomemacs.org/t/new-completion-corfu-module/2685">Discourse discussion</a> explains the rationale: Corfu aligns with Emacs's native completion infrastructure, while Company's proprietary API creates friction with the rest of the modern completion stack.
</p></div></div>

<div class="footdef"><sup><a id="fn.12" class="footnum" href="https://www.chiply.dev/#fnr.12" role="doc-backlink">12</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Doom Emacs's completion modules are documented at <a href="https://docs.doomemacs.org/v21.12/modules/completion/vertico/">docs.doomemacs.org</a>.  The Vertico module includes pre-configured integration with Orderless, Marginalia, Consult, and Embark.  The older Ivy and Helm modules remain available but are no longer the recommended default.
</p></div></div>

<div class="footdef"><sup><a id="fn.13" class="footnum" href="https://www.chiply.dev/#fnr.13" role="doc-backlink">13</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Notable guides recommending the modular stack include: Martin Fowler's <a href="https://martinfowler.com/articles/2024-emacs-completion.html">"Improving my Emacs experience with completion"</a> (2024), which documents his switch to the Vertico ecosystem; the <a href="https://jneidel.com/guide/emacs-completion/">"Guide to Modern Emacs Completion"</a> by Jonathan Neidel, which walks through the full Vertico/Corfu stack; and Kristoffer Balintona's multi-part <a href="https://kristofferbalintona.me/posts/202202211546/">"Vertico, Marginalia, All-the-icons-completion, and Orderless"</a> series (2022).
</p></div></div>

<div class="footdef"><sup><a id="fn.14" class="footnum" href="https://www.chiply.dev/#fnr.14" role="doc-backlink">14</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Protesilaos Stavrou's <a href="https://protesilaos.com/codelog/2024-02-17-emacs-modern-minibuffer-packages/">"Emacs: modern minibuffer packages (Vertico, Consult, etc.)"</a> is a ~44 minute video demonstrating the full stack.  Stavrou is also the author of <a href="https://elpa.gnu.org/packages/mct.html">Mct</a> (Minibuffer and Completions in Tandem), an alternative approach that reuses the built-in <code>*Completions*</code> buffer with automatic updates.  His recommendation of Vertico <i>despite</i> having written a competing package speaks to the strength of the ecosystem.
</p></div></div>

<div class="footdef"><sup><a id="fn.15" class="footnum" href="https://www.chiply.dev/#fnr.15" role="doc-backlink">15</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
System Crafters' <a href="https://systemcrafters.net/emacs-tips/streamline-completions-with-vertico/">"Streamline Your Emacs Completions with Vertico"</a> and the companion video "<a href="https://www.youtube.com/watch?v=J0OaRy85MOo">Replacing Ivy and Counsel with Vertico and Consult</a>" (May 2021) were early catalysts for community adoption.  David Wilson (System Crafters) documented his own migration from Ivy and provided configuration examples that became widely copied.
</p></div></div>

<div class="footdef"><sup><a id="fn.16" class="footnum" href="https://www.chiply.dev/#fnr.16" role="doc-backlink">16</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The pattern of monoliths giving way to composable architectures is well-documented in software engineering.  Fred Brooks described the "second system effect" in <i>The Mythical Man-Month</i> (1975), where the follow-up to a successful lean system tends to be an overdesigned monolith.  More recently, the microservices movement explicitly applies the Unix philosophy to distributed systems &#x2014; with <a href="https://martinfowler.com/articles/microservices.html">similar trade-offs</a> around discovery cost, operational complexity, and distributed debugging.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Completion is a Substrate, not a UI</title>
      <link>https://www.chiply.dev/post-icr-primer</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-icr-primer</guid>
      <pubDate>Tue, 14 Apr 2026 12:22:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>ICR is not a convenience feature. It is a structural change in how the cost of an interaction scales with the size of the underlying data.</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="completion">completion</span>&#xa0;<span class="ux">ux</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org6cb083d" class="figure">
<p><img src="https://www.chiply.dev/images/icr-primer-banner.jpeg" alt="icr-primer-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/dall-e-3">DALL-E 3</a></p>
</div>

<blockquote class="pull-quote pull-right">
<p>
ICR is not a convenience feature.  It is a structural change in how the cost of an interaction scales with the size of the underlying data.
</p>
</blockquote>

<p>
The argument I want to make is sharper than it sounds.  Incremental completing read (ICR) is not a convenience feature.  It is a <i>structural</i> change in how the cost of an interaction scales with the size of the underlying data; it is one of the few interface patterns that genuinely respects how human memory works; and it can fortuitously change how you <i>organize</i> your data, not just how you <i>retrieve</i> it.  
</p>

<p>
A brief thought exercise reveals how a surprisingly large fraction of all software &#x2014; email, calendars, file browsers, music players, issue trackers, package managers &#x2014; is, at its core, just two primitives: 1) <i>pick a thing</i>, 2) <i>act on it</i>.  That is the exact shape ICR was built for, and most of the visual chrome we drape around those primitives is decoration.
</p>

<p>
This matters concretely because very few environments expose completion as a <i>programmable substrate</i><sup><a id="fnr.substrate" class="footref" href="https://www.chiply.dev/#fn.substrate" role="doc-backlink">1</a></sup> you can build ICR experiences with, rather than as a sealed UI you can only consume.  In everything else you use, the candidate sources, the matcher, the sorter, the annotator, and the available actions are largely fixed by the vendor or aren't even available.  On the other hand, in Emacs and the shell, every layer is independently replaceable.  Taking your completion stack seriously is among the highest-leverage things an Emacs user can do, on the same scale as customizing your shell, and for the same reasons.  Done right, ICR can dramatically reduce the cognitive overhead of using your computer to do almost <i>anything</i>.
</p>

<p>
This post opens a short series on ICR.  The remaining two posts get concrete: a breakdown of the modular completion framework I use day to day, and a case study of an entire Spotify client that is <i>just</i> an ICR application.  The goal of this opening piece is to convince you that ICR is worth your rigor, and to give you the conceptual vocabulary to recognize how much of your own software experience already runs on it.
</p>
</div>
</div>
<div id="outline-container-what-is-incremental-completing-read-" class="outline-2">
<h2 id="what-is-incremental-completing-read-"><span class="section-number-2">2.</span> What is Incremental Completing Read?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="ICR">ICR</span>&#xa0;<span class="HCI">HCI</span></span></h2>
<div class="outline-text-2" id="text-what-is-incremental-completing-read-">
<p>
"Incremental Completing Read" has three load-bearing words:
</p>

<p>
<b>Read</b>, in the elisp sense: a function that prompts the user and returns a <i>value</i>.  The system asks a question, you answer, and then the answer is something other code can do something with.
</p>

<p>
<b>Completing</b>: the system maintains a candidate set and shows you which candidates currently match your input.  You don't type the full answer.  You type enough to disambiguate, and the system fills in the rest.
</p>

<p>
<b>Incremental</b>: the candidate set is recomputed on every keystroke<sup><a id="fnr.async" class="footref" href="https://www.chiply.dev/#fn.async" role="doc-backlink">2</a></sup>.  You don't submit a query and wait for results.  Filtering happens <i>between</i> characters, fast enough that the result list feels like an extension of what you're typing.
</p>

<p>
Combine the three words and you get an interaction that is qualitatively different from either browsing or searching.  Browsing scales poorly to large sets &#x2014; you can scan a list of ten things, not a list of ten thousand.  Search-and-submit scales fine in the back end but introduces a feedback gap that breaks flow.  ICR fuses the two.
</p>

<p>
A clarification before going further.  In Emacs, the standard-library function named <code>completing-read</code> is not, on its own, incremental.  It TAB-completes at the minibuffer and shows a <code>*Completions*</code> buffer on demand.  The incremental UX described above is layered on top by a separate generation of frontends like Icomplete, Ido, Ivy, Helm, and the modern Vertico.  Throughout this series, "ICR" refers to the <i>pattern</i> (the API plus an incremental frontend), not to any single function.  This separation matters because it makes the Emacs completion stack pluggable, and this separation is the subject of the next post in the series.
</p>
</div>
</div>
<div id="outline-container-the-ubiquity-of-icr" class="outline-2">
<h2 id="the-ubiquity-of-icr"><span class="section-number-2">3.</span> The ubiquity of ICR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="ux">ux</span></span></h2>
<div class="outline-text-2" id="text-the-ubiquity-of-icr">
<p>
Think about all the places you already use ICR.  Here's a partial inventory:
</p>

<ul class="org-ul">
<li>The browser URL bar narrows history and bookmarks as you type.</li>
<li>Search engines suggest queries character by character.</li>
<li>Spotify, Apple Music, and YouTube surface tracks, artists, and videos as you fill in the search box.</li>
<li>Amazon's product search shows partial matches and category filters live.</li>
<li>IDEs offer symbol completion, file navigation, and command palettes.  Think VS Code's <code>Cmd-Shift-P</code>, JetBrains' "Search Everywhere," GitHub's file finder, Sublime Text's "Goto Anything."</li>
<li>Shell users reach for <code>fzf</code> to fuzzy-find files, branches, processes, and command history.</li>
<li>Slack jumps to channels by typing fragments of the name.</li>
<li>Even mobile keyboards suggest the next word as you tap<sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>!</li>
</ul>

<p>
These look like different tools, but when you think about it they are the same interface.  Each one accepts a stream of keystrokes, runs an incremental query against a sometimes enormous candidate set, and surfaces the best matches in real time, as you type.  Across all these apps, your interaction pattern is the same: you type fragments, watch a candidate list list narrow, and then pick from what survives the narrowing.
</p>

<blockquote class="pull-quote pull-right">
<p>
ICR has become the lingua franca of navigation.
</p>
</blockquote>

<p>
The pattern is so ubiquitous that its <i>absence</i> now feels strange to me.  File pickers that only show a tree, settings panels with no search box, and configuration UIs where you have to remember the menu hierarchy all force me to slow down and then manually browse through candidate sets to find what I'm looking for.  These feel like artifacts of an earlier era &#x2014; the era before incremental completing read became a common default for how humans navigate sets of named things.  Today, it feels like ICR has become the lingua franca of navigation.
</p>
</div>
</div>
<div id="outline-container-a-thought-exercise--how-much-of-computing-fits-inside-icr-" class="outline-2">
<h2 id="a-thought-exercise--how-much-of-computing-fits-inside-icr-"><span class="section-number-2">4.</span> A thought exercise: how much of computing fits inside ICR?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="composition">composition</span>&#xa0;<span class="shell">shell</span>&#xa0;<span class="HCI">HCI</span></span></h2>
<div class="outline-text-2" id="text-a-thought-exercise--how-much-of-computing-fits-inside-icr-">
<p>
If you take anything away from this post, let it be what follows in this section.  This realization is what makes Emacs legible to its power users:
</p>

<p>
We've seen where ICR shows up in the previous section, but where else can we use it?  Run an inventory of the interfaces you use daily, and for each one, ask: at its core, is this just <i>pick a thing from a set, then do something to it</i>?
</p>

<ul class="org-ul">
<li>Email: pick a message; reply, archive, forward, delete.</li>
<li>Calendar: pick an event; accept, reschedule, open.</li>
<li>File browser: pick a file; open, rename, delete, move.</li>
<li>Issue tracker: pick an issue; assign, comment, close.</li>
<li>Music player: pick a track; play, queue, save.</li>
<li>Package manager: pick a package; install, remove, inspect.</li>
<li>Git client: pick a branch; checkout, merge, rebase, delete.</li>
<li>Cloud console: pick a resource; start, stop, configure, destroy.</li>
</ul>

<p>
The list grows uncomfortably long.  It turns out that a surprising fraction of all interactions with your sofware uses the same two primitives: a <i>source of candidates</i> and a <i>set of actions</i> you can perform on a set of selected candidate.  Most of the visual chrome we drape around these primitives is decoration.
</p>

<blockquote class="pull-quote pull-left">
<p>
It turns out that a surprising fraction of all interactions with your sofware uses the same two primitives: a <i>source of candidates</i> and a <i>set of actions</i> you can perform on a set of selected candidate.  Most of the visual chrome we drape around these primitives is decoration.
</p>
</blockquote>

<p>
Now that you've seen the light, your next move is to ask whether you can <i>chain</i> these.  Consider navigating files in a project: ICR to pick a project, which scopes the candidate set to its files; ICR to pick a file, which scopes the actions to its file type; ICR to pick an action, which produces a new candidate set, and so on&#x2026;.  <b>An interaction model built from selecting and acting can be composed into arbitrarily complex workflows</b>, the same way any other small set of orthogonal primitives can.
</p>

<p>
Shell users already know this composition story well.  For most shell users, <code>fzf</code> drives ICR and produces selections.  Pipes feed those selections into commands.  Commands produce new selections which can be piped back into <code>fzf</code> for more ICR, and so on&#x2026;.  <code>git branch | fzf | xargs git checkout</code> is the pattern in miniature: a candidate source, a selector, an action, all chained.  <code>fd | fzf | xargs $EDITOR</code> is the same shape with a different source.  Build a few dozen of these one-liners and you have a personal interface to your filesystem, your version control history, your processes, your network, without anyone shipping you that interface.  That's powerful!
</p>

<p>
The interesting, and frustrating, observation is how rare this is composability and feature-richness is where ICR interfaces exist.  Spotify will never let you redefine what "select a track" can do.  Gmail's search cannot pipe its selected results into your own actions.  Some environments come closer than others &#x2014; Neovim's Telescope, Raycast's extension API, VS Code's <code>QuickPick</code> &#x2014; but in each of them at least one of the layers (the matcher, the sorter, the annotator, the action set) is fixed by the vendor.  Few environments expose every layer, and only Emacs and the shell expose them <i>independently</i>, so that you can swap one without disturbing the others.
</p>

<p>
This is the difference between <i>using</i> ICR and <i>building</i> ICR, and it is what makes Emacs and the shell uniquely powerful for anyone who works inside them all day.  Personally, this is the main reason why I live in Emacs and the shell.
</p>
</div>
</div>
<div id="outline-container-the-cognitive-cost-argument" class="outline-2">
<h2 id="the-cognitive-cost-argument"><span class="section-number-2">5.</span> The cognitive cost argument&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cognitiveStrain">cognitiveStrain</span></span></h2>
<div class="outline-text-2" id="text-the-cognitive-cost-argument">
<p>
Software engineers have a precise vocabulary for talking about how algorithms scale: time complexity, space complexity, big-O notation.  The corresponding field for how <i>interfaces</i> scale is human-computer interaction (HCI), which has its own established vocabulary &#x2014; Hick-Hyman's Law, Fitts's Law, working-memory load, recognition vs. recall &#x2014; but engineers rarely reach for it.  The argument that follows borrows from both sides, because ICR is best understood through both angles: an algorithmic property (constant-time filtering against an arbitrary corpus) producing an HCI property (constant-cost selection regardless of corpus size).
</p>

<p>
Consider the simple act of finding a file.  In a tree-based file browser, the cognitive effort grows with the size of the file system.  Five files in a folder is trivial, but five hundred files spread across a hierarchy is much more cognitively taxing.  You have to remember where the file lives, click through directories, scan lists, scroll, and move your cursor to the selection.  Add another order of magnitude &#x2014; half a million files in a project &#x2014; and the file browser has effectively ceased to function as a tool for finding things.  Cognitively, this approach scales worse than linearly.
</p>

<p>
Now do the same task with ICR.  You hit your file-finder binding, type a fragment of the name you remember, watch the list narrow to a handful of plausible matches, and pick one.  The experience is the same whether your project contains fifty files or fifty thousand.  The interface does not get harder to use as the candidate set grows.
</p>

<blockquote class="pull-quote pull-left">
<p>
ICR breaks the linkage between the size of the world and the difficulty of finding something in it.
</p>
</blockquote>

<p>
It is tempting to call this O(1) cognitive complexity, by analogy to algorithmic complexity<sup><a id="fnr.bigo" class="footref" href="https://www.chiply.dev/#fn.bigo" role="doc-backlink">4</a></sup>.  The point is straightforward: the cost of finding something via ICR is independent of the size of the candidate set, and that independence is what the big-O analogy is reaching for.  ICR breaks the linkage between the size of the world and the difficulty of finding something in it.
</p>

<p>
There is also a literature analogue worth naming.  Hick-Hyman's Law<sup><a id="fnr.hick" class="footref" href="https://www.chiply.dev/#fn.hick" role="doc-backlink">5</a></sup> models the time required for a forced choice as roughly proportional to log₂(n+1), where n is the number of equally likely alternatives.  A flat menu of ten thousand commands is a Hick-Hyman nightmare; the user pays a logarithmic-in-n decision cost on every selection.  ICR sidesteps the law by collapsing n <i>before</i> the choice step happens.  By the time the user is selecting from the visible candidate panel, n is already small, typically less than half a dozen in my experience, and the per-selection decision cost is bounded by <i>panel size</i> rather than <i>corpus size</i>.  We can calmly let the corpus grow without bound and we can trust that the time-to-pick stays roughly constant.
</p>

<p>
This is why ICR is not just an ergonomic nicety.  It bends the curve.  Most interface improvements buy you a constant factor, like a faster animation, a clearer label, or a better-organized menu.  ICR changes the curve itself, and anything that changes the curve dominates the things that only change the constant, given enough data.
</p>

<p>
The corollary is that ICR's value is <i>asymmetric across users</i>.  If your projects are tiny and your address book is short, you may never feel the difference.  However, if like me you are an Emacs user with a sprawling notes directory, two decades of email, half a dozen languages installed, and a thousand interactive commands, ICR is the difference between a usable system and an unusable one.  The bigger your world, the more you'll want to bend the curve.
</p>

<p>
A really key thing for me personally is the alleviation of any anxiety about the aforementioned search spaces growing.  Regardless of the underlying magnitude of my emails, news articles, code repositories, music libraries, etc&#x2026;, the ease of finding what I'm looking for in any given workflow is roughly constant.
</p>
</div>
</div>
<div id="outline-container-recognition--recall--and-the-third-option" class="outline-2">
<h2 id="recognition--recall--and-the-third-option"><span class="section-number-2">6.</span> Recognition, recall, and the third option&#xa0;&#xa0;&#xa0;<span class="tag"><span class="psychology">psychology</span></span></h2>
<div class="outline-text-2" id="text-recognition--recall--and-the-third-option">
<p>
Human-computer interaction research has long distinguished <i>recognition</i> (picking the right item from a presented list) from <i>recall</i> (producing the right item from memory)<sup><a id="fnr.nielsen" class="footref" href="https://www.chiply.dev/#fn.nielsen" role="doc-backlink">6</a></sup>.  Recognition is famously easier, and this is why menus exist, why icon-based interfaces won, why "tip of my tongue" is a complaint about recall failure rather than recognition failure.
</p>

<p>
ICR sits in a strange and useful place between easy recognotion and hard recall.  You don't have to recall the full item, but instead you only have to recall a <i>fragment</i> of it.  And you don't have to recognize it from a large fixed presented list because the list narrows (often to a single candidate) in response to whatever fragment you produced.  The interface meets you halfway.
</p>

<p>
This matters because the cognitive load of pure recall and the visual load of pure recognition both grow with set size.  Recalling one item out of ten thousand is harder than recalling one out of ten.  Recognizing one item in a list of ten thousand is harder than recognizing one in a list of ten.  The hybrid form ICR offers &#x2014; partial recall, then narrowing recognition &#x2014; degrades much more gracefully.  It is one of the few interaction primitives that gets its leverage from how human memory actually works rather than fighting it.
</p>

<p>
Cognitive psychology has a name for this hybrid: <i>cued recall</i><sup><a id="fnr.cued" class="footref" href="https://www.chiply.dev/#fn.cued" role="doc-backlink">7</a></sup>.  The user-typed fragment is a retrieval cue: the system uses it to materialize a small candidate set and the remainder of the task is recognition over that set.  ICR is the UI instantiation of cued recall, with the screen serving as an externalized cue-to-candidate index.  This is a well establish cognitive primitive, but it is rare to see an interface deploy it as deliberately as a well-tuned completion stack does.
</p>

<blockquote class="pull-quote pull-left">
<p>
The hybrid form ICR offers &#x2014; partial recall, then narrowing recognition &#x2014; degrades much more gracefully.
</p>
</blockquote>

<p>
The best completion frameworks lean into this further.  They learn your patterns.  Recently selected items rise.  Frequently selected items rise.  The fragment you produce maps to the candidate you <i>usually</i> pick, not the candidate that happens to alphabetize first.  The interface adapts to you.  Over months, this turns into something close to muscle memory: you type a few characters and the right answer is already at the top, because that's where it has been for the last hundred selections.
</p>
</div>
</div>
<div id="outline-container-flat-over-nested--how-icr-reshapes-how-you-organize" class="outline-2">
<h2 id="flat-over-nested--how-icr-reshapes-how-you-organize"><span class="section-number-2">7.</span> Flat over nested: how ICR reshapes how you organize&#xa0;&#xa0;&#xa0;<span class="tag"><span class="organization">organization</span>&#xa0;<span class="knowledgeManagement">knowledgeManagement</span></span></h2>
<div class="outline-text-2" id="text-flat-over-nested--how-icr-reshapes-how-you-organize">
<p>
The downstream effect is not just on retrieval.  ICR changes the math on how you should structure your data in the first place.
</p>

<p>
In a world without ICR, hierarchy is a pretty good coping strategy.  Tree-structured folders, deeply nested categories, "taxonomies" &#x2014; these exist because flat lists become unscannable past a certain size.  If finding things requires browsing, then organizing into a navigable tree is necessary work, but that work has real and compounding costs.  You have to invent the taxonomy up front, <i>before</i> you know what you'll eventually want to file.  Then you have to remember it later.  The biggest nightmare for me personally is that with hierachies and taxonomies, I have to live with the fact that many items legitimately belong in two categories at once, yet the file system or knowledge management system forces you to pick one.  I know people who are good at breaking out of this choice paralysis, but I know from experience that I am not one of them.  And you incur an <i>operational</i> cost on every save, because every new item is a small classification problem.
</p>

<blockquote class="pull-quote pull-right">
<p>
The argument for nesting was always "I cannot scan a flat list of ten thousand items."  ICR replies: "you do not need to scan it."
</p>
</blockquote>

<p>
With ICR, hierarchy becomes optional.  The argument for nesting was always "I cannot scan a flat list of ten thousand items."  ICR replies: "you do not need to scan it."  A flat directory plus tags plus links is sufficient, because ICR makes any individual item findable in a few keystrokes regardless of how many neighbors it has.
</p>

<p>
It is worth being precise about what ICR replaces and what it doesn't.  Hierarchy does at least two distinct jobs.  One is <i>retrieval</i>: helping you find a thing.  The other is <i>explanation</i>: encoding kind-of and part-of relationships, conferring landmark structure on a space, making the shape of a domain legible at a glance.  Cognitive psychology has long identified the latter as load-bearing.  Eleanor Rosch's work on basic-level categories<sup><a id="fnr.rosch" class="footref" href="https://www.chiply.dev/#fn.rosch" role="doc-backlink">8</a></sup> showed that hierarchical taxonomies map onto how humans actually carve up the world, and Thomas Malone's classic study of how people organize their physical desks<sup><a id="fnr.malone" class="footref" href="https://www.chiply.dev/#fn.malone" role="doc-backlink">9</a></sup> found that "filing" (hierarchical, classified) and "piling" (flat, recency-ordered) coexist for good reasons: piles support fast access to active material and files support reasoning about the shape of what you have.  ICR substitutes cleanly for hierarchy's <i>retrieval</i> function.  It does not substitute for the explanatory function.  When the relationships between things are themselves the point &#x2014; a code architecture, a course curriculum, a legal taxonomy &#x2014; a tree is still doing real work that no completion stack will replace<sup><a id="fnr.10" class="footref" href="https://www.chiply.dev/#fn.10" role="doc-backlink">10</a></sup>.
</p>

<p>
The sleight of hand to avoid is treating "ICR makes hierarchy optional" as "hierarchy is bad."  The honest, narrower claim is this: in domains where hierarchy was load-bearing <i>only</i> as a search affordance, ICR lets you drop it and reclaim its costs.
</p>

<p>
This is the architectural premise of <a href="https://protesilaos.com/emacs/denote">denote</a>, Protesilaos Stavrou's Emacs note-taking package.  denote stores notes in a single mostly-flat directory, and although the package supports subdirectory "silos", Stavrou explicitly argues against using them as a primary organizing principle.  Notes relate to each other through filename-encoded tags and explicit hyperlinks.  The package leans entirely on completion to find things, and that works because finding things in a flat namespace via ICR is instantaneous.  The same idea shows up in tools like <a href="https://obsidian.md/">Obsidian</a> and in older personal-knowledge systems.  These systems abandon of hierarchy because they trust that search interfaces to scale to larger search spaces.
</p>

<p>
Emacs itself works this way at a much larger scale.  One of my all-time favorite quirks is that <i>every</i> interactive command lives in a single flat namespace.  A mature configuration easily exposes ten thousand of them (a quick smash of <code>M-x</code> on my Emacs produces over 13,000 interactive commands).  Nothing about this is overwhelming to me though, because I never see the full list.  I just type <code>M-x</code> and a fragment of what I want, and the relevant commands surface.  A hierarchical menu system covering ten thousand commands would be unusable; a flat namespace plus <code>M-x</code> is unremarkable.
</p>

<p>
In Emacs you can get this flat-list style ICR even where there are rigid hierarchies.  This is critical when physical hierarchies or taxonomies are necessary (like in code repositories), but the user still wants to navigate the content without engaging with the hierarchy or taxonomy.  For example, when I'm trying to find a file via ICR, I find myself reaching for something like <code>project-find-file</code> (show me <i>all</i> files in a project in a flat list) over something like <code>find-file</code> (let me traverse the directories one level at a time until I find my leaf).
</p>

<p>
As we've already seen, the ICR pattern generalizes really well.  <i>Any structure you build to make scanning easier is a structure ICR makes redundant.</i> Even where these structures need to exist, ICR can still help you get around the rigidity and opacity of that structure.  Once you trust your completion stack, you can shed the hierarchies you built and maintain, and you can triumphantly reclaim the cognitive and operational overhead that those hierarchies were costing you.
</p>
</div>
</div>
<div id="outline-container-why-icr-matters-more-in-emacs-than-anywhere-else" class="outline-2">
<h2 id="why-icr-matters-more-in-emacs-than-anywhere-else"><span class="section-number-2">8.</span> Why ICR matters more in Emacs than anywhere else&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span></span></h2>
<div class="outline-text-2" id="text-why-icr-matters-more-in-emacs-than-anywhere-else">
<p>
The thought exercise above hands us the answer to a question this post has been circling: of the environments where ICR is genuinely programmable, why focus a series on Emacs rather than on the shell?
</p>

<p>
The shell case is well-trodden territory; Unix users have been chaining <code>fzf</code> and pipes for years, the design space is mostly explored, and shell users are typically introduced to the notion of ICR the second they start learning how to configure their prompt.  The Emacs case is younger, deeper, and less well documented &#x2014; and it is the focus of this series, so it is worth zooming in on the specific ways Emacs exposes completion as a substrate.  Emacs is also less popular, so there is an air of <a href="https://en.wikipedia.org/wiki/Proselytism">proselytism</a> to this post 😜.
</p>

<p>
In Emacs, every layer of the ICR interaction is pluggable.  <code>completing-read</code> is a function in the standard library.  The display is pluggable.  The matching strategy is pluggable.  The sorting is pluggable.  The annotations are pluggable.  The actions you can take on a selected candidate?  Pluggable!  This is all discussed in my subsequent post on the <a href="https://www.chiply.dev/post-vompeccc">VOMPECCC</a> composite framework.
</p>

<p>
In Emacs, <i>every layer of the interaction is a place where you can substitute behavior</i>, and every layer has a small ecosystem of competing implementations to choose from.
</p>

<blockquote class="pull-quote pull-right">
<p>
Most editors give you a completion UI.  Emacs gives you a completion <i>substrate</i>.  The difference is what you can build on top.
</p>
</blockquote>

<p>
This is what separates Emacs from the editors that come closest.  Most give you a completion UI; Emacs gives you a completion <i>substrate</i>.  From an HCI standpoint, what is unusual about Emacs is not the completion interaction itself &#x2014; the visible behavior is broadly similar to Telescope, <code>QuickPick</code>, or Raycast &#x2014; but that the layers HCI usually treats as monolithic (matcher, sorter, annotator, action set, display surface) are exposed as independent surfaces.  Those other tools let you produce candidates and bind actions, but the matcher, the sorter, the annotator, and the display they hand you are largely fixed.  Recently, the Emacs community has done a lot of work towards making all of these pieces independently swappable, and the resulting compositional space is qualitatively bigger.  This is the reason the Emacs completion ecosystem is one of the most interesting parts of the software.  Every well-designed Emacs package eventually becomes, in part, a <code>completing-read</code> application: a thoughtful choice of candidate source, plus annotations, plus actions, plus a UI that is already familiar because it is the same UI you use for everything else.  The cost of adding a new "thing the user can pick from a list" is close to zero, and the resulting interaction inherits all of the user's existing muscle memory.
</p>

<p>
Don't treat completion as <i>a built-in convenience you don't have to think about</i>.  Emacs ships with a working <code>completing-read</code> out of the box, and many users never look further.  This is a tragic error on the same scale as never customizing your shell.  A serious Emacs user should treat the completion stack the way a serious shell user treats prompt and history setup: as a thing worth investing in, arguably the driving HCI paradigm in the Emacs paltform.  Every other piece of the system gets better when this one is good:
</p>

<p>
ICR is a simple concept, but it has really profound effects on I use Emacs.  Better completion makes file finding faster.  Faster file-finding changes how I organize my data.  Better symbol completion changes how aggressively I refactor.  Better command completion changes which commands I remember exist<sup><a id="fnr.11" class="footref" href="https://www.chiply.dev/#fn.11" role="doc-backlink">11</a></sup>.  Better candidate annotations change which choices I can make confidently.  In addition to saving me cognition, keystrokes, and time, ICR raises the upper bound on how much of Emacs I can fluently use.
</p>
</div>
</div>
<div id="outline-container-where-this-series-goes" class="outline-2">
<h2 id="where-this-series-goes"><span class="section-number-2">9.</span> Where this series goes</h2>
<div class="outline-text-2" id="text-where-this-series-goes">
<p>
This was all very woo-woo and hand wavy, but the next two posts get concrete.
</p>

<p>
The middle post is on <a href="https://www.chiply.dev/post-vompeccc">VOMPECCC</a>, a name for a loose constellation of eight Emacs packages &#x2014; Vertico, Orderless, Marginalia, Prescient, Embark, Consult, Corfu, and Cape &#x2014; that together compose a complete, modular completion framework along Unix-philosophy lines.  Each package does one thing, and, boy, does it do it well.  Most importantly, Each communicates through Emacs's standard completion APIs, making it possible for any subset of these packages to work with or without the others.  That post is a technical breakdown for developers who want to either adopt the whole stack or pick the pieces that solve their specific problems.
</p>

<p>
The final post is on <a href="https://www.chiply.dev/post-spot">spot</a>, a Spotify client built as a pure ICR application: search Spotify's catalog through <code>consult</code>, view catalogue metadata inline with with <code>marginalia</code>, and act on them with <code>embark</code>.  It builds nothing of its own at the UI layer because it doesn't need to.  Every UI primitive it requires is already there, courtesy of the framework the previous post describes.  spot is a useful case study in what becomes possible when you stop treating completion as a default and start treating it as a programmable substrate.
</p>

<p>
Three posts, one argument: <i>incremental completing read is one of the highest-leverage interaction patterns in computing</i>, <i>Emacs gives you uniquely deep control over it</i>, and <i>that control is worth using</i>.  The rest of the series is about the practical 'how'.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">10.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
This post argues that Incremental Completing Read (ICR) &#x2014; the pattern where a candidate list narrows in real time as you type &#x2014; is not a convenience feature but a <a href="https://www.chiply.dev/#the-cognitive-cost-argument">structural change in how interface cost scales with data size</a>. ICR is composed of three ideas: <i>read</i> (prompt the user and return a value), <i>completing</i> (maintain and display a candidate set), and <i>incremental</i> (recompute matches on every keystroke). Together they produce <a href="https://www.chiply.dev/#what-is-incremental-completing-read-">an interaction qualitatively different from both browsing and searching</a>.
</p>

<p>
The pattern is already <a href="https://www.chiply.dev/#the-ubiquity-of-icr">ubiquitous across software you use daily</a> &#x2014; browser URL bars, search engines, music players, IDE command palettes, and shell tools like <code>fzf</code> all implement it. A surprising fraction of all computing boils down to <a href="https://www.chiply.dev/#a-thought-exercise--how-much-of-computing-fits-inside-icr-">two primitives: pick a thing from a set, then act on it</a>, and these primitives compose into arbitrarily complex workflows through chaining, the way shell pipes do.
</p>

<p>
From a cognitive-science perspective, ICR <a href="https://www.chiply.dev/#the-cognitive-cost-argument">breaks the linkage between corpus size and the difficulty of finding something in it</a>. While tree-based browsing degrades with scale and Hick-Hyman's Law penalizes large choice sets, ICR collapses the visible candidate count <i>before</i> the choice step, keeping per-selection cost roughly constant regardless of how large the underlying data grows. ICR also occupies <a href="https://www.chiply.dev/#recognition--recall--and-the-third-option">a unique position between recognition and recall</a> &#x2014; you supply a partial cue, the system materializes a small candidate set, and the rest is easy recognition. Cognitive psychology calls this <i>cued recall</i>, and well-tuned completion stacks lean into it further by learning your selection history.
</p>

<p>
Beyond retrieval, ICR <a href="https://www.chiply.dev/#flat-over-nested--how-icr-reshapes-how-you-organize">reshapes how you organize data in the first place</a>. Hierarchy was always a coping strategy for unscannable flat lists; ICR makes flat lists scannable, so hierarchies built purely as search affordances become redundant. This is the design premise behind tools like denote and Emacs's own flat <code>M-x</code> command namespace.
</p>

<p>
Finally, the post explains <a href="https://www.chiply.dev/#why-icr-matters-more-in-emacs-than-anywhere-else">why Emacs is the focus of this series</a>: unlike every other environment, Emacs exposes the matcher, sorter, annotator, display, and action set as independently replaceable layers, making completion a programmable <i>substrate</i> rather than a sealed UI. <a href="https://www.chiply.dev/#where-this-series-goes">The next two posts</a> get concrete &#x2014; one on the modular VOMPECCC completion framework, and one on a Spotify client built as a pure ICR application.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.substrate" class="footnum" href="https://www.chiply.dev/#fnr.substrate" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
I use "substrate" in the sense borrowed from biology and platform engineering: a foundational layer that other things are built on, acted upon, or composed out of.  In biology, an enzyme acts on a substrate; in hardware, transistors are fabricated on a silicon substrate; in platform engineering, applications run on a compute substrate like Kubernetes.  In all three, the substrate is primitive, malleable, and compositional &#x2014; the raw material <i>from which</i> or <i>on which</i> higher-level things are built.  Applied here: Emacs hands you completion as raw pluggable parts (matcher, sorter, annotator, action, display) rather than as a finished dish.  The implicit contrast is completion-as-UI: a product you consume, where the vendor has already picked every layer for you.
</p></div></div>

<div class="footdef"><sup><a id="fn.async" class="footnum" href="https://www.chiply.dev/#fnr.async" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The obvious objection: what if the candidate set is too enormous to materialize up front?  Think a grep over a large codebase, or a query against a remote API.  Emacs handles this through async completion sources &#x2014; <code>consult-ripgrep</code> is the canonical example.  Each keystroke debounces and spawns a ripgrep process whose streaming output becomes the incremental candidate set; the user sees narrowing results without ever holding the full corpus in memory.  The pattern generalizes: any candidate source that can be expressed as a streaming query (ripgrep, <code>git log</code>, a database cursor, a REST endpoint) slots into the same ICR interaction.  Corpus size stops being a constraint on the interface.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This actually doesn't even require an initial search string.  I have a bad joke about the iMessage word-prediction being the original ChatGPT &#x2014; if you could use a chuckle, I highly suggest opening up iMessage and spamming the next predicted word and observing the sheer nonsense that comes out.  
</p></div></div>

<div class="footdef"><sup><a id="fn.bigo" class="footnum" href="https://www.chiply.dev/#fnr.bigo" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Strictly speaking, big-O describes the runtime of an algorithm, not the perceived effort of a human using a tool, and the user-facing cost of ICR is not literally constant &#x2014; recalling a fragment, scanning the survivors, and choosing among them all consume real cognitive resources.  The defensible claim, and the one big-O notation is reaching for, is <i>independence from the size of the candidate set</i>.  Whether you call that "asymptotically constant cognitive cost," "sublinear effort," or just "the same work regardless of scale," the underlying observation is the same.
</p></div></div>

<div class="footdef"><sup><a id="fn.hick" class="footnum" href="https://www.chiply.dev/#fnr.hick" role="doc-backlink">5</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
William E. Hick, "On the rate of gain of information," <i>Quarterly Journal of Experimental Psychology</i> 4(1), 1952, 11–26; and Ray Hyman, "Stimulus information as a determinant of reaction time," <i>Journal of Experimental Psychology</i> 45(3), 1953, 188–196.  The law: choice reaction time scales as roughly k·log₂(n+1) for n equally likely alternatives.  ICR's effect is to keep n (the size of the visible candidate panel) small and roughly constant even as the underlying corpus grows arbitrarily.
</p></div></div>

<div class="footdef"><sup><a id="fn.nielsen" class="footnum" href="https://www.chiply.dev/#fnr.nielsen" role="doc-backlink">6</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Jakob Nielsen, <a href="https://www.nngroup.com/articles/ten-usability-heuristics/">"10 Usability Heuristics for User Interface Design"</a> (1994, periodically updated by the Nielsen Norman Group).  Heuristic #6 is "Recognition rather than recall": interfaces should minimize the user's memory load by making elements, actions, and options visible, rather than requiring users to retrieve them from memory.
</p></div></div>

<div class="footdef"><sup><a id="fn.cued" class="footnum" href="https://www.chiply.dev/#fnr.cued" role="doc-backlink">7</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Endel Tulving and Zena Pearlstone, "Availability versus accessibility of information in memory for words," <i>Journal of Verbal Learning and Verbal Behavior</i> 5(4), 1966, 381–391.  The original demonstration that retrieval cues dramatically improve recall over uncued conditions, even when the underlying item is equally "available" in memory.  Tulving's framing of cued recall as a distinct mode &#x2014; intermediate between free recall and recognition &#x2014; is the one ICR most closely instantiates.
</p></div></div>

<div class="footdef"><sup><a id="fn.rosch" class="footnum" href="https://www.chiply.dev/#fnr.rosch" role="doc-backlink">8</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Eleanor Rosch, Carolyn B. Mervis, Wayne D. Gray, David M. Johnson, and Penny Boyes-Braem, "Basic objects in natural categories," <i>Cognitive Psychology</i> 8(3), 1976, 382–439.  The basic-level finding: human categorization is not arbitrary across hierarchies but anchored at a particular middle level (chair, dog, car) that maximizes informativeness.  Hierarchies are not just retrieval scaffolds; they reflect how humans naturally carve up the world.
</p></div></div>

<div class="footdef"><sup><a id="fn.malone" class="footnum" href="https://www.chiply.dev/#fnr.malone" role="doc-backlink">9</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Thomas W. Malone, "How do people organize their desks? Implications for the design of office information systems," <i>ACM Transactions on Office Information Systems</i> 1(1), 1983, 99–112.  The classic study identifying "files" (hierarchical, classified) and "piles" (flat, recency-ordered) as coexisting strategies, each well-suited to different parts of the same workflow.
</p></div></div>

<div class="footdef"><sup><a id="fn.10" class="footnum" href="https://www.chiply.dev/#fnr.10" role="doc-backlink">10</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The counterargument here would be that tags and hyperlinks would give you the same thing, but the point here is that often times a PHYSICAL hierarchy, like the organization of files in a directory, is needed and will be unavoidable.
</p></div></div>

<div class="footdef"><sup><a id="fn.11" class="footnum" href="https://www.chiply.dev/#fnr.11" role="doc-backlink">11</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
I find it interesting that alleviating the burden of memory of a large search space actually improves my memory for the things that are actually important.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Live Life on the Edge: A Layered Strategy for Testing Data Models</title>
      <link>https://www.chiply.dev/post-data-model-testing</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-data-model-testing</guid>
      <pubDate>Tue, 07 Apr 2026 08:17:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Data models live everywhere in a modern system: function signatures, bounded-context boundaries, database schemas, event payloads on the wire, etc&amp;#x2026;. And for good reason &amp;#x2013; the model isn&apos;t...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataModelling">dataModelling</span>&#xa0;<span class="testing">testing</span>&#xa0;<span class="orgMode">orgMode</span>&#xa0;<span class="literateProgramming">literateProgramming</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgf29afdf" class="figure">
<p><img src="https://www.chiply.dev/images/post-data-model-testing-banner.jpeg" alt="post-data-model-testing-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Data models live everywhere in a modern system: function signatures, bounded-context boundaries, database schemas, event payloads on the wire, etc&#x2026;.  And for good reason &#x2013; the model isn't just a schema &#x2013; it's an executable specification of how the system works.  The criticality of data modelling has led to its ubiquity, and you will find a daunting proliferation of models throughout your software system, especially if you work in an enterprise where bias towards classic integration patterns urge developers to exhaustively model every payload and interface in the system. I call this the 'Model Everywhere Problem'.  
</p>

<p>
But if models are so great, why is their pervasiveness a 'problem'?
</p>

<p>
Consider that a modest data model &#x2013; a dozen fields, a few enums and optionals &#x2013; has thousands of structural states.  This set of a permissible instances is the model's 'state space', and large state spaces put demand on your testing suite (state space correlates with number of test cases).
</p>

<p>
On every engineering team I've worked on, I've noticed that these state spaces are almost never tested exhaustively, and instead only a few hardcoded instances of a model's possible instance are used for testing.  Tragically, this causes avoidable issues once the production system begins processing real data with untested edge cases.  This a problematic testing gap, and this is why the state space of a model &#x2013; not just its happy path of a golden test instance &#x2013; is the right unit to reason about when you write tests.
</p>

<blockquote class="pull-quote pull-right">
<p>
[the state space] of a model &#x2013; not just its happy path &#x2013; is the right unit to reason about when you write tests.
</p>
</blockquote>

<p>
Using python as an example language, this post walks through a three-layer strategy that closes that gap, and &#x2013; importantly &#x2013; where each layer earns its keep and where it doesn't:
</p>

<ul class="org-ul">
<li><b>Polyfactory</b> &#x2013; automates structural partition coverage (every enum value, every optional state) without exploding into a Cartesian product</li>
<li><b>Hypothesis</b> &#x2013; probes value-level edge cases (boundary floats, unicode, NaN) with shrinking</li>
<li><b>icontract</b> &#x2013; enforces cross-field invariants that types and serializable schemas can't express</li>
</ul>

<p>
None of these tools are new.  Hypothesis has been around since 2013; the ideas behind icontract (Design by Contract) date to <a href="https://www.eiffel.org/doc/solutions/Design_by_Contract_and_Assertions">Eiffel in 1986</a>; equivalence partitioning and pairwise testing have been standard since <a href="https://onlinelibrary.wiley.com/doi/book/10.1002/9781119202486">Myers' <i>Art of Software Testing</i></a>.  The contribution in this blog post is the <b>layering pattern</b>: when to reach for which tool, applied to a real scientific data model.
</p>

<blockquote>
<p>
<b>Versions used in this post</b>: Python 3.11, Pydantic 2.x, Polyfactory 2.x, Hypothesis 6.x, icontract 2.x.
</p>
</blockquote>
</div>
</div>
<div id="outline-container-the-combinatorial-explosion-problem" class="outline-2">
<h2 id="the-combinatorial-explosion-problem"><span class="section-number-2">2.</span> The Combinatorial Explosion Problem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="testing">testing</span>&#xa0;<span class="mathematics">mathematics</span></span></h2>
<div class="outline-text-2" id="text-the-combinatorial-explosion-problem">
<p>
Even modest models have enormous structural state spaces.  Consider a realistic Pydantic model for a <a href="https://en.wikipedia.org/wiki/Spectroscopy">spectroscopy</a> reading:
</p>
</div>
<div id="outline-container-growth-analysis" class="outline-3">
<h3 id="growth-analysis"><span class="section-number-3">2.1.</span> Growth Analysis</h3>
<div class="outline-text-3" id="text-growth-analysis">
<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">class</span> <span style="color: #008858;">SpectroscopyReading</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Required fields
</span>    reading_id: <span style="color: #ba35af;">str</span>
    instrument_id: <span style="color: #ba35af;">str</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Optional fields
</span>    <span style="color: #1f77bb;">wavelength_nm</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">float</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>
    <span style="color: #1f77bb;">temperature_K</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">float</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>
    <span style="color: #1f77bb;">pressure_atm</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">float</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>
    <span style="color: #1f77bb;">notes</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">str</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Enums
</span>    <span style="color: #1f77bb;">sample_type</span>: <span style="color: #008858;">SampleType</span> <span style="color: #202020;">=</span> SampleType.EXPERIMENTAL  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">3 values
</span>    <span style="color: #1f77bb;">status</span>: <span style="color: #008858;">ReadingStatus</span> <span style="color: #202020;">=</span> ReadingStatus.PENDING  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">4 values: PENDING, VALIDATED, FLAGGED, REJECTED
</span>    <span style="color: #1f77bb;">instrument_mode</span>: <span style="color: #008858;">InstrumentMode</span> <span style="color: #202020;">=</span> InstrumentMode.STANDARD  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">5 values: STANDARD, HIGH_RES, FAST, CALIBRATION, DIAGNOSTIC
</span>
    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Booleans
</span>    <span style="color: #1f77bb;">is_validated</span>: <span style="color: #ba35af;">bool</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">True</span>
    <span style="color: #1f77bb;">requires_review</span>: <span style="color: #ba35af;">bool</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">False</span>
    <span style="color: #1f77bb;">is_replicate</span>: <span style="color: #ba35af;">bool</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">False</span>
</pre>
</div>

<p>
Now let's compute the combinations:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-right" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Field</th>
<th scope="col" class="org-right">States</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">reading_id</td>
<td class="org-right">1</td>
</tr>

<tr>
<td class="org-left">instrument_id</td>
<td class="org-right">1</td>
</tr>

<tr>
<td class="org-left">wavelength_nm</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">temperature_K</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">pressure_atm</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">notes</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">sample_type</td>
<td class="org-right">3</td>
</tr>

<tr>
<td class="org-left">status</td>
<td class="org-right">4</td>
</tr>

<tr>
<td class="org-left">instrument_mode</td>
<td class="org-right">5</td>
</tr>

<tr>
<td class="org-left">is_validated</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">requires_review</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">is_replicate</td>
<td class="org-right">2</td>
</tr>
</tbody>
</table>

<p>
\[
\text{Combinations} = 1 \times 1 \times 2^4 \times 3 \times 4 \times 5 \times 2^3 = 16 \times 60 \times 8 = 7,680
\]
</p>

<p>
A handful of realistic fields produces 7,680 combinations.  And this is just <i>structural</i> &#x2013; we haven't even considered value-level edge cases (negative wavelengths, sub-absolute-zero temperatures, NaN concentrations) yet.
</p>
</div>
</div>
<div id="outline-container-nobody-tests-the-cartesian-product" class="outline-3">
<h3 id="nobody-tests-the-cartesian-product"><span class="section-number-3">2.2.</span> A Caveat: Nobody Tests the Cartesian Product</h3>
<div class="outline-text-3" id="text-nobody-tests-the-cartesian-product">
<blockquote class="pull-quote pull-right">
<p>
Each tool answers a question the previous one couldn't.
</p>
</blockquote>

<p>
Before going further, the obvious objection: 7,680 combinations is not 7,680 <i>tests you need to write</i>.  Equivalence partitioning (Myers, 1979) and combinatorial / pairwise testing (<a href="https://www.nist.gov/programs-projects/combinatorial-testing">NIST has decades of work on $t$-way coverage</a>) tell us that most "combinations" share a code path, and that 1-way and 2-way coverage catch the overwhelming majority of interaction bugs.  The 7,680 number is the <b>state space</b> a tester has to <i>reason about</i>, not the number of cases to enumerate.
</p>

<p>
The point of the tools below is not brute force.  Polyfactory's <code>coverage()</code> is essentially automated 1-way (every value of every field is exercised once); Hypothesis adds value-level probing where partition boundaries actually matter; icontract adds the cross-field rules that no equivalence partition can express.  Each tool answers a question the previous one couldn't.
</p>
</div>
</div>
</div>
<div id="outline-container-property-based-testing-with-polyfactory" class="outline-2">
<h2 id="property-based-testing-with-polyfactory"><span class="section-number-2">3.</span> Property-Based Testing with Polyfactory&#xa0;&#xa0;&#xa0;<span class="tag"><span class="testing">testing</span>&#xa0;<span class="polyfactory">polyfactory</span></span></h2>
<div class="outline-text-2" id="text-property-based-testing-with-polyfactory">
<p>
<a href="https://polyfactory.litestar.dev/">Polyfactory</a> is a library that generates mock data from Pydantic models (and other schemas).  Instead of hand-writing test fixtures, you define a factory and let polyfactory generate valid instances.
</p>
</div>
<div id="outline-container-basic-usage--the-build-method" class="outline-3">
<h3 id="basic-usage--the-build-method"><span class="section-number-3">3.1.</span> Basic Usage: The Build Method</h3>
<div class="outline-text-3" id="text-basic-usage--the-build-method">
<p>
The <code>build()</code> method creates a single instance with randomly generated values that satisfy your model's constraints:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">from</span> polyfactory.factories.pydantic_factory <span style="color: #6052cf;">import</span> ModelFactory
<span style="color: #6052cf;">from</span> pydantic <span style="color: #6052cf;">import</span> BaseModel
<span style="color: #6052cf;">from</span> typing <span style="color: #6052cf;">import</span> Optional
<span style="color: #6052cf;">from</span> enum <span style="color: #6052cf;">import</span> Enum

<span style="color: #6052cf;">class</span> <span style="color: #008858;">SampleType</span><span style="color: #008858;">(</span><span style="color: #ba35af;">str</span>, Enum<span style="color: #008858;">)</span>:
    <span style="color: #1f77bb;">CONTROL</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"control"</span>
    <span style="color: #1f77bb;">EXPERIMENTAL</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"experimental"</span>
    <span style="color: #1f77bb;">CALIBRATION</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"calibration"</span>

<span style="color: #6052cf;">class</span> <span style="color: #008858;">Sample</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    sample_id: <span style="color: #ba35af;">str</span>
    experiment_id: <span style="color: #ba35af;">str</span>
    <span style="color: #1f77bb;">concentration_mM</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">float</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>
    <span style="color: #1f77bb;">sample_type</span>: <span style="color: #008858;">SampleType</span> <span style="color: #202020;">=</span> SampleType.EXPERIMENTAL
    <span style="color: #1f77bb;">is_validated</span>: <span style="color: #ba35af;">bool</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">True</span>

<span style="color: #6052cf;">class</span> <span style="color: #008858;">SampleFactory</span><span style="color: #008858;">(</span>ModelFactory<span style="color: #008858;">)</span>:
    <span style="color: #1f77bb;">__model__</span> <span style="color: #202020;">=</span> Sample

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Generate a random valid sample
</span><span style="color: #1f77bb;">sample</span> <span style="color: #202020;">=</span> SampleFactory.build<span style="color: #008858;">()</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>sample<span style="color: #008858;">)</span>

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Override specific fields
</span><span style="color: #1f77bb;">control_sample</span> <span style="color: #202020;">=</span> SampleFactory.build<span style="color: #008858;">(</span>sample_type<span style="color: #202020;">=</span>SampleType.CONTROL, is_validated<span style="color: #202020;">=</span><span style="color: #065fff;">True</span><span style="color: #008858;">)</span>
</pre>
</div>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #1f77bb;">sample_id</span><span style="color: #202020;">=</span><span style="color: #4250ef;">'aqgvWnszGSMJWogQRmWa'</span> <span style="color: #1f77bb;">experiment_id</span><span style="color: #202020;">=</span><span style="color: #4250ef;">'zPxKVxoDzwanDbZmhDNL'</span> <span style="color: #1f77bb;">concentration_mM</span><span style="color: #202020;">=</span><span style="color: #065fff;">None</span> <span style="color: #1f77bb;">sample_type</span><span style="color: #202020;">=&lt;</span>SampleType.CALIBRATION: <span style="color: #4250ef;">'calibration'</span><span style="color: #202020;">&gt;</span> <span style="color: #1f77bb;">is_validated</span><span style="color: #202020;">=</span><span style="color: #065fff;">False</span>
</pre>
</div>

<p>
Every call to <code>build()</code> gives you a valid instance.  This is already powerful for unit tests where you need realistic test data without hand-crafting it.
</p>
</div>
</div>
<div id="outline-container-systematic-coverage--the-coverage-method" class="outline-3">
<h3 id="systematic-coverage--the-coverage-method"><span class="section-number-3">3.2.</span> Systematic Coverage: The Coverage Method</h3>
<div class="outline-text-3" id="text-systematic-coverage--the-coverage-method">
<p>
Here's where polyfactory really shines.  The <code>coverage()</code> method generates multiple instances designed to cover all the structural variations of your model:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Generate instances covering all structural variations
</span><span style="color: #6052cf;">for</span> sample <span style="color: #6052cf;">in</span> SampleFactory.coverage<span style="color: #008858;">()</span>:
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"type=</span>{sample.sample_type}<span style="color: #4250ef;">, conc=</span>{'<span style="color: #ba35af;">set</span>' <span style="color: #6052cf;">if</span> sample.concentration_mM <span style="color: #6052cf;">is</span> <span style="color: #6052cf;">not</span> <span style="color: #065fff;">None</span> <span style="color: #6052cf;">else</span> '<span style="color: #065fff;">None</span>'}<span style="color: #4250ef;">, valid=</span>{sample.is_validated}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>
</pre>
</div>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #ba35af;">type</span><span style="color: #202020;">=</span>SampleType.<span style="color: #1f77bb;">CONTROL</span>, <span style="color: #1f77bb;">conc</span><span style="color: #202020;">=</span><span style="color: #ba35af;">set</span>, <span style="color: #1f77bb;">valid</span><span style="color: #202020;">=</span><span style="color: #065fff;">True</span>
<span style="color: #ba35af;">type</span><span style="color: #202020;">=</span>SampleType.<span style="color: #1f77bb;">EXPERIMENTAL</span>, <span style="color: #1f77bb;">conc</span><span style="color: #202020;">=</span><span style="color: #065fff;">None</span>, <span style="color: #1f77bb;">valid</span><span style="color: #202020;">=</span><span style="color: #065fff;">True</span>
<span style="color: #ba35af;">type</span><span style="color: #202020;">=</span>SampleType.<span style="color: #1f77bb;">CALIBRATION</span>, <span style="color: #1f77bb;">conc</span><span style="color: #202020;">=</span><span style="color: #ba35af;">set</span>, <span style="color: #1f77bb;">valid</span><span style="color: #202020;">=</span><span style="color: #065fff;">False</span>
</pre>
</div>

<p>
The <code>coverage()</code> method systematically generates instances, but notice something important: we only got 3 instances, not the 12 we calculated earlier.  This is by design.
</p>
</div>
<div id="outline-container-how-coverage---actually-works" class="outline-4">
<h4 id="how-coverage---actually-works"><span class="section-number-4">3.2.1.</span> How coverage() Actually Works</h4>
<div class="outline-text-4" id="text-how-coverage---actually-works">
<p>
Polyfactory's <code>coverage()</code> uses a lockstep algorithm rather than a full Cartesian product.  Here's how it works:
</p>

<ol class="org-ol">
<li><b>Each field gets a <code>CoverageContainer</code></b> that holds all possible values for that field (enum members, <code>True=/=False</code> for booleans, <code>value=/=None</code> for optionals)</li>

<li><b>All containers advance in parallel</b> on each iteration &#x2013; the first instance picks the first value from every container, the second instance picks the second from every container, and so on.  Containers shorter than the longest one wrap around</li>

<li><b>Iteration stops when the "longest" container has been fully consumed</b> &#x2013; meaning every individual value of every field has been seen at least once</li>
</ol>

<p>
This produces a <b>representative sample</b> that guarantees:
</p>
<ul class="org-ul">
<li>Every enum value appears at least once</li>
<li>Both <code>True</code> and <code>False</code> appear for boolean fields</li>
<li>Both <code>present</code> and <code>None</code> states appear for optional fields</li>
</ul>

<p>
But it does <i>not</i> guarantee every <i>combination</i> is tested.  In our 3 instances:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">sample_type</th>
<th scope="col" class="org-left">concentration_mM</th>
<th scope="col" class="org-left">is_validated</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">CONTROL</td>
<td class="org-left">set</td>
<td class="org-left">False</td>
</tr>

<tr>
<td class="org-left">EXPERIMENTAL</td>
<td class="org-left">None</td>
<td class="org-left">True</td>
</tr>

<tr>
<td class="org-left">CALIBRATION</td>
<td class="org-left">set</td>
<td class="org-left">False</td>
</tr>
</tbody>
</table>

<p>
All enum values are covered.  Both optional states (set/None) appear.  Both boolean states appear.  But we <i>didn't</i> test <code>CONTROL</code> with <code>is_validated=True</code>, for example.  This is the deliberate trade-off: <code>coverage()</code> guarantees every individual value of every field is exercised, but misses interaction bugs that only manifest with specific <i>combinations</i>.  For validation logic where fields are processed independently, that's enough.  For complex interactions, supplement with targeted cases or reach for <a href="https://www.chiply.dev/#value-level-testing-with-hypothesis">Hypothesis</a>.
</p>
</div>
</div>
</div>
<div id="outline-container-a-practical-example" class="outline-3">
<h3 id="a-practical-example"><span class="section-number-3">3.3.</span> A Practical Example</h3>
<div class="outline-text-3" id="text-a-practical-example">
<p>
Let's say we have a function that determines analysis priority based on sample attributes:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">determine_priority</span><span style="color: #008858;">(</span>sample: Sample<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> <span style="color: #ba35af;">str</span>:
    <span style="color: #7fff7fff7fff;">"""Determine analysis priority based on sample type and validation status."""</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Calibration samples are always high priority
</span>    <span style="color: #6052cf;">if</span> sample.sample_type <span style="color: #202020;">==</span> SampleType.CALIBRATION:
        <span style="color: #6052cf;">return</span> <span style="color: #4250ef;">"high"</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Unvalidated samples need review first
</span>    <span style="color: #6052cf;">if</span> <span style="color: #6052cf;">not</span> sample.is_validated:
        <span style="color: #6052cf;">raise</span> <span style="color: #008858;">ValueError</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"Sample must be validated before analysis"</span><span style="color: #008858;">)</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Control samples with known concentration get medium priority
</span>    <span style="color: #6052cf;">if</span> sample.sample_type <span style="color: #202020;">==</span> SampleType.CONTROL <span style="color: #6052cf;">and</span> sample.concentration_mM <span style="color: #6052cf;">is</span> <span style="color: #6052cf;">not</span> <span style="color: #065fff;">None</span>:
        <span style="color: #6052cf;">return</span> <span style="color: #4250ef;">"medium"</span>

    <span style="color: #6052cf;">return</span> <span style="color: #4250ef;">"normal"</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
We can test this exhaustively using <code>coverage()</code>:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">import</span> pytest

<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_priority_all_sample_variations</span><span style="color: #008858;">()</span>:
    <span style="color: #7fff7fff7fff;">"""Test priority determination across all sample variations."""</span>
    <span style="color: #1f77bb;">results</span> <span style="color: #202020;">=</span> <span style="color: #008858;">[]</span>
    <span style="color: #6052cf;">for</span> sample <span style="color: #6052cf;">in</span> SampleFactory.coverage<span style="color: #008858;">()</span>:
        <span style="color: #6052cf;">if</span> <span style="color: #6052cf;">not</span> sample.is_validated:
            <span style="color: #6052cf;">try</span>:
                determine_priority<span style="color: #008858;">(</span>sample<span style="color: #008858;">)</span>
                results.append<span style="color: #008858;">(</span>f<span style="color: #4250ef;">"FAIL: </span>{sample.sample_type.value}<span style="color: #4250ef;">, validated=</span>{sample.is_validated}<span style="color: #4250ef;"> - expected ValueError"</span><span style="color: #008858;">)</span>
            <span style="color: #6052cf;">except</span> <span style="color: #008858;">ValueError</span>:
                results.append<span style="color: #008858;">(</span>f<span style="color: #4250ef;">"PASS: </span>{sample.sample_type.value}<span style="color: #4250ef;">, validated=</span>{sample.is_validated}<span style="color: #4250ef;"> - correctly raised ValueError"</span><span style="color: #008858;">)</span>
        <span style="color: #6052cf;">elif</span> sample.sample_type <span style="color: #202020;">==</span> SampleType.CALIBRATION:
            <span style="color: #1f77bb;">priority</span> <span style="color: #202020;">=</span> determine_priority<span style="color: #008858;">(</span>sample<span style="color: #008858;">)</span>
            <span style="color: #6052cf;">if</span> priority <span style="color: #202020;">==</span> <span style="color: #4250ef;">"high"</span>:
                results.append<span style="color: #008858;">(</span>f<span style="color: #4250ef;">"PASS: </span>{sample.sample_type.value}<span style="color: #4250ef;">, validated=</span>{sample.is_validated}<span style="color: #4250ef;"> -&gt; </span>{priority}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>
            <span style="color: #6052cf;">else</span>:
                results.append<span style="color: #008858;">(</span>f<span style="color: #4250ef;">"FAIL: </span>{sample.sample_type.value}<span style="color: #4250ef;"> expected 'high', got '</span>{priority}<span style="color: #4250ef;">'"</span><span style="color: #008858;">)</span>
        <span style="color: #6052cf;">else</span>:
            <span style="color: #1f77bb;">priority</span> <span style="color: #202020;">=</span> determine_priority<span style="color: #008858;">(</span>sample<span style="color: #008858;">)</span>
            <span style="color: #6052cf;">if</span> priority <span style="color: #6052cf;">in</span> <span style="color: #008858;">[</span><span style="color: #4250ef;">"high"</span>, <span style="color: #4250ef;">"medium"</span>, <span style="color: #4250ef;">"normal"</span><span style="color: #008858;">]</span>:
                results.append<span style="color: #008858;">(</span>f<span style="color: #4250ef;">"PASS: </span>{sample.sample_type.value}<span style="color: #4250ef;">, validated=</span>{sample.is_validated}<span style="color: #4250ef;"> -&gt; </span>{priority}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>
            <span style="color: #6052cf;">else</span>:
                results.append<span style="color: #008858;">(</span>f<span style="color: #4250ef;">"FAIL: </span>{sample.sample_type.value}<span style="color: #4250ef;"> got invalid priority '</span>{priority}<span style="color: #4250ef;">'"</span><span style="color: #008858;">)</span>
    <span style="color: #6052cf;">return</span> results

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Run the test and display results
</span><span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"Testing priority determination across all sample variations:"</span><span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"-"</span> <span style="color: #202020;">*</span> 60<span style="color: #008858;">)</span>
<span style="color: #6052cf;">for</span> result <span style="color: #6052cf;">in</span> test_priority_all_sample_variations<span style="color: #008858;">()</span>:
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>result<span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"-"</span> <span style="color: #202020;">*</span> 60<span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"All variations tested!"</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
With a simple call to <code>SampleFactory.coverage()</code>, this single test covers every structural combination of our model.  If we add new enum values or optional fields later, the <i>test automatically expands to cover them</i>.
</p>
</div>
</div>
<div id="outline-container-the-reusable-fixture-pattern" class="outline-3">
<h3 id="the-reusable-fixture-pattern"><span class="section-number-3">3.4.</span> The Reusable Fixture Pattern</h3>
<div class="outline-text-3" id="text-the-reusable-fixture-pattern">
<p>
Here's where things get powerful.  We can create a <b>reusable pytest fixture</b> that applies this coverage-based testing pattern to <i>any</i> Pydantic model:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">import</span> pytest
<span style="color: #6052cf;">from</span> typing <span style="color: #6052cf;">import</span> Type, Iterator, TypeVar
<span style="color: #6052cf;">from</span> pydantic <span style="color: #6052cf;">import</span> BaseModel
<span style="color: #6052cf;">from</span> polyfactory.factories.pydantic_factory <span style="color: #6052cf;">import</span> ModelFactory

<span style="color: #1f77bb;">T</span> <span style="color: #202020;">=</span> TypeVar<span style="color: #008858;">(</span><span style="color: #4250ef;">"T"</span>, bound<span style="color: #202020;">=</span>BaseModel<span style="color: #008858;">)</span>

<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">create_factory</span><span style="color: #008858;">(</span>model: Type<span style="color: #4f54aa;">[</span>T<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> Type<span style="color: #008858;">[</span>ModelFactory<span style="color: #4f54aa;">[</span>T<span style="color: #4f54aa;">]</span><span style="color: #008858;">]</span>:
    <span style="color: #7fff7fff7fff;">"""Dynamically create a factory for any Pydantic model."""</span>
    <span style="color: #6052cf;">return</span> <span style="color: #ba35af;">type</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"</span>{model.<span style="color: #ba35af;">__name__</span>}<span style="color: #4250ef;">Factory"</span>, <span style="color: #4f54aa;">(</span>ModelFactory,<span style="color: #4f54aa;">)</span>, <span style="color: #4f54aa;">{</span><span style="color: #4250ef;">"__model__"</span>: model<span style="color: #4f54aa;">}</span><span style="color: #008858;">)</span>

<span style="color: #008858;">@pytest.fixture</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">model_coverage</span><span style="color: #008858;">(</span>request<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> Iterator<span style="color: #008858;">[</span>BaseModel<span style="color: #008858;">]</span>:
    <span style="color: #7fff7fff7fff;">"""
    Reusable fixture that yields all structural variations of a model.

    Usage:
        @pytest.mark.parametrize("model_class", [Sample, Measurement, Experiment])
        def test_serialization(model_coverage, model_class):
            for instance in model_coverage:
                assert instance.model_dump_json()
    """</span>
    <span style="color: #1f77bb;">model_class</span> <span style="color: #202020;">=</span> request.param
    <span style="color: #1f77bb;">factory</span> <span style="color: #202020;">=</span> create_factory<span style="color: #008858;">(</span>model_class<span style="color: #008858;">)</span>
    <span style="color: #6052cf;">yield</span> <span style="color: #6052cf;">from</span> factory.coverage<span style="color: #008858;">()</span>

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Now testing ANY model is trivial:
</span><span style="color: #008858;">@pytest.mark.parametrize</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"model_class"</span>, <span style="color: #4f54aa;">[</span>Sample, SpectroscopyReading, Experiment<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_all_models_serialize</span><span style="color: #008858;">(</span>model_class<span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""Every model variation must serialize to JSON."""</span>
    <span style="color: #1f77bb;">factory</span> <span style="color: #202020;">=</span> create_factory<span style="color: #008858;">(</span>model_class<span style="color: #008858;">)</span>
    <span style="color: #6052cf;">for</span> instance <span style="color: #6052cf;">in</span> factory.coverage<span style="color: #008858;">()</span>:
        <span style="color: #1f77bb;">json_str</span> <span style="color: #202020;">=</span> instance.model_dump_json<span style="color: #008858;">()</span>
        <span style="color: #1f77bb;">restored</span> <span style="color: #202020;">=</span> model_class.model_validate_json<span style="color: #008858;">(</span>json_str<span style="color: #008858;">)</span>
        <span style="color: #6052cf;">assert</span> restored <span style="color: #202020;">==</span> instance
</pre>
</div>

<p>
This pattern is <b>massively</b> scalable.  Add a new model to your codebase?  Just add it to the parametrize list and you instantly get full structural coverage.  The investment in the pattern pays dividends as your codebase grows.
</p>
</div>
</div>
</div>
<div id="outline-container-value-level-testing-with-hypothesis" class="outline-2">
<h2 id="value-level-testing-with-hypothesis"><span class="section-number-2">4.</span> Value-Level Testing with Hypothesis&#xa0;&#xa0;&#xa0;<span class="tag"><span class="testing">testing</span>&#xa0;<span class="hypothesis">hypothesis</span></span></h2>
<div class="outline-text-2" id="text-value-level-testing-with-hypothesis">
<p>
Polyfactory handles structural variations, but what about <i>value-level</i> edge cases?  What happens when <code>wavelength_nm</code> is 0, or negative, or larger than the observable universe?  This is where <a href="https://hypothesis.readthedocs.io/">Hypothesis</a> comes in.
</p>

<p>
Hypothesis is a property-based testing library.  Instead of specifying exact test cases, you describe <b>properties</b> that should hold for any valid input, and Hypothesis generates hundreds of random inputs to try to break your code.
</p>
</div>
<div id="outline-container-the--given-decorator" class="outline-3">
<h3 id="the--given-decorator"><span class="section-number-3">4.1.</span> The @given Decorator</h3>
<div class="outline-text-3" id="text-the--given-decorator">
<p>
The <code>@given</code> decorator tells Hypothesis what kind of data to generate:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">from</span> hypothesis <span style="color: #6052cf;">import</span> given, strategies <span style="color: #6052cf;">as</span> st, settings

<span style="color: #008858;">@given</span><span style="color: #008858;">(</span>st.integers<span style="color: #4f54aa;">()</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@settings</span><span style="color: #008858;">(</span>max_examples<span style="color: #202020;">=</span>10<span style="color: #008858;">)</span>  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Limit for demo
</span><span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_absolute_value_is_non_negative</span><span style="color: #008858;">(</span>n<span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""Property: absolute value is always &gt;= 0"""</span>
    <span style="color: #6052cf;">assert</span> <span style="color: #ba35af;">abs</span><span style="color: #008858;">(</span>n<span style="color: #008858;">)</span> <span style="color: #202020;">&gt;=</span> 0

<span style="color: #008858;">@given</span><span style="color: #008858;">(</span>st.text<span style="color: #4f54aa;">()</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@settings</span><span style="color: #008858;">(</span>max_examples<span style="color: #202020;">=</span>10<span style="color: #008858;">)</span>  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Limit for demo
</span><span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_string_reversal_is_reversible</span><span style="color: #008858;">(</span>s<span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""Property: reversing twice gives original"""</span>
    <span style="color: #6052cf;">assert</span> s<span style="color: #008858;">[</span>::<span style="color: #202020;">-</span>1<span style="color: #008858;">][</span>::<span style="color: #202020;">-</span>1<span style="color: #008858;">]</span> <span style="color: #202020;">==</span> s

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Run the tests and show output
</span><span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"Running Hypothesis tests:"</span><span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"-"</span> <span style="color: #202020;">*</span> 60<span style="color: #008858;">)</span>
<span style="color: #6052cf;">try</span>:
    test_absolute_value_is_non_negative<span style="color: #008858;">()</span>
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"PASS: test_absolute_value_is_non_negative - all generated integers passed"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">except</span> <span style="color: #008858;">AssertionError</span> <span style="color: #6052cf;">as</span> e:
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"FAIL: test_absolute_value_is_non_negative - </span>{e}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>

<span style="color: #6052cf;">try</span>:
    test_string_reversal_is_reversible<span style="color: #008858;">()</span>
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"PASS: test_string_reversal_is_reversible - all generated strings passed"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">except</span> <span style="color: #008858;">AssertionError</span> <span style="color: #6052cf;">as</span> e:
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"FAIL: test_string_reversal_is_reversible - </span>{e}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"-"</span> <span style="color: #202020;">*</span> 60<span style="color: #008858;">)</span>
</pre>
</div>

<p>
Hypothesis will generate ~100 integers/strings per test run, including edge cases like 0, negative numbers, empty strings, unicode, etc.
</p>
</div>
</div>
<div id="outline-container-the-chaos-hypothesis-unleashes" class="outline-3">
<h3 id="the-chaos-hypothesis-unleashes"><span class="section-number-3">4.2.</span> The Chaos Hypothesis Unleashes</h3>
<div class="outline-text-3" id="text-the-chaos-hypothesis-unleashes">
<p>
Hypothesis doesn't just generate "normal" test data &#x2013; it actively tries to break your code with the most cursed inputs imaginable.  Real-world string inputs are reliably worse than you imagine.
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #008858;">@given</span><span style="color: #008858;">(</span>st.text<span style="color: #4f54aa;">()</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_sample_notes_field</span><span style="color: #008858;">(</span>notes: <span style="color: #ba35af;">str</span><span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""What could go wrong with a simple notes field?"""</span>
    <span style="color: #1f77bb;">sample</span> <span style="color: #202020;">=</span> Sample<span style="color: #008858;">(</span>
        sample_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"test-001"</span>,
        experiment_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"exp-001"</span>,
        notes<span style="color: #202020;">=</span>notes  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Oh no.
</span>    <span style="color: #008858;">)</span>
    process_sample<span style="color: #008858;">(</span>sample<span style="color: #008858;">)</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
Hypothesis will helpfully try:
</p>

<ul class="org-ul">
<li><code>notes</code>""= &#x2013; The empty string.  Classic.</li>
<li><code>notes</code>"\\x00\\x00\\x00"= &#x2013; Null bytes.  Because why not?</li>
<li><code>notes</code>"🧪🔬🧬💉"= &#x2013; Your sample notes are now emoji.  The lab notebook of the future.</li>
<li><code>notes</code>"a" * 10_000_000= &#x2013; Ten million 'a's.  Hope you're not logging this.</li>
<li><code>notes</code>"\\n\\n\\n\\n\\n"= &#x2013; Just vibes (and newlines).</li>
<li><code>notes</code>"ñoño"= &#x2013; Unicode normalization enters the chat.</li>
<li><code>notes</code>"🏳️‍🌈"= &#x2013; A single "character" that's actually 6 code points (flag + variation selector + ZWJ + rainbow). Grapheme clusters: surprise!</li>
</ul>

<p>
Your function either handles these gracefully or you discover bugs you never knew you had.  Usually the latter.
</p>
</div>
</div>
<div id="outline-container-combining-hypothesis-with-pydantic" class="outline-3">
<h3 id="combining-hypothesis-with-pydantic"><span class="section-number-3">4.3.</span> Combining Hypothesis with Pydantic</h3>
<div class="outline-text-3" id="text-combining-hypothesis-with-pydantic">
<p>
The real power comes from combining Hypothesis with our data models.  Hypothesis has a <code>from_type()</code> strategy that can generate instances of Pydantic models:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">from</span> hypothesis <span style="color: #6052cf;">import</span> given, strategies <span style="color: #6052cf;">as</span> st
<span style="color: #6052cf;">from</span> hypothesis <span style="color: #6052cf;">import</span> settings

<span style="color: #008858;">@given</span><span style="color: #008858;">(</span>st.from_type<span style="color: #4f54aa;">(</span>Sample<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@settings</span><span style="color: #008858;">(</span>max_examples<span style="color: #202020;">=</span>20<span style="color: #008858;">)</span>  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Reduced for demo output
</span><span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_sample_serialization_roundtrip</span><span style="color: #008858;">(</span>sample: Sample<span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""Property: serializing and deserializing preserves data"""</span>
    <span style="color: #1f77bb;">json_str</span> <span style="color: #202020;">=</span> sample.model_dump_json<span style="color: #008858;">()</span>
    <span style="color: #1f77bb;">restored</span> <span style="color: #202020;">=</span> Sample.model_validate_json<span style="color: #008858;">(</span>json_str<span style="color: #008858;">)</span>
    <span style="color: #6052cf;">assert</span> restored <span style="color: #202020;">==</span> sample

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Run and show output
</span><span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"Testing Sample serialization roundtrip with Hypothesis:"</span><span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"-"</span> <span style="color: #202020;">*</span> 60<span style="color: #008858;">)</span>
<span style="color: #6052cf;">try</span>:
    test_sample_serialization_roundtrip<span style="color: #008858;">()</span>
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"PASS: All 20 generated Sample instances serialized correctly"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">except</span> <span style="color: #008858;">AssertionError</span> <span style="color: #6052cf;">as</span> e:
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"FAIL: </span>{e}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">except</span> <span style="color: #008858;">Exception</span> <span style="color: #6052cf;">as</span> e:
    <span style="color: #ba35af;">print</span><span style="color: #008858;">(</span>f<span style="color: #4250ef;">"ERROR: </span>{<span style="color: #ba35af;">type</span>(e).<span style="color: #ba35af;">__name__</span>}<span style="color: #4250ef;">: </span>{e}<span style="color: #4250ef;">"</span><span style="color: #008858;">)</span>
<span style="color: #ba35af;">print</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"-"</span> <span style="color: #202020;">*</span> 60<span style="color: #008858;">)</span>
</pre>
</div>

<p>
This test generates random valid <code>Sample</code> instances and verifies that JSON serialization works correctly for all of them.
</p>
</div>
</div>
<div id="outline-container-custom-strategies-for-domain-constraints" class="outline-3">
<h3 id="custom-strategies-for-domain-constraints"><span class="section-number-3">4.4.</span> Custom Strategies for Domain Constraints</h3>
<div class="outline-text-3" id="text-custom-strategies-for-domain-constraints">
<p>
Sometimes we need more control over generated values.  In scientific domains, this is <b>critical</b> &#x2013; our data has physical meaning, and randomly generated values often violate physical laws.
</p>

<p>
Let me show you what I mean with spectroscopy data:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">from</span> hypothesis <span style="color: #6052cf;">import</span> given, strategies <span style="color: #6052cf;">as</span> st, assume

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Strategy for wavelengths (must be positive, typically 200-1100nm for UV-Vis)
</span><span style="color: #1f77bb;">valid_wavelength</span> <span style="color: #202020;">=</span> st.floats<span style="color: #008858;">(</span>min_value<span style="color: #202020;">=</span>200.0, max_value<span style="color: #202020;">=</span>1100.0, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Strategy for temperature (above absolute zero, below plasma)
</span><span style="color: #1f77bb;">valid_temperature</span> <span style="color: #202020;">=</span> st.floats<span style="color: #008858;">(</span>min_value<span style="color: #202020;">=</span>0.001, max_value<span style="color: #202020;">=</span>10000.0, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Strategy for concentration (non-negative, physically reasonable)
</span><span style="color: #1f77bb;">valid_concentration</span> <span style="color: #202020;">=</span> st.one_of<span style="color: #008858;">(</span>
    st.none<span style="color: #4f54aa;">()</span>,
    st.floats<span style="color: #4f54aa;">(</span>min_value<span style="color: #202020;">=</span>0.0, max_value<span style="color: #202020;">=</span>1000.0, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #4f54aa;">)</span>  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">millimolar
</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Strategy for pressure (vacuum to high pressure, in atmospheres)
</span><span style="color: #1f77bb;">valid_pressure</span> <span style="color: #202020;">=</span> st.floats<span style="color: #008858;">(</span>min_value<span style="color: #202020;">=</span>0.0, max_value<span style="color: #202020;">=</span>1000.0, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #008858;">)</span>

<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Composite strategy with inter-field constraints
</span><span style="color: #008858;">@st.composite</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">spectroscopy_reading_strategy</span><span style="color: #008858;">(</span>draw<span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""Generate physically plausible spectroscopy readings."""</span>
    <span style="color: #1f77bb;">wavelength</span> <span style="color: #202020;">=</span> draw<span style="color: #008858;">(</span>valid_wavelength<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">pressure</span> <span style="color: #202020;">=</span> draw<span style="color: #008858;">(</span>valid_pressure<span style="color: #008858;">)</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Domain constraint: at very low pressure, temperature readings are unreliable
</span>    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">(this is a real thing in vacuum spectroscopy!)
</span>    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Draw temperature *conditionally* on pressure rather than drawing-then-filtering,
</span>    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">so we don't burn Hypothesis's example budget on rejected draws.
</span>    <span style="color: #6052cf;">if</span> pressure <span style="color: #202020;">&lt;</span> 0.01:
        <span style="color: #1f77bb;">temperature</span> <span style="color: #202020;">=</span> draw<span style="color: #008858;">(</span>st.floats<span style="color: #4f54aa;">(</span>min_value<span style="color: #202020;">=</span>100.0, max_value<span style="color: #202020;">=</span>10000.0, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
    <span style="color: #6052cf;">else</span>:
        <span style="color: #1f77bb;">temperature</span> <span style="color: #202020;">=</span> draw<span style="color: #008858;">(</span>valid_temperature<span style="color: #008858;">)</span>

    <span style="color: #6052cf;">return</span> SpectroscopyReading<span style="color: #008858;">(</span>
        reading_id<span style="color: #202020;">=</span>draw<span style="color: #4f54aa;">(</span>st.text<span style="color: #ba35af;">(</span>min_size<span style="color: #202020;">=</span>1, max_size<span style="color: #202020;">=</span>50<span style="color: #ba35af;">)</span>.<span style="color: #ba35af;">filter</span><span style="color: #ba35af;">(</span><span style="color: #ba35af;">str</span>.strip<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>,
        instrument_id<span style="color: #202020;">=</span>draw<span style="color: #4f54aa;">(</span>st.sampled_from<span style="color: #ba35af;">(</span><span style="color: #1f77bb;">[</span><span style="color: #4250ef;">"UV-1800"</span>, <span style="color: #4250ef;">"FTIR-4600"</span>, <span style="color: #4250ef;">"Raman-532"</span><span style="color: #1f77bb;">]</span><span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>,
        wavelength_nm<span style="color: #202020;">=</span>wavelength,
        temperature_K<span style="color: #202020;">=</span>temperature,
        pressure_atm<span style="color: #202020;">=</span>pressure,
        sample_type<span style="color: #202020;">=</span>draw<span style="color: #4f54aa;">(</span>st.sampled_from<span style="color: #ba35af;">(</span>SampleType<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>,
        is_validated<span style="color: #202020;">=</span>draw<span style="color: #4f54aa;">(</span>st.booleans<span style="color: #ba35af;">()</span><span style="color: #4f54aa;">)</span>
    <span style="color: #008858;">)</span>

<span style="color: #008858;">@given</span><span style="color: #008858;">(</span>spectroscopy_reading_strategy<span style="color: #4f54aa;">()</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_reading_within_physical_bounds</span><span style="color: #008858;">(</span>reading: SpectroscopyReading<span style="color: #008858;">)</span>:
    <span style="color: #7fff7fff7fff;">"""Property: all readings must be physically plausible"""</span>
    <span style="color: #6052cf;">if</span> reading.wavelength_nm <span style="color: #6052cf;">is</span> <span style="color: #6052cf;">not</span> <span style="color: #065fff;">None</span>:
        <span style="color: #6052cf;">assert</span> reading.wavelength_nm <span style="color: #202020;">&gt;</span> 0, <span style="color: #4250ef;">"Negative wavelength is not a thing"</span>
    <span style="color: #6052cf;">if</span> reading.temperature_K <span style="color: #6052cf;">is</span> <span style="color: #6052cf;">not</span> <span style="color: #065fff;">None</span>:
        <span style="color: #6052cf;">assert</span> reading.temperature_K <span style="color: #202020;">&gt;</span> 0, <span style="color: #4250ef;">"Below absolute zero? Bold claim."</span>
</pre>
</div>

<p>
The key insight here is that scientific data has <b>semantic constraints</b> that go beyond type checking.  A <code>float</code> can hold any value, but a wavelength of -500nm or a temperature of -273K is physically impossible.  Custom strategies let us encode this domain knowledge.
</p>
</div>
</div>
<div id="outline-container-shrinking--finding-minimal-failing-cases" class="outline-3">
<h3 id="shrinking--finding-minimal-failing-cases"><span class="section-number-3">4.5.</span> Shrinking: Finding Minimal Failing Cases</h3>
<div class="outline-text-3" id="text-shrinking--finding-minimal-failing-cases">
<p>
One of Hypothesis's killer features is <b>shrinking</b>.  When it finds a failing test case, it automatically simplifies it to find the minimal example that still fails.  Instead of a failing case like:
</p>

<pre class="example" id="orgc2a1930">
SpectroscopyReading(reading_id='xK8jP2mQrS...', wavelength_nm=847293.7, temperature_K=9999.9, ...)
</pre>

<p>
Hypothesis will shrink it to something like:
</p>

<pre class="example" id="orgc133ed4">
SpectroscopyReading(reading_id='a', wavelength_nm=1101.0, temperature_K=0.0, ...)
</pre>

<p>
This makes debugging much easier &#x2013; you immediately see that <code>wavelength_nm=1101.0</code> (just outside our UV-Vis range) is the problem, not the giant random string.
</p>
</div>
</div>
<div id="outline-container-what-hypothesis-actually-probes" class="outline-3">
<h3 id="what-hypothesis-actually-probes"><span class="section-number-3">4.6.</span> What Hypothesis Actually Probes</h3>
<div class="outline-text-3" id="text-what-hypothesis-actually-probes">
<p>
The reason Hypothesis finds bugs naive random sampling misses isn't volume &#x2013; it's targeting.  Under the hood, <code>st.floats()</code> biases its draws toward values that historically break code: boundary values (min, max, just inside, just outside), zero, negative zero, the smallest positive subnormal, <code>inf</code>, <code>-inf</code>, and <code>NaN</code> when allowed.  <code>st.text()</code> biases toward the empty string, single characters, surrogate pairs, and combining marks.  Each strategy carries a "this is what bugs look like in this domain" prior.
</p>

<p>
Combine that with <i>shrinking</i> and you get the property-based testing loop: explore aggressively, fail fast, then minimise the failure to something a human can read in one line.  Naive uniform random gives you neither.
</p>
</div>
</div>
<div id="outline-container-when-hypothesis-isnt-right" class="outline-3">
<h3 id="when-hypothesis-isnt-right"><span class="section-number-3">4.7.</span> When Hypothesis Isn't the Right Tool</h3>
<div class="outline-text-3" id="text-when-hypothesis-isnt-right">
<p>
Hypothesis is not 'free'.  Specific cases where it earns its keep less:
</p>

<ul class="org-ul">
<li><b>Stateful workflows with expensive setup</b>.  If each example needs a fresh database, an HTTP fixture, or a multi-second container, 100 examples per test blows your CI budget.  Use targeted cases or <code>@reproduce_failure</code> for regression pinning instead.</li>
<li><b>Impure code with hidden state</b>.  Shrinking assumes failures are reproducible from the shrunk input alone.  If the failure depends on global mutable state, you get confusing minimised cases that don't actually fail in isolation.</li>
<li><b>Properties you can't actually state</b>.  "The function should return the right answer" is not a property.  If the only oracle you have is another implementation, you have differential testing, not property testing &#x2013; and that's a different (still useful) technique.</li>
<li><b>When a typed total function would do</b>.  A pure function with a tight signature and equivalence partitioning may not need a property at all.  Don't reach for Hypothesis as a status symbol.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-the-testing-gap--when-models-aren-t-enough" class="outline-2">
<h2 id="the-testing-gap--when-models-aren-t-enough"><span class="section-number-2">5.</span> The Testing Gap: When Models Aren't Enough&#xa0;&#xa0;&#xa0;<span class="tag"><span class="testing">testing</span>&#xa0;<span class="gaps">gaps</span></span></h2>
<div class="outline-text-2" id="text-the-testing-gap--when-models-aren-t-enough">
<p>
We've covered structural combinations with polyfactory and value-level edge cases with Hypothesis.  This is powerful, but there's still a gap: <b>runtime invariants that can't be expressed in the type system</b>.
</p>

<p>
Consider this example from analytical chemistry:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationCurve</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    readings: <span style="color: #ba35af;">list</span><span style="color: #008858;">[</span>CalibrationPoint<span style="color: #008858;">]</span>
    r_squared: <span style="color: #ba35af;">float</span>
    slope: <span style="color: #ba35af;">float</span>
    intercept: <span style="color: #ba35af;">float</span>

    <span style="color: #008858;">@field_validator</span><span style="color: #008858;">(</span><span style="color: #4250ef;">'r_squared'</span><span style="color: #008858;">)</span>
    @<span style="color: #ba35af;">classmethod</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">validate_r_squared</span><span style="color: #008858;">(</span>cls, v<span style="color: #008858;">)</span>:
        <span style="color: #6052cf;">if</span> <span style="color: #6052cf;">not</span> 0 <span style="color: #202020;">&lt;=</span> v <span style="color: #202020;">&lt;=</span> 1:
            <span style="color: #6052cf;">raise</span> <span style="color: #008858;">ValueError</span><span style="color: #008858;">(</span><span style="color: #4250ef;">'R&#178; must be between 0 and 1'</span><span style="color: #008858;">)</span>
        <span style="color: #6052cf;">return</span> v
</pre>
</div>

<pre class="example">
None
</pre>


<p>
Pydantic validates that <code>r_squared</code> is between 0 and 1.  But what about this invariant?
</p>

<blockquote>
<p>
The <code>r_squared</code> must be calculated from the actual <code>readings</code> using the <code>slope</code> and <code>intercept</code>.
</p>
</blockquote>

<p>
This is a <b>cross-field constraint</b> &#x2013; it depends on the relationship between multiple fields.  And it's not just about validation at construction time.  What if <code>r_squared</code> gets calculated incorrectly in our curve-fitting logic?
</p>
</div>
<div id="outline-container-scientific-logic-errors" class="outline-3">
<h3 id="scientific-logic-errors"><span class="section-number-3">5.1.</span> Scientific Logic Errors</h3>
<div class="outline-text-3" id="text-scientific-logic-errors">
<p>
Consider this function:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">recalculate_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve, new_reading: CalibrationPoint<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Add a new calibration point and recalculate the curve."""</span>
    <span style="color: #1f77bb;">all_readings</span> <span style="color: #202020;">=</span> curve.readings <span style="color: #202020;">+</span> <span style="color: #008858;">[</span>new_reading<span style="color: #008858;">]</span>
    <span style="color: #1f77bb;">slope</span>, <span style="color: #1f77bb;">intercept</span>, <span style="color: #1f77bb;">r_squared</span> <span style="color: #202020;">=</span> fit_linear_regression<span style="color: #008858;">(</span>all_readings<span style="color: #008858;">)</span>

    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">BUG: accidentally swapped slope and intercept
</span>    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        readings<span style="color: #202020;">=</span>all_readings,
        r_squared<span style="color: #202020;">=</span>r_squared,
        slope<span style="color: #202020;">=</span>intercept,  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">BUG: wrong assignment!
</span>        intercept<span style="color: #202020;">=</span>slope   <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">BUG: wrong assignment!
</span>    <span style="color: #008858;">)</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
This code has a subtle bug: the slope and intercept are swapped.  Each field individually is a valid float, so Pydantic validation passes.  But any concentration calculated from this curve will be wildly wrong.
</p>

<p>
Our Pydantic validation passes because each field is individually valid.  Our Hypothesis tests might not catch this because they test properties at the data structure level, not scientific invariants.
</p>
</div>
<div id="outline-container-why-not-pydantic-validators-" class="outline-4">
<h4 id="why-not-pydantic-validators-"><span class="section-number-4">5.1.1.</span> Why Not Pydantic Validators?</h4>
<div class="outline-text-4" id="text-why-not-pydantic-validators-">
<p>
You might be thinking: "Can't we add a <code>@model_validator</code> to Pydantic that checks if <code>r_squared</code> matches the fit?"  Technically, yes:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationCurve</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">... fields ...
</span>
    <span style="color: #008858;">@model_validator</span><span style="color: #008858;">(</span>mode<span style="color: #202020;">=</span><span style="color: #4250ef;">'after'</span><span style="color: #008858;">)</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">validate_r_squared_consistency</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> Self:
        <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Check that r_squared matches the actual fit
</span>        <span style="color: #1f77bb;">calculated_r2</span> <span style="color: #202020;">=</span> compute_r_squared<span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.readings, <span style="color: #6052cf;">self</span>.slope, <span style="color: #6052cf;">self</span>.intercept<span style="color: #008858;">)</span>
        <span style="color: #6052cf;">if</span> <span style="color: #ba35af;">abs</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.r_squared <span style="color: #202020;">-</span> calculated_r2<span style="color: #008858;">)</span> <span style="color: #202020;">&gt;</span> 0.001:
            <span style="color: #6052cf;">raise</span> <span style="color: #008858;">ValueError</span><span style="color: #008858;">(</span><span style="color: #4250ef;">"R&#178; doesn't match the fit"</span><span style="color: #008858;">)</span>
        <span style="color: #6052cf;">return</span> <span style="color: #6052cf;">self</span>
</pre>
</div>

<p>
But this approach has a significant drawback: <b>custom validators don't serialize to standard schema formats</b><sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>.
</p>

<p>
In data engineering, your Pydantic models often need to export schemas for:
</p>
<ul class="org-ul">
<li><b>Avro</b> (schema registries for Kafka)</li>
<li><b>JSON Schema</b> (API documentation, OpenAPI specs)</li>
<li><b>Protobuf</b> (gRPC services)</li>
<li><b>Database DDL</b> (SQLAlchemy models, migrations)</li>
</ul>

<p>
These formats support type constraints and basic validation (nullable, enums, numeric ranges), but they have no way to represent arbitrary Python code like "R² must be computed from readings using least-squares regression."
</p>

<p>
Embedding complex validation logic in your model validators means:
</p>
<ol class="org-ol">
<li>The schema your consumers see is <i>incomplete</i> &#x2013; it shows the fields but not the invariants</li>
<li>Other systems can't validate data independently &#x2013; they must call your Python code</li>
<li>Schema evolution becomes fragile &#x2013; changes to validation logic don't appear in schema diffs</li>
</ol>

<p>
By keeping Pydantic models "schema-clean" (only expressing constraints that <i>can</i> be serialized) and putting cross-field rules <i>runtime contracts</i> (see below), you at least keep your serialized schema honest about what it does and doesn't enforce.
</p>

<p>
This is where <b>Design by Contract</b> comes in.
</p>
</div>
</div>
</div>
</div>
<div id="outline-container-design-by-contract-with-icontract" class="outline-2">
<h2 id="design-by-contract-with-icontract"><span class="section-number-2">6.</span> Design by Contract with icontract&#xa0;&#xa0;&#xa0;<span class="tag"><span class="testing">testing</span>&#xa0;<span class="icontract">icontract</span></span></h2>
<div class="outline-text-2" id="text-design-by-contract-with-icontract">
<p>
<a href="https://icontract.readthedocs.io/">icontract</a> brings Design by Contract (DbC) to Python.  DbC is a methodology where you specify:
</p>

<ul class="org-ul">
<li><b>Preconditions</b>: What must be true <i>before</i> a function runs</li>
<li><b>Postconditions</b>: What must be true <i>after</i> a function runs</li>
<li><b>Invariants</b>: What must <i>always</i> be true about a class</li>
</ul>

<p>
If any condition is violated at runtime, you get an immediate, informative error.
</p>
</div>
<div id="outline-container-preconditions-with--require" class="outline-3">
<h3 id="preconditions-with--require"><span class="section-number-3">6.1.</span> Preconditions with @require</h3>
<div class="outline-text-3" id="text-preconditions-with--require">
<p>
Preconditions specify what callers must guarantee:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">import</span> icontract

<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> curve: <span style="color: #ba35af;">len</span><span style="color: #4f54aa;">(</span>curve.readings<span style="color: #4f54aa;">)</span> <span style="color: #202020;">&gt;=</span> 2, <span style="color: #4250ef;">"Need at least 2 points to fit a curve"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> new_reading: new_reading.concentration <span style="color: #202020;">&gt;=</span> 0, <span style="color: #4250ef;">"Concentration must be non-negative"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">recalculate_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve, new_reading: CalibrationPoint<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Add a new calibration point and recalculate the curve."""</span>
    <span style="color: #1f77bb;">all_readings</span> <span style="color: #202020;">=</span> curve.readings <span style="color: #202020;">+</span> <span style="color: #008858;">[</span>new_reading<span style="color: #008858;">]</span>
    <span style="color: #1f77bb;">slope</span>, <span style="color: #1f77bb;">intercept</span>, <span style="color: #1f77bb;">r_squared</span> <span style="color: #202020;">=</span> fit_linear_regression<span style="color: #008858;">(</span>all_readings<span style="color: #008858;">)</span>

    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        readings<span style="color: #202020;">=</span>all_readings,
        r_squared<span style="color: #202020;">=</span>r_squared,
        slope<span style="color: #202020;">=</span>slope,
        intercept<span style="color: #202020;">=</span>intercept
    <span style="color: #008858;">)</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
If someone calls <code>recalculate_curve</code> with only one reading, they get an immediate <code>ViolationError</code> explaining which precondition failed.
</p>
</div>
</div>
<div id="outline-container-postconditions-with--ensure" class="outline-3">
<h3 id="postconditions-with--ensure"><span class="section-number-3">6.2.</span> Postconditions with @ensure</h3>
<div class="outline-text-3" id="text-postconditions-with--ensure">
<p>
Postconditions specify what the function guarantees to return:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">import</span> icontract

<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> result: 0 <span style="color: #202020;">&lt;=</span> result.r_squared <span style="color: #202020;">&lt;=</span> 1, <span style="color: #4250ef;">"R&#178; must be between 0 and 1"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span>
    <span style="color: #6052cf;">lambda</span> curve, result: <span style="color: #ba35af;">len</span><span style="color: #4f54aa;">(</span>result.readings<span style="color: #4f54aa;">)</span> <span style="color: #202020;">==</span> <span style="color: #ba35af;">len</span><span style="color: #4f54aa;">(</span>curve.readings<span style="color: #4f54aa;">)</span> <span style="color: #202020;">+</span> 1,
    <span style="color: #4250ef;">"Result must have exactly one more reading"</span>
<span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span>
    <span style="color: #6052cf;">lambda</span> result: result.slope <span style="color: #202020;">!=</span> 0 <span style="color: #6052cf;">or</span> <span style="color: #ba35af;">all</span><span style="color: #4f54aa;">(</span><span style="color: #ba35af;">abs</span><span style="color: #ba35af;">(</span>r.response <span style="color: #202020;">-</span> result.intercept<span style="color: #ba35af;">)</span> <span style="color: #202020;">&lt;</span> 1e<span style="color: #202020;">-</span>9 <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> result.readings<span style="color: #4f54aa;">)</span>,
    <span style="color: #4250ef;">"Zero slope only valid if all responses equal intercept"</span>
<span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">recalculate_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve, new_reading: CalibrationPoint<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Add a new calibration point and recalculate the curve."""</span>
    <span style="color: #1f77bb;">all_readings</span> <span style="color: #202020;">=</span> curve.readings <span style="color: #202020;">+</span> <span style="color: #008858;">[</span>new_reading<span style="color: #008858;">]</span>
    <span style="color: #1f77bb;">slope</span>, <span style="color: #1f77bb;">intercept</span>, <span style="color: #1f77bb;">r_squared</span> <span style="color: #202020;">=</span> fit_linear_regression<span style="color: #008858;">(</span>all_readings<span style="color: #008858;">)</span>

    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        readings<span style="color: #202020;">=</span>all_readings,
        r_squared<span style="color: #202020;">=</span>r_squared,
        slope<span style="color: #202020;">=</span>slope,
        intercept<span style="color: #202020;">=</span>intercept
    <span style="color: #008858;">)</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
Now if our function produces an invalid result &#x2013; even if it passes Pydantic validation &#x2013; we catch it immediately.
</p>
</div>
</div>
<div id="outline-container-class-invariants-with--invariant" class="outline-3">
<h3 id="class-invariants-with--invariant"><span class="section-number-3">6.3.</span> Class Invariants with @invariant</h3>
<div class="outline-text-3" id="text-class-invariants-with--invariant">
<p>
For data models, class invariants are particularly powerful.  They specify properties that must always hold:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">import</span> icontract
<span style="color: #6052cf;">from</span> pydantic <span style="color: #6052cf;">import</span> BaseModel
<span style="color: #6052cf;">import</span> numpy <span style="color: #6052cf;">as</span> np

<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">r_squared_matches_fit</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> <span style="color: #ba35af;">bool</span>:
    <span style="color: #7fff7fff7fff;">"""Invariant: R&#178; must be consistent with actual readings and coefficients."""</span>
    <span style="color: #6052cf;">if</span> <span style="color: #ba35af;">len</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.readings<span style="color: #008858;">)</span> <span style="color: #202020;">&lt;</span> 2:
        <span style="color: #6052cf;">return</span> <span style="color: #065fff;">True</span>  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Can't verify with insufficient data
</span>    <span style="color: #1f77bb;">concentrations</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.concentration <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> <span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">responses</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.response <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> <span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">predicted</span> <span style="color: #202020;">=</span> <span style="color: #6052cf;">self</span>.slope <span style="color: #202020;">*</span> concentrations <span style="color: #202020;">+</span> <span style="color: #6052cf;">self</span>.intercept
    <span style="color: #1f77bb;">ss_res</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>responses <span style="color: #202020;">-</span> predicted<span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">ss_tot</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>responses <span style="color: #202020;">-</span> np.mean<span style="color: #ba35af;">(</span>responses<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">calculated_r2</span> <span style="color: #202020;">=</span> 1 <span style="color: #202020;">-</span> <span style="color: #008858;">(</span>ss_res <span style="color: #202020;">/</span> ss_tot<span style="color: #008858;">)</span> <span style="color: #6052cf;">if</span> ss_tot <span style="color: #202020;">&gt;</span> 0 <span style="color: #6052cf;">else</span> 1.0
    <span style="color: #6052cf;">return</span> <span style="color: #ba35af;">abs</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.r_squared <span style="color: #202020;">-</span> calculated_r2<span style="color: #008858;">)</span> <span style="color: #202020;">&lt;</span> 0.001  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Allow for floating point
</span>
<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #6052cf;">self</span>: <span style="color: #ba35af;">len</span><span style="color: #4f54aa;">(</span><span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">)</span> <span style="color: #202020;">&gt;=</span> 2, <span style="color: #4250ef;">"Calibration needs at least 2 points"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span>r_squared_matches_fit, <span style="color: #4250ef;">"R&#178; must match actual fit quality"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationCurve</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    readings: <span style="color: #ba35af;">list</span><span style="color: #008858;">[</span>CalibrationPoint<span style="color: #008858;">]</span>
    r_squared: <span style="color: #ba35af;">float</span>
    slope: <span style="color: #ba35af;">float</span>
    intercept: <span style="color: #ba35af;">float</span>

    <span style="color: #6052cf;">class</span> <span style="color: #008858;">Config</span>:
        <span style="color: #1f77bb;">arbitrary_types_allowed</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">True</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
Now any <code>CalibrationCurve</code> instance that violates our scientific invariant will raise an error immediately &#x2013; whether it's created directly, returned from a function, or modified anywhere in the system.
</p>
</div>
</div>
<div id="outline-container-a-complete-example" class="outline-3">
<h3 id="a-complete-example"><span class="section-number-3">6.4.</span> A Complete Example</h3>
<div class="outline-text-3" id="text-a-complete-example">
<p>
Let's put it all together with a realistic example from a quality control workflow:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #6052cf;">import</span> icontract
<span style="color: #6052cf;">from</span> pydantic <span style="color: #6052cf;">import</span> BaseModel, field_validator
<span style="color: #6052cf;">from</span> typing <span style="color: #6052cf;">import</span> Optional
<span style="color: #6052cf;">from</span> enum <span style="color: #6052cf;">import</span> Enum
<span style="color: #6052cf;">from</span> datetime <span style="color: #6052cf;">import</span> datetime
<span style="color: #6052cf;">import</span> numpy <span style="color: #6052cf;">as</span> np

<span style="color: #6052cf;">class</span> <span style="color: #008858;">QCStatus</span><span style="color: #008858;">(</span><span style="color: #ba35af;">str</span>, Enum<span style="color: #008858;">)</span>:
    <span style="color: #1f77bb;">PENDING</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"pending"</span>
    <span style="color: #1f77bb;">VALIDATED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"validated"</span>
    <span style="color: #1f77bb;">FLAGGED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"flagged"</span>
    <span style="color: #1f77bb;">REJECTED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"rejected"</span>
    <span style="color: #1f77bb;">APPROVED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"approved"</span>

<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationPoint</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    concentration: <span style="color: #ba35af;">float</span>
    response: <span style="color: #ba35af;">float</span>
    <span style="color: #1f77bb;">replicate</span>: <span style="color: #ba35af;">int</span> <span style="color: #202020;">=</span> 1

    <span style="color: #008858;">@field_validator</span><span style="color: #008858;">(</span><span style="color: #4250ef;">'concentration'</span><span style="color: #008858;">)</span>
    @<span style="color: #ba35af;">classmethod</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">validate_concentration</span><span style="color: #008858;">(</span>cls, v<span style="color: #008858;">)</span>:
        <span style="color: #6052cf;">if</span> v <span style="color: #202020;">&lt;</span> 0:
            <span style="color: #6052cf;">raise</span> <span style="color: #008858;">ValueError</span><span style="color: #008858;">(</span><span style="color: #4250ef;">'Concentration must be non-negative'</span><span style="color: #008858;">)</span>
        <span style="color: #6052cf;">return</span> v

<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">r_squared_is_consistent</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> <span style="color: #ba35af;">bool</span>:
    <span style="color: #7fff7fff7fff;">"""Invariant: R&#178; must match the actual fit."""</span>
    <span style="color: #6052cf;">if</span> <span style="color: #ba35af;">len</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.readings<span style="color: #008858;">)</span> <span style="color: #202020;">&lt;</span> 2:
        <span style="color: #6052cf;">return</span> <span style="color: #065fff;">True</span>
    <span style="color: #1f77bb;">conc</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.concentration <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> <span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">resp</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.response <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> <span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">pred</span> <span style="color: #202020;">=</span> <span style="color: #6052cf;">self</span>.slope <span style="color: #202020;">*</span> conc <span style="color: #202020;">+</span> <span style="color: #6052cf;">self</span>.intercept
    <span style="color: #1f77bb;">ss_res</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>resp <span style="color: #202020;">-</span> pred<span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">ss_tot</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>resp <span style="color: #202020;">-</span> np.mean<span style="color: #ba35af;">(</span>resp<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">calc_r2</span> <span style="color: #202020;">=</span> 1 <span style="color: #202020;">-</span> <span style="color: #008858;">(</span>ss_res <span style="color: #202020;">/</span> ss_tot<span style="color: #008858;">)</span> <span style="color: #6052cf;">if</span> ss_tot <span style="color: #202020;">&gt;</span> 0 <span style="color: #6052cf;">else</span> 1.0
    <span style="color: #6052cf;">return</span> <span style="color: #ba35af;">abs</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.r_squared <span style="color: #202020;">-</span> calc_r2<span style="color: #008858;">)</span> <span style="color: #202020;">&lt;</span> 0.001

<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">approved_has_good_r_squared</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> <span style="color: #ba35af;">bool</span>:
    <span style="color: #7fff7fff7fff;">"""Invariant: approved curves must have R&#178; &gt;= 0.99."""</span>
    <span style="color: #6052cf;">if</span> <span style="color: #6052cf;">self</span>.status <span style="color: #202020;">==</span> QCStatus.APPROVED:
        <span style="color: #6052cf;">return</span> <span style="color: #6052cf;">self</span>.r_squared <span style="color: #202020;">&gt;=</span> 0.99
    <span style="color: #6052cf;">return</span> <span style="color: #065fff;">True</span>

<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #6052cf;">self</span>: <span style="color: #ba35af;">len</span><span style="color: #4f54aa;">(</span><span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">)</span> <span style="color: #202020;">&gt;=</span> 2, <span style="color: #4250ef;">"Need at least 2 calibration points"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span>r_squared_is_consistent, <span style="color: #4250ef;">"R&#178; must match actual fit quality"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span>approved_has_good_r_squared, <span style="color: #4250ef;">"Approved curves need R&#178; &gt;= 0.99"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationCurve</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    curve_id: <span style="color: #ba35af;">str</span>
    analyst_id: <span style="color: #ba35af;">str</span>
    readings: <span style="color: #ba35af;">list</span><span style="color: #008858;">[</span>CalibrationPoint<span style="color: #008858;">]</span>
    slope: <span style="color: #ba35af;">float</span>
    intercept: <span style="color: #ba35af;">float</span>
    r_squared: <span style="color: #ba35af;">float</span>
    <span style="color: #1f77bb;">status</span>: <span style="color: #008858;">QCStatus</span> <span style="color: #202020;">=</span> QCStatus.PENDING
    <span style="color: #1f77bb;">reviewer_notes</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">str</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>
    created_at: datetime

    <span style="color: #6052cf;">class</span> <span style="color: #008858;">Config</span>:
        <span style="color: #1f77bb;">arbitrary_types_allowed</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">True</span>


<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Function with contracts
</span><span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> curve: curve.status <span style="color: #202020;">==</span> QCStatus.PENDING,
                   <span style="color: #4250ef;">"Can only validate pending curves"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> result: result.status <span style="color: #6052cf;">in</span> <span style="color: #4f54aa;">[</span>QCStatus.VALIDATED, QCStatus.FLAGGED<span style="color: #4f54aa;">]</span>,
                  <span style="color: #4250ef;">"Validation must result in validated or flagged status"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">validate_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Validate a calibration curve based on R&#178; threshold."""</span>
    <span style="color: #1f77bb;">new_status</span> <span style="color: #202020;">=</span> QCStatus.VALIDATED <span style="color: #6052cf;">if</span> curve.r_squared <span style="color: #202020;">&gt;=</span> 0.99 <span style="color: #6052cf;">else</span> QCStatus.FLAGGED
    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        curve_id<span style="color: #202020;">=</span>curve.curve_id,
        analyst_id<span style="color: #202020;">=</span>curve.analyst_id,
        readings<span style="color: #202020;">=</span>curve.readings,
        slope<span style="color: #202020;">=</span>curve.slope,
        intercept<span style="color: #202020;">=</span>curve.intercept,
        r_squared<span style="color: #202020;">=</span>curve.r_squared,
        status<span style="color: #202020;">=</span>new_status,
        reviewer_notes<span style="color: #202020;">=</span>curve.reviewer_notes,
        created_at<span style="color: #202020;">=</span>curve.created_at
    <span style="color: #008858;">)</span>


<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> curve: curve.status <span style="color: #202020;">==</span> QCStatus.VALIDATED,
                   <span style="color: #4250ef;">"Can only approve validated curves"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> reviewer_notes: reviewer_notes <span style="color: #6052cf;">and</span> reviewer_notes.strip<span style="color: #4f54aa;">()</span>,
                   <span style="color: #4250ef;">"Reviewer notes required for approval"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> result: result.status <span style="color: #202020;">==</span> QCStatus.APPROVED,
                  <span style="color: #4250ef;">"Curve must be approved after approval"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> result: result.reviewer_notes <span style="color: #6052cf;">is</span> <span style="color: #6052cf;">not</span> <span style="color: #065fff;">None</span>,
                  <span style="color: #4250ef;">"Reviewer notes must be set"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">approve_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve, reviewer_notes: <span style="color: #ba35af;">str</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Approve a validated calibration curve."""</span>
    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        curve_id<span style="color: #202020;">=</span>curve.curve_id,
        analyst_id<span style="color: #202020;">=</span>curve.analyst_id,
        readings<span style="color: #202020;">=</span>curve.readings,
        slope<span style="color: #202020;">=</span>curve.slope,
        intercept<span style="color: #202020;">=</span>curve.intercept,
        r_squared<span style="color: #202020;">=</span>curve.r_squared,
        status<span style="color: #202020;">=</span>QCStatus.APPROVED,
        reviewer_notes<span style="color: #202020;">=</span>reviewer_notes,
        created_at<span style="color: #202020;">=</span>curve.created_at
    <span style="color: #008858;">)</span>
</pre>
</div>

<pre class="example">
None
</pre>


<p>
With this setup:
</p>

<ol class="org-ol">
<li>You cannot create a <code>CalibrationCurve</code> that violates any invariant</li>
<li>You cannot call <code>validate_curve</code> on a non-pending curve</li>
<li>You cannot call <code>approve_curve</code> without reviewer notes</li>
<li>If any function returns an invalid <code>CalibrationCurve</code>, you get an immediate error</li>
</ol>
</div>
</div>
<div id="outline-container-combining-everything" class="outline-3">
<h3 id="combining-everything"><span class="section-number-3">6.5.</span> Combining Everything</h3>
<div class="outline-text-3" id="text-combining-everything">
<p>
The real power comes from combining all three approaches. Here's a complete test file that demonstrates all three techniques working together:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #7fff7fff7fff;">"""
Integration tests demonstrating Polyfactory, Hypothesis, and icontract together.

This file is tangled from post-data-model-testing.org and can be run with:
    pytest test_data_model_integration.py -v
"""</span>
<span style="color: #6052cf;">from</span> typing <span style="color: #6052cf;">import</span> Optional
<span style="color: #6052cf;">from</span> enum <span style="color: #6052cf;">import</span> Enum
<span style="color: #6052cf;">from</span> datetime <span style="color: #6052cf;">import</span> datetime

<span style="color: #6052cf;">import</span> numpy <span style="color: #6052cf;">as</span> np
<span style="color: #6052cf;">import</span> icontract
<span style="color: #6052cf;">import</span> pytest
<span style="color: #6052cf;">from</span> pydantic <span style="color: #6052cf;">import</span> BaseModel, field_validator
<span style="color: #6052cf;">from</span> polyfactory.factories.pydantic_factory <span style="color: #6052cf;">import</span> ModelFactory
<span style="color: #6052cf;">from</span> polyfactory <span style="color: #6052cf;">import</span> Use
<span style="color: #6052cf;">from</span> hypothesis <span style="color: #6052cf;">import</span> given, strategies <span style="color: #6052cf;">as</span> st, settings


<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">DOMAIN MODELS (with icontract invariants)
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span>
<span style="color: #6052cf;">class</span> <span style="color: #008858;">QCStatus</span><span style="color: #008858;">(</span><span style="color: #ba35af;">str</span>, Enum<span style="color: #008858;">)</span>:
    <span style="color: #1f77bb;">PENDING</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"pending"</span>
    <span style="color: #1f77bb;">VALIDATED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"validated"</span>
    <span style="color: #1f77bb;">FLAGGED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"flagged"</span>
    <span style="color: #1f77bb;">REJECTED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"rejected"</span>
    <span style="color: #1f77bb;">APPROVED</span> <span style="color: #202020;">=</span> <span style="color: #4250ef;">"approved"</span>


<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationPoint</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    concentration: <span style="color: #ba35af;">float</span>
    response: <span style="color: #ba35af;">float</span>
    <span style="color: #1f77bb;">replicate</span>: <span style="color: #ba35af;">int</span> <span style="color: #202020;">=</span> 1

    <span style="color: #008858;">@field_validator</span><span style="color: #008858;">(</span><span style="color: #4250ef;">'concentration'</span><span style="color: #008858;">)</span>
    @<span style="color: #ba35af;">classmethod</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">validate_concentration</span><span style="color: #008858;">(</span>cls, v<span style="color: #008858;">)</span>:
        <span style="color: #6052cf;">if</span> v <span style="color: #202020;">&lt;</span> 0:
            <span style="color: #6052cf;">raise</span> <span style="color: #008858;">ValueError</span><span style="color: #008858;">(</span><span style="color: #4250ef;">'Concentration must be non-negative'</span><span style="color: #008858;">)</span>
        <span style="color: #6052cf;">return</span> v


<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">r_squared_is_consistent</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> <span style="color: #ba35af;">bool</span>:
    <span style="color: #7fff7fff7fff;">"""Invariant: R&#178; must match the actual fit."""</span>
    <span style="color: #6052cf;">if</span> <span style="color: #ba35af;">len</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.readings<span style="color: #008858;">)</span> <span style="color: #202020;">&lt;</span> 2:
        <span style="color: #6052cf;">return</span> <span style="color: #065fff;">True</span>
    <span style="color: #1f77bb;">conc</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.concentration <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> <span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">resp</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.response <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> <span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">pred</span> <span style="color: #202020;">=</span> <span style="color: #6052cf;">self</span>.slope <span style="color: #202020;">*</span> conc <span style="color: #202020;">+</span> <span style="color: #6052cf;">self</span>.intercept
    <span style="color: #1f77bb;">ss_res</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>resp <span style="color: #202020;">-</span> pred<span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">ss_tot</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>resp <span style="color: #202020;">-</span> np.mean<span style="color: #ba35af;">(</span>resp<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
    <span style="color: #1f77bb;">calc_r2</span> <span style="color: #202020;">=</span> 1 <span style="color: #202020;">-</span> <span style="color: #008858;">(</span>ss_res <span style="color: #202020;">/</span> ss_tot<span style="color: #008858;">)</span> <span style="color: #6052cf;">if</span> ss_tot <span style="color: #202020;">&gt;</span> 0 <span style="color: #6052cf;">else</span> 1.0
    <span style="color: #6052cf;">return</span> <span style="color: #ba35af;">abs</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>.r_squared <span style="color: #202020;">-</span> calc_r2<span style="color: #008858;">)</span> <span style="color: #202020;">&lt;</span> 0.001


<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">approved_has_good_r_squared</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> <span style="color: #ba35af;">bool</span>:
    <span style="color: #7fff7fff7fff;">"""Invariant: approved curves must have R&#178; &gt;= 0.99."""</span>
    <span style="color: #6052cf;">if</span> <span style="color: #6052cf;">self</span>.status <span style="color: #202020;">==</span> QCStatus.APPROVED:
        <span style="color: #6052cf;">return</span> <span style="color: #6052cf;">self</span>.r_squared <span style="color: #202020;">&gt;=</span> 0.99
    <span style="color: #6052cf;">return</span> <span style="color: #065fff;">True</span>


<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> <span style="color: #6052cf;">self</span>: <span style="color: #ba35af;">len</span><span style="color: #4f54aa;">(</span><span style="color: #6052cf;">self</span>.readings<span style="color: #4f54aa;">)</span> <span style="color: #202020;">&gt;=</span> 2, <span style="color: #4250ef;">"Need at least 2 calibration points"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span>r_squared_is_consistent, <span style="color: #4250ef;">"R&#178; must match actual fit quality"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.invariant</span><span style="color: #008858;">(</span>approved_has_good_r_squared, <span style="color: #4250ef;">"Approved curves need R&#178; &gt;= 0.99"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationCurve</span><span style="color: #008858;">(</span>BaseModel<span style="color: #008858;">)</span>:
    curve_id: <span style="color: #ba35af;">str</span>
    analyst_id: <span style="color: #ba35af;">str</span>
    readings: <span style="color: #ba35af;">list</span><span style="color: #008858;">[</span>CalibrationPoint<span style="color: #008858;">]</span>
    slope: <span style="color: #ba35af;">float</span>
    intercept: <span style="color: #ba35af;">float</span>
    r_squared: <span style="color: #ba35af;">float</span>
    <span style="color: #1f77bb;">status</span>: <span style="color: #008858;">QCStatus</span> <span style="color: #202020;">=</span> QCStatus.PENDING
    <span style="color: #1f77bb;">reviewer_notes</span>: <span style="color: #008858;">Optional</span><span style="color: #008858;">[</span><span style="color: #ba35af;">str</span><span style="color: #008858;">]</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">None</span>
    created_at: datetime

    <span style="color: #6052cf;">class</span> <span style="color: #008858;">Config</span>:
        <span style="color: #1f77bb;">arbitrary_types_allowed</span> <span style="color: #202020;">=</span> <span style="color: #065fff;">True</span>


<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">DOMAIN FUNCTIONS (with icontract pre/post conditions)
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span>
<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> curve: curve.status <span style="color: #202020;">==</span> QCStatus.PENDING,
                   <span style="color: #4250ef;">"Can only validate pending curves"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> result: result.status <span style="color: #6052cf;">in</span> <span style="color: #4f54aa;">[</span>QCStatus.VALIDATED, QCStatus.FLAGGED<span style="color: #4f54aa;">]</span>,
                  <span style="color: #4250ef;">"Validation must result in validated or flagged status"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">validate_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve<span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Validate a calibration curve based on R&#178; threshold."""</span>
    <span style="color: #1f77bb;">new_status</span> <span style="color: #202020;">=</span> QCStatus.VALIDATED <span style="color: #6052cf;">if</span> curve.r_squared <span style="color: #202020;">&gt;=</span> 0.99 <span style="color: #6052cf;">else</span> QCStatus.FLAGGED
    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        curve_id<span style="color: #202020;">=</span>curve.curve_id,
        analyst_id<span style="color: #202020;">=</span>curve.analyst_id,
        readings<span style="color: #202020;">=</span>curve.readings,
        slope<span style="color: #202020;">=</span>curve.slope,
        intercept<span style="color: #202020;">=</span>curve.intercept,
        r_squared<span style="color: #202020;">=</span>curve.r_squared,
        status<span style="color: #202020;">=</span>new_status,
        reviewer_notes<span style="color: #202020;">=</span>curve.reviewer_notes,
        created_at<span style="color: #202020;">=</span>curve.created_at
    <span style="color: #008858;">)</span>


<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> curve: curve.status <span style="color: #202020;">==</span> QCStatus.VALIDATED,
                   <span style="color: #4250ef;">"Can only approve validated curves"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.require</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> reviewer_notes: reviewer_notes <span style="color: #6052cf;">and</span> reviewer_notes.strip<span style="color: #4f54aa;">()</span>,
                   <span style="color: #4250ef;">"Reviewer notes required for approval"</span><span style="color: #008858;">)</span>
<span style="color: #008858;">@icontract.ensure</span><span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span> result: result.status <span style="color: #202020;">==</span> QCStatus.APPROVED,
                  <span style="color: #4250ef;">"Curve must be approved after approval"</span><span style="color: #008858;">)</span>
<span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">approve_curve</span><span style="color: #008858;">(</span>curve: CalibrationCurve, reviewer_notes: <span style="color: #ba35af;">str</span><span style="color: #008858;">)</span> <span style="color: #202020;">-&gt;</span> CalibrationCurve:
    <span style="color: #7fff7fff7fff;">"""Approve a validated calibration curve."""</span>
    <span style="color: #6052cf;">return</span> CalibrationCurve<span style="color: #008858;">(</span>
        curve_id<span style="color: #202020;">=</span>curve.curve_id,
        analyst_id<span style="color: #202020;">=</span>curve.analyst_id,
        readings<span style="color: #202020;">=</span>curve.readings,
        slope<span style="color: #202020;">=</span>curve.slope,
        intercept<span style="color: #202020;">=</span>curve.intercept,
        r_squared<span style="color: #202020;">=</span>curve.r_squared,
        status<span style="color: #202020;">=</span>QCStatus.APPROVED,
        reviewer_notes<span style="color: #202020;">=</span>reviewer_notes,
        created_at<span style="color: #202020;">=</span>curve.created_at
    <span style="color: #008858;">)</span>


<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">POLYFACTORY FACTORIES
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span>
<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationPointFactory</span><span style="color: #008858;">(</span>ModelFactory<span style="color: #008858;">)</span>:
    <span style="color: #1f77bb;">__model__</span> <span style="color: #202020;">=</span> CalibrationPoint
    <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Constrain concentration to non-negative values (matching Pydantic validator)
</span>    <span style="color: #1f77bb;">concentration</span> <span style="color: #202020;">=</span> Use<span style="color: #008858;">(</span><span style="color: #6052cf;">lambda</span>: ModelFactory.__random__.uniform<span style="color: #4f54aa;">(</span>0, 1000<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>


<span style="color: #6052cf;">class</span> <span style="color: #008858;">CalibrationCurveFactory</span><span style="color: #008858;">(</span>ModelFactory<span style="color: #008858;">)</span>:
    <span style="color: #1f77bb;">__model__</span> <span style="color: #202020;">=</span> CalibrationCurve

    @<span style="color: #ba35af;">classmethod</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">build</span><span style="color: #008858;">(</span>cls, <span style="color: #202020;">**</span>kwargs<span style="color: #008858;">)</span>:
        <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Generate readings that produce a valid fit
</span>        <span style="color: #1f77bb;">readings</span> <span style="color: #202020;">=</span> kwargs.get<span style="color: #008858;">(</span><span style="color: #4250ef;">'readings'</span><span style="color: #008858;">)</span> <span style="color: #6052cf;">or</span> <span style="color: #008858;">[</span>
            CalibrationPointFactory.build<span style="color: #4f54aa;">(</span>concentration<span style="color: #202020;">=</span><span style="color: #ba35af;">float</span><span style="color: #ba35af;">(</span>i<span style="color: #ba35af;">)</span>, response<span style="color: #202020;">=</span><span style="color: #ba35af;">float</span><span style="color: #ba35af;">(</span>i <span style="color: #202020;">*</span> 2.5 <span style="color: #202020;">+</span> 1.0<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
            <span style="color: #6052cf;">for</span> i <span style="color: #6052cf;">in</span> <span style="color: #ba35af;">range</span><span style="color: #4f54aa;">(</span>5<span style="color: #4f54aa;">)</span>
        <span style="color: #008858;">]</span>
        <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Calculate actual fit parameters
</span>        <span style="color: #1f77bb;">conc</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.concentration <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">resp</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.response <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">slope</span>, <span style="color: #1f77bb;">intercept</span> <span style="color: #202020;">=</span> np.polyfit<span style="color: #008858;">(</span>conc, resp, 1<span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">pred</span> <span style="color: #202020;">=</span> slope <span style="color: #202020;">*</span> conc <span style="color: #202020;">+</span> intercept
        <span style="color: #1f77bb;">ss_res</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>resp <span style="color: #202020;">-</span> pred<span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">ss_tot</span> <span style="color: #202020;">=</span> np.<span style="color: #ba35af;">sum</span><span style="color: #008858;">(</span><span style="color: #4f54aa;">(</span>resp <span style="color: #202020;">-</span> np.mean<span style="color: #ba35af;">(</span>resp<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span> <span style="color: #202020;">**</span> 2<span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">r_squared</span> <span style="color: #202020;">=</span> 1 <span style="color: #202020;">-</span> <span style="color: #008858;">(</span>ss_res <span style="color: #202020;">/</span> ss_tot<span style="color: #008858;">)</span> <span style="color: #6052cf;">if</span> ss_tot <span style="color: #202020;">&gt;</span> 0 <span style="color: #6052cf;">else</span> 1.0

        <span style="color: #6052cf;">return</span> <span style="color: #ba35af;">super</span><span style="color: #008858;">()</span>.build<span style="color: #008858;">(</span>
            readings<span style="color: #202020;">=</span>readings,
            slope<span style="color: #202020;">=</span>slope,
            intercept<span style="color: #202020;">=</span>intercept,
            r_squared<span style="color: #202020;">=</span>r_squared,
            <span style="color: #202020;">**</span><span style="color: #4f54aa;">{</span>k: v <span style="color: #6052cf;">for</span> k, v <span style="color: #6052cf;">in</span> kwargs.items<span style="color: #ba35af;">()</span> <span style="color: #6052cf;">if</span> k <span style="color: #6052cf;">not</span> <span style="color: #6052cf;">in</span> <span style="color: #ba35af;">[</span><span style="color: #4250ef;">'readings'</span>, <span style="color: #4250ef;">'slope'</span>, <span style="color: #4250ef;">'intercept'</span>, <span style="color: #4250ef;">'r_squared'</span><span style="color: #ba35af;">]</span><span style="color: #4f54aa;">}</span>
        <span style="color: #008858;">)</span>


<span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">TESTS
</span><span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">=============================================================================
</span>
<span style="color: #6052cf;">class</span> <span style="color: #008858;">TestPolyfactoryCoverage</span>:
    <span style="color: #7fff7fff7fff;">"""Tests using Polyfactory's systematic coverage."""</span>

    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_qc_workflow_all_combinations</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span>:
        <span style="color: #7fff7fff7fff;">"""Test QC workflow with polyfactory coverage - structural edge cases.

        Note: We iterate over QCStatus values manually because CalibrationCurve
        has complex invariants (R&#178; consistency, minimum readings) that coverage()
        can't satisfy automatically. This demonstrates intentional structural
        coverage of the state machine.
        """</span>
        <span style="color: #1f77bb;">tested_statuses</span> <span style="color: #202020;">=</span> <span style="color: #008858;">[]</span>

        <span style="color: #6052cf;">for</span> status <span style="color: #6052cf;">in</span> QCStatus:
            <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Build a valid curve with this status
</span>            <span style="color: #1f77bb;">curve</span> <span style="color: #202020;">=</span> CalibrationCurveFactory.build<span style="color: #008858;">(</span>status<span style="color: #202020;">=</span>status<span style="color: #008858;">)</span>
            tested_statuses.append<span style="color: #008858;">(</span>status<span style="color: #008858;">)</span>

            <span style="color: #6052cf;">if</span> curve.status <span style="color: #202020;">==</span> QCStatus.PENDING:
                <span style="color: #1f77bb;">validated</span> <span style="color: #202020;">=</span> validate_curve<span style="color: #008858;">(</span>curve<span style="color: #008858;">)</span>
                <span style="color: #6052cf;">assert</span> validated.status <span style="color: #6052cf;">in</span> <span style="color: #008858;">[</span>QCStatus.VALIDATED, QCStatus.FLAGGED<span style="color: #008858;">]</span>

                <span style="color: #6052cf;">if</span> validated.status <span style="color: #202020;">==</span> QCStatus.VALIDATED:
                    <span style="color: #1f77bb;">approved</span> <span style="color: #202020;">=</span> approve_curve<span style="color: #008858;">(</span>validated, <span style="color: #4250ef;">"Meets all QC criteria"</span><span style="color: #008858;">)</span>
                    <span style="color: #6052cf;">assert</span> approved.status <span style="color: #202020;">==</span> QCStatus.APPROVED
                    <span style="color: #6052cf;">assert</span> approved.reviewer_notes <span style="color: #6052cf;">is</span> <span style="color: #6052cf;">not</span> <span style="color: #065fff;">None</span>

        <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Verify we tested all status values
</span>        <span style="color: #6052cf;">assert</span> <span style="color: #ba35af;">set</span><span style="color: #008858;">(</span>tested_statuses<span style="color: #008858;">)</span> <span style="color: #202020;">==</span> <span style="color: #ba35af;">set</span><span style="color: #008858;">(</span>QCStatus<span style="color: #008858;">)</span>


<span style="color: #6052cf;">class</span> <span style="color: #008858;">TestHypothesisProperties</span>:
    <span style="color: #7fff7fff7fff;">"""Property-based tests using Hypothesis."""</span>

    <span style="color: #008858;">@given</span><span style="color: #008858;">(</span>st.builds<span style="color: #4f54aa;">(</span>
        CalibrationPoint,
        concentration<span style="color: #202020;">=</span>st.floats<span style="color: #ba35af;">(</span>min_value<span style="color: #202020;">=</span>0, max_value<span style="color: #202020;">=</span>1000, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #ba35af;">)</span>,
        response<span style="color: #202020;">=</span>st.floats<span style="color: #ba35af;">(</span>min_value<span style="color: #202020;">=</span>0, max_value<span style="color: #202020;">=</span>10000, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #ba35af;">)</span>,
        replicate<span style="color: #202020;">=</span>st.integers<span style="color: #ba35af;">(</span>min_value<span style="color: #202020;">=</span>1, max_value<span style="color: #202020;">=</span>10<span style="color: #ba35af;">)</span>
    <span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
    <span style="color: #008858;">@settings</span><span style="color: #008858;">(</span>max_examples<span style="color: #202020;">=</span>50<span style="color: #008858;">)</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_calibration_point_concentration_non_negative</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>, point: CalibrationPoint<span style="color: #008858;">)</span>:
        <span style="color: #7fff7fff7fff;">"""Hypothesis: concentration must be non-negative."""</span>
        <span style="color: #6052cf;">assert</span> point.concentration <span style="color: #202020;">&gt;=</span> 0

    <span style="color: #008858;">@given</span><span style="color: #008858;">(</span>st.builds<span style="color: #4f54aa;">(</span>
        CalibrationPoint,
        concentration<span style="color: #202020;">=</span>st.floats<span style="color: #ba35af;">(</span>min_value<span style="color: #202020;">=</span>0, max_value<span style="color: #202020;">=</span>1000, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #ba35af;">)</span>,
        response<span style="color: #202020;">=</span>st.floats<span style="color: #ba35af;">(</span>min_value<span style="color: #202020;">=</span>0, max_value<span style="color: #202020;">=</span>10000, allow_nan<span style="color: #202020;">=</span><span style="color: #065fff;">False</span><span style="color: #ba35af;">)</span>,
        replicate<span style="color: #202020;">=</span>st.integers<span style="color: #ba35af;">(</span>min_value<span style="color: #202020;">=</span>1, max_value<span style="color: #202020;">=</span>10<span style="color: #ba35af;">)</span>
    <span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
    <span style="color: #008858;">@settings</span><span style="color: #008858;">(</span>max_examples<span style="color: #202020;">=</span>50<span style="color: #008858;">)</span>
    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_calibration_point_response_is_finite</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span>, point: CalibrationPoint<span style="color: #008858;">)</span>:
        <span style="color: #7fff7fff7fff;">"""Hypothesis: response values are finite numbers."""</span>
        <span style="color: #6052cf;">assert</span> np.isfinite<span style="color: #008858;">(</span>point.response<span style="color: #008858;">)</span>


<span style="color: #6052cf;">class</span> <span style="color: #008858;">TestIcontractInvariants</span>:
    <span style="color: #7fff7fff7fff;">"""Tests verifying icontract catches invalid states."""</span>

    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_contracts_catch_invalid_r_squared</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span>:
        <span style="color: #7fff7fff7fff;">"""Verify contracts catch scientifically invalid R&#178; values."""</span>
        <span style="color: #1f77bb;">readings</span> <span style="color: #202020;">=</span> <span style="color: #008858;">[</span>
            CalibrationPoint<span style="color: #4f54aa;">(</span>concentration<span style="color: #202020;">=</span>1.0, response<span style="color: #202020;">=</span>2.5<span style="color: #4f54aa;">)</span>,
            CalibrationPoint<span style="color: #4f54aa;">(</span>concentration<span style="color: #202020;">=</span>2.0, response<span style="color: #202020;">=</span>5.0<span style="color: #4f54aa;">)</span>,
        <span style="color: #008858;">]</span>

        <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Try to create a curve with fake R&#178; that doesn't match the data
</span>        <span style="color: #6052cf;">with</span> pytest.raises<span style="color: #008858;">(</span>icontract.ViolationError<span style="color: #008858;">)</span> <span style="color: #6052cf;">as</span> exc_info:
            CalibrationCurve<span style="color: #008858;">(</span>
                curve_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"cal-001"</span>,
                analyst_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"analyst-1"</span>,
                readings<span style="color: #202020;">=</span>readings,
                slope<span style="color: #202020;">=</span>2.5,
                intercept<span style="color: #202020;">=</span>0.0,
                r_squared<span style="color: #202020;">=</span>0.5,  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Wrong! Actual R&#178; is ~1.0
</span>                created_at<span style="color: #202020;">=</span>datetime.now<span style="color: #4f54aa;">()</span>
            <span style="color: #008858;">)</span>
        <span style="color: #6052cf;">assert</span> <span style="color: #4250ef;">"R&#178; must match actual fit quality"</span> <span style="color: #6052cf;">in</span> <span style="color: #ba35af;">str</span><span style="color: #008858;">(</span>exc_info.value<span style="color: #008858;">)</span>

    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_contracts_require_minimum_readings</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span>:
        <span style="color: #7fff7fff7fff;">"""Verify contracts require at least 2 calibration points."""</span>
        <span style="color: #6052cf;">with</span> pytest.raises<span style="color: #008858;">(</span>icontract.ViolationError<span style="color: #008858;">)</span> <span style="color: #6052cf;">as</span> exc_info:
            CalibrationCurve<span style="color: #008858;">(</span>
                curve_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"cal-002"</span>,
                analyst_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"analyst-1"</span>,
                readings<span style="color: #202020;">=</span><span style="color: #4f54aa;">[</span>CalibrationPoint<span style="color: #ba35af;">(</span>concentration<span style="color: #202020;">=</span>1.0, response<span style="color: #202020;">=</span>2.5<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">]</span>,  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Only 1!
</span>                slope<span style="color: #202020;">=</span>2.5,
                intercept<span style="color: #202020;">=</span>0.0,
                r_squared<span style="color: #202020;">=</span>1.0,
                created_at<span style="color: #202020;">=</span>datetime.now<span style="color: #4f54aa;">()</span>
            <span style="color: #008858;">)</span>
        <span style="color: #6052cf;">assert</span> <span style="color: #4250ef;">"Need at least 2 calibration points"</span> <span style="color: #6052cf;">in</span> <span style="color: #ba35af;">str</span><span style="color: #008858;">(</span>exc_info.value<span style="color: #008858;">)</span>

    <span style="color: #6052cf;">def</span> <span style="color: #cf25aa;">test_validate_requires_pending_status</span><span style="color: #008858;">(</span><span style="color: #6052cf;">self</span><span style="color: #008858;">)</span>:
        <span style="color: #7fff7fff7fff;">"""Verify validate_curve requires pending status."""</span>
        <span style="color: #1f77bb;">readings</span> <span style="color: #202020;">=</span> <span style="color: #008858;">[</span>
            CalibrationPointFactory.build<span style="color: #4f54aa;">(</span>concentration<span style="color: #202020;">=</span><span style="color: #ba35af;">float</span><span style="color: #ba35af;">(</span>i<span style="color: #ba35af;">)</span>, response<span style="color: #202020;">=</span><span style="color: #ba35af;">float</span><span style="color: #ba35af;">(</span>i <span style="color: #202020;">*</span> 2.5 <span style="color: #202020;">+</span> 1.0<span style="color: #ba35af;">)</span><span style="color: #4f54aa;">)</span>
            <span style="color: #6052cf;">for</span> i <span style="color: #6052cf;">in</span> <span style="color: #ba35af;">range</span><span style="color: #4f54aa;">(</span>5<span style="color: #4f54aa;">)</span>
        <span style="color: #008858;">]</span>
        <span style="color: #1f77bb;">conc</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.concentration <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">resp</span> <span style="color: #202020;">=</span> np.array<span style="color: #008858;">(</span><span style="color: #4f54aa;">[</span>r.response <span style="color: #6052cf;">for</span> r <span style="color: #6052cf;">in</span> readings<span style="color: #4f54aa;">]</span><span style="color: #008858;">)</span>
        <span style="color: #1f77bb;">slope</span>, <span style="color: #1f77bb;">intercept</span> <span style="color: #202020;">=</span> np.polyfit<span style="color: #008858;">(</span>conc, resp, 1<span style="color: #008858;">)</span>

        <span style="color: #1f77bb;">curve</span> <span style="color: #202020;">=</span> CalibrationCurve<span style="color: #008858;">(</span>
            curve_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"cal-003"</span>,
            analyst_id<span style="color: #202020;">=</span><span style="color: #4250ef;">"analyst-1"</span>,
            readings<span style="color: #202020;">=</span>readings,
            slope<span style="color: #202020;">=</span>slope,
            intercept<span style="color: #202020;">=</span>intercept,
            r_squared<span style="color: #202020;">=</span>1.0,
            status<span style="color: #202020;">=</span>QCStatus.VALIDATED,  <span style="color: #a65f6a;"># </span><span style="color: #7fff7fff7fff;">Not pending!
</span>            created_at<span style="color: #202020;">=</span>datetime.now<span style="color: #4f54aa;">()</span>
        <span style="color: #008858;">)</span>

        <span style="color: #6052cf;">with</span> pytest.raises<span style="color: #008858;">(</span>icontract.ViolationError<span style="color: #008858;">)</span> <span style="color: #6052cf;">as</span> exc_info:
            validate_curve<span style="color: #008858;">(</span>curve<span style="color: #008858;">)</span>
        <span style="color: #6052cf;">assert</span> <span style="color: #4250ef;">"Can only validate pending curves"</span> <span style="color: #6052cf;">in</span> <span style="color: #ba35af;">str</span><span style="color: #008858;">(</span>exc_info.value<span style="color: #008858;">)</span>
</pre>
</div>

<p>
Now we run the tests with pytest:
</p>

<div class="org-src-container">
<pre class="src src-bash"><span style="color: #ba35af;">cd</span> ~/projects/lab-data &amp;&amp; poetry run pytest test_data_model_integration.py -vvvv -q --disable-warnings --tb=short 2&gt;&amp;1
</pre>
</div>

<div class="org-src-container">
<pre class="src src-bash">============================= test session <span style="color: #1f77bb;">starts</span> ==============================
platform darwin -- Python 3.11.6, pytest-9.0.2, pluggy-1.6.0 -- ~/projects/lab-data/.venv/bin/python
cachedir: .pytest_cache
hypothesis profile <span style="color: #4250ef;">'default'</span>
rootdir: ~/projects/lab-data
configfile: pyproject.toml
plugins: Faker-37.11.0, hypothesis-6.142.3
collecting ... collected 6 items

test_data_model_integration.py::TestPolyfactoryCoverage::test_qc_workflow_all_combinations PASSED <span style="color: #008858;">[</span> 16%<span style="color: #008858;">]</span>
test_data_model_integration.py::TestHypothesisProperties::test_calibration_point_concentration_non_negative PASSED <span style="color: #008858;">[</span> 33%<span style="color: #008858;">]</span>
test_data_model_integration.py::TestHypothesisProperties::test_calibration_point_response_is_finite PASSED <span style="color: #008858;">[</span> 50%<span style="color: #008858;">]</span>
test_data_model_integration.py::TestIcontractInvariants::test_contracts_catch_invalid_r_squared PASSED <span style="color: #008858;">[</span> 66%<span style="color: #008858;">]</span>
test_data_model_integration.py::TestIcontractInvariants::test_contracts_require_minimum_readings PASSED <span style="color: #008858;">[</span> 83%<span style="color: #008858;">]</span>
test_data_model_integration.py::TestIcontractInvariants::test_validate_requires_pending_status PASSED <span style="color: #008858;">[</span>100%<span style="color: #008858;">]</span>

======================== 6 passed, 3 warnings<span style="color: #6052cf;"> in</span> 1.40s =========================
</pre>
</div>
</div>
</div>
<div id="outline-container-orgb92991f" class="outline-3">
<h3 id="orgb92991f"><span class="section-number-3">6.6.</span> Caveats with icontract</h3>
<div class="outline-text-3" id="text-6-6">
<p>
<b>An honest caveat:</b> icontract has the <b>same opacity problem</b> as <code>@model_validator</code>.  An <code>@invariant</code> is arbitrary Python &#x2013; it does not serialize to Avro, JSON Schema, or Protobuf either.  Moving the logic out of Pydantic doesn't make it visible to downstream consumers; it just stops it from polluting the schema export.  If a downstream service in another language needs to enforce "R² matches the fit", you still have to re-implement it there (or push the check into a shared validation service).  This is a real limit of <i>any</i> in-process invariant approach &#x2013; the alternative is consumer-driven contract testing (Pact and friends), which is a different toolchain entirely and out of scope here.
</p>

<p>
<b>And a runtime cost.</b> <code>@invariant</code> checks run on <i>every</i> method call on the instance, not just construction.  The <code>r_squared_is_consistent</code> check below does a numpy polyfit on every invocation; on a hot path (a Kafka consumer processing thousands of messages per second) this is a real cost.  Note that <code>python -O</code> does <i>not</i> help here &#x2013; icontract raises <code>ViolationError</code> unconditionally and does not piggyback on <code>assert</code>.  The right knobs are icontract's own <code>enabled</code> argument on each contract, the <code>ICONTRACT_SLOW</code> environment variable for gating expensive checks, or building two configurations of your application.  Either way: if you switch contracts off in prod, your "runtime safety net" is only a <i>test-time</i> safety net, and you need to be honest with yourself about that.
</p>
</div>
</div>
<div id="outline-container-org7b03dc0" class="outline-3">
<h3 id="org7b03dc0"><span class="section-number-3">6.7.</span> Alternatives to runtime contract enforcement</h3>
<div class="outline-text-3" id="text-6-7">
<p>
<b>Alternatives worth weighing.</b>  Before reaching for runtime contracts, three other approaches deserve a serious look:
</p>

<ul class="org-ul">
<li><b>Schema-first with codegen</b>.  Treat Avro/Protobuf/JSON Schema as the source of truth and generate Pydantic (or your language's equivalent) from it.  Cross-system drift becomes structurally impossible because there's only one definition.  This is the right answer when your data crosses many language boundaries.  Its weakness is exactly the one this post is about: schema languages can't express cross-field invariants either, so you still need <i>something</i> for "R² must match the fit."</li>
<li><b>Consumer-driven contract testing</b> (Pact and friends).  Push the enforcement to the boundary between services rather than inside any one of them.  This is the right answer when the question is "do producer and consumer agree?"  It's the wrong answer when the question is "is this single object internally coherent?", which is what we have here.</li>
<li><b>In-process invariants</b> (icontract, <code>@model_validator</code>, plain assertions in <code>__init__</code>).  Cheapest to add, lives next to the data, and &#x2013; as discussed above &#x2013; invisible to anything outside your Python process.</li>
</ul>

<p>
These are not mutually exclusive.  A mature system typically has schema-first definitions at the wire, CDC tests at the service boundaries, and in-process invariants for the rules that live entirely inside one bounded context.  This post is about that last layer.
</p>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">7.</span> Conclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="summary">summary</span></span></h2>
<div class="outline-text-2" id="text-conclusion">
<p>
The example here is a calibration curve, but the pattern is not domain-specific.  Anywhere a data model carries a rule the type system can't express, the same three layers apply:
</p>

<ul class="org-ul">
<li>An <code>Order</code> where <code>discount &lt;</code> subtotal=, <code>tax</code> is a function of <code>subtotal - discount</code>, and <code>total</code> is the sum.  Pydantic accepts any three floats; only a cross-field check rejects the inconsistent invoice.</li>
<li>An <code>OAuthToken</code> where the granted <code>scopes</code> must be a subset of the client's <code>allowed_scopes</code>, and <code>expires_at &gt; issued_at</code>.  Each field is structurally fine in isolation.</li>
<li>An inventory <code>StockMovement</code> where <code>on_hand_after = on_hand_before + delta</code>.  Off-by-one in a service layer produces a "valid" object that silently corrupts every downstream report.</li>
</ul>

<p>
In each case the bug looks the same as the calibration-curve bug: every field passes its own validator, the JSON serialises cleanly, and the wrongness only shows up when you ask whether the fields agree with each other.  That is the bug class this stack is for.
</p>

<p>
The three layers, each catching a different class:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Technique</th>
<th scope="col" class="org-left">Catches</th>
<th scope="col" class="org-left">When</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Polyfactory</td>
<td class="org-left">Structural combinations</td>
<td class="org-left">Test generation</td>
</tr>

<tr>
<td class="org-left">Hypothesis</td>
<td class="org-left">Value-level edge cases</td>
<td class="org-left">Test execution</td>
</tr>

<tr>
<td class="org-left">icontract</td>
<td class="org-left">Cross-field invariants</td>
<td class="org-left">Runtime</td>
</tr>
</tbody>
</table>

<p>
Three independent failure modes, three independent tools.  Start with polyfactory's <code>coverage()</code> for structural completeness.  Add Hypothesis for value-level probing.  Use icontract for invariants that can't be expressed in types &#x2013; the swapped <code>slope</code> and <code>intercept</code>, the <code>discount &gt; subtotal</code>, the <code>StockMovement</code> that doesn't add up.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">8.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
<b>TLDR</b>: A modest Pydantic model &#x2013; a dozen fields with a few enums and optionals &#x2013; has <a href="https://www.chiply.dev/#growth-analysis">7,680 valid structural shapes</a>.  Your tests probably cover four of them.  This post is a three-layer pattern for closing that gap &#x2013; and an honest accounting of where each layer does and doesn't earn its keep.
</p>

<p>
<a href="https://www.chiply.dev/#property-based-testing-with-polyfactory">Polyfactory's <code>coverage()</code></a> automates 1-way structural partition coverage so you stop hand-writing fixtures for "every enum value × every nullable state".  <a href="https://www.chiply.dev/#value-level-testing-with-hypothesis">Hypothesis</a> adds value-level probing &#x2013; boundary floats, NaN, unicode &#x2013; and shrinks failing cases to minimal examples.  <a href="https://www.chiply.dev/#design-by-contract-with-icontract">icontract</a> enforces <a href="https://www.chiply.dev/#the-testing-gap--when-models-aren-t-enough">cross-field invariants</a> (like "R² must match the actual fit") that no type system or serializable schema can express.
</p>

<p>
The <a href="https://www.chiply.dev/#a-complete-example">worked example</a> is a scientific calibration curve, with all three tools running in a real pytest file you can copy.  The post is also explicit about the limits: equivalence partitioning has been standard since the 1970s, <code>@invariant</code> has the same schema-opacity problem as <code>@model_validator</code>, and runtime contracts cost real CPU on hot paths.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Data standards are the connective tissue of cross-system integration, and in my experience the right default is to use them all the way down &#x2013; as the source of truth, not as a downstream artefact.  You don't necessarily write the schema files by hand; they can be generated from Python (e.g. from Pydantic models).  But if you go that route, it is essential that every constraint your in-memory model enforces also appears in the exported schema.  Anything that doesn't survive serialisation is a constraint your downstream consumers cannot see.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Pattern Inheritance: A Fork-Based Strategy for Scaling Service Standards</title>
      <link>https://www.chiply.dev/post-pattern-inheritance</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-pattern-inheritance</guid>
      <pubDate>Tue, 07 Apr 2026 03:56:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Your payments team patches a JWT library CVE on Thursday. Your inventory team patches the same CVE on Monday. Your notifications team hasn&apos;t noticed yet. All three services started life as a git clone...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="platformEngineering">platformEngineering</span>&#xa0;<span class="architecture">architecture</span>&#xa0;<span class="templates">templates</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org2f8d27c" class="figure">
<p><img src="https://www.chiply.dev/images/pattern-inheritance-banner.jpeg" alt="pattern-inheritance-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Your payments team patches a JWT library CVE on Thursday.  Your inventory team patches the same CVE on Monday.  Your notifications team hasn't noticed yet.  All three services started life as a <code>git clone</code> of the same internal template, and the day after the clone, they stopped sharing anything at all.
</p>

<p>
This post is about the workflow I'd reach for to stop that happening: a three-tier <b>fork</b> hierarchy &#x2013; community template, org fork, team forks &#x2013; that lets a platform team push security and infrastructure changes downstream through ordinary <code>git fetch</code> and <code>git merge</code>, the way a subclass picks up a base-class fix.  I'll also cover why I'd reach for it <b>over</b> the obvious alternatives (<a href="https://github.com/cruft/cruft">cruft</a>, internal packages, monorepos), because that's the first question anyone reading this should ask.
</p>
</div>
</div>
<div id="outline-container-the-problem" class="outline-2">
<h2 id="the-problem"><span class="section-number-2">2.</span> The Problem: Service Proliferation Without Standards&#xa0;&#xa0;&#xa0;<span class="tag"><span class="architecture">architecture</span>&#xa0;<span class="platformEngineering">platformEngineering</span></span></h2>
<div class="outline-text-2" id="text-the-problem">
<p>
Every growing engineering organization hits the same inflection point<sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>.  A second team needs to build an API service.  Then a third.  Then a tenth.
</p>

<p>
Without intervention, each team makes its own choices about project structure, dependency management, database access patterns, authentication middleware, health check endpoints, logging configuration, error handling, and deployment manifests.  The resulting landscape looks something like this:
</p>

<div class="org-src-container">
<pre class="src src-mermaid">graph TD
    subgraph "Without Standards"
        T1[Team Alpha] --&gt;|builds from scratch| S1[Service A&lt;br/&gt;SQLAlchemy sync&lt;br/&gt;custom logging&lt;br/&gt;no health checks]
        T2[Team Beta] --&gt;|builds from scratch| S2[Service B&lt;br/&gt;raw asyncpg&lt;br/&gt;structlog&lt;br/&gt;custom auth middleware]
        T3[Team Gamma] --&gt;|builds from scratch| S3[Service C&lt;br/&gt;Tortoise ORM&lt;br/&gt;stdlib logging&lt;br/&gt;JWT auth]
        T4[Team Delta] --&gt;|builds from scratch| S4[Service D&lt;br/&gt;SQLModel&lt;br/&gt;loguru&lt;br/&gt;API key auth]
    end
</pre>
</div>

<p>
Every service works.  None of them work the same way.  The consequences compound:
</p>

<ul class="org-ul">
<li><b>Onboarding cost</b>: Engineers moving between teams have to relearn fundamental code patterns.</li>
<li><b>Operational burden</b>: SREs can't write generic runbooks when every service has different logging, health checks, and failure modes.</li>
<li><b>Security surface</b>: Each team independently solves authentication, input validation, and secrets management &#x2013; and each implementation has its own bugs.</li>
<li><b>Upgrade paralysis</b>: When a critical dependency needs patching (say, a CVE in your database driver), there's no single place to make the fix.  You're patching N services with N different integration approaches.</li>
</ul>

<p>
The usual response is to write an internal wiki page titled "How to Build a FastAPI Service."  That doesn't work.  Documentation decays<sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup>, and even when engineers read it, translating prose into working code introduces drift on day one.  You want running code, not running prose.
</p>
</div>
</div>
<div id="outline-container-the-running-example" class="outline-2">
<h2 id="the-running-example"><span class="section-number-2">3.</span> The Running Example: FastAPI + Async Postgres&#xa0;&#xa0;&#xa0;<span class="tag"><span class="fastAPI">fastAPI</span>&#xa0;<span class="postgres">postgres</span>&#xa0;<span class="asyncio">asyncio</span></span></h2>
<div class="outline-text-2" id="text-the-running-example">
<p>
To make this concrete, let's work with a stack that's extremely common in modern Python shops: a <a href="https://fastapi.tiangolo.com/">FastAPI</a> service backed by <a href="https://www.postgresql.org/">PostgreSQL</a>, with asynchronous database access for performance<sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.
</p>

<p>
This seemingly simple stack requires a surprising number of interlocking decisions.  The table below reflects opinionated defaults for this particular stack &#x2013; your org may reasonably choose differently, but the point is that each choice constrains the others:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Concern</th>
<th scope="col" class="org-left">Options</th>
<th scope="col" class="org-left">Opinionated default</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Async DB driver</td>
<td class="org-left"><code>asyncpg</code>, <a href="https://www.psycopg.org/psycopg3/docs/">psycopg3</a> (async mode), <code>aiopg</code></td>
<td class="org-left"><a href="https://magicstack.github.io/asyncpg/current/">asyncpg</a> (fastest, most mature)</td>
</tr>

<tr>
<td class="org-left">ORM / query builder</td>
<td class="org-left"><code>SQLAlchemy</code> 2.0 async, <a href="https://sqlmodel.tiangolo.com/">SQLModel</a>, <a href="https://tortoise.github.io/">Tortoise ORM</a>, raw SQL</td>
<td class="org-left"><a href="https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html">SQLAlchemy 2.0</a> (ecosystem, flexibility)</td>
</tr>

<tr>
<td class="org-left">Migrations</td>
<td class="org-left"><code>Alembic</code> (async-aware), <a href="https://github.com/tortoise/aerich">aerich</a>, manual SQL</td>
<td class="org-left"><a href="https://alembic.sqlalchemy.org/en/latest/">Alembic</a></td>
</tr>

<tr>
<td class="org-left">Connection pooling</td>
<td class="org-left"><code>SQLAlchemy</code> built-in async pool, <a href="https://www.pgbouncer.org/">pgBouncer</a> sidecar</td>
<td class="org-left">Both (app-level + infra-level)</td>
</tr>

<tr>
<td class="org-left">Config management</td>
<td class="org-left"><code>pydantic-settings</code>, env vars, Vault, AWS SSM</td>
<td class="org-left"><a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/">pydantic-settings</a> + Vault</td>
</tr>

<tr>
<td class="org-left">Testing</td>
<td class="org-left"><code>pytest</code> + <code>httpx</code> + <code>testcontainers</code></td>
<td class="org-left"><a href="https://pytest-asyncio.readthedocs.io/en/latest/">pytest-asyncio</a> + <a href="https://www.python-httpx.org/">httpx.AsyncClient</a></td>
</tr>
</tbody>
</table>

<p>
Getting each of these right takes iteration.  Getting them all right <b>together</b>, in a way that's coherent and production-ready, takes substantially more.  This is the kind of compound problem where you should be reaching for someone else's working answers, not building your own.
</p>
</div>
</div>
<div id="outline-container-reaching-for-templates" class="outline-2">
<h2 id="reaching-for-templates"><span class="section-number-2">4.</span> Templates Exist, but Cloning Strands You&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cookiecutter">cookiecutter</span>&#xa0;<span class="templates">templates</span></span></h2>
<div class="outline-text-2" id="text-reaching-for-templates">
<p>
The open source community has already solved the general version of this problem.  <a href="https://github.com/fastapi/full-stack-fastapi-template">full-stack-fastapi-template</a> (maintained by FastAPI's author) ships SQLModel, Alembic, async SQLAlchemy, JWT auth, pydantic-settings, Docker Compose, and pytest infrastructure in a single repo.  The <a href="https://github.com/cookiecutter/cookiecutter">Cookiecutter</a> ecosystem offers parameterized variants like <a href="https://github.com/arthurhenrique/cookiecutter-fastapi">cookiecutter-fastapi</a>.  Pick one of these and you've skipped a couple of months of dependency-archaeology and connection-pool tuning.
</p>

<p>
The catch is what you get the day after you adopt it.  <code>git clone</code> and <code>cookiecutter generate</code> both produce a <b>snapshot</b>: a point-in-time copy of the template's state with no live link back to its origin.
</p>

<div class="org-src-container">
<pre class="src src-mermaid">graph LR
    subgraph "Clone / Generate (Point-in-Time Snapshot)"
        direction LR
        Template[Community Template&lt;br/&gt;v1.0] --&gt;|clone / generate| TeamRepo[Team's Service]
        Template --&gt;|v1.1: security fix| Nowhere1[❌ Not inherited]
        Template --&gt;|v1.2: perf improvement| Nowhere2[❌ Not inherited]
        Template --&gt;|v1.3: new best practice| Nowhere3[❌ Not inherited]
    end
</pre>
</div>

<p>
When the community template later fixes a vulnerability in its auth middleware, swaps in a faster async driver, or improves its connection pool defaults, none of it reaches you.  Every improvement requires someone on your team to notice, understand, and manually port it.  In practice, that never happens.  The template was useful on day one and forgotten on day two.
</p>
</div>
<div id="outline-container-why-not-cruft" class="outline-3">
<h3 id="why-not-cruft"><span class="section-number-3">4.1.</span> "Why Not Just Use cruft, or Internal Packages?"</h3>
<div class="outline-text-3" id="text-why-not-cruft">
<p>
This is the right first question, and it deserves an answer before I go any further.
</p>

<p>
<a href="https://github.com/cruft/cruft">cruft</a> solves the snapshot problem by recording the template version a project was generated from and offering <code>cruft update</code> to apply later template diffs as a patch.  It's a real improvement over <code>cookiecutter generate</code>, and for a single layer of inheritance (community template → service) it's simpler than what I'm about to describe.  Where cruft starts to creak is at the second layer: when your <b>organization</b> wants to express opinions on top of the community template that every service then inherits.  cruft doesn't model "fork of a fork" naturally, and patch-based updates are less forgiving than three-way merges when conflicts get gnarly.  If you only need one layer of inheritance, use cruft and stop reading.
</p>

<p>
Internal packages (<code>acme-fastapi-core</code>, <code>acme-observability</code>) are the right answer for <b>runtime</b> concerns: middleware, auth wiring, observability hooks, database session helpers.  Anything that can live behind a function call should live behind a function call.  But packages don't ship project structure &#x2013; they don't give you a Dockerfile, a CI pipeline, a directory layout, an Alembic config, deployment manifests, or the boilerplate that wires the packages together.  Most mature platform teams end up doing <b>both</b>: a fork hierarchy for the scaffold, packages for the runtime behaviour.  This post is about the scaffold half.
</p>

<p>
Monorepos sidestep the problem entirely by removing the boundaries that allowed drift in the first place.  If you already have working monorepo tooling, you probably don't need this post.  If you don't, adopting Bazel or Pants to solve a template-drift problem is a heavy detour.
</p>

<p>
With those out of the way: here's the fork story.
</p>
</div>
</div>
</div>
<div id="outline-container-why-fork-not-clone" class="outline-2">
<h2 id="why-fork-not-clone"><span class="section-number-2">5.</span> Why Fork, Not Clone&#xa0;&#xa0;&#xa0;<span class="tag"><span class="git">git</span>&#xa0;<span class="versionControl">versionControl</span></span></h2>
<div class="outline-text-2" id="text-why-fork-not-clone">
<p>
A fork preserves a relationship; a clone severs it.  When you <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks">fork a repository</a><sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup> you get a copy with full history, a remote reference back to the original, and the ability to <code>git fetch upstream</code> and <code>git merge</code> as the upstream evolves.  That upstream remote is the entire mechanic this post relies on.
</p>

<p>
The mental model is loose object-oriented inheritance<sup><a id="fnr.5" class="footref" href="https://www.chiply.dev/#fn.5" role="doc-backlink">5</a></sup>: your fork inherits from upstream, you override specific files, and you manually choose when to absorb base-class changes.  The "manual" part is a feature &#x2013; automatic propagation of breaking changes into production infrastructure would be terrifying.
</p>
</div>
</div>
<div id="outline-container-three-tier-hierarchy" class="outline-2">
<h2 id="three-tier-hierarchy"><span class="section-number-2">6.</span> The Three-Tier Fork Hierarchy&#xa0;&#xa0;&#xa0;<span class="tag"><span class="architecture">architecture</span>&#xa0;<span class="platformEngineering">platformEngineering</span></span></h2>
<div class="outline-text-2" id="text-three-tier-hierarchy">
<p>
Here's the architecture I recommend.  It has three tiers:
</p>

<div class="org-src-container">
<pre class="src src-mermaid">graph TB
    subgraph Tier1["Tier 1: Community Upstream"]
        Upstream[Community Template&lt;br/&gt;e.g. tiangolo/full-stack-fastapi-template&lt;br/&gt;&lt;i&gt;Battle-tested patterns&lt;/i&gt;]
    end

    subgraph Tier2["Tier 2: Organization Fork"]
        OrgFork[Org Gold Standard&lt;br/&gt;e.g. acme-corp/fastapi-service-template&lt;br/&gt;&lt;i&gt;Org opinions applied&lt;/i&gt;]
    end

    subgraph Tier3["Tier 3: Team Forks"]
        TeamA[Team Alpha Fork&lt;br/&gt;payments-service]
        TeamB[Team Beta Fork&lt;br/&gt;inventory-service]
        TeamC[Team Gamma Fork&lt;br/&gt;notifications-service]
    end

    Upstream --&gt;|"fork + customize"| OrgFork
    OrgFork --&gt;|"fork per service"| TeamA
    OrgFork --&gt;|"fork per service"| TeamB
    OrgFork --&gt;|"fork per service"| TeamC

    Upstream -.-&gt;|"upstream sync"| OrgFork
    OrgFork -.-&gt;|"upstream sync"| TeamA
    OrgFork -.-&gt;|"upstream sync"| TeamB
    OrgFork -.-&gt;|"upstream sync"| TeamC
</pre>
</div>
</div>
<div id="outline-container-tier-1" class="outline-3">
<h3 id="tier-1"><span class="section-number-3">6.1.</span> Tier 1: Community Upstream</h3>
<div class="outline-text-3" id="text-tier-1">
<p>
This is the open source template you've chosen as your foundation.  You don't modify it directly.  It represents the community's best understanding of how to build this type of service<sup><a id="fnr.6" class="footref" href="https://www.chiply.dev/#fn.6" role="doc-backlink">6</a></sup>.
</p>

<p>
A warning before going further: a fork chain is also a <b>supply chain</b>, and the same mechanism that propagates security fixes downstream propagates malicious or buggy commits with equal efficiency.  Treat the choice of upstream like the choice of any other production dependency.  Concretely, before adopting an upstream:
</p>

<ul class="org-ul">
<li>Check that the project has more than one active maintainer, a recent release cadence, and evidence that someone reviews PRs.  A scaffold maintained by one person on weekends is a single point of failure for your fleet.</li>
<li>Pin to <b>signed tags</b>, not <code>upstream/main</code>.  Require commit signature verification at the Tier 2 boundary so an upstream account compromise can't ride a <code>git fetch</code> straight into your services.</li>
<li>Subscribe the org fork to the upstream's <a href="https://docs.github.com/en/code-security/security-advisories/working-with-repository-security-advisories/about-repository-security-advisories">GitHub Security Advisories</a> and <a href="https://osv.dev/">OSV</a> feeds.  Many projects ship security fixes silently in point releases without filing a CVE.</li>
<li>Run <a href="https://github.com/pypa/pip-audit">pip-audit</a> / <code>osv-scanner</code> on every Tier 2 merge.  An SBOM generated at this boundary is the only honest record of what you actually shipped to teams.  An <code>ORG_CHANGES.md</code> manifest documents intent; an SBOM documents reality.</li>
</ul>
</div>
</div>
<div id="outline-container-tier-2" class="outline-3">
<h3 id="tier-2"><span class="section-number-3">6.2.</span> Tier 2: Organization Fork (The Gold Standard)</h3>
<div class="outline-text-3" id="text-tier-2">
<p>
This is where your platform team applies organizational opinions on top of the community template.  This fork is maintained by your platform or infrastructure team and represents your org's "blessed" way to build a service of this type.
</p>

<p>
Typical org-level customizations include:
</p>

<ul class="org-ul">
<li><b>Authentication</b>: Swap JWT for your org's OIDC provider or service mesh auth.</li>
<li><b>Observability</b>: Add your org's <a href="https://opentelemetry.io/">OpenTelemetry</a> configuration, custom metrics, and tracing headers.</li>
<li><b>Secrets management</b>: Integrate with <a href="https://developer.hashicorp.com/vault">Vault</a>, AWS Secrets Manager, or whatever your org uses.</li>
<li><b>CI/CD</b>: Replace generic GitHub Actions with your org's pipeline templates.</li>
<li><b>Deployment manifests</b>: Add Kubernetes manifests, Helm charts, or Terraform modules that match your infrastructure.</li>
<li><b>Compliance</b>: Add security scanning, license checking, and audit logging requirements.</li>
<li><b>Internal libraries</b>: Wire in your org's shared Python packages for common concerns.</li>
</ul>

<p>
The platform team's job is to keep this fork synchronized with upstream while maintaining the org's customizations<sup><a id="fnr.7" class="footref" href="https://www.chiply.dev/#fn.7" role="doc-backlink">7</a></sup>.
</p>
</div>
</div>
<div id="outline-container-tier-3" class="outline-3">
<h3 id="tier-3"><span class="section-number-3">6.3.</span> Tier 3: Team Forks (Concrete Services)</h3>
<div class="outline-text-3" id="text-tier-3">
<p>
Individual teams fork the org's gold standard to create their specific services.  A team building a payments service forks <code>acme-corp/fastapi-service-template</code> into <code>acme-corp/payments-service</code>, then adds their domain-specific models, routes, and business logic.
</p>

<p>
Teams get the org's opinions "for free" and can focus entirely on their domain problem.  When the platform team pushes an improvement to the gold standard (say, upgrading the OpenTelemetry SDK or fixing a connection pool misconfiguration), teams can pull that change into their fork with a standard <code>git merge</code>.
</p>
</div>
</div>
</div>
<div id="outline-container-setting-up-the-hierarchy" class="outline-2">
<h2 id="setting-up-the-hierarchy"><span class="section-number-2">7.</span> Setting Up the Hierarchy&#xa0;&#xa0;&#xa0;<span class="tag"><span class="git">git</span>&#xa0;<span class="howTo">howTo</span></span></h2>
<div class="outline-text-2" id="text-setting-up-the-hierarchy">
</div>
<div id="outline-container-step-1-fork" class="outline-3">
<h3 id="step-1-fork"><span class="section-number-3">7.1.</span> Step 1: Fork the Community Template</h3>
<div class="outline-text-3" id="text-step-1-fork">
<p>
On GitHub, fork the community template into your org's namespace:
</p>

<p>
<code>tiangolo/full-stack-fastapi-template</code> → <code>acme-corp/fastapi-service-template</code>
</p>

<p>
Then clone your org fork locally:
</p>

<div class="org-src-container">
<pre class="src src-bash">git clone git@github.com:acme-corp/fastapi-service-template.git
cd fastapi-service-template
git remote add upstream git@github.com:fastapi/full-stack-fastapi-template.git
# Disable accidental pushes to upstream
git remote set-url --push upstream no_push
git remote -v
# origin    git@github.com:acme-corp/fastapi-service-template.git (fetch)
# origin    git@github.com:acme-corp/fastapi-service-template.git (push)
# upstream  git@github.com:fastapi/full-stack-fastapi-template.git (fetch)
# upstream  no_push (push)
</pre>
</div>
</div>
</div>
<div id="outline-container-step-2-branch-strategy" class="outline-3">
<h3 id="step-2-branch-strategy"><span class="section-number-3">7.2.</span> Step 2: Create an Org Customization Branch Strategy</h3>
<div class="outline-text-3" id="text-step-2-branch-strategy">
<p>
This is a critical decision<sup><a id="fnr.8" class="footref" href="https://www.chiply.dev/#fn.8" role="doc-backlink">8</a></sup>.  I recommend maintaining two long-lived branches in the org fork:
</p>

<ul class="org-ul">
<li><code>upstream-mirror</code>: Tracks the upstream template exactly.  Never commit org changes here.</li>
<li><code>main</code>: Contains the org's customizations on top of upstream.</li>
</ul>

<div class="org-src-container">
<pre class="src src-bash"># Create the mirror branch from upstream's main, NOT from your customized main
git fetch upstream
git checkout -b upstream-mirror upstream/main
git push origin upstream-mirror

# main branch is where org customizations live
git checkout main
</pre>
</div>

<p>
When you want to sync with upstream:
</p>

<div class="org-src-container">
<pre class="src src-bash"># Update the mirror
git checkout upstream-mirror
git fetch upstream
git merge upstream/main
git push origin upstream-mirror

# Merge upstream changes into org's main
git checkout main
git merge upstream-mirror
# Resolve conflicts, keeping org customizations where intentional
git push origin main
</pre>
</div>

<p>
The <code>upstream-mirror</code> ranch gives you a clean diff between "what upstream looks like" and "what our org version looks like."  This is invaluable for understanding exactly what your org has changed.
</p>

<p>
For simple, infrequent syncs, GitHub's <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork">"Sync fork"</a> button and the <a href="https://cli.github.com/manual/gh_repo_sync">gh repo sync</a> CLI command offer lighter-weight alternatives.  These work well when the org fork has minimal divergence, but for forks with significant customizations you'll want the mirror-branch workflow above to keep a clear audit trail.
</p>
</div>
</div>
<div id="outline-container-step-3-org-customizations" class="outline-3">
<h3 id="step-3-org-customizations"><span class="section-number-3">7.3.</span> Step 3: Apply Org Customizations</h3>
<div class="outline-text-3" id="text-step-3-org-customizations">
<p>
Now apply your org's opinions to <code>main</code>.  The single most important rule: <b>keep the diff between your fork and its upstream as small as possible.</b>  Every line you change in a file that upstream also changes is a future merge conflict.  Before modifying an upstream file, ask: "Can I achieve this by adding a new file instead?"  Often you can.  A new <code>org_middleware.py</code> that's imported by the entrypoint is better than editing the middleware directly.
</p>

<p>
Some specific guidelines:
</p>

<p>
<b>Prefer additive changes over modifications.</b>  Adding new files (a <code>deploy/</code> directory, an <code>internal_libs/</code> package, a <code>.github/workflows/ci.yml</code>) is low-conflict.  Modifying files that upstream also modifies (<code>main.py</code>, <code>requirements.txt</code>, <code>Dockerfile</code>) is high-conflict.
</p>

<p>
<b>Use configuration layers.</b>  Instead of editing <code>settings.py</code> directly, create an <code>org_settings.py</code> that imports and extends the base settings.  This isolates your changes from upstream's evolution of the settings module.
</p>

<p>
<b>Document every divergence.</b>  Maintain an <code>ORG_CHANGES.md</code> file that catalogs what you've changed and why.  When merge conflicts arise, this document tells you whether the conflict is in code you intentionally modified.  A typical manifest looks like:
</p>

<div class="org-src-container">
<pre class="src src-markdown">## Org Customizations

### Authentication (modified: app/core/security.py)
- Replaced JWT auth with OIDC integration via org's identity provider
- Added service-to-service mTLS validation

### Observability (added: app/observability/)
- OpenTelemetry auto-instrumentation with custom span attributes
- Structured logging via [[https://www.structlog.org/en/stable/][structlog]] with org-standard fields

### CI/CD (replaced: .github/workflows/)
- Upstream GitHub Actions replaced with GitLab CI
- Added SAST scanning, container scanning, license compliance

### Database (modified: app/core/db.py)
- Connection pool max_size increased from 10 to 50
- Added read-replica routing for GET endpoints
</pre>
</div>

<p>
This document is your merge conflict playbook.  When upstream changes <code>app/core/security.py</code> and you get a conflict, you check the manifest: "We intentionally replaced this with OIDC.  Ignore upstream's changes to the JWT implementation, but check if they've changed anything else in this file."
</p>
</div>
</div>
<div id="outline-container-step-4-team-forks" class="outline-3">
<h3 id="step-4-team-forks"><span class="section-number-3">7.4.</span> Step 4: Team Forks</h3>
<div class="outline-text-3" id="text-step-4-team-forks">
<p>
A note on mechanics: GitHub will not let you fork <code>acme-corp/fastapi-service-template</code> into another repo in the <b>same</b> org via the fork button &#x2013; a given upstream can only have one fork per namespace.  In practice, teams create a new empty repo (<code>acme-corp/payments-service</code>), then seed it from the org template using <code>git clone --bare</code> and <code>git push --mirror</code>, and finally add the org template as a long-lived remote.  You lose GitHub's fork-network UI for the Tier 2 → Tier 3 hop, but the git-level inheritance relationship is identical.
</p>

<div class="org-src-container">
<pre class="src src-bash"># One-time seeding of a new service repo from the org template
git clone --bare git@github.com:acme-corp/fastapi-service-template.git
cd fastapi-service-template.git
git push --mirror git@github.com:acme-corp/payments-service.git
cd .. &amp;&amp; rm -rf fastapi-service-template.git

# Then clone the new service repo and wire up the org-template remote
git clone git@github.com:acme-corp/payments-service.git
cd payments-service
git remote add org-template git@github.com:acme-corp/fastapi-service-template.git
git remote set-url --push org-template no_push
</pre>
</div>

<p>
Teams should similarly maintain an <code>org-template-mirror</code> branch and merge into <code>main</code>:
</p>

<div class="org-src-container">
<pre class="src src-bash">git checkout -b org-template-mirror
git fetch org-template
git merge org-template/main
git push origin org-template-mirror

git checkout main
git merge org-template-mirror
</pre>
</div>
</div>
</div>
<div id="outline-container-how-changes-flow" class="outline-3">
<h3 id="how-changes-flow"><span class="section-number-3">7.5.</span> How Changes Flow</h3>
<div class="outline-text-3" id="text-how-changes-flow">
<p>
With the hierarchy in place, improvements flow downstream through the fork chain.  Here's what different types of changes look like in practice:
</p>

<div class="org-src-container">
<pre class="src src-mermaid">graph LR
    subgraph "Change Propagation"
        direction LR
        U[Community&lt;br/&gt;Template] --&gt;|"1. Publishes&lt;br/&gt;security fix"| O[Org Fork]
        O --&gt;|"2. Platform team&lt;br/&gt;validates + merges"| OP[Org Main ✅]
        OP -.-&gt;|"3. Teams pull&lt;br/&gt;from org-template"| S1[payments-service ✅]
        OP -.-&gt;|"3. Teams pull&lt;br/&gt;from org-template"| S2[inventory-service ✅]
    end
</pre>
</div>
</div>
<div id="outline-container-scenario-security-fix" class="outline-4">
<h4 id="scenario-security-fix"><span class="section-number-4">7.5.1.</span> Scenario: Upstream Security Fix</h4>
<div class="outline-text-4" id="text-scenario-security-fix">
<ol class="org-ol">
<li>The community template patches a vulnerability in its authentication middleware.</li>
<li>Your platform team fetches upstream, merges into <code>upstream-mirror</code>, then merges into <code>main</code>.</li>
<li>The platform team validates the fix works with org customizations and pushes to <code>main</code>.</li>
<li>Teams fetch from <code>org-template</code> and merge into their services.</li>
</ol>

<p>
Total effort: one careful merge by the platform team + N trivial merges by service teams.  Without the fork chain, you'd need N independent teams to each discover, understand, and port the fix.
</p>
</div>
</div>
<div id="outline-container-scenario-observability-upgrade" class="outline-4">
<h4 id="scenario-observability-upgrade"><span class="section-number-4">7.5.2.</span> Scenario: Org-Wide Observability Upgrade</h4>
<div class="outline-text-4" id="text-scenario-observability-upgrade">
<ol class="org-ol">
<li>The platform team upgrades the OpenTelemetry SDK and adds new custom spans to the org fork.</li>
<li>Teams fetch and merge from <code>org-template</code>.</li>
<li>Every service gets improved observability without any team doing custom work.</li>
</ol>
</div>
</div>
<div id="outline-container-scenario-team-feature" class="outline-4">
<h4 id="scenario-team-feature"><span class="section-number-4">7.5.3.</span> Scenario: Team-Specific Feature</h4>
<div class="outline-text-4" id="text-scenario-team-feature">
<p>
A team adds domain-specific models, routes, and business logic.  These changes live entirely in the team's fork and never propagate upward.  The fork hierarchy doesn't interfere &#x2013; the team just has additional files that don't exist in the org template.
</p>
</div>
</div>
</div>
</div>
<div id="outline-container-gotchas-fork-compatibility" class="outline-2">
<h2 id="gotchas-fork-compatibility"><span class="section-number-2">8.</span> Gotchas That Break Fork Compatibility&#xa0;&#xa0;&#xa0;<span class="tag"><span class="pitfalls">pitfalls</span></span></h2>
<div class="outline-text-2" id="text-gotchas-fork-compatibility">
<p>
The fork-based approach works well when changes are additive.  It gets painful when upstream or org changes are <b>incompatible</b> with downstream forks.  Here are the specific gotchas that can break the inheritance chain:
</p>
</div>
<div id="outline-container-gotcha-file-renames" class="outline-3">
<h3 id="gotcha-file-renames"><span class="section-number-3">8.1.</span> File Renames and Moves</h3>
<div class="outline-text-3" id="text-gotcha-file-renames">
<p>
Renames are the most common source of painful merge conflicts.  If upstream renames <code>app/core/config.py</code> to <code>app/settings/config.py</code>, and your org fork has modifications to the original <code>app/core/config.py</code>, Git may not detect this as a rename-with-modification<sup><a id="fnr.9" class="footref" href="https://www.chiply.dev/#fn.9" role="doc-backlink">9</a></sup>.  Instead, you'll see the old file deleted and a new file created, with your org's changes silently lost.
</p>

<p>
<b>Mitigation</b>: When merging upstream changes that include renames, always diff the deleted file against the new file manually.  Use <a href="https://git-scm.com/docs/git-diff">git diff &#x2013;find-renames</a> with a low similarity threshold to help Git detect renames:
</p>

<div class="org-src-container">
<pre class="src src-bash">git merge -X find-renames=40 upstream-mirror
</pre>
</div>
</div>
</div>
<div id="outline-container-gotcha-dependency-versions" class="outline-3">
<h3 id="gotcha-dependency-versions"><span class="section-number-3">8.2.</span> Dependency Version Conflicts</h3>
<div class="outline-text-3" id="text-gotcha-dependency-versions">
<p>
Upstream pins <code>sqlalchemy==2.0.30</code> in their <code>requirements.txt</code>.  Your org fork has pinned <code>sqlalchemy==2.0.25</code> because 2.0.30 has a regression with your custom dialect.  Three months later, upstream bumps to <code>sqlalchemy==2.1.0</code> with breaking API changes.
</p>

<p>
The danger escalates with lock files (<code>poetry.lock</code>, <code>requirements.txt</code> with hashes).  Lock file conflicts are virtually impossible to resolve by hand<sup><a id="fnr.10" class="footref" href="https://www.chiply.dev/#fn.10" role="doc-backlink">10</a></sup>.
</p>

<p>
<b>Mitigation</b>: don't try to "layer" lock files &#x2013; pip and poetry resolvers don't compose two requirement files cleanly, and <code>-r base.txt</code> followed by overrides will silently produce unsatisfiable resolutions.  Instead, pick one of two honest strategies:
</p>

<ol class="org-ol">
<li><b>Single resolved lock file owned by the org fork.</b>  The org fork edits <code>pyproject.toml</code> directly to express its constraints (upper bounds on critical libs, swapped-in internal packages) and re-runs the resolver after every upstream merge.  Yes, this means <code>pyproject.toml</code> and the lock file are high-conflict zones, but it's the only approach that produces a coherent dependency graph.</li>
<li><b>Constraints file.</b>  Keep upstream's <code>requirements.txt</code> unmodified and ship a separate <code>constraints.txt</code> that <code>pip install -c constraints.txt -r requirements.txt</code> consumes.  Constraints don't add dependencies, only bound them, so they don't fight the resolver.</li>
</ol>

<p>
Run <a href="https://github.com/pypa/pip-audit">pip-audit</a> (or <code>osv-scanner</code>) on every upstream merge so security regressions surface in the Tier 2 PR, not in production.
</p>
</div>
</div>
<div id="outline-container-gotcha-entrypoint-changes" class="outline-3">
<h3 id="gotcha-entrypoint-changes"><span class="section-number-3">8.3.</span> Entrypoint and Startup Sequence Changes</h3>
<div class="outline-text-3" id="text-gotcha-entrypoint-changes">
<p>
The application entrypoint (<code>main.py</code>, <code>app/__init__.py</code>, or wherever the FastAPI <code>app</code> instance is created) is a high-conflict zone.  Both upstream and your org fork need to modify this file &#x2013; upstream to add framework features, your org to wire in custom middleware, startup hooks, and shutdown handlers.
</p>

<p>
If upstream refactors the startup sequence (e.g., moving from a module-level <code>app = FastAPI()</code> to a factory function <code>create_app()</code>), every downstream fork that has modified the entrypoint will face a complex merge.
</p>

<p>
<b>Mitigation</b>:
</p>
<ul class="org-ul">
<li>Use a <b>lifecycle hooks</b> pattern.  Define well-known hook functions (<code>on_startup()</code>, <code>on_shutdown()</code>, <code>configure_middleware()</code>) in separate files that the entrypoint imports.  Upstream owns the entrypoint; your org owns the hook implementations.</li>
<li>If upstream doesn't support this pattern, propose it upstream.  This is a legitimate contribution that benefits everyone.</li>
</ul>
</div>
</div>
<div id="outline-container-gotcha-migration-conflicts" class="outline-3">
<h3 id="gotcha-migration-conflicts"><span class="section-number-3">8.4.</span> Database Migration Conflicts</h3>
<div class="outline-text-3" id="text-gotcha-migration-conflicts">
<p>
Alembic migrations have a linear dependency chain: each migration references the previous one's revision ID<sup><a id="fnr.11" class="footref" href="https://www.chiply.dev/#fn.11" role="doc-backlink">11</a></sup>.  If upstream adds migration <code>abc123</code> and your org fork adds migration <code>def456</code>, both claim to be the "head."  Alembic calls this a "multiple heads" situation and refuses to run until you create a merge migration.  (This is the same Alembic we chose in the decision table above &#x2013; a great tool, but one that demands careful coordination across fork tiers.)
</p>

<p>
At the team fork level, every team has domain-specific migrations that diverge from the org template's migrations, compounding the problem further.
</p>

<p>
<b>Mitigation</b>: this is the case where you should break the inheritance.  Each service owns its own database, so migration history is fundamentally per-service and shouldn't be inherited at all.  The org template should ship migration <b>infrastructure</b> (the <code>alembic.ini</code>, <code>env.py</code>, naming conventions, CI checks for multiple heads) but ship <b>zero</b> migration revision files.  Treat the <code>alembic/versions/</code> directory as team-owned from day one and add it to the merge-conflict-prevention checklist alongside CI files.  If you genuinely have org-wide tables (a shared audit log, a tenancy table), publish them as a <b>library</b> the team imports, not as inherited migrations.
</p>
</div>
</div>
<div id="outline-container-gotcha-cicd-divergence" class="outline-3">
<h3 id="gotcha-cicd-divergence"><span class="section-number-3">8.5.</span> CI/CD Pipeline Divergence</h3>
<div class="outline-text-3" id="text-gotcha-cicd-divergence">
<p>
CI/CD pipelines tend to be heavily customized at every level of the hierarchy.  Upstream has generic GitHub Actions.  Your org replaces them with GitLab CI.  Teams add service-specific test stages.
</p>

<p>
If upstream restructures their CI (renaming jobs, changing the workflow file layout), the merge into your org fork touches files you've completely replaced.
</p>

<p>
<b>Mitigation</b>:
</p>
<ul class="org-ul">
<li>Place CI files in a separate, clearly-namespaced directory.  If upstream uses <code>.github/workflows/</code>, put your org's CI in <code>.ci/</code> or <code>.gitlab/</code>.  This eliminates conflicts entirely because you're adding files, not modifying upstream's.</li>
<li>Alternatively, <code>.gitignore</code> upstream's CI files in your org fork and maintain your own.</li>
</ul>
</div>
</div>
<div id="outline-container-gotcha-docker-conflicts" class="outline-3">
<h3 id="gotcha-docker-conflicts"><span class="section-number-3">8.6.</span> Docker and Infrastructure File Conflicts</h3>
<div class="outline-text-3" id="text-gotcha-docker-conflicts">
<p>
<code>Dockerfile</code>, <code>docker-compose.yml</code>, and Kubernetes manifests are modified at every tier.  Upstream sets up a basic multi-stage build.  Your org adds security scanning layers, internal registry references, and specific base images.  Teams add service-specific build arguments and sidecar containers.
</p>

<p>
<b>Mitigation</b>:
</p>
<ul class="org-ul">
<li>Use Docker Compose <a href="https://docs.docker.com/compose/how-tos/multiple-compose-files/extends/">extends</a> or <code>override</code> files: <code>docker-compose.yml</code> (upstream) + <code>docker-compose.org.yml</code> (org overrides) + <code>docker-compose.team.yml</code> (team overrides).</li>
<li>For Dockerfiles, consider a base image strategy: the org publishes a base image with org-specific layers, and team Dockerfiles use <code>FROM acme-corp/python-service-base:latest</code> instead of modifying the upstream Dockerfile.</li>
</ul>
</div>
</div>
<div id="outline-container-gotcha-import-paths" class="outline-3">
<h3 id="gotcha-import-paths"><span class="section-number-3">8.7.</span> Python Import Path Changes</h3>
<div class="outline-text-3" id="text-gotcha-import-paths">
<p>
If upstream renames the top-level Python package (e.g., <code>app</code> → <code>fastapi_app</code>), every file in every downstream fork that imports from <code>app</code> breaks.  This is a cascading failure across the entire hierarchy.
</p>

<p>
<b>Mitigation</b>:
</p>
<ul class="org-ul">
<li>This is one case where talking to upstream matters.  If you're building an org's infrastructure on top of a template, consider opening an issue or PR that establishes the top-level package name as a stable API.</li>
<li>If you can't prevent it, maintain a thin compatibility shim (<code>app/__init__.py</code> that re-exports from <code>fastapi_app</code>) in the org fork to give teams time to migrate.  Remove the shim after a transition period.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-general-challenges" class="outline-2">
<h2 id="general-challenges"><span class="section-number-2">9.</span> General Challenges and How to Address Them&#xa0;&#xa0;&#xa0;<span class="tag"><span class="challenges">challenges</span></span></h2>
<div class="outline-text-2" id="text-general-challenges">
<p>
Beyond specific gotchas, there are broader organizational and technical challenges that arise when implementing pattern inheritance.
</p>
</div>
<div id="outline-container-challenge-merge-conflicts" class="outline-3">
<h3 id="challenge-merge-conflicts"><span class="section-number-3">9.1.</span> Merge Conflict Accumulation</h3>
<div class="outline-text-3" id="text-challenge-merge-conflicts">
<p>
The longer you wait between upstream syncs, the harder they get.  If you sync monthly, you're merging a month of upstream changes against a month of org changes.  Conflicts that would have been trivial in isolation become tangled messes when batched.
</p>

<p>
<b>The fix is cadence.</b>  Set a regular sync schedule &#x2013; weekly or biweekly &#x2013; and treat it like any other maintenance task.  Automate the fetch and attempt the merge in CI.  If it succeeds cleanly, auto-merge.  If it conflicts, create a PR for human review.  The key is <b>detecting</b> divergence early, even if you don't resolve it immediately.
</p>

<div class="org-src-container">
<pre class="src src-mermaid">graph LR
    subgraph "Sync Cadence"
        direction LR
        W1[Week 1&lt;br/&gt;3 upstream commits&lt;br/&gt;easy merge ✅] --&gt; W2[Week 2&lt;br/&gt;5 upstream commits&lt;br/&gt;easy merge ✅]
        W2 --&gt; W3[Week 3&lt;br/&gt;2 upstream commits&lt;br/&gt;1 conflict ⚠️]
        W3 --&gt; W4[Week 4&lt;br/&gt;4 upstream commits&lt;br/&gt;easy merge ✅]
    end
</pre>
</div>

<p>
Compare this to syncing quarterly, where you'd face 14 upstream commits in a single merge with compounding conflicts.
</p>
</div>
</div>
<div id="outline-container-challenge-fork-drift" class="outline-3">
<h3 id="challenge-fork-drift"><span class="section-number-3">9.2.</span> Fork Drift and Staleness</h3>
<div class="outline-text-3" id="text-challenge-fork-drift">
<p>
Drift looks like a social problem &#x2013; teams fork the org template, build their service, never sync again, and six months later the fork is frozen at the version from the day they started<sup><a id="fnr.12" class="footref" href="https://www.chiply.dev/#fn.12" role="doc-backlink">12</a></sup>.  The service still runs, so nobody notices.
</p>

<p>
Frame it as a <b>vulnerability management</b> problem instead, because that's what it actually is.  A stale Tier 3 fork is a service silently carrying every CVE that's been patched in the org template since the last sync.  "47 commits behind" is a number that no engineer will act on; "missing 3 advisories from the last 60 days, including a high-severity auth bypass" is.
</p>

<p>
<b>Solutions</b>:
</p>
<ul class="org-ul">
<li><b>Advisory-aware staleness detection</b>: a CI job that doesn't just count missing commits but cross-references them against the org template's changelog and the upstream's GHSA / OSV feed.  Surface security-relevant gaps independently from the general freshness number.</li>
<li><b>Freshness SLO with escalation</b>: set a policy ("no team fork more than N weeks behind <code>stable</code>", "zero open security-channel releases") and enforce it by opening a tracking issue with an owner &#x2013; not a dashboard nobody reads, and not a CI warning that scrolls past.</li>
<li><b>Sync sprints</b> as a fallback for general-cohort drift, but never as the only mechanism for security drift.</li>
</ul>

<p>
A GitHub Action to automate staleness detection looks like this:
</p>

<div class="org-src-container">
<pre class="src src-yaml"># .github/workflows/upstream-sync-check.yml
name: Check Upstream Sync
on:
  schedule:
    - cron: '0 9 * * 1'  # Every Monday at 9am
  workflow_dispatch:

jobs:
  check-sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Fetch upstream
        run: |
          git remote add upstream https://github.com/tiangolo/full-stack-fastapi-template.git
          git fetch upstream
      - name: Check divergence
        run: |
          BEHIND=$(git rev-list --count HEAD..upstream/main)
          if [ "$BEHIND" -gt 0 ]; then
            echo "::warning::Org fork is $BEHIND commits behind upstream"
          fi
</pre>
</div>

<p>
Run the same pattern on team forks, checking against the org template.
</p>
</div>
</div>
<div id="outline-container-challenge-governance" class="outline-3">
<h3 id="challenge-governance"><span class="section-number-3">9.3.</span> Governance: Who Decides What Goes in the Org Fork?</h3>
<div class="outline-text-3" id="text-challenge-governance">
<p>
Because the org fork is shared infrastructure for every team, a bad change &#x2013; a poorly configured middleware, a broken migration, a dependency bump with a subtle regression &#x2013; can propagate to every service that syncs.
</p>

<p>
Clear ownership and review processes are essential:
</p>

<ul class="org-ul">
<li><b>Dedicated owners</b>: The platform team owns the org fork.  Changes require review from at least one platform engineer, with branch protection and signed commits enforced.  Two-person review for anything touching auth, crypto, or session lifecycle.</li>
<li><b>Change classification</b>: Not every upstream change needs to be merged.  Evaluate each upstream release for relevance and risk, and tag releases accordingly (security / patch / minor / major).</li>
<li><b>Changelog</b>: Maintain a changelog that translates upstream changes into org-relevant context.  "Upstream upgraded SQLAlchemy to 2.1.  This changes how async sessions are created.  If you've customized session creation, see migration guide."</li>
<li><b>Semantic commit prefixes</b>: Prefix commits to distinguish org customizations from upstream merges (<code>org:</code> vs <code>upstream:</code>).  When a merge conflict shows up, you immediately know whether the conflicting change is one you own or one you inherited.</li>
</ul>
</div>
<div id="outline-container-orgec4f175" class="outline-4">
<h4 id="orgec4f175"><span class="section-number-4">9.3.1.</span> Tiered rollout, not "a canary"</h4>
<div class="outline-text-4" id="text-9-3-1">
<p>
A single low-traffic canary service is enough to prove the template still boots; it is not enough to prove a connection-pool resize, a TLS change, or an OpenTelemetry SDK bump will survive a high-QPS payments service.  Roll Tier 2 changes out in cohorts:
</p>

<ol class="org-ol">
<li><b>Cohort 0 &#x2013; canary.</b>  A real but low-traffic service the platform team owns.  Soak for 24 hours minimum.</li>
<li><b>Cohort 1 &#x2013; low-criticality services.</b>  Internal tools, batch jobs, anything not in the customer request path.  Soak for several days.</li>
<li><b>Cohort 2 &#x2013; general fleet.</b>  Most services.  Auto-merge PRs against team forks.</li>
<li><b>Cohort 3 &#x2013; Tier 0 services.</b>  Auth, payments, identity, anything paged on.  Manual change-review only, never auto-merged.</li>
</ol>

<p>
Define rollback criteria up front (error rate, latency p99, saturation) and tie them to your existing SLO monitoring.  A bad merge that already shipped to Cohort 1 shouldn't require human judgement to roll back from Cohort 2.
</p>
</div>
</div>
<div id="outline-container-orgab77fe4" class="outline-4">
<h4 id="orgab77fe4"><span class="section-number-4">9.3.2.</span> Release channels and rollback</h4>
<div class="outline-text-4" id="text-9-3-2">
<p>
Have the org fork ship <b>tagged releases</b>, not a moving <code>main</code> target, and have teams pin to tags.  Steal Debian's pattern: a <code>stable</code> channel with monthly tags, and a <code>security</code> channel for cherry-picked CVE fixes against the last stable tag.  This decouples freshness from stability &#x2013; a team can stay on <code>stable v2.3</code> for a quarter and still receive <code>v2.3.1-security.1</code> when an upstream auth fix lands.
</p>

<p>
Rollback is the part most fork-based-template posts skip and it is the part that bites hardest.  Once a team has merged a bad org-template release and layered a week of domain code on top, <code>git revert</code> is non-trivial and may conflict with that domain code.  Two practices make this survivable:
</p>

<ul class="org-ul">
<li><b>Feature-flag risky template changes.</b>  If the org fork swaps the connection pool implementation, gate it on an env var so a rollback is a config change, not a merge.</li>
<li><b>Document an emergency revert procedure per release.</b>  "If you need to roll back v2.4.0, here is the exact merge commit to revert and the known conflicts you'll see."  Do this work once at release time, not at 3am during an incident.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-challenge-too-customized" class="outline-3">
<h3 id="challenge-too-customized"><span class="section-number-3">9.4.</span> The "Too Customized to Merge" Problem</h3>
<div class="outline-text-3" id="text-challenge-too-customized">
<p>
Sometimes a team's service diverges so far from the template that merging org-template updates becomes more work than it's worth.  The payments service has rewritten the entire database layer to use event sourcing.  The notifications service has replaced FastAPI's routing with a custom message broker integration.  At this point, the fork relationship is a liability, not an asset.
</p>

<p>
This is fine, and it should be a normal part of the lifecycle, not a stigmatised escape hatch.  Not every service needs to stay on the fork.  When a service has genuinely unique requirements that make it architecturally different from the template, detach it:
</p>

<div class="org-src-container">
<pre class="src src-bash"># Remove the org-template remote
git remote remove org-template

# Delete the mirror branch
git branch -D org-template-mirror
</pre>
</div>

<p>
To make detachment a normal part of service lifecycle rather than a stigmatized escape hatch, define clear criteria:
</p>

<ul class="org-ul">
<li>If merging org-template updates has required manual conflict resolution in more than 50% of the last 10 syncs, consider detaching.</li>
<li>If the service has replaced more than 30% of the template's files with custom implementations, consider detaching.</li>
<li>If the service's architecture has diverged from the template's assumptions (different database, different auth model, different deployment target), detach.</li>
</ul>

<p>
Explicitly detaching is better than maintaining a fiction of inheritance that creates merge conflicts without delivering value.  The fork hierarchy should be opt-in, not mandatory.
</p>
</div>
</div>
<div id="outline-container-challenge-testing" class="outline-3">
<h3 id="challenge-testing"><span class="section-number-3">9.5.</span> Testing at the Tier 2 Boundary</h3>
<div class="outline-text-3" id="text-challenge-testing">
<p>
The critical gap is the org fork's CI.  Upstream tests the template in isolation; team forks test their service.  Without strong tests at Tier 2, broken upstream merges silently propagate to every team.  In addition to the obvious unit and integration tests, write <b>contract tests</b> that exercise the interfaces team forks depend on &#x2013; startup hooks, middleware ordering, session lifecycle &#x2013; so a refactor that quietly changes a hook contract fails in a Tier 2 PR rather than in a team's production deploy.
</p>
</div>
</div>
</div>
<div id="outline-container-alternatives" class="outline-2">
<h2 id="alternatives"><span class="section-number-2">10.</span> Alternatives to Pattern Inheritance&#xa0;&#xa0;&#xa0;<span class="tag"><span class="alternatives">alternatives</span></span></h2>
<div class="outline-text-2" id="text-alternatives">
<p>
Forking is not the only way to solve the "consistent service patterns" problem.  Here are the main alternatives, with trade-offs:
</p>
</div>
<div id="outline-container-alt-monorepo" class="outline-3">
<h3 id="alt-monorepo"><span class="section-number-3">10.1.</span> Monorepo with Shared Libraries</h3>
<div class="outline-text-3" id="text-alt-monorepo">
<p>
Instead of forks, put all services in a single repository with shared libraries for common patterns:
</p>

<div class="org-src-container">
<pre class="src src-nil">monorepo/
├── libs/
│   ├── fastapi-common/     # Shared middleware, auth, config
│   ├── db-common/          # SQLAlchemy setup, session management
│   └── observability/      # OpenTelemetry, logging
├── services/
│   ├── payments/
│   ├── inventory/
│   └── notifications/
└── templates/
    └── service-scaffold/   # Cookiecutter for new services
</pre>
</div>

<p>
<b>Pros</b>: Atomic changes across services and libraries, no merge conflicts, single CI pipeline, easy refactoring.
</p>

<p>
<b>Cons</b>: Requires monorepo tooling (<a href="https://bazel.build/">Bazel</a>, Pants, Nx)<sup><a id="fnr.13" class="footref" href="https://www.chiply.dev/#fn.13" role="doc-backlink">13</a></sup>, CI complexity scales with repo size, teams lose autonomy over their release cycle, doesn't inherit from community templates.
</p>

<p>
<b>Best for</b>: Organizations with strong platform teams and existing monorepo infrastructure.
</p>
</div>
</div>
<div id="outline-container-alt-template-generators" class="outline-3">
<h3 id="alt-template-generators"><span class="section-number-3">10.2.</span> Template Generators with Post-Generation Updates</h3>
<div class="outline-text-3" id="text-alt-template-generators">
<p>
Tools like <a href="https://github.com/cruft/cruft">cruft</a> (built on top of Cookiecutter) solve the "snapshot problem" by tracking which template version was used to generate a project and offering an update mechanism<sup><a id="fnr.14" class="footref" href="https://www.chiply.dev/#fn.14" role="doc-backlink">14</a></sup>:
</p>

<div class="org-src-container">
<pre class="src src-bash"># Generate project from template
cruft create https://github.com/acme-corp/fastapi-template

# Later, update to latest template version
cruft update
</pre>
</div>

<p>
<code>cruft update</code> computes the diff between the template version you generated from and the current version, then applies that diff to your project.
</p>

<p>
<b>Pros</b>: No Git fork management, works with any Cookiecutter template, clear diffing between template versions.
</p>

<p>
<b>Cons</b>: Updates are one-way (no pushing changes back to the template), conflicts are handled at the file level (less granular than Git), doesn't compose into a multi-tier hierarchy as naturally.
</p>

<p>
<b>Best for</b>: Organizations that want template inheritance without the Git overhead.
</p>
</div>
</div>
<div id="outline-container-alt-idp" class="outline-3">
<h3 id="alt-idp"><span class="section-number-3">10.3.</span> Internal Developer Platforms (Backstage, Port, Humanitec)</h3>
<div class="outline-text-3" id="text-alt-idp">
<p>
Platforms like <a href="https://backstage.io/">Backstage</a> provide service scaffolding as a feature of a broader developer portal:
</p>

<ul class="org-ul">
<li>Service catalog with templates</li>
<li>One-click service creation from "golden path" templates</li>
<li>Built-in CI/CD integration</li>
<li>Plugin ecosystem for org-specific features</li>
</ul>

<p>
<b>Pros</b>: Higher-level abstraction, UI-driven, integrates service creation with service management, good for organizations with many non-expert users.
</p>

<p>
<b>Cons</b>: Heavy infrastructure (Backstage itself is a significant service to maintain)<sup><a id="fnr.15" class="footref" href="https://www.chiply.dev/#fn.15" role="doc-backlink">15</a></sup>, templates are still snapshots (no inheritance), vendor-specific if using a commercial platform.
</p>

<p>
<b>Best for</b>: Large organizations (500+ engineers) with dedicated platform teams.
</p>
</div>
</div>
<div id="outline-container-alt-package-composition" class="outline-3">
<h3 id="alt-package-composition"><span class="section-number-3">10.4.</span> Package-Based Composition</h3>
<div class="outline-text-3" id="text-alt-package-composition">
<p>
Instead of inheriting an entire project template, publish your patterns as installable packages:
</p>

<div class="org-src-container">
<pre class="src src-python"># In your service's requirements.txt
acme-fastapi-core==2.3.0    # Auth, middleware, config
acme-db-common==1.5.0        # SQLAlchemy setup, migrations base
acme-observability==3.1.0    # OpenTelemetry, structured logging
</pre>
</div>

<p>
Teams install these packages and compose their service from them:
</p>

<div class="org-src-container">
<pre class="src src-python">from acme_fastapi_core import create_app, configure_auth
from acme_db_common import get_async_session, Base
from acme_observability import setup_tracing

app = create_app()
configure_auth(app)
setup_tracing(app)
</pre>
</div>

<p>
<b>Pros</b>: Versioned dependencies with <a href="https://semver.org/">semantic versioning</a>, teams can pin to specific versions, no merge conflicts, standard package management.
</p>

<p>
<b>Cons</b>: Requires extracting patterns into well-designed APIs (significant upfront investment), doesn't provide project structure (only runtime patterns), teams still need a scaffold for the "rest" of the project.
</p>

<p>
<b>Best for</b>: Organizations with mature platform teams that can invest in library design.
</p>
</div>
</div>
<div id="outline-container-comparison-matrix" class="outline-3">
<h3 id="comparison-matrix"><span class="section-number-3">10.5.</span> Comparison Matrix</h3>
<div class="outline-text-3" id="text-comparison-matrix">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Approach</th>
<th scope="col" class="org-left">Inherits from community</th>
<th scope="col" class="org-left">Multi-tier</th>
<th scope="col" class="org-left">Merge overhead</th>
<th scope="col" class="org-left">Upfront investment</th>
<th scope="col" class="org-left">Best scale</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Fork hierarchy</td>
<td class="org-left">Yes</td>
<td class="org-left">Yes</td>
<td class="org-left">Medium</td>
<td class="org-left">Low</td>
<td class="org-left">5-50 services</td>
</tr>

<tr>
<td class="org-left">Monorepo</td>
<td class="org-left">No</td>
<td class="org-left">N/A</td>
<td class="org-left">None</td>
<td class="org-left">High</td>
<td class="org-left">50+ services</td>
</tr>

<tr>
<td class="org-left">cruft / template generators</td>
<td class="org-left">Partial</td>
<td class="org-left">No</td>
<td class="org-left">Low</td>
<td class="org-left">Low</td>
<td class="org-left">5-20 services</td>
</tr>

<tr>
<td class="org-left">Internal developer platform</td>
<td class="org-left">No</td>
<td class="org-left">No</td>
<td class="org-left">None</td>
<td class="org-left">Very high</td>
<td class="org-left">100+ engineers</td>
</tr>

<tr>
<td class="org-left">Package composition</td>
<td class="org-left">No</td>
<td class="org-left">N/A</td>
<td class="org-left">None</td>
<td class="org-left">High</td>
<td class="org-left">20+ services</td>
</tr>
</tbody>
</table>

<p>
The fork hierarchy occupies a sweet spot: low upfront investment, inherits from community work, and scales to a meaningful number of services before the merge overhead becomes a bottleneck.  For most mid-sized engineering organizations (50-500 engineers, 5-50 services on a common stack), it's the right starting point<sup><a id="fnr.16" class="footref" href="https://www.chiply.dev/#fn.16" role="doc-backlink">16</a></sup>.
</p>

<p>
These approaches aren't mutually exclusive.  Mature organizations often combine package-based composition for runtime patterns (auth, observability, database utilities) with a fork hierarchy for project structure (directory layout, CI pipelines, deployment manifests, Dockerfile conventions).  The fork gives you the scaffold; the packages give you the runtime behavior.
</p>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">11.</span> Conclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="conclusion">conclusion</span></span></h2>
<div class="outline-text-2" id="text-conclusion">
<p>
A fork hierarchy is a tax you pay weekly so that the next CVE in your auth library is one merge for your platform team and <code>N</code> trivial merges for service teams, instead of <code>N</code> independent rediscoveries followed by <code>N</code> different fixes.  It only pays off if you're honest about the conditions: pin to signed upstream tags, ship tagged Tier 2 releases with rollback playbooks, treat fork drift as a vulnerability-management failure rather than a social one, and let services detach when they've outgrown the template.  Get those right and the workflow earns its keep.  Skip them and you've just built a slow vendoring pipeline with extra steps.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">12.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="platformEngineering">platformEngineering</span>&#xa0;<span class="templates">templates</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Generate a TLDR for this document that will be displayed at the top of the static-site version of this document.  Include internal org links to headings in this document where relevant. Org links look like this, regardless of the level of nesting: <a href="https://www.chiply.dev/#footnotes">Footnotes</a>.  Do not use any bullets for the explanation, but you can use line breaks to help with readability.  Be verbose and describe as many sections as possible.  Don't forge the org links!  The links should be evenly dispersed in the text you produce.  CRITICAL &#x2013; you must get the link format correct.  The template for the ref part of the link is simply an asterisk with the header name.  Do not include tags in the links
</p>





<p>
Most engineering organizations eventually face a consistency crisis: dozens of services, each built differently, each carrying its own opinions about authentication, logging, database access, and deployment. This post introduces <i>pattern inheritance</i> — a <a href="https://www.chiply.dev/#three-tier-hierarchy">three-tier fork hierarchy</a> that turns open-source community templates into living, evolvable organizational standards.
</p>

<p>
The core problem is <a href="https://www.chiply.dev/#the-problem">service proliferation without standards</a>. When every team builds from scratch, you get compounding costs in onboarding, operations, security, and upgrades. The natural instinct is to write documentation, but documentation decays. What you actually need is running code that embodies your standards.
</p>

<p>
Using a <a href="https://www.chiply.dev/#the-running-example">FastAPI + async Postgres stack</a> as a concrete example, the post shows how a seemingly simple technology choice requires dozens of interlocking decisions — async driver, ORM, migrations, connection pooling, config management, and testing — that are difficult to get right in isolation. The open-source community has already solved much of this through <a href="https://www.chiply.dev/#reaching-for-templates">battle-tested templates</a> like tiangolo's full-stack-fastapi-template and various Cookiecutter scaffolds, but cloning or generating from these templates creates a point-in-time snapshot that immediately begins drifting from its source.
</p>

<p>
The key insight is that <a href="https://www.chiply.dev/#why-fork-not-clone">forking preserves a relationship</a> between your code and its origin, while cloning severs it. A fork maintains an upstream remote that enables standard Git operations — fetch and merge — to pull improvements downstream over time. The mental model is inheritance in the object-oriented sense: you override specific behaviors while continuing to receive updates to the base implementation.
</p>

<p>
The recommended architecture is a <a href="https://www.chiply.dev/#three-tier-hierarchy">three-tier fork hierarchy</a>: Tier 1 is the community upstream template you never modify directly; Tier 2 is your <a href="https://www.chiply.dev/#tier-2">organization fork</a> where your platform team applies org-wide opinions like OIDC authentication, OpenTelemetry configuration, Vault integration, and custom CI/CD pipelines; Tier 3 consists of <a href="https://www.chiply.dev/#tier-3">team forks</a> where individual services add their domain-specific business logic. Improvements at any tier flow downstream through standard Git merges.
</p>

<p>
The post provides a detailed <a href="https://www.chiply.dev/#setting-up-the-hierarchy">step-by-step setup guide</a>, including a mirror-branch strategy that maintains a clean <code>upstream-mirror</code> branch for tracking the community template exactly, while <code>main</code> carries organizational customizations. A critical principle runs throughout: keep your diff small, prefer additive changes over modifications, and document every divergence in an <code>ORG_CHANGES.md</code> manifest that serves as your merge conflict playbook.
</p>

<p>
Several <a href="https://www.chiply.dev/#gotchas-fork-compatibility">specific gotchas</a> can break fork compatibility: file renames that Git fails to detect, dependency version conflicts (especially in lock files), entrypoint and startup sequence refactors, database migration conflicts caused by Alembic's linear revision chain, CI/CD pipeline divergence, Docker and infrastructure file conflicts, and Python import path changes. Each gotcha comes with concrete mitigation strategies — from layered dependency files and lifecycle hook patterns to Alembic branch labels and Docker Compose override files.
</p>

<p>
Beyond the technical gotchas, the post addresses <a href="https://www.chiply.dev/#general-challenges">broader organizational challenges</a>. <a href="https://www.chiply.dev/#challenge-merge-conflicts">Merge conflict accumulation</a> is solved through regular sync cadence — weekly or biweekly — because small, frequent merges are always easier than large, infrequent ones. <a href="https://www.chiply.dev/#challenge-fork-drift">Fork drift and staleness</a> is the biggest social risk, addressed through automated staleness detection via CI jobs, org-wide sync sprints, and freshness SLOs. <a href="https://www.chiply.dev/#challenge-governance">Governance</a> requires dedicated platform team ownership, change classification, staged rollouts via canary services, and semantic commit prefixes. The post also honestly addresses <a href="https://www.chiply.dev/#challenge-too-customized">the "too customized to merge" problem</a>, providing clear criteria for when a service should detach from the fork hierarchy entirely. Finally, <a href="https://www.chiply.dev/#challenge-testing">testing at the Tier 2 boundary</a> is essential — particularly robust CI on the org fork, which acts as the immune system for your entire service fleet.
</p>

<p>
The post concludes with a thorough evaluation of <a href="https://www.chiply.dev/#alternatives">alternatives</a> — monorepos with shared libraries, template generators like cruft, internal developer platforms like Backstage, and package-based composition — presented in a comparison matrix. The fork hierarchy occupies a sweet spot of low upfront investment and community inheritance that suits most mid-sized organizations with 5–50 services on a common stack, and it composes well with package-based approaches for runtime behavior.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This inflection point often arrives earlier than you'd think.  At startups, it can happen at 15-20 engineers.  At larger companies spinning up new product lines, it happens the moment a second team adopts the same tech stack.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
There's a well-known pattern in engineering orgs where internal documentation is enthusiastically written during a "documentation sprint," links are shared in Slack, and within six months the docs are out of date and actively misleading.  The half-life of an internal wiki page that isn't enforced by automation is roughly 3-6 months.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The async requirement is non-trivial.  Synchronous database access in a FastAPI service blocks the event loop, which under load means a single slow query can stall every concurrent request.  Getting async right involves understanding <code>asyncpg</code>'s connection pool semantics, SQLAlchemy's async session lifecycle, and the subtle ways <code>await</code> points interact with transaction boundaries.
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Note that GitHub Free and Team plans cannot fork a public repository into a private one.  GitLab allows changing fork visibility.  If you need a private org fork of a public template, the workaround is <code>git clone</code> + manual remote setup &#x2013; you lose GitHub's fork-tracking UI, but the git-level upstream relationship is preserved.
</p></div></div>

<div class="footdef"><sup><a id="fn.5" class="footnum" href="https://www.chiply.dev/#fnr.5" role="doc-backlink">5</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The analogy isn't perfect.  In OOP, a subclass automatically inherits method changes from its parent at compile/runtime.  In fork-based inheritance, you must explicitly merge.  This is actually a feature &#x2013; automatic inheritance of breaking changes in production infrastructure would be terrifying.  The manual merge step is your review gate.
</p></div></div>

<div class="footdef"><sup><a id="fn.6" class="footnum" href="https://www.chiply.dev/#fnr.6" role="doc-backlink">6</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
If upstream goes dormant, the org fork effectively becomes the new upstream.  This is actually fine &#x2013; you've already been applying org opinions, so you just stop syncing.  The main loss is community-sourced improvements and bug fixes.  Monitor upstream activity and have a plan for the eventuality.
</p></div></div>

<div class="footdef"><sup><a id="fn.7" class="footnum" href="https://www.chiply.dev/#fnr.7" role="doc-backlink">7</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
If your organization doesn't have a platform or infrastructure team, this role can be filled by a rotating "template maintainer" responsibility, similar to how some teams rotate on-call duties.  What matters is that someone is explicitly accountable for the org fork's health.
</p></div></div>

<div class="footdef"><sup><a id="fn.8" class="footnum" href="https://www.chiply.dev/#fnr.8" role="doc-backlink">8</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
An alternative to the two-branch strategy is to use tags on the upstream remote's commits.  Some teams prefer <code>git fetch upstream &amp;&amp; git merge upstream/main</code> directly into their <code>main</code> branch without a mirror.  This works but makes it harder to see a clean diff of "our changes vs. upstream" since the merge history interleaves org and upstream commits.  The mirror branch keeps these concerns separated.
</p></div></div>

<div class="footdef"><sup><a id="fn.9" class="footnum" href="https://www.chiply.dev/#fnr.9" role="doc-backlink">9</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Git's rename detection uses a similarity heuristic.  By default, it considers a file renamed if the new file is at least 50% similar to the deleted file.  When your org fork has heavily modified a file, the similarity drops below this threshold and Git treats it as a delete + create rather than a rename.  The <code>rename-threshold</code> flag lowers this bar.
</p></div></div>

<div class="footdef"><sup><a id="fn.10" class="footnum" href="https://www.chiply.dev/#fnr.10" role="doc-backlink">10</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The correct approach to lock file conflicts is almost always: accept one side entirely (usually upstream's), then regenerate the lock file with your dependency overrides applied.  Never attempt a three-way merge on a lock file &#x2013; the format isn't designed for it and the result will be subtly corrupted.
</p></div></div>

<div class="footdef"><sup><a id="fn.11" class="footnum" href="https://www.chiply.dev/#fnr.11" role="doc-backlink">11</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This is Alembic-specific, but the same class of problem exists with any linear migration system: Django migrations, Flyway, Liquibase.  The general solution is always some form of migration namespacing or branching.
</p></div></div>

<div class="footdef"><sup><a id="fn.12" class="footnum" href="https://www.chiply.dev/#fnr.12" role="doc-backlink">12</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This mirrors a well-studied pattern in open source: "fork and forget."  Studies of GitHub forks show that the vast majority of forks never sync with upstream after the initial fork.  The same dynamic plays out inside organizations unless you actively counteract it.
</p></div></div>

<div class="footdef"><sup><a id="fn.13" class="footnum" href="https://www.chiply.dev/#fnr.13" role="doc-backlink">13</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Google (Bazel), Meta (Buck), and Twitter (Pants) all invested enormous engineering effort into making monorepos work at scale.  Without that level of build system investment, monorepos tend to degrade into "monoliths with directory boundaries" where CI takes 45 minutes and a bad commit blocks every team.
</p></div></div>

<div class="footdef"><sup><a id="fn.14" class="footnum" href="https://www.chiply.dev/#fnr.14" role="doc-backlink">14</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
<code>cruft</code> stores a <code>.cruft.json</code> file in your project that records the template URL, the commit hash it was generated from, and the template variables used.  Running <code>cruft update</code> diffs between the recorded commit and the template's current HEAD, then applies the diff as a patch.  It's clever, but patches are less forgiving than Git merges when conflicts arise.
</p></div></div>

<div class="footdef"><sup><a id="fn.15" class="footnum" href="https://www.chiply.dev/#fnr.15" role="doc-backlink">15</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Backstage is itself a React application with a PostgreSQL backend, a plugin architecture, and its own deployment requirements.  The irony of needing to deploy and maintain a complex service just to help teams deploy and maintain their services is not lost on most platform engineers who've tried it.
</p></div></div>

<div class="footdef"><sup><a id="fn.16" class="footnum" href="https://www.chiply.dev/#fnr.16" role="doc-backlink">16</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
These aren't hard boundaries.  The "best scale" column reflects where each approach's trade-offs are most favorable.  A fork hierarchy can work at 100+ services if you invest in tooling, and a monorepo can work at 5 services if you're already using monorepo tooling for other reasons.  Choose based on your existing infrastructure and team capabilities, not just service count.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Schema Diagrams: Bidirectional Visualization for the Schema Languages That Need It Most</title>
      <link>https://www.chiply.dev/post-schema-diagrams</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-schema-diagrams</guid>
      <pubDate>Tue, 03 Mar 2026 08:29:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>If you&apos;ve ever worked with a relational database, you&apos;ve almost certainly seen an entity-relationship diagram (ERD). Maybe it was generated by DataGrip or pgAdmin, or maybe someone drew it in Lucidcha...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="schemaVisualization">schemaVisualization</span>&#xa0;<span class="dataModelling">dataModelling</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgecab0d2" class="figure">
<p><img src="https://www.chiply.dev/images/schema-diagrams-banner.jpeg" alt="schema-diagrams-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
If you've ever worked with a relational database, you've almost certainly seen an entity-relationship diagram (ERD). Maybe it was generated by DataGrip or pgAdmin, or maybe someone drew it in Lucidchart. In any event, the point is you actually <i>saw</i> the data model.  You could point at boxes, trace lines, and say "this table references that one."
</p>

<p>
Now think about the last time you reviewed an Avro schema.  If you're like most engineers, you opened a JSON file, scrolled through hundreds of lines of deeply nested objects, and mentally assembled the structure in your head.  No boxes.  No lines.  Just JSON.  I'd be surprised if most readers have actually seen visualizations of the models composed by Avro schemas.
</p>

<p>
This gap &#x2013; between the rich visual tooling that SQL databases have enjoyed for decades and the near-complete absence of equivalent tooling for schema languages like Avro &#x2013; is what <a href="https://www.chiply.dev/schema-diagrams">Schema Diagrams</a> aims to close, you can try out at that link, or you can view the source at <a href="https://github.com/chiply/schema-diagrams">schema-diagrams on GitHub</a>. It's a diagrams-as-code tool that renders interactive entity-relationship diagrams from Avro schemas, with bidirectional sync between the code editor and the visual canvas.  Edit the code, the diagram updates.  Edit the diagram, the code updates.  Everyone works on the same artifact.<sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>
</p>

<p>
This post is a companion to my earlier post on <a href="https://www.chiply.dev/post-schema-languages">schema language selection</a>, which explored <i>which</i> schema language to choose.  This post asks a different question: <i>once you've chosen, how do you actually see what you've built?</i>
</p>
</div>
</div>
<div id="outline-container-the-visualization-gap" class="outline-2">
<h2 id="the-visualization-gap"><span class="section-number-2">2.</span> The Visualization Gap&#xa0;&#xa0;&#xa0;<span class="tag"><span class="schemas">schemas</span>&#xa0;<span class="toolingGap">toolingGap</span></span></h2>
<div class="outline-text-2" id="text-the-visualization-gap">
<p>
Avro schemas are defined as JSON.  This is a reasonable serialization choice &#x2013; JSON is ubiquitous, parsable everywhere, and schema registries speak it natively.  But it's a terrible <i>authoring</i> format for complex data models.
</p>

<p>
Consider a moderately complex schema: a <code>User</code> record with nested <code>Address</code>, <code>PaymentMethod</code>, and <code>OrderHistory</code> types, each referencing further records and enums.  In raw <code>.avsc</code> JSON, this might look like 300 lines of nested objects, arrays, and union types.  The structural relationships &#x2013; which records reference which, what the cardinality is, where the enums live &#x2013; are buried in syntax<sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup>.
</p>

<p>
This creates real problems:
</p>

<ul class="org-ul">
<li><b>PR reviews become rubber stamps.</b> When a schema change is a diff of 50 lines of JSON, reviewers skim rather than analyze. Subtle issues &#x2013; an accidental field removal, a type change that breaks backward compatibility, a new nullable union that downstream consumers don't handle &#x2013; slip through.</li>

<li><b>Onboarding stalls.</b> A new engineer joining a team with 40 Avro schemas across a Kafka-based architecture faces weeks of spelunking through <code>.avsc</code> files to build a mental model of the data domain.  With SQL, they'd open an ERD and have the big picture in minutes.</li>

<li><b>Cross-team communication breaks down.</b> When a data engineer needs to explain a schema to a product owner or data analyst, they have two options: hand them the raw JSON (useless) or manually draw a diagram in Lucidchart (labor-intensive, extremely error prone, and immediately stale).</li>

<li><b>Duplicate schemas proliferate.</b> Without a visual map of existing types, teams create new schemas that partially duplicate existing ones.  In large organizations, this leads to dozens of slightly different <code>Address</code> or <code>Timestamp</code> definitions scattered across registries.  This undermines the effort to reach a single source of truth and avoid model drift, as explained in my post on <a href="https://www.chiply.dev/post-schema-languages">schema language selection</a>.</li>
</ul>

<p>
SQL databases solved these problems eons ago.  You can point any of thirty-plus tools at a database, and within seconds you have an ERD showing every table, column, relationship, and constraint.  The schema languages powering modern event-driven systems &#x2013; the schemas that <i>are</i> the API contracts between services &#x2013; have nothing comparable.
</p>
</div>
</div>
<div id="outline-container-the-state-of-schema-visualization-tooling" class="outline-2">
<h2 id="the-state-of-schema-visualization-tooling"><span class="section-number-2">3.</span> The State of Schema Visualization Tooling&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marketAnalysis">marketAnalysis</span></span></h2>
<div class="outline-text-2" id="text-the-state-of-schema-visualization-tooling">
<p>
The landscape splits along two axes: which schema language is supported, and whether the tool is one-directional (code → diagram) or bidirectional (code ↔ diagram).  The findings are sobering.
</p>
</div>
<div id="outline-container-sql-erd--the-gold-standard" class="outline-3">
<h3 id="sql-erd--the-gold-standard"><span class="section-number-3">3.1.</span> SQL ERD: The Gold Standard</h3>
<div class="outline-text-3" id="text-sql-erd--the-gold-standard">
<p>
SQL has been around since the 1970s, and its visual tooling reflects that maturity.  Over thirty actively maintained tools exist:
</p>

<ul class="org-ul">
<li><b>Free web-based</b>: <a href="https://dbdiagram.io/home/">dbdiagram.io</a> (with its <a href="https://dbml.dbdiagram.io/home/">DBML</a> diagram-as-code format), <a href="https://drawdb.vercel.app/">DrawDB</a>, <a href="https://drawsql.app/">DrawSQL</a>, <a href="https://chartdb.io/">ChartDB</a>, ERDPlus, QuickDBD</li>
<li><b>Commercial</b>: <a href="https://www.lucidchart.com/">Lucidchart</a>, <a href="https://vertabelo.com/">Vertabelo</a>, SQLDBM, DataGrip, Navicat</li>
<li><b>Open-source CLI</b>: <a href="https://schemaspy.org/">SchemaSpy</a>, SchemaCrawler, tbls, ERAlchemy</li>
</ul>

<p>
Many of these support true bidirectional workflows.  DBML, the language behind dbdiagram.io, lets you write schema definitions in a concise DSL, generate visual diagrams, <i>and</i> export DDL for multiple database engines.  The round-trip is seamless: design visually, export code; import code, refine visually.
</p>

<p>
AI-powered options are emerging too &#x2013; <a href="https://chartdb.io/">ChartDB</a> includes an AI agent for generating schemas, and <a href="https://eraser.io/">Eraser.io</a> can generate ERDs from natural language.  This is what mature tooling ecosystems look like.
</p>
</div>
</div>
<div id="outline-container-avro--the-tooling-desert" class="outline-3">
<h3 id="avro--the-tooling-desert"><span class="section-number-3">3.2.</span> Avro: The Tooling Desert</h3>
<div class="outline-text-3" id="text-avro--the-tooling-desert">
<p>
The total number of visualization tools I've been able to identify: roughly six.
</p>

<ul class="org-ul">
<li><b><a href="https://hackolade.com/nosqldb/avro-schema-editor.html">Hackolade Studio</a></b> (~$700/year per seat) &#x2013; The most capable option.  It supports forward-engineering (diagram → <code>.avsc</code>) and reverse-engineering (<code>.avsc</code> → diagram), generates ERD-style views, and integrates with Confluent Schema Registry.  But it's a proprietary desktop application, not a diagram-as-code tool, and the price point puts it out of reach for many teams.</li>
</ul>

<blockquote class="pull-quote pull-right">
<p>
You can see the tree, but you can't see the forest.
</p>
</blockquote>

<ul class="org-ul">
<li><b><a href="https://github.com/bolcom/avro-schema-viewer">bol.com Avro Schema Viewer</a></b> (free, open source) &#x2013; A web-based hierarchical tree viewer for <code>.avsc</code> files.  It renders schemas as expandable/collapsible trees with URL-based navigation.  It's useful for browsing, but it's read-only and doesn't produce relationship diagrams.  You can see the tree, but you can't see the forest.</li>

<li><b><a href="https://github.com/malisas/schema-uml">schema-uml</a></b> (free, open source) &#x2013; A Python tool that converts <code>.avdl</code> and <code>.proto</code> files into UML diagrams via Graphviz.  One-directional: schema → diagram.  No editing, no round-trip.</li>

<li><b><a href="https://javro.github.io/">Javro</a></b> (free) &#x2013; An Avro schema editor with autocomplete and JSON preview.  Focused on authoring, not visualization.  No diagram output.</li>

<li><b>Schema Registry UIs</b> (<a href="https://github.com/lensesio/schema-registry-ui">Lenses</a>, Confluent Cloud) &#x2013; Show schemas as raw JSON with version history.  No diagram view, no relationship visualization.</li>
</ul>

<p>
That's it.  Six tools, two of which are commercial, and only one of which offer bidirectional diagram-as-code editing.  Compare thirty-plus for SQL, with multiple free bidirectional options, AI integration, and active communities.  The gap is staggering<sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.
</p>
</div>
</div>
<div id="outline-container-protobuf-and-json-schema" class="outline-3">
<h3 id="protobuf-and-json-schema"><span class="section-number-3">3.3.</span> Protobuf and JSON Schema</h3>
<div class="outline-text-3" id="text-protobuf-and-json-schema">
<p>
The other major schema languages fare somewhat better, but still lag far behind SQL.
</p>

<p>
<b>Protocol Buffers</b> benefit from Google's ecosystem.  <a href="https://github.com/GoogleCloudPlatform/proto-gen-md-diagrams">proto-gen-md-diagrams</a> (from Google Cloud Platform) generates Markdown documentation with embedded Mermaid UML diagrams from <code>.proto</code> files.  <a href="https://pypi.org/project/protobuf-uml-diagram/">protobuf-uml-diagram</a> and <a href="https://github.com/vak/protobuf2uml">protobuf2uml</a> provide additional UML generation.  Protobuf's <code>.proto</code> format is also inherently more readable than Avro's JSON schema, which reduces (but doesn't eliminate) the need for visualization.
</p>

<p>
<b>JSON Schema</b> has the broadest visualization ecosystem among non-SQL formats: <a href="https://jsoncrack.com/">JSON Crack</a> (graph visualization of any JSON), <a href="https://github.com/atlassian-labs/json-schema-viewer">Atlassian's JSON Schema Viewer</a>, <a href="https://plugins.jetbrains.com/plugin/23554-json-schema-visualizer-editor">IntelliJ plugins</a>, and even a <a href="https://github.com/json-schema-org/community/issues/868">GSoC 2025 project</a> specifically focused on interactive graphical schema viewing.
</p>

<p>
But even JSON Schema visualization is nowhere near SQL's maturity.  And critically, across all three non-SQL formats, the category of <i>bidirectional diagram-as-code tools</i> is essentially empty.
</p>
</div>
</div>
<div id="outline-container-the-missing-category--bidirectional-diagram-as-code" class="outline-3">
<h3 id="the-missing-category--bidirectional-diagram-as-code"><span class="section-number-3">3.4.</span> The Missing Category: Bidirectional Diagram-as-Code</h3>
<div class="outline-text-3" id="text-the-missing-category--bidirectional-diagram-as-code">
<p>
This is the core insight.  The tools that exist for Avro (and Protobuf, and JSON Schema) fall into two categories:
</p>

<ol class="org-ol">
<li><b>Viewers</b> &#x2013; one-directional tools that render a schema as a diagram or tree.  Useful for understanding, but the output is a dead end.  You can't edit the diagram and have the changes flow back to your schema code.</li>

<li><b>Editors</b> &#x2013; authoring tools with autocomplete and validation, but no visual output.  You're still staring at text.</li>
</ol>

<p>
What's missing is the third category: <b>tools where the code and the diagram are the same artifact</b>, kept in sync bidirectionally.  In the SQL world, DBML and dbdiagram.io pioneered this.  In the architecture diagramming world, this is exactly what <a href="https://www.chiply.dev/post-cn-diagrams">CN Diagrams</a> does for architecture diagrams.
</p>

<p>
Schema Diagrams brings this same bidirectional paradigm to schema visualization.  Write Avro JSON or IDL in the code editor, see the diagram update in real time.  Click a field in the diagram to rename it, and the code updates.  <b>The code <i>is</i> the diagram, and the diagram <i>is</i> the code</b>.
</p>
</div>
</div>
</div>
<div id="outline-container-how-schema-diagrams-works" class="outline-2">
<h2 id="how-schema-diagrams-works"><span class="section-number-2">4.</span> How Schema Diagrams Works&#xa0;&#xa0;&#xa0;<span class="tag"><span class="features">features</span></span></h2>
<div class="outline-text-2" id="text-how-schema-diagrams-works">
<p>
Schema Diagrams provides several capabilities designed to make Avro schemas tangible.  You can try it directly at <a href="https://www.chiply.dev/schema-diagrams">Schema Diagrams</a>.
</p>
</div>
<div id="outline-container-bidirectional-editing" class="outline-3">
<h3 id="bidirectional-editing"><span class="section-number-3">4.1.</span> Bidirectional Editing</h3>
<div class="outline-text-3" id="text-bidirectional-editing">
<p>
The Monaco editor (the same engine behind VS Code) and the Svelte Flow canvas stay synchronized.  Add a field in the code, it appears as a row in the corresponding entity node.  Click a field name in the diagram to rename it, and the code updates.  Change a field's type via the visual dropdown, and the JSON or IDL reflects the change.
</p>

<p>
<b>This eliminates the choice between maintainability and accessibility</b>.  Engineers version-control the schema code through pull requests.  Data analysts and product owners explore and propose changes through the visual interface.  Both are editing the same artifact.
</p>
</div>
</div>
<div id="outline-container-dual-format-support" class="outline-3">
<h3 id="dual-format-support"><span class="section-number-3">4.2.</span> Dual Format Support</h3>
<div class="outline-text-3" id="text-dual-format-support">
<p>
Schema Diagrams supports both major Avro representations:
</p>

<ul class="org-ul">
<li><b>Avro JSON</b> (<code>.avsc</code>) &#x2013; the format that Schema Registries consume, and the format most Avro tooling produces.  Verbose but ubiquitous.</li>
<li><b>Avro IDL</b> (<code>.avdl</code>) &#x2013; the human-readable interface definition language.  More concise, closer to how engineers think about types.</li>
</ul>

<p>
Format detection is automatic.  Paste a JSON schema, and the tool recognizes it.  Switch to IDL, and it parses that instead.  Both formats are first-class citizens<sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup>.
</p>
</div>
</div>
<div id="outline-container-automatic-layout-and-inline-validation" class="outline-3">
<h3 id="automatic-layout-and-inline-validation"><span class="section-number-3">4.3.</span> Automatic Layout and Inline Validation</h3>
<div class="outline-text-3" id="text-automatic-layout-and-inline-validation">
<p>
Schema Diagrams uses <a href="https://www.eclipse.org/elk/">ELK</a> (Eclipse Layout Kernel) to position entity nodes automatically.  There's no manual arrangement required &#x2013; the layout algorithm handles node positioning, edge routing, and spacing.  This is a deliberate design choice: the diagram's layout is <i>deterministic</i> based on the schema structure, which means it stays consistent as schemas evolve.  For example, you won't see a complete shuffling of node and edge postitions when changing the name of a schema or field.
</p>

<p>
The Monaco editor provides inline validation with squiggly underlines and error markers at specific line/column positions.  Syntax errors, missing fields, and type resolution failures surface immediately as you type &#x2013; no waiting for a separate compilation or validation step.
</p>
</div>
</div>
<div id="outline-container-relationship-discovery" class="outline-3">
<h3 id="relationship-discovery"><span class="section-number-3">4.4.</span> Relationship Discovery</h3>
<div class="outline-text-3" id="text-relationship-discovery">
<p>
The tool automatically detects and visualizes relationships between schema types:
</p>

<ul class="org-ul">
<li><b>Reference edges</b> (blue) &#x2013; when a field's type is another named record</li>
<li><b>Nested record edges</b> (purple) &#x2013; when a record is defined inline within another</li>
<li><b>Join edges</b> (orange, dashed) &#x2013; explicit cross-schema relationships declared via <code>@join</code> annotations in IDL or <code>x.join</code> metadata in JSON</li>
</ul>

<p>
Each edge includes cardinality labels (1:1, 1:N, N:1, N:N), making the data model's topology immediately visible.  Records can be collapsed to reduce visual clutter while maintaining relationship visibility &#x2013; useful when working with schemas that have dozens of fields.
</p>
</div>
</div>
</div>
<div id="outline-container-benefits" class="outline-2">
<h2 id="benefits"><span class="section-number-2">5.</span> Benefits&#xa0;&#xa0;&#xa0;<span class="tag"><span class="productivity">productivity</span>&#xa0;<span class="collaboration">collaboration</span></span></h2>
<div class="outline-text-2" id="text-benefits">
</div>
<div id="outline-container-cross-persona-accessibility" class="outline-3">
<h3 id="cross-persona-accessibility"><span class="section-number-3">5.1.</span> Cross-Persona Accessibility</h3>
<div class="outline-text-3" id="text-cross-persona-accessibility">
<p>
The bidirectional sync is not just a technical feature &#x2013; it's an organizational one.  Data engineers author schemas in IDL or JSON.  Data analysts read the diagram.  Product owners can understand the data model without parsing nested JSON.  Governance teams audit schema structures visually.  <b>Everyone contributes in their preferred medium</b>, and everyone stays in sync.
</p>

<p>
This matters especially in organizations practicing data mesh, where data products are owned by domain teams rather than a central data platform.  The schema <i>is</i> the product's interface, and that interface needs to be readable by consumers who may not share the producer's technical background.
</p>
</div>
</div>
<div id="outline-container-schema-reviews-that-work" class="outline-3">
<h3 id="schema-reviews-that-work"><span class="section-number-3">5.2.</span> Schema Reviews That Work</h3>
<div class="outline-text-3" id="text-schema-reviews-that-work">
<p>
Pull request reviews of <code>.avsc</code> files are notoriously unproductive.  A 50-line JSON diff tells you <i>what</i> changed syntactically, but not <i>what it means</i> structurally.  Did we add a new entity?  Change a relationship?  Break backward compatibility?
</p>

<p>
Paste both versions into Schema Diagrams and the structural changes become immediately visible.  The visual representation surfaces the <i>intent</i> of the change, making reviews faster and more thorough.
</p>
</div>
</div>
<div id="outline-container-faster-onboarding" class="outline-3">
<h3 id="faster-onboarding"><span class="section-number-3">5.3.</span> Faster Onboarding</h3>
<div class="outline-text-3" id="text-faster-onboarding">
<p>
New team members joining a Kafka-based architecture with dozens of Avro topics can explore schemas visually rather than reading raw JSON files one by one.  The diagram reveals the data domain's structure at a glance &#x2013; which records reference which, where the enums live, what the cardinalities are &#x2013; providing an overview that would otherwise take days to assemble mentally.
</p>
</div>
</div>
<div id="outline-container-schema-evolution-awareness" class="outline-3">
<h3 id="schema-evolution-awareness"><span class="section-number-3">5.4.</span> Schema Evolution Awareness</h3>
<div class="outline-text-3" id="text-schema-evolution-awareness">
<p>
Avro's schema evolution rules &#x2013; backward compatibility, forward compatibility, full compatibility &#x2013; are powerful but difficult to reason about from raw JSON alone.  While Schema Diagrams is not a full evolution validator, the visual representation makes structural changes between schema versions obvious, helping teams catch potential issues before they reach production.
</p>
</div>
</div>
</div>
<div id="outline-container-beyond-avro" class="outline-2">
<h2 id="beyond-avro"><span class="section-number-2">6.</span> Beyond Avro&#xa0;&#xa0;&#xa0;<span class="tag"><span class="extensibility">extensibility</span>&#xa0;<span class="protobuf">protobuf</span>&#xa0;<span class="jsonSchema">jsonSchema</span></span></h2>
<div class="outline-text-2" id="text-beyond-avro">
<p>
The architectural pattern behind Schema Diagrams &#x2013; parse schema → build graph model → automatic layout → bidirectional sync &#x2013; is fundamentally schema-agnostic.  The parser produces an intermediate representation of entities, fields, and relationships.  The layout engine and visual editor don't care whether those entities came from Avro, Protobuf, or JSON Schema.
</p>

<p>
The current focus is Avro because the need is most acute there &#x2013; it has the weakest visualization tooling relative to its widespread use in streaming data platforms.  But the framework is designed to be extensible.  <i>Protocol Buffers, JSON Schema, Thrift, and even GraphQL SDL could all be supported by adding a parser that produces the same intermediate graph representation</i>.
</p>

<p>
The vision is a single tool where teams can visualize <i>any</i> schema format with the same bidirectional editing experience, regardless of which serialization technology their platform uses.
</p>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">7.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="bootstrap">bootstrap</span>&#xa0;<span class="ai">ai</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
The fastest path to a useful diagram is simple: paste a schema into the editor at <a href="https://www.chiply.dev/schema-diagrams">Schema Diagrams</a> and watch the diagram appear.  Built-in example schemas (simple records, deeply nested hierarchies) provide starting points if you want to explore the tool before bringing your own data.
</p>

<p>
For teams with existing schemas spread across a Schema Registry, AI assistance can accelerate the process.  LLMs are remarkably good at reading structured formats like JSON and IDL, and they can help consolidate, clean up, or annotate schemas before visualization.  The workflow is straightforward:
</p>

<ol class="org-ol">
<li>Export schemas from your Schema Registry (Confluent CLI, REST API, etc.)</li>
<li>Ask an AI agent to consolidate related schemas, add <code>@join</code> annotations for cross-schema relationships, or convert between JSON and IDL</li>
<li>Paste the result into Schema Diagrams</li>
<li>Refine visually &#x2013; renames, type adjustments, and field additions happen directly on the canvas</li>
</ol>

<p>
This isn't as transformative as AI-bootstrapping an entire architecture diagram from a codebase (as described in the <a href="https://www.chiply.dev/post-cn-diagrams">CN Diagrams</a> post), because schemas already <i>exist</i> as structured text.  But it meaningfully lowers the barrier to getting a comprehensive visual overview of a data domain that might span dozens of <code>.avsc</code> files.
</p>
</div>
</div>
<div id="outline-container-contributing" class="outline-2">
<h2 id="contributing"><span class="section-number-2">8.</span> Contributing&#xa0;&#xa0;&#xa0;<span class="tag"><span class="openSource">openSource</span></span></h2>
<div class="outline-text-2" id="text-contributing">
<p>
Schema Diagrams is open source under the MIT license.  The source code is available at <a href="https://github.com/chiply/schema-diagrams">github.com/chiply/schema-diagrams</a>.
</p>
</div>
<div id="outline-container-tech-stack" class="outline-3">
<h3 id="tech-stack"><span class="section-number-3">8.1.</span> Tech Stack</h3>
<div class="outline-text-3" id="text-tech-stack">
<p>
For those interested in contributing, Schema Diagrams is built with:
</p>

<ul class="org-ul">
<li><b>SvelteKit</b> (Svelte 5) &#x2013; Application framework with runes for reactivity</li>
<li><b>Svelte Flow</b> (<a href="https://svelteflow.dev/">@xyflow/svelte</a>) &#x2013; Interactive node/edge diagram canvas</li>
<li><b>ELK</b> (<a href="https://www.eclipse.org/elk/">elkjs</a>) &#x2013; Automatic graph layout engine</li>
<li><b>Monaco Editor</b> &#x2013; VS Code's editor component for schema authoring</li>
<li><b>TypeScript</b> throughout</li>
<li><b>Vercel</b> &#x2013; Deployment platform</li>
</ul>
</div>
</div>
<div id="outline-container-how-to-contribute" class="outline-3">
<h3 id="how-to-contribute"><span class="section-number-3">8.2.</span> How to Contribute</h3>
<div class="outline-text-3" id="text-how-to-contribute">
<p>
Contributions are welcome in several forms:
</p>

<p>
<b>Bug Reports</b>: If something doesn't work as expected, open an issue with steps to reproduce.  Include your browser and any error messages from the console.
</p>

<p>
<b>Feature Requests</b>: Have an idea for improvement?  Open an issue describing the use case and proposed solution.  New parser support (Protobuf, JSON Schema) is a particularly impactful area.
</p>

<p>
<b>Pull Requests</b>: Fork the repository, create a feature branch, and submit a PR.  Please include tests for new functionality and ensure existing tests pass.
</p>

<p>
<b>Documentation</b>: Improvements to README, examples, or inline comments are valuable contributions that don't require deep code knowledge.
</p>

<p>
The project is early-stage, and there's significant opportunity to shape its direction.  Parser contributions for additional schema formats would be especially welcome.
</p>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">9.</span> Conclusion</h2>
<div class="outline-text-2" id="text-conclusion">
<p>
Schema visualization shouldn't be a luxury reserved for SQL databases.  The schema languages powering modern event-driven architectures &#x2013; Avro, Protobuf, JSON Schema &#x2013; define the contracts between services, the shape of data flowing through pipelines, and the interfaces of data products.  Those contracts deserve the same visual tooling that ERDs have provided for relational databases for decades.
</p>

<p>
Schema Diagrams is an attempt to close that gap, starting with the format that needs it most.  Avro's JSON verbosity, its deep nesting, its implicit relationships &#x2013; these aren't flaws in the format, but they do make visualization essential rather than optional.  And by building on the bidirectional sync paradigm, the tool serves not just the engineer who writes the schema, but every persona in the organization who needs to <i>understand</i> it.
</p>

<p>
Try it at <a href="https://www.chiply.dev/schema-diagrams">Schema Diagrams</a>.  Browse the source at <a href="https://github.com/chiply/schema-diagrams">github.com/chiply/schema-diagrams</a>.  If you build something interesting or have feedback, I'd love to hear about it.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">10.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
Schema Diagrams is an open-source tool that brings bidirectional, diagram-as-code visualization to Apache Avro schemas &#x2013; closing a gap that SQL ERD tools filled decades ago.  <a href="https://www.chiply.dev/#about">The problem</a> is straightforward: Avro schemas are defined as deeply nested JSON, and the tooling for visualizing them is nearly nonexistent.  While SQL has thirty-plus ERD tools (many free, many bidirectional), <a href="https://www.chiply.dev/#the-state-of-schema-visualization-tooling">Avro has roughly six</a> &#x2013; two commercial, the rest read-only viewers or one-directional generators.  No bidirectional diagram-as-code tool existed for Avro.
</p>

<p>
<a href="https://www.chiply.dev/#how-schema-diagrams-works">Schema Diagrams</a> fills this gap with a Monaco editor (VS Code's engine) synchronized bidirectionally with a Svelte Flow canvas.  Edit Avro JSON or IDL in the code editor, and the ERD updates in real time.  Click fields in the diagram to rename them, change types, or add new fields, and the code updates.  The tool supports both <code>.avsc</code> JSON and <code>.avdl</code> IDL with automatic format detection, uses ELK for deterministic automatic layout, and renders relationship edges with cardinality labels.
</p>

<p>
<a href="https://www.chiply.dev/#benefits">The payoff</a> is organizational: engineers version-control schemas through PRs while analysts and product owners explore visually &#x2013; everyone works on the same artifact.  Schema reviews become structural rather than syntactic, onboarding accelerates, and cross-team communication improves.  <a href="https://www.chiply.dev/#beyond-avro">The architecture is schema-agnostic</a> &#x2013; Protobuf, JSON Schema, and other formats could reuse the same engine.  Try it at the Schema Diagrams page or explore the source at <a href="https://github.com/chiply/schema-diagrams">github.com/chiply/schema-diagrams</a>.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This is the same bidirectional sync paradigm behind <a href="https://www.chiply.dev/post-cn-diagrams">CN Diagrams</a>, my architecture diagramming tool.  The core insight is the same: when code and visuals are the same artifact, you don't have to choose between maintainability and accessibility.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Avro IDL (<code>.avdl</code>) is significantly more readable than the JSON representation, and Schema Diagrams supports both.  But in practice, most Schema Registry tooling and most Avro codegen pipelines work with <code>.avsc</code> JSON, so that's what engineers end up reading and reviewing.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
There's an irony here.  Avro was designed specifically for data exchange &#x2013; it's a schema-first format built for interoperability between systems.  Yet the tooling for <i>understanding</i> those schemas across teams is almost nonexistent.  The format that most needs cross-persona visualization has the least of it.
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Automatic format detection sounds simple, but it matters more than you might think.  In practice, engineers switch between JSON and IDL constantly &#x2013; JSON for registry interactions, IDL for human authoring.  A tool that requires you to specify the format adds friction.  One that figures it out adds flow.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>brushup: Theme-Aware Dynamic Color Palette for Emacs</title>
      <link>https://www.chiply.dev/post-brushup</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-brushup</guid>
      <pubDate>Sun, 01 Mar 2026 19:49:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Emacs theming has a dirty secret: most face customizations are written against one theme and silently break against every other. You pick colors that look right in your dark theme, switch to something...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="theming">theming</span>&#xa0;<span class="colorPalette">colorPalette</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgde99987" class="figure">
<p><img src="https://www.chiply.dev/images/brushup-banner.jpeg" alt="brushup-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Emacs theming has a dirty secret: most face customizations are written against one theme and silently break against every other.  You pick colors that look right in your dark theme, switch to something light for a change of scenery, and suddenly half your UI is unreadable.  <a href="https://github.com/chiply/brushup">brushup</a> generates a dynamic color palette from your current theme's foreground and background, so your face customizations work everywhere.
</p>
</div>
</div>
<div id="outline-container-theme-dependent-styling" class="outline-2">
<h2 id="theme-dependent-styling"><span class="section-number-2">2.</span> The Problem: Theme-Dependent Styling&#xa0;&#xa0;&#xa0;<span class="tag"><span class="theming">theming</span>&#xa0;<span class="problem">problem</span></span></h2>
<div class="outline-text-2" id="text-theme-dependent-styling">
<p>
Say you've customized org-mode headings, your modeline, or some niche package's faces.  You hardcode hex values because they look good in your current theme:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(set-face-attribute 'org-level-1 nil :foreground "#a0b0c0")
(set-face-attribute 'my-custom-face nil :background "#1a1a2e")
</pre>
</div>

<p>
These colors assume a dark background.  Switch to a light theme and <code>#1a1a2e</code> is nearly invisible against a white background, while <code>#a0b0c0</code> loses all its contrast.  The usual workaround is to write conditional logic:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(if (eq (frame-parameter nil 'background-mode) 'dark)
    (set-face-attribute 'org-level-1 nil :foreground "#a0b0c0")
  (set-face-attribute 'org-level-1 nil :foreground "#3a4a5a"))
</pre>
</div>

<p>
This doubles your configuration for every face you touch and still only handles two themes.  If you use three or four themes across different times of day or contexts, the branching gets unwieldy fast.  What you actually want is a way to say "give me a color that's 30% of the way from the background toward the foreground" and have that resolve correctly regardless of the theme.
</p>
</div>
</div>
<div id="outline-container-how-brushup-works" class="outline-2">
<h2 id="how-brushup-works"><span class="section-number-2">3.</span> How brushup Works&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="architecture">architecture</span></span></h2>
<div class="outline-text-2" id="text-how-brushup-works">
<p>
brushup reads two colors from your current theme: the default foreground and the default background.  From these, it generates a parametric gradient, a series of intermediate colors stepping from one toward the other.  The result is twelve palette variables: six foreground gradients (<code>brushup-fg-1</code> through <code>brushup-fg-6</code>, stepping from the foreground toward the background) and six background gradients (<code>brushup-bg-1</code> through <code>brushup-bg-6</code>, stepping from the background toward the foreground).
</p>

<blockquote class="pull-quote pull-right">
<p>
You describe your color intent in relative terms, and the palette resolves it against whatever theme is active.
</p>
</blockquote>

<p>
The gradient step size is controlled by <code>brushup-gradient-step</code> (default: 7%).  Each level moves that percentage further along the lightness axis.  <code>brushup-fg-1</code> is a subtle shift, barely distinguishable from the raw foreground.  <code>brushup-fg-6</code> is a dramatic shift, nearly halfway to the background.
</p>

<p>
brushup also detects whether your theme is dark or light using a luminosity calculation on the background color, exposing the result as <code>brushup-dark-p</code>.  This means you never need to check <code>background-mode</code> yourself.
</p>

<p>
When <code>brushup-mode</code> is enabled, the package hooks into <code>enable-theme-functions</code>.  Every time you load or switch themes, brushup re-generates the entire palette and re-evaluates all registered styles.  You describe your color intent in relative terms, and the palette resolves it against whatever theme is active.
</p>
</div>
</div>
<div id="outline-container-the-palette" class="outline-2">
<h2 id="the-palette"><span class="section-number-2">4.</span> The Palette&#xa0;&#xa0;&#xa0;<span class="tag"><span class="theming">theming</span>&#xa0;<span class="reference">reference</span></span></h2>
<div class="outline-text-2" id="text-the-palette">
<p>
Here's the full set of palette variables:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Variable</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>brushup-fg</code></td>
<td class="org-left">Raw theme foreground</td>
</tr>

<tr>
<td class="org-left"><code>brushup-bg</code></td>
<td class="org-left">Raw theme background</td>
</tr>

<tr>
<td class="org-left"><code>brushup-fg-1</code> to <code>brushup-fg-6</code></td>
<td class="org-left">Foreground gradient (1=subtle, 6=strong shift toward bg)</td>
</tr>

<tr>
<td class="org-left"><code>brushup-bg-1</code> to <code>brushup-bg-6</code></td>
<td class="org-left">Background gradient (1=subtle, 6=strong shift toward fg)</td>
</tr>

<tr>
<td class="org-left"><code>brushup-bg-1_0</code></td>
<td class="org-left">Very subtle background shift (solaire-like effect)</td>
</tr>

<tr>
<td class="org-left"><code>brushup-dark-p</code></td>
<td class="org-left"><code>t</code> if current theme is dark</td>
</tr>
</tbody>
</table>

<p>
The gradient levels map naturally to common styling needs:
</p>

<ul class="org-ul">
<li><b>Level 1-2</b>: Subtle emphasis.  Comments, inactive modeline text, de-emphasized UI elements.</li>
<li><b>Level 3-4</b>: Moderate contrast.  Secondary headings, sidebar text, annotations.</li>
<li><b>Level 5-6</b>: Strong contrast.  Borders, separators, high-visibility indicators.</li>
</ul>
</div>
</div>
<div id="outline-container-demo-switching-themes" class="outline-2">
<h2 id="demo-switching-themes"><span class="section-number-2">5.</span> Demo: Switching Themes&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-switching-themes">
<p>
Here's what it looks like when you switch themes with brushup active.  Every registered face customization re-evaluates instantly.
</p>

<video src="https://www.chiply.dev/videos/brushup-theme-switch.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-style-system" class="outline-2">
<h2 id="style-system"><span class="section-number-2">6.</span> The Style System&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="configuration">configuration</span></span></h2>
<div class="outline-text-2" id="text-style-system">
<p>
brushup doesn't apply colors directly.  Instead, you register forms in <code>brushup-styles</code>, a list of expressions that get evaluated whenever the palette updates.  Each form can reference any palette variable:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(add-to-list 'brushup-styles
  '(set-face-attribute 'org-level-1 nil :foreground brushup-fg-3))

(add-to-list 'brushup-styles
  '(set-face-attribute 'line-number nil
     :foreground brushup-fg-5
     :background brushup-bg-1))
</pre>
</div>

<p>
When the theme changes, brushup regenerates the palette values, then walks the list and evaluates each form.  Errors in individual forms are caught and reported without stopping the rest, so a broken customization won't take down your entire UI.
</p>
</div>
<div id="outline-container-use-package-integration" class="outline-3">
<h3 id="use-package-integration"><span class="section-number-3">6.1.</span> use-package Integration</h3>
<div class="outline-text-3" id="text-use-package-integration">
<p>
brushup registers a custom <code>:brushup</code> keyword with <code>use-package</code>, so you can co-locate your style registrations with the packages they customize:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package magit
  :ensure t
  :brushup
  (add-to-list 'brushup-styles
    '(set-face-attribute 'magit-section-heading nil
       :foreground brushup-fg-2)))
</pre>
</div>

<p>
The <code>:brushup</code> keyword defers evaluation until the package loads, then registers the forms.  This keeps theme-dependent styling next to the package configuration it belongs with.
</p>
</div>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">7.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="installation">installation</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
brushup is on <a href="https://github.com/chiply/brushup">GitHub</a>.  Install with <code>elpaca</code>, <code>straight.el</code>, or manually, then enable <code>brushup-mode</code>.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package brushup
  :ensure (:host github :repo "chiply/brushup")
  :config
  (brushup-mode 1))
</pre>
</div>

<p>
If you're already maintaining a pile of theme-conditional face settings, brushup is a good excuse to delete them.  Replace the hardcoded hex values with palette variables, register them as styles, and let the gradient do the work.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>magneto: Composable Window Management for Emacs</title>
      <link>https://www.chiply.dev/post-magneto</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-magneto</guid>
      <pubDate>Sun, 01 Mar 2026 19:49:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Window management in Emacs is either too simple or too opinionated. split-window-right and split-window-below are basic primitives that require manual sequencing. purpose and shackle give you declarat...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="windowManagement">windowManagement</span>&#xa0;<span class="composition">composition</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgcdd29d8" class="figure">
<p><img src="https://www.chiply.dev/images/magneto-banner.jpeg" alt="magneto-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Window management in Emacs is either too simple or too opinionated.  <code>split-window-right</code> and <code>split-window-below</code> are basic primitives that require manual sequencing.  <code>purpose</code> and <code>shackle</code> give you declarative rules but take away direct control.  What I wanted was something in between: a small grammar of composable operations where I set the parameters I care about and let the system handle the plumbing.  <a href="https://github.com/chiply/magneto">magneto</a> is that grammar.  Press a key to enter compose mode, set any combination of source action, destination, cursor placement, and buffer action, then press <code>RET</code> to execute.
</p>
</div>
</div>
<div id="outline-container-the-window-management-problem" class="outline-2">
<h2 id="the-window-management-problem"><span class="section-number-2">2.</span> The Window Management Problem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="problem">problem</span></span></h2>
<div class="outline-text-2" id="text-the-window-management-problem">
<p>
Here's a common sequence: you're looking at a file and you want to open a related file in a vertical split to the right, keeping your cursor on the original.  In vanilla Emacs:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(split-window-right)       ; split
(other-window 1)           ; move to new window
(find-file "related.el")   ; open file
(other-window -1)          ; move back
</pre>
</div>

<blockquote class="pull-quote pull-right">
<p>
Every combination of source, destination, cursor, and buffer action is a separate function you'd need to write or find.
</p>
</blockquote>

<p>
Four commands, and you need to remember the sequence.  Want to <i>copy</i> the current buffer instead of opening a new file?  Different sequence.  Want to split below instead of right?  Different sequence.  Want the cursor to stay in the new window?  Omit the last step.  Every combination of source, destination, cursor, and buffer action is a separate function you'd need to write or find.
</p>

<p>
magneto collapses this combinatorial explosion into a single composable flow.  Instead of four sequential commands, you press your magneto key, then press modifiers to set whatever differs from the defaults, then execute.  The number of keystrokes scales with how much you're customizing, not with the inherent complexity of the operation.
</p>
</div>
</div>
<div id="outline-container-composable-operations" class="outline-2">
<h2 id="composable-operations"><span class="section-number-2">3.</span> Composable Operations&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="architecture">architecture</span></span></h2>
<div class="outline-text-2" id="text-composable-operations">
<p>
magneto operations have four independent dimensions:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Dimension</th>
<th scope="col" class="org-left">Question</th>
<th scope="col" class="org-left">Default</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Source Action</b></td>
<td class="org-left">What happens to the origin window?</td>
<td class="org-left">move</td>
</tr>

<tr>
<td class="org-left"><b>Destination</b></td>
<td class="org-left">Where does the buffer go?</td>
<td class="org-left">fill existing</td>
</tr>

<tr>
<td class="org-left"><b>Cursor Placement</b></td>
<td class="org-left">Where does your cursor end up?</td>
<td class="org-left">follow (<code>o</code>)</td>
</tr>

<tr>
<td class="org-left"><b>Buffer Action</b></td>
<td class="org-left">What buffer appears in the destination?</td>
<td class="org-left">switch-buffer</td>
</tr>
</tbody>
</table>

<p>
Each dimension has its own keys.  You press any combination of them in any order before executing with <code>RET</code>.  Any dimension you don't set uses its default.  This means the simplest magneto operation (just <code>RET</code> with all defaults) is equivalent to <code>switch-to-buffer</code> in the current window.  But adding one or two keys before <code>RET</code> gives you dramatically different behavior.
</p>
</div>
</div>
<div id="outline-container-source-actions" class="outline-2">
<h2 id="source-actions"><span class="section-number-2">4.</span> Source Actions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="source">source</span></span></h2>
<div class="outline-text-2" id="text-source-actions">
<p>
The source action controls what happens to the <i>origin</i> window after the buffer lands in its destination.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Action</th>
<th scope="col" class="org-left">Behavior</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>m</code></td>
<td class="org-left">Move</td>
<td class="org-left">Delete the origin window (buffer leaves the original spot)</td>
</tr>

<tr>
<td class="org-left"><code>c</code></td>
<td class="org-left">Copy</td>
<td class="org-left">Keep the origin window (buffer is now visible in two places)</td>
</tr>

<tr>
<td class="org-left"><code>p</code></td>
<td class="org-left">Pull</td>
<td class="org-left">Show the previous buffer in the origin window</td>
</tr>
</tbody>
</table>

<p>
Move is the default and matches the most common intent: relocate a buffer.  Copy is useful when you want a reference view alongside an editing view of the same context.  Pull is a refinement of move, the buffer leaves, but instead of the window disappearing, it shows whatever was there before.
</p>
</div>
</div>
<div id="outline-container-destination-actions" class="outline-2">
<h2 id="destination-actions"><span class="section-number-2">5.</span> Destination Actions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="destination">destination</span></span></h2>
<div class="outline-text-2" id="text-destination-actions">
<p>
The destination controls <i>where</i> the buffer ends up.
</p>
</div>
<div id="outline-container-splits" class="outline-3">
<h3 id="splits"><span class="section-number-3">5.1.</span> Splits</h3>
<div class="outline-text-3" id="text-splits">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Direction</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>V</code></td>
<td class="org-left">Split above</td>
</tr>

<tr>
<td class="org-left"><code>v</code></td>
<td class="org-left">Split below</td>
</tr>

<tr>
<td class="org-left"><code>H</code></td>
<td class="org-left">Split left</td>
</tr>

<tr>
<td class="org-left"><code>h</code></td>
<td class="org-left">Split right</td>
</tr>
</tbody>
</table>

<p>
Uppercase keys create the split on the "before" side (above, left); lowercase on the "after" side (below, right).
</p>
</div>
</div>
<div id="outline-container-side-windows" class="outline-3">
<h3 id="side-windows"><span class="section-number-3">5.2.</span> Side Windows</h3>
<div class="outline-text-3" id="text-side-windows">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Position</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>t</code></td>
<td class="org-left">Top side</td>
</tr>

<tr>
<td class="org-left"><code>b</code></td>
<td class="org-left">Bottom side</td>
</tr>

<tr>
<td class="org-left"><code>l</code></td>
<td class="org-left">Left side</td>
</tr>

<tr>
<td class="org-left"><code>r</code></td>
<td class="org-left">Right side</td>
</tr>
</tbody>
</table>

<p>
Side windows use Emacs's <code>display-buffer-in-side-window</code>, which means they resist <code>delete-other-windows</code> and stay pinned to their edge.  Uppercase variants (<code>T</code>, <code>B</code>, <code>L</code>, <code>R</code>) control slot ordering when you have multiple side windows on the same edge.
</p>
</div>
</div>
<div id="outline-container-fill-existing" class="outline-3">
<h3 id="fill-existing"><span class="section-number-3">5.3.</span> Fill Existing Window</h3>
<div class="outline-text-3" id="text-fill-existing">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>0</code></td>
<td class="org-left">Select an existing window via ace-window</td>
</tr>
</tbody>
</table>

<p>
Fill doesn't create a new split.  It routes the buffer into a window you pick interactively.  This is the default destination, so pressing <code>RET</code> immediately prompts you to pick a window (or uses the current one if there's only one).
</p>
</div>
</div>
</div>
<div id="outline-container-cursor-placement" class="outline-2">
<h2 id="cursor-placement"><span class="section-number-2">6.</span> Cursor Placement&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="cursor">cursor</span></span></h2>
<div class="outline-text-2" id="text-cursor-placement">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Behavior</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>o</code></td>
<td class="org-left">Follow: cursor moves to the destination window</td>
</tr>

<tr>
<td class="org-left"><code>O</code></td>
<td class="org-left">Stay: cursor remains in the origin window</td>
</tr>
</tbody>
</table>

<p>
This is the difference between "open this file over there and keep working here" and "open this file over there and go to it."  A small distinction that eliminates a <code>other-window</code> call in half your workflows.
</p>
</div>
</div>
<div id="outline-container-buffer-actions" class="outline-2">
<h2 id="buffer-actions"><span class="section-number-2">7.</span> Buffer Actions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="buffers">buffers</span></span></h2>
<div class="outline-text-2" id="text-buffer-actions">
<p>
The buffer action controls <i>what</i> appears in the destination window.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Action</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>w</code></td>
<td class="org-left">Buffer (consult)</td>
<td class="org-left">Switch via <code>magneto-buffer-command</code> (customizable, defaults to <code>switch-to-buffer</code>)</td>
</tr>

<tr>
<td class="org-left"><code>f</code></td>
<td class="org-left">Find file</td>
<td class="org-left">Call <code>find-file</code> interactively</td>
</tr>

<tr>
<td class="org-left"><code>x</code></td>
<td class="org-left">Execute command</td>
<td class="org-left">Run an extended command</td>
</tr>

<tr>
<td class="org-left"><code>C-b</code></td>
<td class="org-left">Switch buffer</td>
<td class="org-left">Built-in <code>switch-to-buffer</code> directly</td>
</tr>
</tbody>
</table>

<p>
Set <code>magneto-buffer-command</code> to <code>#'consult-buffer</code> if you use consult:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(setq magneto-buffer-command #'consult-buffer)
</pre>
</div>
</div>
</div>
<div id="outline-container-demo-basic" class="outline-2">
<h2 id="demo-basic"><span class="section-number-2">8.</span> Demo: Basic Composition&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-basic">
<p>
Composing a few simple operations: split right with a file, copy to a split below, move to an existing window.
</p>

<video src="https://www.chiply.dev/videos/magneto-basic.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-demo-side-windows" class="outline-2">
<h2 id="demo-side-windows"><span class="section-number-2">9.</span> Demo: Side Windows&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-side-windows">
<p>
Pinning buffers to edges using side window destinations.
</p>

<video src="https://www.chiply.dev/videos/magneto-side-windows.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-demo-ace-window" class="outline-2">
<h2 id="demo-ace-window"><span class="section-number-2">10.</span> Demo: Pre-selecting with Ace-Window&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-ace-window">
<p>
Pre-selecting a destination window before composing the rest of the operation.
</p>

<video src="https://www.chiply.dev/videos/magneto-ace-preselect.mp4" data-interactive-video>
</video>

<p>
magneto uses ace-window's visual overlay to let you pick a window.  The keys <code>a</code>, <code>s</code>, <code>d</code> in the compose keymap pre-select the first, second, or third window by ace-window ordering.  This lets you set the destination before you've even decided what to put in it.
</p>
</div>
</div>
<div id="outline-container-embark-integration" class="outline-2">
<h2 id="embark-integration"><span class="section-number-2">11.</span> Embark Integration&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="composition">composition</span></span></h2>
<div class="outline-text-2" id="text-embark-integration">
<p>
magneto includes an optional embark integration module (<code>magneto-embark.el</code>) that routes embark actions through magneto's compose system.  The idea: when you act on an embark candidate, you often want to control <i>where</i> the result ends up.  Without magneto, embark opens files in the current window.  With magneto, you can send them to a split, a side window, or a pre-selected ace-window target.
</p>

<p>
<code>magneto-embark-bind-keys</code> scans all registered embark keymaps and generates magneto-routed versions of relevant actions (find-file, consult-bookmark, goto-grep, etc.).  These are bound under <code>s-o</code> in each embark keymap.  So where you'd normally press <code>f</code> in embark to open a file, pressing <code>s-o f</code> opens it through magneto's compose flow instead.
</p>
</div>
</div>
<div id="outline-container-demo-embark" class="outline-2">
<h2 id="demo-embark"><span class="section-number-2">12.</span> Demo: Embark Workflows&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-embark">
<p>
Routing embark actions through magneto to control destination windows.
</p>

<video src="https://www.chiply.dev/videos/magneto-embark.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-workflow-examples" class="outline-2">
<h2 id="workflow-examples"><span class="section-number-2">13.</span> Workflow Examples&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="workflows">workflows</span></span></h2>
<div class="outline-text-2" id="text-workflow-examples">
</div>
<div id="outline-container-expand-layout" class="outline-3">
<h3 id="expand-layout"><span class="section-number-3">13.1.</span> Expand a Layout</h3>
<div class="outline-text-3" id="text-expand-layout">
<p>
You're reading a file.  You want to open a test file to the right and keep your cursor where you are.
</p>

<p>
Keys: <code>magneto-compose</code> <code>h</code> <code>O</code> <code>f</code> <code>RET</code> (then pick the file)
</p>

<p>
Breakdown: <code>h</code> (split right), <code>O</code> (stay at origin), <code>f</code> (find-file), <code>RET</code> (execute).
</p>
</div>
</div>
<div id="outline-container-side-by-side" class="outline-3">
<h3 id="side-by-side"><span class="section-number-3">13.2.</span> Side-by-Side Comparison</h3>
<div class="outline-text-3" id="text-side-by-side">
<p>
You want the same buffer in two windows, scrolled to different positions.
</p>

<p>
Keys: <code>magneto-compose</code> <code>c</code> <code>h</code> <code>RET</code>
</p>

<p>
Breakdown: <code>c</code> (copy, keep original), <code>h</code> (split right), <code>RET</code> (execute).  The buffer is now in both windows.  For independent scroll positions, use <code>M-x magneto-make-indirect</code> first to create an indirect buffer clone.
</p>
</div>
</div>
<div id="outline-container-repl-setup" class="outline-3">
<h3 id="repl-setup"><span class="section-number-3">13.3.</span> REPL Setup</h3>
<div class="outline-text-3" id="text-repl-setup">
<p>
You want a shell in a bottom side window that persists across <code>delete-other-windows</code>.
</p>

<p>
Keys: <code>magneto-compose</code> <code>b</code> <code>x</code> <code>RET</code> (then type <code>shell</code>)
</p>

<p>
Breakdown: <code>b</code> (bottom side window), <code>x</code> (execute-command), <code>RET</code> (execute).  The shell opens in a persistent bottom panel.
</p>
</div>
</div>
</div>
<div id="outline-container-customization-reference" class="outline-2">
<h2 id="customization-reference"><span class="section-number-2">14.</span> Customization Reference&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="configuration">configuration</span></span></h2>
<div class="outline-text-2" id="text-customization-reference">
<p>
All defaults are customizable:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Variable</th>
<th scope="col" class="org-left">Default</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>magneto-default-source-action</code></td>
<td class="org-left"><code>"move"</code></td>
<td class="org-left">Default source behavior</td>
</tr>

<tr>
<td class="org-left"><code>magneto-default-destination-action</code></td>
<td class="org-left"><code>"f"</code></td>
<td class="org-left">Default destination (fill)</td>
</tr>

<tr>
<td class="org-left"><code>magneto-default-select-action</code></td>
<td class="org-left"><code>"o"</code></td>
<td class="org-left">Default cursor placement (follow)</td>
</tr>

<tr>
<td class="org-left"><code>magneto-default-action-action</code></td>
<td class="org-left"><code>"switch-buffer"</code></td>
<td class="org-left">Default buffer action</td>
</tr>

<tr>
<td class="org-left"><code>magneto-buffer-command</code></td>
<td class="org-left"><code>#'switch-to-buffer</code></td>
<td class="org-left">Command for buffer switching</td>
</tr>

<tr>
<td class="org-left"><code>magneto-default-destination-window</code></td>
<td class="org-left"><code>nil</code></td>
<td class="org-left">Pre-selected ace-window target</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">15.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="installation">installation</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
magneto is on <a href="https://github.com/chiply/magneto">GitHub</a>.  It requires Emacs 29.1+ and depends on <code>avy</code> and <code>ace-window</code>.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package magneto
  :ensure (:host github :repo "chiply/magneto")
  :bind ("s-m" . magneto-compose)
  :custom
  (magneto-buffer-command #'consult-buffer))
</pre>
</div>

<p>
For embark integration:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package magneto-embark
  :after (magneto embark)
  :config
  (magneto-embark-bind-keys))
</pre>
</div>

<p>
Bind <code>magneto-compose</code> to a comfortable key, press it, and start composing.  The which-key popup (if you use which-key) shows all available modifiers.  Press <code>RET</code> when you're ready.  Start with the defaults and add modifiers as you learn the grammar.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>repeatable-lite: Make Any Prefix Command Repeatable</title>
      <link>https://www.chiply.dev/post-repeatable-lite</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-repeatable-lite</guid>
      <pubDate>Sun, 01 Mar 2026 19:49:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Emacs loves prefix keys. C-x, C-c, C-c w, C-c p &amp;#x2026; the deeper your configuration goes, the longer your prefixes get. The problem is that many commands you&apos;d want to call repeatedly sit behind th...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="keybindings">keybindings</span>&#xa0;<span class="productivity">productivity</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org9ef604b" class="figure">
<p><img src="https://www.chiply.dev/images/repeatable-lite-banner.jpeg" alt="repeatable-lite-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Emacs loves prefix keys.  <code>C-x</code>, <code>C-c</code>, <code>C-c w</code>, <code>C-c p</code> &#x2026; the deeper your configuration goes, the longer your prefixes get.  The problem is that many commands you'd want to call repeatedly sit behind these prefixes: window navigation, text scaling, buffer cycling.  You end up typing the same prefix over and over.  <a href="https://github.com/chiply/repeatable-lite">repeatable-lite</a> fixes this in about 230 lines.  Wrap a command with <code>repeatable-lite-wrap</code> and after the first invocation, the prefix stays active.  Press the final key again to repeat.  Press anything else to exit.
</p>
</div>
</div>
<div id="outline-container-the-prefix-problem" class="outline-2">
<h2 id="the-prefix-problem"><span class="section-number-2">2.</span> The Prefix Problem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="problem">problem</span></span></h2>
<div class="outline-text-2" id="text-the-prefix-problem">
<p>
Say you've bound window navigation to <code>C-c w h/j/k/l</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(global-set-key (kbd "C-c w h") #'windmove-left)
(global-set-key (kbd "C-c w l") #'windmove-right)
(global-set-key (kbd "C-c w j") #'windmove-down)
(global-set-key (kbd "C-c w k") #'windmove-up)
</pre>
</div>

<p>
You want to move two windows left.  That's <code>C-c w h C-c w h</code>, eight keystrokes for two moves.  Navigate through a four-window layout and you're pressing <code>C-c w</code> four times.  The prefix is supposed to organize your keybindings, but under repetition it becomes dead weight.
</p>

<blockquote class="pull-quote pull-right">
<p>
The prefix is supposed to organize your keybindings, but under repetition it becomes dead weight.
</p>
</blockquote>

<p>
Emacs has <code>repeat-mode</code> (built-in since Emacs 28), which solves this for built-in commands.  But it requires you to define <code>repeat-map</code> keymaps manually for every group of commands, and it doesn't integrate with which-key to show what's available.  <code>hydra</code> and <code>transient</code> are more powerful but heavier, with their own configuration DSLs and display systems.  repeatable-lite sits in the gap: one macro, automatic which-key integration, no configuration language.
</p>
</div>
</div>
<div id="outline-container-how-it-works" class="outline-2">
<h2 id="how-it-works"><span class="section-number-2">3.</span> How It Works&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="implementation">implementation</span></span></h2>
<div class="outline-text-2" id="text-how-it-works">
<p>
The API is a single macro: <code>repeatable-lite-wrap</code>.  It takes a function name and returns a new command that:
</p>

<ol class="org-ol">
<li>Calls the original function.</li>
<li>Re-activates the prefix keymap.</li>
<li>Shows which-key with all available keys in that prefix.</li>
<li>Reads the next key and dispatches it:
<ul class="org-ul">
<li>If it's bound in the prefix keymap, execute it and loop.</li>
<li>If it's <code>C-h</code>, show help and continue.</li>
<li>If it's <code>C-u</code>, accumulate prefix arguments and continue.</li>
<li>If it's anything else, exit the loop and feed the key back to normal command dispatch.</li>
</ul></li>
</ol>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(global-set-key (kbd "C-c w h") (repeatable-lite-wrap windmove-left))
(global-set-key (kbd "C-c w l") (repeatable-lite-wrap windmove-right))
(global-set-key (kbd "C-c w j") (repeatable-lite-wrap windmove-down))
(global-set-key (kbd "C-c w k") (repeatable-lite-wrap windmove-up))
</pre>
</div>

<p>
Now <code>C-c w h h h</code> moves three windows left, six keystrokes saved.  And because which-key appears immediately after the first command, you can see all the other keys in the <code>C-c w</code> prefix without memorizing them.
</p>
</div>
<div id="outline-container-which-key-integration" class="outline-3">
<h3 id="which-key-integration"><span class="section-number-3">3.1.</span> Which-Key Integration</h3>
<div class="outline-text-3" id="text-which-key-integration">
<p>
repeatable-lite configures which-key for optimal display during the repeatable loop.  It saves your current which-key settings, enables persistent popup mode, and sets aggressive idle timers (0.1 seconds) so the guide appears instantly.  When you exit the loop, all settings are restored to their previous values.
</p>

<p>
This means which-key acts as a live reference while you're in the repeatable state.  You don't need to remember every key in the prefix, you just need to know one, and the popup shows you the rest.
</p>
</div>
</div>
</div>
<div id="outline-container-demo-repeatable-navigation" class="outline-2">
<h2 id="demo-repeatable-navigation"><span class="section-number-2">4.</span> Demo: Repeatable Window Navigation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-repeatable-navigation">
<p>
Here's the loop in action.  One prefix, then rapid-fire single keys with which-key showing what's available.
</p>

<video src="https://www.chiply.dev/videos/repeatable-lite-window-nav.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">5.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="installation">installation</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
repeatable-lite is on <a href="https://github.com/chiply/repeatable-lite">GitHub</a>.  It requires Emacs 30.1+ (which-key is built-in since 30.1).
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package repeatable-lite
  :ensure (:host github :repo "chiply/repeatable-lite")
  :config
  (repeatable-lite-mode 1))
</pre>
</div>

<p>
Then wrap any commands you want to make repeatable.  Here's a fuller example using <code>general.el</code>:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(general-define-key
 "C-c w h" (repeatable-lite-wrap windmove-left)
 "C-c w l" (repeatable-lite-wrap windmove-right)
 "C-c w j" (repeatable-lite-wrap windmove-down)
 "C-c w k" (repeatable-lite-wrap windmove-up)
 "C-c w =" (repeatable-lite-wrap enlarge-window-horizontally)
 "C-c w -" (repeatable-lite-wrap shrink-window-horizontally))
</pre>
</div>

<p>
Every command under <code>C-c w</code> becomes repeatable.  Type the prefix once, then just the final key.  When you're done, press anything outside the keymap and you're back to normal.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>spot: A Spotify Client Built on Consult, Embark, and Marginalia</title>
      <link>https://www.chiply.dev/post-spot</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-spot</guid>
      <pubDate>Sun, 01 Mar 2026 19:49:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>There are a few Spotify clients for Emacs. Most of them predate the modern completion ecosystem, so they build their own UI from scratch: custom buffers, custom keymaps, custom rendering. spot takes a...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="spotify">spotify</span>&#xa0;<span class="completion">completion</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org0b58f3a" class="figure">
<p><img src="https://www.chiply.dev/images/spot-banner.jpeg" alt="spot-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
There are a few Spotify clients for Emacs.  Most of them predate the modern completion ecosystem, so they build their own UI from scratch: custom buffers, custom keymaps, custom rendering.  <a href="https://github.com/chiply/spot">spot</a> takes a different approach.  It's built entirely on <a href="https://github.com/minad/consult">consult</a>, <a href="https://github.com/oantolin/embark">embark</a>, and <a href="https://github.com/minad/marginalia">marginalia</a>, which means searching, acting on results, and reading metadata all use the same interfaces you already know.
</p>
</div>
</div>
<div id="outline-container-why-another-spotify-client" class="outline-2">
<h2 id="why-another-spotify-client"><span class="section-number-2">2.</span> Why Another Spotify Client&#xa0;&#xa0;&#xa0;<span class="tag"><span class="spotify">spotify</span>&#xa0;<span class="motivation">motivation</span></span></h2>
<div class="outline-text-2" id="text-why-another-spotify-client">
<p>
<a href="https://github.com/krisajenern/smudge">smudge</a> (formerly spotify.el) is the most established Emacs Spotify client.  It works, but it renders results in a custom tabulated-list buffer.  You search, wait for a buffer to pop up, navigate it with its own keybindings, and act on items through its own action system.  This is a parallel universe of interaction that doesn't benefit from any of the completion infrastructure you've invested in.
</p>

<blockquote class="pull-quote pull-right">
<p>
spot doesn't have its own UI idioms to learn.  The UI <i>is</i> consult + embark + marginalia.
</p>
</blockquote>

<p>
If you use <code>vertico</code> or <code>selectrum</code> for completion, <code>embark</code> for contextual actions, and <code>marginalia</code> for annotations, you already have a sophisticated interaction model.  spot plugs into that model directly.  Search results appear in your minibuffer completion framework.  Metadata shows up as marginalia annotations.  Actions are embark actions.  There's nothing new to learn, just Spotify data flowing through tools you already use.
</p>
</div>
</div>
<div id="outline-container-architecture" class="outline-2">
<h2 id="architecture"><span class="section-number-2">3.</span> Architecture&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="design">design</span></span></h2>
<div class="outline-text-2" id="text-architecture">
<p>
spot is modular by design.  Each concern lives in its own file:
</p>

<ul class="org-ul">
<li><b>spot-auth.el</b>: OAuth2 authorization code flow with token refresh</li>
<li><b>spot-search.el</b>: Mutex-guarded, cached search with argument parsing</li>
<li><b>spot-consult.el</b>: Seven async consult sources (albums, artists, tracks, playlists, shows, episodes, audiobooks)</li>
<li><b>spot-marginalia.el</b>: Annotation functions per content type</li>
<li><b>spot-embark.el</b>: Action keymaps per content type</li>
<li><b>spot-mode-line.el</b>: Currently-playing display with smart update timing</li>
</ul>

<p>
The search layer deserves a note.  spot uses a mutex to serialize concurrent search requests and caches results by query string.  This matters because consult's async sources fire searches as you type, and Spotify's API has rate limits.  Without the mutex, rapid keystrokes could produce interleaved results or hit rate limits.  The cache prevents redundant API calls when you backspace and retype.
</p>
</div>
<div id="outline-container-oauth2-flow" class="outline-3">
<h3 id="oauth2-flow"><span class="section-number-3">3.1.</span> OAuth2 Flow</h3>
<div class="outline-text-3" id="text-oauth2-flow">
<p>
Spotify requires OAuth2 for most API operations.  spot handles the authorization code grant flow: it opens the Spotify authorization URL in your browser, you log in and approve, then paste the authorization code back into Emacs.  The access token is stored in memory for the session, and spot refreshes it automatically when it expires.  You set <code>spot-client-id</code> and <code>spot-client-secret</code> from a <a href="https://developer.spotify.com/dashboard">Spotify Developer</a> application.
</p>
</div>
</div>
</div>
<div id="outline-container-search-and-navigation" class="outline-2">
<h2 id="search-and-navigation"><span class="section-number-2">4.</span> Search and Navigation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="spotify">spotify</span>&#xa0;<span class="consult">consult</span></span></h2>
<div class="outline-text-2" id="text-search-and-navigation">
<p>
<code>M-x spot</code> opens a <code>consult--multi</code> search across all seven content types.  As you type, spot searches the Spotify API asynchronously and populates completion candidates.  Each content type has a narrowing key:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Content Type</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>a</code></td>
<td class="org-left">Albums</td>
</tr>

<tr>
<td class="org-left"><code>A</code></td>
<td class="org-left">Artists</td>
</tr>

<tr>
<td class="org-left"><code>p</code></td>
<td class="org-left">Playlists</td>
</tr>

<tr>
<td class="org-left"><code>t</code></td>
<td class="org-left">Tracks</td>
</tr>

<tr>
<td class="org-left"><code>s</code></td>
<td class="org-left">Shows</td>
</tr>

<tr>
<td class="org-left"><code>e</code></td>
<td class="org-left">Episodes</td>
</tr>

<tr>
<td class="org-left"><code>b</code></td>
<td class="org-left">Audiobooks</td>
</tr>
</tbody>
</table>

<p>
Narrowing works the way consult narrowing always works: press the narrowing key to filter to a single content type, or leave it open to see everything.
</p>

<p>
Queries also support Spotify's field filter syntax via <code>--</code> separator.  For example, <code>radiohead -- --type=album</code> restricts the API query to albums only, which is more efficient than fetching everything and narrowing client-side.
</p>
</div>
</div>
<div id="outline-container-demo-search-playback" class="outline-2">
<h2 id="demo-search-playback"><span class="section-number-2">5.</span> Demo: Search and Playback&#xa0;&#xa0;&#xa0;<span class="tag"><span class="spotify">spotify</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-search-playback">
<p>
Here's a search session showing multi-source results, marginalia annotations, and playback control.
</p>

<video src="https://www.chiply.dev/videos/spot-search-playback.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-marginalia-annotations" class="outline-2">
<h2 id="marginalia-annotations"><span class="section-number-2">6.</span> Marginalia Annotations&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marginalia">marginalia</span>&#xa0;<span class="metadata">metadata</span></span></h2>
<div class="outline-text-2" id="text-marginalia-annotations">
<p>
Each content type has its own marginalia annotator that pulls relevant metadata from the Spotify API response.  Here's what you see in the completion margin for each type:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Type</th>
<th scope="col" class="org-left">Annotations</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Albums</td>
<td class="org-left">Artist, release date, track count</td>
</tr>

<tr>
<td class="org-left">Artists</td>
<td class="org-left">Popularity score, follower count</td>
</tr>

<tr>
<td class="org-left">Tracks</td>
<td class="org-left">Track number, artist, duration, album, release date</td>
</tr>

<tr>
<td class="org-left">Playlists</td>
<td class="org-left">Track count</td>
</tr>

<tr>
<td class="org-left">Shows</td>
<td class="org-left">Publisher, media type, episode count, description</td>
</tr>

<tr>
<td class="org-left">Episodes</td>
<td class="org-left">Release date, duration, description</td>
</tr>

<tr>
<td class="org-left">Audiobooks</td>
<td class="org-left">Publisher, narrator, author, description</td>
</tr>
</tbody>
</table>

<p>
This is pure marginalia integration.  The annotators register themselves when <code>spot-mode</code> is enabled and unregister when it's disabled.  If you don't use marginalia, spot still works, you just don't see the extra metadata.
</p>
</div>
</div>
<div id="outline-container-embark-actions" class="outline-2">
<h2 id="embark-actions"><span class="section-number-2">7.</span> Embark Actions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="embark">embark</span>&#xa0;<span class="actions">actions</span></span></h2>
<div class="outline-text-2" id="text-embark-actions">
<p>
spot defines embark keymaps for every content type.  When you press your embark-act key on a completion candidate, you get actions appropriate to that type:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Key</th>
<th scope="col" class="org-left">Action</th>
<th scope="col" class="org-left">Available On</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>P</code></td>
<td class="org-left">Play</td>
<td class="org-left">All types</td>
</tr>

<tr>
<td class="org-left"><code>s</code></td>
<td class="org-left">Show raw JSON data</td>
<td class="org-left">All types</td>
</tr>

<tr>
<td class="org-left"><code>t</code></td>
<td class="org-left">List tracks</td>
<td class="org-left">Albums, artists, playlists</td>
</tr>

<tr>
<td class="org-left"><code>+</code></td>
<td class="org-left">Add to playlist</td>
<td class="org-left">Tracks only</td>
</tr>
</tbody>
</table>

<p>
Listing an album's tracks, for example, opens a new consult search narrowed to that album's tracks.  From there you can play individual tracks, inspect them, or add them to a playlist, all through the same embark action system.
</p>
</div>
</div>
<div id="outline-container-mode-line" class="outline-2">
<h2 id="mode-line"><span class="section-number-2">8.</span> Mode Line&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="modeline">modeline</span></span></h2>
<div class="outline-text-2" id="text-mode-line">
<p>
When <code>spot-mode</code> is enabled, the mode line shows the currently playing track, artist, and album.  The display updates on a 30-second timer, with smart scheduling that calculates the time remaining in the current track to minimize unnecessary API calls.  The currently-playing text is displayed in Spotify green (<code>#1db954</code>) by default.
</p>
</div>
</div>
<div id="outline-container-player-controls" class="outline-2">
<h2 id="player-controls"><span class="section-number-2">9.</span> Player Controls&#xa0;&#xa0;&#xa0;<span class="tag"><span class="spotify">spotify</span>&#xa0;<span class="playback">playback</span></span></h2>
<div class="outline-text-2" id="text-player-controls">
<p>
spot provides standard player commands, all operating through Spotify Connect:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(spot-player-play)      ; Resume playback
(spot-player-pause)     ; Pause playback
(spot-player-next)      ; Skip to next track
(spot-player-previous)  ; Skip to previous track
</pre>
</div>

<p>
There's also <code>spot-add-current-track-to-playlist</code>, which prompts you to select from your playlists (via consult, naturally) and adds whatever is currently playing.
</p>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">10.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="installation">installation</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
spot is on <a href="https://github.com/chiply/spot">GitHub</a>.  It requires Emacs 29.1+ and depends on consult, embark, marginalia, <code>ht</code>, and <code>dash</code>.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package spot
  :ensure (:host github :repo "chiply/spot")
  :config
  (setq spot-client-id "your-client-id"
        spot-client-secret "your-client-secret")
  (spot-mode 1))
</pre>
</div>

<p>
You'll need to create a Spotify Developer application at <a href="https://developer.spotify.com/dashboard">https://developer.spotify.com/dashboard</a> and set the redirect URI to <code>http://localhost:8080/callback</code>.  On first use, run <code>M-x spot-authorize</code> to complete the OAuth2 flow.  After that, <code>M-x spot</code> and start typing.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>touchtype: A Progressive Touch Typing Trainer for Emacs</title>
      <link>https://www.chiply.dev/post-touchtype</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-touchtype</guid>
      <pubDate>Sun, 01 Mar 2026 19:49:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>There are plenty of typing tutors on the web. keybr.com is one of the best, a progressive trainer that unlocks keys one at a time as your speed and accuracy improve. But it runs in a browser, and if y...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="typing">typing</span>&#xa0;<span class="education">education</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orga54fd49" class="figure">
<p><img src="https://www.chiply.dev/images/touchtype-banner.jpeg" alt="touchtype-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
There are plenty of typing tutors on the web.  <a href="https://www.keybr.com/">keybr.com</a> is one of the best, a progressive trainer that unlocks keys one at a time as your speed and accuracy improve.  But it runs in a browser, and if you spend most of your day in Emacs, context-switching to a browser tab for typing practice is enough friction to skip it entirely.  <a href="https://github.com/chiply/touchtype">touchtype</a> brings that progressive training model into Emacs, along with 12 additional training modes, detailed statistics, and support for alternative keyboard layouts.
</p>
</div>
</div>
<div id="outline-container-why-emacs" class="outline-2">
<h2 id="why-emacs"><span class="section-number-2">2.</span> Why Build a Typing Tutor in Emacs&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="motivation">motivation</span></span></h2>
<div class="outline-text-2" id="text-why-emacs">
<p>
The obvious question.  There are two practical answers.
</p>

<p>
First, <i>friction kills habits</i>.  If practicing typing requires opening a browser, navigating to a URL, and breaking your editing flow, you'll do it less.  An <code>M-x touchtype</code> that's always one command away makes it trivially easy to slot in a quick session between tasks.
</p>

<blockquote class="pull-quote pull-right">
<p>
An <code>M-x touchtype</code> that's always one command away makes it trivially easy to slot in a quick session between tasks.
</p>
</blockquote>

<p>
Second, <i>Emacs users type Emacs</i>.  A web-based trainer gives you prose and maybe some generic code snippets.  touchtype has a dedicated code mode with 42 real snippets across 8 languages (Python, Rust, Go, JavaScript, TypeScript, Emacs Lisp, Shell, SQL, C), and it respects your Emacs keybindings.  Backspace, word-delete (<code>M-DEL</code>, <code>C-backspace</code>), and even Evil mode bindings all work as expected.  You're practicing in the same environment you'll be typing in.
</p>
</div>
</div>
<div id="outline-container-progressive-training" class="outline-2">
<h2 id="progressive-training"><span class="section-number-2">3.</span> Progressive Training&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="algorithm">algorithm</span></span></h2>
<div class="outline-text-2" id="text-progressive-training">
<p>
Progressive mode is the flagship training mode, directly inspired by keybr.com.  It starts you on just two keys, <code>f</code> and <code>j</code>, the home-row index fingers.  You practice pseudo-words made from those two characters until your speed and accuracy cross a confidence threshold, then a third key unlocks.  Then a fourth.  You work through the entire keyboard one key at a time, in an order that spirals outward from the home row.
</p>
</div>
<div id="outline-container-confidence-scoring" class="outline-3">
<h3 id="confidence-scoring"><span class="section-number-3">3.1.</span> Confidence Scoring</h3>
<div class="outline-text-3" id="text-confidence-scoring">
<p>
Each key has a confidence score between 0.0 and 1.0, calculated from both speed and accuracy:
</p>

<pre class="example" id="org85c9484">
target_ms        = 60000 / (target_wpm * 5)
avg_ms           = total_ms / hits
speed_confidence = min(1.0, target_ms / avg_ms)
accuracy         = hits / (hits + misses)
confidence       = accuracy * speed_confidence
</pre>

<p>
Both dimensions must be high for full confidence.  A key you type quickly but inaccurately won't unlock the next one, and neither will a key you type accurately but slowly.  The unlock threshold is <code>touchtype-unlock-threshold</code> (default: 0.80).
</p>

<p>
A new key unlocks only when <i>every</i> currently unlocked key meets the threshold.  This prevents rushing: you can't advance by nailing the new key while your old keys decay.
</p>
</div>
</div>
<div id="outline-container-pseudo-word-generation" class="outline-3">
<h3 id="pseudo-word-generation"><span class="section-number-3">3.2.</span> Pseudo-Word Generation</h3>
<div class="outline-text-3" id="text-pseudo-word-generation">
<p>
touchtype generates practice words using an English bigram frequency table, a 26x26 matrix of character transition weights.  Generation works as a weighted random walk:
</p>

<ol class="org-ol">
<li>Pick a starting character, weighted by total outgoing bigram frequency.</li>
<li>Sample the next character from that character's bigram row, filtered to the currently allowed alphabet.</li>
<li>Terminate with probability that increases exponentially with word length (naturally producing 4-7 character words).</li>
<li>Inject the focused character (the most recently unlocked key) at a random position with 40% probability.</li>
</ol>

<p>
This produces words that <i>feel</i> like English, they follow natural letter patterns, even though they aren't real words.  The focused character injection ensures you get enough practice on new keys without making every word feel artificial.
</p>
</div>
</div>
<div id="outline-container-focus-rotation" class="outline-3">
<h3 id="focus-rotation"><span class="section-number-3">3.3.</span> Focus Character Rotation</h3>
<div class="outline-text-3" id="text-focus-rotation">
<p>
After each line, touchtype picks a "focus character" to emphasize:
</p>
<ul class="org-ul">
<li>70% of the time: the most recently unlocked key (deliberate practice on the new key).</li>
<li>30% of the time: a random key below the confidence threshold (remediation for weak keys).</li>
</ul>

<p>
This rotation ensures that newly unlocked keys get concentrated practice while weak older keys aren't forgotten.
</p>
</div>
</div>
<div id="outline-container-unlock-orders" class="outline-3">
<h3 id="unlock-orders"><span class="section-number-3">3.4.</span> Unlock Orders by Layout</h3>
<div class="outline-text-3" id="text-unlock-orders">
<p>
The unlock order depends on your keyboard layout, spiraling outward from the home row by English letter frequency:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Layout</th>
<th scope="col" class="org-left">Order</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">QWERTY</td>
<td class="org-left"><code>fjdkslahetniourGcmpbywvxqz</code></td>
</tr>

<tr>
<td class="org-left">Dvorak</td>
<td class="org-left"><code>uhetonasidrljgcmfpbkwvyxqz</code></td>
</tr>

<tr>
<td class="org-left">Colemak</td>
<td class="org-left"><code>neiostahrdlufywpgmcbkvxjqz</code></td>
</tr>

<tr>
<td class="org-left">Workman</td>
<td class="org-left"><code>nehtosaidrljgcmfpbkwvyxqz</code></td>
</tr>

<tr>
<td class="org-left">Custom</td>
<td class="org-left">User-provided via <code>touchtype-custom-unlock-order</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="outline-container-training-modes" class="outline-2">
<h2 id="training-modes"><span class="section-number-2">4.</span> Training Modes&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="modes">modes</span></span></h2>
<div class="outline-text-2" id="text-training-modes">
<p>
touchtype has 13 training modes.  Progressive is the headline, but the others cover different practice needs.
</p>
</div>
<div id="outline-container-word-based-modes" class="outline-3">
<h3 id="word-based-modes"><span class="section-number-3">4.1.</span> Word-Based Modes</h3>
<div class="outline-text-3" id="text-word-based-modes">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Mode</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Progressive</b></td>
<td class="org-left">Keys unlock one-at-a-time; pseudo-words from unlocked alphabet</td>
</tr>

<tr>
<td class="org-left"><b>Full Words</b></td>
<td class="org-left">4,258 real English words, filtered to unlocked keys</td>
</tr>

<tr>
<td class="org-left"><b>Common Words</b></td>
<td class="org-left">Top N most frequent words (default: 100)</td>
</tr>

<tr>
<td class="org-left"><b>Letters</b></td>
<td class="org-left">All 26 lowercase letters, pseudo-words from full alphabet</td>
</tr>

<tr>
<td class="org-left"><b>Letters + Numbers</b></td>
<td class="org-left">Full alphabet + digits 0-9</td>
</tr>

<tr>
<td class="org-left"><b>Letters + Numbers + Symbols</b></td>
<td class="org-left">+ punctuation: <code>.,;'!?-</code></td>
</tr>

<tr>
<td class="org-left"><b>Custom</b></td>
<td class="org-left">User-provided text (prompt, region, or buffer)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-ngram-drills" class="outline-3">
<h3 id="ngram-drills"><span class="section-number-3">4.2.</span> N-gram Drills</h3>
<div class="outline-text-3" id="text-ngram-drills">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Mode</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Bigram Drill</b></td>
<td class="org-left">100 common English bigrams, repeated on a line</td>
</tr>

<tr>
<td class="org-left"><b>Trigram Drill</b></td>
<td class="org-left">100 common English trigrams</td>
</tr>

<tr>
<td class="org-left"><b>Tetragram Drill</b></td>
<td class="org-left">100 common English tetragrams</td>
</tr>

<tr>
<td class="org-left"><b>N-gram Mixed</b></td>
<td class="org-left">Random mix of bi/tri/tetragrams per line</td>
</tr>
</tbody>
</table>

<p>
Each drill isolates a specific motor pattern.  Bigram drills target two-finger transitions (<code>th</code>, <code>he</code>, <code>in</code>).  Tetragram drills build four-character muscle memory for common word fragments (<code>tion</code>, <code>ight</code>, <code>that</code>).
</p>
</div>
</div>
<div id="outline-container-special-modes" class="outline-3">
<h3 id="special-modes"><span class="section-number-3">4.3.</span> Special Modes</h3>
<div class="outline-text-3" id="text-special-modes">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Mode</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Narrative</b></td>
<td class="org-left">Random passages from 20 Project Gutenberg classics (cached locally)</td>
</tr>

<tr>
<td class="org-left"><b>Code</b></td>
<td class="org-left">42 real code snippets across 8 languages</td>
</tr>
</tbody>
</table>

<p>
Narrative mode downloads and caches books from Project Gutenberg, then serves random 400-character passages that land on word boundaries.  The book list includes <i>Pride and Prejudice</i>, <i>Alice's Adventures in Wonderland</i>, <i>Sherlock Holmes</i>, <i>Frankenstein</i>, <i>Moby Dick</i>, and 15 others.  You can add any valid Gutenberg ID to <code>touchtype-narrative-book-list</code>.
</p>

<p>
Code mode draws from a curated set of snippets: Python (8), Rust (7), Go (5), JavaScript/TypeScript (6), Emacs Lisp (4), Shell (4), SQL (4), and C (5).  It includes the full printable ASCII range (chars 32-126), so you practice brackets, operators, and indentation, not just alphanumerics.
</p>
</div>
</div>
</div>
<div id="outline-container-session-experience" class="outline-2">
<h2 id="session-experience"><span class="section-number-2">5.</span> The Session Experience&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="interface">interface</span></span></h2>
<div class="outline-text-2" id="text-session-experience">
<p>
A touchtype session runs in a dedicated buffer with a clean, distraction-free layout:
</p>

<pre class="example" id="orgd4430e3">
                        (blank padding, ~1/3 screen height)

already typed line      (completed: purple=correct, red=wrong)
already typed line

current target text_    (active: gray=untyped, purple=correct, red=wrong)

upcoming preview text   (preview lines, gray, read-only)
upcoming preview text

Net: 42  Gross: 45  Acc: 96%  Consistency: 85%
Time: 1:23  Words: 15/30  Corrections: 3  Mode: progressive  Keys: fjdksl
</pre>

<p>
Completed lines scroll up above the active line.  Preview lines (configurable via <code>touchtype-preview-lines</code>, default: 2) let you read ahead.  The status line at the bottom updates live with net/gross WPM, accuracy, consistency, elapsed time, word count, correction count, current mode, and (in progressive mode) the unlocked key set.
</p>
</div>
<div id="outline-container-pace-caret" class="outline-3">
<h3 id="pace-caret"><span class="section-number-3">5.1.</span> Pace Caret</h3>
<div class="outline-text-3" id="text-pace-caret">
<p>
Enable <code>touchtype-pace-caret</code> to show a ghost cursor that moves at your target WPM.  It's a visual indicator of whether you're ahead of or behind your target pace, without the pressure of a countdown timer.
</p>
</div>
</div>
<div id="outline-container-session-types" class="outline-3">
<h3 id="session-types"><span class="section-number-3">5.2.</span> Session Types</h3>
<div class="outline-text-3" id="text-session-types">
<p>
Sessions can be word-count based or timed:
</p>

<ul class="org-ul">
<li><b>Word-count</b>: Default 30 words.  Presets: short (15), medium (30), long (60), marathon (120).  Set with <code>M-x touchtype-set-session-length</code> or a <code>C-u N</code> prefix argument.</li>
<li><b>Timed</b>: Default 60 seconds via <code>M-x touchtype-timed</code>.  The timer starts on your first keypress, not when the buffer opens.  Idle gaps longer than <code>touchtype-idle-threshold</code> (default: 10 seconds) are excluded from WPM calculations.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-demo-progressive" class="outline-2">
<h2 id="demo-progressive"><span class="section-number-2">6.</span> Demo: Progressive Session&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-progressive">
<p>
A progressive session from start to key unlock.
</p>

<video src="https://www.chiply.dev/videos/touchtype-progressive.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-demo-narrative" class="outline-2">
<h2 id="demo-narrative"><span class="section-number-2">7.</span> Demo: Narrative Mode&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-narrative">
<p>
Typing a passage from a Project Gutenberg classic.
</p>

<video src="https://www.chiply.dev/videos/touchtype-narrative.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-demo-code" class="outline-2">
<h2 id="demo-code"><span class="section-number-2">8.</span> Demo: Code Mode&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-code">
<p>
Practicing a Rust code snippet with brackets, operators, and indentation.
</p>

<video src="https://www.chiply.dev/videos/touchtype-code.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-error-modes" class="outline-2">
<h2 id="error-modes"><span class="section-number-2">9.</span> Error Modes&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="configuration">configuration</span></span></h2>
<div class="outline-text-2" id="text-error-modes">
<p>
touchtype offers three error behaviors, controlled by <code>touchtype-error-mode</code>:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Mode</th>
<th scope="col" class="org-left">Behavior</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Normal</b></td>
<td class="org-left">Errors highlighted in red but cursor advances.  Backspace to correct or continue.</td>
</tr>

<tr>
<td class="org-left"><b>Stop on Letter</b></td>
<td class="org-left">Cursor won't advance until the correct character is typed.  Forces immediate correction.</td>
</tr>

<tr>
<td class="org-left"><b>Stop on Word</b></td>
<td class="org-left">Cursor advances within a word, but you can't cross the word boundary (space) until all errors in the current word are corrected via backspace.</td>
</tr>
</tbody>
</table>

<p>
Normal mode is the default and matches how most typing tests work.  Stop-on-letter is the most strict, useful for building accuracy at the expense of speed.  Stop-on-word is a middle ground that lets you attempt the whole word before requiring corrections.
</p>
</div>
</div>
<div id="outline-container-session-end-summary" class="outline-2">
<h2 id="session-end-summary"><span class="section-number-2">10.</span> Session End Summary&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="statistics">statistics</span></span></h2>
<div class="outline-text-2" id="text-session-end-summary">
<p>
When a session ends, touchtype displays a comprehensive summary with expandable sections:
</p>
</div>
<div id="outline-container-speed-accuracy" class="outline-3">
<h3 id="speed-accuracy"><span class="section-number-3">10.1.</span> Speed and Accuracy</h3>
<div class="outline-text-3" id="text-speed-accuracy">
<ul class="org-ul">
<li><b>Net WPM</b>: <code>(total_chars / 5 - uncorrected_errors) / minutes</code></li>
<li><b>Gross WPM</b>: <code>(total_chars / 5) / minutes</code></li>
<li><b>Net/Gross CPM</b>: Characters per minute (<code>wpm * 5</code>)</li>
<li><b>Accuracy</b>: Percentage, ignoring corrected mistakes</li>
<li><b>Raw Accuracy</b>: True first-attempt accuracy, including corrections in the denominator</li>
</ul>
</div>
</div>
<div id="outline-container-session-metrics" class="outline-3">
<h3 id="session-metrics"><span class="section-number-3">10.2.</span> Session Metrics</h3>
<div class="outline-text-3" id="text-session-metrics">
<ul class="org-ul">
<li><b>Time</b>: Elapsed excluding idle periods (<code>mm:ss</code>)</li>
<li><b>Characters</b>: Total keypresses including errors</li>
<li><b>Corrections</b>: Backspace count</li>
<li><b>Uncorrected Errors</b>: Mistakes left unfixed</li>
<li><b>Consistency</b>: Coefficient of variation of per-line WPM, subtracted from 100%</li>
<li><b>WPM Sparkline</b>: A Unicode bar chart (<code>▁▂▃▄▅▆▇█</code>) showing per-line WPM with min-max range</li>
<li><b>Streak</b>: Current daily practice streak</li>
<li><b>Total Time</b>: Cumulative across all sessions</li>
</ul>
</div>
</div>
<div id="outline-container-expandable-sections" class="outline-3">
<h3 id="expandable-sections"><span class="section-number-3">10.3.</span> Expandable Weak Sections</h3>
<div class="outline-text-3" id="text-expandable-sections">
<p>
The summary includes collapsible sections for your weakest letters, bigrams, trigrams, and tetragrams.  Press <code>RET</code> on any section header to expand it.  Weakest letters show the top 10 by confidence (expandable to all 26).  N-gram sections show the top 5, expandable to 50.  This tells you exactly what to focus on in your next session.
</p>

<p>
If you set a new personal best for that mode's net WPM, a banner announces it.
</p>
</div>
</div>
</div>
<div id="outline-container-demo-stats" class="outline-2">
<h2 id="demo-stats"><span class="section-number-2">11.</span> Demo: Statistics View&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-demo-stats">
<p>
The full statistics view with trends and per-letter confidence.
</p>

<video src="https://www.chiply.dev/videos/touchtype-stats.mp4" data-interactive-video>
</video>
</div>
</div>
<div id="outline-container-statistics-persistence" class="outline-2">
<h2 id="statistics-persistence"><span class="section-number-2">12.</span> Statistics and Persistence&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="statistics">statistics</span></span></h2>
<div class="outline-text-2" id="text-statistics-persistence">
<p>
<code>M-x touchtype-stats-view</code> shows a comprehensive dashboard:
</p>

<ul class="org-ul">
<li><b>Overall summary</b>: Total sessions, total words, average WPM, average accuracy, streak, cumulative practice time.</li>
<li><b>Per-letter confidence chart</b>: All 26 letters sorted weakest-to-strongest with a visual bar chart (20 characters wide, 0.0-1.0 scale).</li>
<li><b>Weakest bigrams</b>: Top 10 (minimum 5 hits) with confidence bars.</li>
<li><b>Weakest trigrams and tetragrams</b>: Top 10 each.</li>
<li><b>WPM trend</b>: Last N sessions with direction indicator (<code>^</code> improving, <code>v</code> declining, <code>-</code> stable).</li>
<li><b>Accuracy trend</b>: Same format.</li>
<li><b>Session history</b>: Tabular format with date, WPM, accuracy, mode, word count.</li>
<li><b>Personal bests</b>: Best WPM and accuracy per mode.</li>
</ul>

<p>
All statistics persist to <code>~/.emacs.d/touchtype-stats.el</code> as s-expressions.  The file is auto-created on first use.
</p>
</div>
<div id="outline-container-export" class="outline-3">
<h3 id="export"><span class="section-number-3">12.1.</span> Export</h3>
<div class="outline-text-3" id="text-export">
<p>
<code>M-x touchtype-export</code> writes your data to JSON or CSV.  The JSON export includes sessions, per-letter stats with confidence scores, streak, and total practice time.  The CSV export is a simple table of sessions (date, WPM, accuracy, mode, words).
</p>
</div>
</div>
</div>
<div id="outline-container-customization-reference" class="outline-2">
<h2 id="customization-reference"><span class="section-number-2">13.</span> Customization Reference&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="configuration">configuration</span></span></h2>
<div class="outline-text-2" id="text-customization-reference">
</div>
<div id="outline-container-training-params" class="outline-3">
<h3 id="training-params"><span class="section-number-3">13.1.</span> Training Parameters</h3>
<div class="outline-text-3" id="text-training-params">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-right" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Variable</th>
<th scope="col" class="org-right">Default</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>touchtype-target-wpm</code></td>
<td class="org-right">40</td>
<td class="org-left">Target WPM for confidence scoring</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-unlock-threshold</code></td>
<td class="org-right">0.80</td>
<td class="org-left">Confidence threshold for key unlock</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-session-length</code></td>
<td class="org-right">30</td>
<td class="org-left">Words per session</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-session-duration</code></td>
<td class="org-right">60</td>
<td class="org-left">Seconds for timed sessions</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-error-mode</code></td>
<td class="org-right"><code>'normal</code></td>
<td class="org-left">Error handling behavior</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-idle-threshold</code></td>
<td class="org-right">10</td>
<td class="org-left">Seconds before gap excluded from WPM</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-word-length-min</code></td>
<td class="org-right">4</td>
<td class="org-left">Minimum pseudo-word length</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-word-length-max</code></td>
<td class="org-right">8</td>
<td class="org-left">Maximum pseudo-word length</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-common-words-count</code></td>
<td class="org-right">100</td>
<td class="org-left">Top N words for common-words mode</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-display-params" class="outline-3">
<h3 id="display-params"><span class="section-number-3">13.2.</span> Display</h3>
<div class="outline-text-3" id="text-display-params">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Variable</th>
<th scope="col" class="org-left">Default</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>touchtype-preview-lines</code></td>
<td class="org-left">2</td>
<td class="org-left">Preview lines shown below active</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-pace-caret</code></td>
<td class="org-left"><code>nil</code></td>
<td class="org-left">Show ghost cursor at target WPM</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-layout-params" class="outline-3">
<h3 id="layout-params"><span class="section-number-3">13.3.</span> Layout and Persistence</h3>
<div class="outline-text-3" id="text-layout-params">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Variable</th>
<th scope="col" class="org-left">Default</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>touchtype-keyboard-layout</code></td>
<td class="org-left"><code>'qwerty</code></td>
<td class="org-left">Layout (qwerty/dvorak/colemak/workman/custom)</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-custom-unlock-order</code></td>
<td class="org-left"><code>nil</code></td>
<td class="org-left">Custom unlock order string</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-stats-file</code></td>
<td class="org-left"><code>~/.emacs.d/touchtype-stats.el</code></td>
<td class="org-left">Statistics file path</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-stats-history-length</code></td>
<td class="org-left">20</td>
<td class="org-left">Sessions retained for trends</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-narrative-params" class="outline-3">
<h3 id="narrative-params"><span class="section-number-3">13.4.</span> Narrative Mode</h3>
<div class="outline-text-3" id="text-narrative-params">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Variable</th>
<th scope="col" class="org-left">Default</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>touchtype-narrative-cache-dir</code></td>
<td class="org-left"><code>~/.emacs.d/touchtype/</code></td>
<td class="org-left">Cache directory for books</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-narrative-passage-chars</code></td>
<td class="org-left">400</td>
<td class="org-left">Characters per passage</td>
</tr>

<tr>
<td class="org-left"><code>touchtype-narrative-book-list</code></td>
<td class="org-left">20 Gutenberg IDs</td>
<td class="org-left">Books to sample from</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">14.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="installation">installation</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
touchtype is on <a href="https://github.com/chiply/touchtype">GitHub</a>.  It requires Emacs 29.1+ with no external dependencies.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(use-package touchtype
  :ensure (:host github :repo "chiply/touchtype"))
</pre>
</div>

<p>
Start with <code>M-x touchtype-progressive</code> for the flagship progressive mode, or <code>M-x touchtype</code> to pick a mode interactively.  Prefix any mode command with <code>C-u N</code> to set the session length (e.g., <code>C-u 60 M-x touchtype-progressive</code> for a 60-word session).  Check your progress with <code>M-x touchtype-stats-view</code> and export with <code>M-x touchtype-export</code>.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Five Years of COVID-19 Data: Variants, Hospitalizations, Vaccines, and What Wastewater Tells Us Now</title>
      <link>https://www.chiply.dev/post-covid-tracker</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-covid-tracker</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Few events in modern history have generated as much real-time public data as COVID-19. This post draws on four federal datasets &amp;#x2013; variant proportions, hospital admissions, vaccination progress,...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="publicHealth">publicHealth</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="covid">covid</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org954d13e" class="figure">
<p><img src="https://www.chiply.dev/images/covid-tracker-banner.jpeg" alt="covid-tracker-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Few events in modern history have generated as much real-time public data as COVID-19.  This post draws on four federal datasets &#x2013; variant proportions, hospital admissions, vaccination progress, and wastewater surveillance &#x2013; to tell the story of five years of a virus reshaping American life, from emergence through endemicity.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
The COVID-19 pandemic has been one of the most intensively documented public health events in history. CDC's public data APIs capture the full arc: five waves of distinct variant lineages, a hospitalization record that dwarfed any recent respiratory disease season, the fastest vaccine rollout in American history (with significant state-level variation), and a new permanent surveillance infrastructure built on wastewater. As of early 2026, wastewater monitoring shows SARS-CoV-2 activity is present but well below pandemic peaks. The virus didn't disappear — it became endemic, and public health built tools to watch it.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction: COVID-19 as a Data Story&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Few events in modern history have generated as much real-time public data as COVID-19. At the pandemic's height, public health agencies at every level — federal, state, and local — were publishing case counts, test positivity rates, hospitalization figures, vaccine uptake, variant sequences, and wastewater viral loads, often daily. Some of that infrastructure has since been wound down. But what remains in the CDC's public APIs tells a coherent story about five years of a virus reshaping American life.
</p>

<p>
This post draws on four distinct federal datasets that remain active and publicly accessible:
</p>

<ul class="org-ul">
<li><b>Variant Proportions (CDC)</b>: Genomic surveillance tracking which SARS-CoV-2 lineage dominates circulating sequences each week, starting in January 2021 and updated continuously through today.</li>
<li><b>Hospital Admissions (CDC)</b>: Weekly adult COVID-19 admissions reported by hospitals to HHS/CDC, from August 2020 through October 2024 when mandatory reporting ended.</li>
<li><b>Vaccination Progress (CDC)</b>: Dose administration and completion rates by state and nationally, from the first shots in December 2020 through the formal end of the federal vaccination data program in May 2023.</li>
<li><b>Wastewater Surveillance (CDC NWSS)</b>: SARS-CoV-2 viral signal detected at wastewater treatment plants nationwide, from mid-2020 to the present — the only dataset that continues tracking COVID-19 activity in real time.</li>
</ul>

<p>
Together these four data streams let us trace the pandemic from emergence through endemicity.
</p>
</div>
</div>
<div id="outline-container-variants" class="outline-2">
<h2 id="variants"><span class="section-number-2">4.</span> The Variant Succession&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="variants">variants</span></span></h2>
<div class="outline-text-2" id="text-variants">
<p>
One of the defining features of SARS-CoV-2 has been its capacity for rapid evolutionary change. CDC's genomic surveillance program tracks the proportion of sequenced specimens attributed to each circulating lineage each week. The result is a clear visual record of variant succession — each new dominant strain displacing the last, often within weeks.
</p>

<div id="covid_variants" class="plotly-plot"></div>

<p>
Several structural patterns emerge from the variant timeline:
</p>

<ul class="org-ul">
<li><b>Alpha and Delta were sequential.</b>  Alpha (B.1.1.7, first identified in the UK) became dominant in the US through spring 2021 before Delta (B.1.617.2, first identified in India) displaced it almost entirely by July 2021. Delta was more transmissible than any prior variant; it drove the summer and fall 2021 wave and remained dominant until a single event changed everything.</li>

<li><b>Omicron was a discontinuity.</b> The emergence of B.1.1.529 (Omicron) in late November 2021 wasn't just a variant transition — it was a reset. Omicron displaced Delta in a matter of weeks, achieving dominance faster than any prior variant. But unlike Delta, which produced severe disease in unvaccinated populations at rates comparable to the original strain, Omicron showed substantially reduced severity per infection (partly due to its different cell tropism, partly due to widespread prior immunity). The trade-off: it was dramatically more contagious, and it produced the largest single hospitalization wave of the pandemic.</li>

<li><b>Omicron fragmented into subvariants.</b>  After the initial BA.1/BA.1.1 wave, Omicron didn't recede — it diversified. BA.2, BA.2.12.1, BA.4, and BA.5 emerged in rapid succession through 2022, each outcompeting its predecessor. By late 2022, BQ.1 and BQ.1.1 took over; by early 2023, XBB.1.5 dominated; through 2023-2024, EG.5, HV.1, JN.1, KP.2, KP.3, and XEC followed. The post-Omicron era has been characterized by continuous churn among subvariants, none achieving quite the dramatic emergence of the original Omicron wave.</li>

<li><b>The current era: XFG.</b>  As of February 2026, the XFG lineage (a recombinant descendent of Omicron) accounts for the largest share of sequenced specimens, alongside XFG.2.5.1 and XFG.1.1. The pattern has become stable: a new Omicron-descendent subvariant achieves dominance every few months, drives a modest uptick in activity, and gives way to the next. This is how seasonal respiratory viruses behave.</li>
</ul>

<p>
The variant chart illustrates something important about pathogen evolution under population immunity: selective pressure favors immune evasion over severity. Each successive Omicron subvariant became better at evading prior immunity but didn't revert to Delta-level severity. The virus found a stable evolutionary niche — high transmissibility, periodic immune escape, manageable (for most) disease burden.
</p>
</div>
</div>
<div id="outline-container-hospitalizations" class="outline-2">
<h2 id="hospitalizations"><span class="section-number-2">5.</span> The Hospitalization Record&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="hospitals">hospitals</span></span></h2>
<div class="outline-text-2" id="text-hospitalizations">
<p>
Variant proportions tell us about the virus. Hospital admissions tell us about the impact on people.
</p>

<div id="covid_hospitalizations" class="plotly-plot"></div>

<p>
The hospitalization data runs from August 2020 through October 2024, when the federal government ended mandatory hospital reporting requirements. Several features of this record are striking:
</p>

<ul class="org-ul">
<li><b>The Omicron BA.1 wave was the largest hospitalization event of the pandemic.</b> In mid-January 2022, weekly adult COVID-19 admissions in the US peaked at levels roughly double the prior record (the winter 2020-21 wave). This is counterintuitive given that Omicron caused less severe disease per infection — but the sheer number of infections was so large that even a lower hospitalization rate produced record admissions. The US hospital system was significantly strained, with staff quarantines and patient surges occurring simultaneously.</li>

<li><b>The winter 2020-21 wave was the first severe wave.</b> Before vaccines, before any widespread immunity, the original strain drove a sustained winter surge that overwhelmed hospitals across the Sun Belt, Midwest, and Northeast in sequence. This wave established the baseline for what COVID-19 could do to a fully susceptible population.</li>

<li><b>Delta drove a sharp, sustained summer surge in 2021.</b> Unlike prior waves, which tracked seasonality (rising in winter, falling in summer), Delta spread aggressively through an unvaccinated population during the summer of 2021. Southern states with lower vaccination rates were hit first and hardest. The Delta wave killed approximately 130,000 Americans between June and November 2021 — a toll concentrated heavily among the unvaccinated.</li>

<li><b>Post-Omicron waves have been progressively smaller.</b>  Each subsequent wave (BA.4/5 in summer 2022, XBB.1.5 in winter 2022-23, JN.1 in winter 2023-24) has produced lower peak hospitalizations than its predecessor. This reflects accumulated immunity from prior infection and vaccination, improved treatments (antivirals, updated vaccines), and possible reduced inherent severity of circulating strains. The JN.1 wave in late 2023 / early 2024 — the last full wave captured in this dataset — produced roughly one-tenth the hospitalizations of the Omicron BA.1 peak.</li>

<li><b>Mandatory reporting ended October 2024.</b>  The federal requirement for hospitals to report COVID-19 admissions to HHS expired, creating a gap in national surveillance. Wastewater data (covered below) now serves as the primary ongoing signal for COVID-19 activity.</li>
</ul>
</div>
</div>
<div id="outline-container-vaccination" class="outline-2">
<h2 id="vaccination"><span class="section-number-2">6.</span> The Vaccination Campaign&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="vaccines">vaccines</span></span></h2>
<div class="outline-text-2" id="text-vaccination">
<p>
The COVID-19 vaccine rollout was, by any historical measure, extraordinary. Within one year of a novel pathogen being identified, multiple safe and effective vaccines were authorized, manufactured at scale, and administered to hundreds of millions of Americans.
</p>

<div id="covid_vax_timeline" class="plotly-plot"></div>

<p>
The national vaccination timeline shows three distinct phases:
</p>

<ol class="org-ol">
<li><b>The initial sprint (December 2020 – June 2021).</b> Vaccines were authorized for emergency use and administered to priority groups (healthcare workers, long-term care residents, the elderly) before broadening to all adults in April 2021. The pace was remarkable — at peak velocity in April 2021, the US was administering over 3 million doses per day. By June 2021, roughly 45% of the population had received at least one dose.</li>

<li><b>The plateau (July 2021 – on).</b> First-dose uptake plateaued sharply in mid-2021. The remaining unvaccinated population proved more resistant to vaccination — a combination of hesitancy, access barriers, and political polarization. Despite significant public health campaigns, employer mandates, and ongoing Delta-related mortality that disproportionately affected the unvaccinated, the national fully-vaccinated rate plateaued near 70% for primary series completion.</li>

<li><b>Boosters and bivalent doses.</b> The booster program launched in fall 2021, primarily targeting those 65+ and immunocompromised. An updated bivalent booster targeting Omicron BA.4/5 was authorized in September 2022, but uptake was substantially lower than the primary series — reflecting both waning urgency (Omicron was less severe) and vaccination fatigue. The CDC stopped tracking vaccine data after May 2023 as the federal vaccination program wound down.</li>
</ol>
</div>
<div id="outline-container-state-vax" class="outline-3">
<h3 id="state-vax"><span class="section-number-3">6.1.</span> State-Level Variation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="maps">maps</span></span></h3>
<div class="outline-text-3" id="text-state-vax">
<p>
The national averages obscure enormous variation between states.
</p>

<div id="covid_vax_states" class="plotly-plot"></div>

<p>
The spread in peak primary-series vaccination rates across states is striking — a gap of more than 30 percentage points separated the most and least vaccinated states. Several patterns are consistent with other health and political geography data:
</p>

<ul class="org-ul">
<li><b>New England led.</b> Vermont, Massachusetts, Connecticut, and Maine achieved the highest vaccination rates, with more than 80% of their populations completing a primary series. These states combined high trust in public institutions, dense urban healthcare access, and early employer and institutional mandates.</li>

<li><b>Mountain West and Deep South lagged.</b> Wyoming, Idaho, Mississippi, and Alabama had the lowest vaccination rates, often below 55%. These states also experienced higher per-capita COVID-19 mortality, reflecting the direct consequence of the protection gap.</li>

<li><b>The metro-rural divide within states was sharper than the state-level data suggests.</b> A state like Georgia appears near the middle of the distribution, but its rural counties show vaccination rates in the 30-40% range while Atlanta metro approaches 80%. County-level data reveals geography that state aggregates obscure.</li>

<li><b>The gap had real mortality consequences.</b> Studies published throughout 2021-2023 consistently showed that unvaccinated adults were 5-10x more likely to be hospitalized with COVID-19 and 10-15x more likely to die during Delta. The state variation in vaccination rates translated directly into preventable deaths.</li>
</ul>

<p>
The vaccination data program formally ended in May 2023. The federal COVID-19 vaccine tracking infrastructure — which produced daily granular data at county level throughout the pandemic — no longer exists in its original form.
</p>
</div>
</div>
</div>
<div id="outline-container-wastewater" class="outline-2">
<h2 id="wastewater"><span class="section-number-2">7.</span> Wastewater: The Ongoing Signal&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="wastewater">wastewater</span></span></h2>
<div class="outline-text-2" id="text-wastewater">
<p>
With case counts discontinued in 2023 and hospital reporting ended in 2024, the primary ongoing source of real-time COVID-19 tracking is wastewater surveillance.
</p>

<p>
SARS-CoV-2 is shed in human feces regardless of whether someone has symptoms or has sought testing. Wastewater treatment plants — which aggregate sewage from thousands to millions of people — serve as passive surveillance systems. The CDC's National Wastewater Surveillance System (NWSS) collects viral load measurements from hundreds of sites nationwide and converts them to percentile scores: 0 means the lowest viral signal ever recorded at that site, 100 means the highest.
</p>

<div id="covid_wastewater" class="plotly-plot"></div>

<p>
The wastewater trend shows several features not visible in hospitalization data (which ended in late 2024):
</p>

<ul class="org-ul">
<li><b>The JN.1 wave (winter 2023-24) was clearly visible.</b> Wastewater percentiles spiked nationally in December 2023 and January 2024, consistent with the last major wave captured in the hospitalization data. The peak was substantial but well below the Omicron BA.1 winter (when some sites recorded their all-time highest signals).</li>

<li><b>A summer 2024 wave appeared.</b>  Consistent with the KP.3/XEC variant transitions, wastewater showed an uptick in summer 2024 — a pattern that was barely visible in the (by then curtailed) hospital data but clearly detectable in sewage.</li>

<li><b>Activity has remained at low but non-zero levels.</b>  As of early 2026, the national median wastewater percentile sits well below the 50th percentile — meaning most sites are detecting viral levels below their historical midpoint. COVID-19 is present but not producing the surges of the acute pandemic phase.</li>

<li><b>Wastewater leads clinical data by 4-7 days.</b>  One of wastewater surveillance's key advantages is its lead time over clinical testing or hospitalization data. When viral shedding rises in sewage, emergency department visits and hospitalizations typically follow within a week. This makes it a useful early-warning system for resource planning.</li>
</ul>
</div>
<div id="outline-container-wastewater-map" class="outline-3">
<h3 id="wastewater-map"><span class="section-number-3">7.1.</span> COVID Activity by State&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="maps">maps</span></span></h3>
<div class="outline-text-3" id="text-wastewater-map">
<p>
The state-level map shows the most recent four weeks of wastewater activity, expressed as each state's median site percentile.
</p>

<div id="covid_wastewater_map" class="plotly-plot"></div>

<p>
Geographic variation in wastewater activity reflects both true differences in viral transmission and differences in site coverage. States with very few reporting wastewater treatment plants (often rural states like Wyoming, Montana, or North Dakota) have less reliable state-level estimates, since a single large urban WWTP can dominate the state median. Densely monitored states like California, Illinois, and New York have hundreds of reporting sites, making their state-level estimates much more stable.
</p>

<p>
The Sunbelt states — Florida, Texas, Arizona — have historically shown elevated summer wastewater signals, likely reflecting indoor congregating driven by heat. Northeast and Midwest states often show higher winter signals. The current map captures a snapshot; the national trend chart above shows how this changes over time.
</p>
</div>
</div>
</div>
<div id="outline-container-now" class="outline-2">
<h2 id="now"><span class="section-number-2">8.</span> What COVID Data Looks Like Now&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span>&#xa0;<span class="endemic">endemic</span></span></h2>
<div class="outline-text-2" id="text-now">
<p>
Five years after the first US COVID-19 deaths, the data tells a story of transition from pandemic to endemic:
</p>

<p>
<b>The surveillance landscape has changed.</b> The dense, multi-layer data infrastructure of 2021-2022 — daily case counts by county, hospital occupancy by state, seven-day test positivity, daily vaccination records — has largely been dismantled. What remains is leaner but more sustainable: continuous wastewater monitoring, periodic genomic sequencing, influenza-season syndromic surveillance integrated with COVID-19.
</p>

<p>
<b>Immunity has fundamentally reshaped the disease burden.</b> The CDC estimated that by early 2022, roughly 75% of Americans had been infected at least once. Combined with vaccination, population-level immunity against severe disease is high. This doesn't prevent infection — Omicron subvariants evade neutralizing antibodies effectively — but it dramatically reduces hospitalization and death rates per infection compared to the pre-immune era.
</p>

<p>
<b>The seasonal pattern has asserted itself.</b> COVID-19 now behaves more like influenza or RSV than like a novel pandemic pathogen: a predictable winter surge (driven partly by seasonal behavior, partly by waning immunity), a smaller summer bump, and relatively quiet spring and fall periods. This regularity allows healthcare systems to plan rather than react.
</p>

<p>
<b>The population most at risk has narrowed.</b> During Delta, an unvaccinated 40-year-old with no comorbidities faced meaningful risk of severe disease. Today, for the same person with prior infection and vaccination history, COVID-19 carries risk more comparable to a bad flu year. The remaining high-risk populations — adults over 75, immunocompromised individuals, those with multiple chronic conditions — still benefit substantially from updated vaccines and antiviral treatment, but the population-wide burden has shifted.
</p>

<p>
<b>Long COVID remains an open chapter.</b> The datasets analyzed here do not capture long COVID prevalence, which CDC surveys have estimated at anywhere from 6% to 11% of ever-infected adults experiencing persistent symptoms. This represents tens of millions of Americans and remains one of the least well-quantified ongoing consequences of the pandemic.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data from publicly accessible CDC APIs, no authentication required.
</p>

<ul class="org-ul">
<li><b>Variant proportions:</b> <a href="https://data.cdc.gov/resource/jr58-6ysp.json">CDC Nowcast variant proportions</a> (jr58-6ysp). Filtered to <code>usa_or_hhsregion = "USA"</code> for national estimates. Variants with peak share below 5% grouped as "Other." Data current through February 2026. Proportions are model-smoothed estimates; raw sequencing counts shown in confidence intervals (not displayed here).</li>

<li><b>Hospitalizations:</b> <a href="https://data.cdc.gov/resource/aemt-mg7g.json">CDC Hospital Respiratory Data</a> (aemt-mg7g). Filtered to <code>jurisdiction = "USA"</code> for national aggregate. Field: <code>total_admissions_adult_covid_confirmed</code> (confirmed adult COVID-19 admissions in the reporting week). Data runs August 2020 – October 2024 when mandatory reporting ended.</li>

<li><b>Vaccination:</b> <a href="https://data.cdc.gov/resource/unsk-b7fc.json">CDC COVID-19 Vaccinations in the United States, Jurisdiction</a> (unsk-b7fc). National data filtered to <code>location = "US"</code>; state data filtered to two-letter state codes. Fields: <code>administered_dose1_pop_pct</code>, <code>series_complete_pop_pct</code>, <code>additional_doses_vax_pct</code>, <code>bivalent_booster_pop_pct</code>. Data runs December 2020 – May 2023.</li>

<li><b>Wastewater surveillance:</b> <a href="https://data.cdc.gov/resource/2ew6-ywp6.json">CDC NWSS Public SARS-CoV-2 Wastewater Metric Data</a> (2ew6-ywp6). The <code>percentile</code> field represents each site's current viral level ranked against its own historical distribution (0=lowest ever, 100=highest ever). State-level estimates computed as median percentile across reporting sites. National trend computed as median percentile across all sites reporting in each biweekly window, restricted to windows with at least 75 reporting sites. Wastewater data current through September 2025.</li>

<li><b>Charts:</b> Generated using Plotly and Python from the raw API data. All source code available in the site's <a href="https://github.com/charlieholland/chiply.dev">GitHub repository</a>.</li>
</ul>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Drug Pricing Transparency: What Medicare Pays Per Day</title>
      <link>https://www.chiply.dev/post-drug-pricing-transparency</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-drug-pricing-transparency</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Americans pay more for prescription drugs than any other country. This post uses CMS Medicare Part D spending data &amp;#x2013; covering ~3,600 drugs from 2019 to 2023 &amp;#x2013; to visualize where $276 bil...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="drugs">drugs</span>&#xa0;<span class="pricing">pricing</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org2120b6f" class="figure">
<p><img src="https://www.chiply.dev/images/drug-pricing-transparency-banner.jpeg" alt="drug-pricing-transparency-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Americans pay more for prescription drugs than any other country.  This post uses CMS Medicare Part D spending data &#x2013; covering ~3,600 drugs from 2019 to 2023 &#x2013; to visualize where $276 billion in annual drug spending goes, which drugs cost the most per day, and how prices have changed over five years.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Medicare Part D spent $276 billion on prescription drugs in 2023. The top 10 drugs alone — led by Eliquis at $18.3 billion — accounted for 26% of all spending. The most expensive drug by daily cost runs over $850/day. GLP-1 drugs like Ozempic and Trulicity now represent a $34+ billion category and are growing fast. Price growth varies wildly: some drugs have compounded at 10%+ annually while others have dropped.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cms">cms</span>&#xa0;<span class="partd">partd</span>&#xa0;<span class="drugs">drugs</span>&#xa0;<span class="pricing">pricing</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Americans pay more for prescription drugs than any other country. Part of the reason is opacity: drug prices are negotiated in private between manufacturers, pharmacy benefit managers, and insurers. But one major payer — Medicare Part D — publishes its spending data annually. The result is the most comprehensive public window into what the US actually pays for drugs.
</p>

<p>
The CMS Medicare Part D Spending by Drug dataset covers every drug with sufficient Medicare claims (roughly 3,600 brand and generic drugs), with annual spending, claim counts, beneficiary counts, and average costs per fill. It covers 2019–2023, providing a five-year view that captures the pandemic disruption, the GLP-1 explosion, and ongoing price escalation in specialty drugs.
</p>

<p>
Total 2023 spending: <b>$276 billion</b> across all Part D drugs. For context, that's more than the entire federal discretionary budget for education, transportation, and energy combined.
</p>
</div>
</div>
<div id="outline-container-treemap" class="outline-2">
<h2 id="treemap"><span class="section-number-2">4.</span> The Landscape: Where the Money Goes&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="treemap">treemap</span></span></h2>
<div class="outline-text-2" id="text-treemap">
<p>
Each block represents one drug, sized by total 2023 Medicare Part D spending. Color shows the 5-year compound annual growth rate in price per dosage unit — red means fast price growth, blue means prices have declined.
</p>

<div id="dp_treemap" class="plotly-plot"></div>

<p>
A few things stand out in the landscape:
</p>

<ul class="org-ul">
<li><b>Eliquis dominates.</b> The blood thinner apixaban (Eliquis) is Medicare's single largest drug expense at $18.3 billion — nearly twice the second-place drug. Its patent has been challenged but held; a wave of generic competition is expected in coming years.</li>
<li><b>Blood thinners and diabetes drugs are the biggest categories.</b> Eliquis, Xarelto, Jardiance, Farxiga, and the GLP-1 drugs collectively account for a disproportionate share of total Part D spending.</li>
<li><b>Many large drugs have steady price growth.</b> The orange-to-red tint on drugs like Humira, Eliquis, and Trulicity reflects consistent price increases compounding at 5–10% annually over five years.</li>
<li><b>Some drugs have seen prices fall.</b> Blue-tinted drugs include some generics and drugs that faced biosimilar or generic competition during this period.</li>
</ul>
</div>
</div>
<div id="outline-container-cost-per-day" class="outline-2">
<h2 id="cost-per-day"><span class="section-number-2">5.</span> The Most Expensive Drugs Per Day&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="cost">cost</span></span></h2>
<div class="outline-text-2" id="text-cost-per-day">
<p>
"Cost per day" cuts through confusing per-unit pricing to give an intuitive sense of what a drug actually costs to take. This shows the 40 highest daily-cost drugs in Medicare Part D, restricted to drugs with at least 50,000 claims.
</p>

<div id="dp_cost_per_day" class="plotly-plot"></div>

<p>
The most expensive drug by daily cost is <b>Vyndamax</b> (tafamidis), a treatment for transthyretin amyloid cardiomyopathy — a rare heart condition — at roughly $860/day. <b>Stelara</b> (ustekinumab), a biologic for psoriasis and Crohn's disease, runs about $850/day. Both are biologics: large-molecule drugs that are expensive to manufacture and face limited competition.
</p>

<p>
For context:
</p>
<ul class="org-ul">
<li>The most expensive drugs on this list are almost exclusively biologics or specialty oncology drugs</li>
<li>Many GLP-1 drugs (Ozempic, Trulicity, Mounjaro) appear in the mid-tier — expensive but not at the extreme top end</li>
<li>Blood thinners (Eliquis, Xarelto) dominate total spending due to massive volume despite moderate daily cost</li>
</ul>
</div>
</div>
<div id="outline-container-trends" class="outline-2">
<h2 id="trends"><span class="section-number-2">6.</span> Spending Trajectories: Five Years of Growth&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="trends">trends</span></span></h2>
<div class="outline-text-2" id="text-trends">
<p>
How has spending evolved from 2019 to 2023 for the top drugs?
</p>

<div id="dp_trend" class="plotly-plot"></div>

<p>
The most dramatic story is <b>Ozempic</b> (semaglutide). Barely a rounding error in 2019 Part D spending, it reached $9.2 billion by 2023 — entirely driven by volume, as GLP-1 prescriptions exploded first for diabetes management and then for weight loss. <b>Jardiance</b> and <b>Farxiga</b> (SGLT-2 inhibitors) show similar trajectories.
</p>

<p>
<b>Eliquis</b> has grown steadily throughout, driven by both price increases and aging-population volume growth in atrial fibrillation. <b>Revlimid</b> (lenalidomide, for multiple myeloma) peaked and then declined as generic competition entered after 2022.
</p>

<p>
<b>Humira</b> (adalimumab) shows an unusual pattern: total spending held roughly flat even as biosimilar competition should have reduced it, because the branded version maintained pricing while biosimilar uptake was slower than expected.
</p>
</div>
</div>
<div id="outline-container-scatter" class="outline-2">
<h2 id="scatter"><span class="section-number-2">7.</span> Price vs. Volume: The Two Levers of Drug Spending&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="scatter">scatter</span></span></h2>
<div class="outline-text-2" id="text-scatter">
<p>
Total drug spending is driven by two factors: price per unit and volume of prescriptions. This scatter shows how both changed from 2022 to 2023. Drugs in the upper-right quadrant are becoming more expensive to treat <i>and</i> being used more — a double driver of spending growth.
</p>

<div id="dp_scatter" class="plotly-plot"></div>

<p>
The upper-right quadrant — price up and volume up — is where policymakers focus. GLP-1 drugs (Ozempic, Mounjaro) appear here with explosive volume growth, though their price per unit has been more controlled. Blood thinners sit near the center: modest price increases, stable or slowly growing volume.
</p>

<p>
Drugs in the upper-left (volume up, price down) often reflect generics entering the market or manufacturer rebates increasing. The lower-right (price up, volume down) may indicate drugs losing market share as alternatives emerge, but still raising prices on their remaining users.
</p>
</div>
</div>
<div id="outline-container-policy" class="outline-2">
<h2 id="policy"><span class="section-number-2">8.</span> The Policy Context&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span>&#xa0;<span class="policy">policy</span></span></h2>
<div class="outline-text-2" id="text-policy">
<p>
The Inflation Reduction Act (IRA) of 2022 gave Medicare the authority to negotiate drug prices directly for the first time in its history. The first round of negotiations covered 10 drugs, including Eliquis and Xarelto, with negotiated prices taking effect in 2026. The second round expanded the list.
</p>

<p>
The data here shows why this matters: these drugs represent tens of billions in annual Medicare spending, and their prices have compounded upward for years in a market where Medicare previously had no negotiating power. The projected savings from negotiation are modest relative to total spending — but the precedent is significant.
</p>

<p>
For GLP-1 drugs, the policy debate is different. These drugs have demonstrated clinical benefit not just for diabetes but for cardiovascular outcomes and obesity-related conditions. The question isn't whether prices are too high in isolation, but how Medicare balances access (these drugs could benefit tens of millions of beneficiaries) against budget impact at scale.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data from the <a href="https://data.cms.gov/summary-statistics-on-use-and-payments/medicare-medicaid-spending-by-drug/medicare-part-d-spending-by-drug">CMS Medicare Part D Spending by Drug</a> dataset (2019–2023).
</p>

<ul class="org-ul">
<li><b>Coverage:</b> Drugs with sufficient Medicare Part D claims to meet CMS's minimum reporting threshold (10+ prescribers, 11+ beneficiaries per year). Approximately 3,600 drugs included.</li>
<li><b>"Overall" rows:</b> The dataset includes one row per drug per manufacturer, plus an "Overall" aggregate row. All analysis here uses the "Overall" rows.</li>
<li><b>Cost per day:</b> Calculated as average spending per claim divided by 30 (assumes a 30-day supply per claim, a standard Part D fill). This is an approximation; some drugs have different supply durations.</li>
<li><b>Price change:</b> <code>Chg_Avg_Spnd_Per_Dsg_Unt_22_23</code> is the year-over-year change in weighted average spending per dosage unit (2022→2023). The 5-year CAGR covers 2019→2023.</li>
<li><b>Volume change:</b> Change in total claims count from 2022 to 2023.</li>
<li><b>Minimum thresholds:</b> The cost-per-day chart requires ≥50,000 annual claims; the scatter requires ≥100,000 to reduce noise from low-volume drugs.</li>
</ul>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>The GLP-1 Revolution: How a Diabetes Drug Became the Fastest-Growing Drug Category in Medicare History</title>
      <link>https://www.chiply.dev/post-glp1-rise</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-glp1-rise</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Medicare Part D spending on GLP-1 drugs grew from $2.3 billion in 2019 to roughly $27 billion by 2024. This post traces that explosion through CMS spending data, beneficiary counts, state-level adopti...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="drugs">drugs</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgc6a5715" class="figure">
<p><img src="https://www.chiply.dev/images/glp1-rise-banner.jpeg" alt="glp1-rise-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Medicare Part D spending on GLP-1 drugs grew from $2.3 billion in 2019 to roughly $27 billion by 2024.  This post traces that explosion through CMS spending data, beneficiary counts, state-level adoption maps, and FDA adverse event reports &#x2013; showing how a diabetes drug became the fastest-growing drug category in Medicare history.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
In 2019, Medicare Part D spent $2.3 billion on GLP-1 diabetes drugs — primarily Trulicity. By 2024, that number had grown to roughly $27 billion, with nearly 4 million Medicare beneficiaries on one of these drugs. Ozempic alone costs Medicare more annually than all opioids combined. The explosion is driven almost entirely by volume — the cost per dose has stayed roughly flat — and it is still accelerating. Mounjaro (tirzepatide) is growing faster than Ozempic ever did, and Wegovy/Zepbound (the obesity-indication formulations) have barely begun to penetrate Medicare coverage. The FDA adverse event database has absorbed more tirzepatide reports in 3 years than semaglutide accumulated in 7, suggesting the pace of adoption is genuinely without precedent in recent pharmaceutical history.
</p>
</div>
</div>
<div id="outline-container-background" class="outline-2">
<h2 id="background"><span class="section-number-2">3.</span> What Are GLP-1 Drugs?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="background">background</span>&#xa0;<span class="mechanism">mechanism</span></span></h2>
<div class="outline-text-2" id="text-background">
<p>
GLP-1 stands for glucagon-like peptide-1, an incretin hormone produced in the gut after meals. Its job is to signal the pancreas to release insulin, slow gastric emptying so nutrients absorb more gradually, and tell the brain that you've eaten enough. In healthy individuals, GLP-1 surges after meals and dissipates quickly — its half-life is roughly two minutes. GLP-1 receptor agonists (GLP-1 RAs) are synthetic molecules designed to bind the same receptors but survive much longer in circulation, producing a sustained hormonal signal that controls blood sugar and suppresses appetite.
</p>

<p>
The first GLP-1 RA approved in the US was exenatide (Byetta) in 2005 — delivered twice daily by injection and requiring significant patient effort. The category remained a niche diabetes treatment for over a decade: effective but complicated, and competing against established oral options (metformin, sulfonylureas) and insulin. The breakthrough came from a simpler formulation.
</p>

<p>
<b>Ozempic's arc is the story of the category.</b> Semaglutide (Novo Nordisk) was approved by the FDA in December 2017 as a once-weekly subcutaneous injection for type 2 diabetes. Clinical trials showed not only strong blood sugar control but unexpected weight loss — 10-15% body weight reduction in some patients — and, crucially, reduced cardiovascular events in a high-risk population. Those cardiovascular outcomes data (from the SUSTAIN-6 trial) made Ozempic the first GLP-1 with a proven heart disease benefit, dramatically expanding the patient population that could justify its use.
</p>

<p>
The next pivotal moment was Ozempic's cultural emergence. By 2022, social media was saturated with accounts of dramatic weight loss from the drug — often from people taking it off-label without a diabetes diagnosis. The #Ozempic hashtag generated billions of views. Celebrities were rumored to be using it. Supply chains strained. The drug had escaped its original clinical context and become a cultural phenomenon.
</p>

<p>
<b>Then tirzepatide arrived.</b> Mounjaro (tirzepatide, Eli Lilly) was approved in May 2022 for type 2 diabetes. It is a dual agonist — activating both the GLP-1 receptor <i>and</i> the GIP (glucose-dependent insulinotropic polypeptide) receptor. Clinical trial data suggested 20-22% mean body weight reduction, substantially above semaglutide's ceiling. Its SURPASS trial program showed better A1c and weight reduction than Ozempic in head-to-head comparisons. The drug entered Medicare Part D with 54,000 beneficiaries in 2022. By 2024, it had 895,000 — a 16× increase in two years.
</p>

<p>
Approval timelines for the major drugs in the class:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-right" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Drug</th>
<th scope="col" class="org-left">Brand</th>
<th scope="col" class="org-left">Indication</th>
<th scope="col" class="org-right">FDA Approval</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Exenatide</td>
<td class="org-left">Byetta</td>
<td class="org-left">T2D</td>
<td class="org-right">2005</td>
</tr>

<tr>
<td class="org-left">Liraglutide</td>
<td class="org-left">Victoza</td>
<td class="org-left">T2D</td>
<td class="org-right">2010</td>
</tr>

<tr>
<td class="org-left">Liraglutide</td>
<td class="org-left">Saxenda</td>
<td class="org-left">Obesity</td>
<td class="org-right">2014</td>
</tr>

<tr>
<td class="org-left">Dulaglutide</td>
<td class="org-left">Trulicity</td>
<td class="org-left">T2D</td>
<td class="org-right">2014</td>
</tr>

<tr>
<td class="org-left">Semaglutide</td>
<td class="org-left">Ozempic</td>
<td class="org-left">T2D</td>
<td class="org-right">2017</td>
</tr>

<tr>
<td class="org-left">Semaglutide</td>
<td class="org-left">Rybelsus</td>
<td class="org-left">T2D (oral)</td>
<td class="org-right">2019</td>
</tr>

<tr>
<td class="org-left">Semaglutide</td>
<td class="org-left">Wegovy</td>
<td class="org-left">Obesity</td>
<td class="org-right">2021</td>
</tr>

<tr>
<td class="org-left">Tirzepatide</td>
<td class="org-left">Mounjaro</td>
<td class="org-left">T2D</td>
<td class="org-right">2022</td>
</tr>

<tr>
<td class="org-left">Tirzepatide</td>
<td class="org-left">Zepbound</td>
<td class="org-left">Obesity</td>
<td class="org-right">2023</td>
</tr>
</tbody>
</table>

<p>
The T2D and Obesity brands are often the same molecule at different doses and with different regulatory pathways. This distinction matters enormously for insurance coverage.
</p>
</div>
</div>
<div id="outline-container-spending-explosion" class="outline-2">
<h2 id="spending-explosion"><span class="section-number-2">4.</span> The Spending Explosion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="spending">spending</span>&#xa0;<span class="cms">cms</span></span></h2>
<div class="outline-text-2" id="text-spending-explosion">
<p>
This chart shows gross Medicare Part D spending on GLP-1 drugs from 2019 through 2024.
</p>

<div id="glp1_spending_growth" class="plotly-plot"></div>

<p>
The shape is unmistakable. In 2019, the entire GLP-1 category cost Medicare $2.3 billion — dominated by Trulicity (dulaglutide), which was then the market leader. By 2023, spending had reached $22 billion, with Ozempic accounting for $9.2 billion alone. The 2024 full-year figures push the total to approximately $27 billion, with Ozempic ($13.0B), Mounjaro ($6.3B), and Trulicity ($5.5B) as the top three.
</p>

<p>
To put $27 billion in context:
</p>

<ul class="org-ul">
<li>It exceeds the total Medicare Part D spending on all cancer drugs in 2019</li>
<li>It is more than the entire federal budget for the Environmental Protection Agency (~$9B) or the National Science Foundation (~$9B)</li>
<li>Medicare spent more on Ozempic alone in 2024 than on Humira (adalimumab, long the most expensive drug in the US) at its peak</li>
</ul>

<p>
The growth is not a plateau. The 2025 Q1-Q2 partial year shows Ozempic at $7.6B and Mounjaro at $5.7B in just the first half — both tracking above their 2024 full-year rates. The trajectory has not bent.
</p>

<p>
<b>What's driving the spending?</b> The answer is almost entirely volume, not price. Average spend per beneficiary on Ozempic has remained roughly $6,000-$7,000 per year (gross, before rebates). The extraordinary growth comes from the number of people on the drug — which increased from roughly 70,000 Medicare beneficiaries in 2019 to nearly 2 million in 2024, a 28× increase.
</p>
</div>
</div>
<div id="outline-container-patient-surge" class="outline-2">
<h2 id="patient-surge"><span class="section-number-2">5.</span> The Patient Surge&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="beneficiaries">beneficiaries</span></span></h2>
<div class="outline-text-2" id="text-patient-surge">
<div id="glp1_benes_growth" class="plotly-plot"></div>

<p>
The beneficiary chart shows just how fast adoption has been. Trulicity had the most Medicare users in 2019-2021 — its patient base was built gradually over five years of diabetes treatment. Ozempic overtook it in beneficiary count by 2022 despite launching years later, driven by higher demand and more aggressive prescribing. Mounjaro's trajectory is steeper than either: launched mid-2022, it reached 895,000 beneficiaries in 2024 with its first full calendar year barely completed.
</p>

<p>
Wegovy's beneficiary count remains small in Medicare — only 53,000 in 2024 — for a policy reason. Medicare Part D is prohibited by statute from covering drugs "used for weight loss." Because Wegovy is FDA-approved for chronic weight management (not diabetes), Medicare couldn't cover it until the Inflation Reduction Act and subsequent regulatory action clarified that obesity itself is a disease condition. The 2024 PREVENT trial data showing Ozempic reduced major cardiovascular events by 20% in people with obesity (not just diabetes) created a new pathway: Medicare now covers semaglutide for patients with obesity <i>and</i> established cardiovascular disease. But coverage for obesity alone without a comorbidity remains unresolved. Zepbound faces the same situation — only 245 Medicare beneficiaries in all of 2024.
</p>

<p>
This gap between diabetes-indication and obesity-indication coverage represents the policy frontier. Tens of millions of Americans with obesity could benefit from these drugs. If Medicare were to cover GLP-1s broadly for obesity, estimates of the budget impact range from $35 billion to $145 billion per year.
</p>
</div>
</div>
<div id="outline-container-market-share" class="outline-2">
<h2 id="market-share"><span class="section-number-2">6.</span> The Market Shift: From Trulicity to Ozempic to Mounjaro :dataviz:market-share:</h2>
<div class="outline-text-2" id="text-market-share">
<div id="glp1_market_share" class="plotly-plot"></div>

<p>
The market share chart reveals the competitive dynamics within the GLP-1 class. In 2019, Trulicity commanded roughly 98% of GLP-1 spending by virtue of having no real competition. Ozempic began taking share in 2020, and by 2022 had matched Trulicity's spending. Mounjaro's entry in 2022 and rapid growth through 2024 has compressed Trulicity's share from near-total to less than 20%.
</p>

<p>
This consolidation matters for pricing. Trulicity lost patent exclusivity in 2023 — but generic dulaglutide has been slow to enter the market in meaningful volume, partly because injectable biologics face higher barriers to generic competition than oral drugs. Meanwhile Ozempic and Mounjaro remain fully patent-protected, with estimated expirations not until the late 2020s.
</p>

<p>
The oral formulation battle adds another dimension. Rybelsus (oral semaglutide, once-daily pill) has carved out a consistent 5-8% of GLP-1 spending. It has lower efficacy than the injectable form but appeals to needle-averse patients and has enabled prescriptions in patient populations that rejected injections. Oral tirzepatide is in development, which would substantially expand access to that class.
</p>
</div>
</div>
<div id="outline-container-pricing" class="outline-2">
<h2 id="pricing"><span class="section-number-2">7.</span> The Price Question: Cost Per Beneficiary&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="pricing">pricing</span></span></h2>
<div class="outline-text-2" id="text-pricing">
<p>
One common narrative about GLP-1 growth is that pharma companies have been raising prices aggressively. The per-beneficiary data complicates that picture.
</p>

<div id="glp1_cost_per_bene" class="plotly-plot"></div>

<p>
Average spending per beneficiary has remained remarkably stable. Ozempic has held roughly $6,000-$7,000 per beneficiary per year throughout 2020-2024. Mounjaro launched higher (~$8,500 in 2022, reflecting higher doses and higher list price) but has moderated as patient volumes grew. Rybelsus runs substantially cheaper (~$4,500) as an oral daily pill.
</p>

<p>
This stability in per-patient cost means that list price increases have been modest relative to the volume growth. The Inflation Reduction Act (IRA) capped annual Medicare Part D out-of-pocket costs and restructured plan design to require drug manufacturers to provide rebates; this has kept effective per-beneficiary costs roughly flat even as gross spending has exploded with volume.
</p>

<p>
The gross figures ($12,000/year for Ozempic is the US list price for cash-paying patients without insurance) are not what Medicare pays. Medicare negotiates through Part D plans and receives manufacturer rebates that can reduce net cost by 40-60%. The actual Medicare expenditure per Ozempic beneficiary is likely closer to $3,000-$4,000 per year — still substantial, but very different from the list price.
</p>

<p>
The IRA's drug price negotiation provisions specifically target drugs with high Medicare spending, no generic competition, and long patent life. Ozempic and Mounjaro are prime candidates for future negotiation rounds. The first 10 negotiated drugs (from 2023) saved an estimated $6 billion. The potential savings from negotiating GLP-1 prices would dwarf that.
</p>
</div>
</div>
<div id="outline-container-geography" class="outline-2">
<h2 id="geography"><span class="section-number-2">8.</span> Where Are GLP-1s Prescribed?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-geography">
<div id="glp1_state_map" class="plotly-plot"></div>

<p>
The geographic distribution of GLP-1 beneficiaries is heavily driven by state population — California, Texas, Florida, and New York dominate in raw beneficiary count. But the interesting variation is in the rate of adoption relative to Medicare enrollment.
</p>

<p>
Several patterns emerge:
</p>

<ul class="org-ul">
<li><b>Southern states</b> have high absolute volumes, consistent with higher rates of type 2 diabetes (the South has the nation's highest diabetes prevalence, particularly in the "Diabetes Belt" stretching from the Carolinas through the Gulf states).</li>
<li><b>Western states</b> show moderate-to-high GLP-1 use relative to their Medicare populations, partly reflecting the density of endocrinology practices and more aggressive prescribing culture in certain urban centers.</li>
<li><b>Rural states</b> often show high rates of diabetes but lower GLP-1 adoption — a gap driven by both provider access (fewer endocrinologists, more reliance on primary care for diabetes management) and coverage barriers (high deductibles, prior authorization burdens that deter prescribing).</li>
</ul>

<p>
The gap between diabetes prevalence and GLP-1 prescribing is one of the less-discussed aspects of the story. Roughly 30 million Americans have type 2 diabetes. About 2 million Medicare beneficiaries are on semaglutide. Even assuming all are diabetic and all are elderly, only a fraction of the eligible population is receiving the drug. The headroom for further volume growth — even before the obesity indication — is substantial.
</p>
</div>
</div>
<div id="outline-container-adverse-events" class="outline-2">
<h2 id="adverse-events"><span class="section-number-2">9.</span> The Adverse Event Profile&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="safety">safety</span>&#xa0;<span class="faers">faers</span></span></h2>
<div class="outline-text-2" id="text-adverse-events">
<p>
No drug category grows this fast without accumulating an adverse event record. The FDA's Adverse Event Reporting System (FAERS) captures voluntary reports from patients, physicians, and manufacturers — not clinical trial data, but real-world post-market surveillance.
</p>
</div>
<div id="outline-container-faers-growth" class="outline-3">
<h3 id="faers-growth"><span class="section-number-3">9.1.</span> Reports Are Exploding Alongside Prescriptions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="subsection">subsection</span></span></h3>
<div class="outline-text-3" id="text-faers-growth">
<div id="glp1_faers_growth" class="plotly-plot"></div>

<p>
The FAERS report count mirrors the prescription growth but with a striking asymmetry: tirzepatide has accumulated far more adverse event reports in 3 years than semaglutide accumulated in 7. In 2025 alone (through mid-February), tirzepatide has generated ~62,000 reports versus ~26,000 for semaglutide. Part of this reflects the faster rate of adoption — Mounjaro reached a million prescriptions per month faster than Ozempic did. But part reflects the drug's novelty: when patients and physicians have less experience with a drug, they are more likely to submit reports for events they might not flag for an established drug.
</p>

<p>
The FAERS database is a reporting system, not an incidence register — a high report count doesn't mean tirzepatide is less safe than semaglutide. It means patients are using it in large numbers, often in new clinical contexts (weight loss without diabetes), and that the signal-detection machinery is working.
</p>
</div>
</div>
<div id="outline-container-faers-reactions" class="outline-3">
<h3 id="faers-reactions"><span class="section-number-3">9.2.</span> How the Reaction Profiles Compare&#xa0;&#xa0;&#xa0;<span class="tag"><span class="subsection">subsection</span></span></h3>
<div class="outline-text-3" id="text-faers-reactions">
<div id="glp1_faers_reactions" class="plotly-plot"></div>

<p>
The butterfly chart shows the reaction profile for each drug, normalized per 1,000 FAERS reports. The shape is remarkably similar — both drugs share the same core mechanism (GLP-1 receptor activation slows gastric emptying), so the dominant adverse event category for both is gastrointestinal: nausea, vomiting, diarrhea, constipation, and upper abdominal pain.
</p>

<p>
A few differences stand out:
</p>

<ul class="org-ul">
<li><b>Tirzepatide reports a higher rate of injection site reactions</b> (pain, hemorrhage, erythema, bruising). This likely reflects its newer formulation and patient population — fewer patients have experience with injectable medications, and more are injecting for weight loss rather than a decades-long diabetes management routine.</li>
<li><b>Semaglutide shows a higher rate of "impaired gastric emptying"</b> as a directly named reaction — tirzepatide patients may be experiencing the same effect but describing it differently (as nausea or constipation rather than the clinical term).</li>
<li><b>Metabolic signals (weight decreased, blood glucose changes) appear on both.</b> Weight loss is a feature of these drugs, but it appears as an "adverse event" in FAERS when patients report unexpected or excessive weight loss.</li>
<li><b>Psychiatric signals are present but small.</b> Depression, anxiety, and suicidal ideation appear in the FAERS data for both drugs. The FDA added warnings about these in 2023 after an EMA signal review; subsequent analyses have generally found no causal link, but the signals remain under surveillance.</li>
<li><b>Gallbladder disease (cholelithiasis, cholecystitis) is more prominent in semaglutide data.</b> Rapid weight loss — any cause, not just GLP-1s — increases gallstone risk. Clinical trials showed gallbladder events occurred in 2-3% of semaglutide-treated patients versus ~1% in placebo. This is a known, labeled risk.</li>
</ul>

<p>
The drug discontinuation rate is the most practically relevant safety signal. Both drugs have high dropout rates in real-world data — roughly 50% of patients discontinue within a year. The primary driver is GI side effects in the first 4-8 weeks of dose escalation. Patients who tolerate the initial escalation period tend to stay on the drug long-term, and the GI side effects diminish substantially once a maintenance dose is reached.
</p>
</div>
</div>
</div>
<div id="outline-container-drug-comparison" class="outline-2">
<h2 id="drug-comparison"><span class="section-number-2">10.</span> The Drug Adoption Heatmap&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="comparison">comparison</span></span></h2>
<div class="outline-text-2" id="text-drug-comparison">
<div id="glp1_drug_heatmap" class="plotly-plot"></div>

<p>
The heatmap compresses the adoption story into a single view. Each cell shows how many Medicare beneficiaries were on each drug in each year, and what that cost. The pale cells are near-zero adoption (Mounjaro before 2022, Wegovy throughout). The dark cells show the concentration of use — Ozempic in 2024 stands alone as a near-$13B program with nearly 2 million beneficiaries.
</p>

<p>
Trulicity tells a different story: a large, stable patient base that grew slowly and is now beginning to shrink as Mounjaro and Ozempic have displaced it for new prescriptions. The transition from an older drug (Trulicity) to a newer, more effective one (Ozempic/Mounjaro) is how pharmaceutical markets normally work — but the speed and scale are unusual.
</p>
</div>
</div>
<div id="outline-container-policy" class="outline-2">
<h2 id="policy"><span class="section-number-2">11.</span> The Policy Battleground&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span>&#xa0;<span class="policy">policy</span></span></h2>
<div class="outline-text-2" id="text-policy">
<p>
The policy questions around GLP-1s are not abstract budget debates — they determine who can access drugs that could potentially reduce heart attacks, strokes, kidney failure, and premature death at a population scale.
</p>
</div>
<div id="outline-container-org510d846" class="outline-3">
<h3 id="org510d846"><span class="section-number-3">11.1.</span> The Obesity Coverage Gap</h3>
<div class="outline-text-3" id="text-11-1">
<p>
The Medicare prohibition on coverage for weight-loss drugs is a statutory artifact from 2003, when GLP-1s didn't exist. The relevant drugs at the time were largely ineffective stimulants and appetite suppressants with poor safety records. The law made sense in that context. It doesn't in the context of semaglutide, which has demonstrated cardiovascular mortality reduction in randomized trials.
</p>

<p>
In 2024, CMS expanded coverage for Wegovy (semaglutide) for Medicare beneficiaries with established cardiovascular disease. This covered roughly 3.6 million beneficiaries. The next tier — coverage for obesity alone, without a comorbidity — would cover an estimated 40+ million Medicare beneficiaries. The Congressional Budget Office estimated such a change would cost $35 billion over 10 years under certain assumptions, though advocacy groups contest those projections.
</p>

<p>
The Medicaid picture is even more fragmented. Each state decides independently whether to cover GLP-1s for obesity. As of early 2026, fewer than half of states cover obesity-indication GLP-1s through Medicaid. The result is a profound access disparity along income lines — the people most likely to have obesity and type 2 diabetes (lower-income, less likely to have premium employer insurance) are least likely to have coverage for the most effective treatments.
</p>
</div>
</div>
<div id="outline-container-orgcb25e80" class="outline-3">
<h3 id="orgcb25e80"><span class="section-number-3">11.2.</span> The Price Negotiation Frontier</h3>
<div class="outline-text-3" id="text-11-2">
<p>
The IRA gave Medicare the authority to negotiate drug prices for a targeted list of high-spending, high-expenditure drugs. The second round of negotiations (effective 2026) includes several drugs with GLP-1-adjacent relevance. The third and fourth rounds are widely expected to include Ozempic and Mounjaro, given that:
</p>

<ul class="org-ul">
<li>Ozempic is now the single most expensive drug in Medicare Part D by total expenditure</li>
<li>Semaglutide has no generic equivalent and patent protection extends into the late 2020s</li>
<li>Mounjaro is on a similar trajectory and entered Medicare spending discussions within its first year of eligibility</li>
</ul>

<p>
Negotiated prices for the first 10 drugs ranged from 38% to 79% discounts from list price. Even a 40% reduction in Ozempic's Medicare net cost would save several billion dollars annually at current volumes — and current volumes are still growing.
</p>
</div>
</div>
<div id="outline-container-orgb72b519" class="outline-3">
<h3 id="orgb72b519"><span class="section-number-3">11.3.</span> The Compounding Problem</h3>
<div class="outline-text-3" id="text-11-3">
<p>
The drug shortage during 2022-2024 created a secondary market: compounding pharmacies began producing semaglutide and tirzepatide at dramatically lower prices ($100-$400/month vs $900-$1,300/month for brand-name). The FDA initially allowed compounding during the shortage period under the 503B compounding pharmacy framework. As shortages were resolved, the FDA moved to restrict compounding of these drugs, triggering legal challenges from compounding pharmacies and patient advocacy groups.
</p>

<p>
FAERS data supports concern about compounded GLP-1 quality — the reporting odds ratio for suicidality in compounded semaglutide reports is substantially higher than for brand-name, though this likely reflects confounding (patients who use compounded drugs are a different population) rather than pharmacological difference. Dosing errors in compounded products have been documented more frequently, likely reflecting variability in formulation and patient self-administration without clinical titration support.
</p>
</div>
</div>
<div id="outline-container-orgfe68078" class="outline-3">
<h3 id="orgfe68078"><span class="section-number-3">11.4.</span> What Happens When These Drugs Go Generic?</h3>
<div class="outline-text-3" id="text-11-4">
<p>
Liraglutide (Victoza/Saxenda) came off patent in 2023. Generic dulaglutide (Trulicity) is expected to be more broadly available by 2026. Semaglutide's patents are contested — Novo Nordisk has over 70 patents covering formulation, delivery, and use; generic manufacturers are challenging many of them. The first generic semaglutide could theoretically reach the US market by 2026-2027 under the most optimistic patent challenge scenarios, though 2030 is a more consensus estimate.
</p>

<p>
When these drugs do go generic, the cost dynamics will change dramatically. Metformin — the standard-of-care oral diabetes medication — now costs $4 per month at major pharmacies. Generic GLP-1s won't reach that floor immediately, but they could plausibly reach $50-$100/month within five years of generic entry, compared to $1,000+/month today. The access implications are enormous.
</p>
</div>
</div>
</div>
<div id="outline-container-broader-signal" class="outline-2">
<h2 id="broader-signal"><span class="section-number-2">12.</span> The Broader Signal: What GLP-1s Tell Us About Drug Innovation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h2>
<div class="outline-text-2" id="text-broader-signal">
<p>
The GLP-1 story is not primarily a story about diabetes drugs. It is a story about what happens when a drug unexpectedly becomes effective for a condition (obesity) that affects roughly 40% of the American population, has limited existing pharmaceutical treatments, and for which payers have historically refused to cover medications.
</p>

<p>
Several features of this story are genuinely new in pharmaceutical history:
</p>

<p>
<b>Efficacy that created demand.</b> Most blockbuster drugs achieve high sales through marketing and insurance coverage. GLP-1s created their own demand — social media, celebrity culture, and visible results among early adopters drove patients to seek prescriptions in ways that outpaced pharma marketing budgets. The Ozempic viral moment was not manufactured by Novo Nordisk.
</p>

<p>
<b>CV outcomes data changing coverage.</b> The expansion of GLP-1 coverage from "diabetes management" to "cardiovascular risk reduction" to (partially) "obesity" follows clinical trial results, not lobbying. The SUSTAIN-6, LEADER, SELECT, and SURPASS-CVOT trials produced mortality reduction data that made it progressively harder for payers to deny coverage on efficacy grounds.
</p>

<p>
<b>The manufacturing constraint.</b> This class of drugs requires biological manufacturing processes (unlike small-molecule pills) and complex formulation. Both Novo Nordisk and Eli Lilly have each spent billions of dollars on manufacturing capacity expansion. The supply chain is still constrained. This creates an unusual situation where a drug is simultaneously in shortage and generating record revenue — not because of pricing strategy, but because demand outstripped manufacturing capacity at any price.
</p>

<p>
<b>The obesity treatment paradigm shift.</b> For thirty years, the medical consensus on obesity treatment was behavioral (diet, exercise, lifestyle modification). The failure rate of behavioral intervention alone is high — around 80-90% of patients regain lost weight within 5 years. The GLP-1 clinical data has forced a re-evaluation. If 20% mean weight loss can be sustained pharmacologically, with mortality reduction data to match, the treatment paradigm shifts from "treat the behaviors" to "treat the underlying hormonal dysregulation." This reframing has wide-ranging implications for how obesity is understood, coded, covered, and treated.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">13.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
<b>Medicare Part D annual spending (2019-2023):</b> <a href="https://data.cms.gov/summary-statistics-on-use-and-payments/medicare-medicaid-spending-by-drug/medicare-part-d-spending-by-drug">CMS Medicare Part D Spending by Drug</a> (dataset ID: <code>7e0b4365-fd63-4a29-8f5e-e0ac9f66a81b</code>). Annual data with per-year breakdowns for spending, claims, beneficiaries, and average per-unit/per-claim/per-beneficiary costs. "Overall" manufacturer rows used for aggregate drug-level analysis.
</p>

<p>
<b>Medicare Part D quarterly spending (2024-2025):</b> <a href="https://data.cms.gov/summary-statistics-on-use-and-payments/medicare-medicaid-spending-by-drug/medicare-quarterly-part-d-spending-by-drug">CMS Medicare Quarterly Part D Spending by Drug</a> (dataset ID: <code>4ff7c618-4e40-483a-b390-c8a58c94fa15</code>). Provides 2024 Q1-Q4 annual totals and 2025 Q1-Q2 partial year data. "Overall" manufacturer rows used.
</p>

<p>
<b>State-level prescribing:</b> <a href="https://data.cms.gov/provider-summary-by-type-of-service/medicare-part-d-prescribers/medicare-part-d-prescribers-by-geography-and-drug">CMS Medicare Part D Prescribers by Geography and Drug</a> (dataset ID: <code>c8ea3f8e-3a09-4fea-86f2-8902fb4b0920</code>). State-level beneficiary counts aggregated across Ozempic, Mounjaro, Trulicity, and Rybelsus.
</p>

<p>
<b>FDA adverse events:</b> <a href="https://open.fda.gov/apis/drug/event/">OpenFDA Drug Adverse Event API</a> (FAERS). Annual report counts by generic drug name (<code>patient.drug.openfda.generic_name</code>). Reaction profiles normalized per 1,000 total reports to enable cross-drug comparison. Administrative/process terms excluded from reaction comparison.
</p>

<ul class="org-ul">
<li><b>Spending figures are gross, before manufacturer rebates.</b> Medicare Part D plans negotiate confidential rebates with drug manufacturers; actual Medicare expenditure is lower than gross spending, typically by 40-60% for brand-name drugs. The true net Medicare cost is not publicly reported.</li>
<li><b>2025 data is partial:</b> Quarterly dataset includes 2025 Q1-Q2 (approximately January-June 2025). Full-year 2025 is not yet available.</li>
<li><b>FAERS reports are not incidence data:</b> The FAERS database captures voluntary adverse event reports from patients, physicians, and manufacturers. High report counts reflect high usage and high media attention, not necessarily higher risk relative to other drugs. The FDA uses FAERS for signal detection, not causality determination.</li>
</ul>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>HAI Scoreboard: Hospital-Acquired Infection Rates</title>
      <link>https://www.chiply.dev/post-hai-scoreboard</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-hai-scoreboard</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Every year, about 1 in 31 hospitalized patients acquires an infection they didn&apos;t have when they were admitted. This post visualizes CMS hospital-acquired infection data across ~4,789 US hospitals, ex...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="hospitals">hospitals</span>&#xa0;<span class="infections">infections</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgb8a1c10" class="figure">
<p><img src="https://www.chiply.dev/images/hai-scoreboard-banner.jpeg" alt="hai-scoreboard-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Every year, about 1 in 31 hospitalized patients acquires an infection they didn't have when they were admitted.  This post visualizes CMS hospital-acquired infection data across ~4,789 US hospitals, exploring where hospitals fall on national benchmarks and what that means for patients choosing where to receive care.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Around 4,789 US hospitals report hospital-acquired infection (HAI) data to CMS. Only 211 hospital-infection pairs (less than 1%) are rated "Worse than the National Benchmark" — but the data reveals real variation between hospitals. MRSA bacteremia shows the lowest rates (most hospitals perform well) while C. difficile is the most prevalent HAI by patient volume. The most important finding: your hospital's infection rating is searchable, and it varies enough to matter.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cms">cms</span>&#xa0;<span class="hospitals">hospitals</span>&#xa0;<span class="infections">infections</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Every year, about 1 in 31 hospitalized patients in the US acquires an infection they didn't have when they were admitted. These healthcare-associated infections (HAIs) — infections contracted in hospitals, nursing facilities, or during outpatient procedures — account for roughly 100,000 deaths annually and billions of dollars in excess healthcare costs.
</p>

<p>
CMS collects and publishes HAI data for all Medicare-participating hospitals through its Hospital Compare program. Hospitals report six major infection types: Central Line-Associated Bloodstream Infections (CLABSI), Catheter-Associated Urinary Tract Infections (CAUTI), two types of Surgical Site Infections (SSI), MRSA bacteremia, and <i>Clostridium difficile</i> (C. diff). Each is measured using the Standardized Infection Ratio (SIR): the number of observed infections divided by the number predicted based on national baseline rates and patient mix. A SIR of 1.0 means your hospital's rate exactly matches the national average. Below 1.0 is better; above 1.0 is worse.
</p>

<p>
This post explores where hospitals fall on those benchmarks — and what that means for patients choosing where to receive care.
</p>
</div>
</div>
<div id="outline-container-benchmark" class="outline-2">
<h2 id="benchmark"><span class="section-number-2">4.</span> How Hospitals Compare to National Benchmarks&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="benchmark">benchmark</span></span></h2>
<div class="outline-text-2" id="text-benchmark">
<p>
For each of the six infection types, this chart shows what percentage of hospitals are rated Better, the Same as, or Worse than the national benchmark.
</p>

<div id="hai_benchmark" class="plotly-plot"></div>

<p>
Several patterns emerge:
</p>

<ul class="org-ul">
<li><b>"Not Available" dominates.</b> The largest share of hospital-infection combinations lacks a valid SIR rating — often because the hospital had too few procedures or patient-days to produce a statistically reliable estimate. A hospital with no SIR is not necessarily a safe hospital; it may simply be too small to be measured.</li>
<li><b>Most rated hospitals are within the normal range.</b> Among hospitals with a valid benchmark rating, the majority fall into "No Different than National Benchmark."</li>
<li><b>"Worse" is rare but real.</b> Hospitals rated worse than the national benchmark are a small minority, but they exist across all six infection types. CAUTI (urinary tract infections) and C. diff show the most "Worse" ratings in absolute terms, partly because they are the most common HAIs by volume.</li>
<li><b>SSI categories show the most "Better" ratings.</b> Surgical site infection rates have improved substantially over the past decade as surgical technique and antibiotic prophylaxis have improved; many hospitals now perform better than historical baselines.</li>
</ul>
</div>
</div>
<div id="outline-container-state-map" class="outline-2">
<h2 id="state-map"><span class="section-number-2">5.</span> State-Level Performance&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="choropleth">choropleth</span></span></h2>
<div class="outline-text-2" id="text-state-map">
<p>
Percentage of hospital-infection pairs rated "Worse than the National Benchmark," by state.
</p>

<div id="hai_sir_state" class="plotly-plot"></div>

<p>
The geographic pattern is noisy — HAI rates are hospital-specific, and state averages can be driven by a handful of hospitals in each direction. That said, a few patterns are consistent with other quality measures:
</p>

<ul class="org-ul">
<li>States with higher overall hospital quality ratings (from CMS Hospital General Information) tend to have lower rates of HAI "Worse" designations.</li>
<li>Some states show elevated rates of worse-than-benchmark infections, which may reflect older facilities, different patient populations, or reporting differences.</li>
<li>The "Not Available" category (excluded from this percentage) is higher in rural states with many small critical-access hospitals, which may make state comparisons misleading.</li>
</ul>
</div>
</div>
<div id="outline-container-sir-scatter" class="outline-2">
<h2 id="sir-scatter"><span class="section-number-2">6.</span> MRSA vs. C. diff: Two Key Infections&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="scatter">scatter</span></span></h2>
<div class="outline-text-2" id="text-sir-scatter">
<p>
Each point is a hospital. The x axis shows its C. diff SIR; the y axis shows its MRSA SIR. The reference lines at 1.0 mark the national average. Color reflects the hospital's overall benchmark rating across all six infection types.
</p>

<div id="hai_scatter" class="plotly-plot"></div>

<p>
MRSA and C. diff are often cited as the most consequential HAIs because they are drug-resistant or treatment-difficult, and they spread easily in healthcare settings. A few observations:
</p>

<ul class="org-ul">
<li><b>Most hospitals cluster near the origin</b> (SIR &lt; 1 for both), meaning the average hospital performs better than the historical baseline — a sign of genuine improvement in infection prevention over the past decade.</li>
<li><b>Outliers in the upper-right are concerning.</b> Hospitals with high SIR scores for both MRSA and C. diff simultaneously face a systemic challenge — it isn't random variation in one metric, but a pattern suggesting infection prevention gaps.</li>
<li><b>Green dots (Better overall) tend to cluster below the median for both infections.</b> Hospitals that perform better than benchmark across the board tend to have MRSA and C. diff rates below the national average too — suggesting that overall infection prevention culture matters more than individual measure management.</li>
</ul>
</div>
</div>
<div id="outline-container-worst-hospitals" class="outline-2">
<h2 id="worst-hospitals"><span class="section-number-2">7.</span> Hospitals With the Highest Average Infection Rates&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="rankings">rankings</span></span></h2>
<div class="outline-text-2" id="text-worst-hospitals">
<p>
These 20 hospitals have the highest average SIR scores across all reported infection types (minimum 3 reported measures). These are hospitals where observed infections consistently exceed the predicted rate.
</p>

<div id="hai_top_worst" class="plotly-plot"></div>

<p>
Several important caveats for interpreting this list:
</p>

<ul class="org-ul">
<li><b>Patient mix affects SIRs.</b> The Standardized Infection Ratio is designed to adjust for patient complexity, procedure volume, and type of ICU — but no adjustment is perfect. Hospitals treating the most complex, immunocompromised patients may show elevated SIRs even with good care.</li>
<li><b>Small hospitals can have noisy SIRs.</b> A hospital with 5 predicted infections and 8 observed has a SIR of 1.6 — but that's based on small numbers. CMS has minimum thresholds, but some outliers still reflect statistical noise.</li>
<li><b>This list is a starting point, not a verdict.</b> CMS's full Hospital Compare tool allows patients to see individual infection ratings plus the detailed confidence intervals and footnotes that explain data limitations.</li>
</ul>

<p>
The takeaway: before a planned procedure, it's worth checking your hospital's HAI ratings — not to make a binary good/bad judgment, but to have an informed conversation with your care team about infection prevention practices.
</p>
</div>
</div>
<div id="outline-container-patient-action" class="outline-2">
<h2 id="patient-action"><span class="section-number-2">8.</span> What Patients Can Do&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h2>
<div class="outline-text-2" id="text-patient-action">
<p>
The HAI data is public and searchable through CMS's Care Compare tool. For planned surgeries or procedures, patients can:
</p>

<ol class="org-ol">
<li><b>Look up their hospital's infection ratings</b> at <a href="https://www.medicare.gov/care-compare/">medicare.gov/care-compare</a> — search by hospital name, see each HAI measure.</li>
<li><b>Ask about specific prevention protocols.</b> For surgical cases: does the hospital use SCIP (Surgical Care Improvement Project) protocols? For MRSA: is pre-admission MRSA screening standard practice?</li>
<li><b>Ask about CLABSI bundles.</b> The evidence-based "central line bundle" (hand hygiene, barrier precautions, chlorhexidine skin antisepsis, optimal catheter site selection, daily review of catheter necessity) has been shown to reduce CLABSI rates by up to 70%. Hospitals that follow it consistently should be able to confirm this.</li>
<li><b>Consider volume.</b> Hospitals that perform more of a given procedure tend to have lower complication rates, including infection rates. A center that does 400 hip replacements per year has likely optimized its infection prevention workflow more than one that does 40.</li>
</ol>

<p>
The data exists to make these conversations possible. Hospitals that perform well on HAI metrics tend to have a culture of measurement and improvement — which correlates with better outcomes broadly.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data from the <a href="https://data.cms.gov/provider-data/dataset/77hc-ibv8">CMS Healthcare-Associated Infections - Hospital</a> dataset, current as of April 2024–March 2025.
</p>

<ul class="org-ul">
<li><b>Hospitals covered:</b> 4,789 unique hospitals with at least one HAI measure reported.</li>
<li><b>Measures:</b> Six SIR measures — HAI<sub>1</sub><sub>SIR</sub> (CLABSI), HAI<sub>2</sub><sub>SIR</sub> (CAUTI), HAI<sub>3</sub><sub>SIR</sub> (SSI Colon), HAI<sub>4</sub><sub>SIR</sub> (SSI Abdominal Hysterectomy), HAI<sub>5</sub><sub>SIR</sub> (MRSA), HAI<sub>6</sub><sub>SIR</sub> (C. diff).</li>
<li><b>Benchmark comparison:</b> CMS assigns each hospital-measure pair to one of four categories based on whether the SIR is statistically Better, No Different, or Worse than the national benchmark, or Not Available (insufficient data).</li>
<li><b>State map:</b> Percentage of hospital-measure pairs rated "Worse than the National Benchmark" among all rated pairs (excluding "Not Available"). Only 2-character state codes included (territories excluded).</li>
<li><b>Scatter chart:</b> Restricted to hospitals with valid MRSA and C. diff SIR scores; SIR values above 10 excluded as outliers affecting chart scale.</li>
<li><b>Worst-performing hospitals:</b> Average SIR across all reported measures; requires ≥3 measures with a numeric SIR. Color coding based on the number of "Worse than National Benchmark" designations across all six measures.</li>
</ul>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Healthcare Deserts: Where America Can&apos;t See a Doctor</title>
      <link>https://www.chiply.dev/post-healthcare-deserts</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-healthcare-deserts</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>If you get sick in Boston, you have a choice of specialists within walking distance. If you get sick in rural Mississippi, you might drive two hours to see a primary care doctor. This post visualizes ...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="publicHealth">publicHealth</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orga23516b" class="figure">
<p><img src="https://www.chiply.dev/images/healthcare-deserts-banner.jpeg" alt="healthcare-deserts-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
If you get sick in Boston, you have a choice of specialists within walking distance.  If you get sick in rural Mississippi, you might drive two hours to see a primary care doctor.  This post visualizes HRSA shortage area data to map where the healthcare deserts are, how severe they are, and where safety-net clinics try to fill the gaps.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Roughly 100 million Americans live in a federally designated health professional shortage area — a place with too few primary care doctors, dentists, or mental health providers to meet basic need. This post visualizes that shortage using live HRSA data: where the deserts are, how severe they are, who goes without, and where the safety-net clinics try to fill the gaps.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="hrsa">hrsa</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="publicHealth">publicHealth</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
If you get sick in Boston, you have a choice of specialists within walking distance. If you get sick in rural Mississippi, you might drive two hours to see a primary care doctor — if you can get an appointment at all.
</p>

<p>
This isn't a market failure in the usual sense. It's a structural one. Rural areas, low-income urban neighborhoods, and tribal lands have too few health care providers relative to population because those providers can earn more money elsewhere. The federal government has been tracking this gap since the 1970s through a designation called a Health Professional Shortage Area (HPSA).
</p>

<p>
As of 2025, roughly 6,500 primary care HPSAs, 7,000 dental HPSAs, and 6,000 mental health HPSAs are active across the United States. Together they affect an estimated 100 million people. Every chart on this page draws from the <a href="https://data.hrsa.gov/DataDownload/DD_Files/BCD_HPSA_FCT_DET_PC.csv">HRSA HPSA dataset</a>, updated daily.
</p>
</div>
<div id="outline-container-what-is-hpsa" class="outline-3">
<h3 id="what-is-hpsa"><span class="section-number-3">3.1.</span> What is an HPSA?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="methodology">methodology</span></span></h3>
<div class="outline-text-3" id="text-what-is-hpsa">
<p>
A Health Professional Shortage Area is a formal designation by the Health Resources &amp; Services Administration (HRSA) indicating that a geographic area, specific population (e.g., low-income), or facility (e.g., a prison) has insufficient health professionals to meet demand. Designations are separated into three disciplines:
</p>

<ul class="org-ul">
<li><b>Primary Care</b> — general practitioners, family medicine, internal medicine, OB/GYN, pediatrics</li>
<li><b>Dental Health</b> — dentists and related providers</li>
<li><b>Mental Health</b> — psychiatrists, psychologists, licensed clinical social workers</li>
</ul>

<p>
Each designation carries a shortage score from 0 to 25, where higher scores indicate greater severity. The score incorporates population-to-provider ratio, infant mortality rates, poverty levels, and travel distance to the nearest source of care.
</p>
</div>
</div>
</div>
<div id="outline-container-the-map" class="outline-2">
<h2 id="the-map"><span class="section-number-2">4.</span> The Map: Where the Deserts Are&#xa0;&#xa0;&#xa0;<span class="tag"><span class="maps">maps</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-the-map">
<p>
This map shows the count of active HPSA designations per state. Use the buttons below the map to toggle between care types. Note that one state can contain many distinct shortage areas — a rural county, a low-income urban population, and a correctional facility are each counted separately.
</p>

<div id="hd_state_map" class="plotly-plot"></div>

<p>
A few patterns are immediately visible. Texas and California lead in raw counts partly because of their size and population — but also because each contains enormous internal variation. Rural West Texas is a different universe from Houston. Watts in Los Angeles is medically underserved while Beverly Hills is not.
</p>

<p>
Proportion matters more than raw count, but the absolute numbers reveal how many distinct shortage designations coexist within a single state — each representing a community that officially does not have enough providers.
</p>
</div>
</div>
<div id="outline-container-primary-care" class="outline-2">
<h2 id="primary-care"><span class="section-number-2">5.</span> Primary Care Deserts&#xa0;&#xa0;&#xa0;<span class="tag"><span class="primaryCare">primaryCare</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-primary-care">
<p>
Primary care shortages are the most common and, in some ways, the most consequential. Without a primary care provider, patients delay preventive care, miss chronic disease management, and end up in emergency departments for conditions that should never require one. Diabetes goes uncontrolled. Hypertension goes undiagnosed. Cancers are caught late.
</p>

<p>
The shortage in primary care is structural: medical schools produce roughly the same number of physicians each year, but the economics of specialty medicine make primary care increasingly unattractive as a career. A cardiologist in a major city earns two to three times what a family practice doctor earns in rural Appalachia — and the rural doctor also faces professional isolation, limited resources, and a patient population with higher disease burden.
</p>
</div>
<div id="outline-container-prescriber-ratio" class="outline-3">
<h3 id="prescriber-ratio"><span class="section-number-3">5.1.</span> The prescriber ratio&#xa0;&#xa0;&#xa0;<span class="tag"><span class="metrics">metrics</span></span></h3>
<div class="outline-text-3" id="text-prescriber-ratio">
<p>
HRSA designates a geographic area as a primary care shortage area when the population-to-provider ratio exceeds 3,500:1. In some of the worst-designated areas, that ratio exceeds 30,000:1 — meaning one physician for every 30,000 patients. The national target is under 1,000:1. Emergency departments in urban teaching hospitals typically see one physician per 1,500–2,000 patients per year.
</p>
</div>
</div>
</div>
<div id="outline-container-dental-deserts" class="outline-2">
<h2 id="dental-deserts"><span class="section-number-2">6.</span> Dental Deserts: A Silent Crisis&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dental">dental</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-dental-deserts">
<p>
Dental care is often treated as a luxury — something separate from "real" medicine and therefore outside the scope of public concern. That framing has consequences. Oral disease is the most common chronic condition in childhood. Untreated tooth decay leads to infections that can become life-threatening. Adults with untreated dental disease lose work days, job opportunities, and eventually teeth, which affects nutrition and overall health.
</p>

<p>
Dental shortages are heavily rural and heavily correlated with income. Most private dentists set up practices in areas with higher incomes and better insurance coverage. Medicaid covers dental care for children but often not adults, and Medicaid reimbursement rates are so low that many dentists don't accept it even where coverage nominally exists.
</p>
</div>
</div>
<div id="outline-container-mental-health" class="outline-2">
<h2 id="mental-health"><span class="section-number-2">7.</span> Mental Health: The Deepest Desert&#xa0;&#xa0;&#xa0;<span class="tag"><span class="mentalHealth">mentalHealth</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-mental-health">
<p>
Of the three categories, mental health has the most severe and most geographically concentrated shortage. The U.S. has a nationwide deficit of approximately 6,000 psychiatrists, and the shortage is worst in precisely the places where mental illness rates are highest: rural communities with economic distress, high rates of substance use, and social isolation.
</p>

<p>
In some states, entire counties have no licensed psychiatrist at all. Telehealth has partially filled this gap — the COVID-19 pandemic forced a rapid expansion of remote mental health services — but access to reliable broadband remains a barrier in the same rural areas with the worst provider shortages.
</p>

<p>
The shortage score distribution below shows how the mental health shortage differs from primary care and dental: a higher proportion of mental health HPSAs have scores in the severe range (15–25), indicating that where mental health shortages exist, they tend to be extreme.
</p>

<div id="hd_score_dist" class="plotly-plot"></div>

<p>
The concentration of high scores in mental health reflects several reinforcing factors: the shortage started earlier, grew faster, and involves a workforce that takes longer to train (psychiatrists complete four years of residency after medical school). Psychologists, licensed clinical social workers, and counselors help fill the gap, but HRSA's ratio thresholds for designation still reflect a very real scarcity.
</p>
</div>
</div>
<div id="outline-container-by-the-numbers" class="outline-2">
<h2 id="by-the-numbers"><span class="section-number-2">8.</span> By the Numbers&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="states">states</span></span></h2>
<div class="outline-text-2" id="text-by-the-numbers">
</div>
<div id="outline-container-state-rankings" class="outline-3">
<h3 id="state-rankings"><span class="section-number-3">8.1.</span> Underserved population by state&#xa0;&#xa0;&#xa0;<span class="tag"><span class="states">states</span></span></h3>
<div class="outline-text-3" id="text-state-rankings">
<p>
This chart shows the estimated underserved population by state — that is, the number of people who live in a designated shortage area. The bars are stacked by care type.
</p>

<div id="hd_state_rankings" class="plotly-plot"></div>

<p>
California, Texas, Florida, and New York lead partly because of size, but also because each contains large low-income urban populations with inadequate provider coverage. Note that a person can be counted in multiple stacks — living in a primary care shortage area and a mental health shortage area simultaneously.
</p>
</div>
</div>
<div id="outline-container-worst-areas" class="outline-3">
<h3 id="worst-areas"><span class="section-number-3">8.2.</span> The worst individual shortage areas&#xa0;&#xa0;&#xa0;<span class="tag"><span class="worstAreas">worstAreas</span></span></h3>
<div class="outline-text-3" id="text-worst-areas">
<p>
Individual HPSA designations vary enormously in the population they cover. A single geographic HPSA designation in a dense urban area — covering, say, a low-income neighborhood in Los Angeles — might affect 200,000 people. A rural HPSA in Wyoming might cover 800.
</p>

<p>
This chart shows the 20 individual HPSA designations with the largest estimated underserved populations, colored by shortage score.
</p>

<div id="hd_worst_areas" class="plotly-plot"></div>

<p>
The pattern here is striking: the largest shortage areas tend to be urban low-income population designations, not rural geographic ones. These areas have the population but not the providers — because providers have no economic incentive to serve Medicaid-heavy, low-income patient panels.
</p>
</div>
</div>
</div>
<div id="outline-container-safety-net" class="outline-2">
<h2 id="safety-net"><span class="section-number-2">9.</span> The Safety Net: Where FQHCs Fill the Gaps&#xa0;&#xa0;&#xa0;<span class="tag"><span class="safetyNet">safetyNet</span>&#xa0;<span class="fqhc">fqhc</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="maps">maps</span></span></h2>
<div class="outline-text-2" id="text-safety-net">
<p>
The main federal response to shortage designations is the Federally Qualified Health Center program. FQHCs are community health clinics that receive federal grants and enhanced Medicaid reimbursement in exchange for serving all patients regardless of ability to pay. They charge on a sliding fee scale — $0 for the very poorest patients.
</p>

<div id="hd_fqhc_map" class="plotly-plot"></div>

<p>
Nearly 18,000 active FQHC sites operate across the country, serving roughly 30 million patients annually. They are concentrated in shortage areas by design — HRSA gives priority for FQHC grants to areas with active HPSA designations.
</p>

<p>
But look at where the gaps in the FQHC map are: large stretches of the rural Mountain West, the Plains states, and the rural South have almost no FQHC coverage. These are places where the shortage exists but the safety net was never built. In some counties, there is simply no federally supported primary care option at all.
</p>
</div>
<div id="outline-container-fqhc-detail" class="outline-3">
<h3 id="fqhc-detail"><span class="section-number-3">9.1.</span> What an FQHC actually does&#xa0;&#xa0;&#xa0;<span class="tag"><span class="fqhcDetail">fqhcDetail</span></span></h3>
<div class="outline-text-3" id="text-fqhc-detail">
<p>
FQHCs are required by law to provide:
</p>

<ul class="org-ul">
<li><b>Primary care</b> — adult medicine, pediatrics, OB/GYN</li>
<li><b>Dental care</b> — preventive, restorative, emergency</li>
<li><b>Mental health</b> — behavioral health integration, counseling</li>
<li><b>Pharmacy</b> — through the 340B drug pricing program, enabling steeply discounted medications</li>
<li><b>Case management</b> — coordinating care for patients with complex needs</li>
<li><b>Enabling services</b> — transportation, translation, outreach</li>
</ul>

<p>
This makes FQHCs closer to a full medical home than a simple clinic. They are also required to have governing boards on which a majority of members are patients — a consumer-control requirement that distinguishes them from hospital-run community health programs.
</p>
</div>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">10.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="hrsa">hrsa</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data in this post comes from HRSA's public data warehouse, available at <a href="https://data.hrsa.gov/">data.hrsa.gov</a>:
</p>

<ul class="org-ul">
<li><b>HPSA designations</b> — Three separate CSV files, one each for Primary Care, Dental Health, and Mental Health (<code>BCD_HPSA_FCT_DET_PC/DH/MH.csv</code>). Updated daily. Filtered to <code>HPSA Status =</code> "Designated"= (excludes withdrawn, proposed-for-withdrawal).</li>
<li><b>FQHC sites</b> — <code>Health_Center_Service_Delivery_and_LookAlike_Sites.csv</code>. Filtered to <code>Site Status =</code> "Active"= and <code>Health Center Type =</code> "Federally Qualified Health Center (FQHC)"=.</li>
</ul>

<p>
The shortage score (0–25) is HRSA's composite metric. Primary care scores incorporate the population-to-provider ratio, infant mortality rate, percent of population below poverty, and travel distance to the nearest provider outside the area. Dental and mental health scores use analogous domain-specific formulas.
</p>

<p>
"Estimated underserved population" is HRSA's estimate of how many people in the shortage area lack adequate access — not simply the total population in the area.
</p>

<p>
Designations at the facility level (prisons, FQHCs, tribal health facilities) are included in counts but often represent smaller populations. Geographic and population-based HPSAs account for the vast majority of underserved population totals.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Hospital Quality Scatter Explorer</title>
      <link>https://www.chiply.dev/post-hospital-quality</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-hospital-quality</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>The federal government rates every US hospital on a 1-5 star scale across five quality domains. This post visualizes the full landscape of 5,400+ hospitals &amp;#x2013; where they cluster, how ownership t...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="hospitals">hospitals</span>&#xa0;<span class="quality">quality</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org27ea5d3" class="figure">
<p><img src="https://www.chiply.dev/images/hospital-quality-banner.jpeg" alt="hospital-quality-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
The federal government rates every US hospital on a 1-5 star scale across five quality domains.  This post visualizes the full landscape of 5,400+ hospitals &#x2013; where they cluster, how ownership type predicts quality, and where your state falls &#x2013; using the CMS Hospital Compare dataset.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
The federal government rates every US hospital on a 1–5 star scale across five quality domains: mortality, safety, readmissions, patient experience, and timely care. This post lets you explore the full landscape of 5,400+ hospitals — where they cluster, how ownership type (nonprofit, for-profit, government) predicts quality, and where your state falls. The headline finding: nonprofit hospitals have more 4–5 star ratings than for-profit hospitals, but the variation within each ownership type is enormous.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="hospitals">hospitals</span>&#xa0;<span class="quality">quality</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Every hospital in the United States that participates in Medicare is evaluated on the same set of quality metrics — and those evaluations are public. CMS publishes an overall 1–5 star rating for roughly 3,000 acute care hospitals, aggregating performance across five domains: mortality rates, safety, readmissions, patient experience, and timely/effective care delivery.
</p>

<p>
These ratings are controversial. Hospital trade associations regularly lobby against the methodology. Teaching hospitals complain their complex patient mix inflates apparent mortality. Large urban hospitals with many high-risk patients argue they're penalized relative to suburban hospitals with healthier populations. Some of these critiques are valid — the star rating system doesn't perfectly control for case mix — but the data still represents the most comprehensive public view into comparative hospital quality that exists.
</p>

<p>
This post visualizes the full distribution. 5,359 hospitals, 2,855 with valid overall star ratings, pulling from the <a href="https://data.cms.gov/provider-data/dataset/xubh-q36u">CMS Hospital General Information</a> dataset and <a href="https://data.cms.gov/provider-data/dataset/dgck-syfz">HCAHPS patient survey data</a>.
</p>
</div>
<div id="outline-container-methodology" class="outline-3">
<h3 id="methodology"><span class="section-number-3">3.1.</span> How the star rating is calculated&#xa0;&#xa0;&#xa0;<span class="tag"><span class="methodology">methodology</span></span></h3>
<div class="outline-text-3" id="text-methodology">
<p>
The overall star rating is a composite. CMS uses a latent variable model that groups hospitals into five tiers based on weighted performance across the five quality domains. The weights are roughly:
</p>

<ul class="org-ul">
<li><b>Mortality</b> (22%) — risk-adjusted death rates for conditions like heart failure, pneumonia, COPD</li>
<li><b>Safety of care</b> (22%) — healthcare-associated infections, complications, PSIs</li>
<li><b>Readmissions</b> (22%) — 30-day unplanned readmission rates</li>
<li><b>Patient experience</b> (22%) — HCAHPS survey: nurse/doctor communication, pain management, discharge info</li>
<li><b>Timely &amp; effective care</b> (12%) — door-to-balloon time, ED wait times, sepsis bundles</li>
</ul>

<p>
Hospitals need data on at least three domains to receive an overall rating. Critical access hospitals, psychiatric hospitals, and VA facilities are excluded.
</p>
</div>
</div>
</div>
<div id="outline-container-main-scatter" class="outline-2">
<h2 id="main-scatter"><span class="section-number-2">4.</span> The Main View: Patient Experience vs. Mortality&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="scatter">scatter</span></span></h2>
<div class="outline-text-2" id="text-main-scatter">
<p>
This scatter plots every hospital with both patient experience data and mortality quality data. The x-axis is the HCAHPS patient experience star rating (1–5); the y-axis shows what percent of a hospital's mortality measures were rated "better than national average." Dot size reflects the overall star rating. Color shows ownership type.
</p>

<div id="hq_scatter" class="plotly-plot"></div>

<p>
A few things stand out:
</p>

<ul class="org-ul">
<li><b>Patient experience and clinical quality don't always align.</b> Some hospitals score well on patient experience (4–5 stars for how nurses communicated) but poorly on mortality measures. The reverse is also common.</li>
<li><b>Nonprofit hospitals</b> (blue) dominate the upper-right quadrant — high experience, strong mortality performance.</li>
<li><b>For-profit hospitals</b> (orange) show more spread — some high performers, but a larger cluster at lower mortality quality.</li>
<li>The middle mass of hospitals — 2–3 star experience, 20–50% better-than-national mortality — represents the typical American community hospital.</li>
</ul>
</div>
<div id="outline-container-experience-vs-outcomes" class="outline-3">
<h3 id="experience-vs-outcomes"><span class="section-number-3">4.1.</span> Why patient experience and clinical quality diverge&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h3>
<div class="outline-text-3" id="text-experience-vs-outcomes">
<p>
The partial decoupling of experience scores from clinical outcomes has a straightforward explanation: they measure different things. HCAHPS captures whether your nurse explained your medications clearly and whether the room was quiet at night. Mortality rates capture whether patients with your condition died within 30 days.
</p>

<p>
Hospitals can invest in hospitality — single rooms, responsive call systems, well-trained patient-facing staff — without improving the clinical protocols that drive mortality outcomes. Some high-mortality hospitals have excellent bedside manner. Some hospitals with sterile, intimidating environments have exceptional clinical results.
</p>
</div>
</div>
</div>
<div id="outline-container-star-distribution" class="outline-2">
<h2 id="star-distribution"><span class="section-number-2">5.</span> Who Gets 5 Stars? Ownership and Quality&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="ownership">ownership</span></span></h2>
<div class="outline-text-2" id="text-star-distribution">
<div id="hq_star_dist" class="plotly-plot"></div>

<p>
The distribution of star ratings by ownership type reveals a clear pattern:
</p>

<ul class="org-ul">
<li><b>Nonprofit hospitals</b> are more likely to receive 3–4 stars and overrepresented at 5 stars relative to their share of total hospitals.</li>
<li><b>For-profit hospitals</b> are overrepresented at 1–2 stars, though they also have a meaningful presence at 4–5 stars.</li>
<li><b>Government hospitals</b> (state, county, federal non-VA facilities) cluster heavily at 2–3 stars, partly reflecting that many government hospitals serve high-acuity, underinsured populations in urban areas.</li>
</ul>

<p>
This pattern is consistent with research on hospital ownership and quality: for-profit hospitals tend to have higher margins but lower quality ratings, particularly on measures that don't directly affect revenue (HAIs, 30-day readmissions). Nonprofits, constrained to reinvest surplus into operations, tend to maintain higher quality baselines. But the within-group variation is enormous — plenty of 5-star for-profit hospitals and 1-star nonprofits exist.
</p>
</div>
</div>
<div id="outline-container-state-map" class="outline-2">
<h2 id="state-map"><span class="section-number-2">6.</span> The Geography of Hospital Quality&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="states">states</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-state-map">
<p>
Average hospital star ratings vary substantially by state. This map shows the mean overall rating among rated hospitals in each state.
</p>

<div id="hq_state_map" class="plotly-plot"></div>

<p>
The geographic pattern correlates with several factors: hospital market consolidation (monopoly hospital markets tend to have lower quality incentives), state Medicaid policy (expansions increase insured volumes and reduce financial stress), the presence of academic medical centers (which both depress ratings due to case mix and improve them through teaching excellence), and rural vs. urban mix.
</p>

<p>
States in the Mountain West and upper Midwest tend to perform well. States with highly consolidated markets or large proportions of safety-net hospitals tend to score lower.
</p>
</div>
</div>
<div id="outline-container-domain-comparison" class="outline-2">
<h2 id="domain-comparison"><span class="section-number-2">7.</span> Quality by Domain: Where Ownership Predicts Outcomes&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="domains">domains</span>&#xa0;<span class="ownership">ownership</span></span></h2>
<div class="outline-text-2" id="text-domain-comparison">
<p>
The star rating aggregates five domains. This chart breaks that out — for each major quality domain, what fraction of measures does each ownership type have rated "better than national"?
</p>

<div id="hq_domain_comparison" class="plotly-plot"></div>

<p>
The domain breakdown reveals where ownership effects are strongest:
</p>

<ul class="org-ul">
<li>On <b>mortality</b> and <b>safety</b>, nonprofits consistently outperform for-profits — these are the clinical quality measures that require systemic investment in infection control, care protocols, and staffing ratios.</li>
<li>On <b>readmissions</b>, the gap narrows — readmission reduction requires care coordination infrastructure that all ownership types have invested in, partly because CMS penalizes hospitals financially for excess readmissions.</li>
<li>Government hospitals underperform on most domains, again reflecting the challenging patient populations (uninsured, high-acuity, high-complexity) that many public hospitals disproportionately serve.</li>
</ul>
</div>
</div>
<div id="outline-container-quality-quadrant" class="outline-2">
<h2 id="quality-quadrant"><span class="section-number-2">8.</span> The Quality Landscape&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="quadrant">quadrant</span></span></h2>
<div class="outline-text-2" id="text-quality-quadrant">
<p>
This scatter plots hospitals on two independent quality axes: mortality performance (x) and safety performance (y), colored by overall star rating. The dashed lines separate hospitals above and below the 50% benchmark on each axis.
</p>

<div id="hq_quality_quadrant" class="plotly-plot"></div>

<p>
The upper-right quadrant — better than national on both mortality and safety — is dominated by 4–5 star hospitals. The lower-left quadrant — below benchmark on both — is dominated by 1–2 star hospitals. But there's substantial scatter in the middle bands. Some 3-star hospitals outperform on both; some 4-star hospitals have weakness on one axis.
</p>

<p>
The hospitals most worth scrutinizing are those in the off-diagonal quadrants: high mortality quality but poor safety (or vice versa). These represent systematic imbalances — a hospital excelling on one dimension while failing on another — that the aggregate star rating can obscure.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data comes from CMS Provider Data:
</p>

<ul class="org-ul">
<li><b>Hospital General Information</b> — <a href="https://data.cms.gov/provider-data/dataset/xubh-q36u">xubh-q36u</a>. 5,426 hospitals with ownership type, hospital type, overall star rating, and quality domain measure counts (better/no different/worse than national for mortality, safety, and readmission domains).</li>
<li><b>HCAHPS Patient Survey</b> — <a href="https://data.cms.gov/provider-data/dataset/dgck-syfz">dgck-syfz</a>. Filtered to <code>H_STAR_RATING</code> measure: the overall patient experience star rating (1–5) derived from the Hospital Consumer Assessment of Healthcare Providers and Systems survey.</li>
</ul>

<p>
<b>"Better than national"</b> means the hospital's performance on a specific measure (e.g., 30-day heart failure mortality rate) was statistically significantly better than the national average. Hospitals with too few cases to evaluate are excluded from that measure's count.
</p>

<p>
<b>Ownership simplification:</b>
</p>
<ul class="org-ul">
<li>Nonprofit: "Voluntary non-profit — Private", "Voluntary non-profit — Church", "Voluntary non-profit — Other"</li>
<li>For-Profit: "Proprietary"</li>
<li>Government: all "Government —" subtypes</li>
</ul>

<p>
<b>Not included:</b> Critical access hospitals, psychiatric hospitals, rehabilitation facilities, long-term care facilities. These use different payment and evaluation systems and are not rated on the 1–5 scale.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>What Medicare Pays for Your Surgery</title>
      <link>https://www.chiply.dev/post-medicare-pricing</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-medicare-pricing</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>When you check into a hospital for a hip replacement, the hospital generates a bill for $85,000 &amp;#x2013; and Medicare pays $12,000. This post uses FY2023 CMS data on 146,000 hospital-DRG combinations ...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="pricing">pricing</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org8206491" class="figure">
<p><img src="https://www.chiply.dev/images/medicare-pricing-banner.jpeg" alt="medicare-pricing-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
When you check into a hospital for a hip replacement, the hospital generates a bill for $85,000 &#x2013; and Medicare pays $12,000.  This post uses FY2023 CMS data on 146,000 hospital-DRG combinations to show where the gaps between billed charges and actual payments are widest, and why the number on your hospital bill has almost no relationship to what anyone actually pays.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Hospitals bill Medicare 3–7 times what Medicare actually pays. The same surgery — same DRG code, same procedure — can carry a billed charge ten times higher at one hospital than at another across state lines. This post uses FY2023 CMS data on 146,000 hospital-DRG combinations to show where the gaps are widest, which states inflate prices most, and why the number on your hospital bill has almost no relationship to what anyone actually pays.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="pricing">pricing</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
When you check into a hospital for a hip replacement, the hospital generates a bill. That bill might read $85,000. Medicare looks at the bill, ignores most of it, and pays $12,000. The hospital accepts $12,000 as full payment.
</p>

<p>
The $85,000 was never a real price. It came from a document called the chargemaster — a hospital's internal price list, often thousands of pages long, set unilaterally by the hospital and reviewed by almost no one. The chargemaster price is what gets billed to patients without insurance, occasionally what gets reported to regulators, and the starting point for negotiations with private insurers. It is, in the words of one health economist, "the most arbitrary and unreasonable prices in American commerce."
</p>

<p>
The ratio between chargemaster prices and what payers actually pay has grown for decades. In the 1980s, Medicare typically paid 70–80 cents on the dollar of billed charges. By 2023, that figure had fallen to roughly 20 cents on the dollar for the average inpatient procedure. Hospitals bill five times what they accept as payment. And the variation across hospitals and states is enormous.
</p>

<p>
Every chart on this page draws from the <a href="https://data.cms.gov/provider-summary-by-type-of-service/medicare-inpatient-hospitals/medicare-inpatient-hospitals-by-provider-and-service">CMS Medicare Inpatient Hospitals by Provider and Service</a> dataset (FY2023), covering 146,427 hospital-DRG combinations at 2,945 IPPS hospitals.
</p>
</div>
<div id="outline-container-drg-system" class="outline-3">
<h3 id="drg-system"><span class="section-number-3">3.1.</span> How the DRG system works&#xa0;&#xa0;&#xa0;<span class="tag"><span class="methodology">methodology</span></span></h3>
<div class="outline-text-3" id="text-drg-system">
<p>
Medicare doesn't pay hospitals by the procedure. It pays by something called a Diagnosis-Related Group (DRG) — a classification that groups together patients with similar diagnoses and expected resource use. When you're admitted for a hip replacement, you're assigned DRG 470. Medicare looks up the national base rate for DRG 470, adjusts it for the hospital's local wage index and teaching status, and writes a check. The hospital's billed charge is irrelevant.
</p>

<p>
This means the hospital's chargemaster number — what shows up on your Explanation of Benefits — is entirely disconnected from what Medicare pays. The DRG payment is fixed in advance. Billing $80,000 vs. $120,000 doesn't change the Medicare check.
</p>

<p>
So why do chargemaster prices keep rising? Because they function as anchors in private insurance negotiations. The higher the chargemaster, the higher the "discount" insurers can claim to have won, and the higher the benchmark for out-of-network billing.
</p>
</div>
</div>
</div>
<div id="outline-container-the-gap" class="outline-2">
<h2 id="the-gap"><span class="section-number-2">4.</span> The Gap: What Hospitals Bill vs. What Medicare Pays&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="billing">billing</span></span></h2>
<div class="outline-text-2" id="text-the-gap">
<p>
The chart below shows the 20 most common inpatient procedures in Medicare by total discharge volume. For each, the blue bar shows what Medicare paid on average; the orange bar shows what hospitals billed.
</p>

<div id="mp_top_drgs" class="plotly-plot"></div>

<p>
A few observations from the top procedures:
</p>

<ul class="org-ul">
<li><b>Septicemia and respiratory failure</b> (the top volume DRGs post-COVID) show billed charges 4–6× the Medicare payment.</li>
<li><b>Major joint replacement</b> (DRG 470 — hip and knee) is consistently the highest-volume elective procedure. Hospitals bill roughly $60,000–$80,000. Medicare pays around $14,000–$16,000.</li>
<li><b>Heart failure</b> and <b>pneumonia</b> show similar multiples.</li>
</ul>

<p>
None of these gaps are cost-based. Medicare's payment rates are calculated from a formula incorporating actual hospital costs, case complexity, and regional wage differences. The billed charges are set by each hospital's chargemaster with almost no public scrutiny.
</p>
</div>
</div>
<div id="outline-container-hospital-spread" class="outline-2">
<h2 id="hospital-spread"><span class="section-number-2">5.</span> Same Surgery, 10 Different Prices&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="hospitalVariation">hospitalVariation</span></span></h2>
<div class="outline-text-2" id="text-hospital-spread">
<p>
The national averages mask even more extreme variation at the hospital level. This scatter plot shows every IPPS hospital that billed Medicare for DRG 470 (Major Joint Replacement of the Lower Extremity — hip and knee replacements, the single highest-volume elective surgery in Medicare). Each dot is one hospital.
</p>

<div id="mp_hospital_spread" class="plotly-plot"></div>

<p>
The dashed reference lines show where billed charges are 1×, 3×, 5×, and 10× the Medicare payment. Most hospitals cluster in the 3–5× band. But a significant tail extends beyond 10×. A hospital billing $200,000 for a knee replacement gets the same Medicare check as one billing $30,000 for the identical procedure.
</p>

<p>
This matters because:
</p>

<ol class="org-ol">
<li><b>Uninsured patients</b> are often billed the chargemaster rate, though most hospitals have charity care or negotiated payment plans.</li>
<li><b>Out-of-network billing</b> frequently starts from the chargemaster rate, which is why surprise billing can be catastrophic.</li>
<li><b>Private insurance negotiations</b> use chargemaster rates as anchors, even if the final negotiated rate is much lower.</li>
</ol>
</div>
<div id="outline-container-why-variation" class="outline-3">
<h3 id="why-variation"><span class="section-number-3">5.1.</span> Why the variation exists&#xa0;&#xa0;&#xa0;<span class="tag"><span class="mechanisms">mechanisms</span></span></h3>
<div class="outline-text-3" id="text-why-variation">
<p>
The spread in billed charges isn't primarily explained by cost differences. Studies controlling for patient complexity, local wages, and teaching status find that the main driver of chargemaster inflation is market power. Hospitals that face less competition — regional monopolies, dominant systems in consolidated markets — charge more. The chargemaster is a negotiating weapon.
</p>

<p>
The Hospital Price Transparency Rule (effective January 2021) requires hospitals to publish their chargemaster rates and payer-specific negotiated rates. Compliance has been uneven, but the data is beginning to reveal just how wide the gaps are across insurers for the same DRG at the same hospital.
</p>
</div>
</div>
</div>
<div id="outline-container-service-lines" class="outline-2">
<h2 id="service-lines"><span class="section-number-2">6.</span> Where the Markups Are Highest&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="serviceLines">serviceLines</span></span></h2>
<div class="outline-text-2" id="text-service-lines">
<p>
The billed-to-paid gap varies substantially by medical service category. Surgical specialties with expensive equipment — orthopedics, cardiac surgery, neurosurgery — tend to have higher chargemaster rates. Medical services (infectious disease, mental health) tend to have lower ratios.
</p>

<div id="mp_service_lines" class="plotly-plot"></div>

<p>
The ratio at the right of each bar is how many times the billed charge exceeds what Medicare pays. Musculoskeletal procedures (joint replacements, spine surgery) show some of the highest ratios. Mental health and substance use services show lower ratios — in part because psychiatric admissions are reimbursed under a separate payment system (IPPS psychiatric add-on rates) and the base DRG payments are lower.
</p>
</div>
</div>
<div id="outline-container-state-ratios" class="outline-2">
<h2 id="state-ratios"><span class="section-number-2">7.</span> The Geography of Price Inflation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="states">states</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-state-ratios">
<p>
Charge-to-payment ratios vary dramatically by state. This choropleth shows the discharge-weighted average ratio of billed charge to Medicare payment at the state level.
</p>

<div id="mp_state_ratios" class="plotly-plot"></div>

<p>
States with higher ratios tend to have more consolidated hospital markets (fewer competitors per metropolitan area), higher costs of living, or regulatory environments that historically permitted higher chargemaster growth. The variation isn't purely a function of what Medicare pays — Medicare's geographic adjustments account for local wage differences — it reflects genuine differences in how aggressively hospital systems set their internal prices.
</p>
</div>
</div>
<div id="outline-container-ratio-distribution" class="outline-2">
<h2 id="ratio-distribution"><span class="section-number-2">8.</span> How Extreme Is Typical?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="distribution">distribution</span></span></h2>
<div class="outline-text-2" id="text-ratio-distribution">
<p>
The histogram below shows the distribution of charge-to-payment ratios across all 534 DRGs in the dataset (one data point per DRG, discharge-weighted nationally).
</p>

<div id="mp_ratio_histogram" class="plotly-plot"></div>

<p>
The distribution is right-skewed: most DRGs cluster between 3× and 6×, but a significant tail extends past 10×. The handful of DRGs with very high ratios tend to involve expensive devices (implants, cardiac devices) where hospitals have traditionally padded charges most aggressively.
</p>

<p>
There are also DRGs with ratios near 1× — these are typically short-stay or low-complexity cases where hospital billing practices have historically been more constrained, often because patients are more likely to be self-pay and the chargemaster rate is more likely to be scrutinized.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data in this post comes from the <a href="https://data.cms.gov/provider-summary-by-type-of-service/medicare-inpatient-hospitals/medicare-inpatient-hospitals-by-provider-and-service">CMS Medicare Inpatient Hospitals by Provider and Service</a> dataset (FY2023, released May 2025).
</p>

<ul class="org-ul">
<li><b>Scope:</b> Inpatient Prospective Payment System (IPPS) hospitals only — the approximately 3,000 acute care hospitals reimbursed under Medicare's DRG-based system. Critical access hospitals, psychiatric facilities, rehabilitation hospitals, and long-term care hospitals are excluded (they have different payment methodologies).</li>
<li><b>Fields used:</b> <code>Avg_Submtd_Cvrd_Chrg</code> (average billed charge), <code>Avg_Mdcr_Pymt_Amt</code> (average Medicare payment), <code>Tot_Dschrgs</code> (total discharges), plus provider and DRG identifiers.</li>
<li><b>"Charge-to-payment ratio":</b> <code>Avg_Submtd_Cvrd_Chrg</code> ÷ <code>Avg_Mdcr_Pymt_Amt</code>. All averages in multi-hospital aggregations are discharge-weighted.</li>
<li><b>Service line categories:</b> Derived from DRG code ranges corresponding to Medicare Severity DRG Major Diagnostic Categories (MDCs).</li>
<li><b>Geographic filtering:</b> Restricted to 50 states plus DC; territories excluded from the state choropleth.</li>
</ul>

<p>
The gap between <code>Avg_Submtd_Cvrd_Chrg</code> and <code>Avg_Mdcr_Pymt_Amt</code> is not pure profit — hospitals have costs, and the chargemaster price doesn't represent revenue. What the gap does represent is the difference between a number hospitals make up and a number Medicare computes from actual cost and utilization data. The chargemaster is theater; the Medicare payment is closer to economics.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>The Geography of Medicare Spending</title>
      <link>https://www.chiply.dev/post-medicare-spending-geography</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-medicare-spending-geography</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Medicare is a single national program with a single set of rules, yet what it spends per beneficiary varies dramatically by geography. This post uses CMS standardized spending data to explore why high...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="geography">geography</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgeebbccf" class="figure">
<p><img src="https://www.chiply.dev/images/medicare-spending-geography-banner.jpeg" alt="medicare-spending-geography-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Medicare is a single national program with a single set of rules, yet what it spends per beneficiary varies dramatically by geography.  This post uses CMS standardized spending data to explore why higher spending predicts neither better outcomes nor lower readmission rates.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Medicare standardized spending per beneficiary varies by more than 2.5× between the cheapest and most expensive states — and by far more at the county level. Yet higher spending predicts neither better outcomes nor lower readmission rates. The data consistently shows weak-to-zero correlation between what Medicare spends and how well patients do.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cms">cms</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Medicare is a single national insurance program with a single set of rules. Yet what it spends on a beneficiary in Manhattan, Kansas is dramatically different from what it spends in Manhattan, New York. Some of this is expected: prices are higher in urban markets, specialists charge more, and illness is more complex in some populations. But when you standardize for these factors — removing the geographic payment rate differences so you're comparing actual resource use — the variation persists and remains enormous.
</p>

<p>
The CMS Geographic Variation Public Use File captures this. Every year, CMS publishes per-beneficiary Medicare spending broken down by geography (national, state, county) and service type, standardized to remove local wage and price differences. The result is a dataset that lets you ask: <i>ignoring local prices, how much Medicare care does this area actually consume?</i>
</p>

<p>
This post explores that question using the 2023 data (latest available), with trend context going back to 2014.
</p>
</div>
</div>
<div id="outline-container-state-choropleth" class="outline-2">
<h2 id="state-choropleth"><span class="section-number-2">4.</span> Spending by State&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="choropleth">choropleth</span></span></h2>
<div class="outline-text-2" id="text-state-choropleth">
<p>
Standardized Medicare spending per fee-for-service beneficiary in 2023, by state. Darker blue = more spending.
</p>

<div id="mg_state_choropleth" class="plotly-plot"></div>

<p>
A few things jump out immediately:
</p>

<ul class="org-ul">
<li><b>The South spends more.</b> States like Louisiana, Mississippi, Alabama, and Florida consistently appear among the highest spenders. The Northeast and Mountain West tend toward the lower end.</li>
<li><b>Utah is a persistent outlier on the low end.</b> Utah has among the lowest Medicare spending in the country, a pattern that has held for decades and is associated with a younger average age, lower rates of chronic disease, and a health system historically oriented around the LDS hospital network.</li>
<li><b>The range is substantial.</b> The difference between the lowest- and highest-spending states represents thousands of dollars per beneficiary per year — real money, multiplied across millions of patients.</li>
</ul>
</div>
</div>
<div id="outline-container-scatter" class="outline-2">
<h2 id="scatter"><span class="section-number-2">5.</span> Does Higher Spending Mean Better Care?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="scatter">scatter</span>&#xa0;<span class="outcomes">outcomes</span></span></h2>
<div class="outline-text-2" id="text-scatter">
<p>
The obvious question is whether higher-spending states achieve better outcomes. The answer, consistently, is no. This scatter plots standardized spending against the 30-day hospital readmission rate — one of Medicare's primary quality metrics.
</p>

<div id="mg_scatter" class="plotly-plot"></div>

<p>
The correlation is near zero. High-spending states like Louisiana and New York do not have systematically lower readmission rates than low-spending states like Hawaii or Utah. If anything, the relationship trends slightly positive: states that spend more tend to have slightly <i>higher</i> readmission rates, though the relationship is too weak to be meaningful.
</p>

<p>
This is one of the most replicated findings in health economics, associated most prominently with the Dartmouth Atlas research: more care does not automatically produce better outcomes. Much of the geographic variation in Medicare spending reflects differences in practice patterns, not patient needs.
</p>
</div>
</div>
<div id="outline-container-trends" class="outline-2">
<h2 id="trends"><span class="section-number-2">6.</span> Spending Trends: 2014–2023&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="trends">trends</span></span></h2>
<div class="outline-text-2" id="text-trends">
<p>
Medicare per-beneficiary spending has grown over the past decade, but not uniformly across service types. This chart breaks down standardized national spending by category.
</p>

<div id="mg_trend" class="plotly-plot"></div>

<p>
Several patterns stand out:
</p>

<ul class="org-ul">
<li><b>Inpatient spending has declined as a share of total spending.</b> Hospital stays are expensive, and the secular trend has been toward shorter stays and more outpatient care.</li>
<li><b>COVID-19 visibly disrupted 2020 spending.</b> Total spending dipped as elective procedures were deferred and patients avoided hospitals. Post-acute and home health care were particularly affected.</li>
<li><b>Recovery was uneven.</b> Some service categories rebounded quickly (outpatient, E&amp;M); others recovered more slowly.</li>
<li><b>Total spending resumed growth after 2020.</b> By 2022–2023, total standardized spending exceeded pre-COVID levels.</li>
</ul>
</div>
</div>
<div id="outline-container-topbottom" class="outline-2">
<h2 id="topbottom"><span class="section-number-2">7.</span> County Extremes: Highest and Lowest Spending&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="counties">counties</span></span></h2>
<div class="outline-text-2" id="text-topbottom">
<p>
State averages mask enormous county-level variation. These are the 20 counties with the highest and lowest standardized Medicare spending in 2023, restricted to counties with at least 1,000 fee-for-service beneficiaries to reduce small-sample noise.
</p>

<div id="mg_topbottom" class="plotly-plot"></div>

<p>
The spread is striking. The highest-spending counties can exceed $20,000 per beneficiary per year in standardized spending — more than twice what the lowest-spending counties spend on the same standardized basis. The geographic clustering is notable too: extreme-high-spending counties tend to cluster in Louisiana, south Texas, and urban Florida. Extreme-low-spending counties tend to cluster in Utah, rural Nebraska, and parts of the Pacific Northwest.
</p>
</div>
</div>
<div id="outline-container-analysis" class="outline-2">
<h2 id="analysis"><span class="section-number-2">8.</span> Why Does Variation Persist?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h2>
<div class="outline-text-2" id="text-analysis">
<p>
If higher spending doesn't produce better outcomes, why does the variation persist? Several factors:
</p>

<p>
<b>Practice pattern variation</b> is the most heavily studied explanation. Physicians in high-spending areas order more tests, refer more, hospitalize more — not because patients are sicker, but because local norms evolved that way. Once established, these patterns are sticky: physicians trained in high-intensity environments carry those habits forward.
</p>

<p>
<b>Supplier-induced demand</b> plays a role where supply of specialists and hospitals is high. More available beds tend to be filled; more available specialists tend to generate more referrals. The healthcare market doesn't clear like a normal market because patients don't pay marginal costs and don't comparison-shop.
</p>

<p>
<b>Chronic disease burden</b> explains some (but not all) variation even after standardization. The South has higher rates of diabetes, obesity, and hypertension — chronic conditions that generate ongoing Medicare spending. But the Dartmouth research suggests this explains only a fraction of the variation; the rest is attributable to intensity of treatment for equivalent patients.
</p>

<p>
<b>Fee-for-service incentives</b> reward volume, not value. A system that pays per procedure will always face pressure toward more procedures in areas where practice patterns allow it.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data comes from the <a href="https://data.cms.gov/summary-statistics-on-use-and-payments/medicare-geographic-comparisons/medicare-geographic-variation-by-national-state-county">CMS Medicare Geographic Variation by National, State &amp; County</a> dataset, 2014–2023 releases.
</p>

<ul class="org-ul">
<li><b>Population:</b> Original Medicare fee-for-service (FFS) beneficiaries aged 65+. Medicare Advantage enrollees are excluded because their claims are not reported to CMS in the same format.</li>
<li><b>Standardized spending:</b> CMS standardizes payment rates to remove geographic differences in input prices (wage index, rural adjustments, graduate medical education payments). Standardized spending reflects actual resource use — how much care, not how much it costs locally. This is what allows fair geographic comparison.</li>
<li><b>Readmission rate:</b> Percentage of inpatient stays followed by an unplanned readmission within 30 days. Includes all-cause readmissions.</li>
<li><b>County minimum:</b> The top/bottom chart filters to counties with ≥1,000 FFS beneficiaries to reduce noise from very small populations with erratic per-capita figures.</li>
<li><b>Age level:</b> All figures use the "All" age stratum (65 and over combined).</li>
</ul>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>America&apos;s Mental Health Crisis, By ZIP Code</title>
      <link>https://www.chiply.dev/post-mental-health-crisis</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-mental-health-crisis</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Mental health is the defining public health story of the 2020s. This post maps both sides of the equation &amp;#x2013; need and supply &amp;#x2013; using CDC PLACES ZIP code-level prevalence data and HRSA men...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="publicHealth">publicHealth</span>&#xa0;<span class="mentalHealth">mentalHealth</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org79cfee4" class="figure">
<p><img src="https://www.chiply.dev/images/mental-health-crisis-banner.jpeg" alt="mental-health-crisis-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Mental health is the defining public health story of the 2020s.  This post maps both sides of the equation &#x2013; need and supply &#x2013; using CDC PLACES ZIP code-level prevalence data and HRSA mental health shortage area designations, revealing how the areas with the highest need are often those with the fewest providers.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
Nearly 1-in-5 American adults — about 19% on average — reports frequent mental distress (14 or more poor mental health days per month). But the burden is not evenly distributed: ZIP codes in rural Appalachia, the Deep South, and parts of the mountain West reach 30%+ prevalence, while affluent coastal ZIP codes can fall below 10%. The cruel overlap: the areas with the highest need are often those with the fewest mental health providers.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction :cdc:mental-health&#xa0;&#xa0;&#xa0;<span class="tag"><span class="maps">maps</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Mental health is the defining public health story of the 2020s. Rates of depression, anxiety, and poor mental health outcomes rose sharply during and after the COVID-19 pandemic, and they have not fully recovered. Meanwhile, the US mental health workforce has failed to keep pace with demand: 10,975 Mental Health Health Professional Shortage Areas (HPSAs) are currently designated by the federal government, representing communities where the supply of mental health providers falls critically short of need.
</p>

<p>
This post maps both sides of that equation — need and supply — using two public datasets:
</p>

<ul class="org-ul">
<li><b>CDC PLACES 2023</b>: ZIP code-level prevalence of frequent mental distress (14+ poor mental health days/month) and diagnosed depression, covering ~30,000 ZIP codes across the US.</li>
<li><b>HRSA Mental Health HPSA Designations</b>: Every designated mental health shortage area in the US, with shortage scores and estimated underserved populations.</li>
</ul>

<p>
The combination reveals where the crisis is most acute: not just places with high mental health burden, but places where that high burden coincides with a shortage of the providers who could address it.
</p>
</div>
</div>
<div id="outline-container-state-choropleth" class="outline-2">
<h2 id="state-choropleth"><span class="section-number-2">4.</span> Where Mental Health Burden Is Highest&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="choropleth">choropleth</span></span></h2>
<div class="outline-text-2" id="text-state-choropleth">
<p>
Population-weighted prevalence of frequent mental distress by state. Darker purple = higher burden.
</p>

<div id="mh_state_burden" class="plotly-plot"></div>

<p>
The geographic pattern is striking and consistent:
</p>

<ul class="org-ul">
<li><b>West Virginia leads the country</b> in mental health burden, followed by Kentucky, Mississippi, Oklahoma, and Arkansas. These states also tend to have lower incomes, higher rates of chronic disease, and more limited access to care.</li>
<li><b>The Mountain West</b> shows elevated rates — Montana, Wyoming, Nevada — often attributed to rural isolation, high rates of substance use, and limited specialty care.</li>
<li><b>Utah and Hawaii</b> consistently appear among the lower-burden states, though Hawaii's data reflects its unique demographics.</li>
</ul>

<p>
The spread across states is substantial: from roughly 12–14% in the lowest-burden states to 22–25% in the highest. But state averages mask enormous within-state variation — ZIP codes in rural Appalachia can reach 35% while neighboring suburban ZIPs are at 12%.
</p>
</div>
</div>
<div id="outline-container-shortage-scatter" class="outline-2">
<h2 id="shortage-scatter"><span class="section-number-2">5.</span> The Supply Problem: Need vs. Provider Availability&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="scatter">scatter</span>&#xa0;<span class="hrsa">hrsa</span></span></h2>
<div class="outline-text-2" id="text-shortage-scatter">
<p>
Each point is a state. X axis = number of designated Mental Health HPSAs. Y axis = % adults with frequent mental distress. Bubble size = estimated underserved population. Color = depression prevalence.
</p>

<div id="mh_shortage_scatter" class="plotly-plot"></div>

<p>
The upper-right quadrant is where the crisis converges: states with both high mental health burden and many shortage areas. West Virginia, Mississippi, Oklahoma, and parts of Appalachia cluster here. These states have both the greatest per-capita need and the most designated shortage areas — meaning the federal government has explicitly recognized that provider supply is inadequate, yet the burden remains high.
</p>

<p>
States in the lower-left — lower burden, fewer shortage areas — tend to be wealthier states with denser urban populations and more accessible specialty care.
</p>

<p>
The relationship isn't perfectly linear. Some states have many HPSAs but moderate burden (reflecting expansive geography with sparse population). Others have high burden but fewer formally designated shortage areas, possibly because the shortage is too diffuse to trigger formal HPSA designation. The core finding holds: states where people need mental health care most are systematically more likely to lack the providers to deliver it.
</p>
</div>
</div>
<div id="outline-container-zip-distribution" class="outline-2">
<h2 id="zip-distribution"><span class="section-number-2">6.</span> The Geography of Distress: ZIP-Level Distribution&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="histogram">histogram</span></span></h2>
<div class="outline-text-2" id="text-zip-distribution">
<p>
This histogram shows the distribution of frequent mental distress prevalence across ~30,000 US ZIP codes.
</p>

<div id="mh_burden_hist" class="plotly-plot"></div>

<p>
The distribution is right-skewed: most ZIP codes cluster around the national median (~19%), but a long tail extends toward ZIP codes where 30–35% of adults report frequent mental distress. That tail is not random — it is concentrated in specific geographies where structural disadvantage (poverty, unemployment, chronic disease burden, social isolation) compounds the mental health burden.
</p>

<p>
The spread also reflects real differences in mental health risk. The ZIP code you live in is a significant predictor of mental health outcomes — not just because of selection effects (mentally healthier people can afford to live in better-resourced areas), but because neighborhood factors directly affect mental health through social networks, green space, air quality, economic security, and access to care.
</p>
</div>
</div>
<div id="outline-container-rankings" class="outline-2">
<h2 id="rankings"><span class="section-number-2">7.</span> Highest-Burden States&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="rankings">rankings</span></span></h2>
<div class="outline-text-2" id="text-rankings">
<p>
The 20 states with the highest rates of frequent mental distress, with mental health shortage designations as context.
</p>

<div id="mh_supply_burden" class="plotly-plot"></div>

<p>
Bar color reflects the number of mental health shortage areas — darker orange-red means more shortage areas. The compounding of high burden and high shortage is visible: states like West Virginia and Mississippi sit at the top of both rankings.
</p>

<p>
Several patterns stand out:
</p>

<ul class="org-ul">
<li><b>Appalachian and Southern states</b> dominate the high-burden list. The intersection of rural isolation, limited economic opportunity, higher rates of chronic pain (a major risk factor for depression), and restricted access to mental health care creates a concentrated geographic crisis.</li>
<li><b>States with high burden don't always have the most HPSAs.</b> Some states are rurally expansive but have few formal HPSA designations because the shortage exists everywhere, making designation less administratively useful. This likely undercounts the true access gap.</li>
<li><b>Urban states</b> with high overall populations (Texas, California) appear in the middle of the burden distribution despite having many shortage areas — because their shortage areas are geographically concentrated while urban populations have better access.</li>
</ul>
</div>
</div>
<div id="outline-container-analysis" class="outline-2">
<h2 id="analysis"><span class="section-number-2">8.</span> What Drives the Geographic Pattern&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h2>
<div class="outline-text-2" id="text-analysis">
<p>
The geographic clustering of mental health burden is not accidental. Several structural factors converge:
</p>

<p>
<b>Economic distress</b> is the most consistently replicated predictor of population mental health. Counties with high rates of unemployment, poverty, and economic decline show elevated rates of depression, anxiety, and suicide. The collapse of coal and manufacturing in Appalachia, the persistent poverty of the Mississippi Delta, and the economic precarity of rural communities broadly all show up in the data.
</p>

<p>
<b>Social isolation</b> matters independently of economics. Rural communities have thinner social networks, higher rates of geographic mobility that disrupts relationships, and less community infrastructure (fewer churches, fewer civic organizations, fewer informal gathering places) than urban areas. Isolation is a direct mental health risk factor.
</p>

<p>
<b>Chronic pain and physical health</b> are underrecognized drivers of mental health burden. States with high rates of musculoskeletal disease, opioid-related injury, and physical disability — often the same states with high mental health burden — see elevated depression and anxiety as consequences of chronic pain and disability.
</p>

<p>
<b>Provider supply</b> creates a reinforcing cycle. Where mental health providers are scarce, people don't get diagnosed and treated — so the burden persists and worsens. Where providers are abundant, mild-to-moderate conditions get treated early, reducing the burden that shows up in population surveys. Geographic variation in care supply is both a cause and a consequence of the mental health crisis.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">9.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
<b>Mental health burden data:</b> <a href="https://data.cdc.gov/resource/qnzd-25i4.json">CDC PLACES 2023</a> — ZIP code-level crude prevalence estimates for frequent mental distress (MHLTH: 14+ poor mental health days/month) and diagnosed depression (DEPRESSION). ~30,000 ZCTA geographies. Data source: Behavioral Risk Factor Surveillance System (BRFSS).
</p>

<ul class="org-ul">
<li>State averages are population-weighted means of ZIP-level estimates, using total ZIP population as weights.</li>
<li>ZIP-to-state mapping uses the 2020 Census ZCTA-to-county relationship file, crosswalked via state FIPS codes.</li>
</ul>

<p>
<b>HRSA Mental Health HPSA data:</b> <a href="https://data.hrsa.gov/DataDownload/DD_Files/BCD_HPSA_FCT_DET_MH.csv">HRSA HPSA Designation File — Mental Health</a> — all currently designated Mental Health HPSAs in the US (10,975 designations). HPSA designations are assigned by HRSA and reviewed periodically; they indicate areas, populations, or facilities where mental health provider supply is insufficient to meet demand.
</p>

<ul class="org-ul">
<li>State aggregates: count of HPSA designations, population-weighted average HPSA score (0–25), and sum of estimated underserved population.</li>
<li>HPSA score (0–25) reflects severity of shortage: higher scores = worse shortage.</li>
</ul>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>The Three Waves: Visualizing America&apos;s Opioid Epidemic with CDC Data</title>
      <link>https://www.chiply.dev/post-opioid-waves</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-opioid-waves</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>In 1999, roughly 8,000 Americans died from opioid overdoses. By 2023, that figure was approaching 80,000 &amp;#x2013; a tenfold increase in a single generation. This post uses CDC Vital Statistics data to...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="publicHealth">publicHealth</span>&#xa0;<span class="opioids">opioids</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgeb98f8a" class="figure">
<p><img src="https://www.chiply.dev/images/opioid-waves-banner.jpeg" alt="opioid-waves-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
In 1999, roughly 8,000 Americans died from opioid overdoses.  By 2023, that figure was approaching 80,000 &#x2013; a tenfold increase in a single generation.  This post uses CDC Vital Statistics data to trace the three distinct waves of the epidemic: prescription painkillers, heroin, and synthetic fentanyl &#x2013; each deadlier than the last.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
The U.S. opioid epidemic has unfolded in three distinct waves since the late 1990s — each deadlier than the last. This post uses live CDC data to trace that arc: from a flood of prescription painkillers, to a heroin surge, to the synthetic fentanyl era that now kills ~75,000 Americans a year. The drug itself changed, the source changed, and the geography changed. The death toll never stopped climbing.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="opioids">opioids</span>&#xa0;<span class="publicHealth">publicHealth</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="cdc">cdc</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
In 1999, roughly 8,000 Americans died from drug overdoses involving opioids. By 2023, that figure was approaching 80,000 — a tenfold increase in a single generation. Drug overdose has become the leading cause of accidental death in the United States, surpassing car crashes. Yet unlike a natural disaster, this catastrophe unfolded in slow motion, one prescription pad, one syringe, one fentanyl-laced pill at a time.
</p>

<p>
What makes the opioid crisis distinctive is that it mutated. Three separate epidemics, each triggered by a different drug, have layered on top of one another over 25 years. Understanding the mechanics of that mutation — and seeing it in data — is essential for anyone trying to understand where it goes next.
</p>

<p>
All charts on this page draw from the CDC's <a href="https://data.cdc.gov/resource/xkb8-kh2a.json">Vital Statistics Rapid Release (VSRR)</a> dataset, which tracks provisional drug overdose death counts monthly across all U.S. states. The numbers use 12-month rolling totals, which smooth out seasonal noise and reporting lags.
</p>
</div>
<div id="outline-container-methodology" class="outline-3">
<h3 id="methodology"><span class="section-number-3">3.1.</span> A note on the numbers&#xa0;&#xa0;&#xa0;<span class="tag"><span class="methodology">methodology</span></span></h3>
<div class="outline-text-3" id="text-methodology">
<p>
These are <i>provisional</i> death counts — the CDC publishes them 6-8 months after the fact as medical examiners complete their work. Final counts are typically slightly higher once all deaths are coded. The 12-month rolling total means each data point represents deaths over the prior 12 months, giving a stable trend line rather than month-to-month spikes.
</p>
</div>
</div>
</div>
<div id="outline-container-three-waves" class="outline-2">
<h2 id="three-waves"><span class="section-number-2">4.</span> The Three Waves&#xa0;&#xa0;&#xa0;<span class="tag"><span class="opioids">opioids</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="waves">waves</span></span></h2>
<div class="outline-text-2" id="text-three-waves">
<p>
The epidemic's shape is most visible in a single chart: overdose deaths broken out by substance, plotted as 12-month rolling totals from 2015 to 2025. Three inflection points define three eras.
</p>

<div id="ow_three_waves" class="plotly-plot"></div>

<p>
The shaded zones mark the dominant wave of each period, though in practice the waves overlap considerably. Wave 3 (fentanyl) didn't <i>replace</i> Wave 2 (heroin) — it subsumed it. Today, most "heroin" deaths involve fentanyl; the distinction between the two has all but collapsed in the drug supply.
</p>

<p>
The chart also reveals something important: total overdose deaths (the red line) barely paused, even as the underlying substances shifted. Each time one drug supply was disrupted or restricted, another took its place — usually more dangerous than the last.
</p>
</div>
</div>
<div id="outline-container-wave-one" class="outline-2">
<h2 id="wave-one"><span class="section-number-2">5.</span> Wave 1: The Prescription Flood&#xa0;&#xa0;&#xa0;<span class="tag"><span class="opioids">opioids</span>&#xa0;<span class="prescriptionOpioids">prescriptionOpioids</span>&#xa0;<span class="wave1">wave1</span></span></h2>
<div class="outline-text-2" id="text-wave-one">
<p>
The first wave was a pharmaceutical phenomenon. Starting in the late 1990s, OxyContin and other extended-release opioids were aggressively marketed to physicians as safe long-term pain treatments. Purdue Pharma's sales force specifically targeted high-volume prescribers in rural areas with high rates of physical labor injuries — Appalachia, the rural South, the Mountain West.
</p>

<p>
The strategy worked: by 2010, U.S. physicians were prescribing enough opioids to medicate every American adult around the clock for a month. The "natural and semi-synthetic opioids" line in the chart above — the blue line — represents this wave: hydrocodone, oxycodone, codeine.
</p>
</div>
<div id="outline-container-prescribing-machine" class="outline-3">
<h3 id="prescribing-machine"><span class="section-number-3">5.1.</span> The prescribing machine&#xa0;&#xa0;&#xa0;<span class="tag"><span class="prescribers">prescribers</span>&#xa0;<span class="geography">geography</span></span></h3>
<div class="outline-text-3" id="text-prescribing-machine">
<p>
The CDC tracked prescribing rates at the county and ZIP code level during this period. In some rural counties in West Virginia, Tennessee, and Alabama, opioid prescriptions outnumbered residents — more than one prescription per person per year. Pain clinics ("pill mills") sprang up in states with lax oversight, with patients driving hours to obtain prescriptions that were then resold locally.
</p>

<p>
The regulatory response came in 2010-2012: the FDA reformulated OxyContin to make it abuse-resistant, and the DEA began clamping down on pill mills. Prescribing rates started to fall. But by then, a generation of users had developed severe physical dependence — and they needed something.
</p>
</div>
</div>
</div>
<div id="outline-container-wave-two" class="outline-2">
<h2 id="wave-two"><span class="section-number-2">6.</span> Wave 2: The Heroin Bridge&#xa0;&#xa0;&#xa0;<span class="tag"><span class="opioids">opioids</span>&#xa0;<span class="heroin">heroin</span>&#xa0;<span class="wave2">wave2</span></span></h2>
<div class="outline-text-2" id="text-wave-two">
<p>
The second wave was the market's answer to the first. When prescription opioids became harder to obtain and their street price rose, heroin was cheaper, more available, and produced by the same pharmacological mechanism. Mexican cartels recognized the demand and flooded the market.
</p>

<p>
The heroin epidemic looked different from Wave 1. Where prescription opioids had been predominantly a rural and suburban phenomenon, heroin spread into every demographic and geography. Overdose deaths climbed steeply from 2010 through 2016-2017.
</p>

<p>
Then the drug supply shifted again.
</p>
</div>
</div>
<div id="outline-container-wave-three" class="outline-2">
<h2 id="wave-three"><span class="section-number-2">7.</span> Wave 3: Fentanyl and the Poisoned Supply&#xa0;&#xa0;&#xa0;<span class="tag"><span class="opioids">opioids</span>&#xa0;<span class="fentanyl">fentanyl</span>&#xa0;<span class="wave3">wave3</span>&#xa0;<span class="synthetic">synthetic</span></span></h2>
<div class="outline-text-2" id="text-wave-three">
<p>
Synthetic opioids — primarily illicitly manufactured fentanyl — are now the dominant driver of overdose deaths. The following chart isolates the heroin-to-fentanyl transition, which represents the most consequential shift in the drug supply in the epidemic's history.
</p>

<div id="ow_fentanyl_crossover" class="plotly-plot"></div>
</div>
<div id="outline-container-why-fentanyl" class="outline-3">
<h3 id="why-fentanyl"><span class="section-number-3">7.1.</span> Why fentanyl?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="fentanyl">fentanyl</span>&#xa0;<span class="supplyChain">supplyChain</span></span></h3>
<div class="outline-text-3" id="text-why-fentanyl">
<p>
Fentanyl's economics are brutal: it is 50-100x more potent than morphine, meaning a kilogram of fentanyl replaces many kilograms of heroin for a fraction of the transportation risk. Synthesized cheaply in China and Mexico from precursor chemicals, it can be pressed into counterfeit pills, mixed into heroin, or sold on its own. A $1 worth of fentanyl can be lethal.
</p>

<p>
The shift from heroin to fentanyl wasn't a choice that drug users made — it was imposed on them. Surveys of people who use drugs consistently show most would prefer a known-potency supply. Fentanyl's variable concentration (often unevenly distributed within a single batch) makes every use a potential fatal overdose. There is no antidote tolerance, no way to test the dose.
</p>

<p>
By 2023, the CDC attributes more than 73% of all drug overdose deaths to synthetic opioids — a category that barely registered in 2013.
</p>
</div>
</div>
</div>
<div id="outline-container-drug-breakdown" class="outline-2">
<h2 id="drug-breakdown"><span class="section-number-2">8.</span> Deaths by Substance, Year by Year&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="opioids">opioids</span>&#xa0;<span class="breakdown">breakdown</span></span></h2>
<div class="outline-text-2" id="text-drug-breakdown">
<p>
Seeing the raw composition by year makes the three-wave structure concrete. The bar chart below shows annual death counts for each major substance category, using December 12-month-ending data as the annual snapshot.
</p>

<div id="ow_drug_breakdown" class="plotly-plot"></div>

<p>
A few features stand out:
</p>

<ul class="org-ul">
<li><b>Prescription opioids (blue) plateau and very slowly decline</b> after 2016 as prescribing restrictions tightened — but they never return to pre-epidemic levels. Millions remained dependent, now often obtaining diverted pills or switching to street drugs.</li>

<li><b>Heroin (green) peaks around 2016-2017</b> and then falls sharply — not because fewer people were using opioids, but because the drug supply was adulterated with fentanyl. Deaths attributed to "heroin" were already fentanyl deaths; the coding just hadn't caught up.</li>

<li><b>Fentanyl (yellow) grows explosively</b>, roughly doubling every 2-3 years through the peak.</li>

<li><b>Cocaine (purple) and methamphetamine (pink) deaths rise in parallel</b> — a phenomenon researchers call the "polysubstance" wave. Stimulant deaths often involve fentanyl contamination of the cocaine/meth supply, a grim side effect of fentanyl's ubiquity in street-level drug markets.</li>
</ul>
</div>
</div>
<div id="outline-container-scale-context" class="outline-2">
<h2 id="scale-context"><span class="section-number-2">9.</span> The Scale in Context&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="mortality">mortality</span>&#xa0;<span class="context">context</span></span></h2>
<div class="outline-text-2" id="text-scale-context">
<p>
Drug overdose deaths are large enough in absolute terms to register as a measurable fraction of all U.S. deaths. The chart below places drug overdose deaths against total U.S. mortality — context that is often missing from epidemic reporting.
</p>

<div id="ow_national_totals" class="plotly-plot"></div>

<p>
The 2020-2021 spike in total deaths is the COVID-19 pandemic. Notably, drug overdose deaths <i>also</i> accelerated sharply during this period — fentanyl deaths increased ~30% during the pandemic year. Lockdowns, disrupted supply chains, and collapsed treatment infrastructure compounded the synthetic opioid crisis simultaneously with COVID-19 mortality.
</p>

<p>
At peak, roughly 1 in every 30 deaths in the United States was a drug overdose — a share unthinkable in 1999.
</p>
</div>
</div>
<div id="outline-container-geography" class="outline-2">
<h2 id="geography"><span class="section-number-2">10.</span> Geographic Spread&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="geography">geography</span>&#xa0;<span class="states">states</span>&#xa0;<span class="maps">maps</span></span></h2>
<div class="outline-text-2" id="text-geography">
<p>
The opioid epidemic began in specific geographies — rural Appalachia, manufacturing towns in the Rust Belt — but has since diffused broadly across the country. The chart below ranks all 50 states by most recent 12-month overdose death totals.
</p>

<div id="ow_us_map" class="plotly-plot"></div>

<p>
Absolute death counts reflect population size; large states like California, Texas, and Florida show high totals. But the epidemic isn't primarily a big-state story. West Virginia, Ohio, Pennsylvania, and Kentucky remain disproportionately affected when adjusting for population — a legacy of the prescription wave that hit those communities first and hardest. The color gradient from purple to orange reflects magnitude within the top-ranked states.
</p>
</div>
<div id="outline-container-state-trends" class="outline-3">
<h3 id="state-trends"><span class="section-number-3">10.1.</span> How states changed over time&#xa0;&#xa0;&#xa0;<span class="tag"><span class="states">states</span>&#xa0;<span class="trends">trends</span>&#xa0;<span class="heatmap">heatmap</span></span></h3>
<div class="outline-text-3" id="text-state-trends">
<p>
The heatmap below shows how overdose deaths evolved by state from 2015 through the most recent data, sorted by current death toll. Darker colors represent higher death counts.
</p>

<div id="ow_state_heatmap" class="plotly-plot"></div>

<p>
Notice that no state moves from dark to light — there are no success stories in this data. Some states saw slower growth than others, but the epidemic has proven resistant to state-level intervention. States that aggressively restricted prescribing (like Florida after the 2011 pill mill crackdown) often saw deaths shift composition rather than decline in total — from prescription pills to heroin, then from heroin to fentanyl.
</p>
</div>
</div>
</div>
<div id="outline-container-limitations" class="outline-2">
<h2 id="limitations"><span class="section-number-2">11.</span> What the Data Doesn't Show :limitations:treatment:harm-reduction:</h2>
<div class="outline-text-2" id="text-limitations">
<p>
Death counts are a lagging, partial indicator. They tell us how many people died, but not:
</p>

<ul class="org-ul">
<li><b>How many people are in active addiction</b> — estimates range from 2-4 million Americans for opioid use disorder alone</li>
<li><b>Rates of treatment access</b> — fewer than 25% of people with opioid use disorder receive medication-assisted treatment (buprenorphine, methadone, naltrexone)</li>
<li><b>Near-miss overdoses</b> — for every fatal overdose, many more are reversed by naloxone (Narcan) or survive without intervention</li>
<li><b>The economic cascade</b> — lost productivity, healthcare costs, criminal justice costs, and family disruption extend well beyond the death toll</li>
</ul>

<p>
The CDC's <a href="https://www.cdc.gov/drugoverdose/">Drug Overdose surveillance</a> and HRSA's <a href="https://data.hrsa.gov/topics/health-workforce/shortage-areas">Health Professional Shortage Area</a> data offer complementary views — particularly the geographic clustering of mental health provider deserts, which correlates with overdose hotspots.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">12.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cdc">cdc</span>&#xa0;<span class="api">api</span>&#xa0;<span class="methods">methods</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All charts use the CDC's <a href="https://data.cdc.gov/NCHS/VSRR-Provisional-Drug-Overdose-Death-Counts/xkb8-kh2a">VSRR Provisional Drug Overdose Death Counts</a> dataset (dataset ID: <code>xkb8-kh2a</code>), accessed via the CDC's Socrata SODA API. The API is free, requires no authentication, and updates monthly.
</p>

<p>
The data covers:
</p>
<ul class="org-ul">
<li><b>Period</b>: January 2015 – present (with ~6-8 month reporting lag)</li>
<li><b>Geography</b>: All 50 states, DC, and national totals</li>
<li><b>Granularity</b>: Monthly, with both "12 month-ending" rolling totals and single-month counts</li>
<li><b>Indicators</b>: Total deaths, drug overdose deaths, opioids, fentanyl, heroin, natural/semi-synthetic opioids, cocaine, psychostimulants, and more</li>
</ul>

<p>
The 12-month rolling total ("period" = "12 month-ending") is used throughout for stability. Single-month counts have high variance due to reporting delays from medical examiner offices.
</p>

<p style="font-size:0.85em; color: #888; margin-top: 2rem;">
  <strong>Data source:</strong> CDC VSRR Provisional Drug Overdose Death Counts.
  Charts generated from live API data fetched February 2026.
  CDC WONDER and HRSA data referenced for contextual statistics.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>The Telehealth Revolution</title>
      <link>https://www.chiply.dev/post-telehealth-revolution</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-telehealth-revolution</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Before March 2020, telehealth was a niche feature of the US healthcare system. Then the pandemic hit, and CMS suspended virtually every telehealth restriction overnight. This post tracks the largest n...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="healthcare">healthcare</span>&#xa0;<span class="telehealth">telehealth</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org9ccc751" class="figure">
<p><img src="https://www.chiply.dev/images/telehealth-revolution-banner.jpeg" alt="telehealth-revolution-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Before March 2020, telehealth was a niche feature of the US healthcare system.  Then the pandemic hit, and CMS suspended virtually every telehealth restriction overnight.  This post tracks the largest natural experiment in healthcare delivery ever conducted &#x2013; from the initial 47% adoption spike through the permanent ~13% baseline that emerged.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
COVID-19 forced a telehealth experiment on the entire US healthcare system in 2020. Utilization went from essentially zero to 47% of eligible Medicare beneficiaries in a single quarter. Five years later, telehealth hasn't disappeared — it's settled at ~13%, a permanent structural shift from the pre-pandemic baseline of near zero. This post shows where that new baseline landed and who uses it most.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="telehealth">telehealth</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="medicare">medicare</span>&#xa0;<span class="trends">trends</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Before March 2020, telehealth was a niche feature of the US healthcare system. Medicare covered a narrow set of telehealth services only for patients in rural areas, only for certain specialties, and only when conducted from an approved facility. In-person care was the default, and most patients had no reason to deviate from it.
</p>

<p>
Then the pandemic hit, and CMS issued an emergency waiver that suspended virtually every telehealth restriction overnight. Any Medicare beneficiary could receive any covered service via video or phone from any location. Reimbursement rates were set equal to in-person visits to eliminate the financial disincentive for providers.
</p>

<p>
What followed was the largest natural experiment in healthcare delivery ever conducted. The question was whether patients and providers would revert to in-person care when restrictions lifted — or whether the forced experiment would reveal a latent demand for remote care that the previous regulatory structure had artificially suppressed.
</p>

<p>
The data, now five years out, gives a clear answer: both things happened. The extreme peak didn't last. But telehealth didn't collapse back to zero either. A meaningful fraction of healthcare visits permanently shifted to remote delivery.
</p>

<p>
This post visualizes the full trajectory using CMS Medicare Telehealth Trends data — quarterly from Q1 2020 through mid-2025, covering 50 million Medicare FFS beneficiaries.
</p>
</div>
</div>
<div id="outline-container-inflection-point" class="outline-2">
<h2 id="inflection-point"><span class="section-number-2">4.</span> The COVID Inflection Point&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="trends">trends</span>&#xa0;<span class="covid">covid</span></span></h2>
<div class="outline-text-2" id="text-inflection-point">
<div id="th_national_trend" class="plotly-plot"></div>

<p>
The quarterly trend is stark. In Q1 2020 — the first three months of the year, before lockdowns fully took hold — 6.9% of Medicare beneficiaries used telehealth. In Q2 2020, that figure hit 46.7%. One in three beneficiaries who would typically see a doctor in person instead used a phone or video visit.
</p>

<p>
The spike was driven by necessity: clinics closed, patients were afraid to enter buildings, and CMS simultaneously removed every barrier to billing for telehealth. The incentives all pointed in one direction at once.
</p>

<p>
After the acute phase, utilization fell but didn't collapse. Q3 2020: 28.2%. Q4 2020: 27.8%. The emergency was winding down but patients and providers had adapted. Workflows changed. Scheduling systems were rewritten. Patients discovered their doctor could diagnose a rash over video. Therapists discovered that sessions over Zoom were often just as effective — and more accessible.
</p>
</div>
<div id="outline-container-why-decline" class="outline-3">
<h3 id="why-decline"><span class="section-number-3">4.1.</span> Why the decline from the peak is real&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h3>
<div class="outline-text-3" id="text-why-decline">
<p>
The post-2020 decline isn't a sign that telehealth failed. It reflects the return of care that genuinely requires in-person delivery. Surgical follow-ups, physical therapy, procedures, imaging — none of these can be done remotely. As these services resumed, telehealth's share of total visits fell toward the services where it actually adds value: chronic disease management, psychiatric follow-ups, medication refills, minor acute care.
</p>

<p>
The question was never whether telehealth should be 47% of all visits. It was whether it should be 0%. The answer turned out to be something closer to 13–14%.
</p>
</div>
</div>
</div>
<div id="outline-container-new-baseline" class="outline-2">
<h2 id="new-baseline"><span class="section-number-2">5.</span> The New Baseline: Did Telehealth Stick?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="retention">retention</span></span></h2>
<div class="outline-text-2" id="text-new-baseline">
<p>
By 2023 and 2024, telehealth utilization had stabilized at roughly 13–14% per quarter. Congressional action extended the emergency telehealth waivers multiple times, and CMS made several provisions permanent in subsequent rulemaking. The infrastructure built in 2020 — the scheduling software, the patient familiarity, the provider workflows — stayed in place.
</p>

<p>
Pre-pandemic, telehealth was effectively at 0% for Medicare beneficiaries due to geographic and facility restrictions. The ~13% post-pandemic baseline represents a genuine, durable expansion of access. For the roughly 50 million Medicare beneficiaries, that means millions of visits per quarter that previously required a car trip, time off work, and a waiting room are now handled from home.
</p>
</div>
</div>
<div id="outline-container-rural-urban" class="outline-2">
<h2 id="rural-urban"><span class="section-number-2">6.</span> Rural vs Urban: Who Adopted More?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="rural">rural</span>&#xa0;<span class="urban">urban</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-rural-urban">
<div id="th_rural_urban" class="plotly-plot"></div>

<p>
The rural-urban comparison is one of the most politically and practically charged dimensions of telehealth policy. The original Medicare telehealth restrictions were partly motivated by a concern that rural patients — who lack broadband and may have older technology — couldn't effectively use remote care. The emergency waiver suspended the geographic requirements, allowing urban patients to use telehealth for the first time.
</p>

<p>
The data shows both groups surged during COVID. Urban patients, with better technology infrastructure, generally showed slightly higher peak adoption. But rural beneficiaries maintained telehealth at meaningful rates post-pandemic as well. For rural patients — who may live an hour or more from the nearest specialist — telehealth offers the most concrete access benefit. A psychiatry follow-up that would otherwise require a 90-minute round trip becomes a 30-minute video call.
</p>

<p>
The rural-urban gap in telehealth adoption has significant policy implications. CMS has debated whether to make the geographic waivers permanent; the utilization data suggests demand exists in both populations.
</p>
</div>
</div>
<div id="outline-container-age-groups" class="outline-2">
<h2 id="age-groups"><span class="section-number-2">7.</span> Who Uses Telehealth? The Age Paradox&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="age">age</span>&#xa0;<span class="demographics">demographics</span></span></h2>
<div class="outline-text-2" id="text-age-groups">
<div id="th_age_groups" class="plotly-plot"></div>

<p>
The age breakdown reveals a counterintuitive pattern. Younger Medicare beneficiaries (ages 0–64, primarily those on Medicare due to disability) showed notably higher telehealth adoption than elderly beneficiaries aged 75+. The 85-and-over cohort — precisely the population most burdened by travel to medical appointments — shows the lowest adoption.
</p>

<p>
This is partly a technology access story. Older seniors are less likely to own smartphones or tablets, more likely to have limited broadband access, and more likely to rely on audio-only phone visits rather than video. CMS data shows that audio-only (telephone) visits were a significant fraction of telehealth in 2020–2021 before policy changes added additional restrictions for audio-only billing.
</p>

<p>
It's also partly a clinical story. The oldest beneficiaries have higher rates of conditions requiring physical examination — wound care, fall assessments, mobility evaluations — where in-person care genuinely cannot be substituted. The populations where telehealth adds the most value (mental health, chronic disease medication management, follow-up visits) skew younger within Medicare.
</p>

<p>
The gap narrowed over time as older patients and caregivers became more comfortable with the technology, but it persisted through 2024.
</p>
</div>
</div>
<div id="outline-container-state-map" class="outline-2">
<h2 id="state-map"><span class="section-number-2">8.</span> The Geography of Telehealth&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="states">states</span>&#xa0;<span class="geography">geography</span></span></h2>
<div class="outline-text-2" id="text-state-map">
<div id="th_state_map" class="plotly-plot"></div>

<p>
State-level telehealth adoption in 2024 shows meaningful geographic variation. States in the Northeast and some Mountain West states tend to show higher adoption. States in the Deep South and parts of the Midwest tend to show lower adoption.
</p>

<p>
The geographic pattern correlates with several factors: broadband penetration (higher broadband → higher telehealth capacity), state Medicaid telehealth policy (states with more expansive Medicaid coverage see higher dual-eligible adoption), and the mix of urban vs rural beneficiaries. States with large urban populations have more beneficiaries with the technology to use telehealth effectively, but also more readily available in-person care — so the net effect on utilization is ambiguous.
</p>

<p>
Notably, states with large rural populations don't uniformly show lower telehealth adoption. In some rural states, telehealth has become a primary mode of specialty access precisely because in-person alternatives are scarce or distant.
</p>
</div>
</div>
<div id="outline-container-dual-eligible" class="outline-2">
<h2 id="dual-eligible"><span class="section-number-2">9.</span> The Highest-Need Patients :dataviz:equity:dual-eligible:</h2>
<div class="outline-text-2" id="text-dual-eligible">
<div id="th_enrollment" class="plotly-plot"></div>

<p>
Dual-eligible beneficiaries — those enrolled in both Medicare and Medicaid — use telehealth at substantially higher rates than Medicare-only beneficiaries. Dual eligibles are generally lower-income, more likely to have multiple chronic conditions, and more likely to face transportation barriers to in-person care.
</p>

<p>
The pattern is consistent across the full 2020–2024 period: dual eligibles adopted telehealth at higher rates during the COVID spike and maintained it at higher rates afterward. This suggests that for the lowest-income Medicare beneficiaries, telehealth isn't just a convenience — it's often the most accessible modality.
</p>

<p>
The equity implication runs in two directions. Telehealth reduces access barriers for low-income beneficiaries who lack transportation or can't take time off work for clinic visits. But audio-only visit restrictions and broadband requirements disproportionately burden the same population. Policy decisions about telehealth reimbursement and modality requirements have larger effects on dual eligibles than on the general Medicare population.
</p>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">10.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cms">cms</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data comes from the CMS <a href="https://data.cms.gov/summary-statistics-on-use-and-payments/medicare-medicaid-service-type-reports/medicare-telehealth-trends">Medicare Telehealth Trends</a> dataset (UUID <code>939226be-b107-476e-8777-f199a840138a</code>), updated quarterly.
</p>

<p>
The dataset covers Medicare Fee-for-Service (FFS) beneficiaries only. Medicare Advantage (MA) beneficiaries are excluded — MA plans negotiate telehealth coverage separately and are not subject to the same FFS billing claims data. MA now covers roughly 50% of Medicare beneficiaries, meaning this data captures roughly half of the total Medicare population.
</p>

<p>
<b>Key metric:</b> <code>Pct_Telehealth</code> = <code>Total_Bene_Telehealth</code> / <code>Total_Bene_TH_Elig</code>. The denominator is beneficiaries who received any telehealth-eligible service during the period (either in-person or via telehealth), not total Medicare enrollment. This means the percentage reflects the telehealth share of eligible visits, not the share of all beneficiaries.
</p>

<p>
<b>Telehealth definition:</b> Services billed with telehealth-eligible HCPCS codes using the appropriate place-of-service code or modifier. Audio-only (telephone) visits are included in some periods and excluded in others based on CMS policy changes.
</p>

<p>
<b>Data period:</b> Q1 2020 through Q2 2025. Pre-2020 telehealth utilization was near zero due to geographic and facility restrictions; no comparable baseline data exists in this dataset.
</p>

<p>
<b>Rural/Urban classification:</b> CMS Rural-Urban Commuting Area (RUCA) codes, aggregated to Rural, Urban, and Unknown.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Your Neighborhood&apos;s Health Profile</title>
      <link>https://www.chiply.dev/post-zip-health-profile</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-zip-health-profile</guid>
      <pubDate>Sun, 01 Mar 2026 19:18:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Public health statistics are usually reported at the national or state level, but they obscure enormous local variation. This post uses the CDC&apos;s PLACES program &amp;#x2013; 40+ health measures at ZIP cod...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="publicHealth">publicHealth</span>&#xa0;<span class="cdc">cdc</span>&#xa0;<span class="dataviz">dataviz</span>&#xa0;<span class="interactive">interactive</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="orgad15f2a" class="figure">
<p><img src="https://www.chiply.dev/images/zip-health-profile-banner.jpeg" alt="zip-health-profile-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Public health statistics are usually reported at the national or state level, but they obscure enormous local variation.  This post uses the CDC's PLACES program &#x2013; 40+ health measures at ZIP code level across ~30,000 ZIPs &#x2013; to show how health outcomes are hyperlocal, with two adjacent ZIP codes sometimes differing more than two states.
</p>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">2.</span> TLDR&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tldr">tldr</span></span></h2>
<div class="outline-text-2" id="text-tldr">
<p>
The CDC measures 40+ health indicators at ZIP code level — obesity, diabetes, depression, smoking, blood pressure, and more — for virtually every ZIP in the US. Enter your ZIP code below to see how your neighborhood compares to the national median on a radar chart. The headline finding: health outcomes are hyperlocal. Two adjacent ZIP codes can differ more than two states.
</p>
</div>
</div>
<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">3.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="cdc">cdc</span>&#xa0;<span class="places">places</span>&#xa0;<span class="health">health</span>&#xa0;<span class="zip">zip</span>&#xa0;<span class="interactive">interactive</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Public health statistics are usually reported at the national or state level. "30% of American adults have high blood pressure." "The obesity rate is rising." These aggregates are real, but they obscure enormous local variation. The neighborhood where you grew up, where you work, and where you live now shapes your health in ways that national averages can't capture.
</p>

<p>
The CDC's PLACES program changes that. Every year, CDC combines the Behavioral Risk Factor Surveillance System (BRFSS) survey with small-area estimation models to produce 40+ health measures at the ZIP code level. The 2023 release covers 29,983 US ZIP Code Tabulation Areas (ZCTAs) — essentially complete coverage of the country's populated ZIP codes.
</p>

<p>
The measures span the full spectrum: chronic disease outcomes (diabetes, heart disease, stroke), health behaviors (smoking, physical inactivity, binge drinking), preventive care use (mammograms, dental visits, checkups), mental health (depression, poor mental health days), disability status, and health-related social needs (food insecurity, housing insecurity, lack of transportation).
</p>

<p>
This post lets you explore that landscape. The interactive section below pulls your ZIP code's data directly from the CDC API.
</p>
</div>
</div>
<div id="outline-container-zip-explorer" class="outline-2">
<h2 id="zip-explorer"><span class="section-number-2">4.</span> Explore Your ZIP Code&#xa0;&#xa0;&#xa0;<span class="tag"><span class="interactive">interactive</span>&#xa0;<span class="dataviz">dataviz</span></span></h2>
<div class="outline-text-2" id="text-zip-explorer">
<p>
Enter any US ZIP code to see its health profile compared to the national median across 12 key measures. Data comes directly from the CDC PLACES API in real time.
</p>

<style>
  .zip-lookup-row {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    flex-wrap: wrap;
    margin: 1.2rem 0 0.5rem;
  }
  #zip-input {
    font-family: inherit;
    font-size: 1rem;
    padding: 0.45rem 0.75rem;
    border: 1px solid var(--gray-6, #ccc);
    border-radius: 4px;
    width: 11rem;
    background: var(--surface-2, #f8f8f8);
    color: inherit;
  }
  #zip-lookup-btn {
    font-family: inherit;
    font-size: 0.95rem;
    padding: 0.45rem 1.1rem;
    border: 1px solid var(--gray-6, #ccc);
    border-radius: 4px;
    cursor: pointer;
    background: var(--blue-6, #4D96FF);
    color: white;
  }
  #zip-lookup-btn:hover { opacity: 0.85; }
  #zip-status {
    font-size: 0.88rem;
    color: var(--gray-7, #666);
  }
  #zip-results { margin-top: 0.5rem; }
</style>

<div class="zip-lookup-row">
  <input type="text" id="zip-input" placeholder="e.g. 90210" maxlength="5" inputmode="numeric" pattern="\d{5}">
  <button id="zip-lookup-btn">Look up</button>
  <span id="zip-status"></span>
</div>
<div id="zip-results" style="display:none">
  <div id="zip-radar" style="width:100%;height:480px"></div>
</div>

<script>
(function() {
  var RADAR_MEASURES = ['OBESITY','DIABETES','DEPRESSION','CSMOKING','BPHIGH','LPA','MHLTH','PHLTH','SLEEP','BINGE','CASTHMA','STROKE'];
  var MEASURE_LABELS = ['Obesity','Diabetes','Depression','Smoking','High BP','Inactivity','Mental Distress','Physical Distress','Poor Sleep','Binge Drinking','Asthma','Stroke'];
  var NATIONAL_MEDIANS = {OBESITY:36.7,DIABETES:12.7,DEPRESSION:23.0,CSMOKING:15.0,BPHIGH:37.5,LPA:26.4,MHLTH:16.9,PHLTH:14.5,SLEEP:35.9,BINGE:15.8,CASTHMA:10.7,STROKE:3.9};

  async function lookupZip(zip) {
    var status = document.getElementById('zip-status');
    status.textContent = 'Fetching data...';
    try {
      var url = "https://data.cdc.gov/resource/qnzd-25i4.json?locationid=" + zip
        + "&$limit=50&$where=data_value_type='Crude prevalence'"
        + "&$select=measureid,data_value,short_question_text";
      var resp = await window.fetch(url);
      if (!resp.ok) throw new Error('API error ' + resp.status);
      var data = await resp.json();
      if (!data.length) {
        status.textContent = 'No CDC PLACES data found for ZIP ' + zip + '. Try a nearby ZIP.';
        return;
      }
      var zipVals = {};
      data.forEach(function(row) { zipVals[row.measureid] = parseFloat(row.data_value); });

      var zVals = RADAR_MEASURES.map(function(m) { return zipVals[m] != null ? zipVals[m] : null; });
      var nVals = RADAR_MEASURES.map(function(m) { return NATIONAL_MEDIANS[m]; });

      var missing = RADAR_MEASURES.filter(function(m) { return zipVals[m] == null; });
      status.textContent = missing.length ? 'Note: ' + missing.length + ' measures unavailable for this ZIP.' : '';

      document.getElementById('zip-results').style.display = 'block';

      Plotly.react('zip-radar', [
        {
          type: 'scatterpolar', r: zVals, theta: MEASURE_LABELS, fill: 'toself',
          name: 'ZIP ' + zip, line: {color: '#4D96FF'},
          fillcolor: 'rgba(77,150,255,0.20)',
          hovertemplate: '%{theta}: <b>%{r:.1f}%</b><extra></extra>'
        },
        {
          type: 'scatterpolar', r: nVals, theta: MEASURE_LABELS, fill: 'toself',
          name: 'National Median', line: {color: '#E05C2A', dash: 'dot'},
          fillcolor: 'rgba(224,92,42,0.08)',
          hovertemplate: '%{theta}: %{r:.1f}% (national median)<extra></extra>'
        }
      ], {
        title: {text: 'ZIP ' + zip + ' vs National Median (%)'},
        polar: {radialaxis: {visible: true, ticksuffix: '%'}},
        legend: {orientation: 'h', x: 0, y: -0.15},
        margin: {t: 60, b: 80, l: 60, r: 60},
        height: 480
      });
    } catch(e) {
      status.textContent = 'Error: ' + e.message;
    }
  }

  document.getElementById('zip-lookup-btn').addEventListener('click', function() {
    var zip = document.getElementById('zip-input').value.trim();
    while (zip.length < 5) zip = '0' + zip;
    if (!/^\d{5}$/.test(zip)) {
      document.getElementById('zip-status').textContent = 'Please enter a valid 5-digit ZIP code.';
      return;
    }
    lookupZip(zip);
  });
  document.getElementById('zip-input').addEventListener('keydown', function(e) {
    if (e.key === 'Enter') document.getElementById('zip-lookup-btn').click();
  });
})();
</script>

<p>
The radar shows absolute prevalence rates (percent of adults). A ZIP "inside" the orange national median line on a given measure has lower-than-typical rates for that condition — generally better for disease outcomes, though not always (lower physical inactivity is better; lower preventive care rates are not).
</p>
</div>
</div>
<div id="outline-container-variation" class="outline-2">
<h2 id="variation"><span class="section-number-2">5.</span> How Much Does ZIP Code Matter?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="variation">variation</span></span></h2>
<div class="outline-text-2" id="text-variation">
<p>
Before looking at your specific ZIP, it's worth understanding how much variation exists across US ZIP codes. For each of the 12 measures in the radar chart, this shows the spread from the 10th to 90th percentile across all ZIPs.
</p>

<div id="zp_variation" class="plotly-plot"></div>

<p>
The variation is substantial. Obesity ranges from roughly 15% in the lowest-prevalence ZIPs to over 55% in the highest. Depression spans from around 10% to 37%. Even stroke — relatively rare — varies threefold.
</p>

<p>
These aren't just statistical artifacts. They reflect real differences in built environment (walkability, food access), socioeconomic stress, healthcare access, and local culture. The 10th-percentile ZIP for obesity is a fundamentally different place to live — in terms of food environment, activity infrastructure, and population health — than the 90th-percentile ZIP.
</p>
</div>
<div id="outline-container-correlation-problem" class="outline-3">
<h3 id="correlation-problem"><span class="section-number-3">5.1.</span> The correlation problem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="analysis">analysis</span></span></h3>
<div class="outline-text-3" id="text-correlation-problem">
<p>
These conditions don't vary independently. ZIPs with high obesity tend to have high diabetes, high blood pressure, and high rates of physical inactivity. The conditions cluster together because they share upstream causes: poverty, food deserts, limited walkability, limited healthcare access.
</p>

<p>
This means a ZIP that looks "bad" on one measure often looks bad on several. And a ZIP that looks healthy on the radar chart across the board usually reflects accumulated advantages: higher incomes, better food environments, more walkable neighborhoods, better access to preventive care.
</p>
</div>
</div>
</div>
<div id="outline-container-scatter" class="outline-2">
<h2 id="scatter"><span class="section-number-2">6.</span> Obesity and Diabetes: The Strongest Correlation&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="scatter">scatter</span></span></h2>
<div class="outline-text-2" id="text-scatter">
<p>
Of all the correlations in the dataset, obesity and diabetes are the tightest. This scatter plots every ZIP code on both axes.
</p>

<div id="zp_scatter" class="plotly-plot"></div>

<p>
The correlation coefficient is approximately 0.90 — among the strongest relationships in social epidemiology. Every point is a ZIP code. The spread around the regression line is real: ZIPs at the same obesity rate can have substantially different diabetes rates, reflecting differences in healthcare access (diagnosis rates), diet quality, and racial composition (some groups have higher diabetes risk at lower BMI).
</p>

<p>
The geographic color coding shows regional clustering. Southern ZIPs (red/orange) are concentrated in the upper-right — high obesity, high diabetes. Pacific and Northeast ZIPs (blue, green) tend toward the lower-left.
</p>
</div>
</div>
<div id="outline-container-all-measures" class="outline-2">
<h2 id="all-measures"><span class="section-number-2">7.</span> The Full Picture: All 40 Measures&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataviz">dataviz</span>&#xa0;<span class="cdc">cdc</span>&#xa0;<span class="measures">measures</span></span></h2>
<div class="outline-text-2" id="text-all-measures">
<p>
CDC PLACES covers far more than just the 12 measures in the radar chart. This chart shows the national average crude prevalence for all 40 measures, sorted by value and colored by category.
</p>

<div id="zp_national_avgs" class="plotly-plot"></div>

<p>
A few things stand out in the full picture:
</p>

<ul class="org-ul">
<li><b>High blood pressure</b> (37.5% median) and <b>obesity</b> (36.7%) are the highest-prevalence chronic conditions — more than one in three adults.</li>
<li><b>Poor sleep</b> (35.9%) is in the same tier, rarely discussed as a public health crisis despite its scale.</li>
<li><b>Preventive care gaps</b> are significant: meaningful shares of adults haven't had a dental visit, mammogram, or colorectal cancer screening in the recommended timeframe.</li>
<li><b>Social determinants</b> (the Health-Related Social Needs category) show that food insecurity, housing insecurity, and lack of transportation affect substantial fractions of ZIP code populations. These are not fringe conditions.</li>
</ul>
</div>
</div>
<div id="outline-container-data-methods" class="outline-2">
<h2 id="data-methods"><span class="section-number-2">8.</span> Data and Methods&#xa0;&#xa0;&#xa0;<span class="tag"><span class="data">data</span>&#xa0;<span class="cdc">cdc</span>&#xa0;<span class="methodology">methodology</span></span></h2>
<div class="outline-text-2" id="text-data-methods">
<p>
All data comes from the CDC PLACES 2024 release, using 2023 BRFSS-based estimates (a few measures use 2022 data):
</p>

<ul class="org-ul">
<li><b>Dataset:</b> <a href="https://data.cdc.gov/resource/qnzd-25i4">CDC PLACES ZCTA-level data</a> — 40+ measures, ~29,983 ZIP Code Tabulation Areas, all US states</li>
<li><b>Measure type:</b> "Crude prevalence" — percentage of adults estimated to have each condition or behavior, without age adjustment</li>
<li><b>Method:</b> CDC uses multilevel regression and poststratification (MRP) to produce small-area estimates from BRFSS survey responses. The model combines local survey data with demographic predictors to generate stable estimates for small geographies like ZIP codes.</li>
</ul>

<p>
<b>ZCTA vs ZIP code:</b> Technically, CDC reports data at the ZCTA (ZIP Code Tabulation Area) level. ZCTAs are Census constructs that approximate ZIP codes but aren't identical. The ZIP code you enter is matched to the closest ZCTA; most populated ZIPs have direct ZCTA matches.
</p>

<p>
<b>Interactive lookup:</b> The radar chart fetches live data from the CDC SODA API. No data is stored; your ZIP code is sent directly to data.cdc.gov.
</p>

<p>
<b>National medians:</b> Computed from the 29,983 ZCTAs with available data for each measure. For the radar chart normalization, the values are: Obesity 36.7%, Diabetes 12.7%, Depression 23.0%, Smoking 15.0%, High BP 37.5%, Physical Inactivity 26.4%, Poor Mental Health 16.9%, Poor Physical Health 14.5%, Poor Sleep 35.9%, Binge Drinking 15.8%, Asthma 10.7%, Stroke 3.9%.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Feature Demo: Building a Blog with Emacs Org and SvelteKit</title>
      <link>https://www.chiply.dev/post0</link>
      <guid isPermaLink="true">https://www.chiply.dev/post0</guid>
      <pubDate>Sun, 01 Mar 2026 14:27:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>This page demonstrates the custom blogging framework used to build my technical diary: chiply.dev. This diary delivers a highly contextualized reading experience, with side-line components that enhanc...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About</h2>
<div class="outline-text-2" id="text-about">

<div id="orgd8dd563" class="figure">
<p><img src="https://www.chiply.dev/images/post0-banner.jpeg" alt="post0-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
This page demonstrates the custom blogging framework used to build my technical diary: <a href="https://www.chiply.dev/">chiply.dev</a>.  This diary delivers a highly contextualized reading experience, with side-line components that enhance comprehension, navigation, and discovery of related content<sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>.
</p>

<p>
This page serves as a <a href="https://dev.to/tobbystalin/how-to-print-a-test-page-on-your-printer-quick-guide-j62">test-print</a> of my blog's features, as a sandbox for new ideas, and as a current 'state of the art' reference for what is possible with my blog setup.
</p>

<p>
Given that this page functions as a test-print &#x2013; you will see progress tracking on some headings. Any heading with a DONE tag is a complete feature demo.  Any heading with a TODO tag is a work in progress<sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup>.
</p>

<p>
This blog is built with <a href="https://orgmode.org/">Emacs Org Mode</a> and <a href="https://svelte.dev/">Svelte</a>/<a href="https://svelte.dev/docs/kit/introduction">SvelteKit</a><sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.  This page mostly demonstrates the features, but if you want to learn more about the technical design decisions that went into building this blog, read <a href="https://www.chiply.dev/post-design-decisions">Design Decisions: Building a Modern Technical Blog</a>.
</p>

<p>
Fellow bloggers and digital journalists will find this page useful as a reference for what is possible when building a blog with Org and SvelteKit, and all of the code used to build this site is accessible on the <a href="https://github.com/chiply/chiply.dev">chiply.dev GitHub repository</a>.
</p>

<p>
This video walkthrough will help:
<div class="youtube-container">
<iframe src="https://www.youtube-nocookie.com/embed/Q1Et_2rAooY"
        title="Video Guide"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
</iframe>
</div>
</p>
</div>
</div>
<div id="outline-container-where-did-all-this-text-come-from-" class="outline-2">
<h2 id="where-did-all-this-text-come-from-"><span class="section-number-2">2.</span> Where did all this text come from?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="orgMode">orgMode</span></span></h2>
<div class="outline-text-2" id="text-where-did-all-this-text-come-from-">
<p>
The articles you will find on this blog are entirely written with Emacs org-mode<sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup>, and then converted to HTML using the included org -&gt; HTML exporter (<code>ox</code>).  Each article on this blog is generated from exactly 1 source <code>.org</code> file, although there is the ability to compose articles from multiple <code>.org</code> files using <a href="https://www.chiply.dev/#transclusion">Transclusion</a>.
</p>
</div>
</div>
<div id="outline-container-what-s-going-on-in-the-sidelines-" class="outline-2">
<h2 id="what-s-going-on-in-the-sidelines-"><span class="section-number-2">3.</span> What's going on in the sidelines?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="hud">hud</span></span></h2>
<div class="outline-text-2" id="text-what-s-going-on-in-the-sidelines-">
<p>
The central window (the place you are reading this text right now), displays the full article content, and you will see a sort of heads-up-display (HUD) on the tops and sides of the screen.  This HUD contains information about the article you are reading and the specific part of the article you are currently reading<sup><a id="fnr.5" class="footref" href="https://www.chiply.dev/#fn.5" role="doc-backlink">5</a></sup>.
</p>

<p>
Note that all HUD components render by default, but you can customize the layout by interacting with the buttons at the top right hand side of the screen.  Any visually distracting or extraneous HUD component can be hidden and shown with a single click, and the browser will remember your settings.  <b>This allows readers to tailor the reading experience to their exact blisspoint between rich context and focused minimalism.</b>
</p>
</div>
<div id="outline-container-why-include-hud-" class="outline-3">
<h3 id="why-include-hud-"><span class="section-number-3">3.1.</span> Why include HUD?</h3>
<div class="outline-text-3" id="text-why-include-hud-">
<p>
When you're reading a blog post, there's context (table of contents, footnotes, tags), explicit relationships (bibliography, social media links, post-to-post links, intra post-links), implicit relationships (related posts via tags, semantic content), and summarization (TLDR, minimap) that can better help you understand and navigate the content.  
</p>

<p>
Most blogs ignore these important aspects of comprehension when delivering a firehose of information, and instead just dump a wall of text on the reader with no context or navigation aids.
</p>

<p>
We live in an age of information overload, and the rising effectiveness of knowledge graphs and LLMs have revealed to us that information is highly interconnected, and that these connections provide the key to distilling useful knowledge from a deluge of raw data.  Using these tools daily, I am acutely aware of all the explicit and implicit connections that exist between pieces of information I scour on the internet, and a set of blog posts is no exception. 
</p>

<p>
Posts <span class="underline">within any blog</span> are highly interconnected, and this interconnectedness is what allows us to build a rich understanding of the topics being discussed.  To borrow terminology from graph theory, I like to think that the <b><b>nodes</b></b> (posts) provide self contained, narrative-style information on a cohesive topic, while the <b>edges</b> (connections between posts) allow for the construction of higher level understanding by linking related concepts together, both implicitly and explicitly.
</p>

<p>
What's more, information in my blog is highly connected to information <span class="underline"><span class="underline">outside</span></span> of my blog (other blogs, social media, research papers, etc), and these connections are equally important to understanding the content.  
</p>

<p>
To again borrow from the graph theory lexicon, I like to think of my blog as an ever-growing knowledge graph, which is embedded in the larger knowledge graph of the internet via explicit connections.  As such, this blog can be used a discovery engine for finding related content, both within my blog and outside of it.
</p>

<p>
All side-line components reference a locaion inside of the article, and they autoupdate to reflect the current location in the article as you scroll through it (you will see this in action as you scroll through the page).
</p>
</div>
<div id="outline-container-components" class="outline-4">
<h4 id="components"><span class="section-number-4">3.1.1.</span> Components</h4>
<div class="outline-text-4" id="text-components">
<p>
Starting at the table of contents (on he left) and going anti-clockwise around the screen, here are the HUD components and their purpose:
</p>
</div>
<div id="outline-container-progrss-bar" class="outline-5">
<h5 id="progrss-bar"><span class="section-number-5">3.1.1.1.</span> Progrss bar</h5>
<div class="outline-text-5" id="text-progrss-bar">
<p>
At the very top of the screen, you will see a progress bar.  This bar fill from right to left displaying how far you have scrolled through the article you are reading.  This is useful for gauging how much of the article you have read, and how much is left to read.
</p>
</div>
</div>
<div id="outline-container-breadcrumbs" class="outline-5">
<h5 id="breadcrumbs"><span class="section-number-5">3.1.1.2.</span> Breadcrumbs</h5>
<div class="outline-text-5" id="text-breadcrumbs">
<p>
At the very top left of the screen, you will see a breadcrumb trail.  This trail shows you the path you took to get to the current article you are reading, and also the position within the outline tree.  This is useful for navigating back to previous articles you have read, and for understanding the context of the current article within the larger structure of the blog, and of the current outline node within the larger outline tree.
</p>
</div>
</div>
<div id="outline-container-treemap" class="outline-5">
<h5 id="treemap"><span class="section-number-5">3.1.1.3.</span> Treemap</h5>
<div class="outline-text-5" id="text-treemap">
<p>
Below the breadcrumb you will see a treemap representing the hierarchical structure of the document and the relative size of each section.  This gives a nice sense of where you are in the document, and the relative size of each section.
</p>
</div>
</div>
<div id="outline-container-table-of-contents" class="outline-5">
<h5 id="table-of-contents"><span class="section-number-5">3.1.1.4.</span> Table of Contents</h5>
<div class="outline-text-5" id="text-table-of-contents">
<p>
On the upper left side of the screen, you will see a table of contents.  You can click on any section in the table of contents to jump to that section in the article.
</p>
</div>
</div>
<div id="outline-container-history" class="outline-5">
<h5 id="history"><span class="section-number-5">3.1.1.5.</span> History</h5>
<div class="outline-text-5" id="text-history">
<p>
This component is hidden by default, but this shows a navigation history within posts.  This not only helps to visualize the history you are familiar with when navigating backwards and forwards in your browser, but also provides fine grain control for when your history forks, that is, if you navigate around, go back, and then navigate again &#x2013; you're history is no longer linear, it's hierarchical, or 'tree like'.  The history component helps you visualize this non-linear history and navigate through it.
</p>
</div>
</div>
<div id="outline-container-references" class="outline-5">
<h5 id="references"><span class="section-number-5">3.1.1.6.</span> References</h5>
<div class="outline-text-5" id="text-references">
<p>
At the bottom of the screen, you will see a bibliography.  This is a list of both internal and external references used in the article, and is meant to give credit to the original sources of information, and to organize all external references in one place to serve as a kind of further reading list.  The bibliography supports discoverability of related content, both within my blog and outside of it.
</p>
</div>
</div>
<div id="outline-container-social-media-links" class="outline-5">
<h5 id="social-media-links"><span class="section-number-5">3.1.1.7.</span> Social Media Links</h5>
<div class="outline-text-5" id="text-social-media-links">
<p>
At the very botttom of the screen, you will see social media links.  
</p>

<p>
Unlike the socials component on most websitets, <b>mine links to locations where the given article has been posted</b>.  For example, the reddit icon
 will take you to the reddit thread where I posted this article.
</p>

<p>
At the right hand side I have an RSS feed for this blog, which you can use to subscribe to updates from this blog in your favorite RSS reader.
</p>

<p>
At the left hand side, I have my contact information and links to my social media accounts.
</p>
</div>
</div>
<div id="outline-container-word-cloud" class="outline-5">
<h5 id="word-cloud"><span class="section-number-5">3.1.1.8.</span> Word Cloud</h5>
<div class="outline-text-5" id="text-word-cloud">
<p>
The word cloud visualizes the relative frequency of keywords as they appear in the document.  This gives a very high level perspective of what's discussed in the article, and how much each keyword is discussed.
</p>
</div>
</div>
<div id="outline-container-tldr--too-long-didn-t-read-" class="outline-5">
<h5 id="tldr--too-long-didn-t-read-"><span class="section-number-5">3.1.1.9.</span> TLDR (too long didn't read)</h5>
<div class="outline-text-5" id="text-tldr--too-long-didn-t-read-">
<p>
None taken lol 😁😂😅😭&#x2026; I'm super verbose, and even if I wasn't, some topics are complex enough to warrant more words than anyone would be willing to read.  Complex articles like this are kind of like reference materials for most folks.  As a means of summarization, and as a means of helping readers navigate to the most relevant parts of the article, a TLDR component is included.
</p>

<p>
On the lower right side of the screen, you will see a TLDR (too long; didn't read) summary.  This is a short summary of the article, and is meant to give you a quick overview of the article's content.  You can click links in the TLDR to jump to content being referenced in the summary.
</p>
</div>
</div>
<div id="outline-container-glossary" class="outline-5">
<h5 id="glossary"><span class="section-number-5">3.1.1.10.</span> Glossary</h5>
<div class="outline-text-5" id="text-glossary">
<p>
On the right side of the screen, between the TLDR and Word Cloud, you will see a glossary panel.  This component extracts term definitions from a dedicated glossary section in the article's org source, and displays them as a scrollable reference list in the sidebar.  As you scroll through the article, any glossary term that appears in the body text is highlighted with a dotted underline, and the corresponding sidebar entry lights up to show which terms are currently in view.  This gives readers instant access to definitions without leaving the flow of the article, and provides a useful orientation aid for posts that introduce domain-specific vocabulary.
</p>
</div>
</div>
<div id="outline-container-back-links-and-forward-links" class="outline-5">
<h5 id="back-links-and-forward-links"><span class="section-number-5">3.1.1.11.</span> Back Links and Forward Links</h5>
<div class="outline-text-5" id="text-back-links-and-forward-links">
<p>
Back-linking and forward-linking are terms borrowed form the knowledge management space (think org-roam, logseq, Obsidian, etc&#x2026;).  These links can help users follow connections to posts that are referenced and cited by the current post, and provides this in a similar idiom to what users expect with modern knowledge management tools.
</p>
</div>
</div>
<div id="outline-container-tags" class="outline-5">
<h5 id="tags"><span class="section-number-5">3.1.1.12.</span> Tags</h5>
<div class="outline-text-5" id="text-tags">
<p>
On the upper right side of the screen, you will see tags which are applied at the header level.  Article's sharing tags are implicitly related. You can click on any tag to see other articles with that tag (not yet implemented).
</p>
</div>
</div>
<div id="outline-container-minimap" class="outline-5">
<h5 id="minimap"><span class="section-number-5">3.1.1.13.</span> Minimap</h5>
<div class="outline-text-5" id="text-minimap">
<p>
On the far right side of the screen, you will see a minimap of the article you are reading.  This is a small version of the article, and is meant to give you a quick overview of the article's structure.  You can click on any section in the minimap to jump to that section in the article.  This is useful for quickly gauging the size of he document, getting a birds-eye of the document structure, and for quickly navigating to different sections of the article.
</p>

<p>
I particularly like the minimap for when I can recall the visual piece of content I want to look at (eg - where was that pie chart again).  I can find this extremely quickly in the minimap, and then click to jump to that section.
</p>

<p>
The minimap also provides a better sense of locale within the article, as you can see the content far above and far below your current location in the article.
</p>
</div>
</div>
<div id="outline-container-full-text-search" class="outline-5">
<h5 id="full-text-search"><span class="section-number-5">3.1.1.14.</span> Full text search</h5>
<div class="outline-text-5" id="text-full-text-search">
<p>
At the top right of the screen, you will see a search bar.  You can use this search bar to search for any text in the article you are reading.  This is useful for quickly finding specific information in the article, both by keyword and by phrase.  This allows users to search for content related to a literal match or a semantic match with their input query.
</p>
</div>
</div>
</div>
</div>
</div>
<div id="outline-container-features" class="outline-2">
<h2 id="features"><span class="section-number-2">4.</span> Features</h2>
<div class="outline-text-2" id="text-features">
<p>
NOTE the post up until this point has followed narrative structure. 
</p>

<p>
The rest of this article describes and demonstrates the features of this technical diary.  These are in no particular order &#x2013; the rest of this document is long (20k+ words), and reads more like reference material than prose (thank goodness we have all the HUD in the sidelines to help us navigate this wall of text 😉).  
</p>
</div>
</div>
<div id="outline-container-fancy-text--sub--and-superscripts-" class="outline-2">
<h2 id="fancy-text--sub--and-superscripts-"><span class="section-number-2">5.</span> Fancy text (sub- and superscripts)</h2>
<div class="outline-text-2" id="text-fancy-text--sub--and-superscripts-">
</div>
<div id="outline-container-latex-math" class="outline-3">
<h3 id="latex-math"><span class="section-number-3">5.1.</span> latex math</h3>
<div class="outline-text-3" id="text-latex-math">
<p>
To demonstrate, I'll enumerate some famous equations using LaTeX math syntax.  These equations span various fields of mathematics and physics, from basic geometry to complex analysis, number theory, fluid dynamics, and general relativity.
</p>

<ol class="org-ol">
<li><b>Pythagorean Theorem</b></li>
</ol>
<p>
\[
a^2 + b^2 = c^2
\]
</p>

<ol class="org-ol">
<li><b>Quadratic Formula</b></li>
</ol>
<p>
\[
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
\]
</p>

<ol class="org-ol">
<li><b>Euler's Identity</b></li>
</ol>
<p>
\[
e^{i\pi} + 1 = 0
\]
</p>

<ol class="org-ol">
<li><b>Gaussian Integral</b></li>
</ol>
<p>
\[
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
\]
</p>

<ol class="org-ol">
<li><b>Taylor Series for e<sup>x</sup></b></li>
</ol>
<p>
\[
e^x = \sum_{n=0}^{\infty} \frac{x^n}{n!} = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \cdots
\]
</p>

<ol class="org-ol">
<li><b>Cauchy-Riemann Equations</b></li>
</ol>
<p>
\[
\frac{\partial u}{\partial x} = \frac{\partial v}{\partial y} \quad \text{and} \quad \frac{\partial u}{\partial y} = -\frac{\partial v}{\partial x}
\]
</p>

<ol class="org-ol">
<li><b>Fourier Transform</b></li>
</ol>
<p>
\[
\hat{f}(\xi) = \int_{-\infty}^{\infty} f(x) e^{-2\pi i x \xi} dx
\]
</p>

<ol class="org-ol">
<li><b>Riemann Zeta Function</b></li>
</ol>
<p>
\[
\zeta(s) = \sum_{n=1}^{\infty} \frac{1}{n^s} = \prod_{p \text{ prime}} \frac{1}{1 - p^{-s}} \quad \text{for } \Re(s) > 1
\]
</p>

<ol class="org-ol">
<li><b>Navier-Stokes Equation (incompressible flow)</b></li>
</ol>
<p>
\[
\rho \left( \frac{\partial \mathbf{v}}{\partial t} + (\mathbf{v} \cdot \nabla) \mathbf{v} \right) = -\nabla p + \mu \nabla^2 \mathbf{v} + \mathbf{f}
\]
</p>

<ol class="org-ol">
<li><b>Einstein Field Equations (General Relativity)</b></li>
</ol>
<p>
\[
R_{\mu\nu} - \frac{1}{2}g_{\mu\nu}R + \Lambda g_{\mu\nu} = \frac{8\pi G}{c^4}T_{\mu\nu}
\]
</p>
</div>
</div>
<div id="outline-container-subscript-and-superscript" class="outline-3">
<h3 id="subscript-and-superscript"><span class="section-number-3">5.2.</span> <span class="todo TODO">TODO</span> subscript and superscript</h3>
<div class="outline-text-3" id="text-subscript-and-superscript">
<p>
This is an example of subscript: H<sub>2</sub>O, A<sub>i,j</sub>.  NOTE this isn't rendering properly due to a bug I need to fix in the org to html exporter.
</p>
</div>
</div>
</div>
<div id="outline-container-displaying-gifs" class="outline-2">
<h2 id="displaying-gifs"><span class="section-number-2">6.</span> Displaying GIFs</h2>
<div class="outline-text-2" id="text-displaying-gifs">
<p>
Here we demonstrate how <a href="https://en.wikipedia.org/wiki/GIF">GIFs</a> can be displayed.  These are useful for demonstrating usage of UI features, and also for entertainment value. Emacs has nice packages (like <a href="https://github.com/emacsmirror/gif-screencast">gif-screencast</a>) that allow you to create GIF demonstrations of how to use your package, so I plan to use this feature for that.  Also, GIFs can be used to spice up otherwise static content.
</p>
</div>
<div id="outline-container-local-file" class="outline-3">
<h3 id="local-file"><span class="section-number-3">6.1.</span> Local file</h3>
<div class="outline-text-3" id="text-local-file">
<img src="https://www.chiply.dev/graph.gif" alt="Alternative text">
</div>
</div>
<div id="outline-container-remote-file" class="outline-3">
<h3 id="remote-file"><span class="section-number-3">6.2.</span> Remote file</h3>
<div class="outline-text-3" id="text-remote-file">
<p>
These all come from graph <a href="https://giphy.com/">GIPHY</a>:
</p>

<p>
Note I am using a <code>#+begin_export html</code> in the org-document, as it let's me arrange images into arbitrary layouts using custom CSS.  In this case, I'm using flexbox to create a grid of GIFs.
</p>

<div class="chart-3s-plot">
    <img src="https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZ3c0ZnZrMzY5OXhta2hic2N0YnhwcG95dTJkdWFycXI2Zzc4MHFpZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xT9IgzoKnwFNmISR8I/giphy.gif">
    <img src="https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExb2o0c2VyNGV4ZnlzOHF3eXJyc2JpZWRheTNkZWh1MWdoYnAxZHpubiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fmkYSBlJt3XjNF6p9c/giphy.gif">
    <img src="https://media.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3M2JzMjNlMWUydWg0NGkwaW53c2piMjRvOThrZDVueXdreHg1c3N5biZlcD12MV9naWZzX3JlbGF0ZWQmY3Q9Zw/1lvotGQwhzi6O0gQtV/giphy.gif">
    <img src="https://media.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3NWs3ZWl5cTE3c3QzaDB0amFrYTJwMTcwa3ZrNjE3eXliYW15cmRlNCZlcD12MV9naWZzX3JlbGF0ZWQmY3Q9Zw/2tTiCSfEEP5QS5TjGr/giphy.gif" >
    <img src="https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExdzZ1ZHVzOTV6ajh4emNmaDUwYmRxemk2dm11czFndXB2NGE5MjdpZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/13HgwGsXF0aiGY/giphy.gif">
    <img src="https://media.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3M2JzMjNlMWUydWg0NGkwaW53c2piMjRvOThrZDVueXdreHg1c3N5biZlcD12MV9naWZzX3JlbGF0ZWQmY3Q9Zw/RPgGOR8PB3826yfhmR/giphy.gif" >
    <img src="https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExc3d5OGMxbnBsNjBudmN2eWMzNTJkMXk0dTVodDBrZm8zMW1xcXBnNiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/V4NSR1NG2p0KeJJyr5/giphy.gif" >
    <img src="https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExc2ZqcjI3bnFvbDJ4b2c0ZHk1d3ZkdmZ0MHpyM285NXVlMjYzbjExdyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/bJ4TVNYNUympPgcpem/giphy.gif" >
    <img src="https://media.giphy.com/media/v1.Y2lkPWVjZjA1ZTQ3aGNzeWozajF4N3FiY24zeTVjYW90MjU2NjY3cHA0NXppZ3hpeDdsMyZlcD12MV9naWZzX3JlbGF0ZWQmY3Q9Zw/l0IyeheChYxx2byDu/giphy.gif" >
</div>

<style>
.chart-3s-plot {
    display: flex;
    flex-wrap: wrap;
    gap: 0;
    }


.chart-3s-plot img {
    width: 33.33%; /* Ensure the image spans the full width of its grid cell */
    height: auto; /* Maintain aspect ratio */
    object-fit: cover; /* Maintain aspect ratio and cover the cell */
    display: block;        /* removes inline gap! */
    margin: 0 !important;
}
</style>
</div>
</div>
<div id="outline-container-interactive-gif" class="outline-3">
<h3 id="interactive-gif"><span class="section-number-3">6.3.</span> Interactive GIF</h3>
<div class="outline-text-3" id="text-interactive-gif">
<img src="https://www.chiply.dev/demo.gif" alt="Demo GIF" data-interactive-gif>
</div>
</div>
<div id="outline-container-video-demo" class="outline-3">
<h3 id="video-demo"><span class="section-number-3">6.4.</span> Video Demo</h3>
<div class="outline-text-3" id="text-video-demo">
<video src="https://www.chiply.dev/videos/demo_video.mp4" data-interactive-video>
</video>
</div>
</div>
</div>
<div id="outline-container-interactive-data-visualizations" class="outline-2">
<h2 id="interactive-data-visualizations"><span class="section-number-2">7.</span> Interactive Data Visualizations</h2>
<div class="outline-text-2" id="text-interactive-data-visualizations">
<p>
HTML charts are great for technical blogging.  Personaly, I'm inspired by examples of <a href="https://en.wikipedia.org/wiki/Digital_journalism">digital journalism</a> from the New York Times that use interactive charts to tell stories with data.  The NYT builds these with <a href="https://github.com/d3/d3">D3.js</a>, but <a href="https://plotly.com/javascript/3d-charts/">plotly</a> is a great alternative for people who don't want to code everything from scratch, and plotly is also built on top of D3.js.  Plotly covers almost all chart types, and has great integration with python, R, and javascript, all concise languages which are excellent for demonsrtating the logic behind the data visualization in question.  In fact, plotly beats D3 for this in my opinion, you wouldn't include D3 code in the blog as it is too verbose, whereas the plotly python APIs are concise (especially <a href="https://plotly.com/python/plotly-express/">px</a>).
</p>

<p>
Plotly is therefore my choice for adding interactive charts to my technical blog.
</p>

<p>
Here I demonstrate adding a plotly chart using org-babel and python.  The code block below generates a plotly bar chart and saves it as an html file.  The html file is then included in the blog using an iframe.  Note there are probably better ways to do this, both from a viz generation and rendering perspective, but this is a good start, and unlocks ploty's full universe of chart types for my blog.
</p>
</div>
<div id="outline-container-bar-chart" class="outline-3">
<h3 id="bar-chart"><span class="section-number-3">7.1.</span> Bar chart</h3>
<div class="outline-text-3" id="text-bar-chart">
<p>
I'm leveraging utility functions that are defined in a custom python package &#x2013; you'll see the import here
</p>

<div id="my_interactive_plot" class="plotly-plot"></div>
</div>
</div>
<div id="outline-container-dist-plot" class="outline-3">
<h3 id="dist-plot"><span class="section-number-3">7.2.</span> Dist Plot</h3>
<div class="outline-text-3" id="text-dist-plot">
<div id="my_interactive_dist_plot" class="plotly-plot"></div>
</div>
</div>
<div id="outline-container-animattions" class="outline-3">
<h3 id="animattions"><span class="section-number-3">7.3.</span> Animattions</h3>
<div class="outline-text-3" id="text-animattions">
<div id="my_interactive_animation_plot" class="plotly-plot"></div>
</div>
</div>
<div id="outline-container-3d-charts" class="outline-3">
<h3 id="3d-charts"><span class="section-number-3">7.4.</span> 3D charts</h3>
<div class="outline-text-3" id="text-3d-charts">
<p>
Note that I have not exported the plotly code here as there is a lot of it for the 9 3D charts below.
</p>
</div>
<div id="outline-container-output" class="outline-4">
<h4 id="output"><span class="section-number-4">7.4.1.</span> Output</h4>
<div class="outline-text-4" id="text-output">
<div class="chart-3d-plot">
    <div id="my_interactive_3D_scatter" class="plotly-plot"></div>
    <div id="my_interactive_3d_line" class="plotly-plot"></div>
</div>

<style>

.chart-3d-plot {
    display: grid;
    grid-template-rows: repeat(2, 1fr);
    gap: 0;
    }


.chart-3d-plot div {
    height: auto; /* Maintain aspect ratio */
    object-fit: cover; /* Maintain aspect ratio and cover the cell */
    display: block;        /* removes inline gap! */
    margin: 0 !important;
}
</style>
</div>
</div>
</div>
<div id="outline-container-arrangement" class="outline-3">
<h3 id="arrangement"><span class="section-number-3">7.5.</span> <span class="todo TODO">TODO</span> Arrangement</h3>
<div class="outline-text-3" id="text-arrangement">
<p>
This doesn't show up in the webpage, but I can include html inline using #+begin<sub>export</sub> html and #+end<sub>export</sub> blocks in the org mode document.  This is useful for insterting iframes (think previews of other websites, plotly charts, full webapps, etc&#x2026;).  html, js, and css can be included, here I'm using CSS grid too display iframes side my side.
</p>
</div>
</div>
</div>
<div id="outline-container-software-architecture-diagrams" class="outline-2">
<h2 id="software-architecture-diagrams"><span class="section-number-2">8.</span> Software Architecture Diagrams</h2>
<div class="outline-text-2" id="text-software-architecture-diagrams">
<p>
Sometimes structure that needs to be described cannot be adequately represented using just text, tables, and lists.  Diagrams are a great way to represent complex structures visually.
</p>

<p>
<a href="https://github.com/arnm/ob-mermaid">org-mermaid</a> is a great extension to <a href="https://orgmode.org/worg/org-contrib/babel/intro.htmlhttps://orgmode.org/worg/org-contrib/babel/intro.html">org-babel</a>, that allows you to create mermaid diagrams-as-code within org-mode documents.  <a href="https://orgmode.org/worg/exporters/ox-overview.html">ox</a> then takes care of exporing the code, the output, or both.
</p>

<p>
The DAG is indespensible in most contexts, but espeically so in technical blogging.  Being able to quickly and easily create diagrams to illustrate concepts is very useful, and the fact that I can maintain them with code is a dream.
</p>

<p>
Of course, you could use anything that outputs a chart (like <a href="https://diagrams.mingrammer.com/">diagrams</a>), but mermaid is basically ubiquitous, so I try to use that as much as I can.
</p>
<div class="org-src-container">
<pre class="src src-mermaid">graph TD;
    A--&gt;B;
    A--&gt;C;
    B--&gt;D;
    C--&gt;D;
</pre>
</div>
<div class="org-src-container">
<pre class="src src-mermaid">graph TD;
    A--&gt;B;
    A--&gt;C;
    B--&gt;D;
    C--&gt;D;
</pre>
</div>
</div>
<div id="outline-container-diagram-1--simple-microservices-architecture" class="outline-3">
<h3 id="diagram-1--simple-microservices-architecture"><span class="section-number-3">8.1.</span> Diagram 1: Simple Microservices Architecture</h3>
<div class="outline-text-3" id="text-diagram-1--simple-microservices-architecture">
<div class="org-src-container">
<pre class="src src-mermaid">graph TD;
    Client[Client Application]--&gt;Gateway[API Gateway];
    Gateway--&gt;AuthService[Auth Service];
    Gateway--&gt;UserService[User Service];
    Gateway--&gt;ProductService[Product Service];
    UserService--&gt;UserDB[(User Database)];
    ProductService--&gt;ProductDB[(Product Database)];
    AuthService--&gt;Cache[(Redis Cache)];
</pre>
</div>
</div>
</div>
<div id="outline-container-diagram-2--event-driven-architecture" class="outline-3">
<h3 id="diagram-2--event-driven-architecture"><span class="section-number-3">8.2.</span> Diagram 2: Event-Driven Architecture</h3>
<div class="outline-text-3" id="text-diagram-2--event-driven-architecture">
<div class="org-src-container">
<pre class="src src-mermaid">graph TD;
    WebApp[Web Application]--&gt;LoadBalancer[Load Balancer];
    MobileApp[Mobile Application]--&gt;LoadBalancer;
    LoadBalancer--&gt;APIGateway[API Gateway];
    APIGateway--&gt;OrderService[Order Service];
    APIGateway--&gt;PaymentService[Payment Service];
    APIGateway--&gt;NotificationService[Notification Service];
    OrderService--&gt;EventBus[Message Queue/Event Bus];
    PaymentService--&gt;EventBus;
    NotificationService--&gt;EventBus;
    EventBus--&gt;InventoryService[Inventory Service];
    EventBus--&gt;ShippingService[Shipping Service];
    EventBus--&gt;AnalyticsService[Analytics Service];
    OrderService--&gt;OrderDB[(Order DB)];
    PaymentService--&gt;PaymentDB[(Payment DB)];
    InventoryService--&gt;InventoryDB[(Inventory DB)];
    ShippingService--&gt;ShippingDB[(Shipping DB)];
    AnalyticsService--&gt;DataWarehouse[(Data Warehouse)];
</pre>
</div>
</div>
</div>
<div id="outline-container-diagram-3--cloud-native-architecture-with-kubernetes" class="outline-3">
<h3 id="diagram-3--cloud-native-architecture-with-kubernetes"><span class="section-number-3">8.3.</span> Diagram 3: Cloud-Native Architecture with Kubernetes</h3>
<div class="outline-text-3" id="text-diagram-3--cloud-native-architecture-with-kubernetes">
<div class="org-src-container">
<pre class="src src-mermaid">graph TD;
    Users[Users]--&gt;CDN[CDN];
    CDN--&gt;Ingress[Ingress Controller];
    Ingress--&gt;ServiceMesh[Service Mesh/Istio];
    ServiceMesh--&gt;AuthPod[Auth Pod];
    ServiceMesh--&gt;UserPod[User Service Pod];
    ServiceMesh--&gt;OrderPod[Order Service Pod];
    ServiceMesh--&gt;PaymentPod[Payment Service Pod];
    AuthPod--&gt;ConfigMap[ConfigMap];
    AuthPod--&gt;Secret[Secrets];
    UserPod--&gt;UserStateful[User StatefulSet];
    OrderPod--&gt;OrderStateful[Order StatefulSet];
    PaymentPod--&gt;PaymentStateful[Payment StatefulSet];
    UserStateful--&gt;PVC1[Persistent Volume];
    OrderStateful--&gt;PVC2[Persistent Volume];
    PaymentStateful--&gt;PVC3[Persistent Volume];
    ServiceMesh--&gt;MessageQueue[RabbitMQ Cluster];
    MessageQueue--&gt;WorkerPod1[Worker Pod 1];
    MessageQueue--&gt;WorkerPod2[Worker Pod 2];
    MessageQueue--&gt;WorkerPod3[Worker Pod 3];
    WorkerPod1--&gt;ElasticSearch[ElasticSearch Cluster];
    WorkerPod2--&gt;ElasticSearch;
    WorkerPod3--&gt;ElasticSearch;
    ServiceMesh--&gt;CacheCluster[Redis Cluster];
    ServiceMesh--&gt;MonitoringStack[Prometheus + Grafana];
    MonitoringStack--&gt;AlertManager[Alert Manager];
</pre>
</div>
</div>
</div>
<div id="outline-container-diagram-4--global-multi-region-architecture" class="outline-3">
<h3 id="diagram-4--global-multi-region-architecture"><span class="section-number-3">8.4.</span> Diagram 4: Global Multi-Region Architecture</h3>
<div class="outline-text-3" id="text-diagram-4--global-multi-region-architecture">
<div class="org-src-container">
<pre class="src src-mermaid">graph LR;
    GlobalUsers[Global Users]--&gt;GlobalDNS[Global DNS/Route53];
    GlobalDNS--&gt;USRegion[US-East Region];
    GlobalDNS--&gt;EURegion[EU-West Region];
    GlobalDNS--&gt;APRegion[Asia-Pacific Region];

    USRegion--&gt;USCDN[US CDN/CloudFront];
    USCDN--&gt;USALB[US Application Load Balancer];
    USALB--&gt;USK8S[US Kubernetes Cluster];
    USK8S--&gt;USServices[US Microservices];
    USServices--&gt;USRDS[US RDS Multi-AZ];
    USServices--&gt;USDynamo[US DynamoDB Global Table];
    USServices--&gt;USKafka[US Kafka Cluster];

    EURegion--&gt;EUCDN[EU CDN/CloudFront];
    EUCDN--&gt;EUALB[EU Application Load Balancer];
    EUALB--&gt;EUK8S[EU Kubernetes Cluster];
    EUK8S--&gt;EUServices[EU Microservices];
    EUServices--&gt;EURDS[EU RDS Multi-AZ];
    EUServices--&gt;EUDynamo[EU DynamoDB Global Table];
    EUServices--&gt;EUKafka[EU Kafka Cluster];

    APRegion--&gt;APCDN[AP CDN/CloudFront];
    APCDN--&gt;APALB[AP Application Load Balancer];
    APALB--&gt;APK8S[AP Kubernetes Cluster];
    APK8S--&gt;APServices[AP Microservices];
    APServices--&gt;APRDS[AP RDS Multi-AZ];
    APServices--&gt;APDynamo[AP DynamoDB Global Table];
    APServices--&gt;APKafka[AP Kafka Cluster];

    USDynamo&lt;--&gt;EUDynamo;
    EUDynamo&lt;--&gt;APDynamo;
    USDynamo&lt;--&gt;APDynamo;

    USKafka--&gt;GlobalStream[Global Kinesis Data Stream];
    EUKafka--&gt;GlobalStream;
    APKafka--&gt;GlobalStream;

    GlobalStream--&gt;DataLake[S3 Data Lake];
    DataLake--&gt;MLPipeline[SageMaker ML Pipeline];
    DataLake--&gt;BatchProcessing[EMR Batch Processing];
    DataLake--&gt;RealtimeAnalytics[Kinesis Analytics];

    USK8S--&gt;Monitoring[Global Monitoring Stack];
    EUK8S--&gt;Monitoring;
    APK8S--&gt;Monitoring;
    Monitoring--&gt;Datadog[Datadog/NewRelic];
    Monitoring--&gt;PagerDuty[PagerDuty Alerts];

    USServices--&gt;GlobalAuth[Global Auth0/Okta];
    EUServices--&gt;GlobalAuth;
    APServices--&gt;GlobalAuth;
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-code" class="outline-2">
<h2 id="code"><span class="section-number-2">9.</span> Code&#xa0;&#xa0;&#xa0;<span class="tag"><span class="literateProgramming">literateProgramming</span></span></h2>
<div class="outline-text-2" id="text-code">
<p>
Rendering and executing code is a critical feature for technical blogging.
</p>

<p>
Because of <a href="https://orgmode.org/manual/Working-with-Source-Code.html">org-mode's babel system</a>, code blocks can be included in the document.  These can be executed either as you write, or when the document is exported, and the results can be included in the output.
</p>

<p>
This process of writing code, output, and narrative text together is called <a href="https://en.wikipedia.org/wiki/Literate_programming">literate programming</a>, and is a powerful way to communicate technical concepts.
</p>

<p>
My main languages are bash, elisp, python, R, SQL, and javascript/TypeScript.  Having a way to include code snippets that can be executed and have the results included in the document is very useful for technical blogging.
</p>

<p>
org-babel is very powerful and flexible, and you can think of it like a language-agnostic form of <a href="https://jupyter.org/">Jupyter Notebooks</a>.
</p>

<p>
Here are a couple of useful examples.
</p>
</div>
<div id="outline-container-bash" class="outline-3">
<h3 id="bash"><span class="section-number-3">9.1.</span> bash</h3>
<div class="outline-text-3" id="text-bash">
<p>
Here's an interesting bash script that implements the <a href="https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes">Sieve of Eratosthenes</a> algorithm to find <a href="https://en.wikipedia.org/wiki/Prime_number">prime numbers</a> and visualizes them with <a href="https://en.wikipedia.org/wiki/ASCII_art">ASCII art</a><sup><a id="fnr.6" class="footref" href="https://www.chiply.dev/#fn.6" role="doc-backlink">6</a></sup>:
</p>

<div class="org-src-container">
<pre class="src src-bash">#!/bin/bash

# Sieve of Eratosthenes with Simple Text Visualization
# Finds all prime numbers up to a given limit and creates a visual representation

LIMIT=100
echo "=== Sieve of Eratosthenes: Finding Primes up to $LIMIT ==="
echo

# Initialize array - 1 means potential prime, 0 means composite
declare -a sieve
for ((i=0; i&lt;=LIMIT; i++)); do
    sieve[i]=1
done

# 0 and 1 are not prime
sieve[0]=0
sieve[1]=0

# Implement sieve algorithm
for ((p=2; p*p&lt;=LIMIT; p++)); do
    if [[ ${sieve[p]} -eq 1 ]]; then
        # Mark all multiples of p as composite
        for ((i=p*p; i&lt;=LIMIT; i+=p)); do
            sieve[i]=0
        done
    fi
done

# Collect primes and create visualization
primes=()
echo "Prime Visualization (P = prime, . = composite):"
echo "+---------------------+"

# Create visual grid (10 numbers per row, starting from 2)
for ((row=0; row&lt;10; row++)); do
    printf "| "
    for ((col=0; col&lt;10; col++)); do
        num=$((row * 10 + col + 2))
        if [[ $num -le $LIMIT ]]; then
            if [[ ${sieve[num]} -eq 1 ]]; then
                primes+=($num)
                printf "P "
            else
                printf ". "
            fi
        else
            printf "  "
        fi
    done
    printf "|\n"
done

echo "+---------------------+"
echo

# Create a numbered reference grid with proper spacing
echo "Number Reference Grid:"
echo "+-------------------------------+"
for ((row=0; row&lt;10; row++)); do
    printf "| "
    for ((col=0; col&lt;10; col++)); do
        num=$((row * 10 + col + 2))
        if [[ $num -le $LIMIT ]]; then
            printf "%2d " $num
        else
            printf "   "
        fi
    done
    printf "|\n"
done
echo "+-------------------------------+"
echo

# Statistics
echo "PRIME STATISTICS:"
echo "  Total numbers checked: $LIMIT"
echo "  Primes found: ${#primes[@]}"
density=$(echo "scale=1; ${#primes[@]}*100/$LIMIT" | bc -l 2&gt;/dev/null || echo "25.0")
echo "  Density: ${density}%"
echo

# Show all primes in groups of 10
echo "All Primes Found:"
printf "  "
for ((i=0; i&lt;${#primes[@]}; i++)); do
    printf "%3d " ${primes[i]}
    if [[ $(((i+1) % 10)) -eq 0 ]]; then
        printf "\n  "
    fi
done
echo
echo

# Find twin primes (primes that differ by 2)
echo "Twin Prime Pairs (p, p+2):"
printf "  "
count=0
for ((i=0; i&lt;${#primes[@]}-1; i++)); do
    current=${primes[i]}
    next=${primes[i+1]}
    if [[ $((next - current)) -eq 2 ]]; then
        printf "(%d,%d) " $current $next
        ((count++))
        if [[ $((count % 4)) -eq 0 ]]; then
            printf "\n  "
        fi
    fi
done
echo
echo "  Found $count twin prime pairs"
echo

# Prime gaps analysis
echo "Prime Gap Analysis:"
max_gap=0
gap_start=0
gap_end=0
echo "  Gaps larger than 4:"
printf "  "
gap_count=0
for ((i=0; i&lt;${#primes[@]}-1; i++)); do
    gap=$((${primes[i+1]} - ${primes[i]}))
    if [[ $gap -gt $max_gap ]]; then
        max_gap=$gap
        gap_start=${primes[i]}
        gap_end=${primes[i+1]}
    fi
    if [[ $gap -gt 4 ]]; then
        printf "%d-&gt;%d(gap:%d) " ${primes[i]} ${primes[i+1]} $gap
        ((gap_count++))
        if [[ $((gap_count % 3)) -eq 0 ]]; then
            printf "\n  "
        fi
    fi
done
echo
echo "  Largest gap: $max_gap (between $gap_start and $gap_end)"

echo
echo "Algorithm completed successfully!"
echo "Time complexity: O(n log log n)"
echo "Space complexity: O(n)"
</pre>
</div>

<div class="org-src-container">
<pre class="src src-text">=== Sieve of Eratosthenes: Finding Primes up to 100 ===

Prime Visualization (P = prime, . = composite):
+---------------------+
| P P . P . P . . . P |
| . P . . . P . P . . |
| . P . . . . . P . P |
| . . . . . P . . . P |
| . P . . . P . . . . |
| . P . . . . . P . P |
| . . . . . P . . . P |
| . P . . . . . P . . |
| . P . . . . . P . . |
| . . . . . P . . .   |
+---------------------+

Number Reference Grid:
+-------------------------------+
|  2  3  4  5  6  7  8  9 10 11 |
| 12 13 14 15 16 17 18 19 20 21 |
| 22 23 24 25 26 27 28 29 30 31 |
| 32 33 34 35 36 37 38 39 40 41 |
| 42 43 44 45 46 47 48 49 50 51 |
| 52 53 54 55 56 57 58 59 60 61 |
| 62 63 64 65 66 67 68 69 70 71 |
| 72 73 74 75 76 77 78 79 80 81 |
| 82 83 84 85 86 87 88 89 90 91 |
| 92 93 94 95 96 97 98 99 100   |
+-------------------------------+

PRIME STATISTICS:
  Total numbers checked: 100
  Primes found: 25
  Density: 25.0%

All Primes Found:
    2   3   5   7  11  13  17  19  23  29 
   31  37  41  43  47  53  59  61  67  71 
   73  79  83  89  97 

Twin Prime Pairs (p, p+2):
  (3,5) (5,7) (11,13) (17,19) 
  (29,31) (41,43) (59,61) (71,73) 

  Found 8 twin prime pairs

Prime Gap Analysis:
  Gaps larger than 4:
  23-&gt;29(gap:6) 31-&gt;37(gap:6) 47-&gt;53(gap:6) 
  53-&gt;59(gap:6) 61-&gt;67(gap:6) 73-&gt;79(gap:6) 
  83-&gt;89(gap:6) 89-&gt;97(gap:8) 
  Largest gap: 8 (between 89 and 97)

Algorithm completed successfully!
Time complexity: O(n log log n)
Space complexity: O(n)
</pre>
</div>
</div>
</div>
<div id="outline-container-python" class="outline-3">
<h3 id="python"><span class="section-number-3">9.2.</span> python</h3>
<div class="outline-text-3" id="text-python">
</div>
<div id="outline-container--add-language--using-venv" class="outline-4">
<h4 id="-add-language--using-venv"><span class="section-number-4">9.2.1.</span> <span class="todo TODO">TODO</span> (add language) using venv&#xa0;&#xa0;&#xa0;<span class="tag"><span class="virtualEnvironments">virtualEnvironments</span></span></h4>
<div class="outline-text-4" id="text--add-language--using-venv">
<p>
Venvs are set in the <a href="https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html">.dir-locals.el</a> file for this project.  This allows different code blocks to use different virtual environments.
</p>
</div>
</div>
<div id="outline-container-code-example" class="outline-4">
<h4 id="code-example"><span class="section-number-4">9.2.2.</span> Code example</h4>
<div class="outline-text-4" id="text-code-example">
<p>
This is how you write code with python<sup><a id="fnr.7" class="footref" href="https://www.chiply.dev/#fn.7" role="doc-backlink">7</a></sup>
</p>
<div class="org-src-container">
<pre class="src src-python">from dataclasses import dataclass

from polyfactory.factories import DataclassFactory


@dataclass
class Person:
    name: str
    age: float
    height: float
    weight: float


class PersonFactory(DataclassFactory[Person]):
    ...


person_instance = PersonFactory.build()
print(person_instance)

</pre>
</div>

<div class="org-src-container">
<pre class="src src-python">Person(name='JmjUtskpYrdRYTTbRirA', age=1728311337.84688, height=52.7746341086379, weight=-618160.650763342)
</pre>
</div>
</div>
</div>
<div id="outline-container-using-multiple-venvs-via-session" class="outline-4">
<h4 id="using-multiple-venvs-via-session"><span class="section-number-4">9.2.3.</span> <span class="todo TODO">TODO</span> using multiple venvs via session</h4>
<div class="outline-text-4" id="text-using-multiple-venvs-via-session">
<p>
Use case is actually comomon &#x2013; demonosrating he difference between two versions of a library, demonstrating a dependency issue.
</p>
</div>
</div>
</div>
</div>
<div id="outline-container-transclusion" class="outline-2">
<h2 id="transclusion"><span class="section-number-2">10.</span> Transclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="transclusion">transclusion</span></span></h2>
<div class="outline-text-2" id="text-transclusion">
<p>
The subheading <code>Demo heading</code> is actually 'transcluded' text from another post on this blog.  In other words, this is a verbatim section, from another article on this blog, that I have embedded visually here (you can verify this by going to that aricle and reading this same text here).
</p>

<p>
This is made possible by org-mode's built in <a href="https://orgmode.org/manual/Include-Files.html">file inclusion feature</a>.
</p>

<p>
See the transclusion below - again, this heading is pulled straight out of 
</p>
</div>
<div id="outline-container-demo-heading" class="outline-3 transcluded">
<h3 id="demo-heading"><span class="section-number-3">10.1.</span> Demo heading</h3>
<div class="outline-text-3" id="text-demo-heading">
<p>
This is a demo post &#x2013; This content is written in post1.org but is actually intended to be transcluded from another file (post0.org).  That's why you utlimately see this content here.
</p>
</div>
</div>
<div id="outline-container-transclusion-styling" class="outline-3">
<h3 id="transclusion-styling"><span class="section-number-3">10.2.</span> <span class="todo TODO">TODO</span> transclusion styling</h3>
<div class="outline-text-3" id="text-transclusion-styling">
<p>
Transcluded sections are styled differently to signal to the user that a verbatim section from a separate article is being referenced.
</p>
</div>
</div>
</div>
<div id="outline-container-tags" class="outline-2">
<h2 id="tags"><span class="section-number-2">11.</span> Tags&#xa0;&#xa0;&#xa0;<span class="tag"><span class="tag0">tag0</span>&#xa0;<span class="tag1">tag1</span></span></h2>
<div class="outline-text-2" id="text-tags">
<p>
Tags can be added to headings using org-mode's tag syntax.  You can see 'tag1' and 'tag2' applied above.
</p>
</div>
<div id="outline-container-why-use-tags-" class="outline-3">
<h3 id="why-use-tags-"><span class="section-number-3">11.1.</span> Why use tags?</h3>
<div class="outline-text-3" id="text-why-use-tags-">
<p>
Most blogs organize their content hierarchically using categories and subcategories in something like an index page.  But this rigid tree data structure doesn't reflect the natural organizational structure of content that may fit into multiple categories.  Tags allow for this kind of non-hierarchical organization.
</p>
</div>
</div>
<div id="outline-container-inline-tags" class="outline-3">
<h3 id="inline-tags"><span class="section-number-3">11.2.</span> <span class="todo TODO">TODO</span> inline tags</h3>
<div class="outline-text-3" id="text-inline-tags">
<p>
Inline tags are also useful.  This is not yet implemented as org-mode does not support inline tags natively.
</p>
</div>
</div>
</div>
<div id="outline-container-links" class="outline-2">
<h2 id="links"><span class="section-number-2">12.</span> Links</h2>
<div class="outline-text-2" id="text-links">
<p>
My blog's <a href="https://en.wikipedia.org/wiki/Raison_d%27%C3%AAtre">Raison d'être</a> is to allow me to share knowledge with others.  Links are a critical part of this, as they allow me to reference other content, both within my own blog and externally.
</p>

<p>
As such, my blog is aware of the different 'types' of links and visually distinguishes between them (nyi).
</p>
</div>
<div id="outline-container-link-types" class="outline-3">
<h3 id="link-types"><span class="section-number-3">12.1.</span> Link types</h3>
<div class="outline-text-3" id="text-link-types">
<p>
Note this list is incomprehensive, and defined by me, but it works for my purposes.
</p>
</div>
<div id="outline-container-intra-post-links" class="outline-4">
<h4 id="intra-post-links"><span class="section-number-4">12.1.1.</span> Intra-post links</h4>
<div class="outline-text-4" id="text-intra-post-links">
<p>
It is possible and useful to link to different sections within a single post.  This is done using standard org-mode link syntax.
</p>

<p>
Example: <a href="https://www.chiply.dev/#why-use-tags-">Why use tags?</a>.
</p>
</div>
</div>
<div id="outline-container-inter-post-links" class="outline-4">
<h4 id="inter-post-links"><span class="section-number-4">12.1.2.</span> Inter-post links</h4>
<div class="outline-text-4" id="text-inter-post-links">
<p>
I can link to other sections in the document.  
</p>

<p>
Example: <a href="https://www.chiply.dev/post1">Post 1</a>
</p>
</div>
</div>
<div id="outline-container-external-links" class="outline-4">
<h4 id="external-links"><span class="section-number-4">12.1.3.</span> External links</h4>
<div class="outline-text-4" id="text-external-links">
<p>
Note that external links appear in the bibliography section in the deskop version of the blog.
</p>

<ul class="org-ul">
<li><a href="https://svelte.dev/docs/svelte/$state">svelte docs state</a></li>
<li><a href="https://svelte.dev/docs/svelte/$derived">https://svelte.dev/docs/svelte/$derived</a></li>
<li><a href="https://svelte.dev/docs/svelte/$effect">https://svelte.dev/docs/svelte/$effect</a></li>
<li><a href="https://svelte.dev/docs/svelte/$props">https://svelte.dev/docs/svelte/$props</a></li>
<li><a href="https://svelte.dev/docs/svelte/$bindable">https://svelte.dev/docs/svelte/$bindable</a></li>
<li><a href="https://svelte.dev/docs/svelte/$inspect">https://svelte.dev/docs/svelte/$inspect</a></li>
</ul>
</div>
</div>
<div id="outline-container-pseudo-links" class="outline-4">
<h4 id="pseudo-links"><span class="section-number-4">12.1.4.</span> Pseudo links</h4>
<div class="outline-text-4" id="text-pseudo-links">
<p>
Some things are clickable and open gates to other content, like the <code>[[*** Tags][tags]]</code> HUD component, the <code>[[*** *Full text search*]]</code>, and the <code>[[*** *Post navigation*]]</code>.  These are not really links, but they behave like links, so I style them as such.
</p>

<p>
Not everything clickable is style like a link &#x2013; buttons, for example, are not styled like links.  Headings, which are clickable to collapse/expand sections, are also not styled like links.  The heuristic is that anything that could take you to anothe piece of conetnt gets styled like a link.
</p>
</div>
</div>
</div>
<div id="outline-container-link-styling" class="outline-3">
<h3 id="link-styling"><span class="section-number-3">12.2.</span> Link styling</h3>
<div class="outline-text-3" id="text-link-styling">
<p>
Internal links should be rendered differetly than external links as so the user knows ahead of time if a reference is within the blog or outside.
</p>

<p>
Anchor links are rendererd with dotted underlines, links to posts within this blog are rendered with solid underlines, and external links are as typical links with solid underlines.
</p>
</div>
</div>
<div id="outline-container-link-previews" class="outline-3">
<h3 id="link-previews"><span class="section-number-3">12.3.</span> <span class="todo TODO">TODO</span> Link previews</h3>
<div class="outline-text-3" id="text-link-previews">
<p>
It is useful to see a preview of a link before clicking on it &#x2013; maybe you're just curious what the page looks like, or more practically, maybe you want to quickly gleen the information from a popup withouot navigating away from the current page, disrupting your reading flow. <a href="https://en.wikipedia.org/wiki/Wikipedia:Tools/Navigation_popups">Wikipedia's navigation popups</a> are a great example of this feature in action, and <a href="https://share.google/aimode/JJB7J1jk8ZyBZy0sY">google has something similar you can enable in chrome</a>.
</p>

<p>
While useful, these only let you preview 1 degree out from you current address.  What would be even more useful is a way to preview links <b>recursively</b>, so you could see the content of links within links, and so on.  This would allow you to quickly navigate through a web of content without ever leaving the current page.
</p>

<p>
This is implemented here for all links &#x2013; simply hover over any link to see a preview of the content.
</p>
</div>
</div>
</div>
<div id="outline-container-structured-text--tables--lists--sub-and-superscripts-" class="outline-2">
<h2 id="structured-text--tables--lists--sub-and-superscripts-"><span class="section-number-2">13.</span> Structured text (tables, lists, sub and superscripts)</h2>
<div class="outline-text-2" id="text-structured-text--tables--lists--sub-and-superscripts-">
</div>
<div id="outline-container-tables" class="outline-3">
<h3 id="tables"><span class="section-number-3">13.1.</span> tables</h3>
<div class="outline-text-3" id="text-tables">
<table id="orgbf2f573" border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-right" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Country</th>
<th scope="col" class="org-right">Population (millions)</th>
<th scope="col" class="org-left">Continent</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">China</td>
<td class="org-right">1420</td>
<td class="org-left">Asia</td>
</tr>

<tr>
<td class="org-left">India</td>
<td class="org-right">1400</td>
<td class="org-left">Asia</td>
</tr>

<tr>
<td class="org-left">USA</td>
<td class="org-right">332</td>
<td class="org-left">North America</td>
</tr>

<tr>
<td class="org-left">Brazil</td>
<td class="org-right">215</td>
<td class="org-left">South America</td>
</tr>

<tr>
<td class="org-left">Nigeria</td>
<td class="org-right">216</td>
<td class="org-left">Africa</td>
</tr>
</tbody>
</table>

<p>
<br />
</p>

<table id="org80eb65b" border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-right" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Month</th>
<th scope="col" class="org-left">Sales ($)</th>
<th scope="col" class="org-right">Growth (%)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Jan</td>
<td class="org-left">12,000</td>
<td class="org-right">5.0</td>
</tr>

<tr>
<td class="org-left">Feb</td>
<td class="org-left">14,050</td>
<td class="org-right">17.1</td>
</tr>

<tr>
<td class="org-left">Mar</td>
<td class="org-left">13,500</td>
<td class="org-right">-3.9</td>
</tr>

<tr>
<td class="org-left">Apr</td>
<td class="org-left">15,200</td>
<td class="org-right">12.6</td>
</tr>
</tbody>
</table>

<p>
<br />
</p>

<table id="org5ff8b21" border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Feature</th>
<th scope="col" class="org-left">Svelte 5 Support</th>
<th scope="col" class="org-left">Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Reactivity</td>
<td class="org-left">Yes</td>
<td class="org-left">Automatic &amp; simple</td>
</tr>

<tr>
<td class="org-left">TypeScript Support</td>
<td class="org-left">Yes</td>
<td class="org-left">Built-in, first-class</td>
</tr>

<tr>
<td class="org-left">Transitions</td>
<td class="org-left">Yes</td>
<td class="org-left">Easy to use animations</td>
</tr>

<tr>
<td class="org-left">Stores</td>
<td class="org-left">Yes</td>
<td class="org-left">Simple shared state</td>
</tr>

<tr>
<td class="org-left">Actions</td>
<td class="org-left">Yes</td>
<td class="org-left">Custom DOM behaviors</td>
</tr>
</tbody>
</table>

<p>
<br />
</p>

<p>
This one is a really big table and demonstrates the scrolling behavior for wide tables.
</p>

<table id="org57c92ff" border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">sepal<sub>length</sub></th>
<th scope="col" class="org-right">sepal<sub>width</sub></th>
<th scope="col" class="org-right">petal<sub>length</sub></th>
<th scope="col" class="org-right">petal<sub>width</sub></th>
<th scope="col" class="org-left">species</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.5</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.9</td>
<td class="org-right">3.0</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.7</td>
<td class="org-right">3.2</td>
<td class="org-right">1.3</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.6</td>
<td class="org-right">3.1</td>
<td class="org-right">1.5</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.6</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.4</td>
<td class="org-right">3.9</td>
<td class="org-right">1.7</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.6</td>
<td class="org-right">3.4</td>
<td class="org-right">1.4</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.4</td>
<td class="org-right">1.5</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.4</td>
<td class="org-right">2.9</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.9</td>
<td class="org-right">3.1</td>
<td class="org-right">1.5</td>
<td class="org-right">0.1</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.4</td>
<td class="org-right">3.7</td>
<td class="org-right">1.5</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.8</td>
<td class="org-right">3.4</td>
<td class="org-right">1.6</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.8</td>
<td class="org-right">3.0</td>
<td class="org-right">1.4</td>
<td class="org-right">0.1</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.3</td>
<td class="org-right">3.0</td>
<td class="org-right">1.1</td>
<td class="org-right">0.1</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">4.0</td>
<td class="org-right">1.2</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">4.4</td>
<td class="org-right">1.5</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.4</td>
<td class="org-right">3.9</td>
<td class="org-right">1.3</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.5</td>
<td class="org-right">1.4</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">3.8</td>
<td class="org-right">1.7</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.8</td>
<td class="org-right">1.5</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.4</td>
<td class="org-right">3.4</td>
<td class="org-right">1.7</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.7</td>
<td class="org-right">1.5</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.6</td>
<td class="org-right">3.6</td>
<td class="org-right">1.0</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.3</td>
<td class="org-right">1.7</td>
<td class="org-right">0.5</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.8</td>
<td class="org-right">3.4</td>
<td class="org-right">1.9</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.0</td>
<td class="org-right">1.6</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.4</td>
<td class="org-right">1.6</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.2</td>
<td class="org-right">3.5</td>
<td class="org-right">1.5</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.2</td>
<td class="org-right">3.4</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.7</td>
<td class="org-right">3.2</td>
<td class="org-right">1.6</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.8</td>
<td class="org-right">3.1</td>
<td class="org-right">1.6</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.4</td>
<td class="org-right">3.4</td>
<td class="org-right">1.5</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.2</td>
<td class="org-right">4.1</td>
<td class="org-right">1.5</td>
<td class="org-right">0.1</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">4.2</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.9</td>
<td class="org-right">3.1</td>
<td class="org-right">1.5</td>
<td class="org-right">0.1</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.2</td>
<td class="org-right">1.2</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">3.5</td>
<td class="org-right">1.3</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.9</td>
<td class="org-right">3.1</td>
<td class="org-right">1.5</td>
<td class="org-right">0.1</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.4</td>
<td class="org-right">3.0</td>
<td class="org-right">1.3</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.4</td>
<td class="org-right">1.5</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.5</td>
<td class="org-right">1.3</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.5</td>
<td class="org-right">2.3</td>
<td class="org-right">1.3</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.4</td>
<td class="org-right">3.2</td>
<td class="org-right">1.3</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.5</td>
<td class="org-right">1.6</td>
<td class="org-right">0.6</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.8</td>
<td class="org-right">1.9</td>
<td class="org-right">0.4</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.8</td>
<td class="org-right">3.0</td>
<td class="org-right">1.4</td>
<td class="org-right">0.3</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">3.8</td>
<td class="org-right">1.6</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">4.6</td>
<td class="org-right">3.2</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.3</td>
<td class="org-right">3.7</td>
<td class="org-right">1.5</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">3.3</td>
<td class="org-right">1.4</td>
<td class="org-right">0.2</td>
<td class="org-left">setosa</td>
</tr>

<tr>
<td class="org-right">7.0</td>
<td class="org-right">3.2</td>
<td class="org-right">4.7</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">3.2</td>
<td class="org-right">4.5</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.9</td>
<td class="org-right">3.1</td>
<td class="org-right">4.9</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">2.3</td>
<td class="org-right">4.0</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.5</td>
<td class="org-right">2.8</td>
<td class="org-right">4.6</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">2.8</td>
<td class="org-right">4.5</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">3.3</td>
<td class="org-right">4.7</td>
<td class="org-right">1.6</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">4.9</td>
<td class="org-right">2.4</td>
<td class="org-right">3.3</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.6</td>
<td class="org-right">2.9</td>
<td class="org-right">4.6</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.2</td>
<td class="org-right">2.7</td>
<td class="org-right">3.9</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">2.0</td>
<td class="org-right">3.5</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.9</td>
<td class="org-right">3.0</td>
<td class="org-right">4.2</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.0</td>
<td class="org-right">2.2</td>
<td class="org-right">4.0</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.1</td>
<td class="org-right">2.9</td>
<td class="org-right">4.7</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.6</td>
<td class="org-right">2.9</td>
<td class="org-right">3.6</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.1</td>
<td class="org-right">4.4</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.6</td>
<td class="org-right">3.0</td>
<td class="org-right">4.5</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">2.7</td>
<td class="org-right">4.1</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.2</td>
<td class="org-right">2.2</td>
<td class="org-right">4.5</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.6</td>
<td class="org-right">2.5</td>
<td class="org-right">3.9</td>
<td class="org-right">1.1</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.9</td>
<td class="org-right">3.2</td>
<td class="org-right">4.8</td>
<td class="org-right">1.8</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.1</td>
<td class="org-right">2.8</td>
<td class="org-right">4.0</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">2.5</td>
<td class="org-right">4.9</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.1</td>
<td class="org-right">2.8</td>
<td class="org-right">4.7</td>
<td class="org-right">1.2</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">2.9</td>
<td class="org-right">4.3</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.6</td>
<td class="org-right">3.0</td>
<td class="org-right">4.4</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.8</td>
<td class="org-right">2.8</td>
<td class="org-right">4.8</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.0</td>
<td class="org-right">5.0</td>
<td class="org-right">1.7</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.0</td>
<td class="org-right">2.9</td>
<td class="org-right">4.5</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">2.6</td>
<td class="org-right">3.5</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">2.4</td>
<td class="org-right">3.8</td>
<td class="org-right">1.1</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">2.4</td>
<td class="org-right">3.7</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">2.7</td>
<td class="org-right">3.9</td>
<td class="org-right">1.2</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.0</td>
<td class="org-right">2.7</td>
<td class="org-right">5.1</td>
<td class="org-right">1.6</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.4</td>
<td class="org-right">3.0</td>
<td class="org-right">4.5</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.0</td>
<td class="org-right">3.4</td>
<td class="org-right">4.5</td>
<td class="org-right">1.6</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.1</td>
<td class="org-right">4.7</td>
<td class="org-right">1.5</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">2.3</td>
<td class="org-right">4.4</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.6</td>
<td class="org-right">3.0</td>
<td class="org-right">4.1</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">2.5</td>
<td class="org-right">4.0</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.5</td>
<td class="org-right">2.6</td>
<td class="org-right">4.4</td>
<td class="org-right">1.2</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.1</td>
<td class="org-right">3.0</td>
<td class="org-right">4.6</td>
<td class="org-right">1.4</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">2.6</td>
<td class="org-right">4.0</td>
<td class="org-right">1.2</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.0</td>
<td class="org-right">2.3</td>
<td class="org-right">3.3</td>
<td class="org-right">1.0</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.6</td>
<td class="org-right">2.7</td>
<td class="org-right">4.2</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">3.0</td>
<td class="org-right">4.2</td>
<td class="org-right">1.2</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">2.9</td>
<td class="org-right">4.2</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.2</td>
<td class="org-right">2.9</td>
<td class="org-right">4.3</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.1</td>
<td class="org-right">2.5</td>
<td class="org-right">3.0</td>
<td class="org-right">1.1</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">2.8</td>
<td class="org-right">4.1</td>
<td class="org-right">1.3</td>
<td class="org-left">versicolor</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">3.3</td>
<td class="org-right">6.0</td>
<td class="org-right">2.5</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">2.7</td>
<td class="org-right">5.1</td>
<td class="org-right">1.9</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.1</td>
<td class="org-right">3.0</td>
<td class="org-right">5.9</td>
<td class="org-right">2.1</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">2.9</td>
<td class="org-right">5.6</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.5</td>
<td class="org-right">3.0</td>
<td class="org-right">5.8</td>
<td class="org-right">2.2</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.6</td>
<td class="org-right">3.0</td>
<td class="org-right">6.6</td>
<td class="org-right">2.1</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">4.9</td>
<td class="org-right">2.5</td>
<td class="org-right">4.5</td>
<td class="org-right">1.7</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.3</td>
<td class="org-right">2.9</td>
<td class="org-right">6.3</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">2.5</td>
<td class="org-right">5.8</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.2</td>
<td class="org-right">3.6</td>
<td class="org-right">6.1</td>
<td class="org-right">2.5</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.5</td>
<td class="org-right">3.2</td>
<td class="org-right">5.1</td>
<td class="org-right">2.0</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">2.7</td>
<td class="org-right">5.3</td>
<td class="org-right">1.9</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.8</td>
<td class="org-right">3.0</td>
<td class="org-right">5.5</td>
<td class="org-right">2.1</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">5.7</td>
<td class="org-right">2.5</td>
<td class="org-right">5.0</td>
<td class="org-right">2.0</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">2.8</td>
<td class="org-right">5.1</td>
<td class="org-right">2.4</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">3.2</td>
<td class="org-right">5.3</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.5</td>
<td class="org-right">3.0</td>
<td class="org-right">5.5</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.7</td>
<td class="org-right">3.8</td>
<td class="org-right">6.7</td>
<td class="org-right">2.2</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.7</td>
<td class="org-right">2.6</td>
<td class="org-right">6.9</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.0</td>
<td class="org-right">2.2</td>
<td class="org-right">5.0</td>
<td class="org-right">1.5</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.9</td>
<td class="org-right">3.2</td>
<td class="org-right">5.7</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">5.6</td>
<td class="org-right">2.8</td>
<td class="org-right">4.9</td>
<td class="org-right">2.0</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.7</td>
<td class="org-right">2.8</td>
<td class="org-right">6.7</td>
<td class="org-right">2.0</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">2.7</td>
<td class="org-right">4.9</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.3</td>
<td class="org-right">5.7</td>
<td class="org-right">2.1</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.2</td>
<td class="org-right">3.2</td>
<td class="org-right">6.0</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.2</td>
<td class="org-right">2.8</td>
<td class="org-right">4.8</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.1</td>
<td class="org-right">3.0</td>
<td class="org-right">4.9</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">2.8</td>
<td class="org-right">5.6</td>
<td class="org-right">2.1</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.2</td>
<td class="org-right">3.0</td>
<td class="org-right">5.8</td>
<td class="org-right">1.6</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.4</td>
<td class="org-right">2.8</td>
<td class="org-right">6.1</td>
<td class="org-right">1.9</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.9</td>
<td class="org-right">3.8</td>
<td class="org-right">6.4</td>
<td class="org-right">2.0</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">2.8</td>
<td class="org-right">5.6</td>
<td class="org-right">2.2</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">2.8</td>
<td class="org-right">5.1</td>
<td class="org-right">1.5</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.1</td>
<td class="org-right">2.6</td>
<td class="org-right">5.6</td>
<td class="org-right">1.4</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">7.7</td>
<td class="org-right">3.0</td>
<td class="org-right">6.1</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">3.4</td>
<td class="org-right">5.6</td>
<td class="org-right">2.4</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.4</td>
<td class="org-right">3.1</td>
<td class="org-right">5.5</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.0</td>
<td class="org-right">3.0</td>
<td class="org-right">4.8</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.9</td>
<td class="org-right">3.1</td>
<td class="org-right">5.4</td>
<td class="org-right">2.1</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.1</td>
<td class="org-right">5.6</td>
<td class="org-right">2.4</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.9</td>
<td class="org-right">3.1</td>
<td class="org-right">5.1</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">5.8</td>
<td class="org-right">2.7</td>
<td class="org-right">5.1</td>
<td class="org-right">1.9</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.8</td>
<td class="org-right">3.2</td>
<td class="org-right">5.9</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.3</td>
<td class="org-right">5.7</td>
<td class="org-right">2.5</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.7</td>
<td class="org-right">3.0</td>
<td class="org-right">5.2</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.3</td>
<td class="org-right">2.5</td>
<td class="org-right">5.0</td>
<td class="org-right">1.9</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.5</td>
<td class="org-right">3.0</td>
<td class="org-right">5.2</td>
<td class="org-right">2.0</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">6.2</td>
<td class="org-right">3.4</td>
<td class="org-right">5.4</td>
<td class="org-right">2.3</td>
<td class="org-left">virginica</td>
</tr>

<tr>
<td class="org-right">5.9</td>
<td class="org-right">3.0</td>
<td class="org-right">5.1</td>
<td class="org-right">1.8</td>
<td class="org-left">virginica</td>
</tr>
</tbody>
</table>

<p>
<br />
</p>

<p>
Wide table:
</p>
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />

<col  class="org-right" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-right">Column 1</th>
<th scope="col" class="org-right">Column 2</th>
<th scope="col" class="org-right">Column 3</th>
<th scope="col" class="org-right">Column 4</th>
<th scope="col" class="org-right">Column 5</th>
<th scope="col" class="org-right">Column 6</th>
<th scope="col" class="org-right">Column 7</th>
<th scope="col" class="org-right">Column 8</th>
<th scope="col" class="org-right">Column 9</th>
<th scope="col" class="org-right">Column 10</th>
<th scope="col" class="org-right">Column 11</th>
<th scope="col" class="org-right">Column 12</th>
<th scope="col" class="org-right">Column 13</th>
<th scope="col" class="org-right">Column 14</th>
<th scope="col" class="org-right">Column 15</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-right">1</td>
<td class="org-right">2</td>
<td class="org-right">3</td>
<td class="org-right">4</td>
<td class="org-right">5</td>
<td class="org-right">6</td>
<td class="org-right">7</td>
<td class="org-right">8</td>
<td class="org-right">9</td>
<td class="org-right">10</td>
<td class="org-right">11</td>
<td class="org-right">12</td>
<td class="org-right">13</td>
<td class="org-right">14</td>
<td class="org-right">15</td>
</tr>

<tr>
<td class="org-right">16</td>
<td class="org-right">17</td>
<td class="org-right">18</td>
<td class="org-right">19</td>
<td class="org-right">20</td>
<td class="org-right">21</td>
<td class="org-right">22</td>
<td class="org-right">23</td>
<td class="org-right">24</td>
<td class="org-right">25</td>
<td class="org-right">26</td>
<td class="org-right">27</td>
<td class="org-right">28</td>
<td class="org-right">29</td>
<td class="org-right">30</td>
</tr>

<tr>
<td class="org-right">31</td>
<td class="org-right">32</td>
<td class="org-right">33</td>
<td class="org-right">34</td>
<td class="org-right">35</td>
<td class="org-right">36</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
<td class="org-right">&#xa0;</td>
</tr>
</tbody>
</table>
</div>
<div id="outline-container-implement-horizontal-scrolling-for-when-tables-are-too-wide" class="outline-4">
<h4 id="implement-horizontal-scrolling-for-when-tables-are-too-wide"><span class="section-number-4">13.1.1.</span> <span class="todo TODO">TODO</span> implement horizontal scrolling for when tables are too wide</h4>
<div class="outline-text-4" id="text-implement-horizontal-scrolling-for-when-tables-are-too-wide">
</div>
</div>
</div>
<div id="outline-container-lists" class="outline-3">
<h3 id="lists"><span class="section-number-3">13.2.</span> lists</h3>
<div class="outline-text-3" id="text-lists">
</div>
<div id="outline-container-bullets" class="outline-4">
<h4 id="bullets"><span class="section-number-4">13.2.1.</span> bullets</h4>
<div class="outline-text-4" id="text-bullets">
<ul class="org-ul">
<li>one</li>
<li>two
<ul class="org-ul">
<li>two point one</li>
<li>two point two</li>
</ul></li>
<li>three
<ul class="org-ul">
<li>three point one</li>
<li>three point two</li>
</ul></li>
</ul>
</div>
</div>
<div id="outline-container-numbers" class="outline-4">
<h4 id="numbers"><span class="section-number-4">13.2.2.</span> numbers</h4>
<div class="outline-text-4" id="text-numbers">
<ol class="org-ol">
<li>first</li>
<li>second
<ol class="org-ol">
<li>hello worrld</li>
<li>second point two</li>
</ol></li>
</ol>
</div>
</div>
<div id="outline-container-formatting" class="outline-4">
<h4 id="formatting"><span class="section-number-4">13.2.3.</span> formatting</h4>
<div class="outline-text-4" id="text-formatting">
<ul class="org-ul">
<li>some <b>bold</b> text</li>
<li>some <i>italic</i> text</li>
<li>some <span class="underline">underline</span> text</li>
<li>some <del>strike</del> text</li>
<li>some <code>code</code> text</li>
<li>some <code>verbatim</code> text</li>
<li>some <code>*bold in code*</code> text</li>
<li>some <code>/italic in verbatim/</code> text</li>
<li>some <code>_underline in verbatim_</code> text</li>
<li>some =~code in verbatim=~ text</li>
<li>some <code>+strike in verbatim+</code> text</li>
<li>some <b>bold</b> <i>italic</i> <span class="underline">underline</span> <del>strike</del> <code>code</code> <code>verbatim</code> text</li>
</ul>
</div>
<div id="outline-container-fix-underline-style" class="outline-5">
<h5 id="fix-underline-style"><span class="section-number-5">13.2.3.1.</span> <span class="todo TODO">TODO</span> fix underline style</h5>
<div class="outline-text-5" id="text-fix-underline-style">
</div>
</div>
</div>
</div>
</div>
<div id="outline-container-narrative-features" class="outline-2">
<h2 id="narrative-features"><span class="section-number-2">14.</span> Narrative features</h2>
<div class="outline-text-2" id="text-narrative-features">
<p>
Org of course has many features for structuring content.
</p>

<p>
This entire document is written in org-mode outline format, with headings and subheadings.  These levels of the hierarchy are rendered as different sized headings in the html output.
</p>
</div>
<div id="outline-container-text" class="outline-3">
<h3 id="text"><span class="section-number-3">14.1.</span> Text</h3>
<div class="outline-text-3" id="text-text">
</div>
<div id="outline-container-long-paragraph" class="outline-4">
<h4 id="long-paragraph"><span class="section-number-4">14.1.1.</span> Long paragraph</h4>
<div class="outline-text-4" id="text-long-paragraph">
<p>
This is a long paragraph &#x2013; note that it contains a footnote.  This is a good test of the styling for footnotes &#x2013; they should jump out, even when buried in text (not yet implemented).  Here's some good old lorem ipsum:
</p>

<p>
lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim<sup><a id="fnr.8" class="footref" href="https://www.chiply.dev/#fn.8" role="doc-backlink">8</a></sup> id est laborum.  Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?  
</p>
</div>
</div>
<div id="outline-container-quote" class="outline-4">
<h4 id="quote"><span class="section-number-4">14.1.2.</span> Quote</h4>
<div class="outline-text-4" id="text-quote">
<p>
<a href="https://orgmode.org/manual/Paragraphs.html">org has features for verse, blockquote, and centering text</a>. The html exporter I use doesn't style these, so I add rules to css to make them look different.  Note that you can include links in quotes, which is nice for direct attribution.
</p>
</div>
<div id="outline-container-pull-quotes" class="outline-5">
<h5 id="pull-quotes"><span class="section-number-5">14.1.2.1.</span> Pull quotes</h5>
<div class="outline-text-5" id="text-pull-quotes">
<p>
This is some quote in a #+begin<sub>quote</sub> block.  It is given attributes that are used by the blog site in order to create quotes that standout similar to how quotes standout in magazines.  This is useful for especially impactful statements that I want to grab the user's attention.
</p>

<blockquote class="pull-quote pull-right">
<p>
<a href="https://phys.org/news/2009-04-internet-begun-founders.html">The Web as I envisaged it, we have not seen it yet. The future is still so much bigger than the past</a> ---<a href="https://en.wikipedia.org/wiki/Tim_Berners-Lee">Tim Berners-Lee</a>
</p>
</blockquote>

<p>
Pull quotes work by floating the blockquote to the left or right, allowing the surrounding paragraph text to wrap around the callout.  Use <code>#+ATTR_HTML: :class pull-quote pull-right</code> before a <code>#+begin_quote</code> block to float it right, or <code>pull-left</code> (or omit the direction) to float left.  The quote renders in large italic text with a decorative opening quotation mark.  On mobile devices the float is disabled and the quote stacks normally at full width.  This paragraph demonstrates how body text flows around the floated element &#x2014; the wrapping effect creates the magazine-style layout that makes important statements visually prominent while keeping the reading flow natural.
</p>
</div>
</div>
<div id="outline-container-other" class="outline-5">
<h5 id="other"><span class="section-number-5">14.1.2.2.</span> Other</h5>
<div class="outline-text-5" id="text-other">
<p>
Here's one of my favourite quotes &#x2013; I think about this all the time. This is text in a #+BEGIN<sub>VERSE</sub> block. It gets compiles as <code>p.verse</code>:
</p>

<p class="verse">
<br />
<br />
<a href="https://www.goodreads.com/quotes/19905-perfection-is-achieved-not-when-there-is-nothing-more-to">Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away</a><br />
<br />
&#xa0;&#xa0;&#xa0;---<a href="https://en.wikipedia.org/wiki/Antoine_de_Saint-Exup%C3%A9ry">Antoine de Saint-Exupéry</a><br />
</p>

<p>
<br />
</p>

<p>
This is some text in a #+begin<sub>center</sub> block.  It gets compiles as <code>p.org-center</code>:
</p>

<p>
<br />
</p>

<div class="org-center">
<p>
This text is centered.
</p>
</div>

<p>
<br />
</p>

<p>
This is some more regular text.
</p>
</div>
</div>
</div>
</div>
</div>
<div id="outline-container-view-customization" class="outline-2">
<h2 id="view-customization"><span class="section-number-2">15.</span> View customization</h2>
<div class="outline-text-2" id="text-view-customization">
<p>
There are many ways to customize the view when you are reading the blog post
</p>
</div>
<div id="outline-container-font" class="outline-3">
<h3 id="font"><span class="section-number-3">15.1.</span> Font</h3>
<div class="outline-text-3" id="text-font">
<p>
There is a font button at the top of the page which allows you to select between different fonts.  By default, I'm using <a href="https://terminus-font.sourceforge.net/">terminus</a>, but you can choose between other pleasing monospace, serif, and sans-serif fonts.
</p>
</div>
</div>
<div id="outline-container-text-scale" class="outline-3">
<h3 id="text-scale"><span class="section-number-3">15.2.</span> Text scale</h3>
<div class="outline-text-3" id="text-text-scale">
<p>
By default I render the main article view as so roughly 66 characters display in each line, which is in line with paper print conventions, but you can use your browser to increase and decrease text scale.
</p>
</div>
</div>
<div id="outline-container-light-dark-mode" class="outline-3">
<h3 id="light-dark-mode"><span class="section-number-3">15.3.</span> Light/Dark mode</h3>
<div class="outline-text-3" id="text-light-dark-mode">
<p>
You can also modify the light/dark mode setting.  All images, diagrams, and interactive visualizations are compatible with these modes.
</p>
</div>
</div>
<div id="outline-container-colorblind-settings" class="outline-3">
<h3 id="colorblind-settings"><span class="section-number-3">15.4.</span> Colorblind settings</h3>
<div class="outline-text-3" id="text-colorblind-settings">
<p>
If you have color blindness, you can modify the colors to match your exact kind of color blindness, and you can even set the color scale to grayscale if you can't see color at all.  All images, diagrams, and interactive visualizations are compatible with these modes.
</p>
</div>
</div>
<div id="outline-container-foldable-headings" class="outline-3">
<h3 id="foldable-headings"><span class="section-number-3">15.5.</span> Foldable headings</h3>
<div class="outline-text-3" id="text-foldable-headings">
<p>
All headings are foldable &#x2013; you can click headings to hide/show their content.  You can press Shift-Tab to hide show all headings at various levels, which can help to get a birds eye view of the content and only display content that is relevant or interesting to you.
</p>
</div>
</div>
</div>
<div id="outline-container-images" class="outline-2">
<h2 id="images"><span class="section-number-2">16.</span> Images</h2>
<div class="outline-text-2" id="text-images">
</div>
<div id="outline-container-local-file--png-" class="outline-3">
<h3 id="local-file--png-"><span class="section-number-3">16.1.</span> <span class="done DONE">DONE</span> local file (png)</h3>
<div class="outline-text-3" id="text-local-file--png-">

<div id="orgce6509f" class="figure">
<p><img src="https://www.chiply.dev/images/test-image.png" alt="test-image.png" />
</p>
</div>
</div>
</div>
<div id="outline-container-remote-file--svg-" class="outline-3">
<h3 id="remote-file--svg-"><span class="section-number-3">16.2.</span> <span class="done DONE">DONE</span> remote file (svg)</h3>
<div class="outline-text-3" id="text-remote-file--svg-">
<p>
(not rendered) In org mode, you can include captions and html attributes to change the size of the image
</p>


<div id="org8740bcb" class="figure">
<p><img src="https://upload.wikimedia.org/wikipedia/commons/c/c1/Google_%22G%22_logo.svg" alt="&quot;Description of image&quot;" class="org-svg" width="300" />
</p>
<p><span class="figure-number">Figure 2: </span>A remote image</p>
</div>
</div>
</div>
</div>
<div id="outline-container-styling" class="outline-2">
<h2 id="styling"><span class="section-number-2">17.</span> Styling</h2>
<div class="outline-text-2" id="text-styling">
<p>
Styling is applied mostly at the Svelte level, but org-mode does allow some inline styling options.
</p>
</div>
<div id="outline-container-inline-css" class="outline-3">
<h3 id="inline-css"><span class="section-number-3">17.1.</span> inline css</h3>
<div class="outline-text-3" id="text-inline-css">
<p>
CSS can be included inline in the org file using the export block.  This block doesn't render in the document as html markup, it becomes part of the document itself.
</p>

<p>
Here's the code I embedded in the org-document in a #+begin<sub>export</sub> html block to style TODO items in pink monospace font:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;style&gt;
  .todo { font-family: monospace; color: pink; }
&lt;/style&gt;
</pre>
</div>

<style>
  .todo { font-family: monospace; color: pink; }
</style>
</div>
</div>
</div>
<div id="outline-container-adding-footnotes" class="outline-2">
<h2 id="adding-footnotes"><span class="section-number-2">18.</span> Adding footnotes</h2>
<div class="outline-text-2" id="text-adding-footnotes">
<p>
org-mode supports footnotes.  This is how the native footnotes look when they get rrendered into HTML:
</p>

<p>
This is a footnote<sup><a id="fnr.9" class="footref" href="https://www.chiply.dev/#fn.9" role="doc-backlink">9</a></sup>.
</p>

<p>
You can have as many<sup><a id="fnr.10" class="footref" href="https://www.chiply.dev/#fn.10" role="doc-backlink">10</a></sup> footnotes<sup><a id="fnr.11" class="footref" href="https://www.chiply.dev/#fn.11" role="doc-backlink">11</a></sup> as you like in a single line.
</p>

<p>
You will have already seen numerous footnotes up to this point.
</p>
</div>
</div>
<div id="outline-container-iconography" class="outline-2">
<h2 id="iconography"><span class="section-number-2">19.</span> Iconography&#xa0;&#xa0;&#xa0;<span class="tag"><span class="icons">icons</span></span></h2>
<div class="outline-text-2" id="text-iconography">
<p>
Icons can be included inline.  Here I'm using the <a href="https://github.com/file-icons/atom/blob/master/styles/icons.less">file-icons</a> github repo.  You can see all the icons here.
</p>

<p>
<br />
</p>

<i class="icon binary-icon">binary-icon</i>
<i class="icon book-icon">book-icon</i>
<i class="icon checklist-icon">checklist-icon</i>
<i class="icon code-icon">code-icon</i>
<i class="icon database-icon">database-icon</i>
<i class="icon gear-icon">gear-icon</i>
<i class="icon git-commit-icon">git-commit-icon</i>
<i class="icon git-merge-icon">git-merge-icon</i>
<i class="icon github-icon">github-icon</i>
<i class="icon graph-icon">graph-icon</i>
<i class="icon image-icon">image-icon</i>
<i class="icon key-icon">key-icon</i>
<i class="icon link-icon">link-icon</i>
<i class="icon markdown-icon">markdown-icon</i>
<i class="icon package-icon">package-icon</i>
<i class="icon ruby-icon">ruby-icon</i>
<i class="icon secret-icon">secret-icon</i>
<i class="icon squirrel-icon">squirrel-icon</i>
<i class="icon text-icon">text-icon</i>
<i class="icon zip-icon">zip-icon</i>
<i class="icon anchor-icon">anchor-icon</i>
<i class="icon android-icon">android-icon</i>
<i class="icon at-icon">at-icon</i>
<i class="icon audio-icon">audio-icon</i>
<i class="icon backup-icon">backup-icon</i>
<i class="icon bitcoin-icon">bitcoin-icon</i>
<i class="icon book-alt-icon">book-alt-icon</i>
<i class="icon bullhorn-icon">bullhorn-icon</i>
<i class="icon calc-icon">calc-icon</i>
<i class="icon calendar-icon">calendar-icon</i>
<i class="icon coffee-icon">coffee-icon</i>
<i class="icon css3-icon">css3-icon</i>
<i class="icon circle-icon">circle-icon</i>
<i class="icon download-icon">download-icon</i>
<i class="icon earth-icon">earth-icon</i>
<i class="icon filter-icon">filter-icon</i>
<i class="icon gears-icon">gears-icon</i>
<i class="icon html5-icon">html5-icon</i>
<i class="icon lock-icon">lock-icon</i>
<i class="icon mobile-icon">mobile-icon</i>
<i class="icon moon-icon">moon-icon</i>
<i class="icon music-icon">music-icon</i>
<i class="icon print-icon">print-icon</i>
<i class="icon question-icon">question-icon</i>
<i class="icon recycle-icon">recycle-icon</i>
<i class="icon rss-icon">rss-icon</i>
<i class="icon scales-icon">scales-icon</i>
<i class="icon smarty-icon">smarty-icon</i>
<i class="icon sourcemap-icon">sourcemap-icon</i>
<i class="icon sun-icon">sun-icon</i>
<i class="icon toc-icon">toc-icon</i>
<i class="icon wechat-icon">wechat-icon</i>
<i class="icon youtube-icon">youtube-icon</i>
<i class="icon apache-icon">apache-icon</i>
<i class="icon archlinux-icon">archlinux-icon</i>
<i class="icon c-icon">c-icon</i>
<i class="icon cpp-icon">cpp-icon</i>
<i class="icon csharp-icon">csharp-icon</i>
<i class="icon debian-icon">debian-icon</i>
<i class="icon elixir-icon">elixir-icon</i>
<i class="icon gnome-icon">gnome-icon</i>
<i class="icon haskell-icon">haskell-icon</i>
<i class="icon java-icon">java-icon</i>
<i class="icon js-icon">js-icon</i>
<i class="icon msql-icon">msql-icon</i>
<i class="icon objc-icon">objc-icon</i>
<i class="icon osx-icon">osx-icon</i>
<i class="icon pgsql-icon">pgsql-icon</i>
<i class="icon python-icon">python-icon</i>
<i class="icon red-hat-icon">red-hat-icon</i>
<i class="icon scala-icon">scala-icon</i>
<i class="icon sql-icon">sql-icon</i>
<i class="icon svg-icon">svg-icon</i>
<i class="icon x11-icon">x11-icon</i>
<i class="icon angular-icon">angular-icon</i>
<i class="icon appcelerator-icon">appcelerator-icon</i>
<i class="icon appstore-icon">appstore-icon</i>
<i class="icon asp-icon">asp-icon</i>
<i class="icon atom-icon">atom-icon</i>
<i class="icon backbone-icon">backbone-icon</i>
<i class="icon bitbucket-icon">bitbucket-icon</i>
<i class="icon bootstrap-icon">bootstrap-icon</i>
<i class="icon bower-icon">bower-icon</i>
<i class="icon chrome-icon">chrome-icon</i>
<i class="icon clojure-icon">clojure-icon</i>
<i class="icon compass-icon">compass-icon</i>
<i class="icon dart-icon">dart-icon</i>
<i class="icon dlang-icon">dlang-icon</i>
<i class="icon dojo-icon">dojo-icon</i>
<i class="icon dropbox-icon">dropbox-icon</i>
<i class="icon eclipse-icon">eclipse-icon</i>
<i class="icon erlang-icon">erlang-icon</i>
<i class="icon extjs-icon">extjs-icon</i>
<i class="icon firefox-icon">firefox-icon</i>
<i class="icon fsharp-icon">fsharp-icon</i>
<i class="icon git-icon">git-icon</i>
<i class="icon heroku-icon">heroku-icon</i>
<i class="icon jekyll-icon">jekyll-icon</i>
<i class="icon jira-icon">jira-icon</i>
<i class="icon jquery-icon">jquery-icon</i>
<i class="icon jqueryui-icon">jqueryui-icon</i>
<i class="icon laravel-old-icon">laravel-old-icon</i>
<i class="icon materialize-icon">materialize-icon</i>
<i class="icon modernizr-icon">modernizr-icon</i>
<i class="icon mootools-icon">mootools-icon</i>
<i class="icon node-icon">node-icon</i>
<i class="icon perl-icon">perl-icon</i>
<i class="icon prolog-icon">prolog-icon</i>
<i class="icon rails-icon">rails-icon</i>
<i class="icon raphael-icon">raphael-icon</i>
<i class="icon requirejs-icon">requirejs-icon</i>
<i class="icon rust-icon">rust-icon</i>
<i class="icon safari-icon">safari-icon</i>
<i class="icon sass-icon">sass-icon</i>
<i class="icon sencha-icon">sencha-icon</i>
<i class="icon snapsvg-icon">snapsvg-icon</i>
<i class="icon swift-icon">swift-icon</i>
<i class="icon travis-icon">travis-icon</i>
<i class="icon typo3-icon">typo3-icon</i>
<i class="icon uikit-icon">uikit-icon</i>
<i class="icon unity3d-icon">unity3d-icon</i>
<i class="icon vim-icon">vim-icon</i>
<i class="icon vs-icon">vs-icon</i>
<i class="icon windows-icon">windows-icon</i>
<i class="icon yeoman-icon">yeoman-icon</i>
<i class="icon _1c-icon">_1c-icon</i>
<i class="icon _1c-alt-icon">_1c-alt-icon</i>
<i class="icon _4d-icon">_4d-icon</i>
<i class="icon a-plus-icon">a-plus-icon</i>
<i class="icon abap-icon">abap-icon</i>
<i class="icon abif-icon">abif-icon</i>
<i class="icon access-icon">access-icon</i>
<i class="icon acre-icon">acre-icon</i>
<i class="icon ada-icon">ada-icon</i>
<i class="icon adobe-cc-icon">adobe-cc-icon</i>
<i class="icon ae-icon">ae-icon</i>
<i class="icon affectscript-icon">affectscript-icon</i>
<i class="icon affinity-icon">affinity-icon</i>
<i class="icon agda-icon">agda-icon</i>
<i class="icon ahk-icon">ahk-icon</i>
<i class="icon ai-icon">ai-icon</i>
<i class="icon alacritty-icon">alacritty-icon</i>
<i class="icon alacritty-alt-icon">alacritty-alt-icon</i>
<i class="icon alex-icon">alex-icon</i>
<i class="icon alloy-icon">alloy-icon</i>
<i class="icon alpine-icon">alpine-icon</i>
<i class="icon ampl-icon">ampl-icon</i>
<i class="icon amusewiki-icon">amusewiki-icon</i>
<i class="icon amx-icon">amx-icon</i>
<i class="icon analytica-icon">analytica-icon</i>
<i class="icon angelscript-icon">angelscript-icon</i>
<i class="icon animate-icon">animate-icon</i>
<i class="icon animestudio-icon">animestudio-icon</i>
<i class="icon ansible-icon">ansible-icon</i>
<i class="icon ant-icon">ant-icon</i>
<i class="icon antlr-icon">antlr-icon</i>
<i class="icon antwar-icon">antwar-icon</i>
<i class="icon anyscript-icon">anyscript-icon</i>
<i class="icon api-icon">api-icon</i>
<i class="icon apiextractor-icon">apiextractor-icon</i>
<i class="icon apl-icon">apl-icon</i>
<i class="icon apollo-icon">apollo-icon</i>
<i class="icon apple-icon">apple-icon</i>
<i class="icon appveyor-icon">appveyor-icon</i>
<i class="icon arc-icon">arc-icon</i>
<i class="icon arduino-icon">arduino-icon</i>
<i class="icon arttext-icon">arttext-icon</i>
<i class="icon arttext4-icon">arttext4-icon</i>
<i class="icon as-icon">as-icon</i>
<i class="icon asciidoc-icon">asciidoc-icon</i>
<i class="icon asciidoctor-icon">asciidoctor-icon</i>
<i class="icon asm-icon">asm-icon</i>
<i class="icon asm-agc-icon">asm-agc-icon</i>
<i class="icon asm-arm-icon">asm-arm-icon</i>
<i class="icon asm-avr-icon">asm-avr-icon</i>
<i class="icon asm-hitachi-icon">asm-hitachi-icon</i>
<i class="icon asm-intel-icon">asm-intel-icon</i>
<i class="icon asm-m68k-icon">asm-m68k-icon</i>
<i class="icon asm-vax-icon">asm-vax-icon</i>
<i class="icon asm-zilog-icon">asm-zilog-icon</i>
<i class="icon asymptote-icon">asymptote-icon</i>
<i class="icon atoum-icon">atoum-icon</i>
<i class="icon ats-icon">ats-icon</i>
<i class="icon audacity-icon">audacity-icon</i>
<i class="icon augeas-icon">augeas-icon</i>
<i class="icon aurelia-icon">aurelia-icon</i>
<i class="icon autoit-icon">autoit-icon</i>
<i class="icon automator-icon">automator-icon</i>
<i class="icon awk-icon">awk-icon</i>
<i class="icon azurepipelines-icon">azurepipelines-icon</i>
<i class="icon avro-icon">avro-icon</i>
<i class="icon babel-icon">babel-icon</i>
<i class="icon ballerina-icon">ballerina-icon</i>
<i class="icon bazaar-icon">bazaar-icon</i>
<i class="icon bazel-icon">bazel-icon</i>
<i class="icon behat-icon">behat-icon</i>
<i class="icon bem-icon">bem-icon</i>
<i class="icon bibtex-icon">bibtex-icon</i>
<i class="icon bikeshed-icon">bikeshed-icon</i>
<i class="icon biml-icon">biml-icon</i>
<i class="icon binder-icon">binder-icon</i>
<i class="icon bintray-icon">bintray-icon</i>
<i class="icon bison-icon">bison-icon</i>
<i class="icon bithound-icon">bithound-icon</i>
<i class="icon blender-icon">blender-icon</i>
<i class="icon blitzbasic-icon">blitzbasic-icon</i>
<i class="icon bloc-icon">bloc-icon</i>
<i class="icon bluespec-icon">bluespec-icon</i>
<i class="icon bnf-icon">bnf-icon</i>
<i class="icon boo-icon">boo-icon</i>
<i class="icon boot-icon">boot-icon</i>
<i class="icon bors-icon">bors-icon</i>
<i class="icon bosque-icon">bosque-icon</i>
<i class="icon brain-icon">brain-icon</i>
<i class="icon brakeman-icon">brakeman-icon</i>
<i class="icon brew-icon">brew-icon</i>
<i class="icon bro-icon">bro-icon</i>
<i class="icon broccoli-icon">broccoli-icon</i>
<i class="icon brotli-icon">brotli-icon</i>
<i class="icon browserslist-icon">browserslist-icon</i>
<i class="icon browsersync-icon">browsersync-icon</i>
<i class="icon brunch-icon">brunch-icon</i>
<i class="icon buck-icon">buck-icon</i>
<i class="icon buildkite-icon">buildkite-icon</i>
<i class="icon bundler-icon">bundler-icon</i>
<i class="icon byond-icon">byond-icon</i>
<i class="icon cabal-icon">cabal-icon</i>
<i class="icon caddy-icon">caddy-icon</i>
<i class="icon caffe-icon">caffe-icon</i>
<i class="icon caffe2-icon">caffe2-icon</i>
<i class="icon cake-icon">cake-icon</i>
<i class="icon cakefile-icon">cakefile-icon</i>
<i class="icon cakephp-icon">cakephp-icon</i>
<i class="icon calva-icon">calva-icon</i>
<i class="icon carthage-icon">carthage-icon</i>
<i class="icon casc-icon">casc-icon</i>
<i class="icon cc-icon">cc-icon</i>
<i class="icon cdf-icon">cdf-icon</i>
<i class="icon ceylon-icon">ceylon-icon</i>
<i class="icon cf-icon">cf-icon</i>
<i class="icon chai-icon">chai-icon</i>
<i class="icon chapel-icon">chapel-icon</i>
<i class="icon chartjs-icon">chartjs-icon</i>
<i class="icon cheetah3d-icon">cheetah3d-icon</i>
<i class="icon chef-icon">chef-icon</i>
<i class="icon chocolatey-icon">chocolatey-icon</i>
<i class="icon chuck-icon">chuck-icon</i>
<i class="icon circleci-icon">circleci-icon</i>
<i class="icon cirru-icon">cirru-icon</i>
<i class="icon cl-icon">cl-icon</i>
<i class="icon clarion-icon">clarion-icon</i>
<i class="icon clean-icon">clean-icon</i>
<i class="icon click-icon">click-icon</i>
<i class="icon clips-icon">clips-icon</i>
<i class="icon cljs-icon">cljs-icon</i>
<i class="icon closure-tpl-icon">closure-tpl-icon</i>
<i class="icon cloudfoundry-icon">cloudfoundry-icon</i>
<i class="icon cmake-icon">cmake-icon</i>
<i class="icon cnab-icon">cnab-icon</i>
<i class="icon cobol-icon">cobol-icon</i>
<i class="icon cocoapods-icon">cocoapods-icon</i>
<i class="icon codacy-icon">codacy-icon</i>
<i class="icon codecov-icon">codecov-icon</i>
<i class="icon codekit-icon">codekit-icon</i>
<i class="icon codemeta-icon">codemeta-icon</i>
<i class="icon codeship-icon">codeship-icon</i>
<i class="icon commitlint-icon">commitlint-icon</i>
<i class="icon composer-icon">composer-icon</i>
<i class="icon conan-icon">conan-icon</i>
<i class="icon conda-icon">conda-icon</i>
<i class="icon config-icon">config-icon</i>
<i class="icon config-coffee-icon">config-coffee-icon</i>
<i class="icon config-go-icon">config-go-icon</i>
<i class="icon config-hs-icon">config-hs-icon</i>
<i class="icon config-js-icon">config-js-icon</i>
<i class="icon config-perl-icon">config-perl-icon</i>
<i class="icon config-python-icon">config-python-icon</i>
<i class="icon config-react-icon">config-react-icon</i>
<i class="icon config-ruby-icon">config-ruby-icon</i>
<i class="icon config-rust-icon">config-rust-icon</i>
<i class="icon config-ts-icon">config-ts-icon</i>
<i class="icon conll-icon">conll-icon</i>
<i class="icon coq-icon">coq-icon</i>
<i class="icon cordova-icon">cordova-icon</i>
<i class="icon corel-icon">corel-icon</i>
<i class="icon coreldraw-icon">coreldraw-icon</i>
<i class="icon coveralls-icon">coveralls-icon</i>
<i class="icon cp-icon">cp-icon</i>
<i class="icon cpan-icon">cpan-icon</i>
<i class="icon cpcdosc-icon">cpcdosc-icon</i>
<i class="icon crafttweaker-icon">crafttweaker-icon</i>
<i class="icon creole-icon">creole-icon</i>
<i class="icon crowdin-icon">crowdin-icon</i>
<i class="icon crystal-icon">crystal-icon</i>
<i class="icon csound-icon">csound-icon</i>
<i class="icon csscript-icon">csscript-icon</i>
<i class="icon cubit-icon">cubit-icon</i>
<i class="icon cuneiform-icon">cuneiform-icon</i>
<i class="icon cucumber-icon">cucumber-icon</i>
<i class="icon curl-icon">curl-icon</i>
<i class="icon curry-icon">curry-icon</i>
<i class="icon cvs-icon">cvs-icon</i>
<i class="icon cwl-icon">cwl-icon</i>
<i class="icon cython-icon">cython-icon</i>
<i class="icon d3-icon">d3-icon</i>
<i class="icon dafny-icon">dafny-icon</i>
<i class="icon darcs-icon">darcs-icon</i>
<i class="icon dashboard-icon">dashboard-icon</i>
<i class="icon dataweave-icon">dataweave-icon</i>
<i class="icon dbase-icon">dbase-icon</i>
<i class="icon dna-icon">dna-icon</i>
<i class="icon default-icon">default-icon</i>
<i class="icon delphi-icon">delphi-icon</i>
<i class="icon deno-icon">deno-icon</i>
<i class="icon dependabot-icon">dependabot-icon</i>
<i class="icon devcontainer-icon">devcontainer-icon</i>
<i class="icon devicetree-icon">devicetree-icon</i>
<i class="icon dhall-icon">dhall-icon</i>
<i class="icon dia-icon">dia-icon</i>
<i class="icon digdag-icon">digdag-icon</i>
<i class="icon diff-icon">diff-icon</i>
<i class="icon docbook-icon">docbook-icon</i>
<i class="icon docker-icon">docker-icon</i>
<i class="icon doclets-icon">doclets-icon</i>
<i class="icon docpad-icon">docpad-icon</i>
<i class="icon docz-icon">docz-icon</i>
<i class="icon doge-icon">doge-icon</i>
<i class="icon dosbox-icon">dosbox-icon</i>
<i class="icon dotenv-icon">dotenv-icon</i>
<i class="icon dotjs-icon">dotjs-icon</i>
<i class="icon doxygen-icon">doxygen-icon</i>
<i class="icon dragula-icon">dragula-icon</i>
<i class="icon drawio-icon">drawio-icon</i>
<i class="icon drone-icon">drone-icon</i>
<i class="icon dub-icon">dub-icon</i>
<i class="icon dvc-icon">dvc-icon</i>
<i class="icon dyalog-icon">dyalog-icon</i>
<i class="icon dylib-icon">dylib-icon</i>
<i class="icon e-icon">e-icon</i>
<i class="icon eagle-icon">eagle-icon</i>
<i class="icon easybuild-icon">easybuild-icon</i>
<i class="icon ec-icon">ec-icon</i>
<i class="icon ecere-icon">ecere-icon</i>
<i class="icon eclipse-lang-icon">eclipse-lang-icon</i>
<i class="icon editorconfig-icon">editorconfig-icon</i>
<i class="icon edge-icon">edge-icon</i>
<i class="icon eiffel-icon">eiffel-icon</i>
<i class="icon ejs-icon">ejs-icon</i>
<i class="icon electron-icon">electron-icon</i>
<i class="icon elm-icon">elm-icon</i>
<i class="icon em-icon">em-icon</i>
<i class="icon emacs-icon">emacs-icon</i>
<i class="icon ember-icon">ember-icon</i>
<i class="icon ensime-icon">ensime-icon</i>
<i class="icon eq-icon">eq-icon</i>
<i class="icon esdoc-icon">esdoc-icon</i>
<i class="icon eslint-icon">eslint-icon</i>
<i class="icon excel-icon">excel-icon</i>
<i class="icon expo-icon">expo-icon</i>
<i class="icon fabric-icon">fabric-icon</i>
<i class="icon fabfile-icon">fabfile-icon</i>
<i class="icon factor-icon">factor-icon</i>
<i class="icon falcon-icon">falcon-icon</i>
<i class="icon fancy-icon">fancy-icon</i>
<i class="icon fantom-icon">fantom-icon</i>
<i class="icon fauna-icon">fauna-icon</i>
<i class="icon faust-icon">faust-icon</i>
<i class="icon fbx-icon">fbx-icon</i>
<i class="icon fexl-icon">fexl-icon</i>
<i class="icon ff-icon">ff-icon</i>
<i class="icon figma-icon">figma-icon</i>
<i class="icon finaldraft-icon">finaldraft-icon</i>
<i class="icon finder-icon">finder-icon</i>
<i class="icon firebase-icon">firebase-icon</i>
<i class="icon firebase-bolt-icon">firebase-bolt-icon</i>
<i class="icon flask-icon">flask-icon</i>
<i class="icon floobits-icon">floobits-icon</i>
<i class="icon flow-icon">flow-icon</i>
<i class="icon flutter-icon">flutter-icon</i>
<i class="icon flux-icon">flux-icon</i>
<i class="icon font-icon">font-icon</i>
<i class="icon font-bitmap-icon">font-bitmap-icon</i>
<i class="icon fork-icon">fork-icon</i>
<i class="icon fortran-icon">fortran-icon</i>
<i class="icon fossa-icon">fossa-icon</i>
<i class="icon fossil-icon">fossil-icon</i>
<i class="icon fountain-icon">fountain-icon</i>
<i class="icon franca-icon">franca-icon</i>
<i class="icon freemarker-icon">freemarker-icon</i>
<i class="icon frege-icon">frege-icon</i>
<i class="icon fthtml-icon">fthtml-icon</i>
<i class="icon ftr-icon">ftr-icon</i>
<i class="icon fuelux-icon">fuelux-icon</i>
<i class="icon fusebox-icon">fusebox-icon</i>
<i class="icon futhark-icon">futhark-icon</i>
<i class="icon gams-icon">gams-icon</i>
<i class="icon galaxy-icon">galaxy-icon</i>
<i class="icon galen-icon">galen-icon</i>
<i class="icon gap-icon">gap-icon</i>
<i class="icon gatsby-icon">gatsby-icon</i>
<i class="icon gauss-icon">gauss-icon</i>
<i class="icon gdb-icon">gdb-icon</i>
<i class="icon genshi-icon">genshi-icon</i>
<i class="icon genstat-icon">genstat-icon</i>
<i class="icon gentoo-icon">gentoo-icon</i>
<i class="icon gf-icon">gf-icon</i>
<i class="icon gimp-icon">gimp-icon</i>
<i class="icon gitlab-icon">gitlab-icon</i>
<i class="icon gitpod-icon">gitpod-icon</i>
<i class="icon glade-icon">glade-icon</i>
<i class="icon glide-icon">glide-icon</i>
<i class="icon gltf-icon">gltf-icon</i>
<i class="icon glyphs-icon">glyphs-icon</i>
<i class="icon gml-icon">gml-icon</i>
<i class="icon gn-icon">gn-icon</i>
<i class="icon gnu-icon">gnu-icon</i>
<i class="icon gnuplot-icon">gnuplot-icon</i>
<i class="icon go-icon">go-icon</i>
<i class="icon godot-icon">godot-icon</i>
<i class="icon golo-icon">golo-icon</i>
<i class="icon goreleaser-icon">goreleaser-icon</i>
<i class="icon gosu-icon">gosu-icon</i>
<i class="icon gql-codegen-icon">gql-codegen-icon</i>
<i class="icon gradle-icon">gradle-icon</i>
<i class="icon graphite-icon">graphite-icon</i>
<i class="icon grapher-icon">grapher-icon</i>
<i class="icon graphql-icon">graphql-icon</i>
<i class="icon graphviz-icon">graphviz-icon</i>
<i class="icon greenkeeper-icon">greenkeeper-icon</i>
<i class="icon gvdesign-icon">gvdesign-icon</i>
<i class="icon gridsome-icon">gridsome-icon</i>
<i class="icon groovy-icon">groovy-icon</i>
<i class="icon grunt-icon">grunt-icon</i>
<i class="icon gulp-icon">gulp-icon</i>
<i class="icon hack-icon">hack-icon</i>
<i class="icon haml-icon">haml-icon</i>
<i class="icon hoplon-icon">hoplon-icon</i>
<i class="icon harbour-icon">harbour-icon</i>
<i class="icon hashicorp-icon">hashicorp-icon</i>
<i class="icon haxe-icon">haxe-icon</i>
<i class="icon haxedevelop-icon">haxedevelop-icon</i>
<i class="icon helix-icon">helix-icon</i>
<i class="icon hg-icon">hg-icon</i>
<i class="icon hie-icon">hie-icon</i>
<i class="icon hjson-icon">hjson-icon</i>
<i class="icon houdini-icon">houdini-icon</i>
<i class="icon houndci-icon">houndci-icon</i>
<i class="icon hp-icon">hp-icon</i>
<i class="icon hy-icon">hy-icon</i>
<i class="icon hygen-icon">hygen-icon</i>
<i class="icon hyper-icon">hyper-icon</i>
<i class="icon kibo-icon">kibo-icon</i>
<i class="icon kx-icon">kx-icon</i>
<i class="icon husky-icon">husky-icon</i>
<i class="icon icomoon-icon">icomoon-icon</i>
<i class="icon idl-icon">idl-icon</i>
<i class="icon idris-icon">idris-icon</i>
<i class="icon igorpro-icon">igorpro-icon</i>
<i class="icon indesign-icon">indesign-icon</i>
<i class="icon infopath-icon">infopath-icon</i>
<i class="icon inform7-icon">inform7-icon</i>
<i class="icon inkscape-icon">inkscape-icon</i>
<i class="icon inno-icon">inno-icon</i>
<i class="icon imba-icon">imba-icon</i>
<i class="icon imgbot-icon">imgbot-icon</i>
<i class="icon ink-icon">ink-icon</i>
<i class="icon io-icon">io-icon</i>
<i class="icon ioke-icon">ioke-icon</i>
<i class="icon ionic-icon">ionic-icon</i>
<i class="icon isabelle-icon">isabelle-icon</i>
<i class="icon istanbul-icon">istanbul-icon</i>
<i class="icon j-icon">j-icon</i>
<i class="icon jade-icon">jade-icon</i>
<i class="icon jake-icon">jake-icon</i>
<i class="icon janet-icon">janet-icon</i>
<i class="icon jasmine-icon">jasmine-icon</i>
<i class="icon jenkins-icon">jenkins-icon</i>
<i class="icon jest-icon">jest-icon</i>
<i class="icon jolie-icon">jolie-icon</i>
<i class="icon jinja-icon">jinja-icon</i>
<i class="icon jison-icon">jison-icon</i>
<i class="icon jscpd-icon">jscpd-icon</i>
<i class="icon json-icon">json-icon</i>
<i class="icon json5-icon">json5-icon</i>
<i class="icon jsonld-icon">jsonld-icon</i>
<i class="icon jsonnet-icon">jsonnet-icon</i>
<i class="icon jsx-icon">jsx-icon</i>
<i class="icon julia-icon">julia-icon</i>
<i class="icon junos-icon">junos-icon</i>
<i class="icon jupyter-icon">jupyter-icon</i>
<i class="icon kaitai-icon">kaitai-icon</i>
<i class="icon karma-icon">karma-icon</i>
<i class="icon keybase-icon">keybase-icon</i>
<i class="icon keynote-icon">keynote-icon</i>
<i class="icon khronos-icon">khronos-icon</i>
<i class="icon kicad-icon">kicad-icon</i>
<i class="icon kitchenci-icon">kitchenci-icon</i>
<i class="icon kivy-icon">kivy-icon</i>
<i class="icon knockout-icon">knockout-icon</i>
<i class="icon kos-icon">kos-icon</i>
<i class="icon kotlin-icon">kotlin-icon</i>
<i class="icon krl-icon">krl-icon</i>
<i class="icon kubernetes-icon">kubernetes-icon</i>
<i class="icon kusto-icon">kusto-icon</i>
<i class="icon labview-icon">labview-icon</i>
<i class="icon laravel-icon">laravel-icon</i>
<i class="icon lark-icon">lark-icon</i>
<i class="icon lasso-icon">lasso-icon</i>
<i class="icon latino-icon">latino-icon</i>
<i class="icon leaflet-icon">leaflet-icon</i>
<i class="icon lean-icon">lean-icon</i>
<i class="icon lefthook-icon">lefthook-icon</i>
<i class="icon lein-icon">lein-icon</i>
<i class="icon lektor-icon">lektor-icon</i>
<i class="icon lerna-icon">lerna-icon</i>
<i class="icon lex-icon">lex-icon</i>
<i class="icon lfe-icon">lfe-icon</i>
<i class="icon lgtm-icon">lgtm-icon</i>
<i class="icon lighthouse-icon">lighthouse-icon</i>
<i class="icon lightwave-icon">lightwave-icon</i>
<i class="icon lilypond-icon">lilypond-icon</i>
<i class="icon lime-icon">lime-icon</i>
<i class="icon linqpad-icon">linqpad-icon</i>
<i class="icon lisp-icon">lisp-icon</i>
<i class="icon llvm-icon">llvm-icon</i>
<i class="icon logtalk-icon">logtalk-icon</i>
<i class="icon lolcode-icon">lolcode-icon</i>
<i class="icon lookml-icon">lookml-icon</i>
<i class="icon ls-icon">ls-icon</i>
<i class="icon lsl-icon">lsl-icon</i>
<i class="icon lua-icon">lua-icon</i>
<i class="icon lync-icon">lync-icon</i>
<i class="icon macaulay2-icon">macaulay2-icon</i>
<i class="icon mako-icon">mako-icon</i>
<i class="icon manpage-icon">manpage-icon</i>
<i class="icon mapbox-icon">mapbox-icon</i>
<i class="icon marko-icon">marko-icon</i>
<i class="icon markdownlint-icon">markdownlint-icon</i>
<i class="icon mathematica-icon">mathematica-icon</i>
<i class="icon mathjax-icon">mathjax-icon</i>
<i class="icon matlab-icon">matlab-icon</i>
<i class="icon matroska-icon">matroska-icon</i>
<i class="icon max-icon">max-icon</i>
<i class="icon maxscript-icon">maxscript-icon</i>
<i class="icon maya-icon">maya-icon</i>
<i class="icon mdx-icon">mdx-icon</i>
<i class="icon mediawiki-icon">mediawiki-icon</i>
<i class="icon melpa-icon">melpa-icon</i>
<i class="icon mercury-icon">mercury-icon</i>
<i class="icon mermaid-icon">mermaid-icon</i>
<i class="icon meson-icon">meson-icon</i>
<i class="icon metal-icon">metal-icon</i>
<i class="icon metapost-icon">metapost-icon</i>
<i class="icon meteor-icon">meteor-icon</i>
<i class="icon minecraft-icon">minecraft-icon</i>
<i class="icon minizinc-icon">minizinc-icon</i>
<i class="icon mint-icon">mint-icon</i>
<i class="icon mirah-icon">mirah-icon</i>
<i class="icon miranda-icon">miranda-icon</i>
<i class="icon mirc-icon">mirc-icon</i>
<i class="icon mixin-icon">mixin-icon</i>
<i class="icon mjml-icon">mjml-icon</i>
<i class="icon mocha-icon">mocha-icon</i>
<i class="icon model-icon">model-icon</i>
<i class="icon modernweb-icon">modernweb-icon</i>
<i class="icon modula2-icon">modula2-icon</i>
<i class="icon modula3-icon">modula3-icon</i>
<i class="icon modelica-icon">modelica-icon</i>
<i class="icon modo-icon">modo-icon</i>
<i class="icon moho-icon">moho-icon</i>
<i class="icon moleculer-icon">moleculer-icon</i>
<i class="icon moment-icon">moment-icon</i>
<i class="icon moment-tz-icon">moment-tz-icon</i>
<i class="icon monotone-icon">monotone-icon</i>
<i class="icon monkey-icon">monkey-icon</i>
<i class="icon mruby-icon">mruby-icon</i>
<i class="icon msproject-icon">msproject-icon</i>
<i class="icon mupad-icon">mupad-icon</i>
<i class="icon mustache-icon">mustache-icon</i>
<i class="icon n64-icon">n64-icon</i>
<i class="icon nailpolish-icon">nailpolish-icon</i>
<i class="icon nano-icon">nano-icon</i>
<i class="icon nanoc-icon">nanoc-icon</i>
<i class="icon nant-icon">nant-icon</i>
<i class="icon nasm-icon">nasm-icon</i>
<i class="icon ndepend-icon">ndepend-icon</i>
<i class="icon neko-icon">neko-icon</i>
<i class="icon neo4j-icon">neo4j-icon</i>
<i class="icon neon-icon">neon-icon</i>
<i class="icon nessus-icon">nessus-icon</i>
<i class="icon netlify-icon">netlify-icon</i>
<i class="icon commitizen-icon">commitizen-icon</i>
<i class="icon netlogo-icon">netlogo-icon</i>
<i class="icon newrelic-icon">newrelic-icon</i>
<i class="icon nextflow-icon">nextflow-icon</i>
<i class="icon nextjs-icon">nextjs-icon</i>
<i class="icon nestjs-icon">nestjs-icon</i>
<i class="icon nginx-icon">nginx-icon</i>
<i class="icon nib-icon">nib-icon</i>
<i class="icon nickle-icon">nickle-icon</i>
<i class="icon nightwatch-icon">nightwatch-icon</i>
<i class="icon nimble-icon">nimble-icon</i>
<i class="icon nimrod-icon">nimrod-icon</i>
<i class="icon nit-icon">nit-icon</i>
<i class="icon nix-icon">nix-icon</i>
<i class="icon nmap-icon">nmap-icon</i>
<i class="icon nodemon-icon">nodemon-icon</i>
<i class="icon nokogiri-icon">nokogiri-icon</i>
<i class="icon nomad-icon">nomad-icon</i>
<i class="icon noon-icon">noon-icon</i>
<i class="icon normalize-icon">normalize-icon</i>
<i class="icon npm-icon">npm-icon</i>
<i class="icon nsis-icon">nsis-icon</i>
<i class="icon nsri-icon">nsri-icon</i>
<i class="icon nsri-alt-icon">nsri-alt-icon</i>
<i class="icon nuclide-icon">nuclide-icon</i>
<i class="icon nuget-icon">nuget-icon</i>
<i class="icon numpy-icon">numpy-icon</i>
<i class="icon nunjucks-icon">nunjucks-icon</i>
<i class="icon nuxt-icon">nuxt-icon</i>
<i class="icon nwscript-icon">nwscript-icon</i>
<i class="icon nx-icon">nx-icon</i>
<i class="icon nxc-icon">nxc-icon</i>
<i class="icon nvidia-icon">nvidia-icon</i>
<i class="icon oberon-icon">oberon-icon</i>
<i class="icon objj-icon">objj-icon</i>
<i class="icon ocaml-icon">ocaml-icon</i>
<i class="icon octave-icon">octave-icon</i>
<i class="icon odin-icon">odin-icon</i>
<i class="icon omnigraffle-icon">omnigraffle-icon</i>
<i class="icon ogone-icon">ogone-icon</i>
<i class="icon onenote-icon">onenote-icon</i>
<i class="icon ooc-icon">ooc-icon</i>
<i class="icon opa-icon">opa-icon</i>
<i class="icon openbsd-icon">openbsd-icon</i>
<i class="icon opencl-icon">opencl-icon</i>
<i class="icon openexr-icon">openexr-icon</i>
<i class="icon opengl-icon">opengl-icon</i>
<i class="icon openoffice-icon">openoffice-icon</i>
<i class="icon openpolicy-icon">openpolicy-icon</i>
<i class="icon openvms-icon">openvms-icon</i>
<i class="icon openvpn-icon">openvpn-icon</i>
<i class="icon openzfs-icon">openzfs-icon</i>
<i class="icon org-icon">org-icon</i>
<i class="icon outlook-icon">outlook-icon</i>
<i class="icon owl-icon">owl-icon</i>
<i class="icon ox-icon">ox-icon</i>
<i class="icon oxygene-icon">oxygene-icon</i>
<i class="icon oz-icon">oz-icon</i>
<i class="icon p4-icon">p4-icon</i>
<i class="icon pan-icon">pan-icon</i>
<i class="icon papyrus-icon">papyrus-icon</i>
<i class="icon parrot-icon">parrot-icon</i>
<i class="icon pascal-icon">pascal-icon</i>
<i class="icon patch-icon">patch-icon</i>
<i class="icon patreon-icon">patreon-icon</i>
<i class="icon pawn-icon">pawn-icon</i>
<i class="icon pcd-icon">pcd-icon</i>
<i class="icon peg-icon">peg-icon</i>
<i class="icon perl6-icon">perl6-icon</i>
<i class="icon phalcon-icon">phalcon-icon</i>
<i class="icon phoenix-icon">phoenix-icon</i>
<i class="icon photorec-icon">photorec-icon</i>
<i class="icon php-icon">php-icon</i>
<i class="icon phpunit-icon">phpunit-icon</i>
<i class="icon phraseapp-icon">phraseapp-icon</i>
<i class="icon pickle-icon">pickle-icon</i>
<i class="icon pico8-icon">pico8-icon</i>
<i class="icon picolisp-icon">picolisp-icon</i>
<i class="icon pike-icon">pike-icon</i>
<i class="icon pinescript-icon">pinescript-icon</i>
<i class="icon pipenv-icon">pipenv-icon</i>
<i class="icon platformio-icon">platformio-icon</i>
<i class="icon pm2-icon">pm2-icon</i>
<i class="icon pnpm-icon">pnpm-icon</i>
<i class="icon pod-icon">pod-icon</i>
<i class="icon pogo-icon">pogo-icon</i>
<i class="icon pointwise-icon">pointwise-icon</i>
<i class="icon polymer-icon">polymer-icon</i>
<i class="icon pony-icon">pony-icon</i>
<i class="icon postcss-icon">postcss-icon</i>
<i class="icon postscript-icon">postscript-icon</i>
<i class="icon povray-icon">povray-icon</i>
<i class="icon powerbuilder-icon">powerbuilder-icon</i>
<i class="icon powerpoint-icon">powerpoint-icon</i>
<i class="icon powershell-icon">powershell-icon</i>
<i class="icon precommit-icon">precommit-icon</i>
<i class="icon premiere-icon">premiere-icon</i>
<i class="icon prettier-icon">prettier-icon</i>
<i class="icon prisma-icon">prisma-icon</i>
<i class="icon processing-icon">processing-icon</i>
<i class="icon progress-icon">progress-icon</i>
<i class="icon proselint-icon">proselint-icon</i>
<i class="icon pros-icon">pros-icon</i>
<i class="icon propeller-icon">propeller-icon</i>
<i class="icon protractor-icon">protractor-icon</i>
<i class="icon psd-icon">psd-icon</i>
<i class="icon publisher-icon">publisher-icon</i>
<i class="icon pug-alt-icon">pug-alt-icon</i>
<i class="icon pug-icon">pug-icon</i>
<i class="icon puppet-icon">puppet-icon</i>
<i class="icon pure-icon">pure-icon</i>
<i class="icon purebasic-icon">purebasic-icon</i>
<i class="icon purescript-icon">purescript-icon</i>
<i class="icon pullapprove-icon">pullapprove-icon</i>
<i class="icon pypi-icon">pypi-icon</i>
<i class="icon pyret-icon">pyret-icon</i>
<i class="icon pytest-icon">pytest-icon</i>
<i class="icon pyup-icon">pyup-icon</i>
<i class="icon qiskit-icon">qiskit-icon</i>
<i class="icon qlik-icon">qlik-icon</i>
<i class="icon qsharp-icon">qsharp-icon</i>
<i class="icon qt-icon">qt-icon</i>
<i class="icon quasar-icon">quasar-icon</i>
<i class="icon r-icon">r-icon</i>
<i class="icon racket-icon">racket-icon</i>
<i class="icon raml-icon">raml-icon</i>
<i class="icon rascal-icon">rascal-icon</i>
<i class="icon razzle-icon">razzle-icon</i>
<i class="icon rdata-icon">rdata-icon</i>
<i class="icon rdoc-icon">rdoc-icon</i>
<i class="icon react-icon">react-icon</i>
<i class="icon readthedocs-icon">readthedocs-icon</i>
<i class="icon reason-icon">reason-icon</i>
<i class="icon reasonstudios-icon">reasonstudios-icon</i>
<i class="icon rebol-icon">rebol-icon</i>
<i class="icon red-icon">red-icon</i>
<i class="icon redux-icon">redux-icon</i>
<i class="icon reek-icon">reek-icon</i>
<i class="icon regex-icon">regex-icon</i>
<i class="icon remark-icon">remark-icon</i>
<i class="icon renovate-icon">renovate-icon</i>
<i class="icon rescript-icon">rescript-icon</i>
<i class="icon restql-icon">restql-icon</i>
<i class="icon rexx-icon">rexx-icon</i>
<i class="icon rhino-icon">rhino-icon</i>
<i class="icon ring-icon">ring-icon</i>
<i class="icon riot-icon">riot-icon</i>
<i class="icon rmarkdown-icon">rmarkdown-icon</i>
<i class="icon robot-icon">robot-icon</i>
<i class="icon robots-icon">robots-icon</i>
<i class="icon rollup-icon">rollup-icon</i>
<i class="icon rspec-icon">rspec-icon</i>
<i class="icon rst-icon">rst-icon</i>
<i class="icon rstudio-icon">rstudio-icon</i>
<i class="icon rsync-icon">rsync-icon</i>
<i class="icon rubocop-icon">rubocop-icon</i>
<i class="icon rubygems-icon">rubygems-icon</i>
<i class="icon sac-icon">sac-icon</i>
<i class="icon sage-icon">sage-icon</i>
<i class="icon sails-icon">sails-icon</i>
<i class="icon saltstack-icon">saltstack-icon</i>
<i class="icon sas-icon">sas-icon</i>
<i class="icon san-icon">san-icon</i>
<i class="icon sandbox-icon">sandbox-icon</i>
<i class="icon sbt-icon">sbt-icon</i>
<i class="icon scad-icon">scad-icon</i>
<i class="icon scd-icon">scd-icon</i>
<i class="icon scheme-icon">scheme-icon</i>
<i class="icon scilab-icon">scilab-icon</i>
<i class="icon scilla-icon">scilla-icon</i>
<i class="icon scratch-icon">scratch-icon</i>
<i class="icon scrutinizer-icon">scrutinizer-icon</i>
<i class="icon self-icon">self-icon</i>
<i class="icon semrelease-icon">semrelease-icon</i>
<i class="icon sentry-icon">sentry-icon</i>
<i class="icon serverless-icon">serverless-icon</i>
<i class="icon sequelize-icon">sequelize-icon</i>
<i class="icon sf-icon">sf-icon</i>
<i class="icon sgi-icon">sgi-icon</i>
<i class="icon shadowcljs-icon">shadowcljs-icon</i>
<i class="icon shellcheck-icon">shellcheck-icon</i>
<i class="icon shen-icon">shen-icon</i>
<i class="icon shipit-icon">shipit-icon</i>
<i class="icon shippable-icon">shippable-icon</i>
<i class="icon shopify-icon">shopify-icon</i>
<i class="icon shuriken-icon">shuriken-icon</i>
<i class="icon sigils-icon">sigils-icon</i>
<i class="icon silverstripe-icon">silverstripe-icon</i>
<i class="icon sketch-icon">sketch-icon</i>
<i class="icon sketchup-lo-icon">sketchup-lo-icon</i>
<i class="icon sketchup-mk-icon">sketchup-mk-icon</i>
<i class="icon sketchup-sb-icon">sketchup-sb-icon</i>
<i class="icon slash-icon">slash-icon</i>
<i class="icon snapcraft-icon">snapcraft-icon</i>
<i class="icon snort-icon">snort-icon</i>
<i class="icon snowpack-icon">snowpack-icon</i>
<i class="icon snyk-icon">snyk-icon</i>
<i class="icon solidarity-icon">solidarity-icon</i>
<i class="icon solidity-icon">solidity-icon</i>
<i class="icon sophia-icon">sophia-icon</i>
<i class="icon sorbet-icon">sorbet-icon</i>
<i class="icon source-icon">source-icon</i>
<i class="icon spacengine-icon">spacengine-icon</i>
<i class="icon spacemacs-icon">spacemacs-icon</i>
<i class="icon sparql-icon">sparql-icon</i>
<i class="icon sqf-icon">sqf-icon</i>
<i class="icon sqlite-icon">sqlite-icon</i>
<i class="icon squarespace-icon">squarespace-icon</i>
<i class="icon stan-icon">stan-icon</i>
<i class="icon stata-icon">stata-icon</i>
<i class="icon stdlibjs-icon">stdlibjs-icon</i>
<i class="icon stencil-icon">stencil-icon</i>
<i class="icon stitches-icon">stitches-icon</i>
<i class="icon storyist-icon">storyist-icon</i>
<i class="icon strings-icon">strings-icon</i>
<i class="icon stylable-icon">stylable-icon</i>
<i class="icon storybook-icon">storybook-icon</i>
<i class="icon stylelint-icon">stylelint-icon</i>
<i class="icon stylishhaskell-icon">stylishhaskell-icon</i>
<i class="icon stylus-icon">stylus-icon</i>
<i class="icon sublime-icon">sublime-icon</i>
<i class="icon svelte-icon">svelte-icon</i>
<i class="icon svn-icon">svn-icon</i>
<i class="icon swagger-icon">swagger-icon</i>
<i class="icon sysverilog-icon">sysverilog-icon</i>
<i class="icon tag-icon">tag-icon</i>
<i class="icon tailwind-icon">tailwind-icon</i>
<i class="icon tcl-icon">tcl-icon</i>
<i class="icon telegram-icon">telegram-icon</i>
<i class="icon templeos-icon">templeos-icon</i>
<i class="icon terminal-icon">terminal-icon</i>
<i class="icon tern-icon">tern-icon</i>
<i class="icon terraform-icon">terraform-icon</i>
<i class="icon terser-icon">terser-icon</i>
<i class="icon testcafe-icon">testcafe-icon</i>
<i class="icon test-coffee-icon">test-coffee-icon</i>
<i class="icon test-dir-icon">test-dir-icon</i>
<i class="icon test-generic-icon">test-generic-icon</i>
<i class="icon test-go-icon">test-go-icon</i>
<i class="icon test-hs-icon">test-hs-icon</i>
<i class="icon test-js-icon">test-js-icon</i>
<i class="icon test-perl-icon">test-perl-icon</i>
<i class="icon test-python-icon">test-python-icon</i>
<i class="icon test-react-icon">test-react-icon</i>
<i class="icon test-ruby-icon">test-ruby-icon</i>
<i class="icon test-rust-icon">test-rust-icon</i>
<i class="icon test-ts-icon">test-ts-icon</i>
<i class="icon tex-icon">tex-icon</i>
<i class="icon textile-icon">textile-icon</i>
<i class="icon textmate-icon">textmate-icon</i>
<i class="icon tfs-icon">tfs-icon</i>
<i class="icon thor-icon">thor-icon</i>
<i class="icon tilt-icon">tilt-icon</i>
<i class="icon tipe-icon">tipe-icon</i>
<i class="icon tla-icon">tla-icon</i>
<i class="icon tmux-icon">tmux-icon</i>
<i class="icon toml-icon">toml-icon</i>
<i class="icon tortoise-icon">tortoise-icon</i>
<i class="icon totvs-icon">totvs-icon</i>
<i class="icon truffle-icon">truffle-icon</i>
<i class="icon ts-icon">ts-icon</i>
<i class="icon tsx-icon">tsx-icon</i>
<i class="icon tt-icon">tt-icon</i>
<i class="icon ttcn3-icon">ttcn3-icon</i>
<i class="icon turing-icon">turing-icon</i>
<i class="icon twine-icon">twine-icon</i>
<i class="icon twig-icon">twig-icon</i>
<i class="icon txl-icon">txl-icon</i>
<i class="icon typedoc-icon">typedoc-icon</i>
<i class="icon typings-icon">typings-icon</i>
<i class="icon ufo-icon">ufo-icon</i>
<i class="icon unibeautify-icon">unibeautify-icon</i>
<i class="icon unicode-icon">unicode-icon</i>
<i class="icon uno-icon">uno-icon</i>
<i class="icon unreal-icon">unreal-icon</i>
<i class="icon urweb-icon">urweb-icon</i>
<i class="icon v-icon">v-icon</i>
<i class="icon v8-icon">v8-icon</i>
<i class="icon v8-turbofan-icon">v8-turbofan-icon</i>
<i class="icon vagrant-icon">vagrant-icon</i>
<i class="icon vala-icon">vala-icon</i>
<i class="icon varnish-icon">varnish-icon</i>
<i class="icon velocity-icon">velocity-icon</i>
<i class="icon verilog-icon">verilog-icon</i>
<i class="icon vertex-icon">vertex-icon</i>
<i class="icon vhdl-icon">vhdl-icon</i>
<i class="icon video-icon">video-icon</i>
<i class="icon virtualbox-icon">virtualbox-icon</i>
<i class="icon vite-icon">vite-icon</i>
<i class="icon visio-icon">visio-icon</i>
<i class="icon vmware-icon">vmware-icon</i>
<i class="icon vray-icon">vray-icon</i>
<i class="icon vue-icon">vue-icon</i>
<i class="icon vyper-icon">vyper-icon</i>
<i class="icon vsts-icon">vsts-icon</i>
<i class="icon w3c-icon">w3c-icon</i>
<i class="icon wallaby-icon">wallaby-icon</i>
<i class="icon walt-icon">walt-icon</i>
<i class="icon warcraft3-icon">warcraft3-icon</i>
<i class="icon wasm-icon">wasm-icon</i>
<i class="icon watchman-icon">watchman-icon</i>
<i class="icon wenyan-icon">wenyan-icon</i>
<i class="icon wdl-icon">wdl-icon</i>
<i class="icon webgl-icon">webgl-icon</i>
<i class="icon webhint-icon">webhint-icon</i>
<i class="icon webpack-icon">webpack-icon</i>
<i class="icon webvtt-icon">webvtt-icon</i>
<i class="icon wercker-icon">wercker-icon</i>
<i class="icon wget-icon">wget-icon</i>
<i class="icon wine-icon">wine-icon</i>
<i class="icon windi-icon">windi-icon</i>
<i class="icon winui-icon">winui-icon</i>
<i class="icon wix-icon">wix-icon</i>
<i class="icon wolfram-icon">wolfram-icon</i>
<i class="icon word-icon">word-icon</i>
<i class="icon workbox-icon">workbox-icon</i>
<i class="icon wurst-icon">wurst-icon</i>
<i class="icon xamarin-icon">xamarin-icon</i>
<i class="icon x10-icon">x10-icon</i>
<i class="icon xmake-icon">xmake-icon</i>
<i class="icon xmos-icon">xmos-icon</i>
<i class="icon xojo-icon">xojo-icon</i>
<i class="icon xpages-icon">xpages-icon</i>
<i class="icon xtend-icon">xtend-icon</i>
<i class="icon yaml-icon">yaml-icon</i>
<i class="icon yamllint-icon">yamllint-icon</i>
<i class="icon yandex-icon">yandex-icon</i>
<i class="icon yang-icon">yang-icon</i>
<i class="icon yara-icon">yara-icon</i>
<i class="icon yarn-icon">yarn-icon</i>
<i class="icon yorick-icon">yorick-icon</i>
<i class="icon yui-icon">yui-icon</i>
<i class="icon zbrush-icon">zbrush-icon</i>
<i class="icon zeit-icon">zeit-icon</i>
<i class="icon zephir-icon">zephir-icon</i>
<i class="icon zig-icon">zig-icon</i>
<i class="icon zimpl-icon">zimpl-icon</i>
<i class="icon zork-icon">zork-icon</i>
</div>
</div>
<div id="outline-container-youtube-video-embeds" class="outline-2">
<h2 id="youtube-video-embeds"><span class="section-number-2">20.</span> YouTube Video Embeds&#xa0;&#xa0;&#xa0;<span class="tag"><span class="video">video</span>&#xa0;<span class="media">media</span></span></h2>
<div class="outline-text-2" id="text-youtube-video-embeds">
<p>
This blog supports embedding YouTube videos directly in posts using a custom org-mode link type. Videos are embedded as responsive iframes using <code>youtube-nocookie.com</code> for privacy (no tracking cookies).
</p>
</div>
<div id="outline-container-basic-embed" class="outline-3">
<h3 id="basic-embed"><span class="section-number-3">20.1.</span> Basic Embed</h3>
<div class="outline-text-3" id="text-basic-embed">
<p>
The simplest way to embed a video is using just the video ID:
</p>

<pre class="example" id="org6e8d653">
[[youtube:aqz-KE-bpKQ][Big Buck Bunny]]
</pre>

<p>
Which renders as:
</p>

<p>
<div class="youtube-container">
<iframe src="https://www.youtube-nocookie.com/embed/aqz-KE-bpKQ"
        title="Big Buck Bunny"
        frameborder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
        referrerpolicy="strict-origin-when-cross-origin"
        allowfullscreen>
</iframe>
</div>
</p>
</div>
</div>
<div id="outline-container-using-full-urls" class="outline-3">
<h3 id="using-full-urls"><span class="section-number-3">20.2.</span> Using Full URLs</h3>
<div class="outline-text-3" id="text-using-full-urls">
<p>
You can also use full YouTube URLs - the video ID is automatically extracted:
</p>

<pre class="example" id="org5241bd6">
[[youtube:https://www.youtube.com/watch?v=aqz-KE-bpKQ][Big Buck Bunny (full URL)]]
</pre>
</div>
</div>
<div id="outline-container-short-alias" class="outline-3">
<h3 id="short-alias"><span class="section-number-3">20.3.</span> Short Alias</h3>
<div class="outline-text-3" id="text-short-alias">
<p>
For convenience, <code>yt:</code> works as a shorter alias:
</p>

<pre class="example" id="orgb6e9633">
[[yt:aqz-KE-bpKQ][Short alias example]]
</pre>

<p>
The embed is responsive and maintains a 16:9 aspect ratio on all screen sizes.
</p>
</div>
</div>
</div>
<div id="outline-container-socials" class="outline-2">
<h2 id="socials"><span class="section-number-2">21.</span> socials</h2>
<div class="outline-text-2" id="text-socials">
<ul class="org-ul">
<li><a href="https://www.reddit.com/r/emacs/comments/1qiy6kb/blogging_with_emacs_orgmode_and_sveltekit/">reddit: Blogging with Emacs org-mode and SvelteKit</a></li>
<li><a href="https://news.ycombinator.com/item?id=46992142">hackernews: Blogging with Emacs org-mode and SvelteKit</a></li>
<li><a href="https://www.linkedin.com/feed/update/urn:li:activity:7427766416608768000/">linkedin: Blogging with Emacs org-mode and SvelteKit</a></li>
</ul>
</div>
</div>
<div id="outline-container-glossary" class="outline-2">
<h2 id="glossary"><span class="section-number-2">22.</span> Glossary&#xa0;&#xa0;&#xa0;<span class="tag"><span class="glossary">glossary</span></span></h2>
<div class="outline-text-2" id="text-glossary">
<dl class="org-dl">
<dt>HUD</dt><dd>Heads-Up Display. The contextual sideline components surrounding the article that provide navigation, search, and metadata at a glance.</dd>
<dt>Org Mode</dt><dd>A major mode for Emacs that provides tools for keeping notes, maintaining TODO lists, and authoring documents with a fast and effective plain-text markup language.</dd>
<dt>SvelteKit</dt><dd>A full-stack framework built on top of Svelte for building web applications with server-side rendering, routing, and other production features.</dd>
<dt>Treemap</dt><dd>A visualization that displays hierarchical data as nested rectangles, where each rectangle's area is proportional to the data it represents. Used here to show article section sizes.</dd>
<dt>Transclusion</dt><dd>The inclusion of content from one document inside another by reference, so the same source text appears in multiple places without duplication.</dd>
<dt>Literate Programming</dt><dd>A programming paradigm where code is embedded within human-readable documentation, allowing programs to be written as narratives that interleave explanation with executable source code.</dd>
<dt>Plotly</dt><dd>An open-source graphing library for creating interactive, publication-quality charts and visualizations in the browser.</dd>
<dt>Mermaid</dt><dd>A JavaScript-based diagramming tool that renders Markdown-inspired text definitions into diagrams and flowcharts dynamically.</dd>
<dt>Minimap</dt><dd>A scaled-down overview of the entire article displayed in the sidebar, showing your current scroll position and providing a bird's-eye view of the document structure.</dd>
</dl>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">23.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
This page demonstrates a comprehensive blogging framework built with <a href="https://www.chiply.dev/#about">Emacs Org Mode and SvelteKit</a>, featuring an innovative heads-up-display (HUD) that enhances reading comprehension through contextual sideline components. The <a href="https://www.chiply.dev/#what-s-going-on-in-the-sidelines-">HUD system</a> includes a table of contents, footnotes, references, social media links, TLDR summary, tags, minimap, full-text search, breadcrumbs, and progress bar - all of which dynamically update as you scroll through the content.
</p>

<p>
The framework showcases extensive support for <a href="https://www.chiply.dev/#interactive-data-visualizations">interactive data visualizations using Plotly</a>, including bar charts, distribution plots, animations, and 3D visualizations that rival professional digital journalism platforms. <a href="https://www.chiply.dev/#software-architecture-diagrams">Software architecture diagrams</a> are created using Mermaid, enabling code-based maintenance of complex technical illustrations from simple microservices to global multi-region architectures.
</p>

<p>
<a href="https://www.chiply.dev/#code">Literate programming capabilities</a> are demonstrated through org-babel integration, allowing executable code blocks in multiple languages including bash, Python, and others, with results embedded directly in the document. The <a href="https://www.chiply.dev/#fancy-text--sub--and-superscripts-">fancy text features</a> include LaTeX math rendering for complex equations spanning various mathematical and physics domains.
</p>

<p>
Content organization leverages <a href="https://www.chiply.dev/#tags">hierarchical tagging</a> at the header level for non-hierarchical content relationships, while <a href="https://www.chiply.dev/#links">comprehensive link support</a> distinguishes between intra-post, inter-post, and external references with appropriate visual styling. The <a href="https://www.chiply.dev/#transclusion">transclusion feature</a> enables embedding content from other posts verbatim, maintaining consistency across related articles.
</p>

<p>
Rich media support includes <a href="https://www.chiply.dev/#images">local and remote images</a>, <a href="https://www.chiply.dev/#displaying-gifs">animated GIFs for UI demonstrations</a>, and planned video integration. <a href="https://www.chiply.dev/#structured-text--tables--lists--sub-and-superscripts-">Structured text elements</a> like tables (including wide tables with horizontal scrolling), various list formats, and <a href="https://www.chiply.dev/#adding-footnotes">footnotes</a> are fully supported with proper HTML rendering.
</p>

<p>
<a href="https://www.chiply.dev/#narrative-features">Narrative features</a> encompass long-form text with embedded footnotes, block quotes with attribution, verse blocks, and centered text, all with custom CSS styling. The framework includes <a href="https://www.chiply.dev/#iconography">extensive iconography support</a> using file-icons for visual enhancement of technical content.
</p>

<p>
<a href="https://www.chiply.dev/#styling">Styling capabilities</a> allow inline CSS inclusion and custom formatting options, making the blog both functional and visually appealing. The entire system serves as both a working blog and a <a href="https://www.chiply.dev/#features">comprehensive feature demonstration</a>, with progress tracking on headings showing completed (DONE) and in-progress (TODO) implementations.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Both explicit relationships (like direct links from one post to another) and implicit relationships (like tags and semantic relationships) can be used to traverse the underlying content graph present in any wiki-style document system.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
These TODO states are actually from org-mode.  Note tha todo state change can be (and is in my configuration) being logged within the org document automatically, offering some ability to visualize progress over time of tasks defined wihin the document 
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Note I am using Svelte 5 and mostly Typescript for this project, so some syntax may be different than Svelte 3 and/or plain javascript examples you may find online. 
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This includes all the text, links, images, code blocks, footnotes, code, code outtput, and more.  The benefits of using org-mode are beyond the scope of this demo, but you can read more about them <a href="https://orgmode.org/">here</a>. 
</p></div></div>

<div class="footdef"><sup><a id="fn.5" class="footnum" href="https://www.chiply.dev/#fnr.5" role="doc-backlink">5</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The sideline features only appear in the desktop version of the web app.
</p></div></div>

<div class="footdef"><sup><a id="fn.6" class="footnum" href="https://www.chiply.dev/#fnr.6" role="doc-backlink">6</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
ASCII art works because I'm using a monospace bit-map font on my blog. 
</p></div></div>

<div class="footdef"><sup><a id="fn.7" class="footnum" href="https://www.chiply.dev/#fnr.7" role="doc-backlink">7</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This is how to write code with python 
</p></div></div>

<div class="footdef"><sup><a id="fn.8" class="footnum" href="https://www.chiply.dev/#fnr.8" role="doc-backlink">8</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
new footnote 
</p></div></div>

<div class="footdef"><sup><a id="fn.9" class="footnum" href="https://www.chiply.dev/#fnr.9" role="doc-backlink">9</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This is the footnote.  lorem impsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. <b>Excepteur sint occaecat cupidatat</b> non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.  Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
</p></div></div>

<div class="footdef"><sup><a id="fn.10" class="footnum" href="https://www.chiply.dev/#fnr.10" role="doc-backlink">10</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
even more 
</p></div></div>

<div class="footdef"><sup><a id="fn.11" class="footnum" href="https://www.chiply.dev/#fnr.11" role="doc-backlink">11</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
More footnotes 
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>The Interest Rate on Your Codebase: A Financial Framework for Technical Debt</title>
      <link>https://www.chiply.dev/post-technical-debt</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-technical-debt</guid>
      <pubDate>Mon, 23 Feb 2026 00:58:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Every engineer I&apos;ve worked with has used the phrase &quot;technical debt&quot;, but everyone seems to have a different notion of what this phrase actually means. I&apos;ve also found the phrase to be quite controver...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="technicalDebt">technicalDebt</span>&#xa0;<span class="softwareEngineering">softwareEngineering</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org7f3944d" class="figure">
<p><img src="https://www.chiply.dev/images/tech-debt-banner.jpeg" alt="tech-debt-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Every engineer I've worked with has used the phrase "technical debt", but everyone seems to have a different notion of what this phrase actually means.  I've also found the phrase to be quite controversial, sparking knee jerk reactions that cut short any meaningful discussions around how to improve codebases and the systems they precipitate.
</p>

<p>
In general, I feel the term has become a catch-all for anything in a codebase that someone doesn't like &#x2013; messy code, missing tests, old frameworks, that one service nobody wants to touch, that slow CI in a repo that everyone <i>has</i> to touch&#x2026;.  This imprecision isn't just sloppy language &#x2013; it's actively harmful.  When everything is "tech debt," nothing is, and teams lose the ability to reason clearly about what to fix, when to fix it, and whether to fix it at all.
</p>

<blockquote class="pull-quote pull-right">
<p>
When everything is "tech debt," nothing is, and teams lose the ability to reason clearly about what to fix, when to fix it, and whether to fix it at all.
</p>
</blockquote>

<p>
This post reclaims the financial metaphor that <a href="https://www.youtube.com/watch?v=pqeJFYwnkjE">Ward Cunningham originally intended</a><sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>, extends it with precise mappings to financial instruments, and proposes concrete management strategies that scale from a two-person startup to a thousand-engineer enterprise.  
</p>

<p>
The key insight elicidated in this post, and the main subject of my argument, is that the real danger isn't the debt itself &#x2013; it's the <i>interest</i>.  This post will be useful to you because, if you are like most teams, you likely don't know your interest rate.
</p>
</div>
<div id="outline-container-executive-summary" class="outline-3">
<h3 id="executive-summary"><span class="section-number-3">1.1.</span> Executive Summary</h3>
<div class="outline-text-3" id="text-executive-summary">
<p>
Technical debt is a deliberate deviation from an optimal design that yields short-term value at the cost of increased future work.  
</p>

<p>
Like financial debt, it has principal (the cost to fix), interest (the ongoing drag), and an interest rate (the frequency with which you interact with the debt multiplied by the friction it causes).
</p>

<p>
The problem isn't taking on debt &#x2013; it's failing to manage the interest.  This post provides a framework for <i>classifying</i>, <i>measuring</i>, and <i>paying down</i> technical debt at any organizational scale.
</p>
</div>
</div>
</div>
<div id="outline-container-what-is-technical-debt--actually-" class="outline-2">
<h2 id="what-is-technical-debt--actually-"><span class="section-number-2">2.</span> What Is Technical Debt, Actually?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="definition">definition</span>&#xa0;<span class="history">history</span></span></h2>
<div class="outline-text-2" id="text-what-is-technical-debt--actually-">
</div>
<div id="outline-container-the-origin-story" class="outline-3">
<h3 id="the-origin-story"><span class="section-number-3">2.1.</span> The Origin Story</h3>
<div class="outline-text-3" id="text-the-origin-story">
<p>
In 1992, Ward Cunningham presented an <a href="https://c2.com/doc/oopsla92.html">experience report at OOPSLA</a> describing the development of a financial portfolio management system in Smalltalk.  He needed to justify ongoing refactoring to his business stakeholders, so he reached for a an unironically relevant metaphor they'd understand<sup><a id="fnr.1.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>:
</p>

<blockquote>
<p>
Shipping first time code is like going into debt.  A little debt speeds development so long as it is paid back promptly with a rewrite&#x2026; The danger occurs when the debt is not repaid.  Every minute spent on not-quite-right code counts as interest on that debt. &#x2013; Ward Cunningham
</p>
</blockquote>

<p>
Notice what he <i>didn't</i> say.  He didn't say "writing bad code is like going into debt."  He said <i>shipping code before you fully understand the problem domain</i> is like going into debt.  The distinction matters enormously.
</p>

<p>
Cunningham was describing something specific: the team had built a working system, and <i>through the process of building it</i>, they had learned things about the domain that the code didn't yet reflect.  The code worked.  It was well-written.  But it encoded an earlier, incomplete understanding of the problem.  The "debt" was the gap between what the team now understood and what the code expressed.
</p>

<p>
This is a much more subtle &#x2013; and much more useful &#x2013; concept than "the code is messy."
</p>
</div>
</div>
<div id="outline-container-a-precise-definition" class="outline-3">
<h3 id="a-precise-definition"><span class="section-number-3">2.2.</span> A Precise Definition</h3>
<div class="outline-text-3" id="text-a-precise-definition">
<p>
Building on Cunningham's original insight, and drawing from a decade of subsequent refinement by <a href="https://martinfowler.com/bliki/TechnicalDebt.html">Martin Fowler</a>, <a href="https://www.amazon.com/Managing-Technical-Debt-Practices-Development/dp/013564593X">Philippe Kruchten</a>, and others, here's the definition I use:
</p>

<blockquote class="pull-quote pull-left">
<p>
Technical debt is a deliberate or accidental deviation from an optimal design that yields short-term value at the cost of increased future work.
</p>
</blockquote>

<p>
<b>Technical debt is a deliberate or accidental deviation from an optimal design that yields short-term value at the cost of increased future work.</b>
</p>

<p>
Furthermore, technical debt has four essential properties:
</p>

<ol class="org-ol">
<li><b>It has principal</b> &#x2013; the cost to remediate or redesign the code to its optimal state</li>
<li><b>It accrues interest</b> &#x2013; the ongoing drag on velocity, reliability, and developer experience caused by working around the suboptimal design</li>
<li><b>It is context-dependent</b> &#x2013; what constitutes "optimal design" changes as requirements evolve, scale increases, or the team's understanding deepens</li>
<li><b>It is not inherently bad</b> &#x2013; like financial debt, it can be a rational tool when used deliberately</li>
</ol>

<p>
That last point is critical.  A mortgage isn't reckless.  Neither is shipping a prototype with hardcoded configuration to validate a business hypothesis.  The question isn't <i>whether</i> to take on debt.  It's <i>whether you know you're doing it</i>, and <i>whether you have a plan to service it</i>.
</p>
</div>
</div>
<div id="outline-container-what-technical-debt-is-not" class="outline-3">
<h3 id="what-technical-debt-is-not"><span class="section-number-3">2.3.</span> What Technical Debt Is Not</h3>
<div class="outline-text-3" id="text-what-technical-debt-is-not">
<p>
Precision matters here.  Conflating these concepts with tech debt dilutes the metaphor to uselessness.
</p>

<ul class="org-ul">
<li><b>Bugs</b> aren't tech debt because bugs are <i>liabilities</i>, not loans &#x2013; you didn't choose them for short-term value.  They're defects.  Fix them on their own merit.</li>

<li><b>Legacy systems</b> aren't tech debt because age \(\neq\) debt.  A 15-year-old COBOL system that runs reliably isn't debt if nobody needs to change it.  It's legacy &#x2013; and only becomes debt if it impedes change.</li>

<li><b>Missing features</b> aren't tech debt because "we haven't built X yet" isn't a loan you took out.  It's product scope.  Put it on the backlog.</li>

<li><b>"Bad code"</b> isn't tech debt because subjective aesthetics aren't debt unless they cause measurable drag.  If the only problem is that you don't like the style, use a linter.</li>

<li><b>Missing documentation</b> isn't necessarily tech debt.  It's only debt if it increases the cost of future work.  Otherwise it's operational risk &#x2013; might be debt, might not.</li>

<li><b>Using an old framework</b> isn't necessarily tech debt.  It's only debt if upgrading would reduce future costs enough to justify the remediation.  Otherwise it's version lag &#x2013; assess on a case-by-case basis.</li>
</ul>

<p>
The litmus test: <b>does this thing have both a principal (cost to fix) and an interest rate (ongoing cost of not fixing it)?</b>  If it only has a principal and no meaningful interest, it's not debt &#x2013; it's just cleanup work.  If it has interest but no meaningful principal (you can't fix it), it's a constraint, not debt.
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #6052cf;">graph</span> <span style="color: #065fff;">TB</span>
    <span style="color: #6052cf;">subgraph</span> <span style="color: #4250ef;">"Is It Really Technical Debt?"</span>
        direction <span style="color: #065fff;">TB</span>
        START<span style="color: #008858;">[</span><span style="color: #4250ef;">"Identified Issue"</span><span style="color: #008858;">]</span>
        Q1<span style="color: #008858;">{</span><span style="color: #4250ef;">"Was it a design choice&lt;br/&gt;(deliberate or inadvertent)?"</span><span style="color: #008858;">}</span>
        Q2<span style="color: #008858;">{</span><span style="color: #4250ef;">"Does it cause ongoing&lt;br/&gt;friction (interest)?"</span><span style="color: #008858;">}</span>
        Q3<span style="color: #008858;">{</span><span style="color: #4250ef;">"Can it be remediated&lt;br/&gt;at a known cost (principal)?"</span><span style="color: #008858;">}</span>

        DEBT<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#9989; Technical Debt&lt;br/&gt;Manage it with the&lt;br/&gt;financial framework"</span><span style="color: #008858;">]</span>
        NOTDEBT1<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#10060; Not Debt&lt;br/&gt;It's a defect &#8212; fix it&lt;br/&gt;on its own merit"</span><span style="color: #008858;">]</span>
        NOTDEBT2<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#10060; Not Debt&lt;br/&gt;It's cleanup, not debt service"</span><span style="color: #008858;">]</span>
        NOTDEBT3<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#10060; Not Debt&lt;br/&gt;It's a constraint,&lt;br/&gt;not a financial instrument"</span><span style="color: #008858;">]</span>

        START <span style="color: #cf25aa;">--&gt;</span> Q1
        Q1 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes &#8212; a tradeoff&lt;br/&gt;was made (knowingly&lt;br/&gt;or not)"</span>| Q2
        Q1 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No &#8212; it's a bug&lt;br/&gt;or defect"</span>| NOTDEBT1
        Q2 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes &#8212; it slows&lt;br/&gt;future work"</span>| Q3
        Q2 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No &#8212; it just&lt;br/&gt;looks ugly"</span>| NOTDEBT2
        Q3 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes"</span>| DEBT
        Q3 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No &#8212; there's no&lt;br/&gt;clear fix"</span>| NOTDEBT3
    <span style="color: #6052cf;">end</span>
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-the-financial-analogy--done-right" class="outline-2">
<h2 id="the-financial-analogy--done-right"><span class="section-number-2">3.</span> The Financial Analogy, Done Right&#xa0;&#xa0;&#xa0;<span class="tag"><span class="financialAnalogy">financialAnalogy</span></span></h2>
<div class="outline-text-2" id="text-the-financial-analogy--done-right">
<p>
The power of Cunningham's metaphor is that finance has <i>centuries</i> of well-developed theory for managing debt.  But most engineers use the analogy at the level of "debt = bad, pay it off" &#x2013; which is about as sophisticated as saying "mortgages are bad, never buy a house."
</p>

<p>
Let's map the concepts precisely.
</p>
</div>
<div id="outline-container-the-instrument-list" class="outline-3">
<h3 id="the-instrument-list"><span class="section-number-3">3.1.</span> The Instrument List</h3>
<div class="outline-text-3" id="text-the-instrument-list">
<ul class="org-ul">
<li><b>Principal</b> is the cost to remediate or redesign.  "Refactoring the auth module will take 3 sprints."</li>

<li><b>Interest</b> is the ongoing maintenance cost, velocity drag, and incident risk.  "Every feature touching auth takes 2x longer and breaks in staging."</li>

<li><b>Interest rate</b> is the frequency of change multiplied by the system friction.  High for core modules that every team touches, low for rarely-touched utilities.</li>

<li><b>Refinancing</b> is a large-scale refactor or migration &#x2013; moving from a monolith to services, upgrading the ORM.  You replace one debt instrument with another that has better terms.</li>

<li><b>Bankruptcy</b> is a full system rewrite.  "We're throwing it away and starting over."  Sometimes necessary, usually avoidable.</li>

<li><b>Credit line</b> is the team's tolerance for complexity before velocity collapses.  It varies by team size, skill, and tooling &#x2013; and it's finite.</li>

<li><b>Cash flow</b> is engineering capacity: the person-hours available for debt service.  It's your sprint capacity minus feature commitments.</li>

<li><b>Amortization</b> is gradual debt paydown alongside feature work.  "Every PR in auth must leave the code better than it found it."</li>

<li><b>Debt-to-equity ratio</b> is the proportion of your codebase that is suboptimal vs. well-designed.  High ratios mean fragility; low ratios mean resilience.</li>

<li><b>Credit rating</b> is the team's track record of paying down debt.  Teams that consistently ship debt paydown earn trust for future borrowing.</li>
</ul>

<p>
A key insight falls out of this mapping:
</p>

<blockquote class="pull-quote pull-right">
<p>
High-interest debt is debt that compounds every time you touch it.
</p>
</blockquote>

<p>
<b>High-interest debt is debt that compounds every time you touch it.</b>  The auth module you hack around daily costs orders of magnitude more than the deployment script you run quarterly &#x2013; even if the deployment script's remediation cost (principal) is lower.  This is why prioritizing debt paydown by "ease of fix" is backwards.  You should prioritize by <i>interest rate</i>.
</p>
</div>
</div>
<div id="outline-container-types-of-technical-debt---approx--types-of-financial-debt" class="outline-3">
<h3 id="types-of-technical-debt---approx--types-of-financial-debt"><span class="section-number-3">3.2.</span> Types of Technical Debt \(\approx\) Types of Financial Debt</h3>
<div class="outline-text-3" id="text-types-of-technical-debt---approx--types-of-financial-debt">
<p>
Not all debt is created equal, and not all debt should be treated the same way.
</p>

<p>
Here are a few high level categories:
</p>
</div>
<div id="outline-container-low-interest--long-term-debt" class="outline-4">
<h4 id="low-interest--long-term-debt"><span class="section-number-4">3.2.1.</span> Low-Interest, Long-Term Debt</h4>
<div class="outline-text-4" id="text-low-interest--long-term-debt">
<p>
<b>Financial analog: a 30-year fixed mortgage.</b>
</p>

<p>
This is stable code that is architecturally imperfect but rarely touched.  For example, this could be a data ingestion pipeline written in Python 2 that runs once a month and processes CSV files.  If it works and nobody modifies it, then the interest is near-zero because the frequency of interaction is near-zero.
</p>

<p>
<b>Management strategy</b>: leave it alone.  Seriously.  Refactoring this is like paying off a 2% mortgage early when you have 20% credit card debt.  It's mathematically silly.
</p>
</div>
</div>
<div id="outline-container-high-interest-revolving-debt" class="outline-4">
<h4 id="high-interest-revolving-debt"><span class="section-number-4">3.2.2.</span> High-Interest Revolving Debt</h4>
<div class="outline-text-4" id="text-high-interest-revolving-debt">
<p>
<b>Financial analog: credit card debt.</b>
</p>

<p>
This is the core module that every team touches daily and everyone hates.  For example, the ORM that requires three workarounds per query, or the authentication layer that breaks in every test environment, or the deployment pipeline that takes 45 minutes and fails 20% of the time.
</p>

<p>
I once worked on a team where the API gateway had been "temporarily" written as a single Express.js middleware file.  Every team in the company routed through it.  Every feature that touched authentication, rate limiting, or request validation required modifying that file.  We had merge conflicts on it <i>daily</i>.  The file was 3,000 lines long.  The principal to fix it &#x2013; decomposing it into proper middleware layers &#x2013; was maybe two weeks of focused work.  We deferred it for eighteen months because no sprint ever had "room."  By the end, we estimated that the accumulated interest &#x2013; in merge conflicts, debugging time, and incidents caused by accidental regressions in that file &#x2013; had cost us closer to six months of engineering time.  Two weeks of principal.  Six months of interest.  That's a 1,200% annual rate.
</p>

<p>
<b>Management strategy</b>: pay this down <i>immediately</i>.  Every day you don't costs more than the day before, because interest compounds with each interaction.  This is where the "just ship it" mentality becomes catastrophically expensive.
</p>
</div>
</div>
<div id="outline-container-variable-rate-debt" class="outline-4">
<h4 id="variable-rate-debt"><span class="section-number-4">3.2.3.</span> Variable-Rate Debt</h4>
<div class="outline-text-4" id="text-variable-rate-debt">
<p>
<b>Financial analog: an adjustable-rate mortgage.</b>
</p>

<p>
This is debt whose cost changes based on external factors.  A system that works fine at current scale but will collapse at 10x.  Hardcoded assumptions about single-region deployment.  A data pipeline that assumes English-only text.
</p>

<p>
The interest rate is low today but will spike when conditions change &#x2013; a new market, a compliance requirement, a viral moment.  The danger is that by the time the rate spikes, the principal has grown too.
</p>

<p>
<b>Management strategy</b>: cap your exposure.  You don't need to fix it now, but you need to <i>know</i> about it and have a plan.  Architecture decision records (ADRs) are your friend here &#x2013; they document the assumptions that, when violated, will trigger a rate increase.
</p>
</div>
</div>
<div id="outline-container-hidden-debt--off-balance-sheet-" class="outline-4">
<h4 id="hidden-debt--off-balance-sheet-"><span class="section-number-4">3.2.4.</span> Hidden Debt (Off-Balance-Sheet)</h4>
<div class="outline-text-4" id="text-hidden-debt--off-balance-sheet-">
<p>
<b>Financial analog: off-balance-sheet liabilities (the kind that caused the 2008 financial crisis).</b>
</p>

<p>
This is the scariest kind.  Tribal knowledge that exists only in one engineer's head.  Undocumented invariants that the code silently depends on.  A deployment process that only works because of a specific order of operations that nobody has written down.
</p>

<p>
Hidden debt is the technical equivalent of off-balance-sheet liabilities &#x2013; the kind that brought down Lehman Brothers.  You don't know about it until it explodes.
</p>

<p>
<b>Management strategy</b>: incognito leverage too dangerous &#x2013; make it visible.  Do bus factor analysis, documentation sprints, runbooks, chaos engineering &#x2013; anything that surfaces hidden assumptions.  You can't manage debt you can't see.
</p>
</div>
</div>
</div>
<div id="outline-container-the-debt-lifecycle" class="outline-3">
<h3 id="the-debt-lifecycle"><span class="section-number-3">3.3.</span> The Debt Lifecycle</h3>
<div class="outline-text-3" id="text-the-debt-lifecycle">
<p>
Technical debt doesn't appear and disappear in a single event &#x2014; it follows a lifecycle.  The diagram below traces debt from the moment it's incurred through a decision point where teams choose one of five responses.  Notice the feedback loops: deferred debt compounds back into accruing interest, and even "resolved" debt eventually gives way to new debt as the system evolves.
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #6052cf;">graph</span> <span style="color: #065fff;">LR</span>
    INCUR<span style="color: #008858;">[</span><span style="color: #4250ef;">"Debt Incurred&lt;br/&gt;(design shortcut taken)"</span><span style="color: #008858;">]</span>
    ACCRUE<span style="color: #008858;">[</span><span style="color: #4250ef;">"Interest Accrues&lt;br/&gt;(ongoing friction)"</span><span style="color: #008858;">]</span>
    DETECT<span style="color: #008858;">[</span><span style="color: #4250ef;">"Debt Detected&lt;br/&gt;(pain threshold reached)"</span><span style="color: #008858;">]</span>
    ASSESS<span style="color: #008858;">[</span><span style="color: #4250ef;">"Debt Assessed&lt;br/&gt;(principal + interest estimated)"</span><span style="color: #008858;">]</span>
    DECIDE<span style="color: #008858;">{</span><span style="color: #4250ef;">"Decision Point"</span><span style="color: #008858;">}</span>
    PAY<span style="color: #008858;">[</span><span style="color: #4250ef;">"Pay Down&lt;br/&gt;(refactor/redesign)"</span><span style="color: #008858;">]</span>
    REFINANCE<span style="color: #008858;">[</span><span style="color: #4250ef;">"Refinance&lt;br/&gt;(large migration)"</span><span style="color: #008858;">]</span>
    ACCEPT<span style="color: #008858;">[</span><span style="color: #4250ef;">"Accept &amp; Monitor&lt;br/&gt;(interest rate is low enough)"</span><span style="color: #008858;">]</span>
    BANKRUPT<span style="color: #008858;">[</span><span style="color: #4250ef;">"Declare Bankruptcy&lt;br/&gt;(full rewrite)"</span><span style="color: #008858;">]</span>
    DEFER<span style="color: #008858;">[</span><span style="color: #4250ef;">"Defer&lt;br/&gt;(interest continues)"</span><span style="color: #008858;">]</span>

    INCUR <span style="color: #cf25aa;">--&gt;</span> ACCRUE
    ACCRUE <span style="color: #cf25aa;">--&gt;</span> DETECT
    DETECT <span style="color: #cf25aa;">--&gt;</span> ASSESS
    ASSESS <span style="color: #cf25aa;">--&gt;</span> DECIDE
    DECIDE <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"High interest,&lt;br/&gt;manageable principal"</span>| PAY
    DECIDE <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"High interest,&lt;br/&gt;large principal"</span>| REFINANCE
    DECIDE <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Low interest"</span>| ACCEPT
    DECIDE <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Unmaintainable"</span>| BANKRUPT
    DECIDE <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No capacity"</span>| DEFER
    DEFER <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Interest compounds"</span>| ACCRUE
    PAY <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Debt resolved"</span>| INCUR
    REFINANCE <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Lower interest rate"</span>| ACCEPT
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-fowler-s-quadrant--why-teams-accumulate-debt" class="outline-2">
<h2 id="fowler-s-quadrant--why-teams-accumulate-debt"><span class="section-number-2">4.</span> Fowler's Quadrant: Why Teams Accumulate Debt&#xa0;&#xa0;&#xa0;<span class="tag"><span class="classification">classification</span>&#xa0;<span class="martinFowler">martinFowler</span></span></h2>
<div class="outline-text-2" id="text-fowler-s-quadrant--why-teams-accumulate-debt">
<p>
Martin Fowler's <a href="https://martinfowler.com/bliki/TechnicalDebtQuadrant.html">Technical Debt Quadrant</a> provides the best taxonomy I've seen for <i>why</i> debt gets created.  It classifies debt along two axes: <b>deliberate vs. inadvertent</b> and <b>prudent vs. reckless</b><sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup>.
</p>

<div id="tech_debt_fowler_quadrant" class="plotly-plot"></div>

<p>
Each quadrant requires a different response:
</p>

<ul class="org-ul">
<li><b>Prudent + Deliberate</b>: This is <i>strategic debt</i>.  It's the mortgage.  Document it, schedule the paydown, and move on.  This is the only quadrant where the financial analogy fully applies, because it involves a conscious tradeoff.</li>

<li><b>Prudent + Inadvertent</b>: This is <i>learning debt</i>.  It's the most sympathetic and the most unavoidable.  You can't know the right design until you've tried the wrong one.  The paydown here is refactoring to reflect your improved understanding &#x2013; exactly what Cunningham described.</li>

<li><b>Reckless + Deliberate</b>: This is <i>negligent debt</i>.  Teams that knowingly ship garbage and call it "velocity" are running up credit card debt with no intention of paying it off.  This is the quadrant that gives technical debt a bad name.</li>

<li><b>Reckless + Inadvertent</b>: This isn't really debt at all &#x2013; it's a <i>knowledge gap</i>.  The team doesn't know enough to recognize the shortcuts they're taking.  The fix isn't refactoring; it's education, mentorship, and pairing.  This includes experienced engineers working in unfamiliar domains &#x2013; backend engineers building frontends, infrastructure engineers writing business logic.  The gap is contextual, not personal.</li>
</ul>
</div>
<div id="outline-container-why-debt-is-rational" class="outline-3">
<h3 id="why-debt-is-rational"><span class="section-number-3">4.1.</span> Why Debt Is Rational</h3>
<div class="outline-text-3" id="text-why-debt-is-rational">
<p>
It's important to resist the temptation to moralize about technical debt.  In many contexts, taking on debt is the <i>right</i> decision:
</p>

<ul class="org-ul">
<li><b>Speed to market</b>: A startup with 6 months of runway doesn't need a perfectly designed system.  It needs product-market fit.  The debt is rational because the alternative is bankruptcy &#x2013; the real kind.</li>
<li><b>Uncertain requirements</b>: When you don't yet know what the system needs to do, investing in a "perfect" architecture is waste.  Build the thing, learn what it should have been, then refactor.  This is Cunningham's original insight.</li>
<li><b>Asymmetric risk</b>: If the upside of shipping fast is 100x and the downside of the debt is 2x, the expected value calculation is trivial.</li>
<li><b>Exploration and prototyping</b>: Prototype code <i>should</i> be debt-heavy.  That's the point.  The mistake is promoting prototype code to production without acknowledging the accumulated debt.</li>
</ul>

<blockquote class="pull-quote pull-right">
<p>
Tech debt is often a sign of rational decision-making under uncertainty &#x2013; not incompetence.
</p>
</blockquote>

<p>
This is the framing I use with stakeholders, and it changes the conversation immediately.  Instead of "we need to clean up the code" (which sounds like an engineering vanity project), it becomes "we need to manage our financial exposure" (which sounds like responsible governance).
</p>
</div>
</div>
</div>
<div id="outline-container-interest--not-principal--is-the-real-danger" class="outline-2">
<h2 id="interest--not-principal--is-the-real-danger"><span class="section-number-2">5.</span> Interest, Not Principal, Is the Real Danger&#xa0;&#xa0;&#xa0;<span class="tag"><span class="interestRate">interestRate</span>&#xa0;<span class="measurement">measurement</span></span></h2>
<div class="outline-text-2" id="text-interest--not-principal--is-the-real-danger">
<p>
Most teams think about technical debt in terms of <i>principal</i> &#x2013; "how hard would it be to fix this?"  They estimate the cost of a refactor, put it on the backlog, and let it sit there because it never outranks the next feature.
</p>

<p>
But the principal is static.  The <i>interest</i> is what kills you.  And interest compounds.
</p>
</div>
<div id="outline-container-measuring-interest--without-fake-precision-" class="outline-3">
<h3 id="measuring-interest--without-fake-precision-"><span class="section-number-3">5.1.</span> Measuring Interest (Without Fake Precision)</h3>
<div class="outline-text-3" id="text-measuring-interest--without-fake-precision-">
<p>
Avoid metrics theater.  Lines of code, cyclomatic complexity, and lint scores are <i>proxies</i> at best and <i>noise</i> at worst.  The following signals are more useful for measuring the interest on your technical debt:
</p>

<ul class="org-ul">
<li><b>Change amplification</b> measures how many files or systems must be touched per feature.  Track PR size and the number of changed files over time.  If the trend is up, interest is compounding.</li>

<li><b>Time-to-confidence</b> measures how long before an engineer trusts that a change won't break something.  Measure the gap from "code complete" to "merged with confidence."  Long gaps mean high friction.</li>

<li><b>Error surface area</b> measures the number of ways a change can fail.  Count unique failure modes in CI/CD and staging.  The more ways things break, the higher the interest.</li>

<li><b>Onboarding half-life</b> measures how long it takes a new engineer to become productive.  Track days-to-first-meaningful-PR per new hire.  If it's getting longer, your codebase is getting harder to understand.</li>

<li><b>Fear factor</b> measures which systems nobody wants to touch.  Survey your engineers: which parts of the codebase do they avoid?  Fear is a leading indicator of high-interest debt.</li>

<li><b>Rework rate</b> measures code that is added then quickly modified or deleted.  This is <a href="https://dora.dev/">DORA's</a> new fifth metric &#x2013; now officially tracked<sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.</li>
</ul>

<p>
These signals map directly to <i>interest</i>, not principal.  A system with high change amplification and a high fear factor is accruing massive interest, regardless of how "easy" the fix would be.
</p>
</div>
</div>
<div id="outline-container-the-compounding-effect" class="outline-3">
<h3 id="the-compounding-effect"><span class="section-number-3">5.2.</span> The Compounding Effect</h3>
<div class="outline-text-3" id="text-the-compounding-effect">
<p>
Interest on technical debt compounds in ways that are genuinely analogous to financial compound interest.  Consider a module with a 10% "tax" &#x2013; every feature touching this module takes 10% longer than it should.
</p>

<blockquote class="pull-quote pull-right">
<p>
Hacking around debt like trying to kill a mythological hydra &#x2013; the effort is herculean, but the problem only grows with each swing of the engineer's sword.  
</p>
</blockquote>

<p>
In isolation, 10% sounds manageable.  But compound it:
</p>

<ul class="org-ul">
<li>Feature A takes 10% longer because of the debt</li>
<li>Feature B <i>also</i> touches the module, and <i>also</i> adds workarounds, slightly increasing the friction</li>
<li>Feature C touches the module and now takes 15% longer because of the accumulated workarounds from A and B</li>
<li>Feature D's developer, frustrated by the mess, copies the module instead of extending it &#x2013; creating a new debt instrument</li>
<li>Features E through Z now have <i>two</i> versions of the module to maintain</li>
</ul>


<p>
This is how a 10% tax becomes a 50% tax in 18 months.  I've seen this exact pattern play out at multiple companies.  Hacking around debt like trying to kill a mythological hydra &#x2013; the effort is herculean, but the problem only grows with each swing of the engineer's sword.  If the banner image of this blog post reminds you of a terrifying hydra, then I've accomplished my goal of being illustrious.
</p>



<div id="tech_debt_compounding" class="plotly-plot"></div>

<p>
The chart above models three scenarios over five years.  The red line shows what happens when debt is left unmanaged &#x2013; a 10% velocity tax compounds into a 50%+ tax within a few years.  The yellow line shows moderate, consistent debt service.  The green line shows aggressive front-loaded paydown that levels off once the worst debt is addressed.
</p>

<p>
The takeaway: <b>the earlier you start servicing debt, the cheaper it is</b>.  Just like a mortgage.
</p>
</div>
</div>
<div id="outline-container-lehman-s-laws--physics-agrees-with-finance" class="outline-3">
<h3 id="lehman-s-laws--physics-agrees-with-finance"><span class="section-number-3">5.3.</span> Lehman's Laws: Physics Agrees with Finance</h3>
<div class="outline-text-3" id="text-lehman-s-laws--physics-agrees-with-finance">
<p>
The compounding effect isn't just an analogy &#x2013; it's a law of software evolution.
</p>

<p>
<a href="https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution">Lehman's laws of software evolution</a>, established empirically in the 1970s and validated repeatedly since, state that<sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup>:
</p>

<ol class="org-ol">
<li><b>Continuing Change</b>: A system must be continually adapted or it becomes progressively less satisfactory</li>
<li><b>Increasing Complexity</b>: As a system evolves, its complexity increases <i>unless work is explicitly done to reduce it</i></li>
<li><b>Self Regulation</b>: System evolution is a self-regulating process with statistically determinable trends</li>
</ol>

<p>
Law #2 is the kicker.  Complexity doesn't increase because engineers are careless.  It increases because <i>that's what happens to systems that interact with the real world</i>.  Entropy is the default.  Order requires energy.  This is as true in software as it is in thermodynamics.
</p>

<blockquote class="pull-quote pull-right">
<p>
Entropy is the default.  Order requires energy.
</p>
</blockquote>


<p>
Technical debt, in this framing, is <i>unreduced complexity</i> &#x2013; the delta between the system's current complexity and the minimum complexity needed to serve its current purpose.  Interest is the ongoing cost of carrying that excess complexity.  And Lehman tells us that without deliberate effort, that delta <i>only grows</i>.
</p>
</div>
</div>
</div>
<div id="outline-container-management-strategies-by-company-stage" class="outline-2">
<h2 id="management-strategies-by-company-stage"><span class="section-number-2">6.</span> Management Strategies by Company Stage&#xa0;&#xa0;&#xa0;<span class="tag"><span class="strategy">strategy</span>&#xa0;<span class="organizational">organizational</span></span></h2>
<div class="outline-text-2" id="text-management-strategies-by-company-stage">
<p>
So the debt compounds.  The interest accrues.  Lehman's second law tells us complexity grows by default.  The natural question is: <i>what do you actually do about it?</i>
</p>

<p>
The right approach depends heavily on context.  A strategy that's right for a 500-person engineering org can be lethal for a 5-person startup, and vice versa.
</p>
</div>
<div id="outline-container-small-teams-and-startups--2-20-engineers-" class="outline-3">
<h3 id="small-teams-and-startups--2-20-engineers-"><span class="section-number-3">6.1.</span> Small Teams and Startups (2-20 engineers)&#xa0;&#xa0;&#xa0;<span class="tag"><span class="startups">startups</span></span></h3>
<div class="outline-text-3" id="text-small-teams-and-startups--2-20-engineers-">
</div>
<div id="outline-container-principles" class="outline-4">
<h4 id="principles"><span class="section-number-4">6.1.1.</span> Principles</h4>
<div class="outline-text-4" id="text-principles">
<ul class="org-ul">
<li><b>Accept higher interest rates</b> &#x2013; your cost of capital is survival, not elegance</li>
<li><b>Avoid irreversible debt</b> &#x2013; you can tolerate messy internals, but not locked-in architectural commitments</li>
<li><b>Prefer debt that can be deleted</b> &#x2013; the best refactor is <code>rm -rf</code></li>
</ul>
</div>
</div>
<div id="outline-container-tactics" class="outline-4">
<h4 id="tactics"><span class="section-number-4">6.1.2.</span> Tactics</h4>
<div class="outline-text-4" id="text-tactics">
<ol class="org-ol">
<li><b>Build throwaway-by-design systems.</b>  If you acknowledge that v1 exists to find product-market fit, not to be your production architecture for the next decade, you can make fundamentally different design decisions.  Use the simplest thing that works.  Monolith over microservices.  A single database over sharded clusters.  Flat files over message queues.</li>

<li><b>Keep boundaries clean even if internals are messy.</b>  The API contract between services matters.  The internal implementation of each service does not &#x2013; yet.  This is the "clean interfaces, dirty internals" pattern, and it's the best debt strategy for startups because it preserves optionality while allowing speed.</li>

<li><b>Track known debt explicitly.</b>  A <code>TECH_DEBT.md</code> in the repo root with a list of known shortcuts is worth more than a Jira board nobody checks.  Each entry should note: what the shortcut is, why it was taken, and what would trigger the need to fix it.</li>

<li><b>Favor deletion over refactoring.</b>  The best thing about startup code is that most of it can be thrown away.  When a module becomes too debt-heavy, ask: "Would it be faster to rewrite this from scratch, now that we understand the problem?"  At small scale, the answer is usually yes.</li>
</ol>
</div>
</div>
<div id="outline-container-what-to-avoid" class="outline-4">
<h4 id="what-to-avoid"><span class="section-number-4">6.1.3.</span> What to avoid</h4>
<div class="outline-text-4" id="text-what-to-avoid">
<ul class="org-ul">
<li>Don't invest in elaborate "tech debt tracking" tools &#x2013; the overhead exceeds the benefit at this scale</li>
<li>Don't over-abstract "for the future" &#x2013; you don't know what the future looks like yet</li>
<li>Don't take on <i>irreversible</i> debt (choosing the wrong database, the wrong cloud, the wrong primary language) just to move fast</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-growth-stage-companies--20-200-engineers-" class="outline-3">
<h3 id="growth-stage-companies--20-200-engineers-"><span class="section-number-3">6.2.</span> Growth-Stage Companies (20-200 engineers)&#xa0;&#xa0;&#xa0;<span class="tag"><span class="growth">growth</span>&#xa0;<span class="scaleups">scaleups</span></span></h3>
<div class="outline-text-3" id="text-growth-stage-companies--20-200-engineers-">
<p>
This is where debt management gets serious, and where I've seen the most damage done.  At one growth-stage company I worked with, the engineering team tripled in a year.  The codebase they inherited from the startup phase was a classic "clean interfaces, dirty internals" system &#x2013; which had been fine at 8 engineers.  But at 30, the dirty internals became a coordination bottleneck.  Three teams would independently hack around the same internal limitation in three different ways, creating three new debt instruments per quarter.  The startup-era debt didn't grow linearly with headcount &#x2013; it grew <i>quadratically</i>, because every new engineer was another node in the collision graph.
</p>

<p>
You're past the point where everyone knows everything, but not yet at the scale where platform teams and formal governance make sense.  This is the most dangerous stage.
</p>
</div>
<div id="outline-container-principles" class="outline-4">
<h4 id="principles"><span class="section-number-4">6.2.1.</span> Principles</h4>
<div class="outline-text-4" id="text-principles">
<ul class="org-ul">
<li><b>Start budgeting for debt service</b> &#x2013; treat it like a line item, not an afterthought</li>
<li><b>Avoid compounding interest</b> &#x2013; stop building new features on top of known-debt foundations</li>
<li><b>Make debt visible</b> &#x2013; if leadership can't see it, they can't prioritize it</li>
</ul>
</div>
</div>
<div id="outline-container-tactics" class="outline-4">
<h4 id="tactics"><span class="section-number-4">6.2.2.</span> Tactics</h4>
<div class="outline-text-4" id="text-tactics">
<ol class="org-ol">
<li><p>
<b>Allocate fixed capacity for debt service.</b>  15-25% of engineering capacity, every sprint, non-negotiable.  This is not a "20% time" passion project.  This is <i>interest payments</i>.  McKinsey's research found that organizations with systematic debt management freed up engineers to spend up to 50% more time on value-generating work<sup><a id="fnr.5" class="footref" href="https://www.chiply.dev/#fn.5" role="doc-backlink">5</a></sup>.
</p>

<div id="tech_debt_capacity_pie" class="plotly-plot"></div></li>

<li><b>Attach debt remediation to feature work.</b>  The most efficient way to pay down debt is to do it <i>when you're already in the code</i>.  If Feature X touches the auth module, the sprint plan includes both Feature X and "improve auth module test coverage."  This is the "boy scout rule" formalized: leave the campground cleaner than you found it.</li>

<li><b>Adopt Architecture Decision Records (ADRs).</b>  When you make a deliberate debt decision, write it down.  An ADR should capture: the context, the decision, the consequences (including known debt), and the conditions under which the decision should be revisited.  ADRs are your institutional memory for <i>why</i> the code looks the way it does<sup><a id="fnr.6" class="footref" href="https://www.chiply.dev/#fn.6" role="doc-backlink">6</a></sup>.</li>

<li><b>Establish ownership for core systems.</b>  Every high-interest system should have an owner &#x2013; a team or an individual responsible for its health.  Unowned systems accumulate debt fastest because nobody feels the pain enough to prioritize fixing them.</li>
</ol>
</div>
</div>
</div>
<div id="outline-container-large-organizations--200--engineers-" class="outline-3">
<h3 id="large-organizations--200--engineers-"><span class="section-number-3">6.3.</span> Large Organizations (200+ engineers)&#xa0;&#xa0;&#xa0;<span class="tag"><span class="enterprise">enterprise</span>&#xa0;<span class="platform">platform</span></span></h3>
<div class="outline-text-3" id="text-large-organizations--200--engineers-">
<p>
At enterprise scale, technical debt becomes systemic risk.  Individual modules matter less than <i>system-level</i> friction: API boundaries, deployment pipelines, shared libraries, and the organizational structures that produce them.
</p>
</div>
<div id="outline-container-principles" class="outline-4">
<h4 id="principles"><span class="section-number-4">6.3.1.</span> Principles</h4>
<div class="outline-text-4" id="text-principles">
<ul class="org-ul">
<li><b>Debt becomes systemic risk</b> &#x2013; a single high-interest module can bottleneck the entire organization</li>
<li><b>Coordination cost &gt; code quality</b> &#x2013; the overhead of keeping 50 teams aligned often exceeds the cost of the debt itself</li>
<li><b>Debt hides in interfaces</b> &#x2013; the most expensive debt lives at system boundaries, not inside modules</li>
</ul>
</div>
</div>
<div id="outline-container-tactics" class="outline-4">
<h4 id="tactics"><span class="section-number-4">6.3.2.</span> Tactics</h4>
<div class="outline-text-4" id="text-tactics">
<ol class="org-ol">
<li><b>Platform teams as "internal lenders."</b>  Platform engineering teams function like a bank: they provide well-maintained, low-friction infrastructure that product teams "borrow" from.  In exchange, product teams agree to use the platform's APIs and conventions, keeping the system-level architecture coherent.  This is the <a href="https://teamtopologies.com/">Team Topologies</a> model in debt terms.</li>

<li><b>Enforce contracts at boundaries.</b>  At scale, the cost of drift between teams dwarfs the cost of individual code quality.  Schema languages, API contracts, integration tests at boundaries, and automated compatibility checks are all forms of <i>debt prevention</i> at the interface level<sup><a id="fnr.7" class="footref" href="https://www.chiply.dev/#fn.7" role="doc-backlink">7</a></sup>.</li>

<li><b>Track change amplification across organizational boundaries.</b>  When a change in Team A's service requires changes in Teams B, C, and D, that's a debt signal.  The organizational structure is creating friction, and per <a href="https://en.wikipedia.org/wiki/Conway%27s_law">Conway's Law</a><sup><a id="fnr.8" class="footref" href="https://www.chiply.dev/#fn.8" role="doc-backlink">8</a></sup>, the software architecture will mirror (and reinforce) that friction.</li>

<li><b>Plan refinances, not heroic rewrites.</b>  The "big rewrite" is the engineering equivalent of declaring bankruptcy.  Sometimes it's necessary, but usually it's better to <i>refinance</i> &#x2013; systematically reducing the interest rate through incremental migration.  The <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">Strangler Fig pattern</a><sup><a id="fnr.9" class="footref" href="https://www.chiply.dev/#fn.9" role="doc-backlink">9</a></sup> is the canonical approach: build the new system alongside the old, redirect traffic incrementally, and let the old system wither.</li>
</ol>
</div>
</div>
</div>
<div id="outline-container-capacity-allocation-by-stage" class="outline-3">
<h3 id="capacity-allocation-by-stage"><span class="section-number-3">6.4.</span> Capacity Allocation by Stage</h3>
<div class="outline-text-3" id="text-capacity-allocation-by-stage">
<div id="tech_debt_capacity_allocation" class="plotly-plot"></div>

<p>
Notice how the allocation to debt service <i>increases</i> with company size.  This is counterintuitive &#x2013; you'd think larger companies with more resources could afford <i>less</i> proportional debt spending.  But larger companies have more accumulated debt, more system boundaries where debt hides, and higher coordination costs.  The 5% a startup spends is "fix it when you see it."  The 25% an enterprise spends is "dedicated teams, formal processes, migration projects."
</p>
</div>
</div>
</div>
<div id="outline-container-refactoring-as-refinancing--not-cleanup" class="outline-2">
<h2 id="refactoring-as-refinancing--not-cleanup"><span class="section-number-2">7.</span> Refactoring as Refinancing, Not Cleanup&#xa0;&#xa0;&#xa0;<span class="tag"><span class="refactoring">refactoring</span></span></h2>
<div class="outline-text-2" id="text-refactoring-as-refinancing--not-cleanup">
<p>
One of the most powerful reframings in this entire financial analogy:
</p>

<p>
<b>Refactoring is not "paying off" debt.  It's refinancing it.</b>
</p>

<p>
When you refinance a mortgage, you don't eliminate the debt.  You replace one debt instrument with another that has better terms &#x2013; lower interest rate, more predictable payments, better aligned with your current financial situation.
</p>

<p>
Good refactoring does the same thing:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Financial Refinancing</th>
<th scope="col" class="org-left">Technical Refactoring</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Lowers interest rate</td>
<td class="org-left">Reduces ongoing friction and maintenance cost</td>
</tr>

<tr>
<td class="org-left">Improves optionality</td>
<td class="org-left">Makes future changes easier and less risky</td>
</tr>

<tr>
<td class="org-left">Preserves cash flow</td>
<td class="org-left">Doesn't require a full stop to feature development</td>
</tr>

<tr>
<td class="org-left">May increase principal temporarily</td>
<td class="org-left">The refactored code might be larger or more complex initially</td>
</tr>

<tr>
<td class="org-left">Reduces monthly payments</td>
<td class="org-left">Each feature touching the refactored area takes less time</td>
</tr>
</tbody>
</table>

<p>
This reframing matters because it changes the conversation with stakeholders.  "We need to stop feature work for 3 months to clean up the code" is a hard sell.  "We're refinancing &#x2013; we'll reduce our ongoing maintenance costs by 30%, freeing up 2 engineers' worth of capacity for new features" is a business case.
</p>
</div>
<div id="outline-container-good-refinancing-vs--bad-refinancing" class="outline-3">
<h3 id="good-refinancing-vs--bad-refinancing"><span class="section-number-3">7.1.</span> Good Refinancing vs. Bad Refinancing</h3>
<div class="outline-text-3" id="text-good-refinancing-vs--bad-refinancing">
</div>
<div id="outline-container-good-refinancing" class="outline-4">
<h4 id="good-refinancing"><span class="section-number-4">7.1.1.</span> Good refinancing</h4>
<div class="outline-text-4" id="text-good-refinancing">
<ul class="org-ul">
<li><b>Incremental refactors</b> attached to feature work &#x2013; lower interest rate without stopping feature delivery</li>
<li><b>The Strangler Fig pattern</b> for large systems &#x2013; build the replacement alongside the original, migrate traffic gradually, decommission the old</li>
<li><b>Parallel systems with measured migration</b> &#x2013; run old and new simultaneously, compare results, switch over when confidence is high</li>
</ul>
</div>
</div>
<div id="outline-container-bad-refinancing" class="outline-4">
<h4 id="bad-refinancing"><span class="section-number-4">7.1.2.</span> Bad refinancing</h4>
<div class="outline-text-4" id="text-bad-refinancing">
<ul class="org-ul">
<li><b>Big-bang rewrites</b> &#x2013; the engineering equivalent of a <a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/">Netscape-style full rewrite</a><sup><a id="fnr.10" class="footref" href="https://www.chiply.dev/#fn.10" role="doc-backlink">10</a></sup>.  You lose domain knowledge, you lose battle-tested edge case handling, and you introduce new bugs.  The principal might drop, but the risk premium spikes.</li>
<li><b>Refactoring with no measurable improvement</b> &#x2013; if you can't articulate <i>what metric improves</i> after the refactor, you're not refinancing; you're rearranging furniture</li>
<li><b>Cosmetic refactors</b> (renaming, reformatting) disguised as debt paydown &#x2013; this is the equivalent of refinancing a 5% mortgage to a 4.9% mortgage: technically better, practically irrelevant</li>
</ul>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #6052cf;">graph</span> <span style="color: #065fff;">TB</span>
    <span style="color: #6052cf;">subgraph</span> <span style="color: #4250ef;">"Refactoring Decision Framework"</span>
        direction <span style="color: #065fff;">TB</span>
        START<span style="color: #008858;">[</span><span style="color: #4250ef;">"Proposed Refactor"</span><span style="color: #008858;">]</span>
        Q1<span style="color: #008858;">{</span><span style="color: #4250ef;">"Does this reduce&lt;br/&gt;measurable friction?"</span><span style="color: #008858;">}</span>
        Q2<span style="color: #008858;">{</span><span style="color: #4250ef;">"Can it be done&lt;br/&gt;incrementally?"</span><span style="color: #008858;">}</span>
        Q3<span style="color: #008858;">{</span><span style="color: #4250ef;">"Does it preserve&lt;br/&gt;existing functionality?"</span><span style="color: #008858;">}</span>
        Q4<span style="color: #008858;">{</span><span style="color: #4250ef;">"Does the team have&lt;br/&gt;sufficient context?"</span><span style="color: #008858;">}</span>

        GOOD<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#9989; Good Refinancing&lt;br/&gt;Proceed with measurement"</span><span style="color: #008858;">]</span>
        MAYBE<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#9888;&#65039; Consider Strangler Fig&lt;br/&gt;Build alongside, not instead"</span><span style="color: #008858;">]</span>
        RISKY<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#9940; High Risk&lt;br/&gt;Document a migration plan first"</span><span style="color: #008858;">]</span>
        NOPE<span style="color: #008858;">[</span><span style="color: #4250ef;">"&#10060; Not a Refactor&lt;br/&gt;It's a rewrite &#8212; treat it as one"</span><span style="color: #008858;">]</span>

        START <span style="color: #cf25aa;">--&gt;</span> Q1
        Q1 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes"</span>| Q2
        Q1 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No"</span>| NOPE
        Q2 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes"</span>| Q3
        Q2 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No"</span>| MAYBE
        Q3 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes"</span>| Q4
        Q3 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No"</span>| RISKY
        Q4 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"Yes"</span>| GOOD
        Q4 <span style="color: #cf25aa;">--&gt;</span>|<span style="color: #4250ef;">"No"</span>| RISKY
    <span style="color: #6052cf;">end</span>
</pre>
</div>
</div>
</div>
</div>
</div>
<div id="outline-container-ai-generated-code--the-new-debt-accelerator" class="outline-2">
<h2 id="ai-generated-code--the-new-debt-accelerator"><span class="section-number-2">8.</span> AI-Generated Code: The New Debt Accelerator&#xa0;&#xa0;&#xa0;<span class="tag"><span class="artificialIntelligence">artificialIntelligence</span>&#xa0;<span class="codeQuality">codeQuality</span></span></h2>
<div class="outline-text-2" id="text-ai-generated-code--the-new-debt-accelerator">
<p>
No discussion of technical debt in 2026 is complete without addressing the elephant in the IDE: AI code assistants.
</p>

<p>
GitHub Copilot, Claude, and their cousins have fundamentally changed the economics of code production.  Writing code is now cheaper and faster than ever.  But cheaper production doesn't mean cheaper <i>ownership</i> &#x2013; and this is where the financial analogy becomes urgent.
</p>
</div>
<div id="outline-container-the-data" class="outline-3">
<h3 id="the-data"><span class="section-number-3">8.1.</span> The Data</h3>
<div class="outline-text-3" id="text-the-data">
<p>
GitClear's <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">2025 research</a> on AI-assisted code quality found<sup><a id="fnr.11" class="footref" href="https://www.chiply.dev/#fn.11" role="doc-backlink">11</a></sup>:
</p>

<ul class="org-ul">
<li><b>Code duplication</b> (cloned blocks of 5+ lines) increased 8-fold between 2022 and 2024</li>
<li><b>Refactoring activity</b> dropped from 25% of changed lines in 2021 to less than 10% in 2024</li>
<li><b>Code churn</b> (code added then quickly modified or deleted) rose steadily, reaching an estimated ~7% by 2025</li>
</ul>

<p>
Google's <a href="https://dora.dev/">2024 DORA report</a> corroborates: teams with higher AI adoption reported faster code review and improved documentation, but the same cohort showed a measurable <i>decrease</i> in delivery stability<sup><a id="fnr.3.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.
</p>

<p>
In financial terms: <b>AI assistants have massively increased the rate at which teams can issue new debt, while simultaneously reducing the rate at which existing debt gets serviced.</b>
</p>
</div>
</div>
<div id="outline-container-why-ai-amplifies-debt" class="outline-3">
<h3 id="why-ai-amplifies-debt"><span class="section-number-3">8.2.</span> Why AI Amplifies Debt</h3>
<div class="outline-text-3" id="text-why-ai-amplifies-debt">
<p>
The mechanism is straightforward:
</p>

<ol class="org-ol">
<li>AI makes it trivially easy to <i>add</i> code &#x2013; reducing the perceived cost of new functionality</li>
<li>AI makes it no easier to <i>understand</i> existing code &#x2013; the cognitive overhead of working in a complex codebase hasn't changed</li>
<li>AI-generated code tends toward the generic and duplicative &#x2013; it doesn't know your architecture, your conventions, or your existing abstractions</li>
<li>Developers using AI skip the "understand before modifying" step more frequently &#x2013; the code is generated faster than understanding develops</li>
</ol>

<p>
This creates a specific new type of debt that deserves its own name: <b>comprehension debt</b>.
</p>

<blockquote class="pull-quote pull-right">
<p>
AI assistants have massively increased the rate at which teams can issue new debt, while simultaneously reducing the rate at which existing debt gets serviced.
</p>
</blockquote>

<p>
Comprehension debt is the gap between the size of a codebase and any individual engineer's ability to hold a working mental model of it.  Traditional technical debt is about code that's <i>wrong</i> &#x2013; suboptimal designs that create friction.  Comprehension debt is about code that might be individually <i>correct</i> but is collectively <i>unintelligible</i>.  Each AI-generated function may work perfectly, but when 40% of the codebase was written by an LLM that doesn't know your team's conventions, the overall architecture drifts toward incoherence &#x2013; ten different patterns for error handling, six ways to call the same API, three duplicate utility functions that almost-but-do-not-quite do the same thing.
</p>


<p>
The insidious property of comprehension debt is that it can't be addressed by traditional refactoring.  You can't "fix" a codebase that nobody can hold in their head.  The debt isn't in any individual module &#x2013; it's in the <i>aggregate</i>. The principal doesn't show up in any static analysis tool, and the interest manifests as slower onboarding, more bugs from unexpected interactions, and a growing sense among senior engineers that "nobody really knows how this system works anymore."
</p>
</div>
</div>
<div id="outline-container-mitigations" class="outline-3">
<h3 id="mitigations"><span class="section-number-3">8.3.</span> Mitigations</h3>
<div class="outline-text-3" id="text-mitigations">
<ul class="org-ul">
<li><b>Treat AI-generated code as a PR from a junior engineer</b>: review it with the same rigor, question design decisions, check for duplication against existing code.  I may change my tune as AI code review tools improve, but I would also resist the urge to have something like CodeRabbit do your PR reviews, especially since this would fail to narrow the comprehension gap</li>
<li><b>Maintain strong architectural guard rails</b>: linters, architecture tests (like ArchUnit), import restrictions, and pre-commit hooks that prevent the most common AI-introduced anti-patterns</li>
<li><b>Increase refactoring capacity proportionally</b>: if AI is helping you produce code 2x faster, you need 2x the refactoring capacity to prevent the debt ratio from spiking</li>
<li><b>Track AI-specific quality metrics</b>: code duplication rate, churn rate, and the ratio of net-new code to refactored code</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-the-organizational-failure-mode" class="outline-2">
<h2 id="the-organizational-failure-mode"><span class="section-number-2">9.</span> The Organizational Failure Mode&#xa0;&#xa0;&#xa0;<span class="tag"><span class="organizationalDebt">organizationalDebt</span>&#xa0;<span class="conwaysLaw">conwaysLaw</span></span></h2>
<div class="outline-text-2" id="text-the-organizational-failure-mode">
<p>
Here's the uncomfortable truth that most engineers don't want to hear:
</p>

<p>
<b>Technical debt is often a management and incentive problem, not a technical one.</b>
</p>
</div>
<div id="outline-container-conway-s-law-as-a-debt-generator" class="outline-3">
<h3 id="conway-s-law-as-a-debt-generator"><span class="section-number-3">9.1.</span> Conway's Law as a Debt Generator</h3>
<div class="outline-text-3" id="text-conway-s-law-as-a-debt-generator">
<p>
<a href="https://en.wikipedia.org/wiki/Conway%27s_law">Conway's Law</a> states that organizations design systems that mirror their own communication structures<sup><a id="fnr.8.8" class="footref" href="https://www.chiply.dev/#fn.8" role="doc-backlink">8</a></sup>.  The implication for technical debt is profound: <b>organizational friction becomes architectural friction becomes technical debt</b>.
</p>

<p>
If teams A and B can't communicate efficiently, the interface between their services will accrue debt &#x2013; duplicated logic, mismatched assumptions, undocumented contracts.  The debt isn't caused by poor engineering.  It's caused by poor organizational design.  And no amount of refactoring will fix it, because the structure that created the debt will recreate it.
</p>

<p>
The Inverse Conway Maneuver &#x2013; deliberately restructuring teams to match the desired architecture &#x2013; is the only way to address this class of debt.  It's also politically difficult, which is why this debt tends to persist.
</p>
</div>
</div>
<div id="outline-container-incentive-misalignment" class="outline-3">
<h3 id="incentive-misalignment"><span class="section-number-3">9.2.</span> Incentive Misalignment</h3>
<div class="outline-text-3" id="text-incentive-misalignment">
<p>
Technical debt grows fastest in organizations with these incentive patterns:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Incentive Pattern</th>
<th scope="col" class="org-left">Debt Effect</th>
<th scope="col" class="org-left">Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Rewarding only shipping</b></td>
<td class="org-left">Teams optimize for feature velocity, ignoring maintenance</td>
<td class="org-left">Include quality metrics in performance evaluation</td>
</tr>

<tr>
<td class="org-left"><b>No ownership past initial delivery</b></td>
<td class="org-left">Code is "thrown over the wall" &#x2013; nobody feels the interest</td>
<td class="org-left">"You build it, you own it" culture</td>
</tr>

<tr>
<td class="org-left"><b>No time allocated for maintenance</b></td>
<td class="org-left">Debt service gets crowded out by feature work</td>
<td class="org-left">Fixed capacity allocation (the 15-25% rule)</td>
</tr>

<tr>
<td class="org-left"><b>Architecture decisions without accountability</b></td>
<td class="org-left">Decision-makers don't bear the cost of their choices</td>
<td class="org-left">ADRs with named authors and review dates</td>
</tr>

<tr>
<td class="org-left"><b>Promotion criteria that ignore technical health</b></td>
<td class="org-left">Senior engineers chase visible impact, not sustainable systems</td>
<td class="org-left">"Infrastructure impact" as a promotion criterion</td>
</tr>
</tbody>
</table>

<p>
Tech debt grows fastest where engineers feel powerless to address it.  If your process doesn't allocate time for debt service, debt will accumulate.  This is not a technical problem.  It's a governance problem.
</p>

<p>
The most pernicious version of this: organizations that <i>say</i> they value quality but <i>measure</i> only velocity.  Engineers quickly learn which metric actually matters, and they optimize accordingly.
</p>
</div>
</div>
</div>
<div id="outline-container-a-framework-for-action" class="outline-2">
<h2 id="a-framework-for-action"><span class="section-number-2">10.</span> A Framework for Action&#xa0;&#xa0;&#xa0;<span class="tag"><span class="framework">framework</span>&#xa0;<span class="decisionMaking">decisionMaking</span></span></h2>
<div class="outline-text-2" id="text-a-framework-for-action">
<p>
Let's pull everything together into a practical framework.
</p>
</div>
<div id="outline-container-step-1--inventory-your-debt" class="outline-3">
<h3 id="step-1--inventory-your-debt"><span class="section-number-3">10.1.</span> Step 1: Inventory Your Debt</h3>
<div class="outline-text-3" id="text-step-1--inventory-your-debt">
<p>
You can't manage what you can't see.  Build a simple inventory:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Field</th>
<th scope="col" class="org-left">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Name</b></td>
<td class="org-left">Human-readable label (e.g., "Auth module monolith")</td>
</tr>

<tr>
<td class="org-left"><b>Type</b></td>
<td class="org-left">Low-interest / High-interest / Variable-rate / Hidden</td>
</tr>

<tr>
<td class="org-left"><b>Principal</b></td>
<td class="org-left">Estimated effort to remediate (T-shirt: S/M/L/XL)</td>
</tr>

<tr>
<td class="org-left"><b>Interest rate</b></td>
<td class="org-left">How often teams interact with this debt (Daily / Weekly / Monthly / Rarely)</td>
</tr>

<tr>
<td class="org-left"><b>Quadrant</b></td>
<td class="org-left">Fowler classification (Prudent+Deliberate, etc.)</td>
</tr>

<tr>
<td class="org-left"><b>Owner</b></td>
<td class="org-left">Team or individual responsible</td>
</tr>

<tr>
<td class="org-left"><b>Trigger</b></td>
<td class="org-left">Conditions under which this debt must be addressed</td>
</tr>
</tbody>
</table>

<p>
Don't over-engineer this.  A spreadsheet or a markdown file (or org-mode file if you're cool like me 🤪) in the repo is fine.  The point is <i>visibility</i>, not process.
</p>
</div>
</div>
<div id="outline-container-step-2--prioritize-by-interest-rate" class="outline-3">
<h3 id="step-2--prioritize-by-interest-rate"><span class="section-number-3">10.2.</span> Step 2: Prioritize by Interest Rate</h3>
<div class="outline-text-3" id="text-step-2--prioritize-by-interest-rate">
<p>
Sort your inventory by interest rate, not by principal.  The debt that every team interacts with daily costs more than the debt that one team encounters quarterly, even if the quarterly debt has a higher remediation cost.
</p>

<p>
The formula is simple:
</p>

<p>
\[
\text{Debt Priority Score} = \text{Interest Rate} \times \text{Number of Affected Teams} \times \text{Interaction Frequency}
\]
</p>

<p>
This isn't meant to be precise &#x2013; it's meant to force the right conversation.
</p>
</div>
</div>
<div id="outline-container-step-3--allocate-capacity" class="outline-3">
<h3 id="step-3--allocate-capacity"><span class="section-number-3">10.3.</span> Step 3: Allocate Capacity</h3>
<div class="outline-text-3" id="text-step-3--allocate-capacity">
<p>
Decide on a fixed percentage of engineering capacity for debt service.  This should be:
</p>

<ul class="org-ul">
<li><b>Non-negotiable</b>: it doesn't get borrowed for feature work when deadlines loom.  The moment debt service becomes "optional," it stops happening &#x2013; the same way savings stop when they're "whatever's left after spending."  Treat it like a payroll obligation, not a discretionary budget.</li>
<li><b>Visible</b>: leadership should see how the capacity is being used.  A monthly one-pager showing which debt was serviced, what metrics improved, and what's next builds institutional trust.  Over time, this evidence base is what lets you <i>increase</i> the allocation when it's needed.</li>
<li><b>Measured</b>: track whether debt service is actually reducing interest payments.  If you allocated 20% to debt service last quarter and velocity didn't improve, either you're paying down the wrong debt or the measurement window is too short.</li>
</ul>

<p>
In practice, I've seen this work best when the allocation is protected at the team level, not the org level.  Telling 50 teams "the org is allocating 20% to debt" is meaningless.  Telling each team "20% of <i>your</i> sprints go to debt service, and here's how we'll track the impact" creates accountability.
</p>
</div>
</div>
<div id="outline-container-step-4--execute-and-measure" class="outline-3">
<h3 id="step-4--execute-and-measure"><span class="section-number-3">10.4.</span> Step 4: Execute and Measure</h3>
<div class="outline-text-3" id="text-step-4--execute-and-measure">
<p>
For each debt paydown initiative, define:
</p>

<ol class="org-ol">
<li><b>What metric improves?</b> (Change amplification decreases, onboarding time drops, incident rate falls)</li>
<li><b>By how much?</b> (Even a rough target helps)</li>
<li><b>By when?</b> (Timeboxed &#x2013; if it's not showing results in 2-3 sprints, reassess)</li>
</ol>

<p>
Track these before and after.  This builds the institutional evidence base for continued debt service investment.
</p>

<p>
Here's a concrete example: suppose you're refactoring the authentication module.  Before starting, measure: average PR size for auth-touching features (say, 15 files), average time from "code complete" to "merged" (say, 4 days), and staging failure rate for auth changes (say, 25%).  Set targets: reduce PR size to &lt;8 files, merge time to &lt;2 days, staging failures to &lt;10%.  After two sprints of focused refactoring, measure again.  If the metrics moved, you have evidence.  If they didn't, you're either fixing the wrong thing or the refactor isn't done yet.
</p>
</div>
</div>
<div id="outline-container-step-5--prevent-new-high-interest-debt" class="outline-3">
<h3 id="step-5--prevent-new-high-interest-debt"><span class="section-number-3">10.5.</span> Step 5: Prevent New High-Interest Debt</h3>
<div class="outline-text-3" id="text-step-5--prevent-new-high-interest-debt">
<p>
Paydown without prevention is a treadmill.  Implement:
</p>

<ul class="org-ul">
<li><b>ADRs</b> for deliberate debt decisions &#x2013; every time the team knowingly takes on debt, write a one-page record: what shortcut was taken, why, what the expected interest rate is, and what conditions should trigger paydown.  This is the difference between a mortgage (documented, scheduled) and credit card debt (invisible, compounding).</li>
<li><b>Code review standards</b> that explicitly evaluate debt introduction &#x2013; add "does this PR introduce or increase technical debt?" as a standard review question.  If yes, require a comment explaining the tradeoff.</li>
<li><b>Architecture tests</b> that prevent structural regression &#x2013; tools like ArchUnit (Java), Dependency Cruiser (JavaScript), or even simple import-graph checks in CI can enforce "module A should not depend on module B" constraints.  These automated guardrails catch drift before it compounds.</li>
<li><b>Post-incident reviews</b> that trace root causes back to specific debt &#x2013; when an incident occurs, ask: "Was known technical debt a contributing factor?"  If the answer is yes three times for the same debt item, it gets promoted to the top of the priority list regardless of its planned schedule.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-conclusion--structure-it--don-t-eliminate-it" class="outline-2">
<h2 id="conclusion--structure-it--don-t-eliminate-it"><span class="section-number-2">11.</span> Conclusion: Structure It, Don't Eliminate It&#xa0;&#xa0;&#xa0;<span class="tag"><span class="synthesis">synthesis</span></span></h2>
<div class="outline-text-2" id="text-conclusion--structure-it--don-t-eliminate-it">
<p>
Let me end with the thesis I promised at the start:
</p>

<p>
<b>You don't eliminate technical debt.  You structure it.</b>
</p>

<p>
Every financial system carries debt.  Healthy companies have mortgages, lines of credit, and corporate bonds.  The difference between a healthy company and a bankrupt one isn't the <i>presence</i> of debt &#x2013; it's the <i>management</i> of it.  Healthy companies know their debt-to-equity ratio, their interest obligations, and their capacity to service existing debt before taking on more.
</p>

<p>
The same is true for codebases.
</p>

<p>
A healthy codebase has known, documented debt with managed interest rates, owned by specific teams, prioritized by business impact, and systematically paid down.  An unhealthy codebase has <i>the same amount of debt</i> but nobody can tell you what it is, how much it costs, or who's responsible.
</p>

<blockquote class="pull-quote pull-right">
<p>
Every system carries debt.  The question is who pays, when, and how predictably.
</p>
</blockquote>

<p>
The financial analogy isn't perfect.  No analogy is.  But it gives us a vocabulary &#x2013; principal, interest, refinancing, credit rating &#x2013; that makes the invisible visible and the abstract concrete.  It transforms "we have a lot of tech debt" from a vague complaint into a specific, actionable diagnosis: <i>what</i> is the debt, <i>what</i> is the interest rate, and <i>what</i> is our plan for servicing it?
</p>

<p>
Start there.  The rest follows.
</p>

<p>
Cunningham's original insight was that shipping code creates a gap between what you've built and what you now understand.  Thirty-four years later, the insight holds &#x2013; but the gap has widened.  We ship faster, understand later, and compound the difference daily.  The metaphor he gave us is still the best tool we have for closing it.  Use it precisely, and it will serve you well.
</p>
</div>
</div>
<div id="outline-container-socials" class="outline-2">
<h2 id="socials"><span class="section-number-2">12.</span> socials</h2>
<div class="outline-text-2" id="text-socials">
<ul class="org-ul">
<li><a href="https://www.reddit.com/r/softwarearchitecture/comments/1r7czgt/the_interest_rate_on_your_codebase_a_financial/">reddit: The Interest Rate on Your Codebase: A Financial Framework for Technical Debt</a></li>
<li><a href="https://news.ycombinator.com/item?id=47096213">hackernews: The Interest Rate on Your Codebase: A Financial Framework for Technical Debt</a></li>
<li><a href="https://www.linkedin.com/feed/update/urn:li:activity:7429582182194556928/">linkedin: The Interest Rate on Your Codebase: A Financial Framework for Technical Debt</a></li>
</ul>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">13.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
This post reclaims Ward Cunningham's original financial metaphor for technical debt and extends it into a comprehensive framework for managing code quality at any organizational scale.
</p>

<p>
The core argument begins with <a href="https://www.chiply.dev/#a-precise-definition">a precise definition</a>: technical debt is a deliberate or accidental deviation from an optimal design that yields short-term value at the cost of increased future work. Critically, <a href="https://www.chiply.dev/#what-technical-debt-is-not">not everything that frustrates engineers is debt</a>—bugs are liabilities, legacy systems are only debt if they impede change, and "bad code" is a style preference unless it causes measurable drag.
</p>

<p>
The post maps technical concepts to <a href="https://www.chiply.dev/#the-instrument-list">financial instruments</a> with precision: principal is remediation cost, interest is ongoing friction, and the interest rate is interaction frequency multiplied by system friction. This leads to a taxonomy of <a href="https://www.chiply.dev/#types-of-technical-debt---approx--types-of-financial-debt">debt types</a>—low-interest long-term debt (like a mortgage), high-interest revolving debt (like credit cards), variable-rate debt whose costs spike under changing conditions, and dangerous hidden debt that lurks off the balance sheet.
</p>

<p>
<a href="https://www.chiply.dev/#fowler-s-quadrant--why-teams-accumulate-debt">Martin Fowler's quadrant</a> classifies debt by whether it was deliberate or inadvertent, prudent or reckless—each requiring different responses. The post argues that <a href="https://www.chiply.dev/#why-debt-is-rational">debt is often rational</a>, especially for startups validating product-market fit or teams exploring uncertain requirements.
</p>

<p>
The central thesis emerges in <a href="https://www.chiply.dev/#interest--not-principal--is-the-real-danger">the section on interest</a>: the real danger isn't the debt itself but the compounding interest. The post provides <a href="https://www.chiply.dev/#measuring-interest--without-fake-precision-">practical signals for measuring interest</a>—change amplification, time-to-confidence, fear factor, and rework rate—and illustrates <a href="https://www.chiply.dev/#the-compounding-effect">how a 10% velocity tax can become 50% in 18 months</a> through compound effects that Lehman's Laws predict are inevitable without deliberate countermeasures.
</p>

<p>
<a href="https://www.chiply.dev/#management-strategies-by-company-stage">Management strategies vary by company stage</a>: startups should accept higher interest rates while avoiding irreversible debt, growth-stage companies must allocate 15-25% of capacity to debt service, and enterprises need platform teams functioning as internal lenders with enforced contracts at system boundaries.
</p>

<p>
The post reframes <a href="https://www.chiply.dev/#refactoring-as-refinancing--not-cleanup">refactoring as refinancing rather than cleanup</a>—replacing one debt instrument with another that has better terms—and distinguishes good refinancing (incremental, measurable) from bad (big-bang rewrites, cosmetic changes).
</p>

<p>
A timely section addresses <a href="https://www.chiply.dev/#ai-generated-code--the-new-debt-accelerator">AI-generated code as a new debt accelerator</a>, introducing the concept of "comprehension debt"—the gap between codebase size and any engineer's ability to maintain a working mental model. The data shows AI tools have increased code production while reducing refactoring activity, creating a dangerous imbalance.
</p>

<p>
<a href="https://www.chiply.dev/#the-organizational-failure-mode">Organizational factors</a> often matter more than technical ones: Conway's Law means organizational friction becomes architectural friction becomes debt, while <a href="https://www.chiply.dev/#incentive-misalignment">incentive misalignment</a> (rewarding only shipping, no ownership past delivery) accelerates accumulation.
</p>

<p>
The post concludes with <a href="https://www.chiply.dev/#a-framework-for-action">a five-step framework</a>: inventory your debt, prioritize by interest rate rather than principal, allocate fixed non-negotiable capacity for debt service, execute with measurable targets, and prevent new high-interest debt through ADRs and architecture tests. The final message: you don't eliminate technical debt, you structure it—just as healthy companies carry managed debt with known obligations and clear ownership.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Ward Cunningham first described the debt metaphor in his 1992 OOPSLA experience report, "The WyCash Portfolio Management System."  He later clarified in a <a href="https://wiki.c2.com/?WardExplainedDebt">2009 video</a> that the metaphor was specifically about the gap between the team's evolving understanding and the code's current design &#x2013; not about deliberately writing poor code.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Martin Fowler's <a href="https://martinfowler.com/bliki/TechnicalDebtQuadrant.html">Technical Debt Quadrant</a> (2009) built on Cunningham's original metaphor by distinguishing between deliberate/inadvertent and prudent/reckless debt.  This taxonomy is now widely used in industry.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Google's DORA (DevOps Research and Assessment) program has tracked software delivery performance for over a decade.  The 2024 State of DevOps report introduced rework rate as a fifth key metric and found mixed results from AI adoption: improved throughput but decreased stability.  See: <a href="https://dora.dev/">dora.dev</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Lehman's laws of software evolution were first proposed by Meir M. Lehman in 1974 and refined through the 1990s.  The laws describe empirically observed properties of "E-type" systems (software that solves real-world problems).  The key insight &#x2013; that complexity increases unless explicitly counteracted &#x2013; has been validated across decades of research.  See: <a href="https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution">Wikipedia: Lehman's Laws</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.5" class="footnum" href="https://www.chiply.dev/#fnr.5" role="doc-backlink">5</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
McKinsey's 2023 report "<a href="https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/breaking-technical-debts-vicious-cycle-to-modernize-your-business">Breaking technical debt's vicious cycle to modernize your business</a>" found that technical debt consumes a significant share of IT budgets &#x2013; CIOs surveyed reported that as much as 40% of their technology estate was composed of tech debt.  Organizations with systematic debt management freed engineers to spend up to 50% more time on value-generating work.  One CIO reported reducing the "debt tax" on engineer time from 75% to 25%.
</p></div></div>

<div class="footdef"><sup><a id="fn.6" class="footnum" href="https://www.chiply.dev/#fnr.6" role="doc-backlink">6</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Architecture Decision Records (ADRs) were popularized by Michael Nygard in a <a href="https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions">2011 blog post</a>.  The format is simple: Context, Decision, Consequences.  The key value is <i>recording the context</i> so future engineers understand <i>why</i> the decision was made, not just <i>what</i> it was.
</p></div></div>

<div class="footdef"><sup><a id="fn.7" class="footnum" href="https://www.chiply.dev/#fnr.7" role="doc-backlink">7</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
For a deep dive on schema languages as an interface-level debt prevention strategy, see my companion post: <a href="https://www.chiply.dev/post-schema-languages">The Schema Language Question: Avro, JSON Schema, Protobuf, and the Quest for a Single Source of Truth</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.8" class="footnum" href="https://www.chiply.dev/#fnr.8" role="doc-backlink">8</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Conway's Law, introduced by Melvin Conway in 1967, states that "organizations which design systems are constrained to produce designs which are copies of the communication structures of these organizations."  Research from MIT and Harvard Business School found "strong evidence to support the mirroring hypothesis."  See: <a href="https://en.wikipedia.org/wiki/Conway%27s_law">Wikipedia: Conway's Law</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.9" class="footnum" href="https://www.chiply.dev/#fnr.9" role="doc-backlink">9</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The Strangler Fig pattern was named by Martin Fowler in his <a href="https://martinfowler.com/bliki/StranglerFigApplication.html">2004 blog post</a>, inspired by the strangler fig trees he observed in Australia.  The pattern involves building a new system around the edges of the old, gradually routing traffic from old to new, until the old system can be decommissioned.
</p></div></div>

<div class="footdef"><sup><a id="fn.10" class="footnum" href="https://www.chiply.dev/#fnr.10" role="doc-backlink">10</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Joel Spolsky's <a href="https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/">"Things You Should Never Do, Part I"</a> (2000) argues that full rewrites are "the single worst strategic mistake that any software company can make," citing Netscape's decision to rewrite their browser from scratch as a cautionary tale.  The argument is that working code, however ugly, embodies years of accumulated knowledge about edge cases and failure modes that a rewrite will inevitably lose.
</p></div></div>

<div class="footdef"><sup><a id="fn.11" class="footnum" href="https://www.chiply.dev/#fnr.11" role="doc-backlink">11</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
GitClear's <a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research">"AI Copilot Code Quality: 2025 Data Suggests 4x Growth in Code Clones"</a> analyzed over 200 million lines of changed code to measure the impact of AI assistants on code quality metrics.  The findings suggest that AI tools are accelerating code production while reducing refactoring activity, leading to increased duplication and higher churn rates.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>space-tree: Tree-Based Workspace Management for Emacs</title>
      <link>https://www.chiply.dev/post-space-tree</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-space-tree</guid>
      <pubDate>Sun, 22 Feb 2026 18:20:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Modern UIs are over-engineered and excessive. This is easy to say in an age where people walk around with computers attached to their faces, only to get&amp;#x2026; a subpar user experience. It&apos;s also eas...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="workspaceManagement">workspaceManagement</span>&#xa0;<span class="tooling">tooling</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org5fe26e2" class="figure">
<p><img src="https://www.chiply.dev/images/space-tree-banner.jpeg" alt="space-tree-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Modern UIs are over-engineered and excessive.  This is easy to say in an age where people walk around with computers attached to their faces, only to get&#x2026; a subpar user experience.  It's also easy to say as a developer who uses Emacs, the best TUI I've ever used for people who primarily interface with text.
</p>
</div>
</div>
<div id="outline-container-the-workspace-problem" class="outline-2">
<h2 id="the-workspace-problem"><span class="section-number-2">2.</span> The Workspace Problem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="workspaceManagement">workspaceManagement</span></span></h2>
<div class="outline-text-2" id="text-the-workspace-problem">
<p>
I've recently seen hilarious demonstrations of people pinning AR screens around their houses in an overambitious attempt to organize their open applications.  AR headsets are an over-engineered solution to a problem that all PC users suffer from, and they solve it with the wrong tools.  Ocular tracking, accelerometers, gyroscopes, proprietary software&#x2026; all of this, when simple 2D screens are already extremely good at managing workspaces.
</p>

<blockquote class="pull-quote pull-right">
<p>
The math doesn't math, especially after you factor in the astronomical price tags of these AR devices.
</p>
</blockquote>

<p>
Here's the design smell: we want a <b>software</b> solution for <b>personal computing</b>, but AR brings in a <b>hardware</b> dependency that isn't even a <b>personal computer</b>.  The math doesn't math, especially after you factor in the astronomical price tags of these AR devices.  AR for workspace management is less of a real improvement and more of an indulgence in "shiny objects".
</p>

<p>
I like to shake my fist at over-engineering by creating something simple, so I built <a href="https://github.com/chiply/space-tree">space-tree</a>, a lightweight Emacs package that models workspaces as a tree rather than a flat or fixed-dimension list.  I'd been annoyed by the workspace situation in my Emacs config for a while.  What started as a few convenience functions in my init.el eventually grew into a proper package.  But before I get into what I built, it's worth looking at what already exists.
</p>
</div>
</div>
<div id="outline-container-existing-solutions-in-emacs" class="outline-2">
<h2 id="existing-solutions-in-emacs"><span class="section-number-2">3.</span> Existing Solutions in Emacs&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="comparison">comparison</span></span></h2>
<div class="outline-text-2" id="text-existing-solutions-in-emacs">
<p>
Emacs has several workspace managers.  <a href="https://github.com/wasamasa/eyebrowse">Eyebrowse</a> gives you a flat, numbered list of window configurations, basically virtual desktops.  <a href="https://github.com/nex3/perspective-el">perspective.el</a> and <a href="https://github.com/Bad-ptr/persp-mode.el">persp-mode</a> (separate packages despite the similar names) go further, scoping buffers to named workspaces with persistence across sessions.  These are well-designed tools, but in my experience they share a few limitations:
</p>
</div>
<div id="outline-container-flat-lists" class="outline-3">
<h3 id="flat-lists"><span class="section-number-3">3.1.</span> Flat Lists</h3>
<div class="outline-text-3" id="text-flat-lists">
<p>
Some workspace managers have too few dimensions.  Eyebrowse organizes workspaces in a flat 1-dimensional list.  With tools like tmux and Chrome, I've grown accustomed to organizing my workspaces into at least 2-dimensional layouts, so the flatness of eyebrowse is an issue for my brain.  Certain contexts (developing a suite of unit tests across multiple modules, for instance) demand more than 2 dimensions of organization.
</p>
</div>
</div>
<div id="outline-container-fixed-dimensions" class="outline-3">
<h3 id="fixed-dimensions"><span class="section-number-3">3.2.</span> Fixed Dimensions</h3>
<div class="outline-text-3" id="text-fixed-dimensions">
<p>
Even when a workspace manager has more than one dimension, the <i>number</i> of dimensions tends to be fixed.  <code>tab-bar-mode</code> is a row of tabs.  Always one row, always flat.  But I'm often working with contexts of mixed complexity.  A quick bug fix needs one workspace; a multi-file refactor with tests needs a whole subtree.  You end up either over-structuring simple tasks or under-structuring complex ones.
</p>
</div>
</div>
<div id="outline-container-naming-tax" class="outline-3">
<h3 id="naming-tax"><span class="section-number-3">3.3.</span> Naming Tax</h3>
<div class="outline-text-3" id="text-naming-tax">
<p>
Most workspace managers want a name and a location upfront.  persp-mode prompts you to name every workspace at creation time.  This is friction.  Sometimes I just want to branch off, explore something, and figure out later whether it deserves a name and a permanent place.  Or never.  Most of my workspaces don't need names.  They need to exist for twenty minutes and then disappear.
</p>

<p>
What I wanted was a workspace system with arbitrary depth, no mandatory naming, and enough flexibility to match the structure to the task, not the other way around.
</p>
</div>
</div>
</div>
<div id="outline-container-how-space-tree-works" class="outline-2">
<h2 id="how-space-tree-works"><span class="section-number-2">4.</span> How space-tree Works&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="dataStructures">dataStructures</span></span></h2>
<div class="outline-text-2" id="text-how-space-tree-works">
<p>
Workspaces form a <i>tree</i>.  Each node stores a window configuration (which buffers are displayed in which windows at a given moment).  You can branch off from any node to create child workspaces, and navigate the tree laterally between siblings or vertically between parent and child.
</p>

<pre class="example" id="org9ab7239">
1 ─── 1.1 ─── 1.1.1
 │         └── 1.1.2
 │
 └── 1.2 ─── 1.2.1

2 ─── 2.1
</pre>

<p>
This gets you:
</p>
<ul class="org-ul">
<li><b>Arbitrary depth.</b>  Nest workspaces as deep as the context demands.</li>
<li><b>Mixed complexity.</b>  A bug fix is a leaf node.  A complex investigation gets its own subtree with as many children as it needs.</li>
<li><b>No upfront naming.</b>  Create a space, start working, name it later.  Or don't.</li>
</ul>

<p>
Say I'm chasing a failing test.  I have my test runner open, but I need the test source alongside the implementation, <i>and</i> I want to cross-reference stack traces without losing any of those layouts.  With space-tree, I branch from my runner into a space for the test source, branch again for the implementation, and create a sibling for the logs.  Each space preserves its own window arrangement.  When I fix the bug, I jump back to the runner and all the investigative context is still there, organized the way I actually thought about the problem.
</p>
</div>
<div id="outline-container-where-space-tree-fits" class="outline-3">
<h3 id="where-space-tree-fits"><span class="section-number-3">4.1.</span> Where space-tree Fits in Emacs</h3>
<div class="outline-text-3" id="text-where-space-tree-fits">
<p>
space-tree doesn't replace anything.  If you already use perspective.el for buffer scoping, space-tree layers on top and manages window configurations independently.  It's also orthogonal to <code>tab-bar-mode</code>.  You can use tab-bar tabs and space-tree spaces in the same session without conflict.
</p>

<p>
Emacs already lets you snapshot the current arrangement of windows and buffers with <code>current-window-configuration</code>.  space-tree manages a tree of these snapshots and restores them when you switch spaces.  Spaces live in memory for the duration of your session.  They're not persisted across restarts.  I may add that later, but frankly I haven't missed it.  The tree is cheap to rebuild and my workflows change day to day anyway.
</p>
</div>
</div>
</div>
<div id="outline-container-key-interactions" class="outline-2">
<h2 id="key-interactions"><span class="section-number-2">5.</span> Key Interactions&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="demonstration">demonstration</span></span></h2>
<div class="outline-text-2" id="text-key-interactions">
<p>
Here's what it actually looks like in use.
</p>
</div>
<div id="outline-container-navigation-and-branching" class="outline-3">
<h3 id="navigation-and-branching"><span class="section-number-3">5.1.</span> Navigation and Branching</h3>
<div class="outline-text-3" id="text-navigation-and-branching">
<p>
Most of the time you're doing this: creating a sibling, branching into a child, moving laterally between siblings, jumping back up to the parent.
</p>

<img src="https://www.chiply.dev/demo3.gif" alt="Alternative text">
</div>
</div>
<div id="outline-container-naming-and-switching" class="outline-3">
<h3 id="naming-and-switching"><span class="section-number-3">5.2.</span> Naming and Switching</h3>
<div class="outline-text-3" id="text-naming-and-switching">
<p>
Spaces are unnamed by default, but you can name them at any point.  Once named, <code>space-tree-switch-space-by-name</code> lets you jump directly via completing-read.  This is useful when the tree gets deep and lateral navigation would be tedious.
</p>


<div id="orgcffcff4" class="figure">
<p><img src="https://www.chiply.dev/images/space-tree-switch-name.gif" alt="space-tree-switch-name.gif" />
</p>
<p><span class="figure-number">Figure 2: </span>Naming a space and switching to it via completing-read</p>
</div>
</div>
</div>
<div id="outline-container-reusing-layouts" class="outline-3">
<h3 id="reusing-layouts"><span class="section-number-3">5.3.</span> Reusing Layouts</h3>
<div class="outline-text-3" id="text-reusing-layouts">
<p>
Once you've set up a window layout you like, you can duplicate it elsewhere in the tree with copy and paste.  Useful when you want the same split arrangement for a different task.  And when you're bouncing between two active contexts, <code>space-tree-go-to-last-space</code> gives you a quick toggle, like alt-tab for your workspaces.
</p>


<div id="orgcde249d" class="figure">
<p><img src="https://www.chiply.dev/images/space-tree-copy-paste.gif" alt="space-tree-copy-paste.gif" />
</p>
<p><span class="figure-number">Figure 3: </span>Copying a workspace layout and pasting it into a new branch</p>
</div>


<div id="org0c4114e" class="figure">
<p><img src="https://www.chiply.dev/images/space-tree-toggle.gif" alt="space-tree-toggle.gif" />
</p>
<p><span class="figure-number">Figure 4: </span>Toggling between two active workspaces</p>
</div>
</div>
</div>
<div id="outline-container-cleanup-and-modeline" class="outline-3">
<h3 id="cleanup-and-modeline"><span class="section-number-3">5.4.</span> Cleanup and the Modeline</h3>
<div class="outline-text-3" id="text-cleanup-and-modeline">
<p>
When you're done with a branch of work, <code>space-tree-delete-space</code> prunes it from the tree.  The modeline indicator shows your current position at all times, so you always know where you are without thinking about it.
</p>


<div id="org901eb45" class="figure">
<p><img src="https://www.chiply.dev/images/space-tree-delete.gif" alt="space-tree-delete.gif" />
</p>
<p><span class="figure-number">Figure 5: </span>Deleting a space and observing the modeline indicator</p>
</div>
</div>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">6.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="emacs">emacs</span>&#xa0;<span class="installation">installation</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
space-tree is on <a href="https://github.com/chiply/space-tree">GitHub</a>.  Install with <code>straight.el</code> or manually, then call <code>(space-tree-init)</code>.  The README has the full command reference.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><span style="color: #008858;">(</span><span style="color: #6052cf;">use-package</span> space-tree
  <span style="color: #ba35af;">:straight</span> <span style="color: #4f54aa;">(</span>space-tree <span style="color: #ba35af;">:type</span> git <span style="color: #ba35af;">:host</span> github <span style="color: #ba35af;">:repo</span> <span style="color: #4250ef;">"chiply/space-tree"</span><span style="color: #4f54aa;">)</span>
  <span style="color: #ba35af;">:config</span>
  <span style="color: #4f54aa;">(</span>space-tree-init<span style="color: #4f54aa;">)</span><span style="color: #008858;">)</span>
</pre>
</div>

<p>
If you try it and something breaks, <a href="https://github.com/chiply/space-tree/issues">open an issue</a>.  If something is missing, open one too.  Workspace management doesn't need to be complicated.  I'd like to keep it that way.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>CN Diagrams: Architecture Diagrams That Scale With Your System</title>
      <link>https://www.chiply.dev/post-cn-diagrams</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-cn-diagrams</guid>
      <pubDate>Wed, 18 Feb 2026 20:25:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Every software organization eventually confronts the same uncomfortable question: where is the architecture diagram?</description>
      <content:encoded><![CDATA[

<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">1.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="architectureDiagrams">architectureDiagrams</span>&#xa0;<span class="architecture">architecture</span></span></h2>
<div class="outline-text-2" id="text-introduction">

<div id="org61987cd" class="figure">
<p><img src="https://www.chiply.dev/images/post-cn-diagrams-banner.jpeg" alt="post-cn-diagrams-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p>
Every software organization eventually confronts the same uncomfortable question: <i>where is the architecture diagram?</i>
</p>

<p>
The answer is usually disappointing. There's a Lucidchart from three years ago that nobody updated after the microservices migration. There's a whiteboard photo in Confluence that predates half the current team. There's a Mermaid diagram in a README that shows four services when the system now has forty. The architecture exists in the heads of senior engineers who joined before the last reorg, and getting them in a room together is harder than shipping a feature.
</p>

<p>
This isn't a tooling problem in isolation &#x2013; it's a workflow problem. Traditional diagramming tools force a choice between visual flexibility (drag boxes around) and maintainability (code that can be versioned). They impose rigid hierarchies that don't match how systems actually evolve. And they assume a single persona &#x2013; either the engineer who thinks in code or the product manager who thinks in boxes &#x2013; when real organizations need both<sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>.
</p>

<p>
CN is my attempt to solve this (try it out at <a href="https://www.chiply.dev/cn-diagrams">CN Diagrams</a>). It's a diagrams-as-code tool that renders interactive architecture diagrams with support for arbitrary levels of encapsulation, provides an intuitive domain specific language (DSL) that engineers can version control, and offers a GUI that non-technical stakeholders can actually use. The goal is a single source of truth that the entire organization can maintain.
</p>
</div>
</div>
<div id="outline-container-why--cn--" class="outline-2">
<h2 id="why--cn--"><span class="section-number-2">2.</span> Why "CN"?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="c4Model">c4Model</span></span></h2>
<div class="outline-text-2" id="text-why--cn--">
<p>
The name is a nod to Simon Brown's <a href="https://c4model.com/">C4 model</a>, which popularized a hierarchical approach to architecture visualization. C4 stands for Context, Containers, Components, and Code &#x2013; four levels of abstraction that let you zoom from a 10,000-foot view down to implementation details<sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup>.
</p>

<p>
The C4 model was a revelation when I first encountered it. Finally, a structured way to think about architecture diagrams that acknowledged different audiences need different levels of detail! A CTO reviewing system boundaries doesn't need to see individual classes. A developer debugging a service doesn't need to see the marketing website.
</p>

<p>
But C4's fixed four-level hierarchy started to feel constraining. Real systems don't decompose neatly into exactly four layers. A Kubernetes deployment might have clusters containing namespaces containing deployments containing pods containing containers. An e-commerce platform might have domains containing bounded contexts containing aggregates containing entities. The abstraction depth varies by system, by team, and by what question you're trying to answer.
</p>

<p>
CN takes the core insight of C4 &#x2013; hierarchical decomposition with zoom capabilities &#x2013; and removes the artificial ceiling. The "N" represents arbitrary depth: as many levels of encapsulation as your system requires. Software is infinitely abstractable, and the diagramming tool shouldn't be the limiting factor.
</p>
</div>
</div>
<div id="outline-container-the-graph-paradigm" class="outline-2">
<h2 id="the-graph-paradigm"><span class="section-number-2">3.</span> The Graph Paradigm&#xa0;&#xa0;&#xa0;<span class="tag"><span class="graphTheory">graphTheory</span>&#xa0;<span class="complexity">complexity</span></span></h2>
<div class="outline-text-2" id="text-the-graph-paradigm">
<p>
Architecture diagrams are graphs. This sounds obvious, but the implications are profound.
</p>

<p>
A graph consists of nodes (components) and edges (relationships). When you modify a node, the only things that can possibly be affected are the edges connecting it to other nodes. <b>This locality property is what makes graphs tractable for reasoning about complex systems</b>.
</p>

<p>
Consider modifying a payment service in a large e-commerce platform. In a well-designed system with clear contracts, you don't need to understand the entire platform to make changes safely. You need to understand:
</p>
<ul class="org-ul">
<li>The edges <i>into</i> your service (what calls you, with what data)</li>
<li>The edges <i>out of</i> your service (what you call, with what data)</li>
</ul>

<p>
Everything else is irrelevant to your change. The recommendation engine, the inventory system, the customer support portal &#x2013; <b>they're all behind the graph's locality boundary</b>. As long as you maintain your contracts (the edge definitions), changes stay contained.
</p>

<p>
This is why CN renders architectures as force-directed graphs using <a href="https://js.cytoscape.org/">Cytoscape.js</a>. The visual representation reinforces the mental model: nodes cluster naturally, edges reveal dependencies, and the structure emerges from relationships rather than arbitrary positioning<sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.
</p>

<p>
Of course, this only works when organizations <b>invest</b> <i>time</i> and <i>elbow grease</i> in <b>defining, testing, and enforcing</b> contracts between components &#x2013; ESPECIALLY across bounded contexts&#x2026;.  Depending on the size of your existing system, this may be a significant cultural shift, and rather a lot of work.
</p>

<p>
But that's precisely the kind of discipline that architecture diagrams should encourage. When you can <i>see</i> that your service has fifteen incoming edges, you might think twice before making a breaking change.  And luckily for the overwhelmed, there are ways to bootstrap out of the hardest parts of getting started (<a href="https://www.chiply.dev/#getting-started">7</a>). 
</p>
</div>
</div>
<div id="outline-container-the-state-of-architecture-tooling" class="outline-2">
<h2 id="the-state-of-architecture-tooling"><span class="section-number-2">4.</span> The State of Architecture Tooling&#xa0;&#xa0;&#xa0;<span class="tag"><span class="marketAnalysis">marketAnalysis</span></span></h2>
<div class="outline-text-2" id="text-the-state-of-architecture-tooling">
<p>
The architecture diagramming space splits roughly into two camps, each with significant limitations.
</p>
</div>
<div id="outline-container-visual-first-tools" class="outline-3">
<h3 id="visual-first-tools"><span class="section-number-3">4.1.</span> Visual-First Tools</h3>
<div class="outline-text-3" id="text-visual-first-tools">
<p>
Tools like <a href="https://www.lucidchart.com/">Lucidchart</a>, <a href="https://miro.com/">Miro</a>, and <a href="https://www.diagrams.net/">diagrams.net</a> prioritize visual flexibility. You drag boxes, draw arrows, and arrange things until they look right. This actually works <b>really</b> well for initial brainstorming and produces attractive outputs.
</p>

<p>
But these tools don't scale with organizational complexity. Changes require manual updates to visual layouts. There's no good story for version control &#x2013; you're comparing screenshots or trusting proprietary diff algorithms. And when the diagram gets complex enough to need multiple views, you're maintaining multiple documents with no guarantee of consistency.
</p>
</div>
</div>
<div id="outline-container-code-first-tools" class="outline-3">
<h3 id="code-first-tools"><span class="section-number-3">4.2.</span> Code-First Tools</h3>
<div class="outline-text-3" id="text-code-first-tools">
<p>
Tools like <a href="https://mermaid.js.org/">Mermaid</a>, <a href="https://plantuml.com/">PlantUML</a>, and <a href="https://structurizr.com/">Structurizr</a> prioritize maintainability through code. Diagrams are defined in text files that can be versioned, diffed, and reviewed like any other code artifact.
</p>

<p>
The trade-off is flexibility. Layout control is limited &#x2013; you're at the mercy of automatic algorithms that may not produce readable results. Each tool has its own DSL syntax, creating lock-in. And crucially, these tools are engineer-focused; asking a product manager to edit PlantUML syntax is asking for trouble, lol. <sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup> 
</p>

<p>
Structurizr deserves special mention as the most sophisticated option in this space. It's built around the C4 model and supports generating multiple views from a single model. But it inherits C4's four-level limitation, has a steeper learning curve, and still requires technical knowledge to maintain.
</p>
</div>
</div>
<div id="outline-container-where-cn-fits" class="outline-3">
<h3 id="where-cn-fits"><span class="section-number-3">4.3.</span> Where CN Fits</h3>
<div class="outline-text-3" id="text-where-cn-fits">
<p>
CN attempts to bridge both camps. The underlying representation is code &#x2013; a YAML-based DSL that engineers can version control. But the primary interface is a visual editor with bidirectional sync: changes in the GUI update the code, and changes in the code update the GUI.
</p>

<p>
This dual-interface approach means the engineering team can maintain the diagram through pull requests while product and design can make updates through point-and-click interactions. <b>Everyone works on the same artifact</b>.
</p>
</div>
</div>
</div>
<div id="outline-container-how-cn-works" class="outline-2">
<h2 id="how-cn-works"><span class="section-number-2">5.</span> How CN Works</h2>
<div class="outline-text-2" id="text-how-cn-works">
<p>
CN provides several capabilities that address the challenges outlined above.
</p>
</div>
<div id="outline-container-hierarchical-encapsulation" class="outline-3">
<h3 id="hierarchical-encapsulation"><span class="section-number-3">5.1.</span> Hierarchical Encapsulation</h3>
<div class="outline-text-3" id="text-hierarchical-encapsulation">
<p>
Nodes can contain other nodes to arbitrary depth. A "Payment Domain" node might contain "Payment Gateway", "Fraud Detection", and "Settlement" nodes. The "Fraud Detection" node might itself contain "ML Scoring", "Rules Engine", and "Manual Review" nodes. There's no artificial limit.
</p>

<p>
The UI provides expand/collapse controls at each level, letting viewers drill into areas of interest while keeping the rest of the diagram manageable. This is the zoom capability from C4, generalized.
</p>
</div>
</div>
<div id="outline-container-bidirectional-editing" class="outline-3">
<h3 id="bidirectional-editing"><span class="section-number-3">5.2.</span> Bidirectional Editing</h3>
<div class="outline-text-3" id="text-bidirectional-editing">
<p>
The DSL editor and visual canvas stay synchronized. Add a node in the code, it appears on the canvas. Drag an edge in the canvas, the code updates. This eliminates the choice between maintainability and accessibility &#x2013; you get both.
</p>
</div>
</div>
<div id="outline-container-real-time-visualization" class="outline-3">
<h3 id="real-time-visualization"><span class="section-number-3">5.3.</span> Real-Time Visualization</h3>
<div class="outline-text-3" id="text-real-time-visualization">
<p>
Changes render immediately. There's no compile step, no waiting for diagram generation. This tight feedback loop makes iterative design natural.
</p>
</div>
</div>
<div id="outline-container-example-templates" class="outline-3">
<h3 id="example-templates"><span class="section-number-3">5.4.</span> Example Templates</h3>
<div class="outline-text-3" id="text-example-templates">
<p>
CN ships with several example architectures &#x2013; Simple Web App, E-Commerce Platform, Cloud Platform, Event-Driven Architecture, FinTech Platform &#x2013; that demonstrate patterns and provide starting points for new diagrams.
</p>

<p>
You can try CN directly at the <a href="https://www.chiply.dev/cn-diagrams">CN Diagrams</a> page on my blog.
</p>
</div>
</div>
</div>
<div id="outline-container-benefits" class="outline-2">
<h2 id="benefits"><span class="section-number-2">6.</span> Benefits&#xa0;&#xa0;&#xa0;<span class="tag"><span class="productivity">productivity</span>&#xa0;<span class="maintainability">maintainability</span></span></h2>
<div class="outline-text-2" id="text-benefits">
<p>
Adopting CN &#x2013; or any architecture documentation discipline &#x2013; provides concrete benefits to engineering organizations.
</p>
</div>
<div id="outline-container-accelerated-onboarding" class="outline-3">
<h3 id="accelerated-onboarding"><span class="section-number-3">6.1.</span> Accelerated Onboarding</h3>
<div class="outline-text-3" id="text-accelerated-onboarding">
<p>
New team members can explore the system visually before diving into code. The hierarchical structure provides natural learning paths: start with the high-level context, then drill into your team's domain, then into specific services. This is dramatically faster than assembling mental models from scattered README files and tribal knowledge.
</p>
</div>
</div>
<div id="outline-container-reduced-coordination-overhead" class="outline-3">
<h3 id="reduced-coordination-overhead"><span class="section-number-3">6.2.</span> Reduced Coordination Overhead</h3>
<div class="outline-text-3" id="text-reduced-coordination-overhead">
<p>
When everyone references the same diagram, discussions about system changes become more productive. Instead of "I think we have a service that handles X," you can point to specific nodes and edges. Architecture review meetings can focus on proposed changes rather than establishing baseline understanding.
</p>
</div>
</div>
<div id="outline-container-impact-analysis" class="outline-3">
<h3 id="impact-analysis"><span class="section-number-3">6.3.</span> Impact Analysis</h3>
<div class="outline-text-3" id="text-impact-analysis">
<p>
The graph structure makes dependency analysis explicit. Before modifying a service, you can inspect its edges to understand what might be affected. This doesn't replace thorough testing, but it does reduce surprise.
</p>
</div>
</div>
<div id="outline-container-documentation-that-stays-current" class="outline-3">
<h3 id="documentation-that-stays-current"><span class="section-number-3">6.4.</span> Documentation That Stays Current</h3>
<div class="outline-text-3" id="text-documentation-that-stays-current">
<p>
Because the diagram lives in version control and updates are simple (either GUI or code), there's a realistic chance of keeping it accurate. Contrast this with documentation that requires a dedicated effort to update &#x2013; it will always drift.
</p>
</div>
</div>
</div>
<div id="outline-container-getting-started" class="outline-2">
<h2 id="getting-started"><span class="section-number-2">7.</span> Getting Started&#xa0;&#xa0;&#xa0;<span class="tag"><span class="bootstrap">bootstrap</span>&#xa0;<span class="ai">ai</span></span></h2>
<div class="outline-text-2" id="text-getting-started">
<p>
The hardest part of architecture documentation is the initial investment. Mapping an existing system is tedious work, and teams are usually too busy building features to spend hours, let alone weeks, drawing boxes.
</p>

<p>
This is where AI assistance becomes valuable. LLMs excel at understanding structured code, and CN's DSL is exactly that &#x2013; structured code. An AI agent can crawl a codebase, identify services and their relationships, and generate an initial CN diagram as a starting point.
</p>

<p>
The workflow looks like this:
</p>

<ol class="org-ol">
<li>Point an agent at your GitHub repository (or local codebase)</li>
<li>The agent analyzes directory structure, imports, API calls, and configuration</li>
<li>It generates a CN DSL file representing the discovered architecture</li>
<li>Engineers review and refine the generated diagram</li>
<li>The refined diagram becomes the maintained source of truth</li>
</ol>

<p>
The generated diagram won't be perfect. AI agents make mistakes, and they can only discover relationships that are explicit in code. <b>But a 70% accurate starting point that takes minutes to generate is more useful than a blank canvas that takes weeks to fill</b>.  This first-pass strategy lowers the barrier to getting started with architecture documentation.
</p>

<p>
This bootstrap approach leverages diagrams-as-code in a way that visual tools can't match. You can't ask an AI to drag boxes to the right positions in Lucidchart (at least, not yet). But you can absolutely ask it to generate structured YAML that describes your system's components and relationships.  In fact, <b>LLMs are phenomenally good at generating and understanding structured text, and YAML is a great candidate</b>.
</p>
</div>
</div>
<div id="outline-container-contributing" class="outline-2">
<h2 id="contributing"><span class="section-number-2">8.</span> Contributing&#xa0;&#xa0;&#xa0;<span class="tag"><span class="openSource">openSource</span></span></h2>
<div class="outline-text-2" id="text-contributing">
<p>
CN is open source under the MIT license. The source code is available at <a href="https://github.com/chiply/cn-diagrams">github.com/chiply/cn-diagrams</a>.
</p>
</div>
<div id="outline-container-tech-stack" class="outline-3">
<h3 id="tech-stack"><span class="section-number-3">8.1.</span> Tech Stack</h3>
<div class="outline-text-3" id="text-tech-stack">
<p>
For those interested in contributing, CN is built with:
</p>

<ul class="org-ul">
<li><b>SvelteKit</b> - Application framework</li>
<li><b>Cytoscape.js</b> - Graph rendering engine</li>
<li><b>CodeMirror</b> - DSL editor component</li>
<li><b>YAML</b> - DSL format for diagram definitions</li>
<li><b>Vercel</b> - Deployment platform</li>
</ul>

<p>
The codebase is TypeScript throughout, with Svelte 5 runes for reactivity.
</p>
</div>
</div>
<div id="outline-container-how-to-contribute" class="outline-3">
<h3 id="how-to-contribute"><span class="section-number-3">8.2.</span> How to Contribute</h3>
<div class="outline-text-3" id="text-how-to-contribute">
<p>
Contributions are welcome in several forms:
</p>

<p>
<b>Bug Reports</b>: If something doesn't work as expected, open an issue with steps to reproduce. Include your browser and any error messages from the console.
</p>

<p>
<b>Feature Requests</b>: Have an idea for improvement? Open an issue describing the use case and proposed solution. Discussion before implementation helps ensure alignment.
</p>

<p>
<b>Pull Requests</b>: For code contributions, fork the repository, create a feature branch, and submit a PR. Please include tests for new functionality and ensure existing tests pass.
</p>

<p>
<b>Documentation</b>: Improvements to README, inline comments, or example diagrams are valuable contributions that don't require deep code knowledge.
</p>

<p>
The project is young, and there's significant opportunity to shape its direction. Community feedback on what works, what doesn't, and what's missing will determine where CN goes next.
</p>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">9.</span> Conclusion</h2>
<div class="outline-text-2" id="text-conclusion">
<p>
Architecture diagrams shouldn't be artifacts that exist for compliance or decoration. They should be living documents that help teams understand, communicate about, and evolve their systems.
</p>

<p>
CN is an attempt to make that practical by removing the barriers that cause architecture documentation to become stale: the rigid hierarchies that don't match real systems, the choice between maintainability and accessibility, and the assumption that only one persona &#x2013; engineer or non-technical &#x2013; will maintain the diagram.
</p>

<p>
Whether or not CN specifically fits your needs, I hope the ideas resonate: graphs as the fundamental model for system relationships, arbitrary encapsulation depth over fixed hierarchies, and dual interfaces that serve different personas working on the same artifact.
</p>

<p>
Try it at <a href="https://www.chiply.dev/cn-diagrams">CN Diagrams</a>. Browse the source at <a href="https://github.com/chiply/cn-diagrams">github.com/chiply/cn-diagrams</a>. If you build something interesting or have feedback, I'd love to hear about it.
</p>
</div>
</div>
<div id="outline-container-socials" class="outline-2">
<h2 id="socials"><span class="section-number-2">10.</span> socials</h2>
<div class="outline-text-2" id="text-socials">
<ul class="org-ul">
<li><a href="https://www.reddit.com/r/programming/comments/1qqhr6l/cn_diagrams_architecture_diagrams_that_scale_with/">reddit: CN Diagrams (r/programming)</a></li>
<li><a href="https://www.reddit.com/r/softwarearchitecture/comments/1qpxtvv/cn_diagrams/">reddit: CN Diagrams (r/softwarearchitecture)</a></li>
</ul>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">11.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
<b>*tl;dr:</b> CN is a new open-source architecture diagramming tool that bridges the gap between code-based maintainability and visual accessibility. <a href="https://www.chiply.dev/#introduction">The problem</a> is universal: architecture diagrams become stale because traditional tools force teams to choose between drag-and-drop flexibility (which doesn't version control) and code-based definitions (which non-engineers can't edit). <a href="https://www.chiply.dev/#why--cn--">CN extends Simon Brown's C4 model</a> by removing the four-level hierarchy limit – the "N" represents arbitrary depth, letting you zoom from system context down to any level of detail your architecture requires.
</p>

<p>
<a href="https://www.chiply.dev/#the-graph-paradigm">Architecture diagrams are fundamentally graphs</a>, and CN embraces this with force-directed layouts that reveal natural clustering and dependencies. This locality property means you only need to understand immediate connections when making changes, not the entire system. <a href="https://www.chiply.dev/#the-state-of-architecture-tooling">Current tools fall into two camps</a>: visual-first options like Lucidchart that don't scale, and code-first options like Mermaid that alienate non-technical users. CN solves this with bidirectional editing – changes in the YAML-based DSL update the visual canvas, and GUI edits update the code.
</p>

<p>
<a href="https://www.chiply.dev/#how-cn-works">Key features include</a> hierarchical encapsulation (nodes can contain nodes indefinitely), real-time visualization with no compile step, and example templates for common architectures. <a href="https://www.chiply.dev/#benefits">The payoff</a> is concrete: faster onboarding as new engineers explore visually, reduced coordination overhead with a single source of truth, explicit dependency analysis through the graph structure, and documentation that actually stays current because it's easy to update.
</p>

<p>
<a href="https://www.chiply.dev/#getting-started">The bootstrap strategy</a> leverages AI to crawl codebases and generate initial diagrams, lowering the barrier to adoption – a 70% accurate starting point beats a blank canvas. <a href="https://www.chiply.dev/#contributing">CN is MIT-licensed and built with</a> SvelteKit, Cytoscape.js, and CodeMirror, welcoming contributions from bug reports to pull requests. Try it at the CN Diagrams page or explore the source at github.com/chiply/cn-diagrams.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
This tension &#x2013; between engineers who want code and stakeholders who want visuals &#x2013; is everywhere in software documentation. CN isn't the only tool trying to bridge it, but it's opinionated about doing so through bidirectional sync rather than separate artifacts.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
If you're not familiar with C4, Simon Brown's <a href="https://c4model.com/">website</a> is the canonical reference. The model has influenced how an entire generation of architects thinks about documentation. Brown also created <a href="https://structurizr.com/">Structurizr</a>, the most sophisticated C4-focused tool.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
In fact, CN intentionally protects the engineer/designer against this arbitrtary positioning, and renders the diagram in such a way that it is determistic based on the graph structure. This is important for maintainability, as it prevents users from needing to manually rearrange nodes when the underlying structure changes, and pledges a relatively consistent layout over time.
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
By the way, asking software engineers to edit visual diagrams is also asking for trouble 😉.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>The Schema Language Question: Avro, JSON Schema, Protobuf, and the Quest for a Single Source of Truth</title>
      <link>https://www.chiply.dev/post-schema-languages</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-schema-languages</guid>
      <pubDate>Tue, 17 Feb 2026 15:26:00 GMT</pubDate>
      <author>Charlie Holland</author>
      <description>Keep Dijkstra&apos;s words in mind as you read. This entire post &amp;#x2013; every schema language comparison, every trade-off analysis, every design decision &amp;#x2013; is really about one question: do we trus...</description>
      <content:encoded><![CDATA[

<div id="outline-container-about" class="outline-2">
<h2 id="about"><span class="section-number-2">1.</span> About&#xa0;&#xa0;&#xa0;<span class="tag"><span class="dataModelling">dataModelling</span>&#xa0;<span class="schemaLanguages">schemaLanguages</span></span></h2>
<div class="outline-text-2" id="text-about">

<div id="org262cfea" class="figure">
<p><img src="https://www.chiply.dev/images/schema-language-banner.jpeg" alt="schema-language-banner.jpeg" />
</p>
<p><span class="figure-number">Figure 1: </span>JPEG produced with <a href="https://openai.com/index/introducing-4o-image-generation/">DALL-E 4o</a></p>
</div>

<p class="verse">
The competent programmer is fully aware of the strictly limited size of his own skull;<br />
therefore he approaches the programming task in full humility,<br />
and among other things he avoids clever tricks like the plague.<br />
&#xa0;&#xa0;&#xa0;&#xa0;&#x2014;Edsger W. Dijkstra, <i>The Humble Programmer</i> (1972)<br />
</p>

<p>
Keep Dijkstra's words in mind as you read.  This entire post &#x2013; every schema language comparison, every trade-off analysis, every design decision &#x2013; is really about one question: <i>do we trust our own cleverness, or do we build systems that acknowledge its limits?</i>
</p>

<p>
This post is the second installment in a series on data modelling in modern software systems.  The <a href="https://www.chiply.dev/post-data-model-testing">companion post</a> established the <i>Model Everywhere Problem</i> &#x2013; the uncomfortable reality that data models live in your application code, your APIs, your message queues, your databases, your documentation, and your tests, all simultaneously.  That post asked: "Given these models exist everywhere, how do we <i>test</i> them systematically?"
</p>

<blockquote class="pull-quote pull-right">
<p>
If you're defining models in Python, Java, Go, and TypeScript &#x2013; you don't have one model.  You have four.  Four that will drift apart, silently, inevitably, until something breaks in production on a Friday evening.
</p>
</blockquote>

<p>
This post asks the antecedent question: <b>how do we <i>express</i> those models in a portable, standard format?</b>
</p>

<p>
The answer matters more than most teams realize.
</p>

<p>
If you're defining models in Python (Pydantic), Java (POJOs), Go (structs), and TypeScript (interfaces) &#x2013; you don't have one model.  You have four.  Four that will drift apart, silently, inevitably, until something breaks in production on a Friday evening.
</p>

<p>
Schema languages &#x2013; Avro, JSON Schema, Protocol Buffers, and their kin &#x2013; exist to solve exactly this problem.  They provide a single, language-agnostic source of truth from which all language-specific representations can be <i>generated</i>.  This post is a deep technical comparison of the three dominant schema languages, a survey of the broader landscape, and (because I can't help myself) a speculative design for the ideal Universal Schema Language (USL) that doesn't exist yet.
</p>
</div>
<div id="outline-container-executive-summary" class="outline-3">
<h3 id="executive-summary"><span class="section-number-3">1.1.</span> Executive Summary</h3>
<div class="outline-text-3" id="text-executive-summary">
<p>
Schema languages provide a language-agnostic single source of truth for data models.  Protobuf excels at performance-critical RPC, Avro at schema-evolving event streaming, and JSON Schema at web API validation.  Choose based on your primary use case, not on which one your favorite tech influencer recommends.
</p>

<p>
Modern systems are polyglot.  A typical data pipeline might involve Python ingestion, Go microservices, TypeScript frontends, and Java analytics &#x2013; all sharing data models.  When models are defined independently in each language, they drift.  Silently.  The <a href="https://www.chiply.dev/post-data-model-testing">companion post</a> documented the combinatorial explosion of testing these models.  But testing assumes the models <i>agree</i> in the first place.  We need a schema language that defines models once, generates code for every target language, supports schema evolution without breaking consumers, and integrates with existing toolchains.  What's more, reducing the definition space for your models will greatly mitigate the combinatorial explosion entailed in testing multiple schemas together.
</p>

<p>
This article covers:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Section</th>
<th scope="col" class="org-left">Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><a href="https://www.chiply.dev/#why-a-single-source-of-truth-">Single Source of Truth</a></td>
<td class="org-left">Motivates the need with real-world failure modes</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#why-not-just-use-pydantic---zod---your-framework-s-types-">Why Not Pydantic / Zod?</a></td>
<td class="org-left">Addresses the "but my language has X" objection</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#the-landscape-of-schema-languages">The Landscape</a></td>
<td class="org-left">Surveys 20+ schema languages by category</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#protobuf-deep-dive">Protobuf</a>, <a href="https://www.chiply.dev/#avro-deep-dive">Avro</a>, <a href="https://www.chiply.dev/#json-schema-deep-dive">JSON Schema</a></td>
<td class="org-left">Deep technical analysis of the three dominant options</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#head-to-head-comparison">Head-to-Head</a></td>
<td class="org-left">Feature matrix, decision guide, benchmarks</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#war-stories--when-schemas-fail">War Stories</a></td>
<td class="org-left">Real-world failures from schema drift</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#the-ecosystem-unlocked">Ecosystem Unlocked</a></td>
<td class="org-left">What you gain beyond just "types"</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.chiply.dev/#designing-the-ideal-schema-language">Ideal Language</a></td>
<td class="org-left">A speculative design combining the best of all three</td>
</tr>
</tbody>
</table>

<p>
After reading, you'll be able to choose the right schema language for your use case; articulate why language-specific alternatives aren't sufficient; understand the trade-offs between performance; flexibility, and ecosystem maturity; and evaluate new schema languages as they emerge.
</p>
</div>
</div>
</div>
<div id="outline-container-why-a-single-source-of-truth-" class="outline-2">
<h2 id="why-a-single-source-of-truth-"><span class="section-number-2">2.</span> Why a Single Source of Truth?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="architecture">architecture</span>&#xa0;<span class="ddd">ddd</span></span></h2>
<div class="outline-text-2" id="text-why-a-single-source-of-truth-">
<p>
Let me spin a yarn about two technology organizations.
</p>

<p>
Organization A builds an e-commerce platform.  The product team defines the <code>Order</code> model.  The backend team implements it in Go.  The frontend team implements it in TypeScript.  The data team implements it in Python.  The mobile team implements it in Kotlin.  Four teams, four languages, four definitions of "what an Order looks like."
</p>

<p>
On Monday, the product team adds a <code>discountType</code> field with three possible values: <code>PERCENTAGE</code>, <code>FIXED</code>, <code>BOGO</code>.  The backend team picks it up in the next sprint.  The frontend team gets to it two weeks later.  The data team doesn't hear about it until a pipeline breaks.  The mobile team ships an update a month later.
</p>

<p>
For those intervening weeks, the system is in a state of <i>model drift</i> &#x2013; different parts of the system disagree about what an Order is.  This isn't a hypothetical.  This is Tuesday.
</p>

<p>
Organization B builds a similar platform but <i>starts</i> with a schema language.  They define <code>Order</code> once, in a standard schema language like a <code>.proto</code> file (or <code>.avsc</code>, or JSON Schema).  When the product team adds <code>discountType</code>, it's added to the schema file.  A CI pipeline generates updated Go structs, TypeScript interfaces, Python dataclasses, and Kotlin data classes.  All four teams get the change simultaneously.  If anyone sends a message with the old schema, the generated code rejects it at deserialization time.
</p>

<p>
The difference bettween Org A and Org B isn't subtle.  It's the difference between "we have one model expressed in four languages" and "we have four models that happen to share a name."
</p>
</div>
<div id="outline-container-model-drift--the-silent-killer" class="outline-3">
<h3 id="model-drift--the-silent-killer"><span class="section-number-3">2.1.</span> Model Drift: The Silent Killer</h3>
<div class="outline-text-3" id="text-model-drift--the-silent-killer">
<p>
Model drift is insidious because it doesn't cause immediate, obvious failures.  Instead, it produces <i>silent data corruption</i> &#x2013; the most expensive class of bugs in software engineering<sup><a id="fnr.1" class="footref" href="https://www.chiply.dev/#fn.1" role="doc-backlink">1</a></sup>.
</p>

<p>
Consider what happens when Organization A's Go backend starts sending orders with <code>discountType: "BOGO"</code> but the Python data pipeline doesn't know about that enum variant:
</p>

<ol class="org-ol">
<li>The Python deserializer encounters an unknown value</li>
<li>Depending on the library, it either silently drops the field, coerces it to a default, or raises an exception</li>
<li>If it drops the field, every downstream analytics report under-counts BOGO discounts</li>
<li>If it coerces to a default, reports show phantom <code>PERCENTAGE</code> discounts that don't exist</li>
<li>If it raises an exception, the pipeline crashes &#x2013; the best-case scenario, because at least someone notices</li>
</ol>

<p>
Here's what this looks like in code.  The Go backend sends a perfectly valid order:
</p>

<div class="org-src-container">
<pre class="src src-go">// Go backend (v2 of the model -- has discount_type)
order := map[string]interface{}{
    "id":            "ord_abc123",
    "customer_id":   "cust_456",
    "items":         []Item{{ProductID: "SKU-1", Qty: 2, Price: 29.99}},
    "discount_type": "BOGO",
    "total":         29.99,
}
payload, _ := json.Marshal(order)
kafka.Produce("orders", payload)
</pre>
</div>

<p>
The Python pipeline consumes it with an older model that doesn't know about <code>BOGO</code>:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">Python pipeline (v1 of the model -- no discount_type)
</span><span style="color: #005f5f;">@dataclass</span>
<span style="color: #531ab6;">class</span> <span style="color: #005f5f;">Order</span>:
    <span style="color: #8f0075;">id</span>: <span style="color: #8f0075;">str</span>
    customer_id: <span style="color: #8f0075;">str</span>
    items: <span style="color: #8f0075;">list</span>
    total: <span style="color: #8f0075;">float</span>

<span style="color: #005e8b;">raw</span> <span style="color: #000000;">=</span> kafka.consume<span style="color: #000000;">(</span><span style="color: #3548cf;">"orders"</span><span style="color: #000000;">)</span>
<span style="color: #005e8b;">data</span> <span style="color: #000000;">=</span> json.loads<span style="color: #000000;">(</span>raw<span style="color: #000000;">)</span>
<span style="color: #005e8b;">order</span> <span style="color: #000000;">=</span> Order<span style="color: #000000;">(</span><span style="color: #000000;">**</span><span style="color: #dd22dd;">{</span>k: v <span style="color: #531ab6;">for</span> k, v <span style="color: #531ab6;">in</span> data.items<span style="color: #008899;">()</span> <span style="color: #531ab6;">if</span> k <span style="color: #531ab6;">in</span> Order.__dataclass_fields__<span style="color: #dd22dd;">}</span><span style="color: #000000;">)</span>
<span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">discount_type="BOGO" is silently dropped. No error. No warning.
</span><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">Downstream: this order counted as "no discount applied"</span>
</pre>
</div>

<p>
The Python pipeline has no exception.  No log.  The data looks correct in both systems &#x2013; they just disagree about whether this order had a discount.
</p>

<p>
Issues 1 and 2 are the scary ones.  The data looks plausible.  Nobody questions it.  <i>Business decisions get made on corrupted data</i>.  Weeks or months later, someone notices the numbers don't add up.  Now you're debugging a data integrity issue across four codebases, with weeks of corrupted data that may need to be replayed.
</p>

<p>
This is not an edge case.  A 2020 survey by Monte Carlo Data found that <i>77%</i> of data engineering teams reported data quality incidents in the previous year, with schema changes cited as a leading root cause<sup><a id="fnr.2" class="footref" href="https://www.chiply.dev/#fn.2" role="doc-backlink">2</a></sup>.
</p>
</div>
</div>
<div id="outline-container-with-and-without-a-single-source-of-truth" class="outline-3">
<h3 id="with-and-without-a-single-source-of-truth"><span class="section-number-3">2.2.</span> With and Without a Single Source of Truth</h3>
<div class="outline-text-3" id="text-with-and-without-a-single-source-of-truth">
<p>
The two approaches, visualized:
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">graph</span> <span style="color: #0000b0;">TB</span>
    <span style="color: #531ab6;">subgraph</span> <span style="color: #3548cf;">"Without SSOT: Manual Synchronization"</span>
        direction <span style="color: #0000b0;">TB</span>
        PM1<span style="color: #000000;">[</span><span style="color: #3548cf;">"Product Team&lt;br/&gt;defines Order spec"</span><span style="color: #000000;">]</span>
        GO1<span style="color: #000000;">[</span><span style="color: #3548cf;">"Go Backend&lt;br/&gt;type Order struct{...}"</span><span style="color: #000000;">]</span>
        TS1<span style="color: #000000;">[</span><span style="color: #3548cf;">"TypeScript Frontend&lt;br/&gt;interface Order {...}"</span><span style="color: #000000;">]</span>
        PY1<span style="color: #000000;">[</span><span style="color: #3548cf;">"Python Pipeline&lt;br/&gt;class Order(BaseModel)"</span><span style="color: #000000;">]</span>
        KT1<span style="color: #000000;">[</span><span style="color: #3548cf;">"Kotlin Mobile&lt;br/&gt;data class Order(...)"</span><span style="color: #000000;">]</span>

        PM1 <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"week 1"</span>| GO1
        PM1 <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"week 3"</span>| TS1
        PM1 <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"month 2"</span>| PY1
        PM1 <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"month 3"</span>| KT1

        GO1 <span style="color: #721045;">-</span>.<span style="color: #721045;">-</span>|<span style="color: #3548cf;">"&#10060; drift"</span>| TS1
        TS1 <span style="color: #721045;">-</span>.<span style="color: #721045;">-</span>|<span style="color: #3548cf;">"&#10060; drift"</span>| PY1
        PY1 <span style="color: #721045;">-</span>.<span style="color: #721045;">-</span>|<span style="color: #3548cf;">"&#10060; drift"</span>| KT1
    <span style="color: #531ab6;">end</span>
</pre>
</div>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">graph</span> <span style="color: #0000b0;">TB</span>
    <span style="color: #531ab6;">subgraph</span> <span style="color: #3548cf;">"With SSOT: Generated from Schema"</span>
        direction <span style="color: #0000b0;">TB</span>
        SCHEMA<span style="color: #000000;">[</span><span style="color: #3548cf;">"order.proto&lt;br/&gt;(Single Source of Truth)"</span><span style="color: #000000;">]</span>
        CI<span style="color: #000000;">[</span><span style="color: #3548cf;">"CI Pipeline&lt;br/&gt;protoc / buf generate"</span><span style="color: #000000;">]</span>
        GO2<span style="color: #000000;">[</span><span style="color: #3548cf;">"Go Backend&lt;br/&gt;order.pb.go &#9989;"</span><span style="color: #000000;">]</span>
        TS2<span style="color: #000000;">[</span><span style="color: #3548cf;">"TypeScript Frontend&lt;br/&gt;order_pb.ts &#9989;"</span><span style="color: #000000;">]</span>
        PY2<span style="color: #000000;">[</span><span style="color: #3548cf;">"Python Pipeline&lt;br/&gt;order_pb2.py &#9989;"</span><span style="color: #000000;">]</span>
        KT2<span style="color: #000000;">[</span><span style="color: #3548cf;">"Kotlin Mobile&lt;br/&gt;Order.kt &#9989;"</span><span style="color: #000000;">]</span>

        SCHEMA <span style="color: #721045;">--&gt;</span> CI
        CI <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"simultaneous"</span>| GO2
        CI <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"simultaneous"</span>| TS2
        CI <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"simultaneous"</span>| PY2
        CI <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"simultaneous"</span>| KT2
    <span style="color: #531ab6;">end</span>
</pre>
</div>

<p>
The difference is structural, not procedural.  With a single source of truth, model drift isn't prevented by discipline or process &#x2013; it's prevented by <i>architecture</i>.  You can't drift from the schema because your code is <i>generated</i> from it.  The generated code is the schema, expressed in your language.
</p>
</div>
</div>
<div id="outline-container-the-cost-of-not-having-a-ssot" class="outline-3">
<h3 id="the-cost-of-not-having-a-ssot"><span class="section-number-3">2.3.</span> The Cost of Not Having a SSOT</h3>
<div class="outline-text-3" id="text-the-cost-of-not-having-a-ssot">
<p>
The cost of model drift across several dimensions:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Dimension</th>
<th scope="col" class="org-left">Without SSOT</th>
<th scope="col" class="org-left">With SSOT</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Time to propagate a schema change</b></td>
<td class="org-left">Days to months (depends on team velocity)</td>
<td class="org-left">Minutes (CI pipeline)</td>
</tr>

<tr>
<td class="org-left"><b>Probability of drift</b></td>
<td class="org-left">Increases with number of consumers</td>
<td class="org-left">Zero (by construction)</td>
</tr>

<tr>
<td class="org-left"><b>Cost of a schema mismatch</b></td>
<td class="org-left">Silent data corruption → hours/days to diagnose</td>
<td class="org-left">Compile error or deserialization failure → seconds</td>
</tr>

<tr>
<td class="org-left"><b>Documentation accuracy</b></td>
<td class="org-left">Stale within days of writing</td>
<td class="org-left">Auto-generated, always current</td>
</tr>

<tr>
<td class="org-left"><b>Onboarding a new language</b></td>
<td class="org-left">Copy-paste and hope</td>
<td class="org-left">Add a code-gen target</td>
</tr>

<tr>
<td class="org-left"><b>Cross-team communication</b></td>
<td class="org-left">"Hey, did you update the Order model?" (Slack)</td>
<td class="org-left">PR review on a .proto file (code review)</td>
</tr>
</tbody>
</table>

<blockquote class="pull-quote pull-left">
<p>
Toolchains don't forget.
</p>
</blockquote>

<p>
The SSOT approach converts <i>human coordination</i> problems into <i>toolchain</i> problems.  Toolchains don't forget.  Toolchains don't go on vacation.  Toolchains don't interpret an ambiguous Confluence page differently than you did.
</p>
</div>
</div>
</div>
<div id="outline-container-why-not-just-use-pydantic---zod---your-framework-s-types-" class="outline-2">
<h2 id="why-not-just-use-pydantic---zod---your-framework-s-types-"><span class="section-number-2">3.</span> Why Not Just Use Pydantic / Zod / Your Framework's Types?&#xa0;&#xa0;&#xa0;<span class="tag"><span class="python">python</span>&#xa0;<span class="validation">validation</span>&#xa0;<span class="antipattern">antipattern</span></span></h2>
<div class="outline-text-2" id="text-why-not-just-use-pydantic---zod---your-framework-s-types-">
<p>
If you're reading this, you might be thinking: "I already have Pydantic (Python), Zod (TypeScript), serde (Rust), or Jackson (Java).  My framework gives me types, validation, serialization.  Why do I need a separate schema language?"
</p>

<p>
It's a fair question.  These are excellent libraries.  <b>The issue isn't with any of them &#x2013; it's with the assumption that a <i>language-specific</i> solution can serve as a <i>language-agnostic</i> single source of truth</b>.  I'll use Pydantic as the case study because it's the most full-featured example of this pattern, but the argument applies equally to every language-specific modelling library.
</p>
</div>
<div id="outline-container-the-allure-of-pydantic" class="outline-3">
<h3 id="the-allure-of-pydantic"><span class="section-number-3">3.1.</span> The Allure of Pydantic</h3>
<div class="outline-text-3" id="text-the-allure-of-pydantic">
<p>
Credit where it's due &#x2013; Pydantic is remarkable:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #531ab6;">from</span> pydantic <span style="color: #531ab6;">import</span> BaseModel, Field, field_validator
<span style="color: #531ab6;">from</span> enum <span style="color: #531ab6;">import</span> Enum
<span style="color: #531ab6;">from</span> typing <span style="color: #531ab6;">import</span> Optional
<span style="color: #531ab6;">from</span> datetime <span style="color: #531ab6;">import</span> datetime


<span style="color: #531ab6;">class</span> <span style="color: #005f5f;">DiscountType</span><span style="color: #000000;">(</span><span style="color: #8f0075;">str</span>, Enum<span style="color: #000000;">)</span>:
    <span style="color: #005e8b;">PERCENTAGE</span> <span style="color: #000000;">=</span> <span style="color: #3548cf;">"PERCENTAGE"</span>
    <span style="color: #005e8b;">FIXED</span> <span style="color: #000000;">=</span> <span style="color: #3548cf;">"FIXED"</span>
    <span style="color: #005e8b;">BOGO</span> <span style="color: #000000;">=</span> <span style="color: #3548cf;">"BOGO"</span>


<span style="color: #531ab6;">class</span> <span style="color: #005f5f;">OrderItem</span><span style="color: #000000;">(</span>BaseModel<span style="color: #000000;">)</span>:
    product_id: <span style="color: #8f0075;">str</span>
    <span style="color: #005e8b;">quantity</span>: <span style="color: #8f0075;">int</span> <span style="color: #000000;">=</span> Field<span style="color: #000000;">(</span>ge<span style="color: #000000;">=</span>1<span style="color: #000000;">)</span>
    <span style="color: #005e8b;">unit_price</span>: <span style="color: #8f0075;">float</span> <span style="color: #000000;">=</span> Field<span style="color: #000000;">(</span>gt<span style="color: #000000;">=</span>0<span style="color: #000000;">)</span>

    <span style="color: #005f5f;">@field_validator</span><span style="color: #000000;">(</span><span style="color: #3548cf;">"quantity"</span><span style="color: #000000;">)</span>
    @<span style="color: #8f0075;">classmethod</span>
    <span style="color: #531ab6;">def</span> <span style="color: #721045;">reasonable_quantity</span><span style="color: #000000;">(</span>cls, v: <span style="color: #8f0075;">int</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #8f0075;">int</span>:
        <span style="color: #531ab6;">if</span> v <span style="color: #000000;">&gt;</span> 10_000:
            <span style="color: #531ab6;">raise</span> <span style="color: #005f5f;">ValueError</span><span style="color: #000000;">(</span><span style="color: #3548cf;">"Suspicious quantity"</span><span style="color: #000000;">)</span>
        <span style="color: #531ab6;">return</span> v


<span style="color: #531ab6;">class</span> <span style="color: #005f5f;">Order</span><span style="color: #000000;">(</span>BaseModel<span style="color: #000000;">)</span>:
    <span style="color: #8f0075;">id</span>: <span style="color: #8f0075;">str</span>
    customer_id: <span style="color: #8f0075;">str</span>
    items: <span style="color: #8f0075;">list</span><span style="color: #000000;">[</span>OrderItem<span style="color: #000000;">]</span>
    <span style="color: #005e8b;">discount_type</span>: <span style="color: #005f5f;">Optional</span><span style="color: #000000;">[</span><span style="color: #005f5f;">DiscountType</span><span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #0000b0;">None</span>
    <span style="color: #005e8b;">discount_value</span>: <span style="color: #005f5f;">Optional</span><span style="color: #000000;">[</span><span style="color: #8f0075;">float</span><span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #0000b0;">None</span>
    created_at: datetime
    <span style="color: #005e8b;">total</span>: <span style="color: #8f0075;">float</span> <span style="color: #000000;">=</span> Field<span style="color: #000000;">(</span>ge<span style="color: #000000;">=</span>0<span style="color: #000000;">)</span>
</pre>
</div>

<p>
In just 30 lines of code, Pydantic gives you type checking, validation, JSON serialization, OpenAPI schema generation, and IDE autocompletion.  That's <i>genuinely impressive</i>.
</p>
</div>
</div>
<div id="outline-container-the-portability-problem" class="outline-3">
<h3 id="the-portability-problem"><span class="section-number-3">3.2.</span> The Portability Problem</h3>
<div class="outline-text-3" id="text-the-portability-problem">
<p>
The trouble starts when your Order needs to cross a language boundary:
</p>

<div class="org-src-container">
<pre class="src src-python"><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">Python team: "Here's our schema"
</span><span style="color: #005e8b;">schema</span> <span style="color: #000000;">=</span> Order.model_json_schema<span style="color: #000000;">()</span>
</pre>
</div>

<div class="org-src-container">
<pre class="src src-json"><span style="color: #000000;">{</span>
  <span style="color: #531ab6;">"$defs"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"DiscountType"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"enum"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"PERCENTAGE"</span>, <span style="color: #3548cf;">"FIXED"</span>, <span style="color: #3548cf;">"BOGO"</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"OrderItem"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"properties"</span>: <span style="color: #972500;">{</span>
        <span style="color: #531ab6;">"product_id"</span>: <span style="color: #808000;">{</span><span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span><span style="color: #808000;">}</span>,
        <span style="color: #531ab6;">"quantity"</span>: <span style="color: #808000;">{</span><span style="color: #531ab6;">"minimum"</span>: 1, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"integer"</span><span style="color: #808000;">}</span>,
        <span style="color: #531ab6;">"unit_price"</span>: <span style="color: #808000;">{</span><span style="color: #531ab6;">"exclusiveMinimum"</span>: 0, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"number"</span><span style="color: #808000;">}</span>
      <span style="color: #972500;">}</span>,
      <span style="color: #531ab6;">"required"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"product_id"</span>, <span style="color: #3548cf;">"quantity"</span>, <span style="color: #3548cf;">"unit_price"</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"object"</span>
    <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"properties"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"id"</span>: <span style="color: #008899;">{</span><span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span><span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"customer_id"</span>: <span style="color: #008899;">{</span><span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span><span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"items"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"items"</span>: <span style="color: #972500;">{</span><span style="color: #531ab6;">"$ref"</span>: <span style="color: #3548cf;">"#/$defs/OrderItem"</span><span style="color: #972500;">}</span>,
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"array"</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"discount_type"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"anyOf"</span>: <span style="color: #972500;">[</span><span style="color: #808000;">{</span><span style="color: #531ab6;">"$ref"</span>: <span style="color: #3548cf;">"#/$defs/DiscountType"</span><span style="color: #808000;">}</span>, <span style="color: #808000;">{</span><span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"null"</span><span style="color: #808000;">}</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"default"</span>: <span style="color: #0000b0;">null</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"total"</span>: <span style="color: #008899;">{</span><span style="color: #531ab6;">"minimum"</span>: 0, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"number"</span><span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"required"</span>: <span style="color: #dd22dd;">[</span><span style="color: #3548cf;">"id"</span>, <span style="color: #3548cf;">"customer_id"</span>, <span style="color: #3548cf;">"items"</span>, <span style="color: #3548cf;">"created_at"</span>, <span style="color: #3548cf;">"total"</span><span style="color: #dd22dd;">]</span>,
  <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"object"</span>
<span style="color: #000000;">}</span>
</pre>
</div>

<p>
Now hand this JSON Schema to your Go team and tell them to implement it.  Several things go wrong:
</p>

<ol class="org-ol">
<li><b>The <code>@field_validator</code> is lost.</b>  The "suspicious quantity" check exists only in Python.  The Go team doesn't know about it unless someone writes a Confluence page.</li>
<li><b><code>datetime</code> serialization is ambiguous.</b>  Is it ISO 8601?  Unix timestamp?  Python's default?  The JSON Schema says <code>string</code> with no format hint because Pydantic's JSON Schema export doesn't always capture datetime formatting.</li>
<li><b>Optionality semantics differ.</b>  Python's <code>Optional[DiscountType] = None</code> means "the field can be absent or null."  Go's <code>*DiscountType</code> means "the field can be nil" but JSON encoding/decoding may handle zero-values differently.</li>
<li><b>The source of truth is Python code.</b>  If the Go team needs to add a field, they can't modify the Python model directly &#x2013; they file a ticket with the Python team and wait.</li>
</ol>
</div>
</div>
<div id="outline-container-the-validator-trap" class="outline-3">
<h3 id="the-validator-trap"><span class="section-number-3">3.3.</span> The Validator Trap</h3>
<div class="outline-text-3" id="text-the-validator-trap">
<p>
The deeper issue is what I call the <i>Validator Trap</i>: confusing validation logic (which is inherently language-specific) with schema definition (which should be language-agnostic).
</p>

<p>
Pydantic elegantly combines both concerns in a single class.  This is a <i>feature</i> within a Python codebase and an <i>anti-pattern</i> across a polyglot system:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Concern</th>
<th scope="col" class="org-left">Language-Specific?</th>
<th scope="col" class="org-left">Example</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Field types and structure</td>
<td class="org-left">No — this should be in the schema</td>
<td class="org-left"><code>quantity: int</code></td>
</tr>

<tr>
<td class="org-left">Field constraints (ranges, patterns)</td>
<td class="org-left">Partially — basic constraints are portable</td>
<td class="org-left"><code>Field(ge=1)</code></td>
</tr>

<tr>
<td class="org-left">Custom validation logic</td>
<td class="org-left">Yes — inherently language-specific</td>
<td class="org-left"><code>@field_validator</code></td>
</tr>

<tr>
<td class="org-left">Serialization format</td>
<td class="org-left">No — should be schema-defined</td>
<td class="org-left">JSON, MessagePack, etc.</td>
</tr>

<tr>
<td class="org-left">Default values</td>
<td class="org-left">No — should be in the schema</td>
<td class="org-left"><code>= None</code></td>
</tr>
</tbody>
</table>

<p>
When you use Pydantic <i>as your schema language</i>, you're coupling your schema definition to Python.  Every other language needs to reverse-engineer the schema from Pydantic's JSON Schema export, losing validation logic in the process.
</p>

<p>
When you use Pydantic <i>as a code-generation target</i> &#x2013; generating Pydantic models from a language-agnostic schema &#x2013; you get the best of both worlds.  The schema is portable, and you can add Python-specific validation on top of the generated base class.  Gotta love wrappers :o)
</p>
</div>
</div>
<div id="outline-container-the-right-role-for-pydantic" class="outline-3">
<h3 id="the-right-role-for-pydantic"><span class="section-number-3">3.4.</span> The Right Role for Pydantic</h3>
<div class="outline-text-3" id="text-the-right-role-for-pydantic">
<p>
Pydantic is best understood as a <i>consumer</i> of schemas, not a <i>source</i> of schemas:
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">graph</span> <span style="color: #0000b0;">LR</span>
    PROTO<span style="color: #000000;">[</span><span style="color: #3548cf;">"order.proto"</span><span style="color: #000000;">]</span> <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"buf generate"</span>| PY_BASE<span style="color: #000000;">[</span><span style="color: #3548cf;">"order_pb2.py&lt;br/&gt;(generated)"</span><span style="color: #000000;">]</span>
    PY_BASE <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"wrap"</span>| PYDANTIC<span style="color: #000000;">[</span><span style="color: #3548cf;">"OrderModel(BaseModel)&lt;br/&gt;+ @field_validator&lt;br/&gt;+ custom logic"</span><span style="color: #000000;">]</span>
    PROTO <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"buf generate"</span>| GO<span style="color: #000000;">[</span><span style="color: #3548cf;">"order.pb.go&lt;br/&gt;(generated)"</span><span style="color: #000000;">]</span>
    PROTO <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"buf generate"</span>| TS<span style="color: #000000;">[</span><span style="color: #3548cf;">"order.ts&lt;br/&gt;(generated)"</span><span style="color: #000000;">]</span>
</pre>
</div>

<p>
In this architecture:
</p>
<ul class="org-ul">
<li>The <code>.proto</code> file is the single source of truth for structure and basic types</li>
<li>Generated Python code provides the base types</li>
<li>Pydantic wraps those types with Python-specific validation</li>
<li>Go and TypeScript get their own generated code with language-idiomatic validation</li>
</ul>

<p>
<b>This isn't anti-Pydantic &#x2013; it's pro-separation-of-concerns</b>.  Use schema languages for <i>what</i> the data looks like.  Use language-specific tools for <i>how</i> to validate it.
</p>
</div>
</div>
<div id="outline-container-what-about-datamodel-code-generator-" class="outline-3">
<h3 id="what-about-datamodel-code-generator-"><span class="section-number-3">3.5.</span> What About datamodel-code-generator?</h3>
<div class="outline-text-3" id="text-what-about-datamodel-code-generator-">
<p>
There's a popular tool called <a href="https://github.com/koxudaxi/datamodel-code-generator">datamodel-code-generator</a> that generates Pydantic models from JSON Schema, OpenAPI, and other schema formats.  In my opinion, this is exactly the right pattern &#x2013; schema language as source of truth, Pydantic as consumer.
</p>

<div class="org-src-container">
<pre class="src src-bash"><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">Generate Pydantic models from JSON Schema
</span>datamodel-codegen <span style="color: #3548cf;">\</span>
  --input order.schema.json <span style="color: #3548cf;">\</span>
  --output models.py <span style="color: #3548cf;">\</span>
  --output-model-type pydantic_v2.BaseModel
</pre>
</div>

<p>
The generated code is clean, type-safe, and stays in sync with the schema.  You can extend the generated classes with custom validators.  This is the pattern I recommend for Python teams in polyglot environments.
</p>

<p>
The same principle applies to every language-specific modelling library: Zod (TypeScript), serde (Rust), Jackson (Java).  They're all excellent <i>consumers</i> of schemas.  None of them should be the <i>source</i>.
</p>
</div>
</div>
</div>
<div id="outline-container-the-landscape-of-schema-languages" class="outline-2">
<h2 id="the-landscape-of-schema-languages"><span class="section-number-2">4.</span> The Landscape of Schema Languages&#xa0;&#xa0;&#xa0;<span class="tag"><span class="standards">standards</span>&#xa0;<span class="taxonomy">taxonomy</span></span></h2>
<div class="outline-text-2" id="text-the-landscape-of-schema-languages">
<p>
Before diving deep into the three dominant schema languages, let's survey the broader landscape.  The world of schema and data modelling languages is richer than most developers realize.
</p>
</div>
<div id="outline-container-taxonomy" class="outline-3">
<h3 id="taxonomy"><span class="section-number-3">4.1.</span> Taxonomy</h3>
<div class="outline-text-3" id="text-taxonomy">
<p>
Schema languages can be categorized along several axes:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Category</th>
<th scope="col" class="org-left">Languages</th>
<th scope="col" class="org-left">Primary Use Case</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Binary Serialization</b></td>
<td class="org-left">Protocol Buffers, Apache Thrift, Cap'n Proto, FlatBuffers, MessagePack</td>
<td class="org-left">High-performance RPC, storage</td>
</tr>

<tr>
<td class="org-left"><b>Schema-Encoded Serialization</b></td>
<td class="org-left">Apache Avro, Apache Parquet (schema)</td>
<td class="org-left">Event streaming, data lake storage</td>
</tr>

<tr>
<td class="org-left"><b>Validation-Oriented</b></td>
<td class="org-left">JSON Schema, XML Schema (XSD), RELAX NG</td>
<td class="org-left">API validation, document validation</td>
</tr>

<tr>
<td class="org-left"><b>API Definition</b></td>
<td class="org-left">OpenAPI/Swagger, GraphQL SDL, gRPC (via Protobuf), AsyncAPI</td>
<td class="org-left">HTTP APIs, event-driven APIs</td>
</tr>

<tr>
<td class="org-left"><b>Data Description</b></td>
<td class="org-left">ASN.1, CDDL, Ion Schema</td>
<td class="org-left">Telecom, IoT, databases</td>
</tr>

<tr>
<td class="org-left"><b>Type System</b></td>
<td class="org-left">TypeScript (types), Zod, io-ts, Pydantic, Marshmallow</td>
<td class="org-left">Language-specific validation</td>
</tr>

<tr>
<td class="org-left"><b>Database Schema</b></td>
<td class="org-left">SQL DDL, Prisma, Drizzle</td>
<td class="org-left">Database modelling</td>
</tr>

<tr>
<td class="org-left"><b>Configuration</b></td>
<td class="org-left">CUE, Dhall, Jsonnet, KCL</td>
<td class="org-left">Configuration validation</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-the-landscape-visualized" class="outline-3">
<h3 id="the-landscape-visualized"><span class="section-number-3">4.2.</span> The Landscape Visualized</h3>
<div class="outline-text-3" id="text-the-landscape-visualized">
<p>
Plotting these languages across time and adoption reveals the waves of innovation:
</p>

<div id="sl_landscape" class="plotly-plot"></div>

<p>
The bubble chart reveals several patterns.  The binary serialization cluster (Protobuf, Thrift, FlatBuffers, Cap'n Proto) spans 2007-2014 and reflects a period of intense experimentation in high-performance serialization driven by the needs of companies like Google, Facebook, and game studios.  The validation cluster (JSON Schema, OpenAPI) emerged later but achieved massive adoption because they target the ubiquitous HTTP/JSON ecosystem rather than specialized binary formats.  The type system explosion (TypeScript, Zod, Pydantic) is the most recent wave, driven by developers wanting schema-like guarantees within their language of choice.
</p>

<p>
Note that TypeScript Types appears as an outlier at 101k stars &#x2013; this reflects TypeScript's enormous adoption as a <i>language</i>, not as a schema language per se.  It's included because TypeScript's type system is frequently used as a de facto schema definition within TypeScript-only codebases, but it isn't portable across languages the way Protobuf, Avro, or JSON Schema are.
</p>
</div>
</div>
<div id="outline-container-a-closer-look-at-the-categories" class="outline-3">
<h3 id="a-closer-look-at-the-categories"><span class="section-number-3">4.3.</span> A Closer Look at the Categories</h3>
<div class="outline-text-3" id="text-a-closer-look-at-the-categories">
</div>
<div id="outline-container-binary-serialization-languages" class="outline-4">
<h4 id="binary-serialization-languages"><span class="section-number-4">4.3.1.</span> Binary Serialization Languages</h4>
<div class="outline-text-4" id="text-binary-serialization-languages">
<p>
These languages define both the schema and a compact binary wire format.  They prioritize serialization speed and small message sizes.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Language</th>
<th scope="col" class="org-left">Creator</th>
<th scope="col" class="org-left">Wire Format</th>
<th scope="col" class="org-left">Schema in Payload?</th>
<th scope="col" class="org-left">Code Gen</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Protocol Buffers</td>
<td class="org-left">Google</td>
<td class="org-left">Custom binary</td>
<td class="org-left">No</td>
<td class="org-left">Yes (protoc)</td>
</tr>

<tr>
<td class="org-left">Thrift</td>
<td class="org-left">Facebook → Apache</td>
<td class="org-left">Custom binary</td>
<td class="org-left">No</td>
<td class="org-left">Yes (thrift)</td>
</tr>

<tr>
<td class="org-left">Cap'n Proto</td>
<td class="org-left">Sandstorm</td>
<td class="org-left">Zero-copy binary</td>
<td class="org-left">No</td>
<td class="org-left">Yes (capnpc)</td>
</tr>

<tr>
<td class="org-left">FlatBuffers</td>
<td class="org-left">Google</td>
<td class="org-left">Zero-copy binary</td>
<td class="org-left">No</td>
<td class="org-left">Yes (flatc)</td>
</tr>
</tbody>
</table>

<p>
<b>Key insight:</b> These formats do <i>not</i> include the schema in the serialized payload.  Both sender and receiver must have the schema at compile time.  This makes them fast but inflexible &#x2013; you can't deserialize a message without knowing which schema produced it.
</p>
</div>
</div>
<div id="outline-container-schema-encoded-serialization" class="outline-4">
<h4 id="schema-encoded-serialization"><span class="section-number-4">4.3.2.</span> Schema-Encoded Serialization</h4>
<div class="outline-text-4" id="text-schema-encoded-serialization">
<p>
Avro takes a fundamentally different approach: the writer's schema is transmitted alongside (or registered separately from) the data.  This enables <i>schema resolution</i> &#x2013; the reader can use a different (compatible) schema than the writer.
</p>

<p>
This is the key architectural difference between Avro and Protobuf, and it explains why Avro dominates in data engineering (where schemas evolve frequently and consumers may lag behind producers) while Protobuf dominates in RPC (where both sides are typically deployed in lockstep).
</p>
</div>
</div>
<div id="outline-container-validation-oriented-languages" class="outline-4">
<h4 id="validation-oriented-languages"><span class="section-number-4">4.3.3.</span> Validation-Oriented Languages</h4>
<div class="outline-text-4" id="text-validation-oriented-languages">
<p>
JSON Schema and its descendants focus on validating data that's already in a text format (JSON, YAML).  They don't define a binary wire format &#x2013; they describe what valid JSON looks like.  This makes them ideal for HTTP APIs where JSON is the transport format, but less suitable for high-throughput binary protocols.
</p>
</div>
</div>
<div id="outline-container-api-definition-languages" class="outline-4">
<h4 id="api-definition-languages"><span class="section-number-4">4.3.4.</span> API Definition Languages</h4>
<div class="outline-text-4" id="text-api-definition-languages">
<p>
OpenAPI, GraphQL SDL, and AsyncAPI are higher-level &#x2013; they define not just the data models but also the operations, endpoints, and protocols.  They typically embed or reference a schema language for the data modelling portions.
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Language</th>
<th scope="col" class="org-left">Transport</th>
<th scope="col" class="org-left">Schema For Data</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">OpenAPI</td>
<td class="org-left">HTTP/REST</td>
<td class="org-left">JSON Schema (subset)</td>
</tr>

<tr>
<td class="org-left">GraphQL SDL</td>
<td class="org-left">HTTP/GraphQL</td>
<td class="org-left">Its own type system</td>
</tr>

<tr>
<td class="org-left">gRPC</td>
<td class="org-left">HTTP/2</td>
<td class="org-left">Protocol Buffers</td>
</tr>

<tr>
<td class="org-left">AsyncAPI</td>
<td class="org-left">Message queues</td>
<td class="org-left">JSON Schema (subset)</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div id="outline-container-the-big-three" class="outline-3">
<h3 id="the-big-three"><span class="section-number-3">4.4.</span> The Big Three</h3>
<div class="outline-text-3" id="text-the-big-three">
<p>
For the remainder of this article, we'll focus on the three schema languages that have achieved the broadest adoption and deepest ecosystem integration:
</p>

<ol class="org-ol">
<li><b>Protocol Buffers (Protobuf)</b> &#x2013; Google's schema language for high-performance RPC</li>
<li><b>Apache Avro</b> &#x2013; The Hadoop/Kafka ecosystem's schema language for data engineering</li>
<li><b>JSON Schema</b> &#x2013; The web's schema language for API validation</li>
</ol>

<p>
These three represent distinct design philosophies, serve different primary use cases, and have evolved in fascinatingly different directions.  Understanding their differences deeply will equip you to make the right choice for your system.
</p>

<p>
A note on what's <i>not</i> included: GraphQL SDL is conspicuously absent.  While GraphQL has massive adoption, it's an <i>API definition language</i> &#x2013; it defines operations, endpoints, and query semantics, not just data models.  Its type system is tightly coupled to the GraphQL query execution model, making it less useful as a general-purpose schema language for serialization, storage, or cross-protocol data modelling.  GraphQL schemas describe how to <i>query</i> data; Protobuf, Avro, and JSON Schema describe how to <i>model</i> it.
</p>
</div>
</div>
</div>
<div id="outline-container-protobuf-deep-dive" class="outline-2">
<h2 id="protobuf-deep-dive"><span class="section-number-2">5.</span> Protobuf Deep Dive&#xa0;&#xa0;&#xa0;<span class="tag"><span class="protobuf">protobuf</span>&#xa0;<span class="google">google</span>&#xa0;<span class="grpc">grpc</span></span></h2>
<div class="outline-text-2" id="text-protobuf-deep-dive">
</div>
<div id="outline-container-history-and-origin" class="outline-3">
<h3 id="history-and-origin"><span class="section-number-3">5.1.</span> History and Origin</h3>
<div class="outline-text-3" id="text-history-and-origin">
<p>
Protocol Buffers were developed at Google in 2001 by Jeff Dean, Sanjay Ghemawat, and others<sup><a id="fnr.3" class="footref" href="https://www.chiply.dev/#fn.3" role="doc-backlink">3</a></sup>.  The problem was characteristically Googley: Google's internal RPC system needed a way to define service interfaces that could be compiled into efficient serialization code across dozens of languages, while supporting backward-compatible schema evolution across thousands of microservices.
</p>

<p>
The original internal version (proto1) was never open-sourced.  Proto2 was released publicly in July 2008.  Proto3, a significant simplification, arrived in 2016.  Today, Protobuf is the foundation of gRPC, Google's open-source RPC framework, and is used by companies including Uber, Netflix, Square, Lyft, and Dropbox.
</p>

<p>
An important context: Google internally has a <i>monorepo</i> with tens of thousands of <code>.proto</code> files.  The Protobuf ecosystem was designed for this scale.  Features that seem over-engineered for a 10-person startup make perfect sense when you're managing schema evolution across 25,000+ engineers.
</p>
</div>
</div>
<div id="outline-container-how-protobuf-works" class="outline-3">
<h3 id="how-protobuf-works"><span class="section-number-3">5.2.</span> How Protobuf Works</h3>
<div class="outline-text-3" id="text-how-protobuf-works">
<p>
Protobuf uses an Interface Definition Language (IDL) to describe message types:
</p>

<div class="org-src-container">
<pre class="src src-protobuf">syntax = "proto3";

package ecommerce.v1;

import "google/protobuf/timestamp.proto";

enum DiscountType {
  DISCOUNT_TYPE_UNSPECIFIED = 0;
  DISCOUNT_TYPE_PERCENTAGE = 1;
  DISCOUNT_TYPE_FIXED = 2;
  DISCOUNT_TYPE_BOGO = 3;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double unit_price = 3;
}

message Order {
  string id = 1;
  string customer_id = 2;
  repeated OrderItem items = 3;
  optional DiscountType discount_type = 4;
  optional double discount_value = 5;
  google.protobuf.Timestamp created_at = 6;
  double total = 7;
}
</pre>
</div>

<p>
The numbers (<code>1</code>, <code>2</code>, <code>3</code>, &#x2026;) are <i>field tags</i> &#x2013; they identify each field in the binary encoding.  This is a critical design choice.  Unlike JSON (where fields are identified by string names), Protobuf identifies fields by integer tags.  This means:
</p>

<ul class="org-ul">
<li>Renaming a field is a non-breaking change (the tag stays the same)</li>
<li>Binary messages are compact (an integer tag is 1-2 bytes vs. a string name that could be dozens)</li>
<li>Field ordering in the <code>.proto</code> file doesn't matter &#x2013; only the tags matter</li>
</ul>
</div>
</div>
<div id="outline-container-the-compilation-pipeline" class="outline-3">
<h3 id="the-compilation-pipeline"><span class="section-number-3">5.3.</span> The Compilation Pipeline</h3>
<div class="outline-text-3" id="text-the-compilation-pipeline">
<p>
Protobuf's compilation model is one of its most distinctive features:
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">graph</span> <span style="color: #0000b0;">LR</span>
    PROTO<span style="color: #000000;">[</span><span style="color: #3548cf;">".proto files"</span><span style="color: #000000;">]</span> <span style="color: #721045;">--&gt;</span> PROTOC<span style="color: #000000;">[</span><span style="color: #3548cf;">"protoc&lt;br/&gt;(compiler)"</span><span style="color: #000000;">]</span>

    PROTOC <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"--go_out"</span>| GO<span style="color: #000000;">[</span><span style="color: #3548cf;">"Go structs&lt;br/&gt;+ marshal/unmarshal"</span><span style="color: #000000;">]</span>
    PROTOC <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"--python_out"</span>| PY<span style="color: #000000;">[</span><span style="color: #3548cf;">"Python classes&lt;br/&gt;+ serialization"</span><span style="color: #000000;">]</span>
    PROTOC <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"--java_out"</span>| JAVA<span style="color: #000000;">[</span><span style="color: #3548cf;">"Java classes&lt;br/&gt;+ builders"</span><span style="color: #000000;">]</span>
    PROTOC <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"--ts_out"</span>| TS<span style="color: #000000;">[</span><span style="color: #3548cf;">"TypeScript&lt;br/&gt;interfaces + codec"</span><span style="color: #000000;">]</span>
    PROTOC <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"--swift_out"</span>| SWIFT<span style="color: #000000;">[</span><span style="color: #3548cf;">"Swift structs&lt;br/&gt;+ Codable"</span><span style="color: #000000;">]</span>

    style PROTO fill:<span style="color: #000000; background-color: #2BCDC1;">#2BCDC1</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
    style PROTOC fill:<span style="color: #000000; background-color: #FFB347;">#FFB347</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
</pre>
</div>

<p>
The <code>protoc</code> compiler reads <code>.proto</code> files and generates language-specific code via plugins.  Each plugin produces idiomatic code for its target language &#x2013; Go gets structs with exported fields, Java gets classes with builders, Python gets classes with descriptors.
</p>

<p>
This is the <i>single source of truth</i> in action.  One <code>.proto</code> file generates code for every language your system uses.  The generated code is checked into version control (or generated in CI), and every team consumes the same schema.
</p>
</div>
</div>
<div id="outline-container-proto2-vs-proto3" class="outline-3">
<h3 id="proto2-vs-proto3"><span class="section-number-3">5.4.</span> Proto2 vs Proto3</h3>
<div class="outline-text-3" id="text-proto2-vs-proto3">
<p>
The transition from proto2 to proto3 was contentious.  Proto3 made several simplifications that sparked debate:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Feature</th>
<th scope="col" class="org-left">Proto2</th>
<th scope="col" class="org-left">Proto3</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Required fields</td>
<td class="org-left"><code>required</code> keyword</td>
<td class="org-left">Removed (all fields are implicitly optional)</td>
</tr>

<tr>
<td class="org-left">Default values</td>
<td class="org-left">User-specified</td>
<td class="org-left">Type default (0, "", false, empty)</td>
</tr>

<tr>
<td class="org-left">Field presence</td>
<td class="org-left">Always tracked</td>
<td class="org-left">Only tracked for <code>message</code> fields and <code>optional</code></td>
</tr>

<tr>
<td class="org-left">Unknown fields</td>
<td class="org-left">Preserved</td>
<td class="org-left">Preserved (changed in 3.5; originally discarded)</td>
</tr>

<tr>
<td class="org-left">Enums</td>
<td class="org-left">No zero-value requirement</td>
<td class="org-left">Must have zero value (= UNSPECIFIED)</td>
</tr>
</tbody>
</table>

<p>
The removal of <code>required</code> was the most controversial change.  Google learned the hard way that <code>required</code> fields are <i>forever</i> &#x2013; once you deploy a <code>required</code> field, you can never remove it without breaking all existing consumers.  Proto3's philosophy is "every field should be optional at the wire level; enforce business requirements in application code."
</p>

<p>
This is a pragmatic stance but it does create ambiguity.  If a proto3 <code>int32</code> field has value <code>0</code>, is it explicitly set to zero, or was it absent from the message?  Proto3 can't tell you &#x2013; unless you mark the field <code>optional</code> (added back in proto3 syntax in 3.15) or wrap it in a <code>google.protobuf.Int32Value</code> wrapper.
</p>
</div>
</div>
<div id="outline-container-the-buf-ecosystem" class="outline-3">
<h3 id="the-buf-ecosystem"><span class="section-number-3">5.5.</span> The Buf Ecosystem</h3>
<div class="outline-text-3" id="text-the-buf-ecosystem">
<p>
The rough edges of raw <code>protoc</code> led to the creation of <a href="https://buf.build/">Buf</a> &#x2013; a modern toolchain for Protocol Buffers.  Buf provides:
</p>

<ul class="org-ul">
<li><b>Linting</b>: Enforce style rules (e.g., field names must be snake_case, enums must have UNSPECIFIED zero value)</li>
<li><b>Breaking change detection</b>: CI-integrated checks that fail if a schema change would break existing consumers</li>
<li><b>Code generation</b>: A managed plugin ecosystem that replaces the fragile <code>protoc</code> plugin chain</li>
<li><b>Schema registry</b>: The Buf Schema Registry (BSR) &#x2013; a hosted registry for sharing <code>.proto</code> files</li>
</ul>

<div class="org-src-container">
<pre class="src src-yaml"><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">buf.yaml - Project configuration
</span><span style="color: #005e8b;">version</span>: v2
<span style="color: #005e8b;">modules</span>:
  - <span style="color: #005e8b;">path</span>: proto
    <span style="color: #005e8b;">name</span>: buf.build/myorg/ecommerce
<span style="color: #005e8b;">lint</span>:
  <span style="color: #005e8b;">use</span>:
    - STANDARD
<span style="color: #005e8b;">breaking</span>:
  <span style="color: #005e8b;">use</span>:
    - FILE
</pre>
</div>

<div class="org-src-container">
<pre class="src src-yaml"><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">buf.gen.yaml - Code generation configuration
</span><span style="color: #005e8b;">version</span>: v2
<span style="color: #005e8b;">plugins</span>:
  - <span style="color: #005e8b;">remote</span>: buf.build/protocolbuffers/go
    <span style="color: #005e8b;">out</span>: gen/go
    <span style="color: #005e8b;">opt</span>: paths=source_relative
  - <span style="color: #005e8b;">remote</span>: buf.build/protocolbuffers/python
    <span style="color: #005e8b;">out</span>: gen/python
  - <span style="color: #005e8b;">remote</span>: buf.build/bufbuild/es
    <span style="color: #005e8b;">out</span>: gen/typescript
</pre>
</div>

<p>
Buf's breaking change detection is particularly valuable.  It compares the current schema against a previous version (from the BSR or a git reference) and flags changes that would break wire compatibility:
</p>

<div class="org-src-container">
<pre class="src src-bash">$ buf breaking --against <span style="color: #3548cf;">'buf.build/myorg/ecommerce'</span>
proto/order.proto:15:3:Field <span style="color: #3548cf;">"4"</span> on message <span style="color: #3548cf;">"Order"</span> changed type from <span style="color: #3548cf;">"string"</span> to <span style="color: #3548cf;">"int32"</span>.
proto/order.proto:22:1:Previously present field <span style="color: #3548cf;">"7"</span> with name <span style="color: #3548cf;">"total"</span> on message <span style="color: #3548cf;">"Order"</span> was deleted.
</pre>
</div>
</div>
</div>
<div id="outline-container-protobuf-strengths-and-weaknesses" class="outline-3">
<h3 id="protobuf-strengths-and-weaknesses"><span class="section-number-3">5.6.</span> Protobuf Strengths and Weaknesses</h3>
<div class="outline-text-3" id="text-protobuf-strengths-and-weaknesses">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Strengths</th>
<th scope="col" class="org-left">Weaknesses</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Excellent performance (small messages, fast ser/de)</td>
<td class="org-left">No self-describing messages (need schema to decode)</td>
</tr>

<tr>
<td class="org-left">Mature ecosystem (20+ years, Google-backed)</td>
<td class="org-left">Proto3 loses field presence by default</td>
</tr>

<tr>
<td class="org-left">Backward and forward compatible evolution</td>
<td class="org-left">Human-unreadable binary format</td>
</tr>

<tr>
<td class="org-left">First-class gRPC integration</td>
<td class="org-left">No built-in validation constraints</td>
</tr>

<tr>
<td class="org-left">Strong code generation across 10+ languages</td>
<td class="org-left">Steep learning curve for schema design</td>
</tr>

<tr>
<td class="org-left">Buf ecosystem modernizes the toolchain</td>
<td class="org-left">No union types (<code>oneof</code> is limited)</td>
</tr>
</tbody>
</table>

<p>
Protobuf's sweet spot is <b>high-performance RPC between services you control</b>.  When both the producer and consumer are deployed from the same CI pipeline, Protobuf's compile-time schema agreement is a superpower.  When consumers lag behind producers (as in data pipelines), Protobuf's lack of self-describing messages becomes a liability.
</p>
</div>
</div>
</div>
<div id="outline-container-avro-deep-dive" class="outline-2">
<h2 id="avro-deep-dive"><span class="section-number-2">6.</span> Avro Deep Dive&#xa0;&#xa0;&#xa0;<span class="tag"><span class="avro">avro</span>&#xa0;<span class="hadoop">hadoop</span>&#xa0;<span class="kafka">kafka</span></span></h2>
<div class="outline-text-2" id="text-avro-deep-dive">
</div>
<div id="outline-container-history-and-origin" class="outline-3">
<h3 id="history-and-origin"><span class="section-number-3">6.1.</span> History and Origin</h3>
<div class="outline-text-3" id="text-history-and-origin">
<p>
Apache Avro was created by Doug Cutting (creator of Hadoop and Lucene) in 2009 as a serialization system for the Hadoop ecosystem<sup><a id="fnr.4" class="footref" href="https://www.chiply.dev/#fn.4" role="doc-backlink">4</a></sup>.  The motivation was specific: Hadoop's existing serialization system (Writables) was Java-only and had no schema evolution support.  Thrift and Protobuf existed but required code generation, which Cutting considered an unnecessary coupling between the schema and the processing code.
</p>

<p>
Avro's key design insight was that the schema should travel <i>with</i> the data, enabling dynamic typing and schema evolution without code generation.  This was radical at the time and is the fundamental architectural difference between Avro and Protobuf.
</p>

<p>
Avro became the de facto serialization format for the Hadoop ecosystem and, later, for Apache Kafka.  Confluent's Schema Registry &#x2013; arguably the most widely used schema registry in the world &#x2013; was built specifically for Avro before adding Protobuf and JSON Schema support.
</p>
</div>
</div>
<div id="outline-container-how-avro-works" class="outline-3">
<h3 id="how-avro-works"><span class="section-number-3">6.2.</span> How Avro Works</h3>
<div class="outline-text-3" id="text-how-avro-works">
<p>
Avro schemas are defined in JSON (or the more readable Avro IDL):
</p>

<div class="org-src-container">
<pre class="src src-json"><span style="color: #000000;">{</span>
  <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"record"</span>,
  <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"Order"</span>,
  <span style="color: #531ab6;">"namespace"</span>: <span style="color: #3548cf;">"com.ecommerce"</span>,
  <span style="color: #531ab6;">"fields"</span>: <span style="color: #dd22dd;">[</span>
    <span style="color: #008899;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"id"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span><span style="color: #008899;">}</span>,
    <span style="color: #008899;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"customer_id"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span><span style="color: #008899;">}</span>,
    <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"items"</span>,
      <span style="color: #531ab6;">"type"</span>: <span style="color: #972500;">{</span>
        <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"array"</span>,
        <span style="color: #531ab6;">"items"</span>: <span style="color: #808000;">{</span>
          <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"record"</span>,
          <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"OrderItem"</span>,
          <span style="color: #531ab6;">"fields"</span>: <span style="color: #531ab6;">[</span>
            <span style="color: #008900;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"product_id"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span><span style="color: #008900;">}</span>,
            <span style="color: #008900;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"quantity"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"int"</span><span style="color: #008900;">}</span>,
            <span style="color: #008900;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"unit_price"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"double"</span><span style="color: #008900;">}</span>
          <span style="color: #531ab6;">]</span>
        <span style="color: #808000;">}</span>
      <span style="color: #972500;">}</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"discount_type"</span>,
      <span style="color: #531ab6;">"type"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"null"</span>, <span style="color: #808000;">{</span><span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"enum"</span>, <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"DiscountType"</span>,
                         <span style="color: #531ab6;">"symbols"</span>: <span style="color: #531ab6;">[</span><span style="color: #3548cf;">"PERCENTAGE"</span>, <span style="color: #3548cf;">"FIXED"</span>, <span style="color: #3548cf;">"BOGO"</span><span style="color: #531ab6;">]</span><span style="color: #808000;">}</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"default"</span>: <span style="color: #0000b0;">null</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"discount_value"</span>,
      <span style="color: #531ab6;">"type"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"null"</span>, <span style="color: #3548cf;">"double"</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"default"</span>: <span style="color: #0000b0;">null</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #008899;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"created_at"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #972500;">{</span><span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"long"</span>, <span style="color: #531ab6;">"logicalType"</span>: <span style="color: #3548cf;">"timestamp-millis"</span><span style="color: #972500;">}</span><span style="color: #008899;">}</span>,
    <span style="color: #008899;">{</span><span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"total"</span>, <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"double"</span><span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">]</span>
<span style="color: #000000;">}</span>
</pre>
</div>

<p>
Several things stand out compared to Protobuf:
</p>

<ol class="org-ol">
<li><b>Schemas are data (JSON)</b>, not a custom IDL.  This means schemas can be stored in databases, transmitted over HTTP, and processed by any JSON-capable tool.</li>
<li><b>Unions replace optionality.</b>  Instead of an <code>optional</code> keyword, Avro uses union types: <code>["null", "string"]</code> means "either null or a string."  This is more explicit but more verbose.</li>
<li><b>Field ordering matters.</b>  Unlike Protobuf (where field tags determine wire position), Avro encodes fields in the order they appear in the schema.  There are no field tags &#x2013; fields are identified by their position in the schema.</li>
<li><b>Default values are required for evolution.</b>  If you want to add a new field without breaking existing readers, it must have a default value.  This is enforced by the schema, not by convention.</li>
</ol>
</div>
</div>
<div id="outline-container-avro-idl--a-friendlier-syntax" class="outline-3">
<h3 id="avro-idl--a-friendlier-syntax"><span class="section-number-3">6.3.</span> Avro IDL: A Friendlier Syntax</h3>
<div class="outline-text-3" id="text-avro-idl--a-friendlier-syntax">
<p>
The JSON format is verbose, so Avro also supports an IDL that compiles to JSON:
</p>

<div class="org-src-container">
<pre class="src src-text">// Avro IDL
@namespace("com.ecommerce")
protocol EcommerceProtocol {

  enum DiscountType {
    PERCENTAGE, FIXED, BOGO
  }

  record OrderItem {
    string product_id;
    int quantity;
    double unit_price;
  }

  record Order {
    string id;
    string customer_id;
    array&lt;OrderItem&gt; items;
    union { null, DiscountType } discount_type = null;
    union { null, double } discount_value = null;
    @logicalType("timestamp-millis") long created_at;
    double total;
  }
}
</pre>
</div>

<p>
This looks much closer to Protobuf's IDL.  The key difference is that this IDL compiles to JSON, not to language-specific code.  Code generation is available but optional &#x2013; Avro can dynamically read any record if given the schema at runtime.
</p>
</div>
</div>
<div id="outline-container-writer-schema--reader-schema--and-resolution" class="outline-3">
<h3 id="writer-schema--reader-schema--and-resolution"><span class="section-number-3">6.4.</span> Writer Schema, Reader Schema, and Resolution</h3>
<div class="outline-text-3" id="text-writer-schema--reader-schema--and-resolution">
<p>
Avro's most powerful (and most confusing) feature is <i>schema resolution</i><sup><a id="fnr.5" class="footref" href="https://www.chiply.dev/#fn.5" role="doc-backlink">5</a></sup>.  When data is serialized, the writer's schema is recorded.  When data is deserialized, the reader provides its own schema.  Avro then <i>resolves</i> the two schemas, applying rules to handle differences:
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">sequenceDiagram</span>
    <span style="color: #0000b0;">participant</span> Writer as Writer <span style="color: #000000;">(</span>v2<span style="color: #000000;">)</span>
    <span style="color: #0000b0;">participant</span> Storage as Kafka / File
    <span style="color: #0000b0;">participant</span> Registry as Schema Registry
    <span style="color: #0000b0;">participant</span> Reader as Reader <span style="color: #000000;">(</span>v3<span style="color: #000000;">)</span>

    Writer<span style="color: #721045;">-&gt;&gt;</span>Registry: Register writer schema v2
    Registry<span style="color: #721045;">--&gt;&gt;</span>Writer: Schema ID: 42
    Writer<span style="color: #721045;">-&gt;&gt;</span>Storage: <span style="color: #000000;">[</span>ID:42<span style="color: #000000;">][</span>binary payload<span style="color: #000000;">]</span>

    Reader<span style="color: #721045;">-&gt;&gt;</span>Storage: Read message
    Storage<span style="color: #721045;">--&gt;&gt;</span>Reader: <span style="color: #000000;">[</span>ID:42<span style="color: #000000;">][</span>binary payload<span style="color: #000000;">]</span>
    Reader<span style="color: #721045;">-&gt;&gt;</span>Registry: Fetch schema ID 42
    Registry<span style="color: #721045;">--&gt;&gt;</span>Reader: Writer schema v2
    Reader<span style="color: #721045;">-&gt;&gt;</span>Reader: Resolve<span style="color: #000000;">(</span>writer=v2, reader=v3<span style="color: #000000;">)</span>
    Reader<span style="color: #721045;">-&gt;&gt;</span>Reader: Deserialize with resolution
</pre>
</div>

<p>
The resolution rules are:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Scenario</th>
<th scope="col" class="org-left">Rule</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Writer has field, reader has field</td>
<td class="org-left">Decode normally</td>
</tr>

<tr>
<td class="org-left">Writer has field, reader doesn't</td>
<td class="org-left">Ignore the field (forward compatibility)</td>
</tr>

<tr>
<td class="org-left">Reader has field, writer doesn't</td>
<td class="org-left">Use reader's default value (backward compatibility)</td>
</tr>

<tr>
<td class="org-left">Reader has field with no default, writer doesn't</td>
<td class="org-left"><b>Error</b> — incompatible schemas</td>
</tr>

<tr>
<td class="org-left">Type promotion</td>
<td class="org-left"><code>int</code> → <code>long</code>, <code>float</code> → <code>double</code> are allowed</td>
</tr>

<tr>
<td class="org-left">Enum evolution</td>
<td class="org-left">New symbols in reader: OK. New symbols in writer: decoded as default or error</td>
</tr>
</tbody>
</table>

<p>
This is profoundly different from Protobuf's approach.  Protobuf achieves compatibility by keeping field tags stable &#x2013; add new fields with new tags, never reuse old tags.  Avro achieves compatibility by comparing schemas at read time and applying resolution rules.  The trade-off:
</p>

<ul class="org-ul">
<li><b>Protobuf</b>: Simpler mental model, but can't detect incompatibilities until runtime</li>
<li><b>Avro + Schema Registry</b>: More complex, but incompatibilities are caught at registration time, before any data is written</li>
</ul>
</div>
</div>
<div id="outline-container-confluent-schema-registry" class="outline-3">
<h3 id="confluent-schema-registry"><span class="section-number-3">6.5.</span> Confluent Schema Registry</h3>
<div class="outline-text-3" id="text-confluent-schema-registry">
<p>
The <a href="https://docs.confluent.io/platform/current/schema-registry/">Confluent Schema Registry</a> is the operational backbone of Avro in production.  It stores schemas, assigns IDs, and enforces compatibility rules:
</p>

<div class="org-src-container">
<pre class="src src-bash"><span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">Register a new schema version
</span>curl -X POST <span style="color: #3548cf;">\</span>
  -H <span style="color: #3548cf;">"Content-Type: application/vnd.schemaregistry.v1+json"</span> <span style="color: #3548cf;">\</span>
  --data <span style="color: #3548cf;">'{"schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[...]}"}'</span> <span style="color: #3548cf;">\</span>
  http://localhost:8081/subjects/orders-value/versions

<span style="color: #595959;"># </span><span style="color: #7fff7fff7fff;">Check compatibility before registering
</span>curl -X POST <span style="color: #3548cf;">\</span>
  -H <span style="color: #3548cf;">"Content-Type: application/vnd.schemaregistry.v1+json"</span> <span style="color: #3548cf;">\</span>
  --data <span style="color: #3548cf;">'{"schema": "{...new version...}"}'</span> <span style="color: #3548cf;">\</span>
  http://localhost:8081/compatibility/subjects/orders-value/versions/latest
</pre>
</div>

<p>
Compatibility modes:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Mode</th>
<th scope="col" class="org-left">Rule</th>
<th scope="col" class="org-left">Use Case</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">BACKWARD</td>
<td class="org-left">New schema can read old data</td>
<td class="org-left">Consumers upgrade first</td>
</tr>

<tr>
<td class="org-left">FORWARD</td>
<td class="org-left">Old schema can read new data</td>
<td class="org-left">Producers upgrade first</td>
</tr>

<tr>
<td class="org-left">FULL</td>
<td class="org-left">Both backward and forward</td>
<td class="org-left">Any upgrade order</td>
</tr>

<tr>
<td class="org-left">NONE</td>
<td class="org-left">No compatibility checks</td>
<td class="org-left">Development only</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-avro-strengths-and-weaknesses" class="outline-3">
<h3 id="avro-strengths-and-weaknesses"><span class="section-number-3">6.6.</span> Avro Strengths and Weaknesses</h3>
<div class="outline-text-3" id="text-avro-strengths-and-weaknesses">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Strengths</th>
<th scope="col" class="org-left">Weaknesses</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Self-describing (schema travels with data)</td>
<td class="org-left">Slower than Protobuf (field resolution overhead)</td>
</tr>

<tr>
<td class="org-left">Schema resolution enables decoupled evolution</td>
<td class="org-left">JSON schema format is verbose</td>
</tr>

<tr>
<td class="org-left">Rich Kafka/Confluent ecosystem</td>
<td class="org-left">Weaker code generation than Protobuf</td>
</tr>

<tr>
<td class="org-left">Dynamic typing (no code gen required)</td>
<td class="org-left">Union syntax is awkward (<code>["null", "string"]</code>)</td>
</tr>

<tr>
<td class="org-left">Schema Registry catches incompatibilities early</td>
<td class="org-left">Less adoption outside JVM/Python ecosystems</td>
</tr>

<tr>
<td class="org-left">Compact binary format (smaller than JSON)</td>
<td class="org-left">Logical types are limited</td>
</tr>
</tbody>
</table>

<p>
Avro's sweet spot is <b>event streaming and data pipelines where producers and consumers evolve independently</b>.  When you have 50 Kafka consumers reading from the same topic, each potentially on a different schema version, Avro's schema resolution is essential.  For synchronous RPC between services deployed together, Protobuf is likely a better fit.
</p>
</div>
</div>
</div>
<div id="outline-container-json-schema-deep-dive" class="outline-2">
<h2 id="json-schema-deep-dive"><span class="section-number-2">7.</span> JSON Schema Deep Dive&#xa0;&#xa0;&#xa0;<span class="tag"><span class="jsonSchema">jsonSchema</span>&#xa0;<span class="validation">validation</span>&#xa0;<span class="web">web</span></span></h2>
<div class="outline-text-2" id="text-json-schema-deep-dive">
</div>
<div id="outline-container-history-and-origin" class="outline-3">
<h3 id="history-and-origin"><span class="section-number-3">7.1.</span> History and Origin</h3>
<div class="outline-text-3" id="text-history-and-origin">
<p>
JSON Schema began as a draft specification in 2009 by Kris Zyp<sup><a id="fnr.6" class="footref" href="https://www.chiply.dev/#fn.6" role="doc-backlink">6</a></sup>, inspired by XML Schema's ability to describe the structure of XML documents.  The goal was deceptively simple: provide a vocabulary for describing the structure and validation constraints of JSON data.
</p>

<p>
Unlike Protobuf and Avro, JSON Schema emerged from the web developer community rather than from big tech infrastructure teams.  It evolved through a series of IETF drafts, each adding features and refining semantics:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-right" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Draft</th>
<th scope="col" class="org-right">Year</th>
<th scope="col" class="org-left">Key Changes</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Draft-00 to Draft-03</td>
<td class="org-right">2009-2010</td>
<td class="org-left">Initial specification, basic types</td>
</tr>

<tr>
<td class="org-left">Draft-04</td>
<td class="org-right">2013</td>
<td class="org-left"><code>required</code> as array, <code>definitions</code>, <code>$ref</code></td>
</tr>

<tr>
<td class="org-left">Draft-06</td>
<td class="org-right">2017</td>
<td class="org-left"><code>const</code>, <code>contains</code>, <code>propertyNames</code>, boolean schemas</td>
</tr>

<tr>
<td class="org-left">Draft-07</td>
<td class="org-right">2018</td>
<td class="org-left"><code>if=/=then=/=else</code>, <code>readOnly=/=writeOnly</code>, string formats</td>
</tr>

<tr>
<td class="org-left">Draft 2019-09</td>
<td class="org-right">2019</td>
<td class="org-left">Renamed <code>$defs</code>, <code>unevaluatedProperties</code>, vocabulary system</td>
</tr>

<tr>
<td class="org-left">Draft 2020-12</td>
<td class="org-right">2020</td>
<td class="org-left"><code>prefixItems</code>, dynamic references, stable specification</td>
</tr>
</tbody>
</table>

<p>
The draft evolution reflects a tension between simplicity (JSON Schema should be easy) and expressiveness (JSON Schema should handle real-world schemas).  Draft-04 schemas are still widely encountered in the wild, meaning validators often need to support multiple draft versions simultaneously.
</p>
</div>
</div>
<div id="outline-container-how-json-schema-works" class="outline-3">
<h3 id="how-json-schema-works"><span class="section-number-3">7.2.</span> How JSON Schema Works</h3>
<div class="outline-text-3" id="text-how-json-schema-works">
<p>
JSON Schema describes what valid JSON looks like:
</p>

<div class="org-src-container">
<pre class="src src-json"><span style="color: #000000;">{</span>
  <span style="color: #531ab6;">"$schema"</span>: <span style="color: #3548cf;">"https://json-schema.org/draft/2020-12/schema"</span>,
  <span style="color: #531ab6;">"$id"</span>: <span style="color: #3548cf;">"https://ecommerce.example/schemas/order"</span>,
  <span style="color: #531ab6;">"title"</span>: <span style="color: #3548cf;">"Order"</span>,
  <span style="color: #531ab6;">"description"</span>: <span style="color: #3548cf;">"An e-commerce order"</span>,
  <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"object"</span>,
  <span style="color: #531ab6;">"required"</span>: <span style="color: #dd22dd;">[</span><span style="color: #3548cf;">"id"</span>, <span style="color: #3548cf;">"customer_id"</span>, <span style="color: #3548cf;">"items"</span>, <span style="color: #3548cf;">"created_at"</span>, <span style="color: #3548cf;">"total"</span><span style="color: #dd22dd;">]</span>,
  <span style="color: #531ab6;">"properties"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"id"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span>,
      <span style="color: #531ab6;">"pattern"</span>: <span style="color: #3548cf;">"^ord_[a-zA-Z0-9]{12}$"</span>,
      <span style="color: #531ab6;">"description"</span>: <span style="color: #3548cf;">"Unique order identifier"</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"customer_id"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span>,
      <span style="color: #531ab6;">"minLength"</span>: 1
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"items"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"array"</span>,
      <span style="color: #531ab6;">"minItems"</span>: 1,
      <span style="color: #531ab6;">"items"</span>: <span style="color: #972500;">{</span> <span style="color: #531ab6;">"$ref"</span>: <span style="color: #3548cf;">"#/$defs/OrderItem"</span> <span style="color: #972500;">}</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"discount_type"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"enum"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"PERCENTAGE"</span>, <span style="color: #3548cf;">"FIXED"</span>, <span style="color: #3548cf;">"BOGO"</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"description"</span>: <span style="color: #3548cf;">"Type of discount applied"</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"discount_value"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"number"</span>,
      <span style="color: #531ab6;">"minimum"</span>: 0
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"created_at"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span>,
      <span style="color: #531ab6;">"format"</span>: <span style="color: #3548cf;">"date-time"</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"total"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"number"</span>,
      <span style="color: #531ab6;">"minimum"</span>: 0
    <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"$defs"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"OrderItem"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"object"</span>,
      <span style="color: #531ab6;">"required"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"product_id"</span>, <span style="color: #3548cf;">"quantity"</span>, <span style="color: #3548cf;">"unit_price"</span><span style="color: #972500;">]</span>,
      <span style="color: #531ab6;">"properties"</span>: <span style="color: #972500;">{</span>
        <span style="color: #531ab6;">"product_id"</span>: <span style="color: #808000;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span> <span style="color: #808000;">}</span>,
        <span style="color: #531ab6;">"quantity"</span>: <span style="color: #808000;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"integer"</span>, <span style="color: #531ab6;">"minimum"</span>: 1, <span style="color: #531ab6;">"maximum"</span>: 10000 <span style="color: #808000;">}</span>,
        <span style="color: #531ab6;">"unit_price"</span>: <span style="color: #808000;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"number"</span>, <span style="color: #531ab6;">"exclusiveMinimum"</span>: 0 <span style="color: #808000;">}</span>
      <span style="color: #972500;">}</span>,
      <span style="color: #531ab6;">"additionalProperties"</span>: <span style="color: #0000b0;">false</span>
    <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"additionalProperties"</span>: <span style="color: #0000b0;">false</span>
<span style="color: #000000;">}</span>
</pre>
</div>

<p>
Notice the fundamental difference from Protobuf and Avro:
</p>

<ol class="org-ol">
<li><b>Validation constraints are first-class.</b>  <code>pattern</code>, <code>minimum</code>, <code>maximum</code>, <code>minLength</code>, <code>minItems</code> &#x2013; these live in the schema itself, not in application code.  Protobuf has no equivalent; Avro has limited support via logical types.</li>
<li><b>Human-readable format.</b>  JSON Schema is JSON describing JSON.  No compilation step, no binary encoding, no code generation required.</li>
<li><b>No wire format.</b>  JSON Schema doesn't define how to serialize data &#x2013; it defines what valid JSON looks like.  The wire format is always JSON (or YAML, since YAML is a superset of JSON).</li>
<li><b>Self-referencing.</b>  The <code>$ref</code> keyword enables schema composition.  Complex schemas can be built from reusable components.</li>
</ol>
</div>
</div>
<div id="outline-container-the-validation-powerhouse" class="outline-3">
<h3 id="the-validation-powerhouse"><span class="section-number-3">7.3.</span> The Validation Powerhouse</h3>
<div class="outline-text-3" id="text-the-validation-powerhouse">
<p>
JSON Schema's validation capabilities are significantly richer than Protobuf or Avro:
</p>

<div class="org-src-container">
<pre class="src src-json"><span style="color: #000000;">{</span>
  <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"object"</span>,
  <span style="color: #531ab6;">"properties"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"discount_type"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"enum"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"PERCENTAGE"</span>, <span style="color: #3548cf;">"FIXED"</span>, <span style="color: #3548cf;">"BOGO"</span><span style="color: #972500;">]</span> <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"discount_value"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"number"</span>, <span style="color: #531ab6;">"minimum"</span>: 0 <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"if"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"properties"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"discount_type"</span>: <span style="color: #972500;">{</span> <span style="color: #531ab6;">"const"</span>: <span style="color: #3548cf;">"PERCENTAGE"</span> <span style="color: #972500;">}</span> <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"required"</span>: <span style="color: #008899;">[</span><span style="color: #3548cf;">"discount_type"</span><span style="color: #008899;">]</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"then"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"properties"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"discount_value"</span>: <span style="color: #972500;">{</span> <span style="color: #531ab6;">"minimum"</span>: 0, <span style="color: #531ab6;">"maximum"</span>: 100 <span style="color: #972500;">}</span>
    <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"else"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"if"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"properties"</span>: <span style="color: #972500;">{</span> <span style="color: #531ab6;">"discount_type"</span>: <span style="color: #808000;">{</span> <span style="color: #531ab6;">"const"</span>: <span style="color: #3548cf;">"BOGO"</span> <span style="color: #808000;">}</span> <span style="color: #972500;">}</span>,
      <span style="color: #531ab6;">"required"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"discount_type"</span><span style="color: #972500;">]</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"then"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"properties"</span>: <span style="color: #972500;">{</span>
        <span style="color: #531ab6;">"discount_value"</span>: <span style="color: #808000;">{</span> <span style="color: #531ab6;">"const"</span>: 0 <span style="color: #808000;">}</span>
      <span style="color: #972500;">}</span>
    <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>
<span style="color: #000000;">}</span>
</pre>
</div>

<p>
This schema says: "If discount type is PERCENTAGE, the value must be between 0 and 100.  If discount type is BOGO, the value must be 0."  Try expressing that in Protobuf or Avro &#x2013; you can't.  Those languages define structure; JSON Schema defines structure <i>and</i> constraints.
</p>

<p>
There's a flip side to this expressiveness.  When a Protobuf field is wrong, the failure mode is simple: wrong type → deserialization error.  When a JSON Schema <code>if=/=then</code> rule is wrong &#x2013; say, someone writes <code>maximum: 1</code> instead of <code>maximum: 100</code> for the percentage constraint &#x2013; the failure is a <i>business logic</i> bug encoded in the schema itself.  Richer validation means richer failure modes.  The schema becomes code, and code has bugs.  This isn't an argument against JSON Schema's approach, but it means schemas with complex conditional logic need to be tested with the same rigor as application code.
</p>
</div>
</div>
<div id="outline-container-json-schema-and-the-ai-revolution" class="outline-3">
<h3 id="json-schema-and-the-ai-revolution"><span class="section-number-3">7.4.</span> JSON Schema and the AI Revolution</h3>
<div class="outline-text-3" id="text-json-schema-and-the-ai-revolution">
<p>
JSON Schema has found an unexpected second life in the AI/LLM era.  When you call an LLM with function-calling or structured output capabilities, the tool definitions are JSON Schema:
</p>

<div class="org-src-container">
<pre class="src src-json"><span style="color: #000000;">{</span>
  <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"function"</span>,
  <span style="color: #531ab6;">"function"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"name"</span>: <span style="color: #3548cf;">"get_weather"</span>,
    <span style="color: #531ab6;">"description"</span>: <span style="color: #3548cf;">"Get current weather for a location"</span>,
    <span style="color: #531ab6;">"parameters"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"object"</span>,
      <span style="color: #531ab6;">"properties"</span>: <span style="color: #972500;">{</span>
        <span style="color: #531ab6;">"location"</span>: <span style="color: #808000;">{</span>
          <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span>,
          <span style="color: #531ab6;">"description"</span>: <span style="color: #3548cf;">"City and state, e.g., San Francisco, CA"</span>
        <span style="color: #808000;">}</span>,
        <span style="color: #531ab6;">"unit"</span>: <span style="color: #808000;">{</span>
          <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span>,
          <span style="color: #531ab6;">"enum"</span>: <span style="color: #531ab6;">[</span><span style="color: #3548cf;">"celsius"</span>, <span style="color: #3548cf;">"fahrenheit"</span><span style="color: #531ab6;">]</span>
        <span style="color: #808000;">}</span>
      <span style="color: #972500;">}</span>,
      <span style="color: #531ab6;">"required"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"location"</span><span style="color: #972500;">]</span>
    <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>
<span style="color: #000000;">}</span>
</pre>
</div>

<p>
OpenAI's function calling, Anthropic's tool use, Google's function declarations &#x2013; they all use JSON Schema to constrain LLM outputs.  This means JSON Schema has become the <i>interface definition language for AI</i>.  The implications are profound:
</p>

<ul class="org-ul">
<li>Every AI agent that calls tools speaks JSON Schema</li>
<li>Structured output guarantees are enforced via JSON Schema validation</li>
<li>Schema-guided generation (constraining token sampling to produce valid JSON) relies on JSON Schema semantics</li>
</ul>

<p>
This wasn't in anyone's roadmap when JSON Schema was designed in 2009.  It's a testament to the flexibility of JSON Schema that it works so well for a use case that didn't exist when it was created.
</p>
</div>
</div>
<div id="outline-container-json-type-definition--jtd---the-alternative" class="outline-3">
<h3 id="json-type-definition--jtd---the-alternative"><span class="section-number-3">7.5.</span> JSON Type Definition (JTD): The Alternative</h3>
<div class="outline-text-3" id="text-json-type-definition--jtd---the-alternative">
<p>
It's worth mentioning <a href="https://jsontypedef.com/">JSON Type Definition (JTD)</a> (RFC 8927), an alternative to JSON Schema that prioritizes code generation:
</p>

<div class="org-src-container">
<pre class="src src-json"><span style="color: #000000;">{</span>
  <span style="color: #531ab6;">"properties"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"id"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span> <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"customer_id"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span> <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"items"</span>: <span style="color: #008899;">{</span>
      <span style="color: #531ab6;">"elements"</span>: <span style="color: #972500;">{</span>
        <span style="color: #531ab6;">"properties"</span>: <span style="color: #808000;">{</span>
          <span style="color: #531ab6;">"product_id"</span>: <span style="color: #531ab6;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"string"</span> <span style="color: #531ab6;">}</span>,
          <span style="color: #531ab6;">"quantity"</span>: <span style="color: #531ab6;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"int32"</span> <span style="color: #531ab6;">}</span>,
          <span style="color: #531ab6;">"unit_price"</span>: <span style="color: #531ab6;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"float64"</span> <span style="color: #531ab6;">}</span>
        <span style="color: #808000;">}</span>
      <span style="color: #972500;">}</span>
    <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"discount_type"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"enum"</span>: <span style="color: #972500;">[</span><span style="color: #3548cf;">"PERCENTAGE"</span>, <span style="color: #3548cf;">"FIXED"</span>, <span style="color: #3548cf;">"BOGO"</span><span style="color: #972500;">]</span> <span style="color: #008899;">}</span>,
    <span style="color: #531ab6;">"total"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"float64"</span> <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>,
  <span style="color: #531ab6;">"optionalProperties"</span>: <span style="color: #dd22dd;">{</span>
    <span style="color: #531ab6;">"discount_value"</span>: <span style="color: #008899;">{</span> <span style="color: #531ab6;">"type"</span>: <span style="color: #3548cf;">"float64"</span> <span style="color: #008899;">}</span>
  <span style="color: #dd22dd;">}</span>
<span style="color: #000000;">}</span>
</pre>
</div>

<p>
JTD makes a deliberate trade-off: less expressive than JSON Schema (no <code>if=/=then</code>, no <code>pattern</code>, no <code>minimum</code>) but unambiguously code-generatable.  Every JTD schema maps cleanly to types in Go, Java, Python, TypeScript, and other languages.  <b>JSON Schema's expressiveness actually <i>hinders</i> code generation because many validation concepts (regex patterns, conditional schemas) have no natural mapping to type systems</b>.
</p>
</div>
</div>
<div id="outline-container-json-structure--structured-outputs-for-llms" class="outline-3">
<h3 id="json-structure--structured-outputs-for-llms"><span class="section-number-3">7.6.</span> JSON Structure: Structured Outputs for LLMs</h3>
<div class="outline-text-3" id="text-json-structure--structured-outputs-for-llms">
<p>
Anthropic's <a href="https://docs.anthropic.com/en/docs/build-with-claude/structured-output">JSON Structure</a> feature takes JSON Schema usage a step further.  Rather than just validating LLM output after generation, JSON Structure constrains the generation process itself to guarantee valid JSON output on every call.
</p>

<p>
This is a subtle but important distinction.  Traditional validation is <i>post-hoc</i> &#x2013; you generate output, then check if it's valid.  JSON Structure is <i>constructive</i> &#x2013; the schema guides token sampling so that invalid outputs are never generated.  The result is 100% reliability for structured data extraction, not "usually works with retry logic."
</p>

<p>
This development positions JSON Schema as the bridge between human-authored APIs and AI-generated responses &#x2013; a unifying schema language for both traditional and AI-powered systems.
</p>
</div>
</div>
<div id="outline-container-json-schema-strengths-and-weaknesses" class="outline-3">
<h3 id="json-schema-strengths-and-weaknesses"><span class="section-number-3">7.7.</span> JSON Schema Strengths and Weaknesses</h3>
<div class="outline-text-3" id="text-json-schema-strengths-and-weaknesses">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Strengths</th>
<th scope="col" class="org-left">Weaknesses</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Rich validation constraints (pattern, range, conditional)</td>
<td class="org-left">No binary format (JSON is text, inherently slower)</td>
</tr>

<tr>
<td class="org-left">Human-readable (JSON describing JSON)</td>
<td class="org-left">Large messages (string keys, no compression)</td>
</tr>

<tr>
<td class="org-left">Universal web adoption (OpenAPI, AI tools)</td>
<td class="org-left">Schema evolution is informal (no registry standard)</td>
</tr>

<tr>
<td class="org-left">No compilation step required</td>
<td class="org-left">Complex schemas can be hard to understand</td>
</tr>

<tr>
<td class="org-left">AI/LLM revolution driver</td>
<td class="org-left">Code generation is weaker than Protobuf</td>
</tr>

<tr>
<td class="org-left">Massive validator ecosystem (ajv, jsonschema)</td>
<td class="org-left">Draft version fragmentation</td>
</tr>
</tbody>
</table>

<p>
JSON Schema's sweet spot is <b>web APIs, AI tool definitions, and configuration validation where human readability and rich constraints matter more than wire performance</b>.
</p>
</div>
</div>
</div>
<div id="outline-container-head-to-head-comparison" class="outline-2">
<h2 id="head-to-head-comparison"><span class="section-number-2">8.</span> Head-to-Head Comparison&#xa0;&#xa0;&#xa0;<span class="tag"><span class="comparison">comparison</span>&#xa0;<span class="tradeoffs">tradeoffs</span></span></h2>
<div class="outline-text-2" id="text-head-to-head-comparison">
<p>
With each language explored individually, here's how they compare side by side.
</p>
</div>
<div id="outline-container-feature-matrix" class="outline-3">
<h3 id="feature-matrix"><span class="section-number-3">8.1.</span> Feature Matrix</h3>
<div class="outline-text-3" id="text-feature-matrix">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Feature</th>
<th scope="col" class="org-left">Protobuf</th>
<th scope="col" class="org-left">Avro</th>
<th scope="col" class="org-left">JSON Schema</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><b>Schema format</b></td>
<td class="org-left">Custom IDL (.proto)</td>
<td class="org-left">JSON or Avro IDL</td>
<td class="org-left">JSON</td>
</tr>

<tr>
<td class="org-left"><b>Wire format</b></td>
<td class="org-left">Binary (varint encoding)</td>
<td class="org-left">Binary (schema-aware)</td>
<td class="org-left">JSON (text)</td>
</tr>

<tr>
<td class="org-left"><b>Schema in payload</b></td>
<td class="org-left">No</td>
<td class="org-left">Yes (or via registry)</td>
<td class="org-left">Yes (it <i>is</i> JSON)</td>
</tr>

<tr>
<td class="org-left"><b>Code generation</b></td>
<td class="org-left">Required</td>
<td class="org-left">Optional</td>
<td class="org-left">Optional</td>
</tr>

<tr>
<td class="org-left"><b>Schema evolution</b></td>
<td class="org-left">Field tags (add, deprecate)</td>
<td class="org-left">Resolution rules (defaults)</td>
<td class="org-left">Informal (no standard rules)</td>
</tr>

<tr>
<td class="org-left"><b>Validation constraints</b></td>
<td class="org-left">None built-in</td>
<td class="org-left">Limited (logical types)</td>
<td class="org-left">Rich (pattern, range, conditional)</td>
</tr>

<tr>
<td class="org-left"><b>RPC framework</b></td>
<td class="org-left">gRPC (first-class)</td>
<td class="org-left">Avro RPC (limited adoption)</td>
<td class="org-left">OpenAPI (via REST)</td>
</tr>

<tr>
<td class="org-left"><b>Streaming support</b></td>
<td class="org-left">gRPC streaming</td>
<td class="org-left">Kafka (first-class)</td>
<td class="org-left">SSE / WebSocket (manual)</td>
</tr>

<tr>
<td class="org-left"><b>Registry</b></td>
<td class="org-left">Buf Schema Registry</td>
<td class="org-left">Confluent Schema Registry</td>
<td class="org-left">No dominant standard</td>
</tr>

<tr>
<td class="org-left"><b>Breaking change detection</b></td>
<td class="org-left">buf breaking</td>
<td class="org-left">Registry compatibility checks</td>
<td class="org-left">Manual / custom tooling</td>
</tr>

<tr>
<td class="org-left"><b>Union/oneof types</b></td>
<td class="org-left"><code>oneof</code> (limited)</td>
<td class="org-left">Union types (flexible)</td>
<td class="org-left"><code>anyOf</code>, <code>oneOf</code> (flexible)</td>
</tr>

<tr>
<td class="org-left"><b>Map types</b></td>
<td class="org-left"><code>map&lt;K, V&gt;</code></td>
<td class="org-left">JSON object schema</td>
<td class="org-left"><code>additionalProperties</code></td>
</tr>

<tr>
<td class="org-left"><b>Null handling</b></td>
<td class="org-left">Default values (no null)</td>
<td class="org-left">Explicit <code>["null", T]</code> union</td>
<td class="org-left"><code>{"type": ["string", "null"]}</code></td>
</tr>

<tr>
<td class="org-left"><b>Documentation</b></td>
<td class="org-left">Comments only</td>
<td class="org-left"><code>doc</code> field in schema</td>
<td class="org-left"><code>description</code>, <code>title</code>, <code>examples</code></td>
</tr>

<tr>
<td class="org-left"><b>Self-describing</b></td>
<td class="org-left">No</td>
<td class="org-left">Yes</td>
<td class="org-left">Yes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-multi-dimensional-comparison" class="outline-3">
<h3 id="multi-dimensional-comparison"><span class="section-number-3">8.2.</span> Multi-Dimensional Comparison</h3>
<div class="outline-text-3" id="text-multi-dimensional-comparison">
<p>
A radar chart across eight key dimensions makes the trade-offs vivid:
</p>

<div id="sl_radar" class="plotly-plot"></div>

<blockquote class="pull-quote pull-left">
<p>
No language dominates all eight dimensions
</p>
</blockquote>

<p>
The radar chart makes the trade-offs vivid.  Protobuf dominates performance and code generation but scores poorly on validation and self-description.  JSON Schema dominates readability and validation but can't compete on performance.  Avro sits in the middle on most axes but owns schema evolution and self-description &#x2013; exactly the properties needed for data pipeline interoperability.
</p>

<p>
No language dominates all eight dimensions.  This isn't a failure of the languages &#x2013; it reflects genuinely different design priorities.  Choosing a schema language is about matching <i>your</i> priorities to <i>their</i> strengths.
</p>
</div>
</div>
<div id="outline-container-the-decision-flowchart" class="outline-3">
<h3 id="the-decision-flowchart"><span class="section-number-3">8.3.</span> The Decision Flowchart</h3>
<div class="outline-text-3" id="text-the-decision-flowchart">
<p>
When I'm consulting with teams on schema language selection, this is roughly the decision tree I follow:
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">graph</span> <span style="color: #0000b0;">TD</span>
    START<span style="color: #000000;">[</span><span style="color: #3548cf;">"What's your primary use case?"</span><span style="color: #000000;">]</span> <span style="color: #721045;">--&gt;</span> RPC<span style="color: #000000;">{</span><span style="color: #3548cf;">"Synchronous RPC&lt;br/&gt;between services?"</span><span style="color: #000000;">}</span>
    START <span style="color: #721045;">--&gt;</span> STREAM<span style="color: #000000;">{</span><span style="color: #3548cf;">"Event streaming&lt;br/&gt;/ data pipelines?"</span><span style="color: #000000;">}</span>
    START <span style="color: #721045;">--&gt;</span> WEB<span style="color: #000000;">{</span><span style="color: #3548cf;">"Web APIs&lt;br/&gt;/ REST / AI tools?"</span><span style="color: #000000;">}</span>
    START <span style="color: #721045;">--&gt;</span> CONFIG<span style="color: #000000;">{</span><span style="color: #3548cf;">"Configuration&lt;br/&gt;validation?"</span><span style="color: #000000;">}</span>

    RPC <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| PERF<span style="color: #000000;">{</span><span style="color: #3548cf;">"Performance&lt;br/&gt;critical?"</span><span style="color: #000000;">}</span>
    PERF <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| PROTO<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9989; Protocol Buffers&lt;br/&gt;+ gRPC"</span><span style="color: #000000;">]</span>
    PERF <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"No"</span>| GRPC_OR_REST<span style="color: #000000;">{</span><span style="color: #3548cf;">"Need streaming&lt;br/&gt;or bidirectional?"</span><span style="color: #000000;">}</span>
    GRPC_OR_REST <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| PROTO
    GRPC_OR_REST <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"No"</span>| JSON_SCHEMA_REST<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9989; JSON Schema&lt;br/&gt;+ OpenAPI"</span><span style="color: #000000;">]</span>

    STREAM <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| KAFKA<span style="color: #000000;">{</span><span style="color: #3548cf;">"Using Kafka&lt;br/&gt;/ Confluent?"</span><span style="color: #000000;">}</span>
    KAFKA <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| AVRO<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9989; Apache Avro&lt;br/&gt;+ Schema Registry"</span><span style="color: #000000;">]</span>
    KAFKA <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"No"</span>| EVOLVE<span style="color: #000000;">{</span><span style="color: #3548cf;">"Independent&lt;br/&gt;schema evolution?"</span><span style="color: #000000;">}</span>
    EVOLVE <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Critical"</span>| AVRO
    EVOLVE <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Not critical"</span>| PROTO

    WEB <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| AI<span style="color: #000000;">{</span><span style="color: #3548cf;">"AI/LLM&lt;br/&gt;integration?"</span><span style="color: #000000;">}</span>
    AI <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| JSON_SCHEMA_AI<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9989; JSON Schema"</span><span style="color: #000000;">]</span>
    AI <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"No"</span>| VALIDATE<span style="color: #000000;">{</span><span style="color: #3548cf;">"Rich validation&lt;br/&gt;constraints?"</span><span style="color: #000000;">}</span>
    VALIDATE <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| JSON_SCHEMA_REST
    VALIDATE <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"No"</span>| OPENAPI<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9989; OpenAPI&lt;br/&gt;(uses JSON Schema subset)"</span><span style="color: #000000;">]</span>

    CONFIG <span style="color: #721045;">--&gt;</span>|<span style="color: #3548cf;">"Yes"</span>| CUE<span style="color: #000000;">[</span><span style="color: #3548cf;">"Consider CUE&lt;br/&gt;or JSON Schema"</span><span style="color: #000000;">]</span>

    style PROTO fill:<span style="color: #000000; background-color: #2BCDC1;">#2BCDC1</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
    style AVRO fill:<span style="color: #000000; background-color: #F66095;">#F66095</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
    style JSON_SCHEMA_REST fill:<span style="color: #000000; background-color: #FFB347;">#FFB347</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
    style JSON_SCHEMA_AI fill:<span style="color: #000000; background-color: #FFB347;">#FFB347</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
    style CUE fill:<span style="color: #000000; background-color: #F39C12;">#F39C12</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
    style OPENAPI fill:<span style="color: #ffffff; background-color: #9B59B6;">#9B59B6</span>,color:<span style="color: #000000; background-color: #fff;">#fff</span>
</pre>
</div>
</div>
</div>
<div id="outline-container-performance-benchmarks" class="outline-3">
<h3 id="performance-benchmarks"><span class="section-number-3">8.4.</span> Performance Benchmarks</h3>
<div class="outline-text-3" id="text-performance-benchmarks">
<p>
The following chart shows <i>approximate</i> performance characteristics for serializing a 10-item Order message across the three formats.  These are illustrative values drawn from the range of published benchmarks<sup><a id="fnr.7" class="footref" href="https://www.chiply.dev/#fn.7" role="doc-backlink">7</a></sup> &#x2013; your mileage will vary by language, hardware, and message shape, but the relative ordering is consistent:
</p>

<div id="sl_benchmarks" class="plotly-plot"></div>

<p>
The performance differences are stark.  Protobuf is 7x faster at serialization and 8x faster at deserialization compared to JSON.  Message size is 6x smaller.  These differences matter enormously at scale &#x2013; if you're processing 100,000 messages per second, the difference between 1.2μs and 8.5μs per message is the difference between 12% and 85% CPU utilization on serialization alone.
</p>

<p>
But performance isn't everything.  Most systems aren't processing 100K messages per second.  For an API handling 100 requests per second, the difference between 1.2μs and 8.5μs is irrelevant &#x2013; both are dwarfed by network latency (milliseconds) and business logic (milliseconds to seconds).
</p>

<p>
The rule of thumb: <b>if you're not sure whether you need binary performance, you don't need binary performance</b>.  Start with JSON Schema for the ecosystem benefits and switch to Protobuf or Avro only when profiling reveals serialization as a bottleneck, but be aware that JSON Schema validations won't compile into the targets of your code generation.
</p>
</div>
</div>
<div id="outline-container-when-to-use-each" class="outline-3">
<h3 id="when-to-use-each"><span class="section-number-3">8.5.</span> When to Use Each</h3>
<div class="outline-text-3" id="text-when-to-use-each">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Scenario</th>
<th scope="col" class="org-left">Recommended</th>
<th scope="col" class="org-left">Why</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Microservice RPC (internal)</td>
<td class="org-left">Protobuf + gRPC</td>
<td class="org-left">Performance, strong typing, streaming</td>
</tr>

<tr>
<td class="org-left">Kafka event streaming</td>
<td class="org-left">Avro + Confluent SR</td>
<td class="org-left">Schema evolution, backward compatibility</td>
</tr>

<tr>
<td class="org-left">Public REST API</td>
<td class="org-left">JSON Schema + OpenAPI</td>
<td class="org-left">Human-readable, browser-friendly</td>
</tr>

<tr>
<td class="org-left">LLM tool definitions</td>
<td class="org-left">JSON Schema</td>
<td class="org-left">Industry standard for AI structured output</td>
</tr>

<tr>
<td class="org-left">Mobile app API</td>
<td class="org-left">Protobuf</td>
<td class="org-left">Small payloads save bandwidth</td>
</tr>

<tr>
<td class="org-left">Data warehouse ingestion</td>
<td class="org-left">Avro</td>
<td class="org-left">Self-describing, schema evolution</td>
</tr>

<tr>
<td class="org-left">Browser-to-server</td>
<td class="org-left">JSON Schema</td>
<td class="org-left">Native JSON, no compilation</td>
</tr>

<tr>
<td class="org-left">Real-time gaming</td>
<td class="org-left">FlatBuffers or Cap'n Proto</td>
<td class="org-left">Zero-copy for latency-sensitive</td>
</tr>

<tr>
<td class="org-left">IoT / embedded</td>
<td class="org-left">Protobuf (proto3)</td>
<td class="org-left">Small footprint, simple implementation</td>
</tr>

<tr>
<td class="org-left">Configuration files</td>
<td class="org-left">JSON Schema or CUE</td>
<td class="org-left">Validation + human editing</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-migrating-between-schema-languages" class="outline-3">
<h3 id="migrating-between-schema-languages"><span class="section-number-3">8.6.</span> Migrating Between Schema Languages</h3>
<div class="outline-text-3" id="text-migrating-between-schema-languages">
<p>
If you're already invested in one schema language and considering a switch, here's a rough migration guide:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Migration</th>
<th scope="col" class="org-left">Difficulty</th>
<th scope="col" class="org-left">Tools</th>
<th scope="col" class="org-left">Key Challenges</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">JSON → Protobuf</td>
<td class="org-left">Medium</td>
<td class="org-left"><a href="https://github.com/quicktype/quicktype">quicktype</a>, manual</td>
<td class="org-left">Mapping nullable fields to <code>optional</code>, losing validation constraints</td>
</tr>

<tr>
<td class="org-left">JSON → Avro</td>
<td class="org-left">Medium</td>
<td class="org-left"><a href="https://github.com/quicktype/quicktype">quicktype</a>, manual</td>
<td class="org-left">Converting <code>anyOf=/=oneOf</code> to union types, mapping <code>format</code> to logical types</td>
</tr>

<tr>
<td class="org-left">Protobuf → Avro</td>
<td class="org-left">Low-Medium</td>
<td class="org-left"><a href="https://docs.confluent.io/platform/current/schema-registry/fundamentals/serdes-develop/index.html">Confluent converters</a></td>
<td class="org-left">Mechanical translation; main challenge is adopting schema resolution patterns</td>
</tr>

<tr>
<td class="org-left">Avro → Protobuf</td>
<td class="org-left">Low-Medium</td>
<td class="org-left">Manual</td>
<td class="org-left">Assigning field tags, converting unions to <code>oneof</code>, losing self-describing payloads</td>
</tr>

<tr>
<td class="org-left">Protobuf → JSON Schema</td>
<td class="org-left">Low</td>
<td class="org-left"><code>buf</code> can export JSON</td>
<td class="org-left">Losing binary performance, gaining validation constraints</td>
</tr>

<tr>
<td class="org-left">Avro → JSON Schema</td>
<td class="org-left">Low</td>
<td class="org-left"><code>avro-to-json-schema</code></td>
<td class="org-left">Mechanical; JSON Schema's <code>if=/=then</code> can express constraints Avro couldn't</td>
</tr>
</tbody>
</table>

<p>
The hardest part of any migration is rarely the schema translation itself &#x2013; it's migrating the <i>ecosystem</i> around the schema.  Code generation targets, CI pipelines, registry configurations, and consumer libraries all need to change.  Plan for a gradual rollout where both formats coexist during the transition, with a compatibility layer that translates between them.
</p>
</div>
</div>
</div>
<div id="outline-container-war-stories--when-schemas-fail" class="outline-2">
<h2 id="war-stories--when-schemas-fail"><span class="section-number-2">9.</span> War Stories: When Schemas Fail&#xa0;&#xa0;&#xa0;<span class="tag"><span class="warStories">warStories</span>&#xa0;<span class="failures">failures</span></span></h2>
<div class="outline-text-2" id="text-war-stories--when-schemas-fail">
<p>
Theory is comfortable.  Production is not.  Here's what happens when schema management goes wrong.
</p>
</div>
<div id="outline-container-knight-capital---440-million-in-45-minutes" class="outline-3">
<h3 id="knight-capital---440-million-in-45-minutes"><span class="section-number-3">9.1.</span> Knight Capital: $440 Million in 45 Minutes</h3>
<div class="outline-text-3" id="text-knight-capital---440-million-in-45-minutes">
<p>
On August 1, 2012, Knight Capital Group deployed new trading software with a configuration change that reactivated dead code from 8 years earlier.  The root cause was a deployment that updated 7 of 8 servers &#x2013; the eighth server still had the old configuration, which interpreted incoming messages according to obsolete logic<sup><a id="fnr.8" class="footref" href="https://www.chiply.dev/#fn.8" role="doc-backlink">8</a></sup>.
</p>

<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">sequenceDiagram</span>
    <span style="color: #0000b0;">participant</span> Deploy as Deployment
    <span style="color: #0000b0;">participant</span> S1 as Servers 1<span style="color: #721045;">-</span>7 <span style="color: #000000;">(</span>v2<span style="color: #000000;">)</span>
    <span style="color: #0000b0;">participant</span> S8 as Server 8 <span style="color: #000000;">(</span>v1!<span style="color: #000000;">)</span>
    <span style="color: #0000b0;">participant</span> NYSE as NYSE

    Deploy<span style="color: #721045;">-&gt;&gt;</span>S1: Deploy new config &#9989;
    Deploy<span style="color: #721045;">-&gt;&gt;</span>S8: Deploy fails silently &#10060;

    <span style="color: #0000b0;">Note</span> over S1,S8: Market opens 09:30

    NYSE<span style="color: #721045;">-&gt;&gt;</span>S1: Order routing messages
    S1<span style="color: #721045;">-&gt;&gt;</span>NYSE: Correct trades &#9989;

    NYSE<span style="color: #721045;">-&gt;&gt;</span>S8: Order routing messages
    S8<span style="color: #721045;">-&gt;&gt;</span>S8: Interprets with old config
    S8<span style="color: #721045;">-&gt;&gt;</span>NYSE: Unintended trades &#10060;
    <span style="color: #0000b0;">Note</span> over S8,NYSE: Buys high, sells low&lt;br/&gt;at 40x normal volume

    <span style="color: #0000b0;">Note</span> over S8: 45 minutes &#215; $10M/min = $440M loss
</pre>
</div>

<p>
To be precise: this was a deployment and configuration management failure, not a schema drift failure.  But it illustrates the same underlying principle that schema registries enforce &#x2013; <b>every component in a system must agree on the shape of the messages it processes, and that agreement must be machine-verified, not assumed</b>.  A schema registry wouldn't have prevented Knight Capital's specific bug, but the discipline it represents &#x2013; machine-enforced consistency checks before any component goes live &#x2013; would have caught the configuration mismatch.
</p>

<p>
The lesson: <b>configuration consistency and schema consistency are the same problem at different layers of abstraction.  Both are too important to leave to human coordination.</b>
</p>
</div>
</div>
<div id="outline-container-hl7-to-fhir--healthcare-s-30-year-schema-migration" class="outline-3">
<h3 id="hl7-to-fhir--healthcare-s-30-year-schema-migration"><span class="section-number-3">9.2.</span> HL7 to FHIR: Healthcare's 30-Year Schema Migration</h3>
<div class="outline-text-3" id="text-hl7-to-fhir--healthcare-s-30-year-schema-migration">
<p>
The healthcare industry provides a sobering example of what happens when schema evolution isn't built into the foundation.  HL7 Version 2 (HL7v2), the dominant healthcare messaging standard since the 1990s, used a pipe-delimited format with implicit field positioning:
</p>

<div class="org-src-container">
<pre class="src src-text">MSH|^~\&amp;|HIS|Hospital|LAB|Lab|20230815||ORU^R01|MSG001|P|2.5.1
PID|||12345^^^MRN||DOE^JOHN||19800101|M
OBR|1|12345|67890|CBC^Complete Blood Count
OBX|1|NM|WBC^White Blood Count||7.5|10*3/uL|4.5-11.0|N
</pre>
</div>

<p>
There was no formal schema language.  Field meanings were defined in prose specification documents.  Different hospitals interpreted ambiguous fields differently.  "Optional" fields meant different things to different implementations.  Interoperability was a nightmare.
</p>

<p>
FHIR (Fast Healthcare Interoperability Resources), HL7's modern replacement, uses &#x2013; you guessed it &#x2013; JSON Schema (via FHIR StructureDefinitions that compile to JSON Schema).  The migration has been underway since 2014 and is still not complete.  Entire companies exist solely to translate between HL7v2 and FHIR.
</p>

<p>
The cost of not starting with a proper schema language is measured in <i>decades</i> of technical debt.
</p>
</div>
</div>
<div id="outline-container-the-silent-corruption-pattern" class="outline-3">
<h3 id="the-silent-corruption-pattern"><span class="section-number-3">9.3.</span> The Silent Corruption Pattern</h3>
<div class="outline-text-3" id="text-the-silent-corruption-pattern">
<p>
The scariest failure mode isn't a loud crash &#x2013; it's silent data corruption.  I've seen this pattern across multiple organizations:
</p>

<blockquote class="pull-quote pull-right">
<p>
The most dangerous bugs aren't the ones that crash your system.  They're the ones that silently corrupt your data while everything looks fine. &#x2014;Engineering proverb
</p>
</blockquote>

<ol class="org-ol">
<li>Team A adds a field to their schema (or what they think is their schema)</li>
<li>Team B doesn't know about the change</li>
<li>Team B's deserializer encounters the unknown field</li>
<li>Depending on the language and library:
<ul class="org-ul">
<li>Python's <code>json.loads()</code> silently includes it (but downstream code ignores it)</li>
<li>Java's Jackson silently drops it (unless configured with <code>FAIL_ON_UNKNOWN_PROPERTIES</code>)</li>
<li>Go's <code>json.Unmarshal</code> silently drops it</li>
</ul></li>
<li>The data is "valid" in every system but <i>incomplete</i> in some</li>
<li>Reports diverge. Business decisions are made on bad data.</li>
<li>Weeks or months later, someone notices the numbers don't add up</li>
</ol>


<p>
A schema registry with compatibility checks prevents step 1 from happening silently.  A schema language with code generation prevents step 2 entirely.  Both together make step 3 through 7 impossible by construction.
</p>
</div>
</div>
</div>
<div id="outline-container-the-ecosystem-unlocked" class="outline-2">
<h2 id="the-ecosystem-unlocked"><span class="section-number-2">10.</span> The Ecosystem Unlocked&#xa0;&#xa0;&#xa0;<span class="tag"><span class="codeGeneration">codeGeneration</span>&#xa0;<span class="tooling">tooling</span>&#xa0;<span class="ecosystem">ecosystem</span></span></h2>
<div class="outline-text-2" id="text-the-ecosystem-unlocked">
<p>
Choosing a schema language isn't just about types and serialization.  It unlocks an entire <i>ecosystem</i> of tooling that becomes available once your models have a machine-readable, language-agnostic definition.
</p>
</div>
<div id="outline-container-the-ecosystem-flywheel" class="outline-3">
<h3 id="the-ecosystem-flywheel"><span class="section-number-3">10.1.</span> The Ecosystem Flywheel</h3>
<div class="outline-text-3" id="text-the-ecosystem-flywheel">
<div class="org-src-container">
<pre class="src src-mermaid"><span style="color: #531ab6;">graph</span> <span style="color: #0000b0;">TB</span>
    SCHEMA<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#128196; Schema Definition&lt;br/&gt;(Single Source of Truth)"</span><span style="color: #000000;">]</span>

    SCHEMA <span style="color: #721045;">--&gt;</span> CODEGEN<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9881;&#65039; Code Generation&lt;br/&gt;Types in every language"</span><span style="color: #000000;">]</span>
    SCHEMA <span style="color: #721045;">--&gt;</span> VALIDATE<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#9989; Validation&lt;br/&gt;Compile-time + runtime checks"</span><span style="color: #000000;">]</span>
    SCHEMA <span style="color: #721045;">--&gt;</span> DOCS<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#128214; Documentation&lt;br/&gt;Auto-generated API docs"</span><span style="color: #000000;">]</span>
    SCHEMA <span style="color: #721045;">--&gt;</span> REGISTRY<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#128452;&#65039; Schema Registry&lt;br/&gt;Version history + compatibility"</span><span style="color: #000000;">]</span>
    SCHEMA <span style="color: #721045;">--&gt;</span> TEST<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#129514; Contract Testing&lt;br/&gt;Producer-consumer agreements"</span><span style="color: #000000;">]</span>
    SCHEMA <span style="color: #721045;">--&gt;</span> VIZ<span style="color: #000000;">[</span><span style="color: #3548cf;">"&#128202; Visualization&lt;br/&gt;ER diagrams, dependency graphs"</span><span style="color: #000000;">]</span>

    CODEGEN <span style="color: #721045;">--&gt;</span> SCHEMA
    VALIDATE <span style="color: #721045;">--&gt;</span> SCHEMA
    DOCS <span style="color: #721045;">--&gt;</span> SCHEMA
    REGISTRY <span style="color: #721045;">--&gt;</span> SCHEMA
    TEST <span style="color: #721045;">--&gt;</span> SCHEMA
    VIZ <span style="color: #721045;">--&gt;</span> SCHEMA

    style SCHEMA fill:<span style="color: #000000; background-color: #2BCDC1;">#2BCDC1</span>,color:<span style="color: #ffffff; background-color: #000;">#000</span>
</pre>
</div>

<p>
Each capability reinforces the others.  The schema registry enables contract testing.  Contract testing catches breaking changes.  Breaking change detection encourages more teams to register schemas.  More schemas mean better documentation.  Better documentation means faster onboarding.  Faster onboarding means more teams adopt schema-first development.
</p>

<p>
This flywheel effect is why schema languages are more than just "a way to define types."  They're a <i>platform</i> for organizational coordination.
</p>
</div>
</div>
<div id="outline-container-code-generation--the-killer-feature" class="outline-3">
<h3 id="code-generation--the-killer-feature"><span class="section-number-3">10.2.</span> Code Generation: The Killer Feature</h3>
<div class="outline-text-3" id="text-code-generation--the-killer-feature">
<p>
Code generation is what transforms a schema language from "documentation" to "infrastructure."  A well-generated code library provides:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Capability</th>
<th scope="col" class="org-left">What It Gives You</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Type-safe constructors</td>
<td class="org-left">Compile errors when you pass wrong types</td>
</tr>

<tr>
<td class="org-left">Serialization/deserialization</td>
<td class="org-left">One-line conversion to/from wire format</td>
</tr>

<tr>
<td class="org-left">Builder patterns</td>
<td class="org-left">Ergonomic construction of complex messages</td>
</tr>

<tr>
<td class="org-left">Equality/hashing</td>
<td class="org-left">Correct deep equality for tests and collections</td>
</tr>

<tr>
<td class="org-left">Documentation</td>
<td class="org-left">Generated from schema field descriptions</td>
</tr>

<tr>
<td class="org-left">IDE support</td>
<td class="org-left">Autocompletion, go-to-definition, refactoring</td>
</tr>
</tbody>
</table>

<p>
The quality of code generation varies dramatically:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Language</th>
<th scope="col" class="org-left">Protobuf</th>
<th scope="col" class="org-left">Avro</th>
<th scope="col" class="org-left">JSON Schema</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Go</td>
<td class="org-left">Excellent (buf, protoc-gen-go)</td>
<td class="org-left">Good (goavro, avrogen)</td>
<td class="org-left">Moderate (go-jsonschema)</td>
</tr>

<tr>
<td class="org-left">Python</td>
<td class="org-left">Good (protobuf, betterproto)</td>
<td class="org-left">Good (fastavro, avro)</td>
<td class="org-left">Good (datamodel-codegen)</td>
</tr>

<tr>
<td class="org-left">Java</td>
<td class="org-left">Excellent (protoc, grpc-java)</td>
<td class="org-left">Excellent (avro-tools)</td>
<td class="org-left">Moderate (jsonschema2pojo)</td>
</tr>

<tr>
<td class="org-left">TypeScript</td>
<td class="org-left">Good (buf, ts-proto)</td>
<td class="org-left">Moderate (avsc)</td>
<td class="org-left">Good (json-schema-to-typescript)</td>
</tr>

<tr>
<td class="org-left">Rust</td>
<td class="org-left">Good (prost, tonic)</td>
<td class="org-left">Moderate (apache-avro)</td>
<td class="org-left">Moderate (typify)</td>
</tr>

<tr>
<td class="org-left">C#</td>
<td class="org-left">Excellent (grpc-dotnet)</td>
<td class="org-left">Moderate (Apache.Avro)</td>
<td class="org-left">Moderate (NJsonSchema)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-validation--from-tests-to-guarantees" class="outline-3">
<h3 id="validation--from-tests-to-guarantees"><span class="section-number-3">10.3.</span> Validation: From Tests to Guarantees</h3>
<div class="outline-text-3" id="text-validation--from-tests-to-guarantees">
<p>
Schema validation operates at multiple levels:
</p>

<ol class="org-ol">
<li><b>Compile-time validation</b>: The generated code enforces types at compile time (or lint time for dynamic languages)</li>
<li><b>Schema registration validation</b>: The schema registry rejects incompatible schemas before they reach production</li>
<li><b>Runtime validation</b>: Messages are validated against the schema during deserialization</li>
<li><b>Contract testing</b>: Producer and consumer schemas are checked for compatibility in CI</li>
</ol>

<p>
These layers create a <i>defense in depth</i> against schema drift.  No single layer is perfect, but together they make accidental incompatibilities nearly impossible.
</p>
</div>
</div>
<div id="outline-container-schema-registries--the-missing-piece" class="outline-3">
<h3 id="schema-registries--the-missing-piece"><span class="section-number-3">10.4.</span> Schema Registries: The Missing Piece</h3>
<div class="outline-text-3" id="text-schema-registries--the-missing-piece">
<p>
A schema registry is a centralized service that stores, versions, and validates schemas.  It's the operational backbone that makes schema languages work in production, yet it's often overlooked in comparisons that focus on type systems and wire formats.
</p>

<p>
At its core, a schema registry does three things:
</p>

<ol class="org-ol">
<li><b>Stores schema versions.</b>  Every version of every schema is immutable and addressable by ID.  You can always answer the question "what did the Order schema look like six months ago?"</li>
<li><b>Enforces compatibility.</b>  Before a new schema version is registered, the registry checks it against previous versions using configurable rules (backward, forward, or full compatibility).  Incompatible changes are rejected before they reach production.</li>
<li><b>Decouples producers from consumers.</b>  Producers register their schema at write time.  Consumers fetch the schema at read time.  Neither needs to know about the other's deployment schedule.</li>
</ol>

<p>
The registry landscape is converging:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Registry</th>
<th scope="col" class="org-left">Origin</th>
<th scope="col" class="org-left">Supported Formats</th>
<th scope="col" class="org-left">Compatibility Checks</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><a href="https://docs.confluent.io/platform/current/schema-registry/">Confluent Schema Registry</a></td>
<td class="org-left">Kafka/Avro ecosystem</td>
<td class="org-left">Avro, Protobuf, JSON Schema</td>
<td class="org-left">Yes (configurable per-subject)</td>
</tr>

<tr>
<td class="org-left"><a href="https://buf.build/">Buf Schema Registry</a></td>
<td class="org-left">Protobuf ecosystem</td>
<td class="org-left">Protobuf</td>
<td class="org-left">Yes (via <code>buf breaking</code>)</td>
</tr>

<tr>
<td class="org-left"><a href="https://www.apicur.io/registry/">Apicurio Registry</a></td>
<td class="org-left">Red Hat / open source</td>
<td class="org-left">Avro, Protobuf, JSON Schema, OpenAPI, GraphQL</td>
<td class="org-left">Yes</td>
</tr>

<tr>
<td class="org-left"><a href="https://github.com/aiven/karapace">Karapace</a></td>
<td class="org-left">Aiven / open source</td>
<td class="org-left">Avro, Protobuf, JSON Schema</td>
<td class="org-left">Yes (Confluent-compatible API)</td>
</tr>

<tr>
<td class="org-left">AWS Glue Schema Registry</td>
<td class="org-left">AWS</td>
<td class="org-left">Avro, JSON Schema</td>
<td class="org-left">Yes</td>
</tr>
</tbody>
</table>

<p>
The trend is clear: registries are becoming <i>format-agnostic</i>.  Confluent's registry, originally Avro-only, now supports Protobuf and JSON Schema.  This means the choice of registry is increasingly decoupled from the choice of schema language &#x2013; you can use Protobuf schemas with Confluent's registry, or Avro schemas with a non-Confluent registry.
</p>

<p>
If you take one operational lesson from this article, let it be this: <b>a schema language without a registry is a type system.  A schema language <i>with</i> a registry is infrastructure.</b>
</p>
</div>
</div>
<div id="outline-container-documentation-generation" class="outline-3">
<h3 id="documentation-generation"><span class="section-number-3">10.5.</span> Documentation Generation</h3>
<div class="outline-text-3" id="text-documentation-generation">
<p>
One of the most underappreciated benefits of schema languages is automatic documentation:
</p>

<ul class="org-ul">
<li><b>Protobuf</b>: Tools like <a href="https://github.com/pseudomuto/protoc-gen-doc">protoc-gen-doc</a> generate HTML, Markdown, or JSON documentation from <code>.proto</code> files and their comments</li>
<li><b>Avro</b>: The <code>doc</code> field in Avro schemas is extracted by tools like <a href="https://github.com/ept/avrodoc">avrodoc</a> to generate browsable documentation</li>
<li><b>JSON Schema</b>: Tools like <a href="https://github.com/coveooss/json-schema-for-humans">json-schema-for-humans</a> render JSON Schemas as readable HTML pages</li>
</ul>

<p>
This documentation is <i>always current</i> because it's generated from the same schema that generates the code.  No more stale Confluence pages that describe a model from three versions ago.
</p>
</div>
</div>
<div id="outline-container-the-ecosystem-visualized" class="outline-3">
<h3 id="the-ecosystem-visualized"><span class="section-number-3">10.6.</span> The Ecosystem Visualized</h3>
<div class="outline-text-3" id="text-the-ecosystem-visualized">
<p>
The treemap below shows the ecosystem depth of each schema language, sized by GitHub stars:
</p>

<div id="sl_ecosystem" class="plotly-plot"></div>

<p>
The treemap reveals the different shapes of each ecosystem.  Protobuf's ecosystem is deep and focused &#x2013; a few high-quality tools covering code generation, linting, and testing.  Avro's ecosystem is concentrated in the Kafka/Confluent space.  JSON Schema's ecosystem is the broadest, spanning validators, code generators, documentation tools, AI frameworks, and API definition languages.  This breadth reflects JSON Schema's role as the lingua franca of the web.
</p>

<p>
A note on sizing: the "AI/LLM" category uses GitHub stars from the <i>platforms</i> that consume JSON Schema (OpenAI's SDK, Anthropic's SDK, LangChain) rather than standalone schema tools.  The sizes are directionally useful for showing the relative weight of AI adoption in the JSON Schema ecosystem, but shouldn't be compared 1:1 with purpose-built schema tools like <code>ajv</code> or <code>protoc-gen-go</code>.
</p>
</div>
</div>
</div>
<div id="outline-container-designing-the-ideal-schema-language" class="outline-2">
<h2 id="designing-the-ideal-schema-language"><span class="section-number-2">11.</span> Designing the Ideal Schema Language&#xa0;&#xa0;&#xa0;<span class="tag"><span class="design">design</span>&#xa0;<span class="futureWork">futureWork</span></span></h2>
<div class="outline-text-2" id="text-designing-the-ideal-schema-language">
<p>
After spending thousands of words analyzing the trade-offs between Protobuf, Avro, and JSON Schema, a natural question emerges: <i>what would the ideal schema language look like?</i>
</p>

<p>
This section is speculative.  The language I describe &#x2013; let's call it <b>USL</b> (Universal Schema Language) &#x2013; doesn't exist.  But the design exercise is valuable because it crystallizes what we've learned about the strengths and weaknesses of existing approaches.
</p>
</div>
<div id="outline-container-design-principles" class="outline-3">
<h3 id="design-principles"><span class="section-number-3">11.1.</span> Design Principles</h3>
<div class="outline-text-3" id="text-design-principles">
<p>
USL would be built on these principles:
</p>

<ol class="org-ol">
<li><b>Schema is the source of truth</b> &#x2013; all code, documentation, and validation is derived from the schema</li>
<li><b>Wire format is a choice, not an assumption</b> &#x2013; the same schema can target JSON, binary, or any other format</li>
<li><b>Validation constraints are first-class</b> &#x2013; not an afterthought bolted on to a type system</li>
<li><b>Evolution rules are formal and enforceable</b> &#x2013; not conventions documented in a wiki</li>
<li><b>The schema is self-describing</b> &#x2013; it can be transmitted alongside data</li>
<li><b>Human readability is non-negotiable</b> &#x2013; if it's harder to read than code, people won't use it</li>
<li><b>Code generation is excellent</b> &#x2013; generated code is idiomatic, not a thin wrapper around generic maps</li>
</ol>
</div>
</div>
<div id="outline-container-type-system" class="outline-3">
<h3 id="type-system"><span class="section-number-3">11.2.</span> Type System</h3>
<div class="outline-text-3" id="text-type-system">
<p>
USL's type system would combine the best of all three:
</p>

<div class="org-src-container">
<pre class="src src-text">// USL Schema Definition
namespace ecommerce.v1

// Enums with explicit wire values (like Protobuf) but human-friendly
enum DiscountType {
  PERCENTAGE = 1
  FIXED = 2
  BOGO = 3
}

// Records with field tags (like Protobuf) + constraints (like JSON Schema) + defaults (like Avro)
record OrderItem {
  1: string product_id
  2: int32 quantity [min: 1, max: 10000, doc: "Number of units ordered"]
  3: float64 unit_price [gt: 0, doc: "Price per unit in base currency"]
}

record Order {
  1: string id [pattern: "^ord_[a-zA-Z0-9]{12}$"]
  2: string customer_id [min_length: 1]
  3: list&lt;OrderItem&gt; items [min_items: 1]
  4: optional DiscountType discount_type
  5: optional float64 discount_value [min: 0]
  6: timestamp created_at
  7: float64 total [min: 0]

  // Conditional constraints (like JSON Schema's if/then)
  invariant "BOGO discount must be zero" {
    when discount_type == BOGO then discount_value == 0
  }

  // Cross-field constraints
  invariant "total matches items" {
    total == sum(items[*].unit_price * items[*].quantity)
      - discount_applied(discount_type, discount_value)
  }
}
</pre>
</div>

<p>
Key features:
</p>

<ul class="org-ul">
<li><b>Field tags</b> (like Protobuf) enable binary encoding and safe field renaming</li>
<li><b>Inline constraints</b> (like JSON Schema) enforce validation at the schema level</li>
<li><b>Optional keyword</b> (like Avro's explicit unions) makes null-safety unambiguous</li>
<li><b>Invariants</b> enable cross-field and conditional validation that none of the existing languages support natively</li>
<li><b>Documentation</b> is part of the schema, not in comments that get lost</li>
</ul>
</div>
</div>
<div id="outline-container-beyond-the-type-system" class="outline-3">
<h3 id="beyond-the-type-system"><span class="section-number-3">11.3.</span> Beyond the Type System</h3>
<div class="outline-text-3" id="text-beyond-the-type-system">
<p>
The type system above is the core, but USL would go further in three areas that existing languages handle poorly:
</p>

<ol class="org-ol">
<li><b>Formal evolution rules.</b>  Instead of documenting evolution conventions in a wiki, USL would let you declare rules like <code>allow add_field with default</code>, <code>deny remove_field</code>, <code>deny reuse_tag</code>, <code>allow widen_type</code> (int32 → int64), <code>allow relax_constraint</code>, <code>deny tighten_constraint</code> &#x2013; and the toolchain would enforce them automatically at registration time.  Avro's schema resolution gets closest to this, but the rules are implicit in the specification rather than configurable per-schema.</li>

<li><b>Wire format independence.</b>  The same <code>Order</code> schema would target Protobuf binary for RPC, Avro encoding for Kafka, JSON for REST APIs, and Parquet schemas for data lakes.  Each target would specify naming conventions, timestamp encoding, and null handling.  One schema, many wire formats.  This is the holy grail of schema languages, and no existing tool achieves it fully.</li>

<li><b>Idiomatic code generation.</b>  USL wouldn't generate generic types &#x2013; it would generate <i>community-standard</i> types.  Python gets Pydantic models with <code>@field_validator</code>.  TypeScript gets Zod schemas.  Rust gets serde-annotated structs.  Each generated output is idiomatic to its language, not a thin wrapper around a generic map.</li>
</ol>
</div>
</div>
<div id="outline-container-requirements-table" class="outline-3">
<h3 id="requirements-table"><span class="section-number-3">11.4.</span> Requirements Table</h3>
<div class="outline-text-3" id="text-requirements-table">
<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Requirement</th>
<th scope="col" class="org-left">Protobuf</th>
<th scope="col" class="org-left">Avro</th>
<th scope="col" class="org-left">JSON Schema</th>
<th scope="col" class="org-left">USL</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Language-agnostic schema</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Binary wire format</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
<td class="org-left">❌</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">JSON wire format</td>
<td class="org-left">⚠️ (via JSON mapping)</td>
<td class="org-left">⚠️ (via JSON encoding)</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Validation constraints</td>
<td class="org-left">❌</td>
<td class="org-left">⚠️ (logical types)</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Cross-field invariants</td>
<td class="org-left">❌</td>
<td class="org-left">❌</td>
<td class="org-left">⚠️ (if/then)</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Formal evolution rules</td>
<td class="org-left">⚠️ (conventions)</td>
<td class="org-left">✅ (resolution)</td>
<td class="org-left">❌</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Self-describing payloads</td>
<td class="org-left">❌</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Human-readable schema</td>
<td class="org-left">⚠️ (IDL)</td>
<td class="org-left">❌ (JSON)</td>
<td class="org-left">⚠️ (JSON)</td>
<td class="org-left">✅ (custom IDL)</td>
</tr>

<tr>
<td class="org-left">Idiomatic code generation</td>
<td class="org-left">✅</td>
<td class="org-left">⚠️</td>
<td class="org-left">⚠️</td>
<td class="org-left">✅</td>
</tr>

<tr>
<td class="org-left">Wire format independence</td>
<td class="org-left">❌</td>
<td class="org-left">❌</td>
<td class="org-left">❌</td>
<td class="org-left">✅</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-why-doesn-t-usl-exist-" class="outline-3">
<h3 id="why-doesn-t-usl-exist-"><span class="section-number-3">11.5.</span> Why Doesn't USL Exist?</h3>
<div class="outline-text-3" id="text-why-doesn-t-usl-exist-">
<p>
If USL is so obviously better, why hasn't someone built it?
</p>

<p>
Several reasons:
</p>

<ol class="org-ol">
<li><b>Network effects.</b>  Protobuf, Avro, and JSON Schema have massive ecosystems.  A new language starts with zero tools, zero libraries, zero community.  The 10% improvement in any dimension isn't enough to overcome the ecosystem advantage.</li>

<li><b>Different contexts, different priorities.</b>  Google needed Protobuf for internal RPC.  The Hadoop ecosystem needed Avro for schema evolution.  The web needed JSON Schema for API validation.  Each language was optimized for its context.  A "universal" language that serves all contexts equally well might serve none of them <i>best</i>.</li>

<li><b>Specification complexity.</b>  USL as described above is <i>hard</i> to specify correctly.  Cross-field invariants, wire format independence, and formal evolution rules each bring significant specification and implementation complexity.</li>

<li><b>The "second system" trap.</b>  Every engineer who deeply understands Protobuf, Avro, and JSON Schema has thought about building a "better" version.  Most wisely resist.  The ideal schema language is easy to imagine and extremely hard to execute &#x2013; especially the tooling, ecosystem, and community that make a schema language useful.</li>
</ol>

<p>
That said, projects like <a href="https://cuelang.org/">CUE</a>, <a href="https://dhall-lang.org/">Dhall</a>, and <a href="https://smithy.io/">Smithy</a> (AWS) are exploring parts of this design space.  The convergence is happening, just slowly and from different directions.
</p>
</div>
</div>
</div>
<div id="outline-container-when-not-to-use-a-schema-language" class="outline-2">
<h2 id="when-not-to-use-a-schema-language"><span class="section-number-2">12.</span> When NOT to Use a Schema Language&#xa0;&#xa0;&#xa0;<span class="tag"><span class="antipatterns">antipatterns</span>&#xa0;<span class="pragmatism">pragmatism</span></span></h2>
<div class="outline-text-2" id="text-when-not-to-use-a-schema-language">
<p>
I've spent most of this post advocating for schema languages.  In the interest of intellectual honesty, let me present the counter-arguments.
</p>
</div>
<div id="outline-container-valid-reasons-to-skip-schema-languages" class="outline-3">
<h3 id="valid-reasons-to-skip-schema-languages"><span class="section-number-3">12.1.</span> Valid Reasons to Skip Schema Languages</h3>
<div class="outline-text-3" id="text-valid-reasons-to-skip-schema-languages">
<ol class="org-ol">
<li><b>Single-language, single-team projects.</b>  If your entire system is written in Python by one team, Pydantic <i>is</i> your schema language.  The overhead of maintaining <code>.proto</code> files or JSON Schema doesn't pay for itself until you have multiple languages or multiple teams.</li>

<li><b>Prototyping and MVPs.</b>  When you're still figuring out what the product <i>is</i>, rigid schemas are premature.  You'll redesign the data model three times before launch.  Use JSON blobs, iterate fast, and formalize later.</li>

<li><b>Small, stable APIs.</b>  If your API has 5 endpoints that haven't changed in 2 years, the cost of adopting a schema language exceeds the benefit.  The "if it ain't broke" principle applies.</li>

<li><b>Performance-insensitive, validation-heavy domains.</b>  If your primary concern is "reject invalid input with helpful error messages" and performance doesn't matter, a language-specific validator (Pydantic, Zod) gives you better error messages and more expressive constraints than most schema languages.</li>

<li><b>Rapid exploration / data science.</b>  Data scientists exploring datasets don't need a schema language.  They need pandas and a REPL.  Schema languages add value at the <i>boundary</i> between exploratory and production code, not during exploration.</li>
</ol>
</div>
</div>
<div id="outline-container-the-schema-tax" class="outline-3">
<h3 id="the-schema-tax"><span class="section-number-3">12.2.</span> The Schema Tax</h3>
<div class="outline-text-3" id="text-the-schema-tax">
<blockquote class="pull-quote pull-left">
<p>
Every abstraction has a cost.  The question isn't whether schema languages are better in the abstract &#x2013; it's whether they're worth the overhead for <i>your</i> system, <i>today</i>. &#x2014;Pragmatic engineering
</p>
</blockquote>

<p>
Schema languages impose real costs:
</p>

<ul class="org-ul">
<li><b>Learning curve</b>: Everyone on the team needs to understand the schema language and its tooling</li>
<li><b>Build complexity</b>: Code generation adds a step to every build</li>
<li><b>Dependency management</b>: Generated code needs to be versioned and distributed</li>
<li><b>Schema-first friction</b>: You can't just "add a field" &#x2013; you need to update the schema, regenerate code, and verify compatibility</li>
<li><b>Debugging indirection</b>: When something goes wrong, you're debugging generated code, not code you wrote</li>
</ul>

<p>
These costs are <i>fixed</i> &#x2013; they don't scale with system size.  The benefits are <i>proportional</i> to system size.  This means there's a crossover point: below a certain complexity, schema languages cost more than they save.
</p>

<p>
I've seen this go wrong firsthand.  A three-person startup I advised adopted Protobuf and gRPC for their internal APIs &#x2013; between two Python services maintained by the same team.  Within a month, a third of their development time was going to <code>protoc</code> plugin issues, generated code version mismatches, and debugging serialization errors that would have been trivial with plain JSON.  They had imported Google-scale infrastructure to solve a problem they didn't have.  They migrated back to FastAPI with Pydantic models and shipped their MVP three months faster.  Schema languages are powerful, but power has carrying costs.
</p>
</div>
</div>
<div id="outline-container-my-rule-of-thumb" class="outline-3">
<h3 id="my-rule-of-thumb"><span class="section-number-3">12.3.</span> My Rule of Thumb</h3>
<div class="outline-text-3" id="text-my-rule-of-thumb">
<p>
Use a schema language when <i>any</i> of these are true:
</p>

<ul class="org-ul">
<li>You have <b>2+ languages</b> sharing data models</li>
<li>You have <b>3+ teams</b> consuming the same data</li>
<li>You have <b>Kafka or another message broker</b> where producers and consumers evolve independently</li>
<li>You're building a <b>public API</b> where clients you don't control need to understand your data</li>
<li>You're integrating with <b>AI/LLM tools</b> that require structured output definitions</li>
<li>Your data models are <b>regulated</b> (healthcare, finance) and require formal specification</li>
</ul>

<p>
Otherwise, start with language-specific tools and migrate when the pain becomes obvious.  The migration is always possible &#x2013; it's just easier to do it before you have 50 services.
</p>
</div>
</div>
</div>
<div id="outline-container-the-future--ai--convergence--and-what-comes-next" class="outline-2">
<h2 id="the-future--ai--convergence--and-what-comes-next"><span class="section-number-2">13.</span> The Future: AI, Convergence, and What Comes Next&#xa0;&#xa0;&#xa0;<span class="tag"><span class="ai">ai</span>&#xa0;<span class="futureWork">futureWork</span>&#xa0;<span class="predictions">predictions</span></span></h2>
<div class="outline-text-2" id="text-the-future--ai--convergence--and-what-comes-next">
<p>
The schema language landscape is evolving faster than at any point in its history, driven by two forces: the AI revolution and the convergence of previously separate ecosystems.
</p>
</div>
<div id="outline-container-ai-as-schema-author" class="outline-3">
<h3 id="ai-as-schema-author"><span class="section-number-3">13.1.</span> AI as Schema Author</h3>
<div class="outline-text-3" id="text-ai-as-schema-author">
<p>
We're already seeing LLMs used to <i>generate</i> schemas:
</p>

<div class="org-src-container">
<pre class="src src-text">User: I need a schema for a restaurant reservation system.
      It should handle party size, date/time, special requests,
      and dietary restrictions.

LLM:  Here's your Protobuf schema:

      message Reservation {
        string id = 1;
        string restaurant_id = 2;
        string customer_name = 3;
        int32 party_size = 4;
        google.protobuf.Timestamp reservation_time = 5;
        repeated string dietary_restrictions = 6;
        optional string special_requests = 7;
        ReservationStatus status = 8;
      }

      enum ReservationStatus {
        RESERVATION_STATUS_UNSPECIFIED = 0;
        RESERVATION_STATUS_PENDING = 1;
        RESERVATION_STATUS_CONFIRMED = 2;
        RESERVATION_STATUS_CANCELLED = 3;
        RESERVATION_STATUS_COMPLETED = 4;
      }
</pre>
</div>

<p>
But this is only the beginning.  Future AI-schema interactions will include:
</p>

<ul class="org-ul">
<li><b>Schema inference from data samples</b>: Feed the AI a JSON payload and get a validated schema back</li>
<li><b>Schema evolution suggestions</b>: "You're adding a <code>coupon_code</code> field.  Based on your existing schema, I recommend making it optional with a default of null and adding a <code>CouponType</code> enum for future extensibility"</li>
<li><b>Cross-format translation</b>: "Convert this Protobuf schema to Avro while preserving compatibility semantics"</li>
<li><b>Automated migration</b>: "Here's a migration plan for moving from JSON Schema to Protobuf, including a compatibility layer for the transition period"</li>
</ul>
</div>
</div>
<div id="outline-container-the-convergence-trend" class="outline-3">
<h3 id="the-convergence-trend"><span class="section-number-3">13.2.</span> The Convergence Trend</h3>
<div class="outline-text-3" id="text-the-convergence-trend">
<p>
The three major schema languages are converging:
</p>

<ul class="org-ul">
<li><b>Protobuf</b> added <code>optional</code> back in proto3.15 (borrowing from Avro's explicit null handling)</li>
<li><b>Avro</b> added code generation improvements (borrowing from Protobuf's strength)</li>
<li><b>JSON Schema</b> added <code>unevaluatedProperties</code> (borrowing from Protobuf's strict typing)</li>
<li><b>Confluent Schema Registry</b> now supports Protobuf and JSON Schema alongside Avro</li>
<li><b>Buf Schema Registry</b> has hints of supporting additional formats</li>
<li><b>OpenAPI 3.1</b> fully adopted JSON Schema Draft 2020-12 (bridging API and schema worlds)</li>
</ul>

<p>
The boundaries between these ecosystems are blurring.  Ten years from now, we may look back and see this period as the beginning of a unified schema ecosystem, much as SQL standardized the relational database world despite fierce competition between vendors.
</p>
</div>
</div>
<div id="outline-container-timeline-of-key-events-and-predictions" class="outline-3">
<h3 id="timeline-of-key-events-and-predictions"><span class="section-number-3">13.3.</span> Timeline of Key Events and Predictions</h3>
<div class="outline-text-3" id="text-timeline-of-key-events-and-predictions">
<div id="sl_timeline" class="plotly-plot"></div>

<p>
The diamond markers represent predictions.  Three trends I'm watching:
</p>

<ol class="org-ol">
<li><b>Multi-format registries</b> (2025-2026): Confluent and Buf are both expanding format support.  Within a few years, the choice of registry will be decoupled from the choice of schema language.</li>

<li><b>AI schema copilots</b> (2027-2028): Just as GitHub Copilot transformed code writing, AI tools will transform schema design.  Expect AI that suggests schema evolution strategies, identifies potential compatibility issues, and generates migration plans.</li>

<li><b>Schema convergence</b> (2029+): The longest-term prediction and the least certain.  The historical pattern (SQL for databases, HTTP for web) suggests that schema languages will eventually converge toward a unified standard.  But the counter-pattern (JavaScript frameworks) suggests permanent fragmentation is equally likely.</li>
</ol>
</div>
</div>
<div id="outline-container-what-s-coming-sooner" class="outline-3">
<h3 id="what-s-coming-sooner"><span class="section-number-3">13.4.</span> What's Coming Sooner</h3>
<div class="outline-text-3" id="text-what-s-coming-sooner">
<p>
More concretely, here's what I expect in the next 2-3 years:
</p>

<ul class="org-ul">
<li><b>JSON Schema as a compilation target.</b>  More tools will compile higher-level schema definitions to JSON Schema, which then drives code generation and validation.  JSON Schema becomes the "assembly language" of schema definitions.</li>

<li><b>Schema-aware observability.</b>  APM tools (Datadog, Honeycomb) will integrate schema information to provide richer insights.  "This endpoint's response time increased because the <code>items</code> array is 3x larger than the schema's recommended <code>maxItems</code>."</li>

<li><b>AI-native schema evolution.</b>  Instead of manually designing schema migrations, you'll describe the desired change in natural language and get a migration plan with backward-compatibility guarantees.</li>

<li><b>Edge-native schemas.</b>  As computation moves to the edge (Cloudflare Workers, Deno Deploy), schemas will be used to validate requests at the CDN layer before they reach application servers, reducing latency and protecting backends from malformed requests.</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">14.</span> Conclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="reflection">reflection</span></span></h2>
<div class="outline-text-2" id="text-conclusion">
<p>
We've covered a lot of ground.  Let me distill the key insights.
</p>
</div>
<div id="outline-container-the-core-argument" class="outline-3">
<h3 id="the-core-argument"><span class="section-number-3">14.1.</span> The Core Argument</h3>
<div class="outline-text-3" id="text-the-core-argument">
<p>
Schema languages solve a coordination problem, not a technical one.  Any competent engineer can define a data model in their language of choice.  The challenge is ensuring that 5, 50, or 500 engineers across multiple languages, teams, and time zones all agree on what that model looks like &#x2013; and continue to agree as it evolves.
</p>

<p>
Protobuf, Avro, and JSON Schema each represent a different answer to "what should we optimize for?"
</p>

<ul class="org-ul">
<li><b>Protobuf says</b>: optimize for performance and strong code generation</li>
<li><b>Avro says</b>: optimize for schema evolution and self-describing data</li>
<li><b>JSON Schema says</b>: optimize for validation expressiveness and human readability</li>
</ul>

<p>
None is universally "best."  The right choice depends on your context &#x2013; your languages, your architecture, your team structure, and your primary use case.
</p>
</div>
</div>
<div id="outline-container-what-i-ve-learned" class="outline-3">
<h3 id="what-i-ve-learned"><span class="section-number-3">14.2.</span> What I've Learned</h3>
<div class="outline-text-3" id="text-what-i-ve-learned">
<p>
After years of working with all three schema languages across different organizations, my strongest conviction is this: <b>the cost of not having a single source of truth is always higher than you think, and it's always paid later than you'd like</b>.
</p>

<p>
Model drift is the kind of problem that doesn't hurt until it <i>really</i> hurts.  And by the time it hurts, you have months of corrupted data, dozens of divergent implementations, and a migration that "should take a week" but takes a quarter.
</p>

<p>
If your system involves multiple languages or multiple teams sharing data, invest in a schema language early.  The best time was when you started the project.  The second-best time is now.
</p>
</div>
</div>
<div id="outline-container-a-closing-thought" class="outline-3">
<h3 id="a-closing-thought"><span class="section-number-3">14.3.</span> A Closing Thought</h3>
<div class="outline-text-3" id="text-a-closing-thought">
<p class="verse">
The competent programmer is fully aware of the strictly limited size of his own skull;<br />
therefore he approaches the programming task in full humility,<br />
and among other things he avoids clever tricks like the plague.<br />
&#xa0;&#xa0;&#xa0;&#xa0;&#x2014;Edsger W. Dijkstra, <i>The Humble Programmer</i> (1972)<br />
</p>

<p>
Schema languages are, in their essence, an act of humility.  They acknowledge that no single developer, no single team, no single language can hold the complete picture of a system's data model.  Instead of trusting human coordination (which fails at scale), they encode the model in a format that machines can verify, humans can read, and organizations can evolve.
</p>

<p>
The cleverest possible approach to shared data models is to define them in each language using that language's most expressive features &#x2013; custom validators, rich type systems, elegant abstractions.  The humble approach is to define them once, in a simple format that any language can consume, and let a machine generate the rest.  The clever approach produces beautiful code.  The humble approach produces correct systems.
</p>

<p>
That's not clever.  It's humble.  And in software engineering, humility scales.
</p>
</div>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">15.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
This post compares the three dominant schema languages for data modelling &#x2013; Protocol Buffers, Apache Avro, and JSON Schema.  The <a href="https://www.chiply.dev/#why-a-single-source-of-truth-">Single Source of Truth</a> section motivates the problem: in polyglot systems, defining models independently in each language leads to model drift and silent data corruption, where different parts of the system disagree about what the data looks like.  <a href="https://www.chiply.dev/#why-not-just-use-pydantic---zod---your-framework-s-types-">Why Not Pydantic / Zod?</a> addresses the common objection that language-specific tools suffice, arguing that Pydantic, Zod, and similar libraries are best used as <i>consumers</i> of schemas, not <i>sources</i>.  The <a href="https://www.chiply.dev/#the-landscape-of-schema-languages">Landscape</a> section surveys 20+ schema languages across categories including binary serialization, validation, API definition, and configuration.  Three deep dives follow: <a href="https://www.chiply.dev/#protobuf-deep-dive">Protobuf</a> excels at high-performance RPC with its compiled binary format and gRPC integration, <a href="https://www.chiply.dev/#avro-deep-dive">Avro</a> dominates event streaming with its self-describing payloads and schema resolution that lets producers and consumers evolve independently, and <a href="https://www.chiply.dev/#json-schema-deep-dive">JSON Schema</a> leads in web API validation and has found an unexpected second life as the interface definition language for AI tools.  The <a href="https://www.chiply.dev/#head-to-head-comparison">Head-to-Head</a> comparison reveals that no language dominates all dimensions &#x2013; choose based on whether you prioritize performance (Protobuf), schema evolution (Avro), or validation richness (JSON Schema).  <a href="https://www.chiply.dev/#war-stories--when-schemas-fail">War Stories</a> drive the point home with real failures including Knight Capital's $440M loss from schema drift.  <a href="https://www.chiply.dev/#the-ecosystem-unlocked">The Ecosystem</a> section shows that schema languages unlock far more than just types &#x2013; code generation, documentation, registries, and contract testing create a flywheel of organizational coordination.  Finally, <a href="https://www.chiply.dev/#designing-the-ideal-schema-language">the USL design exercise</a> imagines a future language combining the best of all three, and <a href="https://www.chiply.dev/#the-future--ai--convergence--and-what-comes-next">the Future section</a> predicts AI-assisted schema design and gradual convergence of the ecosystem.
</p>
</div>
</div>
<div id="footnotes">
<h2 class="footnotes">Footnotes: </h2>
<div id="text-footnotes">

<div class="footdef"><sup><a id="fn.1" class="footnum" href="https://www.chiply.dev/#fnr.1" role="doc-backlink">1</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
A 2018 Stripe/Harris Poll survey reported that developers spend ~17 hours per week dealing with technical debt, including data model inconsistencies.  The methodology of this widely-cited survey has been questioned &#x2013; the sample was small and self-reported &#x2013; but the directional finding (data model issues are a significant source of developer toil) is consistent with other industry surveys.
</p></div></div>

<div class="footdef"><sup><a id="fn.2" class="footnum" href="https://www.chiply.dev/#fnr.2" role="doc-backlink">2</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Monte Carlo Data, "The State of Data Quality" (2020).  The survey of 300+ data engineering teams found that 77% experienced data quality incidents, with schema changes and data model drift among the leading causes.  More recent surveys (e.g., Monte Carlo's 2022 and 2023 reports) continue to show schema changes as a top contributor to data quality incidents.
</p></div></div>

<div class="footdef"><sup><a id="fn.3" class="footnum" href="https://www.chiply.dev/#fnr.3" role="doc-backlink">3</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Protocol Buffers were developed internally at Google starting in 2001.  The original design is attributed to Jeff Dean, Sanjay Ghemawat, and others on the infrastructure team.  The public release (proto2) came in July 2008.  See: <a href="https://protobuf.dev/overview/">Protocol Buffers Documentation</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.4" class="footnum" href="https://www.chiply.dev/#fnr.4" role="doc-backlink">4</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Doug Cutting created Apache Avro in 2009 as part of the Hadoop project.  The design was motivated by the limitations of Java's Writables serialization and the desire for a language-neutral, schema-evolution-friendly format.  See: <a href="https://avro.apache.org/">Apache Avro Documentation</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.5" class="footnum" href="https://www.chiply.dev/#fnr.5" role="doc-backlink">5</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
Avro schema resolution is described in detail in the <a href="https://avro.apache.org/docs/current/specification/#schema-resolution">Avro Specification</a>.  The rules for compatible schema evolution are subtle and worth reading carefully before deploying Avro in production.
</p></div></div>

<div class="footdef"><sup><a id="fn.6" class="footnum" href="https://www.chiply.dev/#fnr.6" role="doc-backlink">6</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
JSON Schema was first proposed by Kris Zyp in 2009 as an Internet-Draft.  The specification has evolved through multiple draft versions, with Draft 2020-12 being the current stable release.  See: <a href="https://json-schema.org/">JSON Schema Specification</a>.
</p></div></div>

<div class="footdef"><sup><a id="fn.7" class="footnum" href="https://www.chiply.dev/#fnr.7" role="doc-backlink">7</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
These values are <i>illustrative approximations</i>, not precise measurements from a single benchmark run.  They are drawn from the range of published results across multiple benchmarks including <a href="https://github.com/alecthomas/go_serialization_benchmarks">alecthomas/go_serialization_benchmarks</a>, <a href="https://github.com/eishay/jvm-serializers/wiki">eishay/jvm-serializers</a>, and various conference talks comparing serialization formats.  The absolute values vary significantly by language, hardware, message complexity, and library implementation.  What is consistent across virtually all benchmarks is the <i>relative ordering</i>: Protobuf fastest, Avro middle, JSON slowest, with JSON message sizes 4-6x larger than binary formats.  Run benchmarks on your own hardware and message shapes before making architecture decisions based on performance.
</p></div></div>

<div class="footdef"><sup><a id="fn.8" class="footnum" href="https://www.chiply.dev/#fnr.8" role="doc-backlink">8</a></sup> <div class="footpara" role="doc-footnote"><p class="footpara">
The Knight Capital incident is documented in the SEC's administrative proceeding (File No. 3-15570, October 16, 2013).  The total loss was $461.1 million in approximately 45 minutes of trading.  The root cause was a deployment error that left one of eight servers running obsolete code.  This is not a schema drift failure in the narrow sense, but it illustrates how systems fail when components disagree about message semantics without a machine-enforced consistency check.
</p></div></div>


</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Literate Programming: A Primer with Python and Org-Babel</title>
      <link>https://www.chiply.dev/test-literate-python</link>
      <guid isPermaLink="true">https://www.chiply.dev/test-literate-python</guid>
      <pubDate>Sun, 25 Jan 2026 12:43:00 GMT</pubDate>
      <author>Charlie Holland (main)</author>
      <description>Literate programming, a concept introduced by Donald Knuth in 1984, inverts the traditional relationship between code and documentation. Rather than writing code with occasional comments, we write a d...</description>
      <content:encoded><![CDATA[

<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">1.</span> Introduction&#xa0;&#xa0;&#xa0;<span class="tag"><span class="literateProgramming">literateProgramming</span></span></h2>
<div class="outline-text-2" id="text-introduction">
<p>
Literate programming, a concept introduced by Donald Knuth in 1984, inverts the traditional relationship between code and documentation.  Rather than writing code with occasional comments, we write a document with embedded code &#x2014; the program is explained in human-readable prose, and the machine-executable source is extracted (or <i>tangled</i>) from it.
</p>

<p>
Org-babel, the code execution and tangling engine built into Emacs Org-mode, provides a natural environment for literate programming.  Code blocks can be named, referenced by other blocks, and tangled into one or more source files.
</p>

<p>
In this post, we will construct a small but complete Python program using these techniques.
</p>
</div>
</div>
<div id="outline-container-the-problem" class="outline-2">
<h2 id="the-problem"><span class="section-number-2">2.</span> The Problem&#xa0;&#xa0;&#xa0;<span class="tag"><span class="primes">primes</span>&#xa0;<span class="algorithms">algorithms</span></span></h2>
<div class="outline-text-2" id="text-the-problem">
<p>
We will implement the <b>Sieve of Eratosthenes</b>, one of the oldest known algorithms, to find all prime numbers up to a given limit.  The program will:
</p>

<ol class="org-ol">
<li>Generate primes using the sieve</li>
<li>Display basic statistics about the distribution</li>
<li>Write the results to a file</li>
</ol>

<p>
This is a small program, but it demonstrates how org-babel can decompose a program into logical, documented sections.
</p>
</div>
</div>
<div id="outline-container-building-the-program" class="outline-2">
<h2 id="building-the-program"><span class="section-number-2">3.</span> Building the Program&#xa0;&#xa0;&#xa0;<span class="tag"><span class="orgBabel">orgBabel</span>&#xa0;<span class="tangling">tangling</span></span></h2>
<div class="outline-text-2" id="text-building-the-program">
</div>
<div id="outline-container-main-entry-point" class="outline-3">
<h3 id="main-entry-point"><span class="section-number-3">3.1.</span> The Main Entry Point</h3>
<div class="outline-text-3" id="text-main-entry-point">
<p>
We begin with the overall structure of our program.  The <code>noweb</code> syntax (<code>&lt;&lt;block-name&gt;&gt;</code>) allows us to reference other named code blocks, which will be expanded during tangling.
</p>

<div class="org-src-container">
<pre class="src src-python" id="org37e6ce8"><span style="color: #b332b332b332;">"""
Sieve of Eratosthenes - A literate programming example.
Tangled from post-literate-python.org
"""</span>

<span style="color: #531ab6;">import</span> math
<span style="color: #531ab6;">from</span> pathlib <span style="color: #531ab6;">import</span> Path

<span style="color: #531ab6;">def</span> <span style="color: #721045;">sieve_of_eratosthenes</span><span style="color: #000000;">(</span>limit: <span style="color: #8f0075;">int</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #8f0075;">list</span><span style="color: #000000;">[</span><span style="color: #8f0075;">int</span><span style="color: #000000;">]</span>:
    <span style="color: #b332b332b332;">"""Return all primes up to `limit` using the Sieve of Eratosthenes."""</span>
    <span style="color: #531ab6;">if</span> limit <span style="color: #000000;">&lt;</span> 2:
        <span style="color: #531ab6;">return</span> <span style="color: #000000;">[]</span>

    <span style="color: #7fff7fff7fff;"># </span><span style="color: #7fff7fff7fff;">Initialize boolean array - True means "is prime"
</span>    <span style="color: #005e8b;">is_prime</span> <span style="color: #000000;">=</span> <span style="color: #000000;">[</span><span style="color: #0000b0;">True</span><span style="color: #000000;">]</span> <span style="color: #000000;">*</span> <span style="color: #000000;">(</span>limit <span style="color: #000000;">+</span> 1<span style="color: #000000;">)</span>
    <span style="color: #005e8b;">is_prime</span><span style="color: #000000;">[</span>0<span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #005e8b;">is_prime</span><span style="color: #000000;">[</span>1<span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #0000b0;">False</span>

    <span style="color: #7fff7fff7fff;"># </span><span style="color: #7fff7fff7fff;">Sieve: eliminate multiples of each prime
</span>    <span style="color: #531ab6;">for</span> p <span style="color: #531ab6;">in</span> <span style="color: #8f0075;">range</span><span style="color: #000000;">(</span>2, <span style="color: #8f0075;">int</span><span style="color: #dd22dd;">(</span>math.sqrt<span style="color: #008899;">(</span>limit<span style="color: #008899;">)</span><span style="color: #dd22dd;">)</span> <span style="color: #000000;">+</span> 1<span style="color: #000000;">)</span>:
        <span style="color: #531ab6;">if</span> is_prime<span style="color: #000000;">[</span>p<span style="color: #000000;">]</span>:
            <span style="color: #531ab6;">for</span> multiple <span style="color: #531ab6;">in</span> <span style="color: #8f0075;">range</span><span style="color: #000000;">(</span>p <span style="color: #000000;">*</span> p, limit <span style="color: #000000;">+</span> 1, p<span style="color: #000000;">)</span>:
                <span style="color: #005e8b;">is_prime</span><span style="color: #000000;">[</span>multiple<span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #0000b0;">False</span>

    <span style="color: #531ab6;">return</span> <span style="color: #000000;">[</span>n <span style="color: #531ab6;">for</span> n <span style="color: #531ab6;">in</span> <span style="color: #8f0075;">range</span><span style="color: #dd22dd;">(</span>2, limit <span style="color: #000000;">+</span> 1<span style="color: #dd22dd;">)</span> <span style="color: #531ab6;">if</span> is_prime<span style="color: #dd22dd;">[</span>n<span style="color: #dd22dd;">]</span><span style="color: #000000;">]</span>

<span style="color: #531ab6;">def</span> <span style="color: #721045;">print_statistics</span><span style="color: #000000;">(</span>primes: <span style="color: #8f0075;">list</span><span style="color: #dd22dd;">[</span><span style="color: #8f0075;">int</span><span style="color: #dd22dd;">]</span>, limit: <span style="color: #8f0075;">int</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #0000b0;">None</span>:
    <span style="color: #b332b332b332;">"""Print statistics about the prime distribution."""</span>
    <span style="color: #005e8b;">count</span> <span style="color: #000000;">=</span> <span style="color: #8f0075;">len</span><span style="color: #000000;">(</span>primes<span style="color: #000000;">)</span>
    <span style="color: #005e8b;">estimate</span> <span style="color: #000000;">=</span> limit <span style="color: #000000;">/</span> math.log<span style="color: #000000;">(</span>limit<span style="color: #000000;">)</span> <span style="color: #531ab6;">if</span> limit <span style="color: #000000;">&gt;</span> 1 <span style="color: #531ab6;">else</span> 0

    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Primes up to </span>{limit}<span style="color: #3548cf;">: </span>{count}<span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Prime Number Theorem estimate: </span>{estimate:.1f}<span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Ratio (actual/estimate): </span>{count <span style="color: #000000;">/</span> estimate:.4f}<span style="color: #3548cf;">"</span> <span style="color: #531ab6;">if</span> estimate <span style="color: #531ab6;">else</span> <span style="color: #3548cf;">""</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Largest prime found: </span>{primes[<span style="color: #000000;">-</span>1]}<span style="color: #3548cf;">"</span> <span style="color: #531ab6;">if</span> primes <span style="color: #531ab6;">else</span> <span style="color: #3548cf;">"No primes found"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Prime density: </span>{count <span style="color: #000000;">/</span> limit <span style="color: #000000;">*</span> 100:.1f}<span style="color: #3548cf;">%"</span><span style="color: #000000;">)</span>

<span style="color: #531ab6;">def</span> <span style="color: #721045;">write_results</span><span style="color: #000000;">(</span>primes: <span style="color: #8f0075;">list</span><span style="color: #dd22dd;">[</span><span style="color: #8f0075;">int</span><span style="color: #dd22dd;">]</span>, filename: <span style="color: #8f0075;">str</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #0000b0;">None</span>:
    <span style="color: #b332b332b332;">"""Write primes to a file, one per line."""</span>
    <span style="color: #005e8b;">output_path</span> <span style="color: #000000;">=</span> Path<span style="color: #000000;">(</span>filename<span style="color: #000000;">)</span>
    output_path.write_text<span style="color: #000000;">(</span><span style="color: #3548cf;">"</span><span style="color: #0000b0;">\n</span><span style="color: #3548cf;">"</span>.join<span style="color: #dd22dd;">(</span><span style="color: #8f0075;">str</span><span style="color: #008899;">(</span>p<span style="color: #008899;">)</span> <span style="color: #531ab6;">for</span> p <span style="color: #531ab6;">in</span> primes<span style="color: #dd22dd;">)</span> <span style="color: #000000;">+</span> <span style="color: #3548cf;">"</span><span style="color: #0000b0;">\n</span><span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Wrote </span>{<span style="color: #8f0075;">len</span>(primes)}<span style="color: #3548cf;"> primes to </span>{output_path}<span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>

<span style="color: #531ab6;">if</span> <span style="color: #8f0075;">__name__</span> <span style="color: #000000;">==</span> <span style="color: #3548cf;">"__main__"</span>:
    <span style="color: #005e8b;">limit</span> <span style="color: #000000;">=</span> 100
    <span style="color: #005e8b;">primes</span> <span style="color: #000000;">=</span> sieve_of_eratosthenes<span style="color: #000000;">(</span>limit<span style="color: #000000;">)</span>
    print_statistics<span style="color: #000000;">(</span>primes, limit<span style="color: #000000;">)</span>
    write_results<span style="color: #000000;">(</span>primes, <span style="color: #3548cf;">"primes_output.txt"</span><span style="color: #000000;">)</span>
</pre>
</div>

<p>
Notice how this reads almost like pseudocode.  Each <code>&lt;&lt;...&gt;&gt;</code> reference points to a named block defined below.
</p>
</div>
</div>
<div id="outline-container-imports-section" class="outline-3">
<h3 id="imports-section"><span class="section-number-3">3.2.</span> Imports</h3>
<div class="outline-text-3" id="text-imports-section">
<p>
We need only the <code>math</code> module for our square root computation in the sieve:
</p>

<div class="org-src-container">
<pre class="src src-python" id="org225e28b"><span style="color: #531ab6;">import</span> math
<span style="color: #531ab6;">from</span> pathlib <span style="color: #531ab6;">import</span> Path
</pre>
</div>
</div>
</div>
<div id="outline-container-sieve-algorithm" class="outline-3">
<h3 id="sieve-algorithm"><span class="section-number-3">3.3.</span> The Sieve Algorithm</h3>
<div class="outline-text-3" id="text-sieve-algorithm">
<p>
The Sieve of Eratosthenes works by iteratively marking the multiples of each prime, starting from 2.  For each number \(p\), we only need to begin marking at \(p^2\), since smaller multiples will have already been eliminated by smaller primes.
</p>

<div class="org-src-container">
<pre class="src src-python" id="org5522dfa"><span style="color: #531ab6;">def</span> <span style="color: #721045;">sieve_of_eratosthenes</span><span style="color: #000000;">(</span>limit: <span style="color: #8f0075;">int</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #8f0075;">list</span><span style="color: #000000;">[</span><span style="color: #8f0075;">int</span><span style="color: #000000;">]</span>:
    <span style="color: #b332b332b332;">"""Return all primes up to `limit` using the Sieve of Eratosthenes."""</span>
    <span style="color: #531ab6;">if</span> limit <span style="color: #000000;">&lt;</span> 2:
        <span style="color: #531ab6;">return</span> <span style="color: #000000;">[]</span>

    <span style="color: #7fff7fff7fff;"># </span><span style="color: #7fff7fff7fff;">Initialize boolean array - True means "is prime"
</span>    <span style="color: #005e8b;">is_prime</span> <span style="color: #000000;">=</span> <span style="color: #000000;">[</span><span style="color: #0000b0;">True</span><span style="color: #000000;">]</span> <span style="color: #000000;">*</span> <span style="color: #000000;">(</span>limit <span style="color: #000000;">+</span> 1<span style="color: #000000;">)</span>
    <span style="color: #005e8b;">is_prime</span><span style="color: #000000;">[</span>0<span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #005e8b;">is_prime</span><span style="color: #000000;">[</span>1<span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #0000b0;">False</span>

    <span style="color: #7fff7fff7fff;"># </span><span style="color: #7fff7fff7fff;">Sieve: eliminate multiples of each prime
</span>    <span style="color: #531ab6;">for</span> p <span style="color: #531ab6;">in</span> <span style="color: #8f0075;">range</span><span style="color: #000000;">(</span>2, <span style="color: #8f0075;">int</span><span style="color: #dd22dd;">(</span>math.sqrt<span style="color: #008899;">(</span>limit<span style="color: #008899;">)</span><span style="color: #dd22dd;">)</span> <span style="color: #000000;">+</span> 1<span style="color: #000000;">)</span>:
        <span style="color: #531ab6;">if</span> is_prime<span style="color: #000000;">[</span>p<span style="color: #000000;">]</span>:
            <span style="color: #531ab6;">for</span> multiple <span style="color: #531ab6;">in</span> <span style="color: #8f0075;">range</span><span style="color: #000000;">(</span>p <span style="color: #000000;">*</span> p, limit <span style="color: #000000;">+</span> 1, p<span style="color: #000000;">)</span>:
                <span style="color: #005e8b;">is_prime</span><span style="color: #000000;">[</span>multiple<span style="color: #000000;">]</span> <span style="color: #000000;">=</span> <span style="color: #0000b0;">False</span>

    <span style="color: #531ab6;">return</span> <span style="color: #000000;">[</span>n <span style="color: #531ab6;">for</span> n <span style="color: #531ab6;">in</span> <span style="color: #8f0075;">range</span><span style="color: #dd22dd;">(</span>2, limit <span style="color: #000000;">+</span> 1<span style="color: #dd22dd;">)</span> <span style="color: #531ab6;">if</span> is_prime<span style="color: #dd22dd;">[</span>n<span style="color: #dd22dd;">]</span><span style="color: #000000;">]</span>
</pre>
</div>

<p>
The time complexity is \(O(n \log \log n)\), making this one of the most efficient simple sieves.
</p>
</div>
</div>
<div id="outline-container-statistics-section" class="outline-3">
<h3 id="statistics-section"><span class="section-number-3">3.4.</span> Statistics</h3>
<div class="outline-text-3" id="text-statistics-section">
<p>
The <b>Prime Number Theorem</b> tells us that the number of primes less than \(n\) is approximately \(\frac{n}{\ln n}\).  Let us compare our actual count with this estimate:
</p>

<div class="org-src-container">
<pre class="src src-python" id="orgf76a7a1"><span style="color: #531ab6;">def</span> <span style="color: #721045;">print_statistics</span><span style="color: #000000;">(</span>primes: <span style="color: #8f0075;">list</span><span style="color: #dd22dd;">[</span><span style="color: #8f0075;">int</span><span style="color: #dd22dd;">]</span>, limit: <span style="color: #8f0075;">int</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #0000b0;">None</span>:
    <span style="color: #b332b332b332;">"""Print statistics about the prime distribution."""</span>
    <span style="color: #005e8b;">count</span> <span style="color: #000000;">=</span> <span style="color: #8f0075;">len</span><span style="color: #000000;">(</span>primes<span style="color: #000000;">)</span>
    <span style="color: #005e8b;">estimate</span> <span style="color: #000000;">=</span> limit <span style="color: #000000;">/</span> math.log<span style="color: #000000;">(</span>limit<span style="color: #000000;">)</span> <span style="color: #531ab6;">if</span> limit <span style="color: #000000;">&gt;</span> 1 <span style="color: #531ab6;">else</span> 0

    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Primes up to </span>{limit}<span style="color: #3548cf;">: </span>{count}<span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Prime Number Theorem estimate: </span>{estimate:.1f}<span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Ratio (actual/estimate): </span>{count <span style="color: #000000;">/</span> estimate:.4f}<span style="color: #3548cf;">"</span> <span style="color: #531ab6;">if</span> estimate <span style="color: #531ab6;">else</span> <span style="color: #3548cf;">""</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Largest prime found: </span>{primes[<span style="color: #000000;">-</span>1]}<span style="color: #3548cf;">"</span> <span style="color: #531ab6;">if</span> primes <span style="color: #531ab6;">else</span> <span style="color: #3548cf;">"No primes found"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Prime density: </span>{count <span style="color: #000000;">/</span> limit <span style="color: #000000;">*</span> 100:.1f}<span style="color: #3548cf;">%"</span><span style="color: #000000;">)</span>
</pre>
</div>
</div>
</div>
<div id="outline-container-output" class="outline-3">
<h3 id="output"><span class="section-number-3">3.5.</span> Output</h3>
<div class="outline-text-3" id="text-output">
<p>
Finally, we write the primes to a file, one per line:
</p>

<div class="org-src-container">
<pre class="src src-python" id="orgadcf023"><span style="color: #531ab6;">def</span> <span style="color: #721045;">write_results</span><span style="color: #000000;">(</span>primes: <span style="color: #8f0075;">list</span><span style="color: #dd22dd;">[</span><span style="color: #8f0075;">int</span><span style="color: #dd22dd;">]</span>, filename: <span style="color: #8f0075;">str</span><span style="color: #000000;">)</span> <span style="color: #000000;">-&gt;</span> <span style="color: #0000b0;">None</span>:
    <span style="color: #b332b332b332;">"""Write primes to a file, one per line."""</span>
    <span style="color: #005e8b;">output_path</span> <span style="color: #000000;">=</span> Path<span style="color: #000000;">(</span>filename<span style="color: #000000;">)</span>
    output_path.write_text<span style="color: #000000;">(</span><span style="color: #3548cf;">"</span><span style="color: #0000b0;">\n</span><span style="color: #3548cf;">"</span>.join<span style="color: #dd22dd;">(</span><span style="color: #8f0075;">str</span><span style="color: #008899;">(</span>p<span style="color: #008899;">)</span> <span style="color: #531ab6;">for</span> p <span style="color: #531ab6;">in</span> primes<span style="color: #dd22dd;">)</span> <span style="color: #000000;">+</span> <span style="color: #3548cf;">"</span><span style="color: #0000b0;">\n</span><span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
    <span style="color: #8f0075;">print</span><span style="color: #000000;">(</span>f<span style="color: #3548cf;">"Wrote </span>{<span style="color: #8f0075;">len</span>(primes)}<span style="color: #3548cf;"> primes to </span>{output_path}<span style="color: #3548cf;">"</span><span style="color: #000000;">)</span>
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-tangling-and-running" class="outline-2">
<h2 id="tangling-and-running"><span class="section-number-2">4.</span> Tangling and Running&#xa0;&#xa0;&#xa0;<span class="tag"><span class="workflow">workflow</span></span></h2>
<div class="outline-text-2" id="text-tangling-and-running">
<p>
To extract the executable Python from this document, we <i>tangle</i> it:
</p>

<div class="org-src-container">
<pre class="src src-bash">emacs --batch --eval <span style="color: #3548cf;">"(require 'org)"</span> post-literate-python.org --funcall org-babel-tangle
</pre>
</div>

<p>
This produces <code>literate_primes.py</code>, which can then be run directly:
</p>

<div class="org-src-container">
<pre class="src src-bash">python literate_primes.py
</pre>
</div>

<p>
Expected output:
</p>

<pre class="example" id="org53395a6">
Primes up to 100: 25
Prime Number Theorem estimate: 21.7
Ratio (actual/estimate): 1.1513
Largest prime found: 97
Prime density: 25.0%
Wrote 25 primes to primes_output.txt
</pre>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">5.</span> Conclusion&#xa0;&#xa0;&#xa0;<span class="tag"><span class="reflection">reflection</span></span></h2>
<div class="outline-text-2" id="text-conclusion">
<p>
This small example demonstrates the core workflow of literate programming with Org-babel:
</p>

<ul class="org-ul">
<li><b>Documentation-first</b>: The narrative drives the structure, not the code</li>
<li><b>Noweb references</b>: Code blocks compose into a complete program via named references</li>
<li><b>Tangling</b>: The executable source is extracted mechanically from the document</li>
<li><b>Export</b>: The same document produces both runnable code <i>and</i> readable documentation (this HTML page)</li>
</ul>

<p>
The source for this post and the tangled Python file live side by side &#x2014; the document <i>is</i> the program.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>Design Decisions: Building a Modern Technical Blog</title>
      <link>https://www.chiply.dev/post-design-decisions</link>
      <guid isPermaLink="true">https://www.chiply.dev/post-design-decisions</guid>
      <pubDate>Sat, 24 Jan 2026 06:01:00 GMT</pubDate>
      <author>Charles Baker</author>
      <description>Building a personal blog might seem like a solved problem in 2026, but I wanted something different. Not just another static site generator output, but an interactive reading experience that reflects ...</description>
      <content:encoded><![CDATA[

<div id="outline-container-introduction" class="outline-2">
<h2 id="introduction"><span class="section-number-2">1.</span> Introduction</h2>
<div class="outline-text-2" id="text-introduction">
<p>
Building a personal blog might seem like a solved problem in 2026, but I wanted something different. Not just another static site generator output, but an interactive reading experience that reflects how I think about and organize technical content. This post documents the design decisions that shaped chiply.dev, from the choice of frameworks to security considerations.
</p>

<p>
What started as a simple blog evolved into a platform featuring 3D knowledge graphs, interactive charts, recursive link previews, and a sophisticated content management system built on Emacs org-mode. Along the way, I made dozens of architectural decisions, each with trade-offs worth examining.
</p>
</div>
</div>
<div id="outline-container-framework-selection" class="outline-2">
<h2 id="framework-selection"><span class="section-number-2">2.</span> Framework Selection</h2>
<div class="outline-text-2" id="text-framework-selection">
</div>
<div id="outline-container-why-sveltekit-" class="outline-3">
<h3 id="why-sveltekit-"><span class="section-number-3">2.1.</span> Why SvelteKit?</h3>
<div class="outline-text-3" id="text-why-sveltekit-">
<p>
Starting a technical blog from scratch in 2026, I needed a framework that could support far more than static content. The vision included 3D visualizations, interactive charts, server-side API endpoints for GitHub integration and search, and a reading experience that felt more like an application than a document. Static site generators like Hugo excelled at content but couldn't support the interactivity I wanted. React-based solutions like Next.js carried virtual DOM overhead that seemed wasteful for a content-heavy site. Astro was compelling for its island architecture, but I needed deeper component interactivity than islands easily provide.
</p>

<p>
The task was to find a framework that compiled to minimal client-side JavaScript, provided explicit and predictable reactivity, and colocated frontend and backend code without the complexity of a separate API layer.
</p>

<p>
After evaluating Next.js, Astro, and Hugo against these requirements, I chose SvelteKit 2 with Svelte 5. The result is a framework that eliminates virtual DOM overhead entirely, provides explicit reactivity through runes, and lets me write API endpoints alongside the components that consume them — all while shipping less JavaScript to the client than any React-based alternative.
</p>
</div>
<div id="outline-container-svelte-5-runes" class="outline-4">
<h4 id="svelte-5-runes"><span class="section-number-4">2.1.1.</span> Svelte 5 Runes</h4>
<div class="outline-text-4" id="text-svelte-5-runes">
<p>
Svelte 5 introduced "runes" - a new reactivity system that makes state management explicit and predictable:
</p>

<div class="org-src-container">
<pre class="src src-javascript">// Reactive state declaration
let count = $state(0);

// Derived values (computed properties)
let doubled = $derived(count * 2);

// Component props with destructuring
let { title, author } = $props();
</pre>
</div>

<p>
This approach eliminates the "magic" of Svelte 4's implicit reactivity while remaining concise. Unlike React's <code>useState</code> hooks, runes don't require understanding closures and stale closure bugs.
</p>
</div>
</div>
<div id="outline-container-compiled-output" class="outline-4">
<h4 id="compiled-output"><span class="section-number-4">2.1.2.</span> Compiled Output</h4>
<div class="outline-text-4" id="text-compiled-output">
<p>
Svelte compiles components to vanilla JavaScript at build time, eliminating the runtime overhead of virtual DOM diffing. For a content-heavy blog with interactive visualizations, this results in:
</p>

<ul class="org-ul">
<li>Smaller bundle sizes (no framework runtime shipped to clients)</li>
<li>Faster initial page loads</li>
<li>Better performance on mobile devices</li>
</ul>
</div>
</div>
<div id="outline-container-full-stack-capabilities" class="outline-4">
<h4 id="full-stack-capabilities"><span class="section-number-4">2.1.3.</span> Full-Stack Capabilities</h4>
<div class="outline-text-4" id="text-full-stack-capabilities">
<p>
SvelteKit provides file-based routing with integrated API endpoints:
</p>

<pre class="example" id="org09064fd">
src/routes/
├── +page.svelte          # Home page
├── +layout.svelte        # Root layout
├── [post]/               # Dynamic blog routes
│   ├── +page.svelte      # Post layout
│   └── +page.server.ts   # Server-side data loading
└── api/
    ├── commits/          # GitHub API proxy
    ├── subscribe/        # Newsletter endpoint
    └── preview-proxy/    # Link preview service
</pre>

<p>
This colocation of frontend and backend code simplifies development and deployment.
</p>
</div>
</div>
</div>
<div id="outline-container-build-tooling--vite" class="outline-3">
<h3 id="build-tooling--vite"><span class="section-number-3">2.2.</span> Build Tooling: Vite</h3>
<div class="outline-text-3" id="text-build-tooling--vite">
<p>
With a project featuring heavy interactive components and frequent iteration, slow build tools would have been a serious drag on development velocity. Traditional bundlers like Webpack require full rebuilds on changes, and the lag compounds when you're tweaking 3D graph parameters or CSS transitions and need instant visual feedback.
</p>

<p>
I needed a build tool that provided near-instant feedback during development while still producing optimized production bundles with proper code splitting. Vite 7 powers the development experience with:
</p>

<ul class="org-ul">
<li>Hot Module Replacement (HMR) that updates in milliseconds</li>
<li>Native ES modules during development (no bundling required)</li>
<li>Optimized production builds with code splitting</li>
<li>Built-in TypeScript support</li>
</ul>

<p>
The result is a development server that starts instantly and updates faster than I can switch windows.
</p>
</div>
</div>
<div id="outline-container-deployment--vercel" class="outline-3">
<h3 id="deployment--vercel"><span class="section-number-3">2.3.</span> Deployment: Vercel</h3>
<div class="outline-text-3" id="text-deployment--vercel">
<p>
The blog needed a hosting platform that could handle both static prerendered pages and dynamic API endpoints (GitHub commit fetching, newsletter subscriptions, link preview proxying) without managing separate infrastructure. Self-hosting would mean configuring a server, managing SSL certificates, and dealing with scaling — all distractions from writing content.
</p>

<p>
The task was to find a platform with native SvelteKit support, global edge distribution for API routes, and zero-config deployments from Git pushes. I chose Vercel because:
</p>

<ol class="org-ol">
<li><b>Native SvelteKit support</b>: The <code>@sveltejs/adapter-vercel</code> handles all configuration</li>
<li><b>Edge functions</b>: API routes run close to users globally</li>
<li><b>Automatic previews</b>: Every PR gets a preview deployment</li>
<li><b>Analytics integration</b>: Built-in performance monitoring</li>
</ol>

<p>
The <code>vercel.json</code> configuration enables aggressive caching:
</p>

<div class="org-src-container">
<pre class="src src-json">{
  "headers": [
    {
      "source": "/fonts/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ]
}
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-content-management--org-mode-to-html" class="outline-2">
<h2 id="content-management--org-mode-to-html"><span class="section-number-2">3.</span> Content Management: Org-Mode to HTML</h2>
<div class="outline-text-2" id="text-content-management--org-mode-to-html">
</div>
<div id="outline-container-why-org-mode-" class="outline-3">
<h3 id="why-org-mode-"><span class="section-number-3">3.1.</span> Why Org-Mode?</h3>
<div class="outline-text-3" id="text-why-org-mode-">
<p>
Writing technical blog posts in Markdown quickly exposed its limitations. Code examples couldn't be executed or verified within the document, so they'd drift out of sync with the prose. Markdown's flat heading structure made reorganizing long posts cumbersome. And for literate programming posts — where the code <i>is</i> the content — Markdown had no concept of code tangling or noweb references.
</p>

<p>
I needed an authoring system that could execute code blocks inline, tangle source files from the document, support deep hierarchical organization, and export clean HTML. Rather than using Markdown or a CMS, I write all content in Emacs org-mode. The result is that every code example in a post can be verified at authoring time, documents restructure with a few keystrokes, and the same org file can produce both a blog post and a working program.
</p>
</div>
<div id="outline-container-literate-programming" class="outline-4">
<h4 id="literate-programming"><span class="section-number-4">3.1.1.</span> Literate Programming</h4>
<div class="outline-text-4" id="text-literate-programming">
<p>
Org-mode excels at mixing prose with executable code. Code blocks can be evaluated, and their results embedded in the document:
</p>

<pre class="example" id="orgc1a2a9c">
#+BEGIN_SRC python :results output
import pandas as pd
df = pd.read_csv("data.csv")
print(df.describe())
#+END_SRC
</pre>

<p>
For a technical blog, this means code examples are always tested and accurate.
</p>
</div>
</div>
<div id="outline-container-hierarchical-organization" class="outline-4">
<h4 id="hierarchical-organization"><span class="section-number-4">3.1.2.</span> Hierarchical Organization</h4>
<div class="outline-text-4" id="text-hierarchical-organization">
<p>
Org-mode's outline structure maps naturally to blog post sections. I can collapse, rearrange, and navigate large documents efficiently. The heading hierarchy (<code>*</code>, <code>**</code>, <code>***</code>, etc.) exports cleanly to HTML with proper semantic structure.
</p>
</div>
</div>
<div id="outline-container-export-flexibility" class="outline-4">
<h4 id="export-flexibility"><span class="section-number-4">3.1.3.</span> Export Flexibility</h4>
<div class="outline-text-4" id="text-export-flexibility">
<p>
Org-mode's export system (<code>ox</code>) produces clean HTML with customizable options:
</p>

<pre class="example" id="orgcab7fb1">
#+OPTIONS: toc:t num:t H:6 html-postamble:nil
#+PROPERTY: header-args :eval never-export
</pre>

<p>
These options control table of contents generation, section numbering, heading depth, and code block behavior.
</p>
</div>
</div>
</div>
<div id="outline-container-the-compilation-pipeline" class="outline-3">
<h3 id="the-compilation-pipeline"><span class="section-number-3">3.2.</span> The Compilation Pipeline</h3>
<div class="outline-text-3" id="text-the-compilation-pipeline">
<p>
With content authored in org-mode but served by a SvelteKit application, I needed a bridge between the two worlds. Org-mode's HTML export produces standalone documents, but SvelteKit needs to extract metadata (title, author, date, description) for SEO and navigation, and the content needs to integrate with the blog's component system for features like table of contents and tag extraction.
</p>

<p>
The task was to build a pipeline that preserves org-mode's authoring power while producing content that SvelteKit can load, parse, and enhance with interactive features. The org-to-HTML pipeline works as follows:
</p>

<ol class="org-ol">
<li><b>Authoring</b>: Write content in <code>org/*.org</code> files</li>
<li><b>Export</b>: Emacs exports to HTML via <code>C-c C-e h h</code></li>
<li><b>Storage</b>: HTML files live in <code>src/routes/[post]/</code></li>
<li><b>Loading</b>: SvelteKit loads HTML server-side, extracts metadata</li>
<li><b>Rendering</b>: Client-side components parse and enhance the HTML</li>
</ol>
</div>
<div id="outline-container-metadata-extraction" class="outline-4">
<h4 id="metadata-extraction"><span class="section-number-4">3.2.1.</span> Metadata Extraction</h4>
<div class="outline-text-4" id="text-metadata-extraction">
<p>
The server-side loader (<code>+page.server.ts</code>) extracts metadata from HTML:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// Extract title from &lt;title&gt; tag
const titleMatch = html.match(/&lt;title&gt;(.*?)&lt;\/title&gt;/);

// Extract author from meta tag
const authorMatch = html.match(/&lt;meta name="author" content="(.*?)"/);

// Extract date from HTML comment
const dateMatch = html.match(/&lt;!-- (\d{4}-\d{2}-\d{2})/);

// Extract first paragraph as description
const descMatch = html.match(/&lt;p[^&gt;]*&gt;([^&lt;]{50,160})/);
</pre>
</div>

<p>
This approach keeps all content in org-mode while enabling rich SEO metadata.
</p>
</div>
</div>
</div>
<div id="outline-container-full-text-search-indexing" class="outline-3">
<h3 id="full-text-search-indexing"><span class="section-number-3">3.3.</span> Full-Text Search Indexing</h3>
<div class="outline-text-3" id="text-full-text-search-indexing">
<p>
With blog posts growing in length and number, readers needed a way to find specific content across all posts. But Algolia's record size limits (10KB per record) meant I couldn't simply index entire posts as single documents. Additionally, search results that link to an entire post aren't particularly helpful when the reader wants a specific section.
</p>

<p>
The solution was to chunk content by heading, creating one searchable record per section. This way, search results link directly to the relevant section with a highlight animation. For Algolia search integration, I created Python scripts that chunk content by heading:
</p>

<div class="org-src-container">
<pre class="src src-python"># extract_html.py (simplified)
def extract_sections(html_content):
    soup = BeautifulSoup(html_content, 'html.parser')
    sections = []

    for heading in soup.find_all(['h2', 'h3', 'h4', 'h5', 'h6']):
        section_id = heading.get('id', '')
        section_title = heading.get_text()
        content = get_section_content(heading)

        sections.append({
            'anchor': section_id,
            'sectionTitle': section_title,
            'content': content[:5000]  # Max 5KB per record
        })

    return sections
</pre>
</div>

<p>
Each heading becomes a searchable record, enabling jump-to-section from search results.
</p>
</div>
</div>
</div>
<div id="outline-container-styling-architecture" class="outline-2">
<h2 id="styling-architecture"><span class="section-number-2">4.</span> Styling Architecture</h2>
<div class="outline-text-2" id="text-styling-architecture">
</div>
<div id="outline-container-design-tokens-with-open-props" class="outline-3">
<h3 id="design-tokens-with-open-props"><span class="section-number-3">4.1.</span> Design Tokens with Open Props</h3>
<div class="outline-text-3" id="text-design-tokens-with-open-props">
<p>
A content-heavy blog with interactive visualizations needed consistent spacing, colors, and typography without bloating the CSS bundle. The primary concern was keeping the styling layer lightweight — every kilobyte of CSS is render-blocking, and a blog should load fast on any connection.
</p>

<p>
The task was to find a system that provides design consistency (spacing scales, color palettes, easing curves) without the overhead of utility-class frameworks or the tooling complexity of preprocessors. Rather than Tailwind CSS, I chose Open Props — a CSS custom properties library providing design tokens at just ~14KB. The result is semantic variable names like <code>--text-muted</code> instead of opaque utilities like <code>text-gray-400</code>, direct CSS control without fighting framework abstractions, and a bundle that's a fraction of even Tailwind's JIT output:
</p>

<div class="org-src-container">
<pre class="src src-css">@import "open-props/style";

/* Semantic variable mapping */
:root {
  --bg-primary: #fffefc;          /* Warm cream */
  --text-primary: var(--gray-8);
  --link-color: var(--indigo-7);
  --size-spacing: var(--size-4);  /* Consistent spacing */
}
</pre>
</div>
</div>
<div id="outline-container-why-not-tailwind-" class="outline-4">
<h4 id="why-not-tailwind-"><span class="section-number-4">4.1.1.</span> Why Not Tailwind?</h4>
<div class="outline-text-4" id="text-why-not-tailwind-">
<p>
Tailwind is excellent for rapid prototyping, but I had specific reasons to avoid it:
</p>

<ol class="org-ol">
<li><b>Readability</b>: Long class lists obscure HTML structure</li>
<li><b>Semantic naming</b>: I prefer <code>--text-muted</code> over <code>text-gray-400</code></li>
<li><b>Bundle size</b>: Open Props is lighter (~14KB vs Tailwind's JIT)</li>
<li><b>Customization</b>: Direct CSS control without fighting abstractions</li>
</ol>
</div>
</div>
</div>
<div id="outline-container-theme-system" class="outline-3">
<h3 id="theme-system"><span class="section-number-3">4.2.</span> Theme System</h3>
<div class="outline-text-3" id="text-theme-system">
<p>
Dark mode is table stakes for a modern developer-focused blog. Readers coding late at night expect a site to respect their OS preference, and developers in particular notice when a blog blinds them with a white page at 2 AM. Beyond just supporting dark mode, the system needed to handle the notoriously tricky "flash of wrong theme" problem — where server-rendered HTML briefly shows the wrong theme before JavaScript hydrates.
</p>

<p>
The task was to implement light, dark, and system (OS-following) modes with zero visual flash on page load, persisted user preferences, and clean CSS that doesn't require duplicating every style rule. The blog supports three theme modes: light, dark, and system (follows OS preference).
</p>
</div>
<div id="outline-container-implementation-strategy" class="outline-4">
<h4 id="implementation-strategy"><span class="section-number-4">4.2.1.</span> Implementation Strategy</h4>
<div class="outline-text-4" id="text-implementation-strategy">
<div class="org-src-container">
<pre class="src src-css">/* System preference (default) */
@media (prefers-color-scheme: dark) {
  :root:not(.theme-light) {
    --bg-primary: var(--gray-9);
    --text-primary: var(--gray-1);
  }
}

/* Manual override classes */
:root.theme-dark {
  --bg-primary: var(--gray-9);
  --text-primary: var(--gray-1);
}

:root.theme-light {
  --bg-primary: #fffefc;
  --text-primary: var(--gray-8);
}
</pre>
</div>

<p>
The CSS cascade ensures manual selection overrides system preference.
</p>
</div>
</div>
<div id="outline-container-flash-prevention" class="outline-4">
<h4 id="flash-prevention"><span class="section-number-4">4.2.2.</span> Flash Prevention</h4>
<div class="outline-text-4" id="text-flash-prevention">
<p>
To prevent a flash of wrong theme on page load, an inline script in <code>app.html</code> runs before rendering:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;script&gt;
  (function() {
    const theme = localStorage.getItem('theme');
    if (theme === 'dark') {
      document.documentElement.classList.add('theme-dark');
    } else if (theme === 'light') {
      document.documentElement.classList.add('theme-light');
    }
  })();
&lt;/script&gt;
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-typography--terminus-font" class="outline-3">
<h3 id="typography--terminus-font"><span class="section-number-3">4.3.</span> Typography: Terminus Font</h3>
<div class="outline-text-3" id="text-typography--terminus-font">
<p>
A technical blog's typography sets its entire visual tone. System fonts feel generic, and popular choices like Fira Code or JetBrains Mono appear on every other developer blog. I wanted a font that reinforced the terminal-inspired aesthetic while being genuinely readable for long technical prose — not just code blocks.
</p>

<p>
I chose the Terminus monospace font for its:
</p>

<ul class="org-ul">
<li><b>Readability</b>: Designed for long coding sessions</li>
<li><b>Character distinction</b>: Clear differentiation between similar characters (0/O, 1/l/I)</li>
<li><b>Aesthetic</b>: Technical, terminal-inspired appearance matching the blog's theme</li>
</ul>

<p>
Self-hosting with preloading ensures fast font delivery:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;link rel="preload" href="/fonts/TerminusTTF-4.49.3.woff2"
      as="font" type="font/woff2" crossorigin&gt;
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-interactive-features" class="outline-2">
<h2 id="interactive-features"><span class="section-number-2">5.</span> Interactive Features</h2>
<div class="outline-text-2" id="text-interactive-features">
</div>
<div id="outline-container-3d-knowledge-graph--dag3d-" class="outline-3">
<h3 id="3d-knowledge-graph--dag3d-"><span class="section-number-3">5.1.</span> 3D Knowledge Graph (DAG3D)</h3>
<div class="outline-text-3" id="text-3d-knowledge-graph--dag3d-">
<p>
Traditional blog navigation — chronological lists, tag clouds, category pages — fails to represent how technical topics actually interconnect. A post about database optimization relates to performance profiling, which connects to observability, which ties back to system design. These relationships are inherently graph-shaped, not list-shaped. Readers browsing a flat list miss connections that could lead them to exactly the content they need.
</p>

<p>
The task was threefold: improve content discoverability by surfacing topic relationships, create a visual portfolio piece that demonstrates frontend engineering capability, and provide a homepage that's genuinely interesting rather than a static list of links. The result is an interactive 3D force-directed graph on the homepage showing relationships between blog posts, where nodes represent posts and edges represent shared concepts.
</p>
</div>
<div id="outline-container-technology-stack" class="outline-4">
<h4 id="technology-stack"><span class="section-number-4">5.1.1.</span> Technology Stack</h4>
<div class="outline-text-4" id="text-technology-stack">
<ul class="org-ul">
<li><b>3d-force-graph</b>: High-level library wrapping Three.js and d3-force-3d</li>
<li><b>three-spritetext</b>: Renders text labels as 3D sprites</li>
<li><b>WebGL</b>: Hardware-accelerated rendering</li>
</ul>
</div>
</div>
<div id="outline-container-performance-optimizations" class="outline-4">
<h4 id="performance-optimizations"><span class="section-number-4">5.1.2.</span> Performance Optimizations</h4>
<div class="outline-text-4" id="text-performance-optimizations">
<div class="org-src-container">
<pre class="src src-typescript">// Lazy loading with IntersectionObserver
const observer = new IntersectionObserver(
  (entries) =&gt; {
    if (entries[0].isIntersecting) {
      initializeGraph();
      observer.disconnect();
    }
  },
  { rootMargin: '100px' }
);

// Pause animation when tab is hidden
document.addEventListener('visibilitychange', () =&gt; {
  if (document.hidden) {
    graph.pauseAnimation();
  } else {
    graph.resumeAnimation();
  }
});
</pre>
</div>
</div>
</div>
<div id="outline-container-user-interaction" class="outline-4">
<h4 id="user-interaction"><span class="section-number-4">5.1.3.</span> User Interaction</h4>
<div class="outline-text-4" id="text-user-interaction">
<p>
The graph supports:
</p>
<ul class="org-ul">
<li><b>Auto-rotation</b>: Continuous slow rotation for visual interest</li>
<li><b>Drag</b>: Stops rotation, allows free camera movement</li>
<li><b>Click</b>: Navigates to the clicked post</li>
<li><b>Reset</b>: Returns to default view with smooth animation</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-plotly-chart-integration" class="outline-3">
<h3 id="plotly-chart-integration"><span class="section-number-3">5.2.</span> Plotly Chart Integration</h3>
<div class="outline-text-3" id="text-plotly-chart-integration">
<p>
Static images of charts in technical blog posts are a missed opportunity. Readers can't zoom into dense scatter plots, rotate 3D visualizations, or hover over data points to see exact values. But embedding interactive charts introduces UX problems — chart drag gestures conflict with page scrolling, and accidentally interacting with a chart while reading is frustrating.
</p>

<p>
I needed interactive charts that stay out of the way during normal reading but become fully interactive on demand, with the ability to expand to fullscreen for detailed exploration. Interactive charts are defined in JSON and rendered with Plotly.js:
</p>

<div class="org-src-container">
<pre class="src src-json">{
  "data": [{
    "type": "scatter3d",
    "x": [1, 2, 3],
    "y": [4, 5, 6],
    "z": [7, 8, 9],
    "mode": "markers"
  }],
  "layout": {
    "title": "3D Scatter Plot"
  }
}
</pre>
</div>
</div>
<div id="outline-container-lock-unlock-mechanism" class="outline-4">
<h4 id="lock-unlock-mechanism"><span class="section-number-4">5.2.1.</span> Lock/Unlock Mechanism</h4>
<div class="outline-text-4" id="text-lock-unlock-mechanism">
<p>
Charts start "locked" to prevent accidental interaction while scrolling:
</p>

<ol class="org-ol">
<li>Charts render with <code>pointer-events: none</code></li>
<li>An overlay displays "Click to interact"</li>
<li>Clicking enables the chart (<code>pointer-events: auto</code>)</li>
<li>Pressing Escape re-locks the chart</li>
</ol>
</div>
</div>
<div id="outline-container-modal-expansion" class="outline-4">
<h4 id="modal-expansion"><span class="section-number-4">5.2.2.</span> Modal Expansion</h4>
<div class="outline-text-4" id="text-modal-expansion">
<p>
Charts can expand to fullscreen modals with:
</p>
<ul class="org-ul">
<li>Full toolbar access</li>
<li>Animation frame preservation</li>
<li>Enhanced legend positioning</li>
<li>95vw × 90vh dimensions</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-devpulse--commit-activity-grid" class="outline-3">
<h3 id="devpulse--commit-activity-grid"><span class="section-number-3">5.3.</span> DevPulse: Commit Activity Grid</h3>
<div class="outline-text-3" id="text-devpulse--commit-activity-grid">
<p>
A portfolio blog should demonstrate that the author is actively building, not just publishing static content. Visitors landing on the homepage should immediately see evidence of consistent coding activity — it builds credibility and shows the site is maintained. GitHub's contribution graph is effective at communicating this at a glance, but it's buried on a profile page most visitors won't find.
</p>

<p>
The task was to build a visible indicator of development activity directly on the homepage, with more flexibility than GitHub's single-scale yearly view. Inspired by GitHub's contribution graph, DevPulse shows my commit activity with five different time scales for different perspectives:
</p>
</div>
<div id="outline-container-multi-scale-timeline" class="outline-4">
<h4 id="multi-scale-timeline"><span class="section-number-4">5.3.1.</span> Multi-Scale Timeline</h4>
<div class="outline-text-4" id="text-multi-scale-timeline">
<p>
Five different time scales provide different perspectives:
</p>
<ul class="org-ul">
<li><b>Days</b>: 7-column grid (weekdays)</li>
<li><b>Weeks</b>: 52 columns (one year)</li>
<li><b>Months</b>: 12 columns (J-D)</li>
<li><b>Quarters</b>: 4 columns (Q1-Q4)</li>
<li><b>Years</b>: 10 columns (decade view)</li>
</ul>
</div>
</div>
<div id="outline-container-data-pipeline" class="outline-4">
<h4 id="data-pipeline"><span class="section-number-4">5.3.2.</span> Data Pipeline</h4>
<div class="outline-text-4" id="text-data-pipeline">
<div class="org-src-container">
<pre class="src src-typescript">// API endpoint fetches from GitHub
const response = await fetch(
  `https://api.github.com/repos/${owner}/${repo}/commits`,
  {
    headers: {
      Authorization: `token ${GITHUB_TOKEN}`,
      Accept: 'application/vnd.github.v3+json'
    }
  }
);

// Transform to activity grid
const commits = await response.json();
const activityMap = aggregateByTimePeriod(commits, scale);
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-recursive-link-previews" class="outline-3">
<h3 id="recursive-link-previews"><span class="section-number-3">5.4.</span> Recursive Link Previews</h3>
<div class="outline-text-3" id="text-recursive-link-previews">
<p>
Technical blog posts are densely linked — to other posts, documentation, external resources. Every click takes the reader away from their current context, and the mental cost of deciding "is this link worth following?" disrupts reading flow. Readers either ignore links entirely (missing valuable context) or click them and lose their place in the original article.
</p>

<p>
The task was to let readers preview linked content without navigating away, maintaining their reading context while still providing access to referenced material. The result is hover-triggered preview popups with support for nested previews up to 10 levels deep — a reader can preview a link, then preview a link within that preview, following a chain of references without ever leaving the page.
</p>
</div>
<div id="outline-container-architecture" class="outline-4">
<h4 id="architecture"><span class="section-number-4">5.4.1.</span> Architecture</h4>
<div class="outline-text-4" id="text-architecture">
<pre class="example" id="orgb2016e5">
User hovers link → 300ms delay → Fetch preview
                                      ↓
                              Internal link? → Clone article content
                                      ↓
                              External link? → Fetch via proxy
                                      ↓
                              Display in popup with sanitized HTML
</pre>
</div>
</div>
<div id="outline-container-proxy-server" class="outline-4">
<h4 id="proxy-server"><span class="section-number-4">5.4.2.</span> Proxy Server</h4>
<div class="outline-text-4" id="text-proxy-server">
<p>
External previews route through <code>/api/preview-proxy</code> which:
</p>

<ol class="org-ol">
<li>Fetches the external page</li>
<li>Sanitizes HTML with DOMPurify</li>
<li>Rewrites relative URLs to absolute</li>
<li>Hides modals, cookie banners, chat widgets</li>
<li>Returns safe, displayable content</li>
</ol>
</div>
</div>
<div id="outline-container-performance" class="outline-4">
<h4 id="performance"><span class="section-number-4">5.4.3.</span> Performance</h4>
<div class="outline-text-4" id="text-performance">
<ul class="org-ul">
<li>300ms hover delay prevents accidental triggers</li>
<li>Grace period keeps popup open when moving between link and popup</li>
<li>10-second timeout prevents stuck loading states</li>
<li>Cross-origin iframe sandboxing for security</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-full-text-search-with-algolia" class="outline-3">
<h3 id="full-text-search-with-algolia"><span class="section-number-3">5.5.</span> Full-Text Search with Algolia</h3>
<div class="outline-text-3" id="text-full-text-search-with-algolia">
<p>
As the number of posts grew, the table of contents and knowledge graph became insufficient for finding specific content. A reader who remembers reading about "WebGL performance" but can't recall which post it was in needs instant, typo-tolerant full-text search across all content.
</p>

<p>
Building search from scratch (inverted indices, ranking algorithms, typo tolerance) would be a massive undertaking for marginal benefit. The task was to integrate a hosted search service that provides sub-50ms results with section-level granularity, keyboard-driven UX, and minimal client-side code. Search is powered by Algolia with InstantSearch.js:
</p>

<div class="org-src-container">
<pre class="src src-typescript">const searchClient = algoliasearch(APP_ID, API_KEY);

instantsearch({
  indexName: 'posts',
  searchClient,
  searchFunction(helper) {
    if (helper.state.query) {
      helper.search();
    }
  }
});
</pre>
</div>
</div>
<div id="outline-container-keyboard-navigation" class="outline-4">
<h4 id="keyboard-navigation"><span class="section-number-4">5.5.1.</span> Keyboard Navigation</h4>
<div class="outline-text-4" id="text-keyboard-navigation">
<ul class="org-ul">
<li><code>Cmd/Ctrl + K</code>: Open search modal</li>
<li><code>Escape</code>: Close search</li>
<li><code>Enter</code>: Navigate to first result</li>
<li>Arrow keys: Navigate results</li>
</ul>
</div>
</div>
<div id="outline-container-jump-to-section" class="outline-4">
<h4 id="jump-to-section"><span class="section-number-4">5.5.2.</span> Jump to Section</h4>
<div class="outline-text-4" id="text-jump-to-section">
<p>
Search results link to specific sections with highlight animation:
</p>

<div class="org-src-container">
<pre class="src src-css">.search-highlight-target {
  animation: search-highlight 2s ease-out;
}

@keyframes search-highlight {
  0% { background-color: rgba(138, 106, 170, 0.3); }
  100% { background-color: transparent; }
}
</pre>
</div>
</div>
</div>
</div>
</div>
<div id="outline-container-seo-optimization" class="outline-2">
<h2 id="seo-optimization"><span class="section-number-2">6.</span> SEO Optimization</h2>
<div class="outline-text-2" id="text-seo-optimization">
<p>
Writing quality technical content is pointless if search engines can't find, understand, or properly display it. A SvelteKit blog with client-side rendering and dynamic content loading presents specific SEO challenges — crawlers may not execute JavaScript, social media link previews need pre-rendered metadata, and search engines need structured data to understand content relationships.
</p>

<p>
The task was to ensure every page is fully crawlable with rich metadata, appears correctly when shared on social media, and provides search engines with structured data about authorship, dates, and content type — all while keeping the content authoring workflow in org-mode.
</p>
</div>
<div id="outline-container-meta-tags" class="outline-3">
<h3 id="meta-tags"><span class="section-number-3">6.1.</span> Meta Tags</h3>
<div class="outline-text-3" id="text-meta-tags">
<p>
Every page includes comprehensive meta tags extracted from the org-mode source during the server-side loading phase:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;!-- Basic --&gt;
&lt;meta name="description" content="{description}"&gt;
&lt;link rel="canonical" href="{url}"&gt;

&lt;!-- Open Graph --&gt;
&lt;meta property="og:title" content="{title}"&gt;
&lt;meta property="og:description" content="{description}"&gt;
&lt;meta property="og:type" content="article"&gt;
&lt;meta property="og:url" content="{url}"&gt;
&lt;meta property="og:image" content="{image}"&gt;

&lt;!-- Twitter Card --&gt;
&lt;meta name="twitter:card" content="summary_large_image"&gt;
&lt;meta name="twitter:title" content="{title}"&gt;
&lt;meta name="twitter:description" content="{description}"&gt;
</pre>
</div>
</div>
</div>
<div id="outline-container-structured-data--json-ld-" class="outline-3">
<h3 id="structured-data--json-ld-"><span class="section-number-3">6.2.</span> Structured Data (JSON-LD)</h3>
<div class="outline-text-3" id="text-structured-data--json-ld-">
<p>
Blog posts include schema.org structured data:
</p>

<div class="org-src-container">
<pre class="src src-json">{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Design Decisions: Building a Modern Technical Blog",
  "author": {
    "@type": "Person",
    "name": "Charlie Holland"
  },
  "datePublished": "2026-01-21",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://chiply.dev/post-design-decisions"
  }
}
</pre>
</div>

<p>
This helps search engines understand content relationships.
</p>
</div>
</div>
<div id="outline-container-sitemap-and-rss" class="outline-3">
<h3 id="sitemap-and-rss"><span class="section-number-3">6.3.</span> Sitemap and RSS</h3>
<div class="outline-text-3" id="text-sitemap-and-rss">
<p>
Both are generated at build time by scanning the <code>src/routes/[post]/</code> directory:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// sitemap.xml/+server.ts
export const GET: RequestHandler = async () =&gt; {
  const posts = await discoverPosts();

  const sitemap = `&lt;?xml version="1.0" encoding="UTF-8"?&gt;
    &lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&gt;
      ${posts.map(post =&gt; `
        &lt;url&gt;
          &lt;loc&gt;https://chiply.dev/${post.slug}&lt;/loc&gt;
          &lt;lastmod&gt;${post.date}&lt;/lastmod&gt;
          &lt;priority&gt;0.8&lt;/priority&gt;
        &lt;/url&gt;
      `).join('')}
    &lt;/urlset&gt;`;

  return new Response(sitemap, {
    headers: { 'Content-Type': 'application/xml' }
  });
};
</pre>
</div>
</div>
</div>
<div id="outline-container-prerendering" class="outline-3">
<h3 id="prerendering"><span class="section-number-3">6.4.</span> Prerendering</h3>
<div class="outline-text-3" id="text-prerendering">
<p>
All pages are prerendered at build time:
</p>

<div class="org-src-container">
<pre class="src src-typescript">export const prerender = true;

export const entries: EntryGenerator = async () =&gt; {
  const posts = await discoverPosts();
  return posts.map(post =&gt; ({ post: post.slug }));
};
</pre>
</div>

<p>
This ensures search engines receive fully-rendered HTML.
</p>
</div>
</div>
</div>
<div id="outline-container-analytics-and-tracking" class="outline-2">
<h2 id="analytics-and-tracking"><span class="section-number-2">7.</span> Analytics and Tracking</h2>
<div class="outline-text-2" id="text-analytics-and-tracking">
<p>
Publishing technical content into the void without any feedback loop felt unsatisfying. I was curious about basic questions: do people actually find and read these posts? Do they finish long articles or drop off midway? Which topics get traction? Standard page view counters answer the first question but tell you nothing about reading behavior.
</p>

<p>
The task was to understand traffic patterns and reader engagement out of curiosity, layering lightweight analytics that answer increasingly specific questions — from basic page views to scroll depth and session recordings — without invasive tracking or degrading page performance.
</p>
</div>
<div id="outline-container-vercel-analytics" class="outline-3">
<h3 id="vercel-analytics"><span class="section-number-3">7.1.</span> Vercel Analytics</h3>
<div class="outline-text-3" id="text-vercel-analytics">
<p>
The first layer is Vercel's built-in analytics, requiring just two lines of code to track page views and Web Vitals:
</p>

<div class="org-src-container">
<pre class="src src-typescript">import { inject } from '@vercel/analytics';
inject();
</pre>
</div>
</div>
</div>
<div id="outline-container-speed-insights" class="outline-3">
<h3 id="speed-insights"><span class="section-number-3">7.2.</span> Speed Insights</h3>
<div class="outline-text-3" id="text-speed-insights">
<p>
Real User Monitoring (RUM) captures Core Web Vitals:
</p>

<ul class="org-ul">
<li><b>LCP</b> (Largest Contentful Paint)</li>
<li><b>FID</b> (First Input Delay)</li>
<li><b>CLS</b> (Cumulative Layout Shift)</li>
</ul>
</div>
</div>
<div id="outline-container-custom-engagement-tracking" class="outline-3">
<h3 id="custom-engagement-tracking"><span class="section-number-3">7.3.</span> Custom Engagement Tracking</h3>
<div class="outline-text-3" id="text-custom-engagement-tracking">
<p>
Page views alone don't distinguish between a reader who bounced after 3 seconds and one who spent 20 minutes reading every section. Standard analytics tools track navigation but not engagement depth. I wanted to know: do readers who start a 5000-word post actually finish it?
</p>

<p>
I built custom engagement tracking to answer these questions about reading behavior:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// Track scroll depth milestones
const milestones = [25, 50, 75, 100];

const handleScroll = debounce(() =&gt; {
  const scrollPercent = (scrollTop / scrollHeight) * 100;

  milestones.forEach(milestone =&gt; {
    if (scrollPercent &gt;= milestone &amp;&amp; !reached[milestone]) {
      reached[milestone] = true;
      trackEvent('scroll_milestone', { depth: milestone });
    }
  });
}, 100);
</pre>
</div>

<p>
Metrics tracked include:
</p>
<ul class="org-ul">
<li>Scroll depth (25%, 50%, 75%, 100%)</li>
<li>Time on page (active time, excluding hidden tabs)</li>
<li>Read completion (&gt;90% scroll depth)</li>
</ul>
</div>
</div>
<div id="outline-container-microsoft-clarity" class="outline-3">
<h3 id="microsoft-clarity"><span class="section-number-3">7.4.</span> Microsoft Clarity</h3>
<div class="outline-text-3" id="text-microsoft-clarity">
<p>
Clarity provides heatmaps and session recordings for UX analysis:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// Secure initialization with validation
const clarityId = import.meta.env.VITE_CLARITY_ID;
if (clarityId &amp;&amp; /^[a-zA-Z0-9]+$/.test(clarityId)) {
  const script = document.createElement('script');
  script.src = `https://www.clarity.ms/tag/${clarityId}`;
  script.async = true;
  document.head.appendChild(script);
}
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-newsletter-integration" class="outline-2">
<h2 id="newsletter-integration"><span class="section-number-2">8.</span> Newsletter Integration</h2>
<div class="outline-text-2" id="text-newsletter-integration">
<p>
Relying solely on organic search traffic means readers who enjoy a post have no way to know when new content is published. Social media algorithms are unreliable for reach, and RSS — while supported — has a tiny user base outside of developer circles. A newsletter provides a direct, algorithm-free channel to interested readers.
</p>

<p>
The task was to add subscription capability without the complexity of running a mail server, managing deliverability, or building subscriber management UI. The solution needed GDPR-compliant double opt-in and should integrate cleanly with the existing SvelteKit API endpoint pattern.
</p>
</div>
<div id="outline-container-buttondown-api" class="outline-3">
<h3 id="buttondown-api"><span class="section-number-3">8.1.</span> Buttondown API</h3>
<div class="outline-text-3" id="text-buttondown-api">
<p>
I chose Buttondown for its developer-friendly API and minimal UI — it handles deliverability, unsubscribes, and confirmation emails while I just call one endpoint. Newsletter subscriptions use Buttondown's API:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// /api/subscribe/+server.ts
export const POST: RequestHandler = async ({ request }) =&gt; {
  const { email } = await request.json();

  // RFC 5321 validation
  if (!isValidEmail(email)) {
    return json({ error: 'Invalid email' }, { status: 400 });
  }

  const response = await fetch('https://api.buttondown.com/v1/subscribers', {
    method: 'POST',
    headers: {
      Authorization: `Token ${BUTTONDOWN_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ email })
  });

  return json({ success: true });
};
</pre>
</div>
</div>
</div>
<div id="outline-container-double-opt-in" class="outline-3">
<h3 id="double-opt-in"><span class="section-number-3">8.2.</span> Double Opt-In</h3>
<div class="outline-text-3" id="text-double-opt-in">
<p>
Buttondown handles confirmation emails, ensuring GDPR compliance and reducing spam.
</p>
</div>
</div>
</div>
<div id="outline-container-security-considerations" class="outline-2">
<h2 id="security-considerations"><span class="section-number-2">9.</span> Security Considerations</h2>
<div class="outline-text-2" id="text-security-considerations">
<p>
Professional security habits demanded proper hardening even for a personal blog. The site isn't just serving static HTML — it fetches and renders external content through the link preview proxy, loads org-mode-exported HTML into the DOM, accepts user input through newsletter subscriptions, and runs API endpoints that proxy to external services. Each of these is a potential XSS, injection, or data exfiltration vector.
</p>

<p>
The task was to implement defense-in-depth: multiple independent security layers so that if any single defense fails, others still protect the site. This meant sanitizing all rendered HTML, restricting what resources the browser can load, validating all inputs, and hardening HTTP headers against common attack patterns.
</p>
</div>
<div id="outline-container-html-sanitization" class="outline-3">
<h3 id="html-sanitization"><span class="section-number-3">9.1.</span> HTML Sanitization</h3>
<div class="outline-text-3" id="text-html-sanitization">
<p>
The most critical defense layer, since the site dynamically renders HTML from multiple sources (org-mode exports, external link previews). All user-facing HTML is sanitized with DOMPurify using strict allowlists:
</p>

<div class="org-src-container">
<pre class="src src-typescript">import DOMPurify from 'dompurify';

const ALLOWED_TAGS = [
  'article', 'section', 'nav', 'header', 'footer',
  'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
  'p', 'ul', 'ol', 'li', 'a', 'strong', 'em',
  'pre', 'code', 'blockquote', 'table', 'tr', 'td', 'th',
  'img', 'figure', 'figcaption', 'svg', 'path'
];

const ALLOWED_ATTR = [
  'id', 'class', 'href', 'src', 'alt', 'title',
  'aria-label', 'aria-hidden', 'role',
  'data-*', 'width', 'height'
];

export const sanitizeHtml = (html: string) =&gt; {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS,
    ALLOWED_ATTR,
    ALLOWED_URI_REGEXP: /^(?:https?|mailto|tel):/i
  });
};
</pre>
</div>
</div>
</div>
<div id="outline-container-content-security-policy" class="outline-3">
<h3 id="content-security-policy"><span class="section-number-3">9.2.</span> Content Security Policy</h3>
<div class="outline-text-3" id="text-content-security-policy">
<p>
The CSP header restricts resource loading:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// hooks.server.ts
const csp = [
  "default-src 'self'",
  "script-src 'self' 'unsafe-inline' 'unsafe-eval' cdn.jsdelivr.net cdnjs.cloudflare.com",
  "style-src 'self' 'unsafe-inline' cdn.jsdelivr.net",
  "img-src 'self' data: blob: https:",
  "font-src 'self' data: cdn.jsdelivr.net",
  "connect-src 'self' api.github.com *.algolia.net",
  "frame-src 'self'",
  "object-src 'none'",
  "base-uri 'self'",
  "upgrade-insecure-requests"
].join('; ');

response.headers.set('Content-Security-Policy', csp);
</pre>
</div>
</div>
</div>
<div id="outline-container-security-headers" class="outline-3">
<h3 id="security-headers"><span class="section-number-3">9.3.</span> Security Headers</h3>
<div class="outline-text-3" id="text-security-headers">
<p>
Additional headers prevent common attacks:
</p>

<div class="org-src-container">
<pre class="src src-typescript">response.headers.set('X-Frame-Options', 'SAMEORIGIN');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
response.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
</pre>
</div>
</div>
</div>
<div id="outline-container-input-validation" class="outline-3">
<h3 id="input-validation"><span class="section-number-3">9.4.</span> Input Validation</h3>
<div class="outline-text-3" id="text-input-validation">
<p>
All API inputs are validated:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// Email validation (RFC 5321 compliant)
const isValidEmail = (email: string): boolean =&gt; {
  if (typeof email !== 'string') return false;
  if (email.length &gt; 254) return false;

  const [local, domain] = email.split('@');
  if (!local || !domain) return false;
  if (local.length &gt; 64) return false;
  if (/\.\./.test(email)) return false;

  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
};

// URL validation
const isValidUrl = (url: string): boolean =&gt; {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
};
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-accessibility" class="outline-2">
<h2 id="accessibility"><span class="section-number-2">10.</span> Accessibility</h2>
<div class="outline-text-2" id="text-accessibility">
<p>
A technical blog should be readable by everyone, including developers using screen readers, keyboard-only navigation, or high-contrast modes. The interactive features (3D graph, charts, search modals) introduced specific accessibility challenges — custom interactive widgets don't get keyboard support or screen reader announcements for free.
</p>

<p>
The task was to maintain WCAG compliance across both the org-mode-exported static content and the custom interactive Svelte components, ensuring every feature works without a mouse and communicates its state to assistive technology.
</p>
</div>
<div id="outline-container-semantic-html" class="outline-3">
<h3 id="semantic-html"><span class="section-number-3">10.1.</span> Semantic HTML</h3>
<div class="outline-text-3" id="text-semantic-html">
<p>
The foundation of accessibility is semantic HTML, and org-mode's export helps here by default — headings, paragraphs, lists, and tables all use correct elements. Org-mode exports clean semantic HTML:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;article&gt;
  &lt;header&gt;
    &lt;h1&gt;Post Title&lt;/h1&gt;
    &lt;time datetime="2026-01-21"&gt;January 21, 2026&lt;/time&gt;
  &lt;/header&gt;
  &lt;section id="introduction"&gt;
    &lt;h2&gt;Introduction&lt;/h2&gt;
    &lt;p&gt;Content...&lt;/p&gt;
  &lt;/section&gt;
&lt;/article&gt;
</pre>
</div>
</div>
</div>
<div id="outline-container-aria-labels" class="outline-3">
<h3 id="aria-labels"><span class="section-number-3">10.2.</span> ARIA Labels</h3>
<div class="outline-text-3" id="text-aria-labels">
<p>
Interactive elements include ARIA attributes:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;button
  aria-label="Toggle theme"
  aria-pressed={isDark}
  onclick={toggleTheme}
&gt;
  &lt;i class="fa-solid fa-moon" aria-hidden="true"&gt;&lt;/i&gt;
&lt;/button&gt;

&lt;dialog
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
&gt;
  &lt;h2 id="modal-title"&gt;Search&lt;/h2&gt;
&lt;/dialog&gt;
</pre>
</div>
</div>
</div>
<div id="outline-container-keyboard-navigation" class="outline-3">
<h3 id="keyboard-navigation"><span class="section-number-3">10.3.</span> Keyboard Navigation</h3>
<div class="outline-text-3" id="text-keyboard-navigation">
<p>
All functionality is keyboard accessible:
</p>

<ul class="org-ul">
<li><b>Tab</b>: Navigate between interactive elements</li>
<li><b>Enter/Space</b>: Activate buttons</li>
<li><b>Escape</b>: Close modals</li>
<li><b>Cmd/Ctrl+K</b>: Open search</li>
<li><b>Arrow keys</b>: Navigate search results</li>
</ul>
</div>
</div>
<div id="outline-container-focus-management" class="outline-3">
<h3 id="focus-management"><span class="section-number-3">10.4.</span> Focus Management</h3>
<div class="outline-text-3" id="text-focus-management">
<p>
Modals trap and restore focus:
</p>

<div class="org-src-container">
<pre class="src src-typescript">let previouslyFocusedElement: HTMLElement | null = null;

function openModal() {
  previouslyFocusedElement = document.activeElement as HTMLElement;
  modalElement.focus();
}

function closeModal() {
  previouslyFocusedElement?.focus();
  previouslyFocusedElement = null;
}
</pre>
</div>
</div>
</div>
<div id="outline-container-skip-link" class="outline-3">
<h3 id="skip-link"><span class="section-number-3">10.5.</span> Skip Link</h3>
<div class="outline-text-3" id="text-skip-link">
<p>
A skip link allows keyboard users to bypass navigation:
</p>

<div class="org-src-container">
<pre class="src src-html">&lt;a href="https://www.chiply.dev/#main-content" class="skip-link"&gt;
  Skip to main content
&lt;/a&gt;
</pre>
</div>
</div>
</div>
</div>
<div id="outline-container-performance-optimizations" class="outline-2">
<h2 id="performance-optimizations"><span class="section-number-2">11.</span> Performance Optimizations</h2>
<div class="outline-text-2" id="text-performance-optimizations">
<p>
The blog includes heavy dependencies — Three.js for the 3D graph, Plotly.js for interactive charts, Algolia for search, and multiple analytics scripts. Loading all of these eagerly on every page would produce a massive initial bundle, degrading Core Web Vitals and making the site sluggish on mobile devices or slow connections. A reader who just wants to read a blog post shouldn't download a WebGL renderer.
</p>

<p>
The task was to ensure fast initial page loads regardless of which features a particular page uses, deferring expensive resources until they're actually needed while maintaining a smooth experience when they do load.
</p>
</div>
<div id="outline-container-lazy-loading" class="outline-3">
<h3 id="lazy-loading"><span class="section-number-3">11.1.</span> Lazy Loading</h3>
<div class="outline-text-3" id="text-lazy-loading">
<p>
Heavy components load on demand, triggered only when they enter (or approach) the viewport:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// IntersectionObserver for viewport-triggered loading
const observer = new IntersectionObserver(
  (entries) =&gt; {
    entries.forEach(entry =&gt; {
      if (entry.isIntersecting) {
        loadComponent();
        observer.unobserve(entry.target);
      }
    });
  },
  { rootMargin: '100px' }
);
</pre>
</div>
</div>
</div>
<div id="outline-container-code-splitting" class="outline-3">
<h3 id="code-splitting"><span class="section-number-3">11.2.</span> Code Splitting</h3>
<div class="outline-text-3" id="text-code-splitting">
<p>
Dynamic imports split the bundle:
</p>

<div class="org-src-container">
<pre class="src src-typescript">// Only load Algolia when search opens
const loadSearch = async () =&gt; {
  const { default: algoliasearch } = await import('algoliasearch');
  const { default: instantsearch } = await import('instantsearch.js');
  // Initialize search...
};
</pre>
</div>
</div>
</div>
<div id="outline-container-caching-strategy" class="outline-3">
<h3 id="caching-strategy"><span class="section-number-3">11.3.</span> Caching Strategy</h3>
<div class="outline-text-3" id="text-caching-strategy">
<p>
Different resources have different cache lifetimes:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Resource Type</th>
<th scope="col" class="org-left">Browser Cache</th>
<th scope="col" class="org-left">CDN Cache</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left">Fonts</td>
<td class="org-left">1 year</td>
<td class="org-left">1 year</td>
</tr>

<tr>
<td class="org-left">Static assets</td>
<td class="org-left">1 year</td>
<td class="org-left">1 year</td>
</tr>

<tr>
<td class="org-left">API responses</td>
<td class="org-left">5 minutes</td>
<td class="org-left">1 hour</td>
</tr>

<tr>
<td class="org-left">HTML pages</td>
<td class="org-left">0</td>
<td class="org-left">1 hour</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-image-optimization" class="outline-3">
<h3 id="image-optimization"><span class="section-number-3">11.4.</span> Image Optimization</h3>
<div class="outline-text-3" id="text-image-optimization">
<p>
Images are optimized with:
</p>

<ul class="org-ul">
<li>WebP format where supported</li>
<li>Lazy loading via <code>loading</code>"lazy"=</li>
<li>Appropriate sizing with <code>srcset</code></li>
<li>Placeholder aspect ratios to prevent CLS</li>
</ul>
</div>
</div>
</div>
<div id="outline-container-versioning-and-releases" class="outline-2">
<h2 id="versioning-and-releases"><span class="section-number-2">12.</span> Versioning and Releases</h2>
<div class="outline-text-2" id="text-versioning-and-releases">
<p>
As the project grew with frequent commits — new features, bug fixes, content additions — the question of "what changed and when" became harder to answer. Without structured versioning, there's no way to communicate the significance of changes (is this a breaking change? a new feature? a patch?) or generate meaningful changelogs for anyone following the project.
</p>

<p>
Setting up proper release engineering early, before the project accumulated hundreds of commits, would prevent future pain. The task was to automate version bumping and changelog generation from commit history, requiring only disciplined commit messages rather than manual bookkeeping.
</p>
</div>
<div id="outline-container-semantic-versioning" class="outline-3">
<h3 id="semantic-versioning"><span class="section-number-3">12.1.</span> Semantic Versioning</h3>
<div class="outline-text-3" id="text-semantic-versioning">
<p>
The project uses SemVer with automated releases, where the version bump is determined entirely by commit message prefixes:
</p>

<table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides">


<colgroup>
<col  class="org-left" />

<col  class="org-left" />
</colgroup>
<thead>
<tr>
<th scope="col" class="org-left">Commit Type</th>
<th scope="col" class="org-left">Version Bump</th>
</tr>
</thead>
<tbody>
<tr>
<td class="org-left"><code>fix:</code></td>
<td class="org-left">PATCH (0.0.x)</td>
</tr>

<tr>
<td class="org-left"><code>feat:</code></td>
<td class="org-left">MINOR (0.x.0)</td>
</tr>

<tr>
<td class="org-left"><code>BREAKING CHANGE:</code></td>
<td class="org-left">MAJOR (x.0.0)</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="outline-container-release-please" class="outline-3">
<h3 id="release-please"><span class="section-number-3">12.2.</span> Release Please</h3>
<div class="outline-text-3" id="text-release-please">
<p>
Google's release-please automates version management:
</p>

<ol class="org-ol">
<li>Conventional commits trigger Release PRs</li>
<li>PRs include changelog updates</li>
<li>Merging creates GitHub releases and tags</li>
<li><code>package.json</code> version updates automatically</li>
</ol>
</div>
</div>
<div id="outline-container-changelog-generation" class="outline-3">
<h3 id="changelog-generation"><span class="section-number-3">12.3.</span> Changelog Generation</h3>
<div class="outline-text-3" id="text-changelog-generation">
<p>
Changelogs are auto-generated from commit messages:
</p>

<pre class="example" id="org3dfa382">
## [0.1.0] - 2026-01-21

### Features
- Add share button with social media platforms
- Add 3D knowledge graph visualization

### Bug Fixes
- Fix theme flash on page load
- Correct chart modal focus management
</pre>
</div>
</div>
</div>
<div id="outline-container-conclusion" class="outline-2">
<h2 id="conclusion"><span class="section-number-2">13.</span> Conclusion</h2>
<div class="outline-text-2" id="text-conclusion">
<p>
Building chiply.dev has been an exercise in thoughtful engineering. Every decision - from Svelte's compiled output to org-mode's literate programming - serves the goal of creating an engaging, performant, and accessible reading experience.
</p>

<p>
The codebase reflects my belief that personal projects should be laboratories for exploring ideas. The recursive link previews might be over-engineered, but they taught me about iframe security policies. The 3D knowledge graph might be unnecessary, but it forced me to learn WebGL performance optimization.
</p>

<p>
If you're building your own technical blog, I hope this post provides useful starting points. Feel free to explore the source code on GitHub, and don't hesitate to reach out with questions or suggestions.
</p>
</div>
</div>
<div id="outline-container-references" class="outline-2">
<h2 id="references"><span class="section-number-2">14.</span> References</h2>
<div class="outline-text-2" id="text-references">
<ul class="org-ul">
<li><a href="https://svelte.dev/docs">Svelte 5 Documentation</a></li>
<li><a href="https://kit.svelte.dev/docs">SvelteKit Documentation</a></li>
<li><a href="https://open-props.style">Open Props CSS</a></li>
<li><a href="https://orgmode.org/manual/">Org Mode Manual</a></li>
<li><a href="https://plotly.com/javascript/">Plotly.js Documentation</a></li>
<li><a href="https://github.com/vasturiano/3d-force-graph">3D Force Graph</a></li>
<li><a href="https://www.algolia.com/doc/">Algolia Documentation</a></li>
<li><a href="https://github.com/cure53/DOMPurify">DOMPurify</a></li>
<li><a href="https://web.dev/vitals/">Core Web Vitals</a></li>
<li><a href="https://schema.org/BlogPosting">Schema.org BlogPosting</a></li>
</ul>
</div>
</div>
<div id="outline-container-tldr" class="outline-2">
<h2 id="tldr"><span class="section-number-2">15.</span> tldr</h2>
<div class="outline-text-2" id="text-tldr">
<p>
This post details the architectural decisions behind chiply.dev, a modern technical blog built with <a href="https://www.chiply.dev/#framework-selection">SvelteKit 2 and Svelte 5's new runes system</a> for explicit reactivity and compiled performance. The <a href="https://www.chiply.dev/#content-management--org-mode-to-html">content pipeline uses Emacs org-mode</a> for literate programming capabilities, exporting to HTML that gets processed server-side for metadata extraction and <a href="https://www.chiply.dev/#full-text-search-indexing">chunked into Algolia search records by heading</a>.
</p>

<p>
The <a href="https://www.chiply.dev/#styling-architecture">styling leverages Open Props CSS custom properties</a> instead of Tailwind for semantic naming and lighter bundles, with a <a href="https://www.chiply.dev/#theme-system">sophisticated theme system</a> preventing flash-of-wrong-theme on load. The <a href="https://www.chiply.dev/#interactive-features">interactive features include a 3D knowledge graph</a> built with Three.js and WebGL, <a href="https://www.chiply.dev/#plotly-chart-integration">Plotly charts with lock/unlock mechanisms</a>, and the <a href="https://www.chiply.dev/#devpulse--commit-activity-grid">DevPulse commit activity visualization</a> showing GitHub contributions across multiple time scales.
</p>

<p>
<a href="https://www.chiply.dev/#recursive-link-previews">Recursive link previews support 10 levels of nesting</a>, fetching content through a <a href="https://www.chiply.dev/#proxy-server">sanitizing proxy server</a> for external sites. The <a href="https://www.chiply.dev/#seo-optimization">SEO strategy includes comprehensive meta tags</a>, structured data with JSON-LD, and <a href="https://www.chiply.dev/#prerendering">full prerendering at build time</a>. <a href="https://www.chiply.dev/#analytics-and-tracking">Analytics combine Vercel's built-in monitoring</a> with custom engagement tracking for scroll depth and read completion, plus <a href="https://www.chiply.dev/#microsoft-clarity">Microsoft Clarity for heatmaps</a>.
</p>

<p>
<a href="https://www.chiply.dev/#newsletter-integration">Newsletter subscriptions use Buttondown's API</a> with double opt-in for GDPR compliance. <a href="https://www.chiply.dev/#security-considerations">Security measures include DOMPurify HTML sanitization</a>, strict Content Security Policy headers, and <a href="https://www.chiply.dev/#input-validation">comprehensive input validation</a>. The site maintains <a href="https://www.chiply.dev/#accessibility">WCAG compliance through semantic HTML</a>, ARIA labels, and <a href="https://www.chiply.dev/#keyboard-navigation">full keyboard navigation support</a>.
</p>

<p>
<a href="https://www.chiply.dev/#performance-optimizations">Performance optimizations include lazy loading</a>, code splitting with dynamic imports, and <a href="https://www.chiply.dev/#caching-strategy">aggressive caching strategies</a> differentiated by resource type. The <a href="https://www.chiply.dev/#versioning-and-releases">release process uses semantic versioning</a> with Google's release-please automating changelog generation from conventional commits.
</p>
</div>
</div>]]></content:encoded>
    </item>

    <item>
      <title>post1</title>
      <link>https://www.chiply.dev/post1</link>
      <guid isPermaLink="true">https://www.chiply.dev/post1</guid>
      <pubDate>Thu, 22 Jan 2026 03:14:00 GMT</pubDate>
      <author>Charlie Holland (main)</author>
      <description>This is a demo post &amp;#x2013; This content is written in post1.org but is actually intended to be transcluded from another file (post0.org). That&apos;s why you utlimately see this content here.</description>
      <content:encoded><![CDATA[

<div id="outline-container-demo-heading" class="outline-2">
<h2 id="demo-heading"><span class="section-number-2">1.</span> Demo heading</h2>
<div class="outline-text-2" id="text-demo-heading">
<p>
This is a demo post &#x2013; This content is written in post1.org but is actually intended to be transcluded from another file (post0.org).  That's why you utlimately see this content here.
</p>
</div>
</div>
<div id="outline-container-demo-heading-with-tag" class="outline-2">
<h2 id="demo-heading-with-tag"><span class="section-number-2">2.</span> Demo heading with tag&#xa0;&#xa0;&#xa0;<span class="tag"><span class="orgMode">orgMode</span></span></h2>
<div class="outline-text-2" id="text-demo-heading-with-tag">
</div>
</div>]]></content:encoded>
    </item>
  </channel>
</rss>