Changelog — June 2026
Newest entry on top.
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 exception — playwright.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/dynPromptManifest.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 danbooru→d,
random→random-words, and the -prompt suffix dropped (random-prompt→random,
simple-random-prompt→simple-random, extra-random-prompt→extra-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 dynamic prompts 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
dynPromptGroupDirs/readDynPromptGroup 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 "Dynamic prompts"
(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 — Dynamic prompts: {#name} sigil + gating/wildcard + uniform SPA (2.4.0)
Round 2 of the dynamic-prompt standards pass. Sigil: dynamic prompts 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: isGatedDynPrompt 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/dynPromptManifest.js
adds a dynPromptTags map. SPA: the four behavior blocks collapsed into one uniform Dynamic prompts 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 — Dynamic prompts: parity with lists/expansions + v2/ reorg (2.3.0)
Brought data/dynamic-prompts/ 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/dynamicPrompt.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 readDynPromptMeta / dynPromptForcedPrefixDirs / _-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/dynamic-prompt.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/dynamic-prompts-architecture.md.
2026-06-21 — Moved dynamic-prompts/ from src/ to data/ (2.2.2)
The #name dynamic-prompt generators now live under data/dynamic-prompts/ instead of src/dynamic-prompts/
— 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/dynamic-prompt.js (require prefixed ../../data/), core/nodeLoader.js
(rootDir/data/dynamic-prompts), and core/browserLoader.js (glob ../../data/dynamic-prompts/**/*.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-detail→detail/legacy and detail/legacy-person-detail→detail/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-style→general,
added .force-prefix to artist/scene/style) and fixed the dynamic-prompt 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.txt→keyword-sfw.txt and look/clothes.txt→clothes-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-keyword→d/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.md
→ 2023-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 + dynamic-prompt-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 dynamic prompts, 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 dynamic prompts
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 dynamic-prompt 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:api → docs/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 dynamic-prompts; 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 dynamic-prompt catalog + the image deep-link graph
Notes-only. Added reference/dynamic-prompts.md — a catalog of all 113
dynamic-prompt 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 require→import 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>, #dynamic-prompt, {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 dynamic prompt 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 dynamic prompts, 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 dynamic prompts #, 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, dynamic-prompt 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 dynamic prompts + 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 dynamic-prompt files and the simple helpers/prompt-modules were converted mechanically to
export default function(+export const full/suggestion_excludewhere present). helpers/keywordRepeater.jsbecame named exports;helpers/listFiles.jsstayed a default-export object (it's indexed dynamically). Settings files becameexport default { … }.- The entry points (
index.js,server.js,common.js) and thesrc/loaders were hand-converted. Dynamic JSONrequires becameJSON.parse(fs.readFileSync(...)); the config-driven plugin loaders (dynamic prompts, prompt modules) kept synchronous loading viacreateRequire(import.meta.url)— Node 24 canrequire()ES modules — calling.default(...)on the namespace. - New
chdir.jsis imported first bycommon.jssoprocess.chdir(import.meta.dirname)runs before any settings module reads a cwd-relative file (ES-module import ordering made this necessary; the old inlineprocess.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 dynamic prompts 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.