Tutorial: Changelog — June 2026

Changelog — June 2026

Newest entry on top.

2026-06-30 — Security & code-health integrations (Dependabot, CodeQL, Scorecard, Sonar, CodeRabbit) (docs/CI, no bump)

Added free-for-OSS analysis/automation services on top of CI (owner picked all five). GitHub-native, committed and live now: Dependabot (.github/dependabot.yml — grouped weekly dep PRs for engine + gui + actions), CodeQL (.github/workflows/codeql.yml + config — JS/TS SAST, security-and-quality, frozen/vendored paths ignored), OpenSSF Scorecard (.github/workflows/scorecard.yml — supply-chain score + badge). Account-based, wired but dormant until the owner signs up: SonarQube Cloud (formerly SonarCloud — sonar-project.properties + .github/workflows/sonar.yml, gated on vars.SONAR_ENABLED so no red X pre-setup; imports the lcov coverage; tech-debt + quality-gate badges) and CodeRabbit (.coderabbit.yaml — AI PR reviews on app install). README gained OpenSSF Scorecard + Sonar quality-gate & tech-debt badges. All CI/build-only (no app/user-data touch) → no version bump, legal pages unaffected. Setup links + steps: deployment.md.

2026-06-30 — README badges → shields.io; Codecov coverage in CI (docs/CI, no bump)

Reworked the README badge strip to a single cohesive shields.io set (owner's call, replacing the shieldcn.dev set): Contributors first, dropped the branches count, and added a real Netlify deploy-status badge (shields' netlify/<site-id>), plus CI (Actions), Coverage (Codecov), Code quality (CodeFactor), a Docs badge → the doc site, version (from engine-v3/package.json), Node ≥24, created-at, code-size, top-language, and the issues/PR counts — all linked to their pages.

Wired Codecov: added an lcov reporter to both Vitest coverage configs (Node + gui) and two non-blocking codecov/codecov-action@v4 upload steps in ci.yml (flags node / gui). Owner action: enable the Codecov GitHub app + add a CODECOV_TOKEN secret; the badge populates after the first main CI run. CodeFactor is zero-config (enable its app). CI/test-config/docs only → no version bump; the app bundle is untouched, and Codecov is CI-only so the legal pages are unaffected. See deployment.md.

2026-06-30 — Align the app description everywhere to the new positioning (2.35.1)

Settled on a single plain-English description of the app and used it consistently everywhere it's described: "An open-source generator for AI image and text prompts that automatically builds richer, more detailed prompts than most people write by hand," then run through 40+ models (Midjourney, DALL·E, Gemini, FLUX, Stable Diffusion, and more). Per the owner, these blurbs lead with what the app does — not "DPL" (unknown jargon) and no ad-speak. Updated: the GitHub repo About; the README intro (DPL demoted to an under-the-hood mention); the app's gui/index.html <title>/meta/OG/Twitter/JSON-LD (the shipping bit → PATCH); engine-v3/package.json description; CLAUDE.md's opening line; and notes/context/project.md's definition. Left session logs and DPL/CLI mechanics docs untouched (history / technical, not the pitch). No data-practice change → legal pages unaffected.

2026-06-30 — Seedable, deterministic engine + async batch boundary (2.35.0)

Replaced the "swap global Math.random" seeding hack with a real, threaded PRNG so a generation is reproducible from a seed. New src/core/rng.js: cyrb128 (string→128-bit state) + sfc32 + an Rng class (float/int/chance/pick/sample/shuffle + fork(label) sub-streams) + createRng (records a random seed when none is given). src/helpers/random.js now draws from an ambient source that defaults to Math.random (so unseeded behavior and the legacy Math.random-swap test path are unchanged) and that the engine swaps for a seeded Rng per generation via withAmbientRng. The DPL ctx.rng seam delegates to it, and the raw Math.random() leak in the renderer's weightedSampleN was fixed to draw from ctx.rng.

Closed the determinism holes: four generators still used lodash _.random/_.sample (which capture Math.random at import and can't be seeded) — subject/entity, prompt/d, style/silhouette, style/vibrant-art — now use helpers/random.

Engine API (src/core/engine.js): generate({seed}) is deterministic when seeded; new generateWithSeed() returns {prompt, seed} (auto-seed when absent) for reproduce-later; generateMany gives each prompt its own fork(i) sub-stream (reproducible batch); new generateManyAsync yields between prompts (the async-capable batch boundary — the per-prompt render stays sync by design so the live preview stays instant). settings.seed already flows through the GUI facade, so a seed UI is a thin follow-up. Tests: new tests/unit/rng.test.js + seeded cases in engine.test.js; snapshots stay byte-identical (default path unchanged). Design note: notes/reference/rng-design.md. Full gate green (lint + smoke + 260 Node + 271 web) and the SPA builds. No data-practice change → legal pages unaffected.

Decided (owner, 2026-06-30): we are not pursuing full-pipeline async — it can't be done safely and stably without hacks (the pure-CPU render path is called during React render to drive the instant live preview; forcing it async would ripple through render code for no real gain). Async lives at the batch boundary (generateManyAsync); the render path stays synchronous by design. Settled, not an open item.

2026-06-30 — In-app dialogs replace native alert/confirm/prompt (2.34.0)

Replaced every blocking browser dialog in the SPA with a proper in-app modal system. New gui/src/lib/dialog.js is a tiny Promise-based external store exposing a singleton dialog with alert / confirm / prompt (resolve contract mirrors the natives: confirm→boolean, prompt→string|null, alert→undefined). Because it's a store (not React context), it's callable identically from components and from non-component hook/lib files. gui/src/components/DialogHost.jsx (mounted once at the app root inside the i18n boundary) renders the active request via a portal, reusing the existing .modal / .modal-overlay / .modal-actions styling — accessible (role="dialog" + aria-modal, Escape/backdrop cancel, Enter accepts, Tab trapped, focus moved in on open and restored on close), with a localized OK/Cancel default, optional custom labels, a destructive (red) accept style, and a text input for prompt. Added .modal-input + .btn-destructive CSS.

Migrated all 16 native call sites across 11 files (App, ApiKeyField, Home, ManageFolderEditor, ManageListEditor, Settings, SingleView, and the useImageBatches / useManageTree hooks); the formerly-sync confirm/prompt callers became await. Delete confirms now use a red Delete button. New messages dialog.ok / dialog.cancel / app.deleteAction (en.json + en-XA regenerated). Tests: new dialog.test.js (store) + DialogHost.test.jsx (interaction), and the three tests that stubbed window.confirm/prompt now mock the dialog service (+ async act). Full headless gate green (lint + smoke + 271 Vitest) and the SPA builds. No data-practice change → legal pages unaffected.

2026-06-30 — README revamp + SEO repositioning (2.33.3)

Rewrote the root README.md to lead with what the project actually is — a creative DPL block engine + web app that's provider-agnostic (~40 image/text backends), not a Stable-Diffusion-first tool (SD is still supported but no longer the headline). New structure: a prominent ▶ Try it now block linking the online edition (prompt.fairyfox.io), the docs (fairyfox.io/random-ai-prompt), and the parent site (fairyfox.io); a "what makes it interesting" capabilities section; a DPL example; build/run, project layout, development, and a links section. No images/videos. Added Node and license badges.

SEO pass on the actual indexable page (gui/index.html): retitled to the new positioning, refreshed the meta/OG/Twitter descriptions (kept high-volume model keywords for discoverability), added a canonical link, og:url, absolute og:image URLs, upgraded the Twitter card to summary_large_image, and added a WebApplication JSON-LD block. Also refreshed the GitHub repo About description and topics (prompt-generator, prompt-engineering, ai-art, generative-ai, text-to-image, stable-diffusion, midjourney, blocks, react, vite, open-source). No data-practice change (same self-hosted assets, no new third-party requests), so the legal pages are unaffected.

2026-06-30 — Image provider "Unset" is just Plain text renamed (2.33.2)

Walked back the separate image "Unset = no provider" state added in 2.32.1. Per the owner: for the image provider, "Unset" is simply Plain text renamed — the plain provider already means "no real image API, just emit the prompt." So the plain provider's label is now "Unset" (config) and the extra provider === "none" image branch (ProvidersMenu / Home / ProviderGear) was reverted. The Text and Upscale rows keep their genuine Unset = none/off (those really can be unselected).

2026-06-30 — Fix: provider gear accordion showed image settings for the Text/Upscale roles (2.33.1)

The provider-settings accordion (2.31.0) rendered every role's section with the chosen provider's only settings schema — which is image-generation for image providers (and image-gen for dual image+upscale providers). So OpenAI in the Text role showed the image "Model" field, and Comfy in the Upscale role showed image-gen knobs. A provider has just one (native) schema, so each section now renders it only for the role it serves: Image → the image schema; Text → a note (a rewrite AI uses a fixed model, no extra settings); Upscale → the schema only for upscale-only enhancers, else a note (a dual provider's image settings are edited in its Image section). No more image-gen knobs under Text/Upscale.

2026-06-30 — Natural-language artists & styles + a styles building block (2.33.0)

  • New setting naturalArtistStyle (default on) frames artists as "by <artist>" and styles as "in the style of <style>" in the generated prompt, so a reader (and the model) can tell an artist from a style from a plain keyword. Off = raw names. Lives in the engine settings (src/settings.js) and the SPA defaults, with a toggle in the prompt-settings gear (Vocabulary).
  • {#artists} is framed ("by …") when the setting is on (data/blocks/prompt/artists.js), via a post-process so the raw artistRepeater (and its unit tests) are untouched.
  • New {#styles} generator (data/blocks/prompt/styles.js + .dpl/.json) emits 1–2 tokens from the existing style list, framed "in the style of …" — the styles list finally has a building block (the companion to {#artists}).
  • Verified end-to-end (both gates): npm run smoke, SPA build, 242 unit + 259 web tests pass (no snapshot churn), and a manual expand confirmed "by Edward Ruscha…" / "in the style of Abstract expressionism" with the toggle on and raw names off.

2026-06-30 — Providers-menu polish + language moved to the links menu (2.32.1)

Follow-up refinements to the 2.31.0 header batch, per owner feedback.

  • Language picker moved to the existing links menu (the far-right ⋯ menu, LinksMenu.jsx, alongside GitHub / docs / legal) — and the separate app-settings cog was removed (AppMenu.jsx deleted). One menu, not two.
  • "Unset" offered on every provider row. Image and Upscaler now offer an Unset option (the Upscaler's "Off" became "Unset"); Text already had it. Unset is an option, not the default — the image default stays Plain text. Choosing Unset for the image generates prompts only, no images (handled in Home.jsx; the gear shows a hint when nothing's selected).
  • Hint text moved into tooltips. The two always-on description paragraphs under the Text and Upscaler pickers became hover tooltips (a hint prop on ProviderPicker), decluttering the panel.

2026-06-30 — Local edition gets a real release stage + full file-watch hot reload (2.32.0)

Fixes the dev-build-as-release defect and makes the running app aware of changes to its own files.

  • The local edition now has a proper release stage. The whole /api/* backend (hosted-generation proxy, local-file image storage + gallery feed, ImageMagick convert/resize, Manage, the file-watch, the settings store) was wired only into Vite's dev server — so a real vite build had no backend, and npm run web (the dev server) was being used as the de-facto release. Wrong and unprofessional. The handler is now factored into gui/server/apiHandler.js and mounted by both the dev-server plugin (gui/vite-plugin-api.js, now thin) and a new standalone release server (gui/server/serve.js) — one backend, two transports, can't drift. New scripts: npm start (build → serve the built app + backend) and npm run serve (serve a prebuilt dist/). Default port 4173; NO_OPEN=1 skips the browser.
  • Latent bug fixed in the extraction: the convert / resize / reveal / open routes called exec/execP that were never imported in the old plugin (a runtime ReferenceError); the shared handler imports them properly.
  • Full file-watch hot reload. /api/manage/watch is now a scoped SSE stream — data (lists/blocks → live catalog refresh), output (the gallery feed), and settings (user-settings). The app opens one stream in App.jsx: data edits hot-apply to Generate + Manage, new/removed/changed images appear in the gallery, and external settings edits are re-read. Manage dropped its own EventSource and rides the shared catalog refresh.
  • Never-corrupt-settings safeguards. On-disk settings writes are now atomic (temp file + rename), so a watch reload can't read a half-written file; the settings reload also ignores events caused by the app's own writes (msSinceLastWrite) and only swaps when the value actually changed — so it never fights or clobbers the app's saves.

2026-06-30 — Header & provider UX batch: app-settings cog, settings accordion, unified derived grid (2.31.0)

A cluster of GUI refinements across the header and the single-image view (feature/gui-ux-batch).

  • New app-settings cog in the header (AppMenu.jsx) — a distinct sliders button next to the provider gear, on every tab. The display-language picker moved here out of the prompt-settings gear (Settings.jsx), where an app-wide preference was mixed in with prompt knobs. The cog is the home for future app-level settings.
  • Provider gear is now an accordion (ProviderGear.jsx) covering all three provider roles — Image (always), Text (when a rewrite AI is set), and Upscale (when an upscaler is set). ProviderBox now takes a providerId so one box drives any role; each section header shows the provider's label + tier and toggles its body.
  • Upscaler options grouped Local / Online, matching the image picker (ProvidersMenu.jsx).
  • Text provider "Off" → "Unset" for clarity (the image/upscale "Off" is unchanged).
  • Plain text is the default image provider and is pinned to the top of the Local group — it needs no machine, key, or network, so it's the one provider that works for everyone out of the box.
  • Unified, colour-coded derived grid below the single-image view (DerivedStrips.jsx): the three separate Re-Roll / Variation / Resize strips became one grid, each thumbnail colour-coded by kind with a hover tag, a tooltip, and a small legend.

2026-06-30 — In-app legal pages (Privacy / Terms / Cookies) + menu links (2.30.1)

Added the app's own legal documents and surfaced them in the header menu.

  • Three self-hosted, app-styled static pages under engine-v3/gui/public/legal/: privacy.html, terms.html, cookies.html (dark-first theme with light fallback, app logo, a "← Back to the app" link, and cross-links between the three). Served at /legal/*.html in dev (Vite public/) and on Netlify (real files beat the non-forced /*index.html SPA fallback).
  • Rewritten to match reality, not generic boilerplate. The TermsFeed-generated drafts described accounts, marketing emails, camera access, login cookies and 24-month server logs — none of which this app has. The rewrites state the truth: no accounts, no analytics/cookies/tracking, settings + BYO API keys stored only on the user's device (rap.store. localStorage / local files), prompts + keys sent directly from the device to the chosen provider (no server relay; providers that can't be called directly from a browser are simply unavailable in the web build), Netlify named as the hosting processor. Covers web + desktop + future mobile. Age standardized at 18+ (desktop can generate NSFW). Cookies Policy honestly says "we use no cookies." Contact: fairy@fairyfox.io.
  • Self-hosted fonts (GDPR improvement). Removed the Google Fonts <link>/preconnect from index.html and the three legal pages; fonts now load from gui/public/fonts/fonts.css + local .woff2 (Maven Pro 400/500/600/700, Space Grotesk 500/600/700, latin, font-display: swap). This ends the IP-to-Google transfer on the live site — the only third-party data flow left is the AI provider you pick + Netlify's hosting logs. The .woff2 files are sourced from the @fontsource/maven-pro / @fontsource/space-grotesk packages (copy step in the session log) and committed as static assets. Privacy/Cookies docs updated to say fonts are served locally.
  • Header menu linksLinksMenu.jsx now renders the three legal pages below a .links-sep separator (new ShieldIcon/FileTextIcon/CookieIcon in icons.jsx); legal items open in a new tab (preserving SPA state) but omit the outbound glyph since they're same-origin. New linksMenu.* i18n messages; .links-group/.links-sep styles added.
  • Removed the retired serverless proxy. Deleted the dead Netlify function files (gui/netlify/functions/generate.js + rewrite.js) and dropped the functions setting + /api/* redirect from netlify.toml (the online build has been browser-direct/static since 2.11.0 and never called them). Trimmed tests/providers/netlifyFunctions.test.js to the dispatch/dispatchRewrite hub tests it shares. Kept server/dispatch.js + the Vite dev middleware (vite-plugin-api.js, /api/generate + /api/rewrite) — that's the local dev proxy the desktop full version uses for non-CORS providers. deployment.md updated. Also corrected the Privacy Policy wording, which had wrongly described a "stateless proxy we run."
  • Standing instruction: keeping these three documents accurate as the app's data practices change is now an owned, by-default responsibility (recorded in CLAUDE.md).

2026-06-30 — GUI polish pass: header links menu, JS-highlighted code editor, all-tab header, UX fixes (2.30.0)

A batch of GUI quality-of-life improvements (branch feature/gui-polish):

  • Header links menu — a new overflow-menu button (LinksMenu.jsx, MenuIcon/GitHubIcon/ BookIcon/HomeIcon/ExternalLinkIcon added to icons.jsx) in the top bar opens GitHub, the docs site (fairyfox.io/random-ai-prompt/), and the fairyfox home — on every tab.
  • Header controls on all tabs — the Providers dropdown + provider gear are no longer gated to the Generate view; they (and the NSFW switch + the new menu) show on every tab (App.jsx).
  • JS code editorCodeEditor.jsx now applies real syntax highlighting (a HighlightStyle reusing the DPL palette via .cm-tok-* classes), bracket matching, auto-close brackets, indent-on-input, and Tab-to-indent — but only when a language is supplied (raw list editing keeps plain-text behaviour). @codemirror/lang-javascript was already wired in ManageBlockEditor; this makes its tokens actually colour. No new dependencies.
  • Validity icon flipped to the upper-LEFT of the prompt box (Home.jsx composer-corner-left); the editor's first line is indented so it never sits on typed text.
  • Manage entries are fully clickable — the whole pill opens the editor (click or Enter/Space); the separate Edit icon is gone, Delete stays (stops propagation). EditIcon import dropped.
  • Description UI redesign — the block/list picker hint became a left-aligned info callout with an accent edge + ⓘ icon (was centred text with an awkward underline), and now also surfaces the selected folder's own description, not just the group hint. (Audited the data: every list and generator already carries a description — the gap was the UI hiding folder descriptions.)
  • Smaller fixes — breathing room below the DPL insert bar; lowercased the Blocks/Lists all sub-tab; per-tab descriptive tooltips in the header; brand tooltip.

README now features the live app (prompt.fairyfox.io) + docs link; the GitHub repo About + homepage were set to the app URL. Verify: lint 0 errors, smoke, gui build green, Node + web Vitest 265 web tests pass (full npm test green). Visual e2e baselines will need refreshing for the deliberate UI changes (npm run test:e2e:update). MINOR 2.29.1 → 2.30.0.

2026-06-30 — Doc fix: correct the user-settings folder path (2.29.1)

Notes-only follow-up to 2.29.0: the design note showed the GUI's user-settings folder at engine-v3/user-settings/, but the code writes it under the dev-server root at engine-v3/gui/user-settings/. Corrected the path in plans/storage-and-settings.md and clarified that the folder is created lazily on first write, is gitignored, and is distinct from the Node engine's user-settings.json. No code change. PATCH 2.29.0 → 2.29.1 (cut so the fix ships to main as a tagged release rather than an untagged commit).

2026-06-30 — Unified, versioned, cookie-free storage: one user-settings folder + per-provider files (2.29.0)

The storage/settings overhaul wired end-to-end (design in plans/storage-and-settings.md). All GUI user data now flows through the storage layer (gui/storage/): local mode persists to one user-settings/ folder on disk (one JSON file per namespace; providers/<id>.json per provider) and the online build uses localStorage only — the sole place browser storage is used; zero cookies in any mode. The dev-server /api/storage endpoint became a per-namespace folder writer (nsToFile/readNs/writeNs/removeNs/listNs, traversal-safe) with a one-time migrateLegacyStore that folds the old flat .gui-storage.json in. Each static provider now ships its defaults as a literal <id>.json sidecar (imported by settings.js); per-provider user overrides live in user-settings/providers/<id>.json and cascade over those defaults (midjourney keeps deriving defaults from its catalog). The app's stores (settings, customStore, wrapperStore, provider params) were rerouted off direct localStorage onto a synchronous boot-time hydration cache (gui/storage/cache.js) that migrates legacy keys forward — so component code is unchanged except a one-line hydrate gate in App.jsx. Verify: lint 0 errors, smoke, Node unit 242, web 265, gui build green. MINOR 2.28.18 → 2.29.0. (The in-app storage-management UI is deferred — a first-cut panel was built then reverted at the owner's request; surfacing it, especially online, is the owner's design call.)

2026-06-30 — Versioned config + deep-merge foundation for the storage overhaul (no version move)

First step of the unified storage/settings rework (design in plans/storage-and-settings.md). Adds the pure, isomorphic core the rest builds on, with no app wiring yet so the running app is unchanged: gui/storage/merge.js (deepMerge — recursive objects, arrays replace by default / concat opt-in, null clears, undefined keeps base, never mutates; diff — the sparse inverse; equal) and gui/storage/config.js (versioned documents { __v, …data } over the existing StorageBackend: loadConfig/saveConfig, forward migrations with a one-time self-healing re-save, and the defaults→override loadCascade/saveCascade that stores only the diff). Tested by gui/tests/storage/merge.test.js (13) + config.test.js (9, backend mocked). Web suite 237 → 259 green; eslint clean. Internal scaffolding only — VERSION bumps when the layer is wired to real settings.

2026-06-29 — Memoize the data loaders' static catalog: ~280× faster prompt generation (2.28.18)

Profiling pass over the whole project. The dominant cost was that the data loaders re-did all of their catalog work on every generated prompt: nodeLoader.readListLines rebuilt the entire list-name set (≈4 full recursive readdirSync walks of data/lists per distinct list), and the reserved keyword wildcard re-unioned the entire vocabulary (~48 ms) once per prompt — the list store clears its per-prompt cache on reset(), so it recurred every time. The on-disk (Node) and build-time bundled (browser) catalog is static for the life of a process/page, so all of it is now memoized: src/core/nodeLoader.js caches the directory walks, the sorted catalogs, the full name set, and the keyed reads (readListLines by name|includeAdult, the meta/group sidecars), with a new nodeLoader.refresh() to drop the caches; src/core/browserLoader.js memoizes its name set + resolved-line sets (kills the per-prompt wildcard re-union in the SPA at runtime); and src/core/stages/block.js hoists resolvePool out of the per-token loop. A single generate() went from ~56 ms → ~0.2 ms (≈280×), and one prompt's filesystem pressure from 208 readdirSync + 222 readFileSync → ~0 once warm. No behavior change — safe because the loaders only read a static catalog (the Manage tab's live edits go through a separate runtimeLoader). Verify: lint 0 errors, smoke, Node unit 235, web 237, gui build (785 KB gzip, within budget) — all green. Full report: reference/performance.md. PATCH 2.28.17 → 2.28.18.

2026-06-29 — Split the dev-server API helpers out of vite-plugin-api.js, with tests (2.28.17)

Final Phase 6 file. The 673-line gui/vite-plugin-api.js dev middleware had no test coverage; its helpers move to gui/vite-api-helpers.js (the output-dir constants, ImageMagick detection, the path-traversal-safe resolveOutputFile, the JSON request/response helpers, and the BYOK key store), leaving the plugin file (~570 lines) as just the route wiring. The previously-untested helpers now have a test nettests/unit/viteApiHelpers.test.js (8 tests, in the Node suite since it's a Node module) covering the resolveOutputFile security boundary (rejects .., separators, and URL-encoded traversal) and the send / readJson helpers. Node unit 227 → 235. No behavior change.

2026-06-29 — Extract ManageListEditor's pure ops (tested) + messages (2.28.16)

Phase 6 of the refactor. Two extractions from the 522-line ManageListEditor.jsx: the gnarly, previously-untested pure logic — parsing an AI "expand" reply into clean entries, merging in only the new ones, de-dupe, and sort — moves to lib/manage/listEditorOps.js with a test net (gui/tests/lib/listEditorOps.test.js, 8 tests covering list-prefix stripping, comma fallback, case-insensitive merge/dedupe, and non-mutating sort); and the ~75-line defineMessages block moves to lib/manage/listEditorMessages.js. The component drops to ~460 lines and uses the imported ops. Web suite 214 → 222. No behavior change — lint + web + e2e + build green.

2026-06-29 — Lift the wrapper-preset control into a tested useWrapperPresets hook (2.28.15)

Phase 6 of the refactor. The wrapper FAB's whole behavior — the popover/modal view state + anchor, the saved-preset library (localStorage via wrapperStore), the Start/End editor draft, and every action (apply, load, save / rename / delete, per-pane revert) — moves out of WrapperFab.jsx into lib/wrapper/useWrapperPresets.js. The component drops from 428 to ~340 lines as pure rendering over what the hook returns. This previously-untested logic now has a test netgui/tests/lib/useWrapperPresets.test.jsx (9 tests) mocks wrapperStore and asserts apply (None / Default / named), save (new preset vs editing the Default in place), delete (incl. the Default no-op), per-pane revert, and load-into-editor. Web suite 205 → 214. No behavior change.

2026-06-29 — Extract the DPL insert-toolbar messages from dplInserts.js (2.28.14)

Phase 6 of the refactor. The ~135-line defineMessages block (category + per-construct labels and descriptions) moves out of gui/src/lib/dpl/dplInserts.js into gui/src/lib/dpl/dplInsertsMessages.js, leaving the catalog builder (getDplInserts) and its literal syntax/template/example DPL in a ~280-line file that imports { m }. Public surface unchanged (getDplInserts default + named). No behavior change — the existing dplInserts unit suite (5) + the web suite + build all green.

2026-06-29 — Split the DPL CodeMirror autocomplete out of dplLanguage.js (2.28.13)

Phase 6 of the refactor. The 501-line gui/src/lib/dpl/dplLanguage.js is cut along its natural seam: the context-aware autocomplete half — the completion source, its option renderers (kind badge, section header, the live-example info tooltip), and the front-matter / section-name context detectors — moves into gui/src/lib/dpl/dplComplete.js (~260 lines). dplLanguage.js keeps the StreamLanguage tokenizer, the highlight style, and the section-heading decorations (~285 lines) and re-exports dplKindBadge / inFrontMatter / dplCompletionSource, so the editor's imports are unchanged. The two halves don't cross-reference, so it's a pure relocation. No behavior change — lint + the SPA web suite (205) + e2e (8) + build green; the editor still lives in the lazy chunks.

2026-06-29 — Lighthouse polish: SEO + A11y to 100, Perf to 97 (2.28.12)

Pushed the Lighthouse scores up across the board (Workstream B/C). SEO 92 → 100: added a real gui/public/robots.txt (the SPA fallback was serving HTML for /robots.txt, failing the audit). Accessibility 96 → 100: lightened the --muted/--faint text tokens (#9a9aa4#aaaab2, #74747e#97979f) so they clear WCAG AA 4.5:1 on the darkest panels and the green active-tab background (the two flagged contrast failures: .cat-name and .dpl-insert-lead). Performance 95 → 97: the render-blocking Google Fonts stylesheet now loads non-blocking via the media="print"/onload swap (with a <noscript> fallback), removing ~284 ms of render block off first paint. Verified by a real Lighthouse run — Performance 97, Accessibility 100, Best-Practices 100, SEO 100; visual baselines refreshed for the slightly lighter muted text.

2026-06-29 — SEO meta tags + Lighthouse SEO gate (page-rank greenlight) (2.28.11)

Workstream C of the refactor. The SPA's index.html gains the SEO essentials it was missing: a descriptive <title>, a <meta name="description">, robots: index, follow, an author tag, and a full Open Graph + Twitter-card set (type/site_name/title/description/image) for social previews. A self-referencing canonical/og:url is intentionally omitted (the production URL isn't recorded in-repo, and a wrong canonical is worse than none). lighthouserc.json now also gates categories:seo ≥ 0.9 alongside performance/accessibility, so the page-rank greenlight can't silently regress. Verified with a real Lighthouse run: Performance 95, Accessibility 96, Best-Practices 100, SEO 92 — all green.

2026-06-29 — Split the content-safety lexicons out of contentSafety.js (2.28.10)

Phase 6 of the refactor. The ~270 lines of term lists (slurs, minor-sexual, extreme, the whitelist, and the NSFW lexicon) move verbatim into src/safetyLexicons.js as pure data; contentSafety.js keeps the matcher logic (normalize, classifyRemoval, isNsfw, the built matchers, _sets) and imports the lexicons, dropping from 421 to ~150 lines. Public surface unchanged — every importer (the CSV build scripts, the list-cleanup tooling, and the unit suite) keeps importing from contentSafety.js. No behavior change — the contentSafety unit suite (13) + full unit (227) + build green.

2026-06-29 — Split listManifest.js into focused modules behind a re-export barrel (2.28.9)

Phase 6 of the refactor. The 504-line src/listManifest.js splits — verbatim — into three focused, browser-safe modules: listTags.js (the per-list metadata table), nameOrder.js (variant suffixes, the reserved keyword wildcard, the natural-order comparator, physical→logical names, suffix-path resolveName, and computeButtonNames), and listResolve.js (resolveListLines + autoGroupListDirs with the SFW/NSFW + composite-group model). listManifest.js becomes a thin re-export barrel, so every importer (both engine loaders, the runtime loader, the manage tree, the suggestion builder, and the test suites) keeps importing from the same path unchanged — zero import-graph churn. No behavior change — smoke + the listManifest unit suites (27) + full unit (227) + web (205) + build all green.

2026-06-29 — Lift the Manage tree-CRUD into a tested useManageTree hook (2.28.8)

Finishes the Manage decomposition the right way — floor up, tests under it. The tree state (on-disk tree + stable-branch manifest, expand/select/search/drag state), the catalog/SSE refresh effects, the built+filtered models, and every file operation (new file/folder, move, delete, restore-ghost) move out of Manage.jsx into lib/manage/useManageTree.js. The component drops from ~545 to ~350 lines and becomes pure rendering over what the hook returns. Crucially this lands with a new test netgui/tests/lib/useManageTree.test.jsx (9 tests) mocks the manage backend + catalog and asserts the load→models build and each file op (mkfile ext/boilerplate, delete confirmed vs cancelled, move, restore-ghost, toggle) — so the previously-untested CRUD path is now covered. Web suite 196 → 205. No behavior change; Manage stays its own lazy chunk.

2026-06-29 — Decompose Manage.jsx: extract icons + ManageDetail pane (2.28.7)

Phase 5 of the refactor. The Manage tab's standalone pieces move out of the 727-line file: the six tree icons (caret/gear/edit/refresh/restore/trash) → components/manage/icons.jsx, and the right-pane detail/preview component (plus its previewText cap and its own colocated messages) → components/manage/ManageDetail.jsx. Manage.jsx drops to ~545 lines. The tree-CRUD flow stays in place for now — it's tightly coupled to inner render components and isn't covered by the e2e suite (Manage is gated behind the local backend), so lifting it into a hook is deferred until Manage gets component tests, rather than risk an untested regression. No behavior change — web suite + e2e green; Manage stays its own lazy chunk (CodeMirror still loads only when the tab opens).

2026-06-29 — Decompose SingleView.jsx: 915 → ~440 lines across single/* modules (2.28.6)

Phase 4 of the refactor. The single-image view's sub-pieces are promoted out of the one 915-line file into focused modules: the shared react-intl strings + derive-layer label map → components/single/ messages.js; the pure exporters → lib/single/markdown.js (toMarkdown) and lib/single/json.js (syntaxHighlightJson); and each presentational piece to its own file under components/single/PromptCard (with its TextRow), DetailTable (with DetailRow), CopyButton, LineageHead, DerivedStrips, and KeywordsCard. SingleView.jsx keeps the orchestration + the pick/REST_DROP/ FRAC helpers and imports the rest, dropping to ~440 lines. No behavior change — web component suite + e2e + visual/a11y green; SingleView stays its own lazy chunk (7.2 KB gzip).

2026-06-29 — Home.jsx: lift the image-batch flow into a useImageBatches hook (2.28.5)

Finishes the Home decomposition. The ~220-line image-generation flow — makeBatch (the prose + keyword rewrite passes, the per-image sidecar, the busy/placeholder bookkeeping) and the remove/clear image/batch/all handlers — moves verbatim into lib/home/useImageBatches.js, which now owns the generated-prompt list (prompts), the running id counter, and the in-flight image error, and reports them back. Home.jsx drops from 861 to ~625 lines and is now a thin coordinator over the palette, the composer, and the hook (no more giant generation routine inline). The confirm/error messages move with the logic. No behavior change — web component suite + Home e2e + visual/a11y green.

2026-06-29 — Seedable engine RNG: drop lodash randomness, make the pipeline reproducible (2.28.4)

Workstream A of the refactor (notes/plans/refactor-2026-06.md) — the deep fix behind the earlier test band-aid. The engine drew randomness from lodash (_.random/_.sample/_.shuffle), and lodash captures Math.random at import, so the test seam (withSeed, which swaps Math.random) could never control it — only the DPL renderer was seedable. Every emphasis/list/suggestion test had to either disable emphasis or use single-entry lists to dodge the un-seedable draws. Now a new src/helpers/random.js (randomFloat/randomInt/sample/shuffle, all reading the live Math.random) replaces lodash across the keyword randomizers (randomEmphasis, randomAlternating, randomEditing, keywordRepeater), the list/block stages, listStore, prompt-salt, and the suggestion builder — so the whole pipeline (emphasis included) is reproducible under a seed. The engine's exact-output unit tests drop the emphasis-off workaround and assert real seeded output, plus a new end-to-end determinism test (same seed → byte-identical). A dead lodash import in cleanup.js is removed; src/ no longer uses lodash for randomness. No behavior change for users (production still runs on real Math.random); snapshots are unchanged.

2026-06-29 — Decompose Home.jsx: extract palette, icons, and pure helpers (2.28.3)

Phase 3 of the refactor (notes/plans/refactor-2026-06.md). The 1161-line Home.jsx is reduced to ~860 by pulling out self-contained pieces: the six inline toolbar SVGs → a shared components/icons.jsx; the image-sidecar snapshot helper → lib/home/snapshot.js; the building-block category split (foldersOf/splitCats) → lib/home/blockCategories.js; and the entire left-pane building-block palette (its search box, Blocks/Lists tabs, folder sub-tabs, and chip cloud, plus the search/active-tab state and catalog derivation it alone uses) → a new components/home/BlockPalette.jsx that reports up only the cross-pane actions (insert a token, hover tooltip). No behavior or visual change — verified by the web component suite, the Home e2e (generate + filter), and the visual/a11y snapshots. A follow-up can lift the image-batch/rewrite flow (makeBatch et al.) into a hook.

2026-06-29 — Modularize the DPL compiler: dpl.js 882 → ~120 lines (2.28.2)

Phase 2 of the refactor (notes/plans/refactor-2026-06.md). The single 882-line src/core/dpl/dpl.js is split — by moving code verbatim — into focused sibling modules under src/core/dpl/: words.js (the two 100-step intensity/focus word scales + intensityWord/focusWord), intensity.js (clamp / count-scale / relative-modifier / condition math), rng.js (the default Math.random RNG seam), parser.js (front-matter, lexing, sectioning, the indentation tree, per-line node parsing), and renderer.js (the gate/choice/repeat/ref/weight-sort render). dpl.js keeps only compileDpl (the orchestrator) and re-exports intensityWord so its public surface is unchanged. No behavior change — verified by the smoke test (every block compiles), the DPL unit + snapshot suites (identical output), and the browser build (the import.meta.glob loader compiles through the new modules).

2026-06-29 — Build code-splitting: lazy views + focused vendor/data chunks (2.28.1)

First step of the codebase refactor (plan: notes/plans/refactor-2026-06.md). The SPA used to ship as one 2.4 MB JS bundle. Now the three local-only views — Manage (which pulls in all of CodeMirror), Gallery, and SingleView — are React.lazy + <Suspense> chunks that the browser fetches only when the view is first opened (then they stay mounted, so per-view state is preserved as before). The Vite/Rolldown build also splits React, react-intl, lodash, and the eagerly-globbed prompt-data into their own cacheable chunks (advancedChunks). Net effect: the initial Generate-view JS drops from ~793 KB to ~178 KB gzip (app/engine) with CodeMirror (≈55 KB gzip) no longer on the first paint; total shipped JS is 784 KB gzip, within the 900 KB budget. No behavior, UX, or prompt-output change.

2026-06-29 — Hugging Face + Meta Llama text providers; 18 total (2.28.0)

Two more Text AI options: Hugging Face Inference (its OpenAI-compatible router with an HF token) and the Meta Llama API. The Text dropdown now lists eighteen providers — the full set of simple-key, no-fancy-auth LLM APIs from the list.

2026-06-29 — Seven more Text providers; 16 total (2.27.0)

The Text AI dropdown reaches sixteen: added Fireworks, Cerebras, Qwen (DashScope), Moonshot / Kimi, and AI21 (OpenAI-compatible, via the shared adapter) plus Anthropic Claude and Cohere (their own API shapes, via small bespoke adapters). All take a simple API key and run through the rewrite proxy — no fancy auth. They drive the prompt auto-fix / keyword-rewrite buttons.

2026-06-29 — Six more Text (prompt-rewrite) providers (2.26.0)

The Text AI dropdown grows from 3 to 9: added OpenRouter and Groq (fast, CORS-enabled, so they work online too) plus DeepSeek, Mistral, Together, and Perplexity (local-only, like the other non-CORS providers). They power the prompt auto-fix / keyword-rewrite buttons. Since most LLM APIs speak the OpenAI chat format, a new provider is now just a base URL + a model — a shared adapter does the rest, so the long tail (Fireworks, Cerebras, Qwen, Kimi, local Ollama/LM Studio, …) is easy to add next.

2026-06-29 — Five async-job enhancers: WaveSpeed, Claid, Deep-Image, neural.love, VanceAI (2.25.0)

Five more upscale enhancers, the async/submit-poll kind: WaveSpeed (Real-ESRGAN), Claid / Let's Enhance, Deep-Image.ai, neural.love, and VanceAI. That's ten hosted enhancers and seventeen AI upscalers total. (Pixelbin/Upscale.media was skipped — it's a CDN transform-URL model that doesn't fit a "send image, get image back" API.) These five are best-effort and can't be live-tested, so a couple may need a small tweak against the provider's current API.

2026-06-29 — Two more enhancers: Clipdrop + Venice (2.24.0)

Two more upscale enhancers — Clipdrop (Upscale) and Venice AI (Upscale) — bringing the enhancer count to five (with DeepAI, Picsart, Segmind) and twelve AI upscalers total. Clipdrop needs explicit output dimensions, so the source size is decoded to request a 4× target. Best-effort BYOK.

2026-06-29 — ComfyUI AI upscale (the local SD upscaler set is complete) (2.23.0)

ComfyUI can now AI-upscale a saved image: it uploads the image, runs an upscale-model graph (LoadImage → UpscaleModelLoader → ImageUpscaleWithModel → SaveImage), and brings the result back. You just need an upscale model in ComfyUI/models/upscale_models (the model name auto-detects if left blank). With Forge and SD.Next, that's the full set of local, no-cost upscalers — ten AI upscalers in all now (4 in-repo, 3 hosted enhancers, 3 local). Best-effort; verify against your ComfyUI.

2026-06-29 — Modernized the local SD (A1111) adapter + Forge/SD.Next upscalers (2.22.0)

Fixed the stale local Stable Diffusion WebUI adapter: it now sends sampler_name (the old sampler_index was silently ignored by current A1111/Forge/SD.Next, so your sampler/seed didn't apply), plus a scheduler field and batch_size. On top of that, Forge and SD.Next can now AI-upscale a saved image via their Extras tab (R-ESRGAN 4x+), no key needed — a real, local, no-cost upscaler. Best-effort; verify against your live WebUI. (ComfyUI upscale still to come — it needs a node graph.)

2026-06-29 — More enhancers (Picsart, Segmind) + the Upscaler row shows locked online (2.21.0)

Two more upscale enhancers — Picsart (Upscale) and Segmind (ESRGAN) — bringing the enhancer count to three (with DeepAI). And per feedback, the Upscaler / Enhancer row now stays visible but locked in the online build (with a tooltip pointing to the desktop app) instead of being hidden, so visitors can see the feature exists.

2026-06-29 — Upscale-only providers + an Upscaler / Enhancer row; first one: DeepAI (2.20.0)

A new provider category — upscale-only enhancers (no image generation) — plus a third row in the Providers menu: Upscaler / Enhancer, where you pick the service to enhance a saved image and add its key (below Image and Text). It's a local-only feature (the single-image view), so the row is hidden in the online build. Upscale-only providers no longer clutter the image-generation picker. First enhancer: DeepAI (Super Resolution). The ~15 other services (Topaz, Magnific, Claid, Picsart, …) slot in the same way next.

2026-06-29 — Provider expansion, phase 2c: Leonardo + Replicate upscale (the in-repo set is complete) (2.19.0)

Two more AI upscalers, completing the four providers already in the app: Leonardo AI (Universal Upscaler, up to 2×) and Replicate (Real-ESRGAN, ~4×). With Stability and fal, that's the full in-repo set — pick any of them from a saved image's resize menu (with that provider's key). Replicate runs through a new server-side upscale proxy (it can't be called straight from the browser). Local Stable-Diffusion upscalers and the brand-new hosted services are next.

2026-06-29 — Provider expansion, phase 2b: fal.ai upscale (2.18.2)

A second AI upscaler: fal.ai (Real-ESRGAN, ~4×). Pick AI Upscale · fal.ai from a saved image's resize menu with your fal key. Two upscalers now (Stability + fal); Leonardo and Replicate are next (each needs a heavier flow — see notes/plans/provider-expansion.md).

2026-06-29 — Provider expansion, phase 2a: Stability AI upscale (2.18.1)

The first real AI Upscale provider is live: Stability AI's fast ~4× upscaler. Pick AI Upscale · Stability AI from a saved image's resize menu (with your Stability key set) and the upscaled image lands in its Resizes strip, tracked like a re-roll. fal, Replicate, and Leonardo upscalers come next.

2026-06-29 — Deploy fixes: GitHub Pages re-enabled + Netlify continuous deploy (CI/infra — no version change)

(CI/infra only — no version change.)

The docs site and the web app weren't auto-updating after a release. Fixed both: the JSDoc doc-build (npm run docs) now works across the engine-v3 split (code under engine-v3/, docs/notes/config at the repo root) and tolerates JSDoc's non-fatal type warnings, so the GitHub Pages deploy on push to main is re-enabled. A new workflow gives the Netlify app continuous deploy on push to main (it was a manual deploy before) — it activates once a NETLIFY_AUTH_TOKEN secret is added.

2026-06-29 — Provider expansion, phase 1: AI-upscale framework + NSFW soft-lock (2.18.0)

Groundwork for supporting many more image providers. The AI Upscale option in the single view's resize menu is now real: any provider that ships an upscale adapter shows up there (keyed providers prompt for a key), and running it saves the upscaled result into your Resizes strip — same live, stay-on- the-page flow as a re-roll. No provider implements it yet; the per-provider integrations come next (see notes/plans/provider-expansion.md for the full capability assessment and roadmap).

Also new: NSFW content-policy soft-locks. Providers built for safe-for-work content (OpenAI, Gemini, Ideogram, Stability) get a small lock icon and a neutral tooltip in the Providers menu while NSFW mode is on, and a one-tap "proceed?" confirmation before they're used. It never blocks anything and never tells you what you can or can't do — just a heads-up that NSFW mode is on.

2026-06-29 — Single view, round 2: inline actions, live strips, resize, View Raw, uniform locks (2.17.0)

The re-roll / variation actions moved inline onto the prompt itself: the DPL source line has re-roll and vary links; Sent to model and Translated each have a vary link (all ask to confirm). Making another no longer jumps you to a new page — a live placeholder appears in a strip below the image and fills in when it's ready. There are now up to three strips under the image — Re-Rolls, Variations, and Resizes — each a clickable row of thumbnails.

Upsize / downsize is here: resize an image (¼× ½× 2× 4×) via ImageMagick into a new tracked image. An AI Upscale option sits alongside it, lit up only for providers that offer it (none yet — coming).

Tool menus that need ImageMagick (Convert, Resize) are no longer hidden when it's missing — they show greyed with a lock and a tooltip, so you can see the feature exists. And the details panel gains a View Raw toggle (table ↔ syntax-highlighted JSON) plus Copy as Markdown and Copy as JSON.

2026-06-29 — Single view: re-roll / variation, tracked ancestry, Markdown export, table fix (2.16.0)

The single-image view regains its v1-2 powers, adapted to v3's provider abstraction. A "Make another" cluster re-rolls or makes a variation of an image: Re-roll re-resolves the DPL recipe; Variation can draw from the DPL recipe, the AI translation, or the original engine roll. Each makes a brand-new image with a fresh seed, so the cluster is locked (with a tooltip) for providers that don't support seeds (e.g. OpenAI). The view shows a loading placeholder while it generates, then lands on the new image.

Ancestry is tracked again, the lightweight v1-2 way: every derived image keeps its parent's id, and the gallery feed scan rebuilds the reverse child list (self-healing if a parent is deleted). A new Lineage card shows whether an image is a Base / Re-roll / Variation, links up to its parent, and shows a strip of its derived children — all clickable.

Also on the details panel: a Copy as Markdown button (prompt + negative + details as a tidy block), and a fixed table — sampler/steps/cfg/seed rows now appear only for providers that actually support them, so an OpenAI image no longer shows another provider's settings. New image sidecars also store a provider-scoped settings snapshot, fixing the leak at the source.

2026-06-29 — Test coverage, phase 5: cross-browser, performance & CI (no version change)

(Test/CI-only — no version change.)

Closed out the coverage build-out with the cross-cutting layers. Cross-browser: Playwright gains Firefox + WebKit + Pixel-7 mobile projects behind PLAYWRIGHT_ALL_BROWSERS (the new test:e2e:all wrapper, cross-platform via scripts/run-e2e-all.mjs); visual-regression stays Chromium-only. Performance: a gzipped-JS bundle-size budget (scripts/check-bundle-size.mjs, npm run test:perf — 763 KB vs a 900 KB budget) as a hard gate, plus Lighthouse CI (lighthouserc.json, npm run test:lhci) as an informational report. Coverage gates are now enforced in CI: the check and gui jobs run test:coverage so the Node thresholds (≈engine 90%) and the SPA src/lib/** floor actually fail a regression. CI adds cross-browser and perf jobs. Living testing notes updated (notes/plans/testing.md). Total: 419 headless tests pass (Node 226 + SPA 193).

2026-06-29 — Test coverage, phase 4: component + interaction tests (no version change)

(Test-only — no version change.)

React Testing Library + user-event component tests for the highest-value interactive components (render, the key interaction, and disabled/locked/empty/error states): NsfwToggle (confirm-on-enable gate, immediate disable, Escape, online-locked → full-version), PromptResult (generate button, click-to-copy, busy skeleton, per-image remove, clear), DplStatus (✓/✕/warn from the real validator), DplInsertBar (category popovers, snippet insertion at the cursor, Escape), ProviderPicker (grouped dropdown, selection, key badge, locked option), ApiKeyField (renders only for BYOK providers, session entry, explicit save/confirm). 193 SPA tests pass (was 165). Heavier components (the CodeMirror editors, Home, the Manage tree editors) are exercised by the Playwright e2e flows rather than unit-mounted, and remain a documented follow-up in notes/plans/testing-coverage-plan.md.

2026-06-29 — Test coverage, phase 3: provider adapters + transport (no version change)

(Test-only — no version change.)

MSW-backed contract tests for the image/text provider layer that was almost entirely untested (only local-webui + midjourney before). Covers the shared transports (hostedProxy.callProxy, localDirect.postJson/getJson/normalizeBase incl. the object-error + node_errors readable-error path, the generic submitPoll done/failed/timeout), the hosted server adapters asserting real request shape + response mapping + errors (OpenAI images, Replicate Prefer:wait, Stability multipart, fal Key auth, Gemini inline base64), the rewrite adapters (OpenAI/Grok chat, Gemini generateContent), the browser-facing generate wrappers (OpenAI/Gemini browser-direct, Replicate via proxy, ComfyUI local submit→poll over /api/forward), and the proxy hub (dispatch / dispatchRewrite + the Netlify generate/rewrite handlers: 405/400/200/502 paths). Also prettier-formatted four phase-1 test files. 165 SPA tests pass (was 128).

2026-06-29 — Test coverage, phase 2: SPA lib + MSW (no version change)

(Test-only — no version change.)

Stood up MSW (Mock Service Worker) for the SPA suite (gui/tests/msw/, wired into tests/setup.js with onUnhandledRequest: "bypass" so existing fetch-stub tests are unaffected) and added unit coverage for the previously-untested gui/src/lib modules: keywords (weighting/attention/BREAK-AND stripping, accent-folded dedupe, caps), manageTree (categories, implied groups, force-prefix, NSFW hiding, ghosts, filter), output (MSW-backed ingest/file-actions/meta + the data→blob new-tab opener), rewrite (browser-direct vs proxy fallback + error, registry mocked), online, sessionKeys, providerMeta, wrapperStore, the DPL insert catalog (dplInserts), the dialects map, and the useProvider hooks. 128 SPA tests pass (was 60).

2026-06-29 — Test coverage, phase 1: engine core (no version change)

(Test-only — no VERSION/package.json change, per the versioning rule that test/CI/notes commits don't move the number.)

First phase of the comprehensive test-coverage build-out (plan: notes/plans/testing-coverage-plan.md, owner-approved). Added direct unit coverage for the engine modules that were previously exercised only indirectly: the random* keyword helpers (randomEmphasis across SD/NAI/MDJ/Plain, randomEditing, randomAlternating), the loader-injected stages (listStore, list, block), blockManifest, promptFilesAndSuggestions, engine API edges, aliases, and a settings shape guard; plus extra listManifest cases (auto-prefix collision, keyword wildcard, group cycle/safety) and a real-data nodeLoader integration test. Each module is driven across valid / invalid / boundary inputs; the lodash-RNG landmine is handled with chance extremes (0/1) and min==max settings. 226 Node tests pass (was 138). Fixed the stale vitest.config.js coverage include (it pointed at a non-existent src/diffSettings.js) and added an enforced CI coverage gate (statements 88 / branches 76 / functions 88 / lines 90; engine now ~93% lines).

2026-06-28 — 2.15.0: SPA internationalization (react-intl + FormatJS pipeline)

(MINOR — a notable feature set. VERSION + package.json → 2.15.0.)

The whole React SPA (gui/) is now internationalized with react-intl. Every user-facing string across all ~28 components — visible text, title/placeholder/aria-label attributes, confirm/ prompt dialogs, and Intl-correct plurals/numbers — is wrapped in a defineMessages / FormattedMessage / intl.formatMessage call, ~480 messages in total (incl. the DPL lint + insert-catalog modules). English output is byte-identical to before, so the Playwright text/visual baselines are unaffected.

New i18n module gui/src/i18n/: config.js (locale registry + resolveLocale browser-preference resolution), loadMessages.js (Vite import.meta.glob over compiled catalogs), and I18nProvider.jsx (wraps the app in react-intl's IntlProvider, syncs <html lang/dir>, quietly falls back to English on a missing key). App.jsx was split into a thin root (owns settings + the i18n boundary) and an AppShell. A Display language selector was added to Settings; the choice persists in settings.locale ("auto" follows the browser).

Full FormatJS tooling pipeline. babel-plugin-formatjs (wired into @vitejs/plugin-react) auto-fills message IDs to match the extractor; @formatjs/cli powers npm run i18n:extract (→ src/i18n/messages/en.json) and npm run i18n:pseudo (→ a compiled en-XA pseudo-locale — accented/expanded English that makes any un-internationalized string obvious). A focused gui-scoped ESLint config (gui/eslint.config.js, npm run lint:i18n) runs eslint-plugin-formatjs's enforce-default-message.

Scope: only the shipped real locale is English — the app's DPL/prompt domain jargon would make machine-translated languages low-quality, so none are shipped (the pipeline makes adding one a one-file job). Coverage is complete, including the two DPL-technical lib modules: validateDpl.js (editor lint diagnostics) now takes an optional intl and uses a built-in react-intl createIntl English fallback so its message-asserting tests stay green, and dplInserts.js (the DPL syntax teaching catalog) became a getDplInserts(intl) builder — the DplEditor linter and DplInsertBar thread their intl through. ~480 messages total. Tests: a tests/testUtils.jsx IntlProvider render wrapper keeps the component suite green (60 web tests pass; the 9 validateDpl tests pass unchanged on the English fallback).

2026-06-28 — 2.13.0: DPL focus dial + global layer auto-merge; dial keywords move to $ sigil

(MINOR — a notable language feature set. VERSION + package.json → 2.13.0.)

Three related additions to the DPL engine (src/core/dpl/dpl.js, src/core/stages/block.js, both loaders):

  • Focus dial — a sibling of intensity. A second per-reference dial (1–100, default 50), carried as {#name f80%}, with [f<NN%] line conditions and the $focus / $focus-word keyword. Focus is "how pure / how narrow" the render is: low focus admits fluff / extra / unrelated detail, high focus keeps only what is strictly essential (which also makes a generator stack cleanly as a layer). Unlike intensity it does not auto-scale gates/counts — it is author-judged per line (an AI/human decides what is fluff at what focus), which is exactly the lever the deleted "fluff" keywords needed. Threaded to generators as a 5th argument (ctx.focus in .dpl; 5th param in .js sidecars).
  • Mandatory i/f prefixes + the $ keyword sigil. Conditions and token args must prefix the dial — [i<10%] / [f<40%], {#name i25% f80%} — because the two percents are visually identical; an unprefixed 25% / <10% is not dial syntax (a bare [10] is still a weight). Keyword interpolation moved off {intensity} (which collided with {list} syntax) to the $ sigil: since the dial is a percent, $intensity renders 50% (no separate % form) and $intensity-word the magnitude word, plus the $focus set. All in-tree content was migrated losslessly via byte/EOL-preserving PowerShell passes — {intensity…}$intensity-word (28 files) and the bare [<NN%] conditions → [i<NN%] (18 files).
  • Global layer auto-merge (dedup). Imported generators behave as one-time global layers: a generator renders once per prompt, so a second nested import of the same singular generator (e.g. two scenes both pulling {#weather}) is dropped. User-typed duplicates always render. A generator that legitimately repeats opts out with stacking: true front-matter — set on the chained decorators color / glow / neon / crystal so the change doesn't regress them.
  • Editor support for all of the above. The shared CodeMirror DPL language (gui/src/lib/dpl/) now highlights the dial args (i25% / f80%), [i<NN%] / [f<NN%] conditions, $intensity / $focus, the front-matter block (fences / keys / values), section heading names (with Start / Auto Begin / Auto End emphasized), and the go to / insert / +call name targets. New context-aware autocompletes: the i / f dials (with intensity-vs-focus descriptions) pop up on a space inside {#name …}; front-matter keys inside ---; section names after go to; and generator
    • section names after insert / +. Also normalized every block to a blank line after the front-matter fence (89 files).

Tests: DPL unit + engine-integration suites extended (focus conditions/keywords, dedup, stacking, back-compat); 119 Node + 51 SPA tests green, smoke + gui build clean. Design notes: reference/focus-design.md + reference/layering-design.md added, reference/intensity-design.md updated to the $/i/f syntax.

2026-06-28 — 2.12.1: Manage tab only mounts when the real backend answers (not a static 200)

(PATCH — fixes shipped behavior + main CI.)

managerAvailable() gated the Manage tab on res.ok from GET /api/manage/ping. On a static host (the online build / Playwright's vite preview) unknown routes fall back to the SPA's index.html with HTTP 200, so the probe was a false positive — Manage mounted where it shouldn't (its API calls then fail). It also broke main CI: with Manage mounted, the E2E visual spec's .sidebar locator matched two elements (Generate's + Manage's — both view-panes stay mounted) → strict-mode violation. Fix: the probe now parses the response and requires the real JSON { ok: true }; an HTML fallback fails the parse and reports unavailable. So Manage shows only in true local mode, and E2E sees a single sidebar again (no test changes needed). VERSION + package.json → 2.12.1.

2026-06-28 — 2.12.0: the Manage tab (in-app content manager) + tests/docs

(MINOR — a feature milestone. VERSION + package.json → 2.12.0. Built across phases 1–5 on feature/manage-tab, summarized here for the release.)

A new 4th SPA tab, Manage — edit the catalog (blocks, lists, folders/categories, sidecar options) right in the app, on the real data/ files, with live hot-apply. Local mode only (gated on a file-backend capability probe; locked online). Highlights: a runtime disk-snapshot loader the engine reads through (runtimeLoader.js) so edits apply with no reload (except an edited .js module body — reloads); the real nested folder tree with category/subfolder color-coding, force-prefix/group badges, abstracted _-markers, NSFW gating, and search; block editor (DPL + JS-sidecar tabs / create-from- boilerplate), folder editor (rename, priority/description/forceList, marker toggles), and list editor (virtualized entry mode + raw CodeMirror, seamless at 27k lines); add/delete, drag-to-move, restore- default from main, ghost pills for locally-deleted-but-upstream files (diffed against a published data/manifest.json, disk-cached a day), and external-edit auto-refresh (SSE fs.watch). Backend: gui/server/manageFs.js + /api/manage/*. New dep @codemirror/lang-javascript. Contract tests in tests/integration/manageFs.test.js (11, green); full npm test green (Node 111 + SPA 51). Docs: plans/manage-tab.md, reference/dependencies.md, status.md. Follow-up: jsdom component tests + Playwright e2e/visual baselines for the tab.

2026-06-28 — Manage tab, phase 5: add/delete, drag-to-move, external-edit watch

(No version change yet; feature/manage-tab.)

  • Add controls: an always-visible "+" on every folder and each root opens a small menu — New block/list (creates the file, with minimal DPL boilerplate for blocks, and opens it for editing) or New (sub)folder (empty folders now show in the tree). Names sanitized.
  • Delete: entry pills now have a Delete action (removes content + JS/JSON sidecars, confirmed); the folder editor has Delete folder (recursive, confirmed) and clears the selection.
  • Drag-and-drop move: entry pills are draggable onto any folder (or a root) in the same root, moving the content + sidecars; the drop target highlights. (Per the owner, blocks ride the app's existing name/priority sort — drag is move-between-folders, not a custom order; category priority stays a numeric field in the folder editor.)
  • External-edit watch: GET /api/manage/watch is an SSE stream backed by fs.watch on both roots; the client debounce-refreshes on change, so editing a file in another editor updates the tab live. The manual Refresh button remains the fallback.
  • manageTree: the folder filter now keeps genuinely-empty folders (so a new one shows) and only hides NSFW-named folders when adult is off.
  • Build green.

2026-06-28 — Manage tab, phase 4b: ghost (restorable) entries + published file manifest

(No version change yet; feature/manage-tab.)

Files deleted locally but still present upstream now show as ghost pills in the tree — faded, dashed, with a single Restore action (no edit/delete); wholly-deleted folders reappear as ghost folders (no gear). Restoring fetches the file from the stable branch and it becomes a real entry.

  • scripts/build-data-manifest.mjs (new) + npm run manifest + data/manifest.json: a published static manifest listing every content file per root, regenerated at release. Ghost detection is a simple set difference (manifest − local), so there's no GitHub-API tree scrape (owner's suggestion — simpler, no rate limits).
  • manageFs.remoteManifest: fetches main/.../data/manifest.json and disk-caches it in the OS temp dir for a day (filename carries a repo@branch hash) — checked on boot, re-downloaded at most ~once/day, and falls back to the stale cache when offline (owner's caching request). GET /api/manage/remote-manifest (+ ?fresh=1).
  • manageTree.js: computeGhosts (manifest − local tree, entry files only, NSFW-gated) + injectGhosts (places ghosts in their folder, synthesizing ghost folders); Manage.jsx fetches the manifest, injects ghosts, and renders restore-only ghost pills.
  • Verified: ghost detection is exact (none when intact; the one deleted file flagged with correct kind); manifest generates (76 lists / 89 generators); build green.

2026-06-28 — Manage tab, phase 4: list editor (entry + raw) + restore-default

(No version change yet; feature/manage-tab.)

  • ManageListEditor.jsx (new): edits .txt lists and .group files in two modes — Entries (default): a searchable, virtualized row list (only the visible window renders) with quick add / inline-edit / delete, smooth even on the 20k–27k-line lists, no size warnings; and Raw: the whole file in the plain-text CodeEditor. The file is fetched once and entry edits mutate an in-memory line array (nothing re-serializes per keystroke); trailing-newline is preserved. Plus rename, a description sidecar, and Restore default.
  • Restore-default: POST /api/manage/restore + manageFs.restoreFromRepo fetch the original from the stable branch and overwrite the local copy (404 upstream ⇒ delete local). Branch is main, not master: master is a stale old-layout branch with no engine-v3/ tree (restore would 404); the owner confirmed main, which carries the current layout. Verified main's raw URL serves the files (200) and the 404→delete path works.
  • Manage.jsx: routes list/group entries to the list editor; openEntry no longer pre-loads (each editor loads its own file).

2026-06-28 — Manage tab, phase 3: block + folder/category editors (save + hot-apply)

(No version change yet; feature/manage-tab. Adds dep @codemirror/lang-javascript — to be recorded in reference/dependencies.md + list-credits.md at ship.)

  • ManageBlockEditor.jsx (new): edit a generator's .dpl (reuses DplEditor), name (rename moves the .dpl/.js/.json together), description, and NSFW flag (gated — disabled with the "NSFW option only available in NSFW mode" tooltip when adult is off, and shown forced when the name carries an nsfw token). JS sidecar support: a DPL ⇄ JS tab when a .js sidecar exists, a one-click Create JS sidecar that scaffolds from boilerplate into a JavaScript CodeMirror, and a save that notes "reload to run the changed JS" (DPL + sidecar metadata hot-apply; only JS execution waits for a reload).
  • ManageFolderEditor.jsx (new): rename a folder, edit its sidecar (priority, description, and — lists only — forceList), and toggle its _-markers as plain controls: a Force-prefix checkbox and a group mode (Auto / Always / Never → the enable/disable markers). Saves hot-apply.
  • CodeEditor.jsx (new): a small reusable plain-CodeMirror editor (JS now, raw lists next), built for very large documents (viewport rendering).
  • API: POST /api/manage/{sidecar,marker,fs} (+ the manageFs helpers mergeSidecar, setMarker, fsOp for mkdir/mkfile/delete/move) and client wrappers. Sidecars self-delete when emptied; all ops traversal-guarded; atomic writes.
  • Manage.jsx: routes a generator's Edit to the block editor and a folder gear to the folder editor; handleChanged hot-applies and (on rename) reselects the moved item. manageTree now exposes per-folder enableGroup/disableGroup so the editor shows the true group state.
  • Verified: create → sidecar → marker → rename(+sidecar) → delete all round-trip through the snapshot, traversal is blocked, and the throwaway folder is cleaned up (9/9 headless checks); SPA build green with the new dep.

2026-06-28 — Manage tab, phase 2: the tab + the real folder tree (read-only)

(No version change yet; feature/manage-tab. MINOR bump lands when the feature ships.)

  • gui/src/components/Manage.jsx (new): the Manage tab — Generate's two-pane skeleton, but a content manager. Left: the real nested folder tree of both roots (Blocks = blocks, Lists = lists) with categories vs subfolders color-coded, force-prefix / implied-group folders badged, _-markers abstracted into those badges, a per-folder gear by the name, entry pills with a hover Edit action (clicking a pill never inserts), a search filter, and a Refresh catalog button. Right: a detail/preview pane (the real editors come next). NSFW is gated by the header toggle.
  • gui/src/lib/manageTree.js (new): builds the display model from GET /api/manage/tree, computing categories / force-prefix / implied-group semantics with the engine's own listManifest + gatedLists helpers (so the view can't drift from real behavior) and a name-filter.
  • App.jsx: Manage added as the 4th tab (after Single), gated on a local-mode capability probe (managerAvailable) rather than the build flag — locked with a hint online / on a static host, and only mounted when the backend is present. Header providers/gear stay hidden on this tab; the NSFW toggle stays.
  • styles.css: Manage styling (tree rails by category/subfolder/special, kind-colored entry pills, badges, the detail pane).
  • Verified: model reproduces the real structure (Blocks: 7 categories incl. force-prefixed prompt
    • nested expansion/* groups; Lists: 11 categories with force-prefix on artist/danbooru-d/lore/name/ style), NSFW gating drops the 4 adult lists when off; SPA build green.

2026-06-28 — Manage tab, phase 1: runtime (disk-backed) catalog loader + local-mode API

(No version change yet; foundational plumbing on feature/manage-tab, no UI — the MINOR bump lands when the tab ships.)

First slice of the in-app content Manage tab (plan: notes/plans/manage-tab.md). This phase adds the runtime hot-apply foundation, no UI:

  • gui/server/manageFs.js (new): the local-mode content backend — traversal-guarded fs helpers over data/lists + data/blocks: a full-catalog snapshot builder, a folder-tree builder, a safe path resolver, and an atomic file writer. Extracted as its own module so it's unit-testable in plain Node and reusable by a future production local build.
  • gui/vite-plugin-api.js: new /api/manage/* endpoints — ping (capability probe → unlocks the tab in local mode), snapshot, tree, and file (GET read / POST atomic write).
  • gui/src/lib/runtimeLoader.js (new): a third engine loader implementing the same interface as browserLoader/nodeLoader. Until a snapshot is fetched it delegates to the build-time bundle (so first paint and the online build are unchanged); once a snapshot is installed it serves the catalog live from disk (lists, .dpl text compiled at runtime, groups, sidecars, markers), delegating only .js-module execution + presets to the bundle (running fetched JS would need eval).
  • gui/src/lib/manageApi.js (new): client for /api/manage/* (snapshot, tree, file r/w, managerAvailable probe).
  • gui/src/lib/promptEngine.js: now drives the engine through the runtime loader; getBlocks recomputes the catalog live; added refreshCatalog() (fetch snapshot → loadAll() → notify) plus a subscribeCatalog/getCatalogVersion pub-sub for hot-apply.
  • gui/src/core/browserLoader.js: export dpJsModule(key) so the runtime loader can reach the bundled .js generators/sidecars.
  • App.jsx calls refreshCatalog() on load (no-op online); Home.jsx subscribes so the palette re-renders on a hot-apply.
  • Verified: the snapshot reproduces the Node loader's catalog exactly (87 lists, 89 generators, matching group/force-prefix dirs; dpJs=0 — every generator is .dpl, so the JS-reload boundary is effectively moot). SPA build + npm run smoke green.

(PATCH; setup ergonomics.)

  • engine-v3/package.json: added "postinstall": "npm --prefix gui install", so a single cd engine-v3 && npm install now installs both the engine and the gui/ SPA dependencies. Removes the fresh-clone footgun where npm run web failed with Cannot find package '@vitejs/plugin-react' because the gui deps were never installed. npm run web:install is kept for reinstalling just the gui deps. README updated to the single-install path. Harmless in CI (the existing gui job still installs/builds the SPA independently).

2026-06-28 — README: clearer "build & run from source" for fresh clones

(Docs only; no version change.)

  • README.md: a fresh clone that ran only npm install then npm run web failed with Cannot find package '@vitejs/plugin-react' — the SPA in gui/ is a separate npm package and its deps were never installed. The engine-v3 quick-start now lists the two installs explicitly (npm installnpm run web:installnpm run web), with a one-line note on why there are two. Production build (web:build) is its own block, and testing (npm test / test:e2e) moved to a separate Development subsection so the run-from-source path isn't cluttered with dev steps. Also fixed the stale web-app/gui/ reference.

2026-06-28 — 2.11.1: open generated images reliably in a new tab (online build)

(PATCH; online-build bug.)

  • gui/src/lib/output.js + PromptResult.jsx: clicking a generated image in the online build navigated the <a href={img}> directly. For data: URLs (OpenAI/Gemini/Grok/Stability return base64) browsers block top-level data: navigation (intermittent "nothing happens"); for remote URLs it opened the remote address instead of the cached image. New openImageInNewTab() converts a data: URL to a blob: object URL synchronously (so the window.open stays in the click gesture and isn't popup-blocked) and opens that; blob: / served / remote URLs open directly. The image <a> now intercepts the click and uses it when there's no single view (online). lint clean, build green, tests green.

2026-06-27 — 2.11.0: online build goes fully static (browser-direct generation, no functions)

(MINOR; makes the free online deploy actually viable.)

The Netlify free tier's function limits (125k invocations/mo and a 10s timeout) made the serverless BYOK proxy a poor fit for heavy/bulk generation. Fix: in the online build, call each provider's API straight from the browser with the visitor's own key — no backend, so no cap and no timeout. A live CORS-preflight check decided which providers can do this.

  • transport: "browser-direct" for OpenAI, Gemini, Grok, Stability, Leonardo, fal.ai (all send CORS headers). Their code/generate.js now calls their own code/server.js directly instead of posting to /api/generate. src/lib/rewrite.js likewise calls the provider's rewrite adapter directly (OpenAI/Gemini/Grok), so auto-fix/keyword rewrite is also backend-free online.
  • Replicate, Black Forest Labs (FLUX), Ideogram have no usable browser CORS → kept on hosted-proxy and locked in the online build (greyed, with a tooltip explaining they can't be called from a browser + a link to the desktop version), alongside the local providers.
  • availableProviders() online now excludes hosted-proxy too; the App online-fallback picks the first browser-direct image provider. lockedHint() takes an optional reason; ProvidersMenu/ ProviderPicker pass a per-provider lock reason.
  • The deployed site now uses zero serverless functions. The functions + server/dispatch.js stay in the repo for the local dev proxy (desktop full version → non-CORS providers). Stays on Netlify (static hosting) at prompt.fairyfox.io.
  • Verify: lint clean, online build green (529 modules), npm test green (100 Node + 51 jsdom).

2026-06-27 — 2.10.5: fix serverless BYOK proxy crash (adapter is not a function)

(PATCH; surfaced on the first live Netlify deploy.)

  • gui/server/dispatch.js — Netlify's function bundler hands an ESM export default back as a namespace object ({ default: fn }), so the static default imports of the provider code/server.js / code/rewrite.js adapters weren't callable in the deployed bundle — dispatch threw adapter is not a function and both /api/generate and /api/rewrite returned 502. (Worked locally under native ESM, which is why it wasn't caught.) Added an asFn() unwrap applied to every serverAdapters / rewriteAdapters entry, so both module shapes work. Verified on the live site: the dummy-key call now reaches OpenAI ("Incorrect API key") instead of crashing. lint clean, 51 jsdom tests green.

2026-06-27 — 2.10.4: online build shows local-only features disabled (with a link), not hidden

(PATCH; owner request — prep for the prompt.fairyfox.io Netlify demo.)

  • New gui/src/lib/online.js — shared ONLINE flag, FULL_VERSION_URL, lockedHint(feature), and openFullVersion(). Single source for the online-build "this is in the full version" behaviour.
  • App.jsx — the Gallery/Single tabs and the NSFW toggle are now always rendered; in the online build they show disabled (greyed, lock badge, tooltip; clicking opens the full version) instead of being omitted. Online also auto-switches a saved local provider (e.g. the default ComfyUI) to the first hosted provider so Generate works immediately.
  • NsfwToggle.jsx — new locked prop (greyed + tooltip + link; forced off).
  • ProvidersMenu.jsx / ProviderPicker.jsx — local providers are shown disabled (🔒 "full version" badge) online rather than filtered out.
  • styles.css.is-locked styling for tabs, the NSFW switch, and provider rows.
  • netlify.toml[build.environment] VITE_ONLINE = "true", so a Netlify build is the online variant with no dashboard config. reference/deployment.md updated with the prompt.fairyfox.io setup steps.
  • Verify: lint 0 errors, online build green, npm test green (100 Node + 51 jsdom).

2026-06-27 — 2.10.3: add {#rays} back to the default wrapper end

(PATCH; owner request.)

  • gui/src/lib/wrapperStore.jsDEFAULT_WRAPPER_SEED.end: {#fx}, {#artists}{#fx}, {#artists}\n- {#rays}. God rays return as the optional ~50% bullet (the only piece the owner wanted kept from the old end); the rest stays tossed. Comment updated. SPA build + gui tests green.

2026-06-27 — 2.10.2: strip quality-spam from the default wrapper

(PATCH.)

  • gui/src/lib/wrapperStore.jsDEFAULT_WRAPPER_SEED: removed the quality-booster trash. The START was masterpiece, best quality, highly detailed → now empty; the END was {#fx}, {#artists} + intricate detail / sharp focus / wide shot / {#rays} / {#dap} → trimmed to just {#fx}, {#artists} (random art-style + artist variety — the generator's actual feature — with no masterpiece/best quality-class filler, no forced framing, no art-site tags). Comment updated.
  • WrapperFab.jsx: the wrapper-start input placeholder no longer suggests masterpiece, best quality as the example.
  • New/reset installs get the clean default; existing users keep their saved wrapper until they reset it (it lives in localStorage, which the seed change can't touch). SPA build + gui tests green.

2026-06-27 — 2.10.1: tail cleanup of the block refactor (coffecup typo, beach-merk de-scatter)

(PATCH.)

  • expansion/subject/coffecup: content typo coffecupcoffee cup (the token/filename stays).
  • user/beach-merk (community submission): de-scattered like the core beach — dropped the doubled {#city} pulls, deduped repeated synonyms (oceanside/sand/palm trees), and removed the off-theme {#color} crystal/gemstone; kept Merk's character (palm trees, tropical, oasis/vegetation extra), made waves {intensity}-aware, and preserved the credit.
  • Confirmed nothing auto-pulls the opt-in {#legacy} / {#dap} packs (the masterpiece, 8K, hyper-detailed filler now lives only in a deliberately-invoked expansion — no generator injects it). Closes out the catalog-wide sweep: no remaining inline otherwise: colon bugs, typos, or dead bare-# refs.

2026-06-27 — engine: auto-appended fx/artists now re-resolves nested tokens (root-cause fix)

(On feature/dpl-intensity; part of the 2.10.0 work.)

  • src/core/stages/block.js: the auto-{#fx} / {#artists} append was moved to run before the {#…} resolver loop and now appends the tokens ({#fx}, {#artists}) rather than their pre-rendered output. So any nested {#…} they emit resolves in the same passes. This is the root cause behind the literal {#rays} leak (the fx.dpl content fix removed the symptom; this removes the cause — any future auto-appended generator with nested tokens is now safe). New engine integration test; 0/50 leaks with auto-fx + auto-artists on. Node suite 99 → 100 green.

2026-06-27 — prompt/ batch + fx leak fix (Phase 2 batch 5 — final category of the intensity overhaul)

(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)

  • fx.dpl: replaced the trailing nested {#rays} token with inline god rays, volumetric lighting. Auto-{#fx} is appended after the resolver loop, so that nested token used to survive in output as a literal {#rays}; inlining it removes the leak (0/40 leaks now with auto-fx + auto-artists on). The underlying engine auto-append quirk remains, but no shipped auto-appended generator emits a nested #-token anymore.
  • The rest of prompt/ (artists, d, random, random-words, simple-random, extra-random) are legitimate JS builtins (keyword/artist repeaters, the danbooru stream, the suggestion engine) — random by design — and were left as-is.
  • This completes the block content refactor across all five categories (scene, fragment, subject, style, prompt); expansion/ is the deprecated tree. smoke + Vitest (99 + 51) + SPA build green.

2026-06-27 — style/ block refactor (Phase 2 batch 4 of the intensity overhaul)

(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)

  • Renamed the flagged anime-irlanime-realism (clean break): the name now says what it is. Updated the dynprompt-meta description map and fixed the {#underwater-anime-irl} expansion that referenced it — it used the dead bare #anime-irl syntax, so it had silently stopped resolving; it now reads {#anime-realism} and works again. De-scattered anime-realism (dropped the {#expressive} / {#weather} / {instrument} pulls); kept the makoto shinkai / wlop / artgerm / rossdraws artist Auto End — that artist set is the style (the owner's "some files use artists" case).
  • retro-poster de-scattered (dropped {#weather} / {instrument}; kept its style-artist line). Fixed typos in the isometric set (blue blackgroundbackground, doubled soft colors).
  • Left the other style recipes intact — their render tags (3d blender render, physically based rendering, Pixar render, etc.) genuinely define those publicprompts looks and are not gratuitous quality-spam. smoke + Vitest (99 + 51) + SPA build green.

2026-06-27 — subject/ block refactor (Phase 2 batch 3 of the intensity overhaul)

(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)

  • knight de-scattered (the flagged case): dropped {#landscape} and [[castle]], removed the render-farm Auto End (unreal engine 5, octane render, ray tracing, hdr, …) and the hyperrealistic / detailed face / detailed body filler. Now framed with {#portrait}, a one-of armor pick, {emotion}, {#general-state}, and [>70%]/[<25%] detail conditions.
  • Portrait family de-scattered + bug-fixed: removed the {#expressive} / {#weather} / {instrument} pulls from portrait-person and portrait-princess; fixed inline otherwise: colon artifacts (portrait, portrait-princess, and the earlier beach) and sceptorscepter. Portraits now use {#portrait} framing and the look/ lists ({expression}, {emotion}). furry/wildlife tidied.
  • The entity*.js type-system sidecars (animal, person, living-entity, entity-name, entity) are legitimately JS and were left as-is. smoke + Vitest (99 + 51) + SPA build green.

2026-06-27 — fragment/ block refactor (Phase 2 batch 2 of the intensity overhaul)

(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)

  • Polished 9 of the 15 fragment/*.dpl garnishes. Typos fixed: accesoriesaccessories (room-state), mesmorizingmesmerizing (mystical). Trimmed over-piles: room-state's giant damage list and general-state's filler (detailed, messy, holes); dropped underwater paradise.
  • Intensity-aware: {intensity} magnitude on clutter / crystal formations / snow drifts / embers / foliage / schools of fish / wear. A few sensible synonyms added (ethereal, otherworldly, ominous, foreboding, amethyst, quartz, icicles). The 6 already-clean tiny pools (color, glow, neon, water, weather, expressive) were left as-is. Descriptions unchanged, so no meta churn.
  • smoke + Vitest (99 + 51) + SPA build green; eyeballed low/high-intensity output per fragment.

2026-06-27 — scene/ block refactor (Phase 2 batch 1 of the intensity overhaul)

(Content batch on feature/dpl-intensity; no version bump — part of the 2.10.0 work.)

  • All 26 scene/*.dpl rewritten to be focused and filler-free. De-scattered: beach no longer pulls {#city}; cave/mountains/landscape drop {#settlement}; futuristic/space drop the {#portrait-*} / {#ruins} / {#city} / {#animal} pulls; space's {#ship} (a sea ship) → the correct {#spaceship}. Synonym piles trimmed (ruins/ship/vehicle), typos fixed (interriorinterior).
  • Wired into the lists scenes were ignoring — {view}, {time}, {mood}, {style/building|general|construct}.
  • Intensity-aware: scenes use the new {intensity} keyword for magnitude ({intensity} waves / stalagmites / crowd / sails / decay) and [>70%] / [<25%] conditions for sparse-vs-lush detail; great-tree / great-bridge bias large with {intensity +40%} / {+30%}.
  • 5 JS sidecars converted to pure DPL (futuristic, great-bridge, great-tree, space, spaceship) and the .js deleted; the size-helper logic became the {intensity} keyword. Meta sidecars regenerated. smoke + Vitest (99 + 51) + SPA build all green.

2026-06-27 — 2.10.0: DPL intensity dial ({#name NN%}, conditions, auto-scaling, {intensity} keyword)

(MINOR — a notable new DPL language capability: a per-reference "how much" dial that flows into a generator and reshapes what it renders. Engine + tests + design note; the content refactor that uses it follows in batches.)

A {#name} reference can now carry an intensity percent, and generators react to it.

  • Token argument{#great-bridge 25%} runs the generator at 25% intensity (1–100; 0%1%; unspecified → the default 50%, top-level and nested alike). Parsed in src/core/stages/block.js and handed to the generator as a 4th argument (mod.default(settings, imageSettings, upscaleSettings, intensity)); .dpl sees ctx.intensity, .js sidecars read the 4th param (loaders updated to pass it).
  • Line conditions — a square-bracket slot now also takes an intensity condition: [<10%] - grass renders only when intensity < 10%. Operators < <= > >= = == !=; stackable with a weight, separated by a pipe or a space, either order ([100|<10%], [100 <10%], [<10% 100]). Non-spec brackets ([[castle]], [deemph], [a:b:0.5], the salt literal) still pass through as payload.
  • Auto-scaling — probability gates multiply by intensity/100, and repeat/one of/N of counts scale by round(count × intensity/100). Plain always-on lines and the bare-otherwise branch are not scaled. (Because the default is 50%, an un-dialed generator now renders lighter than before — a deliberate, retunable choice via DEFAULT_INTENSITY.)
  • {intensity} keyword — text can react too: {intensity} → a magnitude word (tiny/small/normal/large/huge/massive), {intensity%} → the percent, {intensity-num} → the number. e.g. {intensity} amount of grass.
  • Relative / derived intensity — any intensity reference accepts a signed modifier taken of the value: {intensity +25%}, {intensity% -10%}, and relative refs {#weather +25%} / {#clouds -40%} (rewritten to an absolute percent inside the DPL renderer, which knows the base).
  • Tests — new DPL unit coverage (conditions, operators, weight/condition stacking, gate + count scaling, the {intensity} keyword + relative modifiers, clamping) and two engine-pipeline integration tests proving {#name NN%} threads end-to-end; the seeded DPL snapshot was updated for the 50% default.
  • Designnotes/reference/intensity-design.md.

2026-06-27 — 2.9.0: provider header redesign (Providers dropdown, BYOK keys, Prompt/Negative switch)

(MINOR — a notable GUI feature set reworking how providers, keys, and the negative prompt are chosen. One cohesive change to the header + composer.)

Header is now a single Providers dropdown + a settings gear + the NSFW switch.

  • ProvidersMenu.jsx (new) — one header trigger opens a panel with two stacked rich pickers (ProviderPicker.jsx, new): Image (grouped Local — incl. Plain text — / Online) and Text (Off + the rewrite AIs: OpenAI / Gemini / Grok). Each row carries its BYOK key field on the right in a fixed two-column grid so the rows align and the key box keeps one width. Selecting the image provider still sets the engine dialect/mode; the text provider sets rewriteProvider.
  • BYOK key moved to the header (ApiKeyField.jsx, new) — one compact pill per provider with the save / clear controls inside the field, plus the info tooltip and "Get a key" link. Keyed by provider id, so the same provider chosen for both rows shares one key (shown once with a "shares the image key" note).
  • Provider settings moved into a header gear (ProviderGear.jsx, new) — the provider's own knobs (ProviderBox.jsx, now bare controls, no collapsible card) render in a popover next to the dropdown, out of the main prompt area; the popover header shows the provider label + tier (name ellipsizes, the tag never wraps).
  • Old ProviderSelect.jsx removed; the rewrite-provider select + its key were removed from the gear Settings.jsx (they live in the Providers dropdown now); auto-fix / keyword toggles stay on the prompt box.

Composer Prompt/Negative switch. The negative prompt left the provider settings; the composer's editor now flips between Prompt and Negative via a segmented switch on the right of the insert bar, shown only when the provider supports a negative prompt. Building-block inserts, clear, and the live preview follow the active field; the negative is stored per provider (providerParams[id].negativePrompt, read by generation as before).

Tests. gui/tests/Settings.test.jsx updated for the moved key + bare ProviderBox; added ApiKeyField, ProviderGear, and ProvidersMenu coverage (51 SPA tests). Verified: lint 0 errors, npm test green (smoke + Node + SPA), gui build green, Playwright 8/8 (incl. a11y); Windows visual baselines refreshed for the new top bar — the Linux set still needs the visual-baselines.yml workflow.

2026-06-27 — 2.8.0: keyword tooling + DPL insert toolbar

(MINOR — a notable two-part GUI feature set. Bundled into one release because the two strands share Home.jsx + styles.css in the working tree and couldn't be cleanly split without breaking the build.)

Keyword tooling (single view + composer).

  • Robust keyword parser (gui/src/lib/keywords.js, new). Turns a real sent-to-model prompt into clean tags: strips SD/NovelAI weighting + attention syntax ((w:1.2), ((w)), [a:b:0.4], <lora:…>, BREAK, pipes, quotes), splits on commas/newlines so multi-word tags stay whole, de-dupes. Keeps the accented display form (café stays café) but computes a de-accented lowercased key for dedupe + gallery search — so cafe still finds café and they collapse to one chip. Replaces the single view's naive comma-split cloud.
  • Single view "Rebuild with AI" (SingleView.jsx). Sends the sent prompt to the rewrite provider in a new keyword mode, alphabetizes the result, and saves it over the image's sidecar (meta.keywords) via a new POST /api/image/meta endpoint (vite-plugin-api.js) + updateImageMeta() (lib/output.js). The cloud prefers the saved list ("Keywords · edited"); the gallery search haystack includes it.
  • Composer keyword-translate toggle (Home.jsx, settings.js autoKeyword). A tag button beside the auto-fix wand. Independent + chainable: auto-fix runs first (if on), then keyword-translate on its output.
  • Keyword rewrite mode threaded through the rewrite pipeline (_shared/rewriteSystem.js KEYWORD_SYSTEM
    • systemFor(); dispatch.js; the openai/gemini/grok adapters take a system override; lib/rewrite.js, vite-plugin-api.js, netlify/functions/rewrite.js pass mode).
  • Details as a real <table> (SingleView.jsx + styles.css), replacing the CSS-grid key/value divs.

DPL insert toolbar (gui/src/components/DplInsertBar.jsx + gui/src/lib/dpl/dplInserts.js, new). A slim row of category buttons (Structure, Chance, Choose, Repeat, Flow, Emphasis, Code) above the prompt box; each popover lists the non-text DPL constructs with name, description, literal syntax, and a live re-rolling example, and drops a CodeMirror snippet (with ${…} tab stops) at the cursor. Backed by DplEditor's new imperative insertSnippet() handle; minor supporting edits to dplLanguage.js, promptEngine.js, ProviderBox.jsx, WrapperFab.jsx.

Tests. Fixed the stale E2E selectors (home.spec.js, accessibility.spec.js) that still targeted the old <textarea> — they now drive the CodeMirror editor (.prompt-input .cm-content, click + pressSequentially), which had left the Playwright job red since the 2.7.26 CodeMirror switch. Linux visual baselines regenerated via the visual-baselines.yml workflow for the new toolbar + buttons. Verified: lint 0 errors, npm test green (smoke + Node + SPA), gui build green.

2026-06-27 — 2.7.29: online build is a stripped, Generate-only variant

(On feature/online-build; merged after 2.7.28's feature/gui-result-polish.)

The deployed (VITE_ONLINE=true) build is now a deliberately stripped variant (App.jsx):

  • No tabbar. The header's Generate/Gallery/Single switch is hidden and the Gallery + Single views aren't rendered at all — the image feed needs the local dev server's filesystem, which the online build doesn't have. The view is pinned to Generate. Generated images open in a new tab (no single view) rather than into the gallery.
  • NSFW off, no option. The header NSFW toggle is removed entirely, and adult content is forced off on load (includeAdult coerced to false) so a previously-saved preference can't carry over.
  • Nothing stored to the browser. The online build skips the feed fetch and never persists images — they stay in-memory for the session only.

The local build is unchanged (full three-view app with the NSFW toggle). Verified: lint 0 errors, both build modes green (vite build and VITE_ONLINE=true vite build), npm test green (smoke + Node 86 + SPA 43).

2026-06-27 — 2.7.28: result/gallery polish — corner preview, click-to-copy, DPL example tooltip, gallery actions

(On feature/gui-result-polish; merged after 2.7.27's feature/dynprompt-sidecar.)

A pass of UI polish on the composer, the generated-prompt list, and the gallery:

  • Preview moved to the prompt box's corner. The live-preview eye left the bottom action bar and now sits in the upper-right corner of the prompt box as a bare icon (.composer-corner / .preview-corner in Home.jsx + styles.css); the clear-✕ shares that corner cluster.
  • Click-to-copy prompts (no copy button). PromptResult.jsx dropped the per-row "copy" button — the prompt text is now click-to-copy (provider-aware, via onCopy) with the full text in a hover tooltip, matching the already-click-to-copy DPL / original lines.
  • DPL hover shows a re-rolling example. Hovering the source-DPL line opens a tooltip with the full DPL on top and a concrete example that re-rolls every second (new DplHoverCode, the same live-preview idea the building-block chips use). PromptResult now takes settings for this.
  • Gallery thumbnails gained the generate-thumbnail actions. Each gallery cell now reveals open in the default app · reveal in explorer · delete on hover (Gallery.jsx reworked from a single <button> to a .g-open button + a shared .img-actions overlay); App passes its existing deleteItem through as onDelete.

Verified: lint 0 errors, npm test green (smoke + Node 86 + SPA 43), gui build green.

2026-06-27 — 2.7.27: block sidecars gain priority (pill order) + an nsfw flag

(On feature/dynprompt-sidecar; merged after 2.7.26's feature/dpl-codemirror.)

Two sidecar-driven changes to the block catalog:

  • Category priority. The block picker now orders the category/folder pills inside the Blocks tab by a priority number in each category folder's .json sidecar — lower lifts it higher, default 1000. getBlocks() was refactored to build category groups ({ priority, name, pill, entries }), with the virtual any wildcard pinned to 0 (leads) and special to 9000 (trails). The metadata script (scripts/dynprompt-meta/write-dynprompt-meta.mjs) writes the curated order — prompt 200, scene 300, subject 400, style 500, fragment 600, user 700 — so the picker reads Any · Prompt · Scene · Subject · Style · Fragment · User · Special. (This reorders the pills within Blocks, not the Blocks/Lists tabs.)
  • nsfw sidecar flag (hard-hide). A generator whose .json sidecar carries nsfw: true is treated as non-existent when the NSFW switch is off — hidden from the picker (getBlocks({ includeAdult }) drops it, and a wholly-adult category vanishes) and never resolved by the engine: the {#name} stage (block.js) now gates on sidecar.nsfw === true || isGatedBlock(name), so it's excluded from {#any} and group picks and resolves to "" if referenced. The automatic nsfw-name-token rule still applies on top; the flag is the escape hatch for an adult generator whose name has no token (the set is empty today — the catalog is SFW). Home.jsx re-derives the palette when the switch flips.

New SPA test asserts the pill order (any → … → special). Verified: lint 0 errors, npm test green (smoke + Node 86 + SPA 44), gui build green. Sidecars regenerated (86 files).

2026-06-27 — 2.7.26: DPL boxes are CodeMirror editors (syntax highlighting + autocomplete)

The prompt, negative-prompt, and wrapper Start/End boxes are now CodeMirror 6 editors instead of plain <textarea>s, sharing one new reusable component (gui/src/components/DplEditor.jsx — controlled, drop-in value/onChange, line-wrapped). Two pieces back it:

  • A DPL language (gui/src/lib/dpl/dplLanguage.js): a line-oriented StreamLanguage tokenizer that mirrors src/core/dpl/dpl.js and a HighlightStyle mapping each token to a CSS class. It colors {#generator} refs and {list} refs distinctly, emphasis weighting ( ) [ ] | :1.2, +ref calls, ; comments, and the line-leading DPL structure (=== headings, - bullets, [900] weights, NN%/maybe/otherwise gates, one of/N of choices, repeat … times, insert/go to flow). Structural keywords only fire at the start of a line, so prose ("a man of war", "maybe later") is never mis-highlighted.
  • Brace-aware autocomplete: inside a { or {#, a dropdown offers every list and generator token from the engine catalog (getDplCompletions() in promptEngine.js), each option replacing the partial brace so there's no double-brace; on a - bullet line it offers the DPL structural keywords.

Highlight colors are theme-aware — they live in styles.css as --dpl-* variables with a light-theme override, not in a JS theme. New SPA deps: @codemirror/{state,view,language,autocomplete,commands} + @lezer/highlight (recorded in reference/dependencies.md). Verified: lint 0 errors, smoke + Node suite (86) + SPA suite (43) green, gui build green. The Playwright visual baselines for the prompt box change still need a refresh (npm run test:e2e:update) before release.

2026-06-26 — 2.7.25 (cont.): three persistent views (Generate / Gallery / Single) with state + scroll

Reworked the SPA into three top-level views that keep their state across tab switches for the session. The top-bar switch is now Generate · Gallery · Single (the single-image view is its own tab). All three stay mounted the whole session and are shown one at a time via a CSS class (.view-pane / .on) — so each view retains its full React state, form inputs, and scroll position with no manual save/restore (scroll comes along for free precisely because the scroll containers are never unmounted — the only clean way, as asked).

The shared coordination state moved up into App: the saved-image feed (items), the gallery search query, and the image open in the single view (current). Gallery.jsx became a controlled grid-only component; the single page was extracted to SingleView.jsx as a standalone view (keyboard nav gated on active so a hidden view doesn't grab arrow keys). Flow: clicking a generated image in Home now opens it in the Single view (Back → Generate, state intact) instead of a new browser tab; clicking a gallery thumbnail opens it there too (Back → Gallery); keyword-cloud chips jump to the Gallery filtered by that term. The Single tab shows the last image you looked at, or — the first time in a session with nothing loaded — a random one from the feed. Delete lands on a neighbour image (or leaves the view when it was the last). test:web 43 still green; lint 0 errors, smoke + gui build green.

2026-06-26 — 2.7.25 (cont.): proper single-image page, negative AI translation, ImageMagick export

Built out the gallery's single-image page into a real app view (migrated from the v1-2 /single) and enriched the metadata it shows. Three strands:

  • Prompt + negative are now recorded in layers. The sidecar moved to a nested shape — prompt: { dpl, roll, ai, final } and negative: { dpl, roll, ai, final } (DPL source → deterministic engine roll → AI translation → what was actually sent). Crucially, the negative prompt is now also AI-translated when auto-fix is on (it was only DPL-expanded before): Home.makeBatch rewrites the rolled negative through the same provider and sends/stores the translated negative. Both rewrites are cached on the prompt entry so re-generating a prompt doesn't re-bill the rewrite API. lib/gallery.js exposes promptLayers/negativeLayers/promptText that read the nested shape and still accept the old flat one.
  • Rebuilt single page (Gallery.jsx). Sticky image left, info right: a Prompt card and a Negative card each showing their layers as labeled, copy-able blocks (duplicates collapsed); a curated Details table (provider, model, sampler, steps, CFG, size, seed, saved, file) over a collapsible All settings and a Raw metadata (JSON) disclosure; a clickable keyword cloud (chips built from the sent prompt's tags that filter the gallery); prev/next + arrow-key nav; and an actions row. The obsolete v1-2 actions (variations, upscale, animate, rerolls, "select", parent, animation frames) were dropped — we don't use them in v3.
  • ImageMagick export. The dev server detects ImageMagick (GET /api/magick) and lists the still (non-animated) formats it can write (magick -list format, filtered to a still-raster allowlist); the single page shows a Convert & download dropdown of those formats, each converting via GET /api/image/convert (first frame only, streamed as a download). Hidden entirely when magick isn't on PATH — graceful, like the rest of the local-only gallery. New lib/magick.js.

gallery.test.js updated for the nested schema (+ layer-helper tests); test:web 43 green. Lint 0 errors, smoke, gui build all green. No VERSION bump (still the unreleased 2.7.25 gallery line on feature/gallery).

2026-06-26 — 2.7.25: photo gallery + per-image JSON metadata sidecars

Brought back the v1-2 image feed as a first-class v3 view. A Generate / Gallery switch in the upper-left of the top-bar flips between the prompt/image composer and a new photo gallery that browses everything saved to output/. To make that gallery meaningful, every generated image now gets a .json metadata sidecar written next to it (same base name, like the v1-2 feed) capturing how it was made: the prompt actually sent, the deterministic engine roll (promptOriginal), the AI translation (the auto-fix rewrite, when on), the source DPL, the negative prompt, the provider, and a full settings snapshot (API keys stripped — never written to disk). The gallery reads these back: a masonry grid of lazy-loaded thumbnails (wide/tall by aspect), a keyword search over prompt/DPL/provider, and a dedicated single-image page (replaces the grid — not a modal — with back + prev/next + keyboard nav) showing the whole record with copy / open / reveal / delete (which removes the image and its sidecar). Implemented on the dev-server side as POST /api/image (now accepts meta and writes the sidecar) + a new GET /api/feed (scans output/, pairs each image with its sidecar, newest first); delete cleans up the sidecar too. Local-only by nature (needs the dev server's filesystem) — a static/ online build shows an empty gallery with an explanatory note rather than an error. New src/lib/gallery.js (+ gallery.test.js, 5 tests); the previously-unused Gallery.jsx was repurposed as the gallery view. Verified: lint 0 errors, format, smoke, gui build (browser-glob gate), test:unit 86 + test:web 40 green.

2026-06-26 — docs (no version bump): adopt fairyfox 0.9.3 delta (verification floor)

Scheduled fairyfox check-for-updates flow (hub 0.9.2 → 0.9.4), then owner-authorized adoption in the same session. The only node-facing change is 0.9.3's clarification that the verification floor is never skipped: an express-authorized (or any automated) adopt skips only the redundant confirmation pause — never verification, which now must run the full floor (build/tests + the standards' ## Verify/compliance checks + project-constraint checks) before and after the apply, with a hard fallback (if verification can't complete, do not auto-apply — fall back to check-report-wait). Reworded the express-auth carve-out in CLAUDE.md and notes/reference/cross-project-sync.md to match. Skipped: 0.9.3's reports_through filename-list change (hub-internal .last-seen.yml, not carried here) and the 0.9.4 hub data/blog/meta pass (bookkeeping). Combined check→adopt report at notes/fairyfox-reports/2026-06-26-adopting-updates-3.md. Notes/CLAUDE.md-only — no VERSION bump.

2026-06-26 — 2.7.24 (PATCH): auto-fix (AI prompt rewrite) before image generation

New opt-in auto-fix: a chosen text AI rewrites the mechanical DPL prompt into something a given image model handles better, before the image request goes out. Configured in the gear → Auto-fix (rewrite) group — a "Rewrite with" Select (None + any provider exposing loadRewrite: OpenAI, Gemini, Grok) plus its own BYOK key row (in-memory until Save, Get-a-key link). The rewrite provider is separate from the active image provider. When a provider is selected, a wand toggle appears on the prompt box (bottom-right, by the preview eye); only then can it be turned on. With it on, each generated prompt is rewritten once (server-side adapters via /api/rewritedispatchRewrite, or the Netlify rewrite function online), the cleaned text is sent to the image provider, and the original is kept under the DPL line in the result (click to copy). New: _shared/rewriteSystem.js, openai|gemini|grok/code/rewrite.js, lib/rewrite.js, netlify/functions/rewrite.js, rewriteProviders() in the registry. Verified: web build, test:web 35. Server changed (dispatch + dev middleware) — restart the dev server.

2026-06-26 — 2.7.23 (PATCH): live-preview eye buttons (prompt + negative) + bigger negative box

New reusable LivePreview — an eye button that, while hovered, shows a tooltip re-rolling the DPL into a concrete example every ~1s (same idea as the building-block chip previews; reads the text fresh each tick). Added to the prompt box field bar and next to the negative prompt (which is now a bigger textarea, 4 rows / 5rem min-height). Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — 2.7.22 (PATCH): grouped/explained provider dropdown + BYOK key links

Replaced the plain provider <select> with a custom dropdown grouped into Local and Online, each entry showing a one-line description (and a "key" badge for BYOK). The provider box gained a "Get a key ↗" link next to the API-key field (per-provider URL). Descriptions/links live in gui/src/lib/providerMeta.js. Retired the generic "Local Stable Diffusion WebUI" (A1111) entry — its adapter + settings stay (reused by Forge/SD.Next); the default provider moved to ComfyUI and a settings migration maps any saved local-webuiforge. Verified: web build, test:web 35 (provider/Settings tests updated). Client-only — a refresh suffices.

2026-06-26 — 2.7.21 (PATCH): NovelAI + plain-text + Forge/SD.Next; catalog at 14 providers

Finished the catalog breadth. NovelAI (syntax tier — NovelAI {}/[] dialect, Copy-prompt) and a generic Plain text target (plain dialect, Copy-prompt) — both no-API, just config. Forge and SD.Next are A1111-sdapi-compatible, so they're thin providers reusing the Local WebUI adapter + settings (distinct labels; set the URL to your server). The catalog is now 14: local-webui (=A1111), forge, sdnext, comfyui, openai, replicate, fal, stability, gemini, grok, bfl, ideogram, leonardo, midjourney, novelai, plain. Client-only — a refresh suffices. Verified: web build, test:web 35. Deferred (bespoke/uncertain APIs, want careful verification): InvokeAI, SwarmUI, Fooocus.

2026-06-26 — 2.7.20 (PATCH): hosted providers — Gemini, Grok, FLUX/BFL, Ideogram, Leonardo

Second hosted batch. Gemini (Google generateContent "Nano Banana" image model → base64), Grok/xAI (OpenAI-compatible api.x.ai/v1/images/generations), FLUX/Black Forest Labs (api.bfl.ai submit → poll polling_url until Ready → result.sample, via the shared submitPoll), Ideogram (v2 JSON /generate, Api-Key), Leonardo (create generation → poll until COMPLETE, model = UUID). Each registered in server/dispatch.js. Dialect plain. Verified: web build, test:web 35. NOTE: dispatch changed — restart the dev server. Confidence: Gemini/Grok/FLUX high; Ideogram (v3 API shift) and Leonardo (model UUIDs drift) are best-effort and may need per-account tweaks — flagged in their config comments. (FLUX/Ideogram are also reachable via the Replicate/fal providers.) Remaining: NovelAI/plain, then local backends.

2026-06-26 — 2.7.19 (PATCH): hosted providers — Replicate, fal.ai, Stability

First catalog expansion on the provider framework: three hosted BYOK image APIs, all single-call server.js adapters (no polling). Replicate (model endpoint + Prefer: wait, model = owner/name slug, output URLs), fal.ai (fal.run/<model> sync, Key auth, named image_size presets), Stability AI (v2beta stable-image/generate/{core,sd3,ultra}, multipart, base64 image, negative prompt). Each is config + settings + code/{generate,server} + (model) data; registered in server/dispatch.js; fieldInfo gained aspectRatio/imageSize. Dialect plain. Verified: web build, test:web 35. NOTE: dispatch changed — restart the dev server for these to call out. API specifics verified against live docs; exact per-model fields may still need refinement. Next batches: Gemini/Grok/ FLUX-BFL/Ideogram/Leonardo, then NovelAI/plain, then the local backends.

2026-06-26 — 2.7.18 (PATCH): full-width Reset button in the gear popover

The "Reset to defaults" button in the prompt-settings gear popover now spans the popover width (.settings-actions stretch + button 100%) instead of being shoved to the right at half width. CSS-only. Verified: web build.

2026-06-26 — 2.7.17 (PATCH): reversed result numbering + always show DPL line

With newest-first ordering, the result position number now counts down from the top (newest = highest number, prompts.length - i, passed as number) instead of index + 1. The per-prompt DPL line now always shows (dropped the "only if it differs from the expanded text" condition). Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — 2.7.16 (PATCH): newest-first ordering + per-prompt DPL

Newest on top: new rolls prepend their prompts, and each new image batch prepends within its prompt (batch numbers stay chronological — newest carries the highest number). Each prompt now remembers the DPL it was rolled from (prompt.dpl) and shows it (monospace, 2-line clamp, click-to-copy) right above its expanded text — replacing the single global "DPL" line, which was wrong once results from different rolls coexisted. Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — 2.7.15 (PATCH): fix gallery width (specificity) + new roll appends

Root-caused the stubborn gallery layout: .prompts li (specificity 0,0,1,1) set align-items: flex-start, overriding .prompt-result's align-items: stretch (0,0,1,0), so the gallery shrank to content width — which forced ~3 columns and made the shimmer placeholders stack regardless of the earlier tile fixes. Bumped the selector to .prompts li.prompt-result so column + stretch win; the gallery now fills the row and images/placeholders flow into all available columns. Also: a new roll now APPENDS to the results list instead of replacing it (removed the replace + disk-delete prompt; Clear all / per-prompt clear handle removal). Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — 2.7.14 (PATCH): gear popover opens downward (no longer clips off the top)

The prompt-settings gear popover now opens downward from the gear (was upward) and caps at 60vh, so it no longer runs off the top of the page when the prompt box sits high. CSS-only. Verified: web build.

2026-06-26 — 2.7.13 (PATCH): results polish — skeletons, clear actions, source DPL, tighter bar

Round of results/composer polish. Skeletons now mirror the image tile structure (a figure per placeholder) so they flow into columns like real images instead of stacking. Clear actions: a global Clear all button in the results header, a per-prompt clear button in the action row, and a new roll replaces the previous results — each offering to delete the corresponding image files from disk. Source DPL: the DPL template a batch was rolled from now shows above the results (2-line clamp + copy), and each result's expanded prompt is clamped to 2 lines (copy button beside it for the full text). Composer bar: the prompts counter × became a "count" label and the gear sits tight beside it (.prompt-tools). Blocks sidebar trimmed ~25% (minmax(320→240px, 400→300px)). Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — 2.7.12 (PATCH): square thumbnail grid + move the gear to the prompt box

Two UI fixes. Gallery grid: images now render as uniform square thumbnails (object-fit: cover inside an aspect-ratio tile) with min-width: 0 on the grid items, so they (and the shimmer placeholders) flow into as many columns as fit instead of blowing the tracks out to the image's intrinsic width (which caused premature wrapping / single-column placeholders). Settings gear moved: removed the header gear + Settings drawer; the gear now sits next to the prompt-count on the prompt box and opens a compact, single-column popover with the same prompt knobs (vocabulary, emphasis, editing/alternating, salt). Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — 2.7.11 (PATCH): auto-render images + loading placeholders + fade-in

Images now auto-render with the prompts: generating prompts kicks off an image batch for each one (api providers). While a batch renders, the grid shows shimmer placeholder tiles sized to the batch count; each finished image fades in once it actually loads (ResultImage with an onLoad opacity transition). makeBatch takes the prompt text directly (so auto-render works before state commits) and records the expected count. Tightened the gallery grid. Verified: web build, test:web 35. Client-only — a refresh suffices.

2026-06-26 — docs (no version bump): adopt fairyfox 0.9.2 delta + fix release tagging

Adopted the hub 0.9.0–0.9.1 standards delta (hub now at 0.9.2) — notes/CLAUDE.md only, so no VERSION/package.json change. Headline fix: CLAUDE.md and git-workflow.md documented hand-tagging (git tag … && git push --tags) for releases, but release.yml derives + creates the tag itself and gates on the tag not already existing — a hand-pushed tag would make the release run skip itself (silent no-op). Removed every hand-tag from the PATCH/MINOR/hotfix paths; added a "Who creates the tag — CI, not by hand" section. Also adopted the express-authorization ledger carve-out (CLAUDE.md, cross-project-sync.md): an active hub/authorizations.yml entry lets an interactive adopt skip only the redundant confirmation pause (every other safety step stays; unattended checks still apply nothing); plus the process-reports combined-report / real-hub_version-anchor rules and a compliance row note. Combined adopt report at notes/fairyfox-reports/2026-06-26-adopting-updates-2.md.

2026-06-26 — 2.7.10 (PATCH): per-prompt image batches + image management actions

Image results are now per prompt: each prompt row carries its own image batches, and a clear "Generate images" / "More images" button adds a batch beneath it (replacing the cryptic icon button); copy stays. New PromptResult.jsx; Home prompts state became { id, text, batches: [{ id, busy, images }] }. Per image: open in the OS default app, reveal in the file explorer, or remove (with a confirm to also delete the file from disk). A batch and the whole prompt's images can be cleared too, each offering disk deletion. Clicking an image still opens it in a new tab (galleries later). New dev-middleware endpoints (/api/image/delete|reveal|open, localhost + output-folder restricted, Windows explorer/start) and output.js helpers (isOutputFile, deleteImageFile, revealImageFile, openImageFile). Verified: web build, test:web 35. NOTE: the dev middleware changed — restart the dev server for the file actions.

2026-06-26 — 2.7.9 (PATCH): provider box — collapse, tooltips, key save flow, negative DPL

Provider-box polish. The box now collapses (collapsed by default) via its header. Every control has an info tooltip (a shared _shared/fieldInfo.js dictionary keyed by field key). The BYOK key is no longer auto-saved — it's held in memory for the session (gui/src/lib/sessionKeys.js) and only persisted when you click Save (with a confirm); a saved key shows a Clear saved button. Generation uses the session key, else the saved one. Negative prompt is now a real textarea that accepts DPL — rolled out via the engine (like the main prompt) right before sending — with a ⟳ random button that materializes one roll. Verified: web build, test:web 35.

2026-06-26 — 2.7.8 (PATCH): UI redesign — provider in header, provider-controls box, slimmed gear

Owner-driven UI restructure (no "named slots" — kept simple). The provider picker moved to the header (next to the gear); choosing a provider still sets the dialect/mode. A new capability-driven provider-controls box (ProviderBox.jsx) renders in the main column showing only that provider's own controls — URL / key / checkpoint / sampler / scheduler / steps / CFG / W·H / batch / seed / negative, whatever it declares — stored per-provider (providerParams[id]). The gear Settings now holds only the non-provider prompt knobs (vocabulary lists, emphasis, editing/alternating, salt). Removed from the UI: keyword min/max and artist counts (better done in the prompt via DPL); auto-add fx/artists disabled by default (includeArtist/autoAddArtists/autoAddFx → false; keys kept so the engine still has values). Added a prompts-per-run counter on the prompt box (bottom-left of the field bar). New ProviderSelect.jsx; base .field + new component styles. Verified: web build, test:web 35 (Settings/ProviderBox tests updated). Client-only change — a browser refresh suffices (no dev-server restart).

2026-06-26 — 2.7.7 (PATCH): central output folder for generated images

Owner's call: every provider's images should land in one central output folder, not be loaded piecemeal from each backend. (Comfy Desktop also 403'd the browser on direct /view loads, so the images showed as broken.) Added to the dev middleware: POST /api/image ingests an image src (a data: URL — decoded — or a localhost URL — fetched server-side, which sidesteps the 403) into engine-v3/output/ and returns a served path; GET /api/output/<file> serves it. New gui/src/lib/output.js ingestImage(); Home.makeImage now funnels every provider's results through it before display, so the saved copies render (same-origin) and accumulate in one folder regardless of provider. Replaced the earlier display-only /api/forward-image proxy. NOTE: changing the Vite plugin requires a dev-server restart to take effect. Verified: web build, test:web 33.

2026-06-26 — 2.7.6 (PATCH): ComfyUI also auto-resolves sampler + scheduler

Follow-up to 2.7.5's checkpoint fix: with the checkpoint resolved, ComfyUI next rejected sampler_name: 'Euler' (the global SD-style default leaked into the request; ComfyUI uses lowercase euler). The adapter now also queries /object_info/KSampler and coerces sampler_name + scheduler to a valid value (exact → case-insensitive → euler/normal → first available), via a shared pick() helper, alongside the checkpoint. So stale/SD-style sampler names self-heal. Verified: web build, test:web 33.

2026-06-26 — 2.7.5 (PATCH): ComfyUI checkpoint auto-resolve + readable local errors

Live-debug fixes after a real ComfyUI run. The app was sending a stale/blank checkpoint name (model.safetensors), so ComfyUI returned a value_not_in_list validation error — and our transport threw new Error(errorObject), which the UI showed as "[object Object]". Two fixes: (1) the ComfyUI adapter (code/generate.js) now resolves the checkpoint against what ComfyUI actually has installed — queries /object_info/CheckpointLoaderSimple, keeps the configured name if valid, else uses the first available, else throws a clear "no checkpoints installed" message — so a missing/blank/stale name self-heals; (2) localDirect.js gained readableError() which extracts ComfyUI's error.message + node_errors details instead of stringifying the object, so failures show the real cause. Verified: web build, test:web 33.

2026-06-26 — 2.7.4 (PATCH): route local-direct providers through the dev server (no browser CORS)

All provider calls now go through a server — no provider is called directly from the browser. The local-direct transport (localDirect.js postJson/getJson) now posts to a new /api/forward endpoint in the Vite dev-middleware (localhost-only, server-side fetch), so ComfyUI / A1111 work even though they send no CORS headers (Comfy Desktop sends none and can't easily be given the --enable-cors-header flag). Image URLs stay direct — an <img> tag isn't CORS-blocked for display, only fetch() is. Hosted providers already routed through /api/generate, so the browser now makes zero cross-origin provider calls. Also set the ComfyUI provider's default checkpoint to v1-5-pruned-emaonly-fp16.safetensors. The local-webui contract test updated to assert the forwarded target + body. Verified: web build, test:web 33. (Surfaced while debugging a live setup: Comfy Desktop was running with no checkpoint model installed — separate user-side fix.)

2026-06-26 — 2.7.3 (PATCH): provider UI re-add — capability-driven settings, image gen, copy-prompt

Phase 2 of the provider work (feature/providers): re-added the image-generation UI (it was in the "removed, pending re-add" bucket). Settings backend is now capability-driven — the provider <Select> renders that provider's OWN settings.js fields (per optionsFrom data lists), stored under a per-provider namespace providerParams[id] so switching providers never clobbers another's knobs; the provider owns the dialect (engine mode is set from it — the standalone "Mode" dropdown and the hardcoded global Image/Negative groups are gone). Re-added the Settings drawer + gear button in App.jsx. Home now generates images via the active provider's adapter — a per-prompt image button (api tier) feeding Gallery — and Copy-prompt runs the prompt through the provider's formatter for the syntax tier (Midjourney --params), so the right thing is copied. New gui/src/lib/useProvider.js (useProviderSettings lazy-loads schema + option data; providerMode; flattenForProvider merges namespaced params + dialect for the adapters). providerParams added to default settings. Verified: lint 0 errors, web build (provider code/settings/format chunks code-split), test:web 33 (+ a Settings capability-driven component test; +2 from 31). Note: the Playwright visual baselines will need a refresh (the home UI gained a gear button + image results) — not part of the headless gate, do on a deliberate test:e2e:update. Follow-ups: submit/poll hosted adapters; surface per-provider presets in the UI.

2026-06-26 — 2.7.2 (PATCH): provider framework foundation + plain dialect

First-class image-provider framework (on feature/providers), replacing the flat gui/src/lib/providers/ (localWebui + a hostedProxy stub). Each provider is now a self-contained folder gui/providers/<id>/config.js (manifest: tier · dialect · transport · capabilities), provider-owned settings.js, presets/, code/, data/ — auto-discovered by gui/providers/index.js via import.meta.glob (the block plugin pattern). The support ladder: api (we render), syntax (emit the tool's grammar + params, Copy-prompt), plain (natural language). New native core change: a plain dialect in src/helpers/randomEmphasis.js that keeps the engine's emphasis rolls but renders them as natural-language words (intensifier/hedge ladder, provider-overridable) instead of weighting syntax — so syntax-less targets still receive emphasis (the list stage already routes non-NAI/MDJ modes through the SD function set, so no stage change). Transport: local-direct (browser → user's server), hosted-proxy (browser → /api/generate → upstream BYOK, served by the Netlify function online and a new Vite dev-middleware locally — both share gui/server/dispatch.js), and none (syntax/plain). New storage subsystem gui/storage/ (pluggable by run mode: online = stripped localStorage; local = real .json file via /api/storage, browser fallback) with a per-provider presetStore. First providers: local-webui (ported, real), comfyui (real local-direct workflow-graph adapter), openai (hosted, client + server adapter), midjourney (syntax tier — data-driven --param catalog split into parameters.json + versions.json). The old mode becomes provider-owned dialect (standalone Mode control to be removed when the UI re-adds image generation). Verified: lint 0 errors, smoke, test:unit (86, +3 plain-dialect), test:web (31, + registry/Midjourney contract), web build. Follow-ups: capability-driven settings UI + image-results + Copy-prompt re-add in Home/Settings; submit/poll hosted providers (Stability/fal/Replicate/BFL/Ideogram/ Leonardo); live ComfyUI run once installed. Design: notes/plans/providers.md.

2026-06-26 — Release 2.7.1 (PATCH): flat blocks, data/sources, web-app → gui

Layout reorg completing the v3-only move. Blocks are now flat under data/blocks/<category>/: the v3/ wrapper folder was removed and the leftover version machinery stripped end-to-end — block.js is one flat catalog (auto-begin/end now unconditional; {#v1/}/{#v2/}/{#any-ver} dropped), nodeLoader/browserLoader lost their startsWith("v3/") filters, promptFilesAndSuggestions dropped v1Files/v2Files (v3/user/user/), and the SPA promptEngine/Home dropped the per-generation browse + version superset switch (this also fixed isExpansionKey, which would otherwise never match flat keys, and the Blocks picker that would have rendered empty). The 5 .js generators' src imports went one level shallower (../../../../src../../../src); dpl-validate/dpl-engine-check/write-dynprompt-meta and the blocks README were de-versioned. Loose raw build inputs (artists.csv, danbooru.csv, nai-tag-expirement.json) moved to data/sources/ (build scripts + .prettierignore updated). The SPA folder was renamed web-app/gui/ — the name anticipates a planned CLI (the core engine is already headless/isomorphic); every config reference updated (CI/pages/visual-baselines, eslint/vitest/ playwright, package.json scripts, build-docs, netlify.toml, the gui tree's own @module tags). The duplicate root Upgrade-2-0.md was removed (it lives in engine-v1-2/). PATCH bump (2.7.0 → 2.7.1) — a refactor/cleanup; v1/v2 addressing was already dead since 2.7.0, so nothing user-facing breaks. Verified: smoke, web build, test:unit (83), test:web (28, +a Blocks-non-empty assertion), lint 0 errors. Known follow-ups: a fuller notes sweep of v1/v2/web-app mentions in the deeper reference/ docs; the netlify.toml engine-v3 path reconciliation (pre-existing); and the preset system rethink.

2026-06-26 — Adopt hub updates: process-reports + compliance audit

Ran the fairyfox check-for-updates flow, then adopted the new hub standards (notes/process-docs only — no VERSION bump, no release). Refreshing the read-only hub clone needed a reset --hard origin/dev because upstream dev was force-pushed (--ff-only aborts). Adopted: the process-reports feedback loop — new notes/fairyfox-reports/ folder (README + this run's report) and reference/process-reports.md; every fairyfox run now ends with a report, even a check-only one. The standards compliance audit (reference/compliance.md) — the recurring whole-set check. Added ## Verify sections to reference/git-workflow.md and reference/versioning.md; folded the "hub also reads notes/fairyfox-reports/" model + a process-report step + the force-push refresh fallback into reference/cross-project-sync.md; updated CLAUDE.md (fairyfox flow ends with a report; names the compliance audit; new notes-maintenance trigger row) and notes/README.md. The git-flow release model the hub finalized was already adopted here (2026-06-25), so no release-mechanics change.

2026-06-26 — Release 2.7.0 (MINOR): two-engine split + v3-only

Milestone release bundling this session's restructure. The repo is now two disconnected projects — engine-v3/ (the active prompt engine + React SPA) and the frozen engine-v1-2/ (the literal pre-revival CommonJS CLI + classic Express/Pug server, restored from 241a148, runnable, on its way out). engine-v3 is v3-only (v1/v2 block generations deleted) and the legacy <expansion> mechanism was removed end-to-end (engine stage, classifier, both loaders, SPA UI, data). Also: classic-only deps pruned, CLAUDE.md + the notes reframed to the split, CI/release/pages workflows retargeted to engine-v3/ (CI uses npm install to dodge npm's cross-platform @emnapi lockfile bug). MINOR bump (2.6.1 → 2.7.0) — a notable milestone, no breaking API for SPA users. Known follow-ups: the doc-site (npm run docs) + GitHub Pages auto-deploy are paused pending build-docs.mjs path reconciliation to the new layout.

2026-06-25 — engine-v3 goes v3-only; drop the legacy expansion mechanism

Simplified/lightened engine-v3 to a single generation. Deleted the v1 + v2 block generations (data/blocks/v1/ + v2/ + their .json metas) — engine-v3 now ships only v3 (89 .dpl + JS sidecars), which stands alone. Removed the legacy <expansion> mechanism end to end: dropped the expansion stage from the pipeline (engine.js + settings.js promptModules are now block → prompt-salt → list → cleanup), deleted core/stages/expansion.js, removed expansion loading from the classifier, deleted the SPA Expansions tab + the "Save as Expansion" feature (button/panel/state/handler/icon) + the custom-expansion store, and deleted the data/expansions-obsolete/ content. The two v3 generators that still referenced expansions (v3/scene/futuristic.js: <dap>, <detail/legacy>) were repointed to their migrated block equivalents ({#dap}, {#legacy} under v3/expansion/, which remain as ordinary {#…} generators). Retired/updated the expansion-specific integration + snapshot tests and the custom-expansion unit tests. Verified green: smoke OK, 83 Vitest + 28 SPA tests, SPA build; lint warnings dropped 140 → 18. Then removed the trivial dead-code remnants in a follow-up commit (the loaders' now-unreachable expansionNames/readExpansion/globs + the orphaned namesUnder/expansionsRoot helpers, fakeLoader's expansion support, and settings.expansionFiles) — engine-v3 now carries no expansion code at all. No VERSION bump pending the release decision.

2026-06-25 — Split the repo: engine-v1-2 (frozen) + engine-v3 (the project)

Untangled the repo into two fully disconnected trees that share zero code. engine-v1-2/ is the literal pre-revival snapshot (commit 241a148, 2023-04-07, CommonJS) restored as-is — its own package.json/lockfile/webui.bat, web/ contained, runs standalone (verified: npm install + the CLI generates a prompt). Frozen, unmaintained, on its way out. engine-v3/ is now the project: the new core engine + SPA + data/ (the v1/v2/v3 block generations on the new lists) + tests + scripts + package.json/configs, moved together so all cross-folder imports survived. Deleted the transitional ESM legacy (classic server.js + web/, the CLI index.js/common.js/applyArgs, image/upscale/animation, loadSettings/createMissingUserSettings/diffSettings, the legacy prompt-modules stages, legacy helpers) since the old system is preserved by the snapshot. Rewrote scripts/smoke-test.mjs to boot the core engine via nodeLoader + settings.js (no common.js). Retargeted CI to run inside engine-v3/ and added a root README pointing to both engines. Verified from engine-v3: lint 0 errors, smoke OK, 84 Vitest + 30 SPA tests, SPA build green. No VERSION bump (the project moved, didn't change behavior). Follow-ups: drop the expansion stage (data still present), sync CLAUDE.md/notes paths to engine-v3/, fix the doc-site (jsdoc/build-docs) paths, decide root VERSION/fairyfox-node handling. Plan: notes/plans/engine-split.md.

2026-06-25 — Triage the old /generate page for SPA carry-over (notes only)

Catalogued every control on the legacy classic-server prompt page (src/web/views/generate.pug) and decided where each one goes as the page is retired: image-AI settings → the future provider abstraction; emphasis/editing/alternating → reworked to provider-dependent output (old (((x))) style is dated — Flux barely responds to weights); keyword-count/chaos/auto-fx/artists/anime-words → replaced by DPL + the v3 wrapper; all animation settings and the salt settings → dropped; per-image actions → deferred to the future image viewer/editor; folder paths + keepers (promptCount, keyword/artist list selectors) → port into the SPA UI. New plan doc notes/plans/generate-page-triage.md holds the full disposition + a four-sweep order (prune first); notes/plans/next-steps.md gains Sweep 1 as the next concrete item. No code touched (classic server is frozen/being deleted); no VERSION bump (notes only).

2026-06-25 — Adopt fairyfox git-flow standard; rename mastermain

Adopted the fairyfox system's updated git standard, which replaces the lean dev → main fast-forward model with full git-flow (long-lived main + dev; feature/*, release/*, hotfix/* support branches; all --no-ff; every commit on main a tagged release). Renamed the stable branch mastermain (mandatory under the new standard) and repointed the CI/Pages/release workflows (ci.yml, pages.yml, release.yml) from master to main. Rewrote notes/reference/git-workflow.md to git-flow and updated the master/FF-only references in CLAUDE.md, deployment.md, versioning.md, and status.md. GitHub-side default-branch flip and origin/master deletion are left for the owner. No VERSION bump (process/CI/docs only). See notes/sessions/2026-06/2026-06-25.md.

2026-06-25 — SFW by default + a top-bar NSFW toggle (2.6.1)

The SPA now defaults to SFW: added includeAdult: false to web-app/src/lib/settings.js defaults. Added a right-aligned NSFW switch to the top-bar (web-app/src/components/NsfwToggle.jsx, wired in App.jsx, styled in styles.css) — a stopgap "until we get an options screen". Enabling it pops a confirmation dialog (18+ / adult-content warning) before flipping settings.includeAdult to true; disabling is immediate. The preference rides in settings so it's remembered in the browser (localStorage). No engine change was needed — the core already gates adult content on includeAdult (core/listStore.js, core/stages/*, gatedLists.js); this exposes the switch. Bumped VERSION + package.json to 2.6.1 (ordinary feature → PATCH).

2026-06-25 — Onboarded into the fairyfox system + themed the docs site

Folded this repo into the fairyfox hub mesh per the hub's onboarding-existing-project runbook, project-side only (the hub repo was not touched). Added the "Cross-project standards & checking the fairyfox system for updates" standing instruction to CLAUDE.md (the on-request check → report → wait flow + guardrails, adapted to the devmaster model and the read-only assets/references/fairyfox.io/ clone), and a new notes/reference/cross-project-sync.md recording the sync model. Themed the JSDoc (docdash) doc-site toward fairyfox.io: new assets/docs-theme/{fairyfox-docs.css,fairyfox-docs.js} reproduce the shared design tokens (dark-first warm palette, Fraunces/Inter/JetBrains, theme-color metas) and inject the required two-way links back to Fairy Fox (sidebar brand, breadcrumb locator, footer); wired through jsdoc.config.json (docdash.scripts/menu/meta) with scripts/build-docs.mjs copying the theme assets into the generated docs/jsdoc/. No VERSION bump (docs/notes/CI-tooling only).

2026-06-22 — Visual-regression now runs in CI too (Linux baselines)

Closed the last gap from the previous entry: visual-regression was the one suite skipped on CI because its toHaveScreenshot baselines were committed for Windows only (*-chromium-win32.png). Generated matching Linux baselines (*-chromium-linux.png) and committed them, so the CI e2e job now runs the visual specs too. Mechanics: playwright.config.js now picks the browser by OS (Windows → system Chrome, Linux → Playwright's bundled chromium — the same browser CI uses) and gates visual on PLAYWRIGHT_SKIP_VISUAL (an escape hatch for a platform with no baselines yet) instead of CI. A new manual workflow, .github/workflows/visual-baselines.yml ("Update visual baselines (Linux)"), regenerates the Linux PNGs on the same runner the e2e job uses (ubuntu-latest + npx playwright install --with-deps chromium) and uploads them as an artifact to download + commit — so baselines and CI render identically. (Local Docker generation via mcr.microsoft.com/playwright was attempted first but the image pull crawled on this connection; generating on the CI runner is both faster and a guaranteed match.) Test/CI only — no version bump.

2026-06-22 — CI now runs the full test suite (Vitest + Playwright)

ci.yml previously ran only lint + format:check + smoke + the web-app build, so the 2.6.0 Vitest and Playwright suites never ran in CI — green CI could miss real regressions. Expanded CI to mirror the local gate: the check job now also runs npm run test:unit (Node Vitest); the web-app job now also runs npm --prefix web-app run test (jsdom Vitest); and a new e2e job installs root + web-app deps and the bundled Playwright chromium and runs npm run test:e2e (E2E + accessibility). Visual-regression is the one deliberate exceptionplaywright.config.js now testIgnores visual.spec.js when process.env.CI is set, because the toHaveScreenshot baselines are committed for Windows only (*-win32.png) and can't match Linux rendering; the same guard switches CI from system Chrome to Playwright's bundled chromium. To turn visual on in CI later, commit Linux baselines (test:e2e:update on Linux) and drop the guard. CI/test config only — no version bump.

2026-06-22 — Unbreak Release + Pages workflows (JSDoc types + verify step)

The first-ever master push (the ship below) triggered release.yml and pages.yml for the first time and both went red — pre-existing problems, unrelated to product code, that had never run before because deployments were held. Two fixes: (1) npm run docs aborted because JSDoc's parser can't read TypeScript-style type expressions — arrow types (n:string)=>(string[]|null) and optional record keys {category?:string,…} in src/listManifest.js, src/promptFilesAndSuggestions.js, src/blockManifest.js — rewrote them in JSDoc-native syntax (function(string): (string[]|null), explicit (T|undefined)); npm run docs now exits 0, fixing Pages and the Release docs-zip step. (2) release.yml's "Verify (lint + smoke test)" step ran npm test, which 2.6.0 had redefined to include the web-app jsdom suite (deps not installed in that job); realigned it to npm run lint + npm run smoke, matching the step's name and the CI gate (master is FF-only from a CI-green dev, and CI checks lint/format/smoke/build). Build/CI only — no version bump.

2026-06-22 — Unbreak CI: resync lockfiles + Prettier pass (ship to master)

CI had been red on every dev push since the test-suite landed, blocking the long-held first ship to master. Two causes, both unrelated to product code: (1) npm ci failed in both jobs because package-lock.json (root) and web-app/package-lock.json had drifted out of sync with package.json after the 2.6.0 Vitest/Rolldown dependency additions (missing @emnapi/* + platform native bindings) — regenerated both with Node 24 / npm 11; and (2) format:check was red because ~40 source/test files added since the last green run were never Prettier-formatted (CI never got past install to catch them) — ran prettier --write. lint, smoke, and the web-app build all pass locally; the CI gate is green again. No version bump (build/style only). This green dev HEAD is the first commit fast-forwarded to master, lifting the intentional deployment hold.

2026-06-22 — Full automated test suite: Vitest + Playwright (2.6.0)

Added a comprehensive, runnable test suite covering every standard test type — the project went from "lint + import smoke" to real coverage. Vitest drives two suites: a Node-side suite under tests/ (environment: node, vitest.config.js) and a jsdom SPA suite under web-app/tests/ (web-app/vitest.config.js, reusing vite.config.js so import.meta.glob + the lodash alias resolve as in the real build). Playwright (playwright.config.js, builds the SPA and serves dist/ via vite preview) drives E2E, visual-regression, and @axe-core/playwright accessibility specs under tests/e2e/.

Test types: unit (contentSafety, diffSettings, keywordRepeater, gatedLists, listManifest, the DPL compiler, cleanup, prompt-salt; SPA share/settings/customStore) · component/UI (Field, TokenPicker via React Testing Library) · integration (the full stage pipeline over a fake loader in Node, and over the real bundled data in the browser facade) · contract/API (the SD WebUI txt2img request/response shape, fetch mocked) · snapshot (seeded, reproducible DPL + pipeline output) · E2E (type → generate → results) · visual regression (stable chrome screenshots, the random suggestion masked) · accessibility (WCAG 2 A/AA, fails on serious/critical) · smoke (the original import-graph gate, retained) · bug regression (tests/regression/, one guard per fixed defect).

Scope deliberately excludes the legacy classic server (src/server.js, src/web/frontend/**, src/prompt-modules/**) — it is being actively phased out; only the pure stages the active core engine still imports (cleanup.js, prompt-salt.js) are tested. New npm scripts: test:unit, test:web, test:e2e, test:e2e:update, test:all, *:coverage; npm test now runs lint + smoke + Node + SPA suites. New dev deps: vitest 4, @vitest/coverage-v8, jsdom, @testing-library/{react,jest-dom,user-event}, @playwright/test, @axe-core/playwright. Result: 118 Vitest tests green (88 Node + 30 SPA) plus 8 Playwright specs green (E2E + visual-regression with committed baselines + axe a11y). The bundled Chrome-for-Testing build hit a Windows side-by-side launch error here (even with the VC++ runtime present), so the Playwright config uses channel: "chrome" (the version-matched system Google Chrome); CI can drop the channel to use the bundled browser. Discovered + documented landmine: lodash captures Math.random at import, so _.random/_.sample/_.shuffle can't be RNG-stubbed — tests assert invariants or use single-entry lists, and only the DPL renderer is seeded.

2026-06-21 — prompt/ category rename + "Prompts" navbar grouping (2.5.0)

Naming + UI polish on the 2.5.0 work. Renamed the v2/engine/ category to v2/prompt/ and marked it _force-prefix (so its generators show/insert as {#prompt/…}); within it danboorud, randomrandom-words, and the -prompt suffix dropped (random-promptrandom, simple-random-promptsimple-random, extra-random-promptextra-random). So {#prompt/random} is the composite suggestion and {#prompt/random-words} the keyword pile; the default settings.prompt (CLI + SPA) is now {#random-words} to preserve the prior default behavior. Fixed the one sibling import (extra-random./random.js) and regenerated sidecars. Reverted the forced {#user} group (the lone v2/user/ folder relies on the normal auto-rule, so it isn't a group). Navbar: collapsed the two tabs into a single "Prompts" heading with one v1/v2 superset switch (rendered v1 v2, v2 default) over full / partial sub-tabs. Category descriptions rewritten to describe each category (no group mechanics). lint (0 errors), smoke, web build (495 modules) green.

2026-06-21 — Pick-one groups + {#any} wildcard + Full/Partial navbar (2.5.0)

Reinstated and finished the "pick one" model for blocks and expansions, reframed (owner) as "pick one GENERATOR/snippet, not one word." Groups: a category folder with 2+ generators is an implied group — {#scene} runs one random scene generator; .group files + _enable/_disable-group-list markers work too; the same for expansions (<lighting> splices one random expansion). Restored blockGroupDirs/readBlockGroup and added expansionGroupDirs/readExpansionGroup in both loaders; the dyn + expansion stages resolve folder/.group refs to one random member (gate-aware) and run/ splice it. Wildcard: {#any} / {#any-sfw} / {#any-nsfw} pick one generator from the whole v2 catalog with the lists' {keyword}-style mode variants. SPA navbar: split into "Blocks" (full) and "Partial prompts" (partial) tabs with clickable folder-group pills + the {#any} family; v1/v2 are superset links on the navbar (v2 default), replacing the inline category toggle. Every group/ wildcard resolves to ONE concrete generator/snippet (never a line union). lint (0 errors), smoke, and web build (493 modules) all green.

2026-06-21 — Blocks: {#name} sigil + gating/wildcard + uniform SPA (2.4.0)

Round 2 of the block standards pass. Sigil: blocks are now written {#name} (brace-delimited, uniform with {list}/<expansion>, and able to carry / paths like {#scene/beach}); the bare #name form is retired (it let stray # in plain text get eaten). Migrated the 204 internal references across 54 v2 generators (scripts/migrate-dynprompt-sigil.mjs, comment-safe + idempotent); v1 has no internal # refs so it was untouched; updated the engine default, suggestion builder, settings, genImg, and SPA defaults. The list stage skips {#…} so the two {…} sigils never collide. Gating: isGatedBlock now keys off the nsfw name token (parity with lists), so an *-nsfw generator is hidden/empty when adult is off — no hardcoded list. Tags: src/blockManifest.js adds a blockTags map. SPA: the four behavior blocks collapsed into one uniform Blocks block — category-folder pills (plain labels) + a v1/v2 toggle on the header, mirroring Lists/Expansions. Per owner, dynamic prompts get no group entry lists and no random-pick wildcard (a folder is organization, not a random-member pool, and there is no {#any} either — a generator is a script with specific I/O, not a list you pick an entry from); the implied-{#folder}-group machinery and the {#any} wildcard added mid-pass were both removed. lint (0 errors), smoke, and web build (493 modules) all green.

2026-06-21 — Blocks: parity with lists/expansions + v2/ reorg (2.3.0)

Brought data/blocks/ up to the list/expansion standards, on both the file and UI sides. File side: a scripted migration (scripts/reorg-dynprompts-v2.mjs) moved the 79 v2 generators + the user-submitted one into category folders under a new v2/ root (scene/subject/fragment/style/engine/user), v1/ frozen, rewriting every relative import by resolving old→new absolute paths (v2 helpers now ../../../../src/…; cross-category siblings like ../fragment/nature.js). #name now resolves by path suffix (resolveName, splitting v1 vs v2 in core/stages/block.js), so every existing reference still works; #name-v1 and #user-name kept as aliases. Added <name>.json description sidecars for all 113 generators + 8 folders (scripts/dynprompt-meta/write-dynprompt-meta.mjs), plus readBlockMeta / blockForcedPrefixDirs / _-internal skip / compareNames sort in both loaders. UI side: the SPA token cloud keeps its Full/Partial/User/V1 sections but gained description tooltips, natural-order sorting, and shortest-#token display (computeButtonNames). Only the new engine was touched — the classic server and prompt-modules/block.js are read-only legacy reference (per owner). Caught + fixed 10 v1/* files importing the moved entity.js (only the vite build gate sees v1, not smoke). npm run lint (0 errors), npm run smoke, and npm --prefix web-app run build (492 modules) all green. New design note: reference/blocks-architecture.md.

2026-06-21 — Moved blocks/ from src/ to data/ (2.2.2)

The #name block generators now live under data/blocks/ instead of src/blocks/ — a deliberate, documented exception to "code lives in src/", since they're authored as prompt content like lists/expansions. git mv preserved history (incl. v1/ and user-submitted/). Updated both loaders: the legacy src/prompt-modules/block.js (require prefixed ../../data/), core/nodeLoader.js (rootDir/data/blocks), and core/browserLoader.js (glob ../../data/blocks/**/*.js). Rewrote the 18 cross-tree imports inside the generators that reach back into src/ (helpers + promptFilesAndSuggestions.js). npm run smoke, npm --prefix web-app run build, and lint all green. Recorded the exception in CLAUDE.md and decisions/architecture.md.

2026-06-21 — Expansions: rename detail/legacy* + port _force-prefix (2.2.1)

Renamed detail/legacy-detaildetail/legacy and detail/legacy-person-detaildetail/legacy-person (the detail/ folder carries the meaning) and added a detail/_force-prefix marker so the editor shows/inserts <detail/legacy> / <detail/legacy-person>. Ported _force-prefix to expansions: nodeLoader markedDirs(marker, base) + expansionForcedPrefixDirs(), a **/_force-prefix glob + generalized markerDirs(files, marker, seg) in browserLoader, fed to computeButtonNames in promptEngine. Updated the one reference (futuristic.js) and the meta-script keys. Display-only like lists (suffix resolution still works). npm test + vite build green.

2026-06-21 — Expansions brought to parity with the list system (2.2.0)

Ported the portable parts of the keyword-list modernization to data/expansions/. Nested the 9 expansions into category folders (detail, style, lighting, subject, scene) with path-suffix resolution (shared resolveName), so existing <rays>/<legacy-detail> references still resolve; both loaders walk the tree recursively and skip _-prefixed files. Added per-expansion + per-folder <name>.json description sidecars (14 files, via the new scripts/expansion-meta/write-expansion-meta.mjs) and readExpansionMeta on both loaders; the SPA "Expansions" cloud is now grouped by folder with category pills + tooltips, mirroring the Lists block. Deliberately NOT ported (don't fit copy/paste snippets): random-union groups, clickable folder pills, SFW/NSFW splitting, _force-prefix. New data/expansions/README.md + notes/reference/expansions-architecture.md. Names/content left unchanged. npm test + vite build green.

2026-06-20 — SPA: Lists panel grouped by folder category (2.1.0)

The SPA "Lists" cloud is now grouped by folder, alphabetical, with an inline category pill before each folder's entries (label = folder's last segment, tooltip = folder description). When the folder is an implied group the pill is clickable and inserts the whole-folder group ({word}, {d}, …). Removed scene/_force-prefix (scene names don't collide). npm test + vite build green.

2026-06-20 — Markers: dotfiles → _-prefixed regular files (2.1.0)

Renamed the folder markers from dotfiles (.force-prefix etc.) to _-prefixed regular files (_force-prefix, _enable-group-list, _disable-group-list) so Vite's import.meta.glob sees them — and dropped the workaround Vite plugin / virtual module. New convention: any _-prefixed file is internal/config, never a list (loaders skip them). Markers are empty files. npm test + vite build green.

2026-06-20 — Fix: force-prefix / group markers ignored in the SPA (Vite skips dotfiles) (2.1.0)

import.meta.glob doesn't match dotfiles, so the browser saw no .force-prefix / .enable/.disable-group- list markers (buttons showed bare names). Added a list-markers Vite plugin that fs-scans data/lists and exposes the marked folders as a virtual:list-markers module the browserLoader imports. Force-prefix and the group overrides now work in the SPA. npm test + vite build green.

2026-06-20 — Lists: implied groups become automatic (folder with 2+ lists) (2.1.0)

A folder with 2+ direct list files is now automatically an implied group ({folder} = union of its own lists); no marker needed. .force-group-list retired; .enable-group-list / .disable-group-list are the overrides. Doesn't stack (own direct files only). autoGroupListDirs in listManifest; loaders compute from .txt-only names + markers; impliedGroupMembers direct-children-only. Implied groups now include word, look, place, nature, lore, scene, style (plus artist, d, name); brand (1 list) is not. npm test + vite build green.

2026-06-20 — Lists: optional <list>.json metadata sidecars (tooltips) (2.1.0)

Each list can have a <list>.json sidecar with a description for the editor button tooltip. Loaders read them (readListMeta); the SPA cloud shows the description in the chip tooltip (falling back to the -sfw file for mixed/implicit lists). Shipped descriptions for all 79 built-in lists/groups. npm test + vite build green.

2026-06-20 — Lists: .force-group-list implied groups + fix refs after folder-suffix rename (2.1.0)

Verified the owner's list refactor (dropped redundant folder-name suffixes like general-stylegeneral, added .force-prefix to artist/scene/style) and fixed the block token references it broke ({building-style}{style/building}, {ship-type}{scene/ship}, etc.) plus listTags keys. Added .force-group-list: a folder with that marker is an implied group ({folder} = union of its lists, mode-aware). Marked artist/, danbooru/d/, name/ and removed the redundant artist.group / d.group / name.group (subset groups digipa, d/character, d/keyword kept). npm test + vite build green.

2026-06-20 — Lists: move d.group up to danbooru/d.group (buttons as {d}) (2.1.0)

Moved the whole-danbooru group out of the forced danbooru/d/ folder up to danbooru/d.group, so it auto-names to {d} instead of {d/d}. listTags key updated. npm test + vite build green.

2026-06-20 — Editor: shortest-unambiguous button names + .force-prefix (2.1.0)

List buttons now show just the filename unless a conflict forces a longer path, or a folder is marked with an empty .force-prefix file (which always shows its path from that folder down and is excluded from the conflict check). Added computeButtonNames to listManifest + forcedPrefixDirs() to both loaders + the danbooru/d/.force-prefix marker; the SPA's token cloud uses it (so danbooru shows {d/general}, everything else a bare filename). Display-only; resolution unchanged. npm test + vite build green.

2026-06-20 — Lists: keyword becomes a reserved wildcard (any-loaded-word) (2.1.0)

keyword is now a reserved name, not a file: {keyword} draws a random word from all loaded vocabulary (mode-aware), {keyword-sfw} = SFW always, {keyword-nsfw} = full set (gated). It supersedes any file named keyword silently and excludes the artist/* and danbooru/* namespaces. Implemented via RESERVED_WILDCARD in listManifest (resolveName short-circuit + resolveListLines union) and surfaced in the picker. The old keyword/ files were relocated first (languages → word/language.txt; adult vocab → word/adult-nsfw.txt) and the folder deleted. keyword stays the default keywordsFilename. npm test green.

2026-06-20 — Lists: second keyword pass — relocate leftovers, drop junk (593 -> 20) (2.1.0)

Hand-reclassified the keyword leftover tail: 354 entries relocated to their proper lists (animal, mythology, astronomy, history, religion, place, person, art-movement, word/noun|adjective, look/time, etc.), 219 junk dropped (chemical symbols, abbreviations, inflected artifacts, fragments), 20 languages kept. keyword-sfw 593 → 20. Lossless coverage-checked via scripts/list-cleanup/reclassify-keyword.mjs. npm test green. (Open: keyword is the default keyword source and is now thin — needs repointing.)

2026-06-20 — Lists: fix incompatible base.txt+base-nsfw.txt pairs (keyword, clothes) (2.1.0)

Renamed keyword/keyword.txtkeyword-sfw.txt and look/clothes.txtclothes-sfw.txt: a plain base.txt beside a base-nsfw.txt is ignored by the safety rule, which hid the implicit {base} button and made the default {keyword} alias mis-resolve to the danbooru keyword group. Now {keyword} and {clothes} resolve to their own SFW lists and the picker shows the implicit base button. npm test green.

2026-06-20 — Lists: move danbooru groups into d/ folder ({d}, {d/keyword}, {d/character}) (2.1.0)

Moved the three danbooru group files inside the d/ folder to match the {d/...} typed convention: d/d.group (ref {d}, was {danbooru}), d/keyword.group ({d/keyword}), d/character.group ({d/character}). Updated all references ({d-character}{d/character} across danbooru/entity/v1 prompts; anime keywordsFilename d-keywordd/keyword; anime detection now startsWith("d/"); web-app migration; listTags keys). npm test green.

2026-06-20 — Lists: SFW/NSFW safety rule — ignore plain name.txt when name-nsfw.txt exists (2.1.0)

Safety precaution: when a <name>-nsfw.txt exists, a plain <name>.txt is ignored entirely (not loaded, not listed) — the SFW half must be <name>-sfw.txt, so a lone <name>.txt beside an NSFW file is treated as NSFW-only and SFW content can't leak from a misnamed file. Enforced in readSfwBase and logicalListNames. npm test green.

2026-06-20 — Lists: SFW/NSFW redesign — filename-token gating + mode-aware auto-combine (2.1.0)

Reworked SFW/NSFW into a filename-driven, mode-aware model (replaces the group-file scheme). Any name with an nsfw token is adult and is fully hidden while adult mode is off (not listed/suggested, resolves to nothing). A mixed list is two files <name>-sfw.txt + <name>-nsfw.txt (no <name>.txt); the bare {name} is implicit and the resolver auto-combines by mode — {name} = SFW/both, {name-sfw} = SFW-only, {name-nsfw} = both (SFW auto-tacked on), with the same suffixes working on groups. Gating is now automatic by name token (no hardcoded list); the three split .group files are gone; the 4 standalone adult lists were renamed to -nsfw. New logicalListNames + includeAdult-aware resolveListLines threaded through all loaders; suggestion pool draws bare bases only; picker hides NSFW when off. npm test green.

2026-06-20 — Lists: SFW/NSFW convention FINAL — plain=SFW, name-nsfw-only, name-nsfw=both (2.1.0)

Reversed to the final, UX-first naming: plain <name>.txt is SFW (so {name} is safe by default with no typing), <name>-nsfw-only.txt is NSFW-only, and <name>-nsfw.group imports both — so {name} = SFW, {name-nsfw-only} = NSFW-only, {name-nsfw} = everything. Applied to danbooru general and rebuilt the groups (danbooru/danbooru-nsfw, d-keyword/d-keyword-nsfw, d/general-nsfw). Gating now targets the NSFW-bearing names; plain SFW names are ungated. Build script, listTags, README, and list-architecture updated. npm test green.

2026-06-20 — Lists: SFW/NSFW convention name=both, name-sfw, name-nsfw (2.1.0)

Finalized the split naming: <name>-sfw + <name>-nsfw exclusive files plus <name>.group importing both, so plain {name} = both, {name-sfw} = SFW-only, {name-nsfw} = NSFW-only. Applied to danbooru general (general-sfw / general-nsfw / general.group). Rewired groups, #danbooru, gatedLists, and the build script. npm test green.

2026-06-20 — Lists: exclusive SFW/NSFW + full-as-group (replaces d-sfw) (2.1.0)

Replaced the d/ + d-sfw/ duplication with the proper model: SFW and NSFW are exclusive lists and the full version is a group importing both, only for files that genuinely mix. Only danbooru/d/general qualified: split into d/general (SFW) + d/general-nsfw (NSFW) with d/general-all.group importing both. Rewired the danbooru groups + #danbooru + gatedLists (general ungated, general-nsfw/general-all gated); build script does the split on regenerate. Deleted d-sfw/. npm test green.

2026-06-20 — Lists: parallel audit + misfit cleanup (2.1.0)

Audited the curated descriptor lists and AI-classified proper-noun lists with 4 parallel review subagents, then applied fixes (fix-audit.mjs): 17 typos fixed, 47 garbage/abbreviation entries removed, 229 misfits relocated (surnames out of given-name into person, fictional names into work, culture-tagged gods out of mythological-creature into mythology, etc.). npm test green; leak scan only the intentional keeps.

2026-06-20 — Lists: preprocess danbooru SFW, drop runtime @filter (2.1.0)

Replaced the runtime @filter sfw (which stripped adult words live, incl. in the browser) with preprocessed SFW-only files: danbooru/d-sfw/* are split from the full danbooru/d/* at build time (split-danbooru-nsfw.mjs + the CSV build script), and danbooru-sfw.group reads them directly as a pure union. Removed @filter from the engine. Full danbooru stays gated; SFW is a real, separate, ungated list. npm test green.

2026-06-20 — Lists: .group files replace hardcoded virtual lists (2.1.0)

Composites are now plain .group files (each line a list reference, resolved by the same path-suffix lookup), not a hardcoded virtualLists object. resolveListLines reads <name>.group, unions its members recursively (MAX_GROUP_DEPTH=3 + cycle guard, de-duped), with an optional @filter sfw|nsfw directive. All three loaders walk .txt + .group. Group files (danbooru, danbooru-sfw, d-character, d-keyword, artist, artist-digipa, name) organized into their folders; gatedLists updated to the new canonical paths. npm test green.

2026-06-20 — Words: strict WordNet POS pass + one list per POS (2.1.0)

Validated the curated word lists against WordNet (reclassify-words.mjs): kept confirmed entries, routed 602 action gerunds to a new look/action, moved 450 cross-POS words to the correct list, kept WordNet-unknown in place. Then collapsed the dictionary lists into the curated ones — dict-adjective→ adjective, dict-noun→noun, dict-verb→verb, dict-adverb→adverb, dict-misc→word/misc — deleting the dict-* files and the redundant *-all virtuals (one list per POS; danbooru stays separate). npm test green; full spot-check wave clean.

2026-06-20 — Lists: spot-check + fix NSFW/misfit leaks (2.1.0)

Reviewed every list for wrong content (scan-leaks.mjs). Relocated 93 NSFW terms into gated look/clothes-adult and word/adult, removed 1 extreme term, pulled 26 danbooru expression/pose tags out of word/adjective into a new look/expression, removed 3 typos, and moved a stray anime title out of artist/anime. Gated lists added to gatedLists/listManifest. Legit false positives kept (real places, "Naked mole rat", "X-ray", "presenting", and dictionary words breast/butt/sex/oral/facial/naked/...). npm test green.

2026-06-20 — Lists: folder organization + path-suffix name resolution (2.1.0)

Organized all lists into folders (danbooru, artist, word, name, place, lore, nature, look, style, scene, brand, keyword) with a data/lists/README.md. New resolveName() resolves a reference by path suffix (bare filename / partial path / full path; shallowest wins, ties by a guaranteed natural order via compareNames() — symbols, numeric numbers, letters). Loaders walk data/lists recursively. Danbooru files nested under danbooru/d/ so old d-general becomes {d/general}. Basenames kept unique so existing {name} refs still resolve; only the danbooru d-* string refs updated. Updated gatedLists, listManifest unions/tags, and the CSV build scripts. npm test green.

2026-06-20 — Lists: proper-noun categorization, keyword.txt 8,859 -> 593 (2.1.0)

Split the remaining proper-noun dump in keyword.txt into category lists. Automatic first pass (split-proper.mjs: compromise + city.txt membership) extracted given-name, place, organization and de-duplicated confirmed cities into city.txt; the remaining ~4,422 were hand-classified individually (AI world-knowledge, 9 batch files under scripts/list-cleanup/cat/, distributed by an idempotent build-categories.mjs with a coverage check) into person, place, organization, mythology, astronomy, people-group, religion, history, work. keyword.txt 8,859 -> 593 (only the uncategorizable tail remains; nothing deleted — unclassified stays). New lists tagged in listManifest, plus a name virtual (given-name + person). Slurs found mid-pass (Jap/Negress/Negroid) added to contentSafety.js and purged. npm test green; safety scan 0.

2026-06-20 — Keyword lists: content-safety purge + full reorganization (2.1.0)

Big data-cleanup milestone across data/lists/ and the CSV sources. (1) Content safety: new browser-safe src/contentSafety.js defines a curated, list-type-aware blocklist (slurs, content sexualizing minors, extreme shock/gore/non-consensual) with whole-word matching and a Scunthorpe-safe exact mode for proper-noun lists. A one-time scan/purge removed 81 entries from the lists and 47 rows from danbooru.csv; the filter is wired into process-danbooru-csv.js / process-artists-csv.js so regeneration stays clean. Ordinary adult/nudity terms are kept and handled via the NSFW lexicon, not deleted. (2) Dictionary reorg: the 48,750-line keyword.txt SCOWL dump was sorted with compromise into dict-adjective/noun/verb/adverb/misc; possessives + redundant inflections + junk were dropped (2,250 lines), leaving keyword.txt as a 10,049-entry proper-noun list (48,750 = 46,500 sorted + 2,250 dropped, fully accounted). (3) Virtual lists: new src/listManifest.js adds composite lists computed on demand with cross-member de-dup and optional sfw/nsfw filtering. The duplicated files the build scripts used to emit (danbooru, d-keyword, d-character, artist, artist-digipa) are now virtual (physical files deleted, 6 artist orphans preserved into artist-special), plus new danbooru-sfw and adjective-all/noun-all/verb-all/adverb-all. Resolution wired into both engine loaders (nodeLoader, browserLoader) and the runtime store (helpers/listFiles.js). npm test green (0 lint errors; smoke loads the full graph and resolved a virtual {adverb-all}). Cleanup tooling lives under scripts/list-cleanup/. Done on branch cleanup/list-reorg for review.

2026-06-20 — Lists: WordNet-authoritative POS sort (2.1.0)

Replaced the guess-from-spelling dictionary sort with WordNet lookups (wordpos/wordnet-db dev dependency; scripts/list-cleanup/pos-dictionary.mjs). Each of the 48,750 SCOWL words is placed in the dict-* list(s) for the part(s) of speech WordNet actually assigns it (bond → noun + verb); capitalized noun-only words stay in keyword.txt as proper nouns (America/Paris/December), capitalized adjective/verb/ adverb words move out, demonyms → demonym.txt, and WordNet-unknown lowercase words → dict-misc. Final: keyword.txt 8,859 proper nouns; dict-noun 22,748, dict-adjective 7,947, dict-verb 6,171, dict-adverb 1,050, dict-misc 5,451, demonym 124 (48,750 conserved). Deleted the four superseded heuristic scripts. npm test green; safety re-scan 0.

2026-06-20 — Lists: keyword.txt second pass + dict-adverb fix (2.1.0)

Follow-up to the 2.1.0 reorg. Fixed a POS-sort bug (bare -ly was treated as an adverb, polluting dict-adverb with -ly nouns/names — now requires the #Adverb tag; 63 entries re-routed). Ran a second pass over keyword.txt: 1,232 entries whose lowercase form is a real dictionary word were moved into the dict-* lists (precise set-membership test), and 127 demonyms split into a new demonym.txt. keyword.txt is now 8,817 genuine proper nouns. demonym wired into listManifest (tags + adjective-all/noun-all). npm test green.

2026-06-20 — Composer: four icon-only field actions (2.0.8)

Owner: Save and Share should be icon-only action buttons in the field next to the others, not a merged text button — Save as a disk icon, Share as the standard share icon. Split the merged control back into two and made all four field actions a cohesive set of round icon buttons: Save (floppy disk), Share (three-node share glyph), Random (shuffle), Generate (sparkle, primary green) — all crisp inline SVGs using currentColor. Each of Save/Share toggles its own inline panel (save = name row, share = link row). State went back to panel: ""|"save"|"share". npm run lint 0 errors, web-app vite build green.

2026-06-20 — Composer: chat-style field with docked actions (2.0.7)

Owner: "still doesn't look like an app." Reworked the composer into a chat-style input field — a bordered container that lights up on focus (:focus-within), with a borderless textarea on top and an action bar docked along the bottom edge. The actions are now part of the field: Generate is an icon-only primary (green) round button in the bottom-right (✦), Random is a matching round icon button (🎲) right next to it, and Save + Share are merged into one "Save / Share" entry point on the bottom-left that opens a single combined panel (name-to-save row + share-link row). Removed the standalone toolbar, the separate Save/Share buttons, and the redundant close ✕ (the toggle closes the panel). CSS + Home.jsx only. npm run lint 0 errors, web-app vite build green.

2026-06-20 — Composer: size the prompt box, don't fill the pane (2.0.6)

Quick follow-up to 2.0.5: the editor-fill layout over-stretched the textarea to the full column height, which looked silly. Made the composer size to its content instead — a comfortable min-height: 8.5rem prompt box that's vertically resizable (capped at 50vh), with the toolbar directly beneath it and the results card flowing below; the column scrolls if it gets tall. CSS-only. web-app vite build green.

2026-06-20 — Composer redesign: editor-fill layout, compact toolbar, anime toggle removed (2.0.5)

Acted on owner feedback that the middle of the SPA still felt clunky and "not like an app." Rebuilt the composer right pane as an editor that fills its space: the prompt textarea now stretches to fill the card (no more big empty gap below a short box; a hover clears it), and a compact action toolbar pins to the bottom — a prominent Generate, a 🎲 Random, and smaller secondary Save / Share buttons. Save expansion moved off its own card into an inline panel triggered from the toolbar (it shares the same inline-panel pattern as Share). When prompts are generated, the results card splits the column with the composer and scrolls internally. Removed the Normal/Anime "Style" toggle: the "Anime" lists (d-keyword.txt, a Danbooru tag dump) silently mix SFW with explicit adult tags, so there was no way to get anime without adult — pulled pending a proper SFW/adult split of the word lists (see plans/removed-pending-readd.md). loadSettings now migrates any browser stuck on d-keyword/d-artist back to the safe keyword/artist defaults. Verified: npm run lint 0 errors, web-app vite build green. (SPA-only; the separate in-progress list-gating work in the tree — src/gatedLists.js, data/lists/keyword-adult.txt — was left untouched.)

2026-06-19 — App-frame layout + simpler prompt component (2.0.4)

Continued the home-page refinement from owner feedback ("feel more like an app", "the prompt component is clunky/cluttered"). Turned the centered max-width page into a full-bleed app window (100vh: title bar, full-height bordered panels, slim footer status bar) and widened the left panel. Replaced the building-blocks accordion with an app-style category tab nav + chip area for the active category (no <details>). Rebuilt the prompt component to be minimal: the textarea's rotating placeholder now is the random suggestion (cycles every 5s); Random just drops the current suggestion into the box; Generate uses whatever is typed (falling back to the suggestion when empty). Removed Preview (owner: "needs to go" — it was a redundant expand-to-preview). Kept the Normal/Anime word-list switch but relabeled it Style with a ? help tooltip explaining it. Reworked Share link (owner: "make it visually and functionally work and better"): clicking it now reveals the link in a selectable field with a Copy button and a clear "✓ Copied" state, and the link stays visible so it works even when the clipboard API is blocked. Verified: npm run lint 0 errors, npm run smoke green, web-app vite build green.

2026-06-19 — Refine the SPA home page (2.0.3): declutter + two-pane layout

Acted on owner feedback to make the home page look better and less cliché. Dropped the made-up tagline and the centered hero (logo → title → tagline) — the logo + wordmark now live only in a slim top-bar. Swapped the display font from Rokkitt (read as Impact-like) to Space Grotesk. Moved the building-block cloud into a sticky left pane (Home.jsx is now a two-pane .workspace grid that collapses to one column ≤860px) and re-added the rotating random suggestion (a fresh #random prompt cycling every 5s, click to fill). Removed, for now, image generation, the chaos knob, presets (apply + save), the Settings button/drawer, and the local/online mode badge — all tracked for re-add in notes/plans/removed-pending-readd.md (presets to return richer: full settings + auto-generation). Verified: npm run lint 0 errors, npm run smoke green, web-app vite build green.

2026-06-19 — Redesign the SPA home page (2.0.2): unified composer + brand restyle

Reworked the React SPA from a cramped light-themed single "Build" tab into a polished home page modeled on the pre-revival generate screen. Restyled styles.css to the brand: dark charcoal canvas, mint-green accent (the pencil logo), Rokkitt display + Maven Pro body via Google Fonts, pill inputs, rounded buttons, a centered hero with the app icon. Merged the separate Build and Generate flows into one Home.jsx composer (prompt textarea + primary actions, Normal/Anime segmented toggle, chaos, presets, live preview, generated-prompts list with copy, in-session gallery, save expansion/preset, and a collapsible building-blocks cloud). Tucked the full settings form into a new right-side SettingsDrawer.jsx (overlay + Esc to close) so the page stays uncluttered, and reworked App.jsx into a brand top-bar (logo, local/online badge, Settings button) + hero + Home + footer. Added web-app/public/ with the logo + favicons. Removed the superseded Builder.jsx / Generate.jsx (folded into Home). Verified: npm run lint 0 errors, npm run smoke green, web-app vite build green, and the page rendered + screenshot-checked in a browser.

2026-06-18 — Fill out the notes + ignore the whole generated /docs/ tree

Completed the notes: wrote the historical changelog for the 2022–2023 original build (version/2022-12.md2023-04.md, reconstructed by theme from the full git log) and indexed it in version.md; closed the remaining June-2026 changelog gaps (the src/+data/ reorg / 2.0.1, the Build-tab-only UI, Vite 6→8, the lockfile + lodash-pin fixes, deployments-held, the DSL/history + block-catalog doc commits, the smoke-test.mjs @file); and refreshed status.md, plans/next-steps.md, and decisions/architecture.md to the current state (all four revival strands + three doc-tooling decisions). Also broadened the .gitignore from /docs/jsdoc/ to all of /docs/ and removed the stale Doxygen docs/html/ output left over from the retirement — the entire /docs/ tree is generated now.

2026-06-18 — Document the web-app React SPA (JSX wired into the JSDoc site)

Closed the last gap: the web-app/ React SPA. Added @module + per-function JSDoc to all 16 SPA files (lib, providers, the 8 React components, App, main, the Netlify function). JSDoc can't parse JSX, so build-docs.mjs now babel-transpiles web-app/src + netlify into a tmp/webapp-docs mirror (JSX stripped, comments kept) that JSDoc reads; @module tags give clean nav names. Fixed the JSDoc source.excludePattern (a generic /lib/ was excluding web-app/src/lib), added tmp/** to the ESLint ignores, and simplified a tuple-type @returns. Every authored file in the repo is now documented in one JSDoc site (~244 pages). npm run docs exit 0; lint 0 errors, smoke green, SPA build green.

2026-06-18 — Add @file to scripts/smoke-test.mjs

Closed the last file-header gap: added an @file header to scripts/smoke-test.mjs, so every authored file under src/, data/, and scripts/ now has one. Comment-only.

2026-06-18 — Doc-site: replace Doxygen with a single JSDoc + docdash site (notes wired in)

Retired Doxygen here in favour of one generator. npm run docs now runs scripts/build-docs.mjs, which wires the whole notes/ tree (+ list-credits / list-help / Upgrade-2-0) into JSDoc tutorials with a hierarchy mirroring the old _nav.dox, and rewrites inter-note links to resolve — then runs JSDoc with the docdash template. The result is one site (docs/jsdoc/) with the README home, the per-function code API, and the living notes, plus a sidebar + search. Removed Doxyfile, docs/doxygen-awesome/, and notes/_nav.dox; switched pages.yml + release.yml to build the JSDoc site; updated documentation.md, deployment.md, fix-patterns.md, notes/README.md, and CLAUDE.md. Builds clean; lint/format/smoke green.

2026-06-18 — Per-function JSDoc on the frontend scripts (docs now complete everywhere)

Closed the last gap: added per-function JSDoc to every top-level function in the 8 web/frontend/* browser scripts (84 functions) via a UTF-8-safe, idempotent script (inferred param types, humanized descriptions, value-return detection). With this, every named function in the codebase is documented — server-side, all 113 blocks, and the frontend; only anonymous callbacks (route arrows, jQuery closures) are left, which no doc generator can extract. JSDoc builds clean (170 pages); lint/format/smoke green. Comment-only.

2026-06-18 — Per-function JSDoc: full server-side API + all 113 blocks

Took the JSDoc coverage from file-level to per-function across the whole server side: @param / @returns / descriptions on every function in the prompt engine (prompt-modules, helpers, core/), the variation/reroll/upscale/animation loaders, the settings loaders, common (batch loop), genImg, promptFilesAndSuggestions, the server.js helpers, and the self-healing image index — plus a uniform documented contract on all 113 block generators (parsed each one's real parameter list). These now extract as a true per-function API in docs/jsdoc/. The legacy web/frontend/* browser scripts stay file-level on purpose (jQuery client being retired). Comment-only; no runtime change. (Also: restored 3 files' em-dashes that an earlier ANSI-vs-UTF-8 PowerShell round-trip had mojibake'd, and fixed an invalid JSDoc optional-property type the parser rejected.)

2026-06-18 — Add JSDoc (the tool) for an ESM-native code API

Wired up JSDoc 4 (npm run docs:apidocs/jsdoc/, config jsdoc.config.json) alongside Doxygen, reusing the @file headers already in the source. JSDoc parses ESM / export default where Doxygen cannot, and renders a page per file with README.md as the homepage. Division of labour: Doxygen hosts the living-notes site + GitHub Pages; JSDoc builds the code API. File-level today (every file has an @file header); per-function @param/@returns on the named exports is a deliberate future pass. See reference/documentation.md.

2026-06-18 — Comprehensive file-level Doxygen docs: @file on every authored JS

Added a /** @file @brief */ header to every authored JavaScript file — 165 under src/ (44 with richer, notes-linked module headers; 113 blocks; 8 frontend scripts) plus the 3 data/process-*.js build scripts — so the Doxygen File List describes every file. Doxygen can't extract this code's symbols (the generators are anonymous export default function), so coverage is deliberately file-level plus the conceptual notes pages, not per-function — recorded in reference/documentation.md. Also cut the Doxygen build warnings 7 → 3 (rest benign) by fixing escapes/anchors and an inline-code-span-wrapped-across-a-newline quirk in esm-patterns.md. Comment/docs only — no runtime change.

2026-06-18 — Exclude assets/ from ESLint, Prettier, and Doxygen

The pinned assets/references/ source snapshot is gitignored, but the tools walk the filesystem and were indexing it (569 phantom lint errors from the old CommonJS/jQuery clone; a polluted doc-site). Excluded assets/ in eslint.config.js, .prettierignore, and Doxyfile. Gitignored ≠ tool-ignored — see reference/fix-patterns.md.

2026-06-18 — Document the block catalog + the image deep-link graph

Notes-only. Added reference/blocks.md — a catalog of all 113 block generators (full vs partial, the _-core/user-/v1 conventions, how #random / #extra-random-prompt discover and weight them) — and documented the image deep-link graph (how a saved image's JSON encodes its command + original settings so re-rolls, variations, upscales, and animation frames link back to their parent). No code change.

2026-06-18 — Verify original-vs-current intactness (session log)

Notes-only. Recorded in the session log the full verification that the 2022–2023 prompt logic survived the CommonJS→ESM migration intact — an inventory diff of the original tree against src//data/, plus a logic-parity spot-check beyond the mechanical requireimport changes. Nothing was lost; the differences are module-system and file-location only.

2026-06-18 — Document the original prompt DSL + enrich pre-revival history

Notes-only. Wrote reference/prompt-dsl.md (the sigils the engine understands — <expansion>, #block, {list}, {salt}, AND-stacking, per-engine emphasis/editing/alternating) and enriched context/history.md with the 2022–2023 build arc, so the project's own language and origins are written down rather than living only in the code and the git log.

2026-06-18 — Record that deployments are intentionally held

Notes-only. Documented that master is deliberately held at the last pre-revival commit (241a148) and that GitHub Releases / Pages deploys are paused on purpose — the rewrite is too early to ship. This is a status decision, not a bug: work continues on dev. See ../status.md and ../reference/deployment.md.

2026-06-18 — Pin lodash to the SPA copy so the Vite 8 / Rolldown build resolves it

Build fix. The repo-root core/ engine and every block import _ from "lodash", but they live outside web-app/, so resolution from them wouldn't find the SPA's lodash — and Vite 8 / Rolldown treats an unresolved import as a hard error (Vite 6 / Rollup only warned). web-app/vite.config.js now aliases lodash to the SPA's own installed copy (and dedupes it), so the build is self-contained and works on CI and Netlify, neither of which installs the repo-root node_modules.

2026-06-18 — Regenerate the lockfile with cross-platform optional deps for CI npm ci

Build fix. The web-app lockfile was missing the platform-specific optional dependencies (Rolldown/SWC native binaries) that npm ci needs on CI's Linux runners, so a clean install failed. Regenerated it to include all platforms' optional deps.

2026-06-18 — Upgrade Vite 6 → 8 and @vitejs/plugin-react 4 → 6

Took the SPA's build tooling to current majors: Vite 8 (Rolldown-based) and @vitejs/plugin-react 6. This is what surfaced the stricter unresolved-import handling addressed by the lodash-pinning and lockfile fixes above.

2026-06-18 — Show only the Build tab while the UI is reworked

Temporarily hid the Generate and Settings tabs in the SPA, leaving only Build, while that part of the UI is reworked — so the in-progress tabs aren't exposed. A presentation-only change; the underlying code stays in place.

2026-06-18 — Reorganize into src/ + data/; modernize toolchain; add CI/docs/notes (2.0.1)

Tidied the repo's shape after the ESM migration: all code moved under src/ and all prompt content (lists, expansions, presets, the CSV sources) under data/, with runtime/user data (output/, user-settings.json, results.json) staying at the repo root. This is the layout the project keeps today and the reason src/chdir.js pins the cwd to the repo root (its parent). Bumped to 2.0.1 (VERSION + package.json in sync). Also rounded out the toolchain and CI/docs/notes scaffolding around the new layout.

2026-06-18 — Prompt Builder power tools: blocks cloud, share links, custom expansions/presets, chaos (phase 4b)

Brought the rest of the original generate screen's prompt-building power into the SPA after a closer look at web/views/generate.pug + generate.js. The Builder now has a categorized building-blocks cloud (Full / Partial blocks, Expansions, Lists, User, V1, and a Special section with {salt}) with Title-Case labels and a single search — clicking inserts the token. Added Share Link (encodes the current settings + prompt into a URL hash, base64, API keys excluded; a shared link seeds settings on load), Save as Expansion and Save Preset stored in localStorage (the no-server equivalents — custom expansions are usable as <name> and are merged into the engine via a composite loader), a Chaos control (scales emphasis/alternating like the CLI's --chaos), and Anime / Normal quick toggles (swap the danbooru vs normal keyword+artist lists).

Refactor: the lib facade (promptEngine.js) now owns the composite loader, the categorized blocks, and the preset list (built-in + custom); catalog.js is a thin re-export. The core engine and CLI were left untouched (chaos is applied in the web facade). Build verified.

2026-06-18 — Web SPA features: Settings editor, Prompt Builder, Generate gallery (phase 4)

Built out the SPA beyond the shell. Three tabs now: Generate (build prompt(s) client-side, run them through the selected provider, in-session image gallery with per-image download), Build (a prompt builder — searchable pickers to insert blocks #, lists {}, and expansions <>; apply a preset; "surprise me" #random; and a live expansion preview), and a full Settings editor exposing every prompt knob (counts, source lists, emphasis, editing, alternating, artists, auto-fx, salt, mode) plus image params and the provider/key config — all persisted to localStorage.

Mechanics: browserLoader now also bundles presets/*.json; a new lib/catalog.js turns the loader's catalog into insertable tokens; expandPrompt() drives the preview. The UI was restructured from one file into focused components (Field, Settings, Builder, Generate, TokenPicker, Gallery) with a small tab router. Build verified (268 modules).

Intentionally out of scope (they need server storage the app deliberately doesn't have): the old persistent image library/feed, search index, single-image stats, delete/download-from-disk, ImageMagick conversions, and animation file handling.

2026-06-18 — Browser-safe prompt-engine core (web migration, phase 3)

Ported the real prompt engine to run in the browser. Added a framework-agnostic core/ — an engine (createEngine(loader)) that runs the full prompt-module pipeline (expansion, block with danbooru/auto-fx, salt, list with emphasis/editing/alternating, cleanup) over a prompt, taking all data through an injected loader. Two loaders implement it: nodeLoader (fs + createRequire) and browserLoader (Vite import.meta.glob, which bundles all 113 blocks + the lists/expansions text). The pure stages (prompt-salt, cleanup) and the random* helpers are reused directly, so there is no duplicated prompt logic.

To make the chain browser-safe: the two built-in list aliases were split into a tiny dependency-free helpers/aliases.js so keywordRepeater no longer imports the fs-backed listFiles; and src/promptFilesAndSuggestions.js (the #random suggestion builder, reached via extra-random-prompt → random-prompt) was refactored to be loader-injected instead of using fs/createRequire, so it now runs in both Node and the browser (a single shared implementation). It's configured once per host — Node in common.js (covers CLI + server), browser in the SPA's engine wiring. A latent bug was fixed in passing: one accumulator (fullRegularExcluded) wasn't reset between scans, so the suggestion pool grew on every #random call.

The SPA's placeholder engine is gone — it now runs the real one. Verified with a Node engine smoke (#random, #simple-random-prompt, #extra-random-prompt, #beach all expand fully, no leftover tokens), the existing Node CLI smoke, and a green web-app build (235 modules bundled). Known follow-up: the eager glob bundles the list/expansion text into the JS (~712 KB gzipped); moving the larger lists to runtime fetch is a size optimization for later.

2026-06-18 — Scaffold the React + Vite web app (online + local migration, phase 1)

Began the move to a React + Vite SPA usable online (bring-your-own-key) or locally. Added web-app/ (Vite + React 19) with a localStorage-backed settings layer, a modular image-provider interface (a local-WebUI provider that calls the user's own WebUI directly, and a hosted-proxy provider), a stateless Netlify proxy function stub (BYOK; stores/logs nothing), and netlify.toml. The prompt engine is a placeholder pending the phase-3 browser-safe core port. Build verified (vite build green, ~63 KB gzipped). Design + phased plan in notes/plans/web-migration.md; rationale in notes/decisions/architecture.md.

2026-06-18 — 2.0.0: ES-module + Node 24 modernization

Moved the entire project off CommonJS and onto ES modules ("type": "module"), targeting Node 24 LTS. About 130 files converted from require/module.exports to import/export:

  • The ~113 block files and the simple helpers/prompt-modules were converted mechanically to export default function (+ export const full / suggestion_exclude where present).
  • helpers/keywordRepeater.js became named exports; helpers/listFiles.js stayed a default-export object (it's indexed dynamically). Settings files became export default { … }.
  • The entry points (index.js, server.js, common.js) and the src/ loaders were hand-converted. Dynamic JSON requires became JSON.parse(fs.readFileSync(...)); the config-driven plugin loaders (blocks, prompt modules) kept synchronous loading via createRequire(import.meta.url) — Node 24 can require() ES modules — calling .default(...) on the namespace.
  • New chdir.js is imported first by common.js so process.chdir(import.meta.dirname) runs before any settings module reads a cwd-relative file (ES-module import ordering made this necessary; the old inline process.chdir(__dirname) would otherwise have run after the settings import).

Dependencies were taken to current majors: Express 5, yargs 18, open 11, cli-progress 3, crc 4, compromise 14, lodash 4, pug 3. node-fetch was removed — Node 24's global fetch replaces it in genImg.js, imageUpscaler.js, and server.js.

Tooling added: ESLint 9 (flat config, separate Node-ESM and browser-script configs), Prettier 3, .editorconfig, .nvmrc (24), engines.node >= 24, and npm scripts (start, server/webui, lint, format). A repo-wide Prettier pass reformatted the code.

Verified with node --check (152 files, 0 syntax errors), npm run lint (0 errors), and an import smoke test that loads the full module graph + all blocks and expands a prompt. Live image generation still needs a running Stable Diffusion WebUI and was not exercised.

2026-06-18 — Set up the CLAUDE.md + notes AI-collaboration system

Added a root CLAUDE.md and a full notes/ tree (status, changelog, sessions, context, systems, reference, decisions, plans), modeled on a sibling project, plus a VERSION single-source-of-truth. This is documentation scaffolding so any AI or human can orient on the repo cold; it carries no code change.