2026-06-20
Newest entry on top.
SPA Lists panel: folder categories + alphabetical (2.1.0)
Owner: group the SPA "Lists" cloud by folder with a category pill before each folder's entries (categories +
entries alphabetical). The pill label is the folder's LAST segment; tooltip is the folder's <folder>.json
description. When the folder is an implied group, the pill is itself clickable and inserts the whole-folder
group ({word}, {d}, …) — merging the header and the group chip. Categories are inline pills (no line breaks;
one wrapping array). Implemented in promptEngine.getBlocks (Lists items now carry {category:true,...}
markers; folder-group names removed as separate entries), Home.jsx (category-aware render + filter + count),
and .cat-pill CSS. Also removed scene/_force-prefix per owner (scene basenames don't collide, so no prefix
needed). npm test + vite build green.
Markers: dotfiles → _-prefixed regular files (drop the Vite plugin) (2.1.0)
Owner's better idea: instead of working around Vite's dotfile-blindness with a plugin, rename the markers
from dotfiles to _-prefixed REGULAR files — import.meta.glob sees those fine. Established the convention:
any file starting with _ is internal/config, never a list (loaders skip _-prefixed names). Renamed
.force-prefix→_force-prefix (artist, danbooru/d, lore, name, scene, style) and .disable-group-list→
_disable-group-list (place). Reverted the list-markers Vite plugin + virtual:list-markers import;
browserLoader globs **/_force-prefix etc. again. Gotcha: marker files must be EMPTY (an eager glob parses
each matched file as a JS module; an extension-less file with a # comment is a parse error, but an empty
file is a valid empty module) — emptied all markers. Loaders skip _-prefixed files in the list walks/globs.
Verified: SPA bundle carries the 6 _force-prefix dirs; node forcedPrefixDirs/groupListDirs correct
(place excluded); npm test + vite build green.
Fix: force-prefix/group markers invisible in the SPA (Vite skips dotfiles) (2.1.0)
Owner reported force-prefix not working on the UI buttons. Root cause: Vite's import.meta.glob silently
SKIPS dotfiles, so the browserLoader's **/.force-prefix (and .enable/.disable-group-list) globs matched
NOTHING — forcedPrefixDirs() was [] in the browser (worked in node, masking it). Confirmed: the built
bundle had zero marker path strings. Fix: a list-markers Vite plugin in web-app/vite.config.js fs-scans
data/lists for the three dotfile markers and exposes them as a virtual:list-markers module
(forcePrefixDirs/enableGroupDirs/disableGroupDirs), with dev-server reload on marker add/unlink.
browserLoader now imports that instead of globbing dotfiles. Verified the bundle now carries
["artist","danbooru/d","lore","name","scene","style"]; npm test + vite build green. (node loaders were
always fine — they fs-walk.)
Implied groups go automatic (2+ files); enable/disable markers (2.1.0)
Owner changed the group-list rule: a folder with 2+ direct list files is automatically an implied group
(no marker). Markers become OVERRIDES — .enable-group-list (force on) / .disable-group-list (force off);
.force-group-list is retired and all three markers removed (artist/, danbooru/d/, name/). Does NOT stack:
only a folder's own direct lists count, never subfolders. New autoGroupListDirs(listNames, enable, disable)
in listManifest (counts distinct base lists per dir, variants collapsed); impliedGroupMembers now direct-
children-only; all three loaders compute group dirs from .txt-only logical names + the marker overrides.
Owner also added .force-prefix to lore/ and name/. Implied groups now: artist, danbooru/d, look, lore,
name, nature, place, scene, style, word (brand has 1 list → not a group). The auto-prefixer
(computeButtonNames) already disambiguates the group folder-names against same-named lists (e.g. {place}
group vs {place/place} list → list shows place/place). Added group-level description JSON for the new
implied groups (86 meta files total). Verified via nodeLoader: 10 group dirs, 0 round-trip failures of 88,
{word}/{place}/etc. resolve. npm test + vite build green.
JSON list metadata sidecars (descriptions / tooltips) (2.1.0)
Added optional <list>.json sidecars ({ description }) for editor button tooltips (owner's ask). Loaders read
them: readListMeta(name) on nodeLoader (fs) + browserLoader (import.meta.glob **/*.json); the SPA's
getBlocks attaches item.description (fallback name → name-sfw for implicit bases / mixed lists) and
Home.jsx shows it in the chip title. Authored 79 concise descriptions for every project list, subset group,
and implied group via scripts/list-cleanup/write-list-meta.mjs (re-runnable). JSON files are ignored as
lists (loaders only read .txt/.group). npm test + vite build (342 modules) green.
Verify owner's list refactor + .force-group-list implied groups (2.1.0)
Owner manually refactored lists (uncommitted): dropped redundant folder-name suffixes (general-style→
general, anime-name→anime, shed-type→shed, artist-digipa→digipa, etc.), added .force-prefix
to artist/, scene/, style/, and moved the "all artists" group up to a root artist.group. Reviewed it
and FIXED the breakage it introduced: 18 dynamic-prompt files still referenced the old tokens
({building-style}→{style/building}, {construct-style}→{style/construct}, {general-style}→
{style/general}, {ship-type}→{scene/ship}, {vehicle-type}→{scene/vehicle}, {store-type}→
{scene/store}); fixed via a node pass (PowerShell Set-Content silently no-op'd). Updated listTags keys
(name/anime-name→name/anime, name/given-name→name/given).
Then implemented .force-group-list (owner's idea): a folder with that empty marker becomes an implied group
({folder} = union of its lists, mode-aware) — no .group file needed. markedDirs/groupListDirs() in
all three loaders; resolveListLines synthesizes members via impliedGroupMembers; loader listNames()
registers the dir names. Added markers to artist/, danbooru/d/, name/ and REMOVED the now-redundant
artist.group, danbooru/d.group, name/name.group. Kept the genuine SUBSET groups artist/digipa.group,
danbooru/d/character.group, danbooru/d/keyword.group. Verified via nodeLoader: groupListDirs=
[artist, danbooru/d, name]; {artist} 3243/3294 (nudity gated), {d} 12399/12938, {name} 3544, {digipa}
897, renamed style/scene lists resolve, 0 round-trip failures of 81, buttons correct. npm test + vite build green.
Move d.group up to danbooru/d.group → buttons as {d} not {d/d} (2.1.0)
Owner: d.group doesn't need the forced d/ prefix — move it out of the danbooru/d/ folder up to
danbooru/d.group so it auto-names to just {d} instead of {d/d}. git mv + updated the listTags key
danbooru/d/d → danbooru/d. Verified: button for danbooru/d = d, {d} resolves to danbooru/d
(12,393 off / 12,932 on), 0 round-trip failures; danbooru buttons now d, d/artist, d/character, d/general, …. npm test + vite build green.
Editor button names: filename-only by default + .force-prefix (2.1.0)
Owner: editor list buttons should show just the filename unless there's a conflict or a folder is marked
required-prefix via an empty .force-prefix file (manual prefix happens first and is excluded from the
auto-prefix table; auto starts at the bare filename and steps out folder-by-folder on conflict; the marker
inherits to all descendants). Implemented computeButtonNames(names, forcedDirs) in listManifest.js
(manual+auto stages + a resolveName round-trip guarantee), added forcedPrefixDirs() to nodeLoader (fs walk)
and browserLoader (import.meta.glob("**/.force-prefix") — Vite bundles the dotfile, confirmed in dist),
created data/lists/danbooru/d/.force-prefix, and wired the SPA's getBlocks "Lists" cloud to the display
tokens. Verified via nodeLoader: forcedPrefixDirs=["danbooru/d"], 0 round-trip failures of 81, danbooru →
d/general etc., all others → bare filename (no conflicts). npm test + vite build green. (Note: the
whole-danbooru group then showed as {d/d} — later moved up to danbooru/d.group so it buttons as {d},
see the newer entry above.)
keyword becomes a reserved wildcard (any-loaded-word) (2.1.0)
Owner: keyword shouldn't be a file — make it a reserved word (like the nsfw filename token) that
supersedes any file named keyword (silently, no error) and resolves to a random word from ANY loaded list,
mode-aware. Implemented RESERVED_WILDCARD="keyword" + isReservedWildcard in listManifest.js:
resolveName short-circuits it (never suffix-matches danbooru/d/keyword); resolveListLines unions all
non-artist, non-danbooru list vocabulary per variant — {keyword} = SFW off / +NSFW on, {keyword-sfw} =
SFW always, {keyword-nsfw} = full (gated, hidden off). pickerListNames offers the reserved buttons
(mode-aware). Relocated the keyword/ files first: 20 languages → word/language.txt; keyword-nsfw.txt
adult vocab merged into word/adult-nsfw.txt (→88); deleted the keyword/ folder. Verified: {keyword}
74,584 off / 74,725 on, {keyword-sfw} 74,584, {keyword-nsfw} 0 off / 74,725 on; languages present;
danbooru/artist excluded; keyword still the default keywordsFilename. npm test green.
Second keyword pass: relocate leftovers, drop junk (593 -> 20) (2.1.0)
Owner flagged keyword as a junk drawer (cities, gods, breeds, etc. in the wrong place). Did a second
hand-classification of the 593-entry remainder via scripts/list-cleanup/reclassify-keyword.mjs (explicit
decision per entry, coverage assertion: 593 = 354 relocated + 20 kept + 219 dropped). Relocated to proper
lists (animal +25, word/noun +183→+111 after dedup, word/adjective +43, name/person +22, lore/history +16,
place/place +12, lore/religion +11, look/time +15 [months/weekdays], style/art-movement +5, lore/mythology +7,
nature/animal breeds & dinosaurs, etc.); dropped 219 junk (chemical symbols, 2-4 letter abbreviations,
inflected artifacts like TELNETTed/Photostatting, fragments, units); kept 20 languages/scripts (Sanskrit,
Urdu, Yiddish, …) with no dedicated list. npm test green. OPEN: keyword is the default keywordsFilename /
{keyword} alias — now thin (20 langs); needs repointing to a richer source (asked owner).
Fix incompatible base.txt+base-nsfw.txt pairs (keyword, clothes) (2.1.0)
Found two lists still stored as a plain base.txt next to base-nsfw.txt, which the safety rule renders
broken: the plain file is ignored, the implicit {base} button disappears, and — for keyword — the
DEFAULT {keyword} alias was silently resolving to danbooru/d/keyword (shallower match won once
keyword/keyword was dropped from the index). Fixed by renaming to the explicit SFW name:
keyword/keyword.txt→keyword-sfw.txt, look/clothes.txt→clothes-sfw.txt. Now {keyword} = 594 off /
653 on (correct leftover list, not danbooru), {clothes} = 972 off / 1030 on, and the picker shows the
implicit base button in both modes (plus -sfw/-nsfw when adult on). Updated fix-leaks.mjs route source
(look/clothes→look/clothes-sfw). npm test green.
Move danbooru groups into d/ folder ({d}, {d/keyword}, {d/character}) (2.1.0)
Owner: keep the danbooru group files within the {d/...} typed convention. Moved
danbooru/danbooru.group→danbooru/d/d.group (ref {d}), d-character.group→d/character.group
({d/character}), d-keyword.group→d/keyword.group ({d/keyword}). The bare {danbooru} reference is
retired in favor of {d}. Updated all active refs: {d-character}→{d/character} in danbooru.js, entity.js
and the 7 v1 prompts; applyArgs anime keywordsFilename d-keyword→d/keyword; the anime-detection in
prompt-modules/dynamic-prompt.js + core/stages/dynamicPrompt.js now tests startsWith("d/") (was d-);
web-app settings migration matches both legacy d-keyword and new d/keyword; listTags keys re-canonicalized
(danbooru/d/d, danbooru/d/keyword, danbooru/d/character). README + list-architecture updated. Verified
via nodeLoader: {d} 12393 off / 12932 on, {d-nsfw} 0/12932, {d/keyword} 11193/11732, {d/character}
3840, legacy {danbooru} now 0. npm test green. (Historical one-off cleanup scripts under
scripts/list-cleanup/ still name the old flat lists; left as-is.)
SFW/NSFW safety rule: plain name.txt ignored when name-nsfw.txt exists (2.1.0)
Owner added a safety precaution: if <name>-nsfw.txt exists, a plain <name>.txt is flat ignored
(never loaded, never listed) — the SFW half must be <name>-sfw.txt. A lone <name>.txt next to a
-nsfw file is therefore treated as NSFW-only (no accompanying SFW). Enforced in both readSfwBase
(reads <base>-sfw only when a <base>-nsfw sibling exists, never the plain file) and logicalListNames
(a plain file with a -nsfw sibling is dropped from the reference set; only <base>-sfw counts as an SFW
source). Verified with a synthetic harness: plain foo.txt + foo-nsfw.txt → logical ["foo-nsfw"],
{foo} off="" / on=NSFW-only (plain content never leaks). npm test green.
SFW/NSFW redesign: filename-token gating + mode-aware auto-combine (2.1.0)
Owner redesigned the whole SFW/NSFW model (supersedes the group-file scheme from earlier today). New rules:
(1) NSFW is keyed off the filename — any name with an nsfw token (word delimited by / - . _ or
string ends) is adult; while adult is off the app acts as if it doesn't exist (not listed, not suggested,
resolves to nothing). (2) A mixed list is two files <name>-sfw.txt + <name>-nsfw.txt with no
<name>.txt — the bare {name} is implicit. (3) The resolver auto-combines by mode, no .group files
for the split: {name} = SFW off / SFW+NSFW on; {name-sfw} = SFW only; {name-nsfw} = nothing off /
SFW+NSFW on (SFW base auto-tacked on — confirmed: no NSFW-exclusive list for now). Variant suffixes also
work on groups ({danbooru-sfw}/{danbooru-nsfw}).
Implemented: gatedLists.js → automatic isGatedList via NSFW_TOKEN regex (dropped the hardcoded array;
gatedDynPrompts emptied, #danbooru ungated since it now draws the mode-aware d/general).
listManifest.js → resolveListLines(name, readers, includeAdult, forced) with variant propagation into
group members + base re-resolution (so {danbooru-sfw} maps to the group path); new logicalListNames()
derives the implicit bare name + -sfw/-nsfw refs from the physical -sfw/-nsfw files; hasVariantSuffix.
Threaded includeAdult through nodeLoader/browserLoader/listStore/listFiles readListLines.
promptFilesAndSuggestions.js → random pool draws only bare bases (skips variant suffixes); new
pickerListNames() hides NSFW (and redundant -sfw) when off, shows all three when on; /api/files/lists
uses it. Files: renamed d/general.txt→general-sfw.txt, general-nsfw-only.txt→general-nsfw.txt;
deleted the three split .group files; renamed the 4 standalone adult lists to -nsfw
(artist/nudity-nsfw, keyword/keyword-nsfw, look/clothes-nsfw, word/adult-nsfw) + updated
artist.group, both CSV build scripts, and fix-leaks.mjs routes. Verified via loader harness across both
modes (e.g. {d/general} 7205 off / 7744 on; {d/general-nsfw} 0 off / 7744 on gated; {danbooru-sfw}
12393; standalone adult lists 0 off / content on, all gated) and npm test green.
SFW/NSFW naming FINAL: name=SFW, name-nsfw-only, name-nsfw=group(both) (2.1.0)
Owner reversed the previous convention (good reasoning: forcing SFW-only users to type -sfw is bad UX /
a footgun). FINAL scheme: plain <name>.txt = SFW (so {name} is safe with no typing),
<name>-nsfw-only.txt = NSFW-only, <name>-nsfw.group = imports both. Renamed general-sfw.txt→
general.txt, general-nsfw.txt→general-nsfw-only.txt, general.group→general-nsfw.group. Rebuilt the
danbooru groups around it: danbooru.group (SFW), danbooru-nsfw.group (both), d-keyword.group (SFW),
d-keyword-nsfw.group (both), d/general-nsfw.group (both). #danbooru (gated) now uses d/general-nsfw.
gatedLists: gate the NSFW-bearing names (danbooru-nsfw, d-keyword-nsfw, d/general-nsfw,
d/general-nsfw-only); the plain SFW names are ungated. Build script writes general.txt +
general-nsfw-only.txt. listTags/headers/README/list-architecture updated. Verified via a temp loader
harness: {d/general}=7205 ungated, {d/general-nsfw}=7742 gated(both), {d/general-nsfw-only}=539 gated,
{danbooru}=12393 ungated, {danbooru-nsfw}=12932 gated, {d-keyword}=11193 ungated, {d-keyword-nsfw}=
11732 gated. npm test green.
SFW/NSFW naming: name=group(both), name-sfw, name-nsfw (2.1.0)
Owner corrected the convention: <name>-sfw.txt (SFW-only) + <name>-nsfw.txt (NSFW-only) + <name>.group
importing both, so plain {name} = BOTH, {name-sfw} is the only SFW-only ref, {name-nsfw} = NSFW-only.
Renamed danbooru/d/general.txt→general-sfw.txt, replaced general-all.group→general.group (imports
general-sfw + general-nsfw). Rewired: danbooru-sfw→general-sfw; danbooru(full)/d-keyword/#danbooru→general.
gatedLists: general (group, both) + general-nsfw gated; general-sfw ungated. Build script now writes
general-sfw/general-nsfw. Verified: {d/general}=7742 gated(both), {d/general-sfw}=7206 ungated,
{d/general-nsfw}=540 gated, danbooru=12932 gated, danbooru-sfw=12393 ungated. npm test green.
Exclusive SFW/NSFW lists + full-as-group (supersedes d-sfw) (2.1.0)
Owner rejected the earlier d/ + d-sfw/ duplication (confusing, bad UX). Correct model: SFW and NSFW are
EXCLUSIVE lists; the full/NSFW-inclusive version is a GROUP importing both — and only split files that
genuinely contain both (otherwise overkill). Only danbooru/d/general truly mixes (7205 sfw / 539 nsfw);
the 1-2 isNsfw hits in artist/character-c/meta are FPs or trivial, left whole. Did: deleted d-sfw/;
split general into d/general (SFW-only) + d/general-nsfw (NSFW-only); added d/general-all.group
(imports both). Rewired groups: danbooru-sfw = sfw exclusives; danbooru (full) + d-keyword use
d/general-all. #danbooru (gated) now uses d/general-all. gatedLists: d/general ungated (clean),
d/general-nsfw + d/general-all gated. Build script splits general on regenerate. Verified: danbooru 12932
(gated), danbooru-sfw 12393 (ungated), d/general 7206 (ungated), d/general-nsfw 540 (gated). npm test
green.
Parallel audit of all risk-area lists + fixes (2.1.0)
Owner worried (after repeated spot-checks finding wrong words) that many lists still had bad entries; asked
to use "separate api calls" to review thoroughly. Dispatched 4 parallel review subagents (Agent tool, fresh
context each) to read the hand-curated descriptor lists (look/, style/, scene/, nature/) and
AI-classified proper-noun lists (name/, lore/, brand/*) in full and report misfits. Biggest find:
name/given-name was full of surnames (Einstein, Mozart, Gandhi…), title abbreviations (Dr, Mr, Sgt…),
and fictional names. Applied all findings via fix-audit.mjs: 17 typos fixed (Aardwark→Aardvark,
Vingette→Vignette, Linen Coset→Closet, …), 47 garbage/fragments/abbreviations removed, 229 misfits moved to
the correct list (surnames given→person, bare first-names person→given, fictional→work, culture-tagged gods
mythological-creature→mythology, an activity emotion→action, etc.). The big WordNet POS lists and city.txt
were excluded (authoritative / pure geography). npm test green; leak scan = only intentional false
positives.
Preprocess danbooru SFW (no runtime filtering) (2.1.0)
Owner objected (rightly) to the @filter sfw directive — it stripped adult words at RUNTIME (incl. in the
browser). Replaced with preprocessed files: scripts/list-cleanup/split-danbooru-nsfw.mjs writes SFW-only
copies of each danbooru/d/* to danbooru/d-sfw/* (full lists left intact + gated). danbooru-sfw.group
now reads d-sfw/* directly (pure union, no filter). Removed @filter parsing from resolveListLines
(and the now-unused isNsfw import). Wired the SFW split into process-danbooru-csv.js so regeneration
keeps it. Verified: danbooru 12932 (full, gated), danbooru-sfw 12389 (preprocessed, ungated). npm test
green.
Group files replace hardcoded virtual lists (2.1.0)
Owner: do composites "proper" as .group files instead of the hardcoded virtualLists object — a .group
file lives anywhere, is referenced like any list, and each line is a list reference (resolved by the same
suffix lookup); groups may import groups with a recursion cutoff (~3 levels). Implemented: removed
virtualLists/isVirtualList; resolveListLines(name, {names, readListFile, readGroupFile}) now reads a
<name>.group file, unions its member refs (recursively, MAX_GROUP_DEPTH=3 + cycle guard, dedup), and
applies an optional @filter sfw|nsfw directive. All three loaders walk .txt + .group and inject the
readers. Created group files (danbooru, danbooru-sfw, d-character, d-keyword, artist, artist-digipa, name)
and — per follow-up — organized them into their folders (danbooru/, artist/, name/); updated gatedLists to
the new canonical paths (danbooru/danbooru, danbooru/d-keyword). Verified all groups resolve + gate
correctly ({danbooru}=12932 gated, {danbooru-sfw}=12389, etc.); npm test green; 0 lint errors.
Word lists: strict WordNet POS pass + collapse dict into one list per POS (2.1.0)
Owner: relocate misfits in the curated word lists, and collapse the dict-* lists into the main ones (one
file per POS — "we don't need to be exclusive"); danbooru stays separate (genuinely its own anime thing).
Did both. reclassify-words.mjs validated every curated adjective/noun/verb/adverb against WordNet: kept
WordNet-confirmed in place, routed 602 action gerunds (nail biting, dual wielding, celebrating) to a new
look/action, moved 450 cross-POS words to the correct list (the curated noun list was full of
adjectives), kept 36 WordNet-unknown in place (no loss). Then merge-dict.mjs merged dict-adjective→
adjective, dict-noun→noun, dict-verb→verb, dict-adverb→adverb (dict-misc→word/misc) and deleted the dict-*
files; removed the now-redundant *-all virtual lists. Final word/: adjective 8166, noun 22979, verb
6182, adverb 1053, misc 5451 (+ adult gated, preposition, interjection). Full spot-check wave: read
samples from every folder (all fit), leak scan = only intentional false positives, 0 stale dict-/*-all
refs, npm test green.
Spot-check: fix NSFW/misfit leaks across the lists (2.1.0)
Owner reported wrong content in lists (NSFW words in word/, "nipple" seen, anime terms). Built
scan-leaks.mjs (flags isNsfw/classifyRemoval hits in supposedly-clean lists) and reviewed every hit
line-by-line. Fixes: relocated 93 NSFW terms into gated look/clothes-adult (lingerie/fetish from
look/clothes) and word/adult (sexual terms from word/); removed 1 extreme ("rape face"); pulled 26
danbooru face/expression/pose tags (torogao, doyagao, evil grin, naughty face, smirk, v-shaped eyebrows,
goldfish scooping, finger biting, ...) out of word/adjective into a new look/expression list; removed
3 typos (faceing, operateing, gnawling); moved "Sailor Moon" from artist/anime to name/anime-name. Added
the gated lists to gatedLists + listManifest tags. Kept genuine false positives (real places Coon
Rapids/Gore/Intercourse/Dyke, "Naked mole rat", "X-ray", "presenting", and legit words breast/butt/ass/
sex/oral/facial/naked/explicit/sexual). Final leak scan = only those intentional keeps. npm test green.
NOTE: the curated word/noun + word/verb still mix in action/pose gerunds (shushing, nose picking) and a
few WordNet edge nouns in dict-verb (whelk, bur) — a stricter POS re-derivation is offered as a follow-up.
Organize lists into folders + path-suffix name resolution (2.1.0)
Owner: organize all lists into folders (e.g. d-* → a danbooru folder, drop prefix), add a
README, reference via /. Then refined it: resolve by cached path-suffix lookup so a bare
filename, partial path, or full path all work (deep folders, short refs); and bring back
short codes as subfolders (danbooru/d/general → {d/general}, easy translation from old
d-general); and guarantee a natural load order (symbols, numeric numbers, letters) so
naming can force defaults.
Built: resolveName() + compareNames() in listManifest; all three loaders (nodeLoader,
browserLoader, helpers/listFiles) now walk data/lists recursively and resolve via suffix.
Moved 73 files into 12 folders (move-to-folders.mjs), keeping basenames UNIQUE so the ~78
existing {name} refs still resolve (verified) — only danbooru d-* string refs updated
(danbooru.js, applyArgs.js, both dynamic-prompt person-replacers). danbooru files nested
under danbooru/d/. Updated gatedLists, listManifest unions + tags, and both CSV build
scripts to the new paths (+ mkdir). Added data/lists/README.md. npm test green; verified
{d/general}, {danbooru/d/general}, {general}, {color}, virtuals, etc. all resolve.
Proper-noun categorization: keyword.txt 8,859 -> 593 (2.1.0)
Owner: keyword.txt was still a big bag of proper nouns with "garbage" in it — collapse more into other
files, no deleting without review, and they asked whether AI could classify individually. Key insight
(theirs): for proper nouns, AI world-knowledge IS the right tool (no dictionary knows Achernar is a star).
Did it: automatic first pass (split-proper.mjs) pulled given-name/place/organization + de-duped confirmed
cities into city.txt; then hand-classified the remaining ~4,422 individually in 9 batches
(scripts/list-cleanup/cat/batch-00*.json) into person/place/organization/mythology/astronomy/
people-group/religion/history/work, distributed by an idempotent build-categories.mjs with a coverage
check (nothing lost; unclassified stays in keyword.txt). Result: keyword.txt 8,859 -> 593, plus person
1810, given-name 1328, place 743, organization 551, people-group 298, mythology 218, religion 174,
astronomy 133, work 150, demonym 124, history 87. New lists tagged in listManifest; added a name virtual
(given-name + person). Slurs found mid-pass (Jap/Negress/Negroid) added to contentSafety + purged. npm test green, safety scan 0. NOTE: superseded surname.txt (came out empty) removed.
Keyword-list content-safety purge + full reorganization (2.1.0)
Owner asked for a big one: the keyword lists were a poorly-structured mess — outright slurs (e.g. the n-word), a blend of "adult-but-ok" and "not-ok-at-all" content, a giant unsorted dictionary, and clunky duplicate lists. Decisions taken with the owner: do it in one pass; remove slurs + illegal + extreme shock/gore/non-con while keeping ordinary adult content (NSFW-gated, not deleted); drop obvious junk (possessives/inflected fragments) during the POS sort; build "virtual lists" to collapse duplicates and compose curated+dictionary.
Built three things. Content safety — src/contentSafety.js (browser-safe): curated blocklist with
whole-word matching, list-type-aware (proper-noun lists match EXACT whole-entry so "Coon Rapids"/the
hamlet "Dyke"/"Rio Negro" survive while bare slur words don't; city.txt fully exempt). Scanned all
lists + danbooru.csv/artists.csv, reviewed the 81 matches (caught the place-name + artist-handle
false positives), purged them, and wired the filter into both CSV build scripts so regeneration stays
clean. Dictionary reorg — classify-pos.mjs sorted the 48,750-line keyword.txt (a SCOWL dump) via
compromise into dict-* POS lists, dropping possessives/redundant inflections/junk; keyword.txt is
now ~10k proper nouns. Math conserved exactly (46,500 sorted + 2,250 dropped). Virtual lists —
src/listManifest.js resolves composite lists (union + de-dup + optional sfw/nsfw filter); collapsed the
duplicated danbooru/d-keyword/d-character/artist/artist-digipa files into virtual definitions
(deleted the physical files, preserved 6 artist orphans), added danbooru-sfw and the *-all
curated+dict composites. Wired into nodeLoader, browserLoader, and helpers/listFiles.js.
Verification: npm test green (0 lint errors, 167 pre-existing warnings; smoke loaded the whole graph and
happened to expand a virtual {adverb-all}). All on branch cleanup/list-reorg; tooling + audit logs
under scripts/list-cleanup/. NOTE for follow-up: danbooru-sfw uses the NSFW lexicon (543 tags
filtered) — it's a strong first pass but not a curated guarantee; the lexicon in contentSafety.js is the
knob to tighten it.
Follow-up: redid the POS sort with WordNet (authoritative)
Owner pushed back (rightly) that keyword.txt still held common words and lost confidence in the
guess-from-spelling scripts. Key correction: a real dictionary states a word's part(s) of speech —
that's not a guess. Switched to WordNet (wordpos / wordnet-db, added as a dev dependency,
index files read directly) and rebuilt the dict-* lists from the original slur-free dump
(scripts/list-cleanup/pos-dictionary.mjs): each word → the POS list(s) WordNet assigns (bond →
noun+verb), capitalized noun-only words kept as proper nouns (America/Paris/December stay), capitalized
adj/verb/adverb words moved out, demonyms → demonym.txt, junk/redundant-inflections dropped. Result:
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 (real words WordNet doesn't index, incl. -ally adverbs). Conservation 48,750
exact. Deleted the superseded heuristic scripts (classify-pos/refine/analyze-proper/measure-keyword).
Verified specific words (bond→noun+verb, holly→noun not adverb, communist→adj+noun, America→proper).
npm test green, safety re-scan 0. NOTE: quickly/really/suddenly aren't in the lists because
they're absent from the SCOWL source, not a bug.
Follow-up: keyword.txt second pass + dict-adverb fix
Owner noticed keyword.txt still held common words. Two fixes: (1) the first POS sort had a bug —
a bare -ly ending was treated as an adverb, polluting dict-adverb with -ly nouns/names (holly,
lily, assembly, Billy/Holly/Sally); re-routed 63 of them to adjective/verb/noun and fixed the rule in
classify-pos.mjs (require the #Adverb tag). (2) Second pass on keyword.txt: moved the 1,232
entries whose lowercase form is a real dictionary word into the dict-* lists (precise set-membership
test, not the unreliable proper-noun tagger), and extracted 127 demonyms to a new demonym.txt.
keyword.txt is now 8,817 genuine proper nouns. Caveat noted: a few name/common-word collisions
(Bond, Amber, Holly) move to common lists — acceptable for prompt generation, reversible on the branch.
demonym added to listManifest (tags + adjective-all/noun-all). npm test green.
Composer: four icon-only field actions (2.0.8)
Owner wanted Save + Share as icon-only buttons in the field next to Random/Generate (Save = disk, Share =
share glyph), not the merged text button. Split them back apart and made all four .field-act buttons a
cohesive inline-SVG icon set (currentColor): Save = floppy disk, Share = three-node share, Random =
shuffle, Generate = sparkle (primary green). Added module-scope icon components (SaveIcon, ShareIcon,
ShuffleIcon, SparkleIcon). State back to panel: ""|"save"|"share"; Save/Share each toggle their own
one-row .action-panel. Dropped .field-util CSS. Lint 0 errors, vite build green. (If owner meant the
iOS box-and-up-arrow share glyph rather than the three-node one, easy swap.)
Composer: chat-style field, docked actions (2.0.7)
Owner still felt it didn't read as an app. Turned the composer into a chat-style input: a bordered
.composer-field (lights up via :focus-within) wrapping a borderless textarea with a .field-bar
docked along the bottom. Actions moved into the field — Generate = icon-only primary round button
(✦, bottom-right), Random = matching round icon (🎲) beside it, and Save + Share merged into one
.field-util button (bottom-left) that opens a single .action-panel with both a save-as-expansion row
and a share-link row. State went from panel: ""|"save"|"share" to a single panelOpen bool; opening it
pre-builds the share link. Dropped the old .composer-toolbar, .generate-btn, .tool-btn, .editor,
and the redundant close ✕. Lint 0 errors, vite build green.
Composer follow-up: size the prompt box (2.0.6)
Owner: the editor-fill layout made the textarea take the whole pane height — "looks silly, no need for
that much space." Reverted the fill behaviour: composer/results now size to content (flex: none), the
prompt box is a comfortable min-height: 8.5rem, resize: vertical, capped at 50vh, and .main-col
scrolls on overflow. CSS-only (styles.css). vite build green.
Composer redesign + anime/adult toggle removed (2.0.5)
Owner feedback: the SPA middle still felt "clunky/like a dev tool, not an app." Three threads, all
SPA-only (web-app/):
-
Anime/adult content. Confirmed the owner's suspicion: the
Styletoggle's "Anime" option swaps indata/lists/d-keyword.txt— a raw Danbooru tag dump that mixes SFW tags with explicit adult ones (nude,sex,cum,penis,pussy,nipples, …). There was no way to get anime without adult, and the separatekeyword-adult.txtisn't even wired into the SPA. A clean SFW/adult split is a big, careful job across ~100k+ lines of lists (the owner flagged this), so for now we removed the Style toggle fromHome.jsxand added a migration inlib/settings.js#loadSettingsthat pulls any browser stuck ond-keyword/d-artistback to the safekeyword/artistdefaults. Logged inplans/removed-pending-readd.mdwith re-add guidance (Style SFW-only + a separate off-by-default "Allow adult content" switch, once the lists are gated). Data files left untouched. -
Compact toolbar (owner chose this layout). Rebuilt the composer actions into one toolbar attached to the prompt box: prominent Generate, 🎲 Random, and smaller ghost Save / Share. Save-expansion moved off its own card into an inline panel (same pattern as Share); the separate save card is gone.
-
Editor-fill layout. The textarea now stretches to fill the composer card (fixes the "big empty gap after it"); a hover ✕ clears the box. The composer flexes to fill the column; when prompts exist, the results card splits the space and scrolls internally. Mobile stacks back to content-height.
Noticed, left alone: the working tree already has uncommitted, unrelated in-progress work on list
gating — src/gatedLists.js (untracked), data/lists/keyword-adult.txt (untracked), and edits to
data/lists/{d-general,d-keyword,danbooru,keyword}.txt, src/applyArgs.js, src/core/listStore.js,
src/helpers/listFiles.js, src/promptFilesAndSuggestions.js, src/settings.js. This looks like the
owner's own start on the SFW/adult separation. Not committed here — only the SPA files for this pass
were staged. Surfaced to the owner.
Verification: npm run lint → 0 errors (165 pre-existing warnings). web-app vite build → green.
Files: web-app/src/components/Home.jsx, web-app/src/lib/settings.js, web-app/src/styles.css,
VERSION + package.json + web-app/package.json (→ 2.0.5), and the notes above.