<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://fairyfox.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://fairyfox.io/" rel="alternate" type="text/html" /><updated>2026-06-23T06:22:49+00:00</updated><id>https://fairyfox.io/feed.xml</id><title type="html">Fairy Fox</title><subtitle>The project hub and documentation library for Fairy Fox&apos;s software work — an index of projects, the documentation behind them, and a log of what&apos;s changing.</subtitle><author><name>Fairy Fox</name></author><entry><title type="html">Pokered Save Editor 2: a static-analysis gate, and tests beyond values</title><link href="https://fairyfox.io/blog/2026/06/23/pokered-save-editor-2-static-analysis-and-tests/" rel="alternate" type="text/html" title="Pokered Save Editor 2: a static-analysis gate, and tests beyond values" /><published>2026-06-23T00:00:00+00:00</published><updated>2026-06-23T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/23/pokered-save-editor-2-static-analysis-and-tests</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/23/pokered-save-editor-2-static-analysis-and-tests/"><![CDATA[<p>After a run of editor-UI work,
<a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2</a>
turned to the parts of a codebase that don’t show up on screen: a static-analysis
gate that didn’t exist yet, and a test suite that had been thorough about <em>values</em>
but quiet about everything around them. The version moved from <code class="language-plaintext highlighter-rouge">0.14.0-alpha</code> to
<code class="language-plaintext highlighter-rouge">0.14.2-alpha</code> across this work.</p>

<h2 id="a-linting-layer-and-the-bug-it-found">A linting layer, and the bug it found</h2>

<p>The project had no static analysis. This pass added one: a curated, defect-focused
<code class="language-plaintext highlighter-rouge">.clang-tidy</code> (the clang static analyzer plus the high-signal bugprone, performance,
and unused-code checks — the noisy style families deliberately left off and
documented as such), a cppcheck suppressions file, <code class="language-plaintext highlighter-rouge">lint</code> scripts for both shells,
and a GitHub Actions <code class="language-plaintext highlighter-rouge">lint</code> workflow. The gate runs file-parallel over ~140 Qt
translation units and lands clean: 143 TUs, zero findings.</p>

<p>It got there by fixing the real defects the first run surfaced. The most consequential
was a percentage that had been wrong for most of its range:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// PokemonBox::expLevelRangePercent() — returns a float</span>
<span class="o">-</span> <span class="k">return</span> <span class="n">curExp</span> <span class="o">/</span> <span class="n">expEnd</span><span class="p">;</span>        <span class="c1">// both var32: integer divide, THEN widen → truncated</span>
<span class="o">+</span> <span class="k">return</span> <span class="n">expEnd</span> <span class="o">?</span> <span class="p">(</span><span class="kt">float</span><span class="p">)</span><span class="n">curExp</span> <span class="o">/</span> <span class="n">expEnd</span> <span class="o">:</span> <span class="mf">0.0f</span><span class="p">;</span>   <span class="c1">// float divide + divide-by-zero guard</span>
</code></pre></div></div>

<p>Because both operands were 32-bit integers, the division happened in integer space
and the fractional part was discarded <em>before</em> the result widened to the float return
type. The progress-through-level percentage was therefore pinned at 0 until the very
top of a level, where it would finally tick to 1. A new mid-window assertion in
<code class="language-plaintext highlighter-rouge">tst_pokemonbox</code> now guards it.</p>

<p>The same run caught an unguarded pointer dereference on one branch of <code class="language-plaintext highlighter-rouge">PokemonBox::update()</code>
(the type-2 path wasn’t guarded the way the type-1 path was), a signed/unsigned <code class="language-plaintext highlighter-rouge">char</code>
comparison, an integer-multiply widening, several loop variables that should have been
const references, and a missing <code class="language-plaintext highlighter-rouge">default</code> in a switch. One finding — a destructor that
can reach a pure-virtual call on an unusual lifetime path — was suppressed and flagged
for a deliberate refactor rather than patched in place.</p>

<h2 id="a-dead-store-that-wasnt-a-no-op">A dead store that wasn’t a no-op</h2>

<p>One analyzer finding was a behavioural bug, not just a code-smell. When <code class="language-plaintext highlighter-rouge">PokemonMove</code>
fills an empty slot it calls <code class="language-plaintext highlighter-rouge">randomize()</code>, which assigns a random 0–3 PP-Ups; the
constructor was then meant to reset that to zero so a brand-new move starts clean. It
didn’t:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// constructor parameter `ppUp` shadows the member of the same name</span>
<span class="o">-</span> <span class="n">ppUp</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>          <span class="c1">// assigns the parameter — a no-op; the member keeps its random value</span>
<span class="o">+</span> <span class="k">this</span><span class="o">-&gt;</span><span class="n">ppUp</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>    <span class="c1">// assigns the member, as intended</span>
</code></pre></div></div>

<p>The assignment targeted the shadowing constructor parameter, so the member kept the
random count and new moves silently carried 0–3 PP-Ups they shouldn’t have. The fix is
scoped to construction only; the deliberate “randomize everything” actions are
unaffected.</p>

<h2 id="tests-that-assert-more-than-values">Tests that assert more than values</h2>

<p>The suite had been strong on values — does a fragment hold the right bytes after an
edit — and largely silent on the machinery around them. This work added the missing
test <em>types</em>:</p>

<ul>
  <li><strong>Signal/slot coverage</strong> (<code class="language-plaintext highlighter-rouge">tst_signals</code>, via <code class="language-plaintext highlighter-rouge">QSignalSpy</code>): mutating a fragment emits
exactly the right change signal, with the right count, and bound-clamped no-ops still
honour their always-emit contract. It also verifies the internal <code class="language-plaintext highlighter-rouge">connect()</code> wiring
fires by spying the <em>downstream</em> signal.</li>
  <li><strong>Model contracts</strong> (<code class="language-plaintext highlighter-rouge">tst_model_tester</code>): every <code class="language-plaintext highlighter-rouge">QAbstractItemModel</code> subclass is wrapped
in Qt’s own <code class="language-plaintext highlighter-rouge">QAbstractItemModelTester</code>, which validates the framework contract and the
change-signal protocol (<code class="language-plaintext highlighter-rouge">beginResetModel</code>/<code class="language-plaintext highlighter-rouge">endResetModel</code>, <code class="language-plaintext highlighter-rouge">dataChanged</code>, <code class="language-plaintext highlighter-rouge">layoutChanged</code>
bracketing) that hand-written tests tend to miss. All models pass clean.</li>
  <li><strong>A visual-regression floor</strong> (<code class="language-plaintext highlighter-rouge">tst_visual_regression</code>): every home-reachable screen is
grabbed through the offscreen renderer and required to show a non-trivial spread of
distinct colours — catching the failure a load-without-warnings test can’t, a screen
that loads clean yet <em>renders blank</em>. It is deliberately not a pixel-perfect baseline
diff, which the project treats as a trap during active UI polish.</li>
  <li><strong>Acceptance scenarios</strong> (<code class="language-plaintext highlighter-rouge">tst_acceptance</code>): the two user-level promises stated as
Given/When/Then — an edited value survives save → close → reopen, and browsing every
screen mutates zero save bytes.</li>
</ul>

<p>That last one is the project’s standing rule, now stated as an executable acceptance test:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GIVEN a loaded save file
WHEN  every screen is visited in turn
THEN  zero save bytes change   // "Save File Integrity Is Sacred"
</code></pre></div></div>

<p>The full <code class="language-plaintext highlighter-rouge">ctest</code> run stays green across all of it. With the test <em>types</em> now broad —
values, signals, model contracts, GUI behaviour, visual rendering, and acceptance — the
remaining testing work is depth: pushing line coverage toward 100%, where a first
gap-fill pass also documented an honest limit, that some uncovered code is reachable only
through QML’s meta-object system because the shared libraries export just their
registered classes.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2 repository</a> ·
<a href="https://fairyfox.io/pokered-save-editor-2/">documentation site</a> ·
<a href="https://github.com/junebug12851/pokered-save-editor-2/releases/tag/v0.14.2-alpha">v0.14.2-alpha release</a></li>
  <li><a href="https://clang.llvm.org/extra/clang-tidy/">clang-tidy</a> ·
<a href="https://doc.qt.io/qt-6/qabstractitemmodeltester.html">QAbstractItemModelTester</a> ·
<a href="https://doc.qt.io/qt-6/qsignalspy.html">QSignalSpy</a></li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="pokered-save-editor-2" /><category term="update" /><summary type="html"><![CDATA[After a run of editor-UI work, Pokered Save Editor 2 turned to the parts of a codebase that don’t show up on screen: a static-analysis gate that didn’t exist yet, and a test suite that had been thorough about values but quiet about everything around them. The version moved from 0.14.0-alpha to 0.14.2-alpha across this work.]]></summary></entry><entry><title type="html">Introducing fairyfox.io</title><link href="https://fairyfox.io/blog/2026/06/22/introducing-fairyfox/" rel="alternate" type="text/html" title="Introducing fairyfox.io" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/22/introducing-fairyfox</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/22/introducing-fairyfox/"><![CDATA[<p>This post marks the launch of fairyfox.io and records how the site is put
together.</p>

<p>fairyfox.io is the hub for Fairy Fox’s software projects. Until now those projects
lived as separate repositories with no shared front door; this site provides one,
along with a place to document the conventions they have in common.</p>

<h2 id="what-the-site-provides">What the site provides</h2>

<ul>
  <li><strong>A project index.</strong> The <a href="/projects/">projects page</a> is generated from a single
registry, so it stays current. It currently covers
<a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2</a>
and <a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt</a>.</li>
  <li><strong>A documentation library.</strong> The <a href="/docs/">docs</a> section collects an ecosystem
overview, the shared engineering standards, and an entry point into each
project’s own documentation site.</li>
  <li><strong>An updates log.</strong> This blog, where changes — including round-ups of activity
across the projects — are written up.</li>
</ul>

<h2 id="how-the-repositories-connect">How the repositories connect</h2>

<p>The site is also a hub in a more literal sense: it holds the canonical shared
standards (git workflow, versioning, the notes system, cross-project sync), and
each project pulls them in on demand through a plain shallow <code class="language-plaintext highlighter-rouge">git</code> operation — no
submodules, no live dependency. In the other direction, the hub keeps read-only
copies of the projects so their changes can be tracked and documented. Each flow
is one-directional git, which keeps the repositories independent and avoids
circular updates. The model is documented under
<a href="/docs/cross-project-sync/">cross-project sync</a>.</p>

<p>A useful side effect of the setup: because the custom domain is configured on the
user site, each project’s GitHub Pages site is served under the same domain, so
the navigation can lead directly into a project’s documentation.</p>]]></content><author><name>Fairy Fox</name></author><category term="meta" /><category term="site" /><summary type="html"><![CDATA[This post marks the launch of fairyfox.io and records how the site is put together.]]></summary></entry><entry><title type="html">Random AI Prompt: a full test suite, and back to shipping</title><link href="https://fairyfox.io/blog/2026/06/22/random-ai-prompt-tests-and-shipping/" rel="alternate" type="text/html" title="Random AI Prompt: a full test suite, and back to shipping" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/22/random-ai-prompt-tests-and-shipping</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/22/random-ai-prompt-tests-and-shipping/"><![CDATA[<p><a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt</a> closed out its
revival sprint with the two things a modernised project needs most: real tests and a
working release path.</p>

<h2 id="from-a-smoke-test-to-a-real-suite">From a smoke test to a real suite</h2>

<p>Until now the project had only linting and an import smoke test. It now has layered
coverage — Vitest for the Node engine and the React app, Playwright for the browser:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm test
✓ lint (0 errors)
✓ smoke (module graph + all prompts)
✓ vitest  — 88 node + 30 web   (118 passed)

$ npm run test:e2e
✓ playwright — e2e + visual + a11y   (8 passed)
</code></pre></div></div>

<p>The browser specs include visual-regression snapshots (with the random suggestion
masked so the page is stable) and accessibility checks via <code class="language-plaintext highlighter-rouge">@axe-core/playwright</code>.</p>

<h2 id="the-landmine-worth-documenting">The landmine worth documenting</h2>

<p>The most instructive detail: the underlying utility library captures the global
random function at import time, so you <em>can’t</em> make its randomness deterministic by
overriding <code class="language-plaintext highlighter-rouge">Math.random</code> in a test:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Does NOT work — lodash already captured Math.random on import</span>
<span class="nb">Math</span><span class="p">.</span><span class="nx">random</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="mf">0.42</span><span class="p">;</span>
<span class="nf">expect</span><span class="p">(</span><span class="nx">_</span><span class="p">.</span><span class="nf">sample</span><span class="p">(</span><span class="nx">list</span><span class="p">)).</span><span class="nf">toBe</span><span class="p">(</span><span class="nx">list</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>   <span class="c1">// still random</span>

<span class="c1">// What the tests do instead: assert invariants, not exact picks</span>
<span class="kd">const</span> <span class="nx">out</span> <span class="o">=</span> <span class="nf">expandPrompt</span><span class="p">(</span><span class="dl">"</span><span class="s2">{#city}</span><span class="dl">"</span><span class="p">);</span>
<span class="nf">expect</span><span class="p">(</span><span class="nx">out</span><span class="p">).</span><span class="nf">toMatch</span><span class="p">(</span><span class="sr">/streetview/</span><span class="p">);</span>          <span class="c1">// structure is stable</span>
<span class="nf">expect</span><span class="p">(</span><span class="nx">out</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="dl">"</span><span class="s2">,</span><span class="dl">"</span><span class="p">).</span><span class="nx">length</span><span class="p">).</span><span class="nf">toBeGreaterThan</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
</code></pre></div></div>

<p>Only the language’s own renderer, which uses its own seeded RNG, is driven
deterministically. (Getting the browser tests to launch on the dev machine was its
own saga, ultimately solved by pointing Playwright at the system Chrome.)</p>

<h2 id="unbreaking-the-pipeline">Unbreaking the pipeline</h2>

<p>With tests in place, the focus turned to shipping. The stable branch had been held at
the pre-revival state, and recent commits were red in CI — not from test failures, but
from the install step:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm error code EUSAGE
npm error `npm ci` can only install with an up to date package-lock.json
npm error Missing: @emnapi/core@1.11.1 from lock file
</code></pre></div></div>

<p>The lockfile had drifted after the new dependencies came in, and an <em>incremental</em>
<code class="language-plaintext highlighter-rouge">npm install</code> on the Windows machine omitted the Linux-only optional packages that CI
needs. The fix that actually works is a clean, full resolve so every platform’s
optional dependencies are recorded:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">rm</span> <span class="nt">-rf</span> node_modules package-lock.json
npm <span class="nb">install</span>        <span class="c"># fresh resolve records @emnapi/*, linux-* bindings, etc.</span>
npm ci             <span class="c"># now passes the sync check</span>
</code></pre></div></div>

<p>Once green, the stable branch fast-forwarded to the current work for the first time in
the revival — which promptly exposed two more first-time deployment breakages in the
docs and release workflows, both fixed. A recurring theme of the week held to the end:
the failures were almost never the code itself, but the build, packaging, and tooling
around it.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt repository</a> ·
<a href="https://fairyfox.io/random-ai-prompt/">documentation site</a></li>
  <li><a href="https://vitest.dev/">Vitest</a> · <a href="https://playwright.dev/">Playwright</a> ·
<a href="https://www.npmjs.com/package/@axe-core/playwright"><code class="language-plaintext highlighter-rouge">@axe-core/playwright</code></a></li>
  <li><a href="https://docs.npmjs.com/cli/v10/commands/npm-ci"><code class="language-plaintext highlighter-rouge">npm ci</code></a> (why a clean lockfile matters in CI)</li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="random-ai-prompt" /><category term="update" /><summary type="html"><![CDATA[Random AI Prompt closed out its revival sprint with the two things a modernised project needs most: real tests and a working release path.]]></summary></entry><entry><title type="html">Random AI Prompt: a readable language for dynamic prompts</title><link href="https://fairyfox.io/blog/2026/06/21/random-ai-prompt-dpl/" rel="alternate" type="text/html" title="Random AI Prompt: a readable language for dynamic prompts" /><published>2026-06-21T00:00:00+00:00</published><updated>2026-06-21T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/21/random-ai-prompt-dpl</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/21/random-ai-prompt-dpl/"><![CDATA[<p>The most ambitious thread in
<a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt</a>’s revival is
<strong>DPL</strong>, a Dynamic Prompt Language. The generators that build prompts had always been
hand-written JavaScript. DPL is a Markdown-readable language so they can be authored
and read by people who don’t write code — with a JavaScript escape hatch kept for the
genuinely logic-heavy ones.</p>

<h2 id="why-a-language-at-all">Why a language at all</h2>

<p>A “dynamic prompt” assembles a prompt from optional pieces, weighted choices, and
references to word lists. Expressing that in JavaScript means every generator is a
small program, which is fine for a programmer and opaque to everyone else. The goal
was a format where the <em>structure</em> of a prompt is visible at a glance.</p>

<h2 id="what-dpl-looks-like">What DPL looks like</h2>

<p>Here’s a complete generator — the city street scene — verbatim:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
description: A city streetview
---
Start
===
city, streetview, {city}
- {style/building}
- cityscape
- downtown

{#nature}, {#weather}, reflective street
</code></pre></div></div>

<p>The pieces are deliberately plain: front matter for metadata, a <code class="language-plaintext highlighter-rouge">===</code> heading for a
section, a plain line that’s always included, <code class="language-plaintext highlighter-rouge">- bullet</code> lines that are optional
(50% by default), <code class="language-plaintext highlighter-rouge">{list}</code> to pull from a word list, and <code class="language-plaintext highlighter-rouge">{#generator}</code> to call
another generator. Probabilities and repetition read naturally too:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start
===
- {art-movement}
- {art-technique}
- 50% repeat 0 to 3 times: {image-effect}
- {#rays}
</code></pre></div></div>

<p>And explicit either/or branching:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start
===
portrait
- maybe: full body
- otherwise: head and chest, upperbody
- up-close
</code></pre></div></div>

<h2 id="logic-stays-in-javascript">Logic stays in JavaScript</h2>

<p>A core design decision is that DPL is <em>data, not code</em> — it has no variables or
counters. Anything that genuinely needs logic delegates to a referenced JavaScript
file, so the <code class="language-plaintext highlighter-rouge">.dpl</code> stays declarative:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>---
description: A subject — animal, character, flower, instrument, creature, tree, or person
script: entity.js
---
</code></pre></div></div>

<p>The bridge works in both directions: DPL can call into JS, and JS can call back into
DPL sections. About 90% of the catalog ended up as pure DPL; the rest keeps a thin
<code class="language-plaintext highlighter-rouge">.js</code> sidecar.</p>

<h2 id="a-weighted-layer-tree">A weighted layer tree</h2>

<p>Underneath, v3 changes the model. The older generators built an ordered string where
position carried meaning. v3 treats every file, section, and line as a <em>layer</em> in a
tree — the user’s prompt box is the root, each building block a child — and renders by
sorting each layer’s children by weight rather than by where they appear. Auto-weights
start at 1000 and increment per line; an explicit <code class="language-plaintext highlighter-rouge">[n]</code> overrides:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Start
===
[100] wide shot, dramatic lighting   # low weight → sorts to the front
ancient forest, morning mist         # auto-weighted (1000, 1001, …)
</code></pre></div></div>

<h2 id="built-in-phases">Built in phases</h2>

<p>To protect a working app, this shipped in phases: (1) the parser and renderer plus a
test harness, with sample generators converted; (2) the entire existing catalog
converted to DPL; (3) wiring DPL into the live engine and both loaders, making the new
v3 catalog the default; and (4) the wrapper UI — a start/end frame applied around
every prompt. The frozen v1 and v2 generations remain fully functional, addressed by a
path prefix:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{#cave}        → v3 (the default)
{#v2/cave}     → the frozen v2 version
{#v1/castle}   → the frozen v1 version
{#any}         → pick one generator at random from the v3 catalog
</code></pre></div></div>

<p>The “full versus partial prompt” distinction that drove a lot of duplication in the
old system is on its way out, which was the underlying goal all along.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt repository</a> ·
<a href="https://fairyfox.io/random-ai-prompt/">documentation site</a></li>
  <li>DPL design and language notes:
<a href="https://github.com/junebug12851/random-ai-prompt/blob/master/notes/reference/dpl-design.md"><code class="language-plaintext highlighter-rouge">reference/dpl-design.md</code></a>,
<a href="https://github.com/junebug12851/random-ai-prompt/blob/master/notes/reference/dpl-language.md"><code class="language-plaintext highlighter-rouge">reference/dpl-language.md</code></a></li>
  <li>Prior art: <a href="https://github.com/adieyal/sd-dynamic-prompts">sd-dynamic-prompts</a> (the wildcard/variant idea in the wider ecosystem)</li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="random-ai-prompt" /><category term="update" /><summary type="html"><![CDATA[The most ambitious thread in Random AI Prompt’s revival is DPL, a Dynamic Prompt Language. The generators that build prompts had always been hand-written JavaScript. DPL is a Markdown-readable language so they can be authored and read by people who don’t write code — with a JavaScript escape hatch kept for the genuinely logic-heavy ones.]]></summary></entry><entry><title type="html">Random AI Prompt: untangling the word lists</title><link href="https://fairyfox.io/blog/2026/06/20/random-ai-prompt-lists/" rel="alternate" type="text/html" title="Random AI Prompt: untangling the word lists" /><published>2026-06-20T00:00:00+00:00</published><updated>2026-06-20T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/20/random-ai-prompt-lists</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/20/random-ai-prompt-lists/"><![CDATA[<p>Lists are the heart of
<a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt</a>. Each is a
plain text file of options, one of which is picked at random when referenced — so
<code class="language-plaintext highlighter-rouge">{color}</code> in a prompt becomes one line from <code class="language-plaintext highlighter-rouge">color.txt</code>. Over the years the lists
had grown into a tangle: a giant unsorted dictionary, proper nouns mixed with common
words, and clunky duplicate lists. This day was a long, careful overhaul of all of
it.</p>

<h2 id="folders-and-path-suffix-resolution">Folders and path-suffix resolution</h2>

<p>The flat pile of files moved into category folders, with resolution by path suffix
so a bare name, a partial path, or a full path all work:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data/lists/
  color.txt                      → {color}
  danbooru/d/general-sfw.txt      → {general-sfw}  or  {d/general-sfw}
  artist/dhigh.txt                → {dhigh}        or  {artist/dhigh}
</code></pre></div></div>

<p>That keeps the ~78 existing <code class="language-plaintext highlighter-rouge">{name}</code> references working while allowing deep
organisation underneath.</p>

<h2 id="a-filename-based-sfw-model">A filename-based SFW model</h2>

<p>The trickiest part was content gating, which went through several iterations before
settling on a rule keyed entirely off the <strong>filename</strong> — no special configuration,
no runtime filtering. A mixed-topic list is two files plus an implicit combined
reference:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>data/lists/danbooru/d/
  general-sfw.txt     # the safe half
  general-nsfw.txt    # the gated half
  general-sfw.json    # { "description": "Danbooru general descriptor tags (SFW)." }
</code></pre></div></div>

<p>The resolver combines them by mode: <code class="language-plaintext highlighter-rouge">{general}</code> yields the SFW half when adult
content is off and both halves when it’s on, while <code class="language-plaintext highlighter-rouge">{general-sfw}</code> is always just the
safe half. The design goal was that a safe-only user never has to type anything
special to stay safe. A content-safety pass also tightened the lists, keeping
ordinary adult content gated rather than deleted, with real place names and artist
handles protected from false positives.</p>

<h2 id="group-files-instead-of-hardcoded-composites">Group files instead of hardcoded composites</h2>

<p>Composite lists became real <code class="language-plaintext highlighter-rouge">.group</code> files — each line references another list (or
group), resolved by the same suffix lookup:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># data/lists/artist/digipa.group
# Group: the three digital-painting impact artist lists.
artist/dhigh
artist/dmed
artist/dlow
</code></pre></div></div>

<p>Folders with two or more lists become implied groups automatically; optional
<code class="language-plaintext highlighter-rouge">&lt;list&gt;.json</code> sidecars supply tooltips in the editor; and <code class="language-plaintext highlighter-rouge">keyword</code> became a reserved
wildcard that draws a word from any loaded list.</p>

<h2 id="letting-a-dictionary-do-the-sorting">Letting a dictionary do the sorting</h2>

<p>The cleanup ran on a principle worth stating: for proper nouns, a model’s world
knowledge is the right classifier (no dictionary knows Achernar is a star), but for
parts of speech a real dictionary <em>states</em> the answer rather than guessing it. So the
dictionary dump was re-sorted into parts of speech using <strong>WordNet</strong> as the
authority, and a roughly 8,800-entry “keyword” junk drawer of proper nouns was
hand-classified into people, places, organisations, mythology, astronomy, religion,
history, and more — with coverage checks so nothing was silently dropped. Several
parallel review passes over the curated lists then caught and fixed hundreds of
misfiled entries (surnames sitting in a first-names list, and the like).</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt repository</a> ·
<a href="https://fairyfox.io/random-ai-prompt/">documentation site</a></li>
  <li><a href="https://wordnet.princeton.edu/">Princeton WordNet</a> ·
<a href="https://www.npmjs.com/package/wordpos"><code class="language-plaintext highlighter-rouge">wordpos</code></a> (the Node interface used)</li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="random-ai-prompt" /><category term="update" /><summary type="html"><![CDATA[Lists are the heart of Random AI Prompt. Each is a plain text file of options, one of which is picked at random when referenced — so {color} in a prompt becomes one line from color.txt. Over the years the lists had grown into a tangle: a giant unsorted dictionary, proper nouns mixed with common words, and clunky duplicate lists. This day was a long, careful overhaul of all of it.]]></summary></entry><entry><title type="html">Random AI Prompt: a new home for the web app</title><link href="https://fairyfox.io/blog/2026/06/19/random-ai-prompt-web-app/" rel="alternate" type="text/html" title="Random AI Prompt: a new home for the web app" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/19/random-ai-prompt-web-app</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/19/random-ai-prompt-web-app/"><![CDATA[<p>With <a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt</a>
modernised and documented, attention turned to its React web app. It worked, but it
felt like a developer tool: a light default theme, a cramped column, and most
features hidden behind comments so only a bare “Build” tab showed.</p>

<h2 id="matching-the-originals-look">Matching the original’s look</h2>

<p>The first pass rebuilt the home page around the visual language the original
pre-revival generator had established — a dark charcoal canvas with a mint-green
accent — captured as CSS variables so the theme is defined in one place:</p>

<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">:root</span> <span class="p">{</span>
  <span class="py">--bg</span><span class="p">:</span>           <span class="nf">hsl</span><span class="p">(</span><span class="m">0</span> <span class="m">0%</span> <span class="m">13%</span><span class="p">);</span>   <span class="c">/* charcoal canvas */</span>
  <span class="py">--text</span><span class="p">:</span>         <span class="nf">hsl</span><span class="p">(</span><span class="m">0</span> <span class="m">0%</span> <span class="m">96%</span><span class="p">);</span>
  <span class="py">--accent</span><span class="p">:</span>       <span class="nx">#34e2a0</span><span class="p">;</span>         <span class="c">/* mint green */</span>
  <span class="py">--font-display</span><span class="p">:</span> <span class="s1">"Space Grotesk"</span><span class="p">,</span> <span class="n">system-ui</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The real source files were rebuilt rather than mocked up, and the shared-link
feature that seeds the app from a URL (<code class="language-plaintext highlighter-rouge">#s=…</code>) was preserved throughout.</p>

<h2 id="decluttering-into-a-workspace">Decluttering into a workspace</h2>

<p>A round of feedback then pushed it from “redesigned” toward “feels like an app.” The
made-up tagline and centred hero went; the display font changed to one that read
less like a logo; and image generation, the chaos control, and presets were pulled
off the home screen for now. Nothing was deleted outright — everything removed was
logged for later in a <code class="language-plaintext highlighter-rouge">removed-pending-readd.md</code> note, with presets explicitly
slated to return as a richer feature. The layout became a full-height app window with
a left building-blocks pane beside the composer, and the prompt input was simplified
so its rotating placeholder <em>is</em> the live suggestion.</p>

<h2 id="a-small-fix-with-a-lesson-attached">A small fix with a lesson attached</h2>

<p>One exchange is worth recording. An offhand remark that the Share link “doesn’t work
and was complicated” was wrongly taken as an instruction to remove it. It was an
observation, not an order — so the feature was restored and then actually fixed. The
old version flashed a tiny “Link copied!” and failed silently when the clipboard API
was blocked; the new one keeps the link visible in a selectable field, so it works
regardless:</p>

<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">ShareLink</span><span class="p">({</span> <span class="nx">url</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">copied</span><span class="p">,</span> <span class="nx">setCopied</span><span class="p">]</span> <span class="o">=</span> <span class="nf">useState</span><span class="p">(</span><span class="kc">false</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">copy</span> <span class="o">=</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">try</span> <span class="p">{</span> <span class="k">await</span> <span class="nb">navigator</span><span class="p">.</span><span class="nx">clipboard</span><span class="p">.</span><span class="nf">writeText</span><span class="p">(</span><span class="nx">url</span><span class="p">);</span> <span class="nf">setCopied</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span> <span class="p">}</span>
    <span class="k">catch</span> <span class="p">{</span> <span class="cm">/* clipboard blocked — the field below still shows the link */</span> <span class="p">}</span>
  <span class="p">};</span>
  <span class="k">return </span><span class="p">(</span>
    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">className</span><span class="p">=</span><span class="s">"share"</span><span class="p">&gt;</span>
      <span class="p">&lt;</span><span class="nt">input</span> <span class="na">readOnly</span> <span class="na">value</span><span class="p">=</span><span class="si">{</span><span class="nx">url</span><span class="si">}</span> <span class="na">onFocus</span><span class="p">=</span><span class="si">{</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nf">select</span><span class="p">()</span><span class="si">}</span> <span class="p">/&gt;</span>
      <span class="p">&lt;</span><span class="nt">button</span> <span class="na">onClick</span><span class="p">=</span><span class="si">{</span><span class="nx">copy</span><span class="si">}</span><span class="p">&gt;</span><span class="si">{</span><span class="nx">copied</span> <span class="p">?</span> <span class="dl">"</span><span class="s2">✓ Copied</span><span class="dl">"</span> <span class="p">:</span> <span class="dl">"</span><span class="s2">Copy</span><span class="dl">"</span><span class="si">}</span><span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The takeaway, noted for next time: a critique or a question is an invitation to
explain or ask, not permission to delete.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt repository</a> ·
<a href="https://fairyfox.io/random-ai-prompt/">documentation site</a></li>
  <li><a href="https://react.dev/">React</a> · <a href="https://vite.dev/">Vite</a></li>
  <li><a href="https://fonts.google.com/specimen/Space+Grotesk">Space Grotesk</a> ·
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText"><code class="language-plaintext highlighter-rouge">navigator.clipboard.writeText</code></a></li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="random-ai-prompt" /><category term="update" /><summary type="html"><![CDATA[With Random AI Prompt modernised and documented, attention turned to its React web app. It worked, but it felt like a developer tool: a light default theme, a cramped column, and most features hidden behind comments so only a bare “Build” tab showed.]]></summary></entry><entry><title type="html">Random AI Prompt: a 2026 revival — ES modules, Node 24, and full docs</title><link href="https://fairyfox.io/blog/2026/06/18/random-ai-prompt-revival/" rel="alternate" type="text/html" title="Random AI Prompt: a 2026 revival — ES modules, Node 24, and full docs" /><published>2026-06-18T00:00:00+00:00</published><updated>2026-06-18T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/18/random-ai-prompt-revival</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/18/random-ai-prompt-revival/"><![CDATA[<p>Attention this week turns to the other project in the hub:
<a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt</a>, a JavaScript
prompt generator for Stable Diffusion. It was largely a late-2022 / early-2023
effort — a substantial one — and then sat dormant until now. This day was its
revival: dragging the codebase onto current tooling without changing what it does.</p>

<h2 id="from-commonjs-to-esm">From CommonJS to ESM</h2>

<p>The starting point was a 2022 CommonJS codebase. The target was ES modules on
<strong>Node 24</strong>, the active LTS, with every dependency moved to its current major. The
entry points opt in through <code class="language-plaintext highlighter-rouge">package.json</code>:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"module"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"engines"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"node"</span><span class="p">:</span><span class="w"> </span><span class="s2">"&gt;=24"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The bulk of the conversion — around 123 files — was done with two throwaway
codemods (<code class="language-plaintext highlighter-rouge">module.exports = function</code> → <code class="language-plaintext highlighter-rouge">export default function</code>,
<code class="language-plaintext highlighter-rouge">require("x")</code> → <code class="language-plaintext highlighter-rouge">import x from "x.js"</code>), then the trickier ~28 files were finished
by hand. A couple of genuine ES-module pitfalls came out of it and are worth
keeping.</p>

<p>The sharpest one: <strong>imports are evaluated before top-level statements.</strong> The old
code changed the working directory at the top of <code class="language-plaintext highlighter-rouge">common.js</code>, but under ESM that
line would now run <em>after</em> the settings module had already been imported and read
its config file:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Old (CommonJS): ran early enough to matter</span>
<span class="nx">process</span><span class="p">.</span><span class="nf">chdir</span><span class="p">(</span><span class="nx">__dirname</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">settings</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="dl">"</span><span class="s2">./settings</span><span class="dl">"</span><span class="p">);</span> <span class="c1">// reads ./user-settings.json</span>

<span class="c1">// New (ESM): the chdir moves into its own module, imported first</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">./chdir.js</span><span class="dl">"</span><span class="p">;</span>          <span class="c1">// performs process.chdir(...) on load</span>
<span class="k">import</span> <span class="nx">settings</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./settings.js</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<p>A second one: the generators are loaded by path inside synchronous string-replace
callbacks, so switching them to <code class="language-plaintext highlighter-rouge">await import()</code> would have meant rewriting the
whole pipeline. <code class="language-plaintext highlighter-rouge">createRequire</code> preserves the synchronous behaviour exactly:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createRequire</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">node:module</span><span class="dl">"</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">require</span> <span class="o">=</span> <span class="nf">createRequire</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">generator</span> <span class="o">=</span> <span class="nf">require</span><span class="p">(</span><span class="nx">generatorPath</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="proving-the-port-changed-nothing">Proving the port changed nothing</h2>

<p>A modernisation is only worth anything if it’s behaviour-preserving, so that was
checked rather than asserted. An import smoke test loads the entire module graph,
pulls in all 113 dynamic-prompt generators, and expands a sample prompt:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ npm run smoke
✓ module graph loaded (152 files, node --check clean)
✓ 113 dynamic prompts loaded
✓ promptSuggestion() ran; {#random} expanded
</code></pre></div></div>

<p>The tree was then reorganised so that <strong>all code lives under <code class="language-plaintext highlighter-rouge">src/</code> and all prompt
content under <code class="language-plaintext highlighter-rouge">data/</code></strong>, with runtime and user data left at the root. That’s a
careful change, because the loaders resolve paths relative to files, not the working
directory. To prove nothing was lost, the original pre-revival source was pinned as
a read-only snapshot and diffed against the current tree:</p>

<table>
  <thead>
    <tr>
      <th>Category</th>
      <th style="text-align: right">Original</th>
      <th style="text-align: right">Current</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>dynamic prompts</td>
      <td style="text-align: right">113</td>
      <td style="text-align: right">113</td>
      <td>none dropped</td>
    </tr>
    <tr>
      <td>prompt modules</td>
      <td style="text-align: right">5</td>
      <td style="text-align: right">5</td>
      <td>none dropped</td>
    </tr>
    <tr>
      <td>lists</td>
      <td style="text-align: right">61</td>
      <td style="text-align: right">61</td>
      <td>none dropped</td>
    </tr>
    <tr>
      <td>expansions</td>
      <td style="text-align: right">9</td>
      <td style="text-align: right">9</td>
      <td>none dropped</td>
    </tr>
    <tr>
      <td>presets</td>
      <td style="text-align: right">26</td>
      <td style="text-align: right">26</td>
      <td>none dropped</td>
    </tr>
  </tbody>
</table>

<p>All 96 curated data files matched the original line-for-line; of 158 shared code
files, the differences were either formatting or understood, deliberate refactors.</p>

<h2 id="documentation-end-to-end">Documentation, end to end</h2>

<p>Finally, the project was brought up to the same management system as its sibling —
living notes, a single-source version number, a documentation site, and CI/release
workflows. The documentation went all the way to per-function coverage across the
server engine, all 113 generators, the classic frontend, and the React web app,
settling on JSDoc with the notes wired in as tutorials — one site of around 244
pages.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/random-ai-prompt">Random AI Prompt repository</a> ·
<a href="https://fairyfox.io/random-ai-prompt/">documentation site</a></li>
  <li><a href="https://nodejs.org/en/about/previous-releases">Node.js release schedule</a> (Node 24 “Krypton” LTS)</li>
  <li><a href="https://nodejs.org/api/module.html#modulecreaterequirefilename"><code class="language-plaintext highlighter-rouge">node:module</code> <code class="language-plaintext highlighter-rouge">createRequire</code></a> ·
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules">MDN: JavaScript modules</a></li>
  <li><a href="https://jsdoc.app/">JSDoc</a></li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="random-ai-prompt" /><category term="update" /><summary type="html"><![CDATA[Attention this week turns to the other project in the hub: Random AI Prompt, a JavaScript prompt generator for Stable Diffusion. It was largely a late-2022 / early-2023 effort — a substantial one — and then sat dormant until now. This day was its revival: dragging the codebase onto current tooling without changing what it does.]]></summary></entry><entry><title type="html">Pokered Save Editor 2: a redesigned Pokémon editor</title><link href="https://fairyfox.io/blog/2026/06/17/pokered-save-editor-2-pokemon-editor/" rel="alternate" type="text/html" title="Pokered Save Editor 2: a redesigned Pokémon editor" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/17/pokered-save-editor-2-pokemon-editor</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/17/pokered-save-editor-2-pokemon-editor/"><![CDATA[<p>The Pokémon detail editor in
<a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2</a>
was the last major area still carrying its 2020-era layout. This day brought all three
of its tabs — General, DV/EV, and Moves — into the same visual language as the rest of
the app: grouped panels of zebra-striped rows, connected segmented controls instead of
loose buttons and overflow menus, and consistent sizing.</p>

<h2 id="the-bug-under-the-redesign">The bug under the redesign</h2>

<p>The General-tab work ended on a real, long-standing bug: the type dropdowns displayed
the wrong type for every Pokémon — a Charizard reading as Ghost/Fighting instead of
Fire/Flying — and “reset to default” appeared to do nothing. A diagnostic test proved
the stored data was correct all along; the combos just <em>rendered</em> it wrong.</p>

<p>The cause was an off-by-one. The type list has a “no type” placeholder at row 0, so a
type stored at index <code class="language-plaintext highlighter-rouge">i</code> lives at model row <code class="language-plaintext highlighter-rouge">i + 1</code> — but the lookup returned <code class="language-plaintext highlighter-rouge">i</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// TypesModel::valToIndex — "-----" placeholder sits at row 0</span>
<span class="o">-</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">i</span><span class="p">;</span>        <span class="c1">// selects one row early → Fire shows as Ghost, Flying as Fighting</span>
<span class="o">+</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span>    <span class="c1">// account for the placeholder</span>
</code></pre></div></div>

<p>The first regression test for it was <em>tautological</em> — it compared the result against
the same source the method reads, so it passed regardless. It was rewritten to assert
the literal type IDs (Fire = 20, Flying = 2), which is the difference between a test that
guards behaviour and one that just agrees with the code.</p>

<h2 id="a-binding-instead-of-a-toggle">A binding instead of a toggle</h2>

<p>The DV/EV tab’s “Future Shiny” control highlights a small but useful QML pattern. Shiny
status is <em>derived</em> from DVs, so the selection has to mirror live data — dragging the DV
sliders should flip it on its own. A checkable toggle can’t do that (a click would break
the binding), so each segment’s active state is a plain binding to the data, and clicking
performs the action that changes the data:</p>

<div class="language-qml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">SegSel</span> <span class="p">{</span>
    <span class="nl">active</span><span class="p">:</span> <span class="nx">boxData</span><span class="p">.</span><span class="nx">isShiny</span>          <span class="c1">// a binding, not a checkable state</span>
    <span class="nl">onClicked</span><span class="p">:</span> <span class="nx">boxData</span><span class="p">.</span><span class="nf">makeShiny</span><span class="p">()</span>   <span class="c1">// changes data → re-drives `active`</span>
<span class="p">}</span>
</code></pre></div></div>

<p>One source of truth, no drift.</p>

<h2 id="closing-the-loop-on-data-safety">Closing the loop on data safety</h2>

<p>The day finished with a data-integrity pass: destructive editor actions — re-roll,
reset, max out, evolve, and the bulk move operations — now ask for confirmation through
a shared dialog, while harmless per-field actions don’t. A status badge was added to the
Pokémon preview, and the level field was widened so “100” fits.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2 repository</a> ·
<a href="https://fairyfox.io/pokered-save-editor-2/">documentation site</a></li>
  <li><a href="https://bulbapedia.bulbagarden.net/wiki/Shiny_Pok%C3%A9mon#Generation_II">Gen 1 shininess is derived from DVs</a>
(Bulbapedia) · <a href="https://doc.qt.io/qt-6/qtqml-syntax-propertybinding.html">QML property binding</a></li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="pokered-save-editor-2" /><category term="update" /><summary type="html"><![CDATA[The Pokémon detail editor in Pokered Save Editor 2 was the last major area still carrying its 2020-era layout. This day brought all three of its tabs — General, DV/EV, and Moves — into the same visual language as the rest of the app: grouped panels of zebra-striped rows, connected segmented controls instead of loose buttons and overflow menus, and consistent sizing.]]></summary></entry><entry><title type="html">Pokered Save Editor 2: grounding item data in the game’s own source</title><link href="https://fairyfox.io/blog/2026/06/16/pokered-save-editor-2-item-data/" rel="alternate" type="text/html" title="Pokered Save Editor 2: grounding item data in the game’s own source" /><published>2026-06-16T00:00:00+00:00</published><updated>2026-06-16T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/16/pokered-save-editor-2-item-data</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/16/pokered-save-editor-2-item-data/"><![CDATA[<p>A theme running through
<a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2</a>
is that claims about the game should be checked against the game, not guessed. This day
put that into practice on item data.</p>

<h2 id="the-disassembly-as-the-authority">The disassembly as the authority</h2>

<p>The Market’s flat “Normal Items” list was split into meaningful groups, with the
documented Pokémon Red disassembly (<a href="https://github.com/pret/pokered">pret/pokered</a>) as
the oracle. Two of its data files settle the questions:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">data/items/prices.asm</code> — the per-item buy price (sell is half). <strong>A price of 0 means
the item can’t be priced or sold.</strong></li>
  <li><code class="language-plaintext highlighter-rouge">data/items/marts.asm</code> — what’s actually stocked; the buyable set is the union of every
reachable shop’s inventory.</li>
  <li><code class="language-plaintext highlighter-rouge">data/items/vending_prices.asm</code> — the Celadon roof vending machine (a separate vendor).</li>
</ul>

<p>The membership sets were transcribed straight from those files into the model, each
index commented with its item name:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// indices transcribed from pret/pokered data/items/marts.asm + vending_prices.asm</span>
<span class="k">static</span> <span class="k">const</span> <span class="n">QSet</span><span class="o">&lt;</span><span class="kt">int</span><span class="o">&gt;</span> <span class="n">kBuyableInGame</span> <span class="o">=</span> <span class="p">{</span> <span class="cm">/* Poké Ball, Potion, Antidote, …,
                                             TMs 01,02,05,07,09,17,32,33,37 */</span> <span class="p">};</span>

<span class="kt">bool</span> <span class="n">ItemMarketModel</span><span class="o">::</span><span class="n">buyableInGame</span><span class="p">(</span><span class="n">ItemDBEntry</span> <span class="o">*</span><span class="n">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">kBuyableInGame</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">e</span><span class="o">-&gt;</span><span class="n">index</span><span class="p">());</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Items group into <strong>Normal</strong> (a shop sells it), <strong>Vending Machine</strong> (the three roof
drinks), and <strong>Special</strong> (priced but no in-game vendor). Items with no price at all are
listed nowhere, because there’s genuinely no way to buy or sell them.</p>

<h2 id="a-naming-sweep-and-the-trap-inside-it">A naming sweep, and the trap inside it</h2>

<p>A display-wide sweep corrected “Poke” to the accented “Poké” everywhere user-visible —
Poké Mart, Poké Ball, Pokédex — touching only display strings, never identifiers. That
tripped a subtle trap: one item name doubles as a lookup key in the map data, so
renaming it orphaned a link until the data reference was updated to match:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>maps.json:  "item": "Poke Ball"   →   "Poké Ball"   (kept in lockstep with the rename)
</code></pre></div></div>

<p>A reminder that display text and data keys aren’t always separable. The editor’s target
was also documented explicitly as the US English release of Red and Blue.</p>

<h2 id="detailed-item-tooltips">Detailed item tooltips</h2>

<p>Finally, the long-deferred detailed-tooltip feature was built as a working vertical
slice: an optional <code class="language-plaintext highlighter-rouge">info</code> field per item, plumbed through to a tooltip with a title and
wrapped body, with the first eleven items’ descriptions authored and checked against the
disassembly. The rest of the items, and wiring the tooltip into the other screens, are
the next steps.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2 repository</a> ·
<a href="https://fairyfox.io/pokered-save-editor-2/">documentation site</a></li>
  <li><a href="https://github.com/pret/pokered/tree/master/data/items">pret/pokered <code class="language-plaintext highlighter-rouge">data/items/</code></a>
(<code class="language-plaintext highlighter-rouge">prices.asm</code>, <code class="language-plaintext highlighter-rouge">marts.asm</code>, <code class="language-plaintext highlighter-rouge">vending_prices.asm</code>)</li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="pokered-save-editor-2" /><category term="update" /><summary type="html"><![CDATA[A theme running through Pokered Save Editor 2 is that claims about the game should be checked against the game, not guessed. This day put that into practice on item data.]]></summary></entry><entry><title type="html">Pokered Save Editor 2: rebuilding the Market screen, plus screenshots and releases</title><link href="https://fairyfox.io/blog/2026/06/15/pokered-save-editor-2-market-and-pipelines/" rel="alternate" type="text/html" title="Pokered Save Editor 2: rebuilding the Market screen, plus screenshots and releases" /><published>2026-06-15T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://fairyfox.io/blog/2026/06/15/pokered-save-editor-2-market-and-pipelines</id><content type="html" xml:base="https://fairyfox.io/blog/2026/06/15/pokered-save-editor-2-market-and-pipelines/"><![CDATA[<p>The Pokémart / Game Corner screen in
<a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2</a>
had long been the most half-finished, fragile part of the app. This day was a long,
iterative rebuild of it, plus two pieces of project infrastructure.</p>

<h2 id="the-market-rework">The Market rework</h2>

<p>The screen became a clean two-pane layout: a shopping list on the left and a cart that
reads like a store receipt on the right, itemised with a running total. Buying and
selling were unified into a single cart per currency, summing into one net total, and
the mode controls moved out of the footer into segmented header strips.</p>

<p>The standout fix was the money/coins exchange. A coin is worth ₽20 — confirmed in the
game’s own data (<code class="language-plaintext highlighter-rouge">gameCorner.json</code> <code class="language-plaintext highlighter-rouge">"price": 20</code>) — but the conversion was inverted:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A coin is worth ₽20.

  old:  coins = rate × money      → ~20 coins per ₽1   (inverted — wrong direction)
  new:  money = rate × coins      → ₽20 per coin        (correct)
</code></pre></div></div>

<p>So the data was right and the C++ math was wrong. It was reworked into a clear
converter — money on one side, coins on the other, live deltas above each. To check
this kind of thing against an authority, the documented Pokémon Red disassembly
(<a href="https://github.com/pret/pokered">pret/pokered</a>) was cloned in as a read-only
reference. The rework also surfaced another use-after-free, traced with a debugger to
shop entries being read from cross-instance static registries after they’d been freed;
the fix sweeps only the current model’s live entries.</p>

<h2 id="an-automated-screenshot-pipeline">An automated screenshot pipeline</h2>

<p>A headless tool now boots the real UI and captures PNGs of every screen and several
states — and it only ever reads the save in memory, so it can’t touch save data.
Getting it to render correctly offscreen took working through several quirks (missing
fonts, blank grabs, dropped visual effects), ending on a real but hidden GPU window:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># render through a real GPU window, parked off-screen, so effects actually draw</span>
<span class="nv">PSE_SHOT_SIZE</span><span class="o">=</span>750x480   <span class="c"># capture at the app's real window size</span>
</code></pre></div></div>

<h2 id="a-release-pipeline">A release pipeline</h2>

<p>A GitHub Actions workflow now builds and publishes a GitHub Release, gated so it only
fires when the version was actually bumped:</p>

<table>
  <thead>
    <tr>
      <th>Artifact</th>
      <th>Platform</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Installer (<code class="language-plaintext highlighter-rouge">.exe</code>, Inno Setup)</td>
      <td>Windows</td>
    </tr>
    <tr>
      <td>Portable (<code class="language-plaintext highlighter-rouge">.zip</code>)</td>
      <td>Windows</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AppImage</code></td>
      <td>Linux</td>
    </tr>
    <tr>
      <td>Portable (<code class="language-plaintext highlighter-rouge">.tar.gz</code>)</td>
      <td>Linux</td>
    </tr>
    <tr>
      <td>Doxygen HTML docs (<code class="language-plaintext highlighter-rouge">.zip</code>)</td>
      <td>—</td>
    </tr>
    <tr>
      <td>UI screenshots (<code class="language-plaintext highlighter-rouge">.zip</code>)</td>
      <td>—</td>
    </tr>
  </tbody>
</table>

<p>The first release, <code class="language-plaintext highlighter-rouge">v0.7.2-alpha</code>, went out with all six artifacts after the usual
first-run shakeout (a <code class="language-plaintext highlighter-rouge">windeployqt</code> flag unsupported on the llvm-mingw toolchain, an
Inno Setup path quirk, and a Linux binary-location difference). Screenshots and docs
are also served from GitHub Pages, so they stay current without bloating the repo.</p>

<h3 id="references">References</h3>

<ul>
  <li><a href="https://github.com/junebug12851/pokered-save-editor-2">Pokered Save Editor 2 repository</a> ·
<a href="https://fairyfox.io/pokered-save-editor-2/">documentation site</a></li>
  <li><a href="https://github.com/pret/pokered">pret/pokered</a> (the save-format / pricing oracle)</li>
  <li><a href="https://docs.github.com/actions">GitHub Actions</a> · <a href="https://jrsoftware.org/isinfo.php">Inno Setup</a> ·
<a href="https://github.com/linuxdeploy/linuxdeploy">linuxdeploy</a></li>
</ul>]]></content><author><name>Fairy Fox</name></author><category term="pokered-save-editor-2" /><category term="update" /><summary type="html"><![CDATA[The Pokémart / Game Corner screen in Pokered Save Editor 2 had long been the most half-finished, fragile part of the app. This day was a long, iterative rebuild of it, plus two pieces of project infrastructure.]]></summary></entry></feed>