Tutorial: 2026-06-29

2026-06-29

Newest entry on top.

Performance profiling pass + loader memoization: ~280× faster generation (2.28.18, branch dev)

First full profiling sweep of engine-v3 (core engine · loaders/startup · GUI/Vite build), then applied the one big, safe win and verified.

Method. Backgrounded Node micro-benchmarks from engine-v3/ (timed engine.generate() over representative prompts; wrapped fs.* with call counters for one generate()); npm run test:perf for build + bundle.

Findings. A single generate() cost ~56 ms and did 208 readdirSync + 222 readFileSync. Root cause: nodeLoader.readListLines rebuilt the whole list-name set on every call (≈4 recursive tree walks per distinct list), and the reserved keyword wildcard (listResolve.js) re-unioned the entire vocabulary (~48 ms) — and the list store clears its per-prompt cache on reset(), so both recurred every prompt. browserLoader did the same logical work at SPA runtime (no fs, but rebuilt the name set + wildcard union per prompt). The GUI build was already healthy (671 modules in ~350 ms; 785 KB gzip within the 900 KB budget; vendor/prompt-data/Manage already chunked + lazy) — left as-is.

Fix (memoization; no behavior change). nodeLoader caches its directory walks, sorted catalogs, full name set, and keyed reads (readListLines/meta/group), + a refresh() to drop them; browserLoader caches allNames() + resolved-line sets; block.js hoists resolvePool out of the per-token loop. Safe because these loaders only read a static catalog — the Manage tab's live edits go through the separate runtimeLoader.

Result. generate() ~56 ms → ~0.2 ms (≈280×); per-prompt fs ~430 syscalls → ~0 once warm. Verify green: lint 0 errors, smoke, Node unit 235, web 237, gui build within budget. New report: reference/performance.md. PATCH 2.28.17 → 2.28.18.

Text-provider expansion, round 3 (final): Hugging Face + Meta Llama; 18 total (2.28.0, branch dev)

The last two simple-key OpenAI-compatible text providers (config-only, makeChatRewrite in dispatch.js): Hugging Face (router router.huggingface.co/v1, default meta-llama/Llama-3.3-70B-Instruct) and Meta Llama API (api.llama.com/compat/v1, Llama-3.3-70B-Instruct). Both hosted-proxy + rewrite: true + textOnly; best-effort default models. MINOR 2.27.0 → 2.28.0. Verify: node --check, lint 0 errors, smoke, gui build, test:web 196 — green.

Text providers: 18 — OpenAI, Gemini, Grok, OpenRouter, Groq, DeepSeek, Mistral, Together, Perplexity, Fireworks, Cerebras, Qwen, Moonshot, AI21, Anthropic, Cohere, Hugging Face, Meta Llama. The simple-key / no-fancy-auth list is now complete; remaining (Bedrock/Azure/Vertex/watsonx/NIM = fancy auth; local Ollama/LM Studio = keyless) are out of the owner's criterion.

Text-provider expansion, round 2: 7 more (Text dropdown 9 → 16) (2.27.0, branch dev)

Per the owner's criterion (simple key + CORS/proxy, no fancy auth):

  • OpenAI-compatible (config-only, makeChatRewrite entry in dispatch.js rewriteAdapters): Fireworks (api.fireworks.ai/inference/v1), Cerebras (api.cerebras.ai/v1), Qwen/DashScope (dashscope-intl.aliyuncs.com/compatible-mode/v1), Moonshot/Kimi (api.moonshot.ai/v1), AI21 (api.ai21.com/studio/v1). All hosted-proxy + rewrite: true + textOnly.
  • Bespoke (_shared/bespokeRewrite.js, plain-key but non-OpenAI shape): Anthropic (Messages API, x-api-key + anthropic-version, content[].text) and Cohere (v2 /chat, Bearer, message.content[].text). Registered in rewriteAdapters.
  • All proxied (online-locked, work locally); model defaults are best-effort and may drift. MINOR 2.26.0 → 2.27.0. Verify: node --check, lint 0 errors, smoke, gui build, test:web 196 — green.
  • Skipped (fancy auth): Bedrock (SigV4), Azure OpenAI / Vertex (AD / service accounts), watsonx, NIM, Baseten/Anyscale (deployment-specific) — out of scope per the owner. Local OpenAI-compatible servers (Ollama/LM Studio/vLLM) are keyless (no key) — a separate future add (needs settings passed through the rewrite proxy).

Text-provider expansion: OpenAI-compatible rewrite framework + 6 providers (2.26.0, branch dev)

Generalized the text (prompt-rewrite) role so OpenAI-compatible LLM APIs are one-line additions, then added six providers (Text dropdown 3 → 9).

  • _shared/openaiCompatRewrite.jsmakeChatRewrite({ baseUrl, model, label }) returns a rewrite fn (POST {baseUrl}/chat/completions, Bearer, the system+user messages, read choices[0].message). Works in the browser (a browser-direct provider's loadRewrite) and server-side (the /api/rewrite proxy's rewriteAdapters) — same plain fetch.
  • Providers: OpenRouter + Groq are CORS-enabled → browser-direct with a loadRewrite (online- capable). DeepSeek, Mistral, Together, Perplexityhosted-proxy + rewrite: true (CORS not assured) with a makeChatRewrite entry in dispatch.js rewriteAdapters; online-locked, work locally. All textOnly: true.
  • Framework: rewriteProviders() now matches loadRewrite || rewrite; ProvidersMenu excludes textOnly (and upscaleOnly) providers from the image picker via a shared notImageRole filter, so text-only providers show only in the Text row. metaFor already returns {} for unknown ids (safe).
  • MINOR 2.25.0 → 2.26.0. Verify: node --check, lint 0 errors, smoke, gui build, test:web 196 — green. The long tail (Fireworks, Cerebras, Qwen, Kimi, local Ollama/LM Studio/vLLM via the same factory) is now a config-only add.

Provider expansion, phase 3f: five async-job enhancers (2.25.0, branch dev)

The owner asked for the six async/job enhancers. Did five; Pixelbin skipped (CDN transform-URL model — upload-to-storage + sr.upscale() URL with account cloud/zone — doesn't fit the POST-image proxy). All via the /api/upscale proxy, registered in dispatch.js; best-effort (researched endpoints, can't live-test):

  • WaveSpeed (Real-ESRGAN) — submit v3/wavespeed-ai/real-esrgan → poll v3/predictions/{id}/result (submitPoll), outputs[0].
  • Claid / Let's Enhancesync multipart v1/image/edit/upload (file + data JSON with restorations.upscale: smart_enhance) → output tmp_url.
  • Deep-Image.airest_api/process_result (x-api-key, data-URI url + decoded 4× dims) → returns result_url or a job we poll via rest_api/result/{job}.
  • neural.love — submit v1/images/process (quality_enhance 4×) → poll v1/images/orders/{id} (submitPoll); request/response shape approximate.
  • VanceAI — unified jobs API: POST /v1/jobs → poll /v1/jobs/{id}/v1/jobs/{id}/result (manual loop, since the result link is a separate endpoint).

submitPoll runs fine server-side. MINOR 2.24.0 → 2.25.0. Verify: node --check (×5), lint 0 errors, smoke, gui build, test:web 196 — green. Upscaler set: 17 (4 in-repo, 10 hosted enhancers, 3 local). The cleanly-integrable hosted enhancer list is now essentially exhausted (remaining are premium/RapidAPI/ URL-model).

Provider expansion, phase 3e: Clipdrop + Venice enhancers (2.24.0, branch dev)

Two more upscale-only enhancers via the proxy pattern (dispatch.js upscaleAdapters):

  • Venice AI (v1/image/upscale, Bearer, JSON base64 image + scale 1–4 → image bytes; uncensored, no sfw tag). Normalizes both binary and JSON responses to a data URL.
  • Clipdrop (image-upscaling/v1/upscale, x-api-key, multipart image_file + target_width/ target_height → image bytes). Clipdrop needs explicit output dims, so the server decodes the source size from the bytes (PNG IHDR / JPEG SOF) and requests a 4× target capped at 4096.
  • MINOR 2.23.0 → 2.24.0. Verify: node --check, lint 0 errors, smoke, gui build, test:web 196 — green.
  • Upscaler set: 12 providers (4 in-repo, 5 hosted enhancers [DeepAI, Picsart, Segmind, Venice, Clipdrop], 3 local). Remaining hosted enhancers are async/premium/niche best-effort — diminishing returns.

Provider expansion, phase 3d: ComfyUI AI upscale — local set complete (2.23.0, branch dev)

ComfyUI upscaling needs a multipart image upload + a graph run, which the JSON forward proxy can't do, so it goes through the server-side /api/upscale path (like Replicate):

  • comfyui/code/upscale-server.js (Node): decode the inlined image → POST /upload/image (multipart) → resolve the upscale model (self-heal against /object_info/UpscaleModelLoader) → submit the minimal graph LoadImage → UpscaleModelLoader → ImageUpscaleWithModel → SaveImage to /prompt → poll /history → return the /view URL (the /api/upscale endpoint fetches it back to a data URL).
  • comfyui/code/upscale.js (client → callUpscaleProxy, passing comfyUrl + comfyUpscaleModel in params); config.js gains capabilities.upscale + loadUpscale; settings.js adds a comfyUpscaleModel field (blank = auto). Registered in dispatch.js upscaleAdapters.
  • Requires an upscale model in ComfyUI/models/upscale_models. Best-effort; verify live.
  • MINOR 2.22.0 → 2.23.0. Verify: node --check, lint 0 errors, smoke, gui build, test:web 196 — green.
  • Upscaler set: 10 providers (Stability, fal, Leonardo, Replicate · DeepAI, Picsart, Segmind · Forge, SD.Next, ComfyUI). Local SD upscaler set complete. Next: new generation providers (Krea/Firefly) + remaining best-effort hosted enhancers.

Provider expansion, phase 3c: modernized A1111 adapter + Forge/SD.Next local upscalers (2.22.0, branch dev)

Owner picked "modernize A1111 adapter" (the local-webui generate adapter Forge + SD.Next share was stale).

  • Modernized local-webui/code/generate.jssampler_indexsampler_name (confirmed via web search: A1111/Forge/SD.Next deprecated sampler_index and silently ignore it, which is why the sampler/ seed didn't take), added a separate scheduler ("Automatic" — avoids the sampler+scheduler autocorrection bug), batch_size, and send_images/save_images. Updated the contract test (tests/providers.test.js) to the new shape.
  • Local Extras upscaler (local-webui/code/upscale.js, shared) — /sdapi/v1/extra-single-image with R-ESRGAN 4x+; the endpoint is independent of txt2img, so it's stable. forge/config.js + sdnext/config.js gain capabilities.upscale + loadUpscale. No key → a free local upscaler.
  • App.upscaleImage now passes the live app settings (+ the upscaler's providerParams) instead of the source image's frozen snapshot, so the local WebUI URL is honored for upscaling.
  • MINOR 2.21.0 → 2.22.0. Verify: node --check, lint 0 errors, smoke, gui build, test:web 196 — green.
  • Upscaler set: 4 in-repo + 3 hosted enhancers + 2 local (Forge/SD.Next) = 9 providers. Next: ComfyUI upscale (graph) and/or new generation providers (Krea/Firefly).

Provider expansion: local SD upscalers DEFERRED — stale A1111 adapter (notes only, branch dev)

Started a local Extras-tab upscaler (/sdapi/v1/extra-single-image) shared by Forge + SD.Next, but the owner flagged that the local-webui A1111 adapter is old and likely no longer works against current A1111/Forge/SD.Next. Backed out the half-written local-webui/code/upscale.js rather than ship an upscaler on an untrustworthy foundation (the Forge/SD.Next configs were never touched). Recorded the deferral in notes/plans/provider-expansion.md: local SD upscalers are blocked on a local-webui modernization + live re-verify pass (re-check the txt2img contract — sampler_indexsampler_name, current param names) before adding the Extras upscaler; ComfyUI separately needs a model-specific graph. Notes-only — no version change.

Where the upscaler set stands: 4 in-repo (Stability, fal, Leonardo, Replicate) + 3 hosted enhancers (DeepAI, Picsart, Segmind) = 7 providers. Remaining hosted enhancers are async/niche/best-effort (diminishing returns); local SD deferred; new generation providers (Krea/Firefly) are the next real-value frontier but each is a researched best-effort module.

Provider expansion, phase 3b: Picsart + Segmind enhancers; Upscaler row locked-online (2.21.0, branch dev)

  • Two more enhancers (proxy path, upscaleOnly): Picsart (tools/1.0/upscale, X-Picsart-API-Key, multipart + upscale_factor, JSON data.url) and Segmind ESRGAN (v1/esrgan, x-api-key, JSON base64 image + scale, returns image bytes → server returns a data URL). Both registered in dispatch.js upscaleAdapters. Verified endpoints/auth via web search; best-effort BYOK.
  • Upscaler/Enhancer row now locked-online instead of hidden (owner: "nice for the user to see it exists"). ProviderPicker gained a locked/lockReason prop — a locked trigger shows 🔒 + a tooltip and routes to the full-version link (the tab pattern), no dropdown. ProvidersMenu always renders the row; locked={ONLINE} with reason "upscaling needs the desktop single-image view"; the key field is still local-only.
  • MINOR 2.20.0 → 2.21.0. Verify: node --check, i18n regen, lint 0 errors, smoke, gui build, test:web 196 — green. Three enhancers now (DeepAI, Picsart, Segmind); more to follow + local SD upscalers + new generation providers.

Provider expansion, phase 3a: upscale-only providers + Upscaler/Enhancer row; DeepAI (2.20.0, branch dev)

New provider category for the ~15 enhancement-only services. Owner confirmed they're upscale-only (no generation) and — since AI upscale lives only in the local-only single view — the whole feature is online-locked, so no per-provider CORS research is needed; all route through the local /api/upscale proxy.

  • Framework: a provider may set upscaleOnly: true (capabilities.upscale + loadUpscale, no loadGenerate). ProvidersMenu now: (a) excludes upscaleOnly from the Image picker groups, and (b) adds a third row, "Upscaler / Enhancer" (below Text) — a picker over every upscale-capable provider (the in-repo ones + the enhancers) + a key field; selection is settings.upscaleProvider. The row is hidden when ONLINE (single view is local-only). Shared-key handling when the chosen upscaler is also the image/text provider.
  • DeepAI (providers/deepai/, first upscale-only enhancer): upscaleOnly, routed via the proxy — code/upscale.js (client → callUpscaleProxy) + code/upscale-server.js (multipart POST to api/torch-srgan, api-key header, returns output_url); registered in dispatch.js upscaleAdapters. Verified the endpoint/auth/response via web search.
  • MINOR 2.19.0 → 2.20.0, ships via release/2.20.0. Verify: node --check, i18n regen, lint 0 errors, smoke, gui build, test:web 196 — green.
  • Next: the remaining enhancers (Topaz/Magnific/Claid/Picsart/…) drop in the same shape; then local SD upscalers + new generation providers.

Provider expansion, phase 2c: Leonardo + Replicate upscale — in-repo set complete (2.19.0, branch dev)

  • Leonardo (leonardo/code/upscale.js, browser-direct, best-effort) — the 5-step Universal Upscaler: POST /init-image (presigned S3) → upload the source blob to S3 → POST /variations/universal-upscaler (initImageId, upscaleMultiplier: 2) → submitPoll GET /variations/{id} until a generated_image_variation_generic entry is COMPLETE → fetch result(s) to data URLs (CORS-fallback to the raw URL). Flagged best-effort; verify against live API.
  • Replicate (proxy path — it's hosted-proxy, not browser-callable): new /api/upscale dev endpoint inlines the local image as a data URI, calls dispatchUpscalereplicate/code/upscale-server.js (Real-ESRGAN nightmareai/real-esrgan via /v1/models/.../predictions + Prefer: wait), then fetches the result server-side and returns data URLs (so the browser never hits Replicate's CORS). New pieces: callUpscaleProxy (hostedProxy.js), replicate/code/upscale.js (client), upscaleAdapters + dispatchUpscale (server/dispatch.js). Online build never calls it (single view is local-only), so no Netlify function needed.
  • Both configs gain capabilities.upscale + loadUpscale. The in-repo upscaler set is now complete: Stability + fal (2.18.1/2.18.2) + Leonardo + Replicate (2.19.0).
  • MINOR 2.18.2 → 2.19.0 (milestone), ships via release/2.19.0. Verify: node --check (server files), lint 0 errors, smoke, gui build, test:web 196 — green.
  • Next per notes/plans/provider-expansion.md: local SD upscalers (ComfyUI/forge/sdnext Extras), then new generation + hosted upscale-only providers.

Provider expansion, phase 2b: fal.ai upscale adapter (2.18.2, branch dev)

Second loadUpscale adapter. fal/code/upscale.js runs fal-ai/esrgan (Real-ESRGAN ~4×) via the synchronous fal.run endpoint (Authorization: Key …), verified the input (image_url + scale) and output (image.url) schema via web search. Key save-model detail: the input image is inlined as a data: URI (fal can't reach a localhost /api/output path) and the fal-CDN output is fetched back into a data: URL so the /api/image ingest (data/localhost only, for SSRF safety) persists it as a tracked Resizes child. fal/config.js gains capabilities.upscale + loadUpscale.

  • PATCH 2.18.1 → 2.18.2, ships dev → main direct. Verify: lint 0 errors, smoke, gui build, test:web 196 — green.
  • Leonardo (TODO) is heavier — POST /init-image presigned S3 upload → variations/universal-upscaler (upscaleMultiplier ≤ 2) → poll. Replicate (TODO) is hosted-proxy, so it needs an upscale action routed through the proxy dispatch (browser can't call Replicate; CORS). Both flagged in notes/plans/provider-expansion.md.

Provider expansion, phase 2a: Stability AI upscale adapter (2.18.1, branch dev)

First real loadUpscale adapter, proving the phase-1 framework end-to-end. stability/code/upscale.js posts the source image (read from its served URL → blob) as multipart to the v2beta fast upscaler (stable-image/upscale/fast, synchronous ~4×, base64 back) — same auth/multipart shape as the existing generate adapter (verified the endpoint + image field via web search). stability/config.js gains capabilities.upscale: true + loadUpscale, so SingleView's resize → AI group now lists AI Upscale · Stability AI (key-gated) and App.upscaleImage runs it into the Resizes strip.

  • PATCH (small feature) → 2.18.0 → 2.18.1, ships dev → main direct.
  • Verify: lint 0 errors, smoke, gui build (browser glob picks up the new adapter), test:web 196 — green.
  • Next: fal (Topaz), Replicate (Real-ESRGAN, async poll), Leonardo (Universal Upscaler), then local engines + the new hosted services per notes/plans/provider-expansion.md.

Deploy fixes: re-enable GitHub Pages (engine-v3 doc-build straddle) + Netlify continuous deploy (branch dev)

After the 2.18.0 release the owner noted the Pages docs and the Netlify app weren't updating. Both root causes were infra, not the release:

  • Pagespages.yml's push:[main] trigger had been paused (only workflow_dispatch) because npm run docs broke in the engine-v3 split. Fixed scripts/build-docs.mjs to straddle two roots: root = engine-v3 (code src/+data/, transpiled-JSX tmp/, the pinned jsdoc binary) and repoRoot = its parent (notes/, assets/, README.md, list-credits.md, jsdoc.config.json). JSDoc now runs from repoRoot via node engine-v3/node_modules/jsdoc/jsdoc.js (not npx jsdoc, which fetched a different version); jsdoc.config.json source.include reaches into engine-v3/src etc.; the site writes to repo-root docs/jsdoc (so pages.yml uploads docs/jsdoc). JSDoc exits 1 on recoverable TS-style type-expression warnings yet still writes all 594 files — the build now tolerates that when index.html landed. Re-enabled the push:[main] trigger. GitHub Pages source was already build_type: workflow (Actions). npm run docs is green (exit 0).
  • Netlify — the site (prompt-fairyfox, id 927e1b3b-…, prompt.fairyfox.io) is not git-connected (manual-CLI history), so pushes never rebuilt it. Added .github/workflows/netlify-deploy.yml — on push to main it runs netlify deploy --build --prod, gated on a NETLIFY_AUTH_TOKEN secret (safe no-op until the owner adds it; site id is public in the workflow env). Also triggered a manual prod deploy to refresh the live site to 2.18.0 now.
  • Updated notes/reference/deployment.md (Pages straddle, Netlify continuous-deploy, "Not done yet").
  • CI/infra only — no version bump. Ships dev → main (release.yml no-ops since VERSION is unchanged; the main push now fires pages.yml + netlify-deploy.yml).

Provider expansion, phase 1: AI-upscale framework + NSFW content-policy soft-lock (2.18.0, branch feature/single-derive-ancestry)

Owner pushed to support many more providers (incl. AI upscalers — I'd wrongly assumed none offered it) and to soft-gate SFW-only providers in NSFW mode. Phase 1 lays the reusable backbone (fully internal, no external API guessing); per-provider integrations are scoped in notes/plans/provider-expansion.md.

  • AI-upscale contractcapabilities.upscale + loadUpscale() adapter. App.upscaleImage(item, providerId) loads the adapter, runs { image, key, settings }, and ingestImages the result as a derivedKind: "resize" child (Resizes strip + live placeholder, like a derive). SingleView builds the resize menu's AI group from providers.filter(p => p.capabilities.upscale && p.loadUpscale), key-gating each. Dormant until a provider ships an adapter (then it lights up automatically).
  • NSFW soft-lock (lib/contentPolicy.js) — contentPolicy: "sfw-only" on openai / gemini / ideogram / stability. softLockedForNsfw(provider, includeAdult) drives: a neutral 🔒 + tooltip on the Providers picker option (ProviderPicker softLock, still selectable), and a one-time confirm ("NSFW mode is on — proceed?") before generate (Home.buildPrompts) / derive / upscale (App). Never hard-blocks, never states a restriction — per the owner's explicit instruction.
  • Roadmapnotes/plans/provider-expansion.md: capability assessment (text/image/upscale) of the owner's provider list + phase order (in-repo upscale → new generation providers → hosted upscale-only → local upscalers), with "verify endpoints against current docs" flagged throughout.
  • Verify: i18n regen, lint 0 errors, smoke, gui build, test:web 196, test:unit 226 — all green. Bump 2.17.0 → 2.18.0. Not released.

Single view, round 2: inline layer actions, live strips, resize, View Raw, uniform locks (2.17.0, branch feature/single-derive-ancestry)

Reworked the single-view actions per owner feedback on the first cut.

  • Inline actions on the prompt layer rows (replacing the separate "Make another" card): the DPL-source row gets a re-roll + vary link (styled like the copy link); Sent to model and Translated each get a vary link. Both ask for confirmation (window.confirm). derive.js gained a final source (vary the sent text verbatim); sources are now dpl / final / ai / roll.
  • No page change on derive (better UX) — App no longer navigates to a pending image. Instead a new derivations list ({id, parentPath, kind}) drives live spinner placeholders in the matching strip; on completion refreshAndResync reloads the feed and re-syncs the open image in place.
  • Two→three strips below the image (DerivedStrips): Re-Rolls, Variations, Resizes, each showing that kind's children (clickable) + live placeholders. Moved out of the meta column; a compact LineageHead (type + parent link) stays on the right.
  • Resize (upsize/downsize) — new POST /api/image/resize (ImageMagick -resize N%) writes a tracked child (derivedKind: "resize", ancestry like a re-roll). derive.js#resizeImage + RESIZE_SCALES (¼× ½× 2× 4×). An AI Upscale entry sits in the same menu, gated on capabilities.upscale (no provider declares it yet, so it shows locked — see the next entry / follow-up to actually wire AI upscalers).
  • Uniform locks — the Convert and Resize menus are no longer hidden when ImageMagick is missing; they're shown greyed with a 🔒 + tooltip (matching the online-locked tabs) so users can discover the feature.
  • Details panel — a View Raw toggle flips the table to a syntax-highlighted raw-JSON view (syntaxHighlightJson); Copy as Markdown + Copy as JSON buttons (the old bottom raw <details> is gone).
  • Verify: i18n regen, lint 0 errors, smoke, gui build, test:web 196, test:unit 226 — all green. Bump 2.16.0 → 2.17.0. Not released. Follow-up requested: wire real AI upscale providers (Stability/fal/Replicate/ Leonardo already in-repo; Topaz/Magnific/Claid/etc. as new providers), NSFW-gated where applicable.

Single view: re-roll / variation + tracked ancestry, Markdown export, table fix (2.16.0, branch feature/single-derive-ancestry)

Ported the v1-2 single-image actions to the v3 SPA and tidied the details table, per an owner request ("variety + re-roll button, look at how v1-2 did it; tracked ancestry; copy-as-markdown; fix the table — OpenAI images carry foreign ComfyUI metadata").

  • lib/derive.js (new) — the v3 port of v1-2's loadRerollData. deriveImage({item, kind, source}) resolves a prompt from a captured layer (dpl re-resolves the recipe through generatePrompt; ai / roll reuse the verbatim text), reuses the parent's provider + settings snapshot, forces a fresh seed (v1-2 deliberately never copied the seed), runs the provider adapter, and ingests a child sidecar carrying parent + derivedKind/derivedSource. Gated by canDerive (provider capabilities.seed) and hasSource. Seedless providers (OpenAI) lock the cluster with a tooltip rather than failing.
  • Ancestry, scan-built (gallery.js#linkAncestry) — exactly v1-2's indexImages.deepLink idea: each child names its parent, and the feed scan hangs it off that parent's children list (self-healing — a child whose parent is gone is just unlinked). Wired into App.loadFeed. New tests in gallery.test.js (+3).
  • SingleView.jsx — a "Make another" cluster (Re-roll = DPL; Variation = DPL / Translated / Original, each enabled only when that layer exists); a pending placeholder (spinner + the resolved prompt) while the new image generates, since a derive opens a fresh single-image target (App holds a pending current); a Lineage card (type label + parent link + child-thumbnail strip); a Copy as Markdown button on the Details card; and a capability-gated table — rows for sampler/steps/cfg/seed are hidden when the provider declares them false, and REST_DROP strips app-orchestration keys from "All settings".
  • App.deriveImage orchestrates: optimistic pending current → run derive (prompt fills in first) → loadFeed (re-links ancestry) → land on the new child; errors revert to the source image.
  • Snapshot leak fixed at the source (Home.jsx#cleanSnapshot) — sidecars now store a provider-scoped settings snapshot (app keys, providerParams, nested objects, empties dropped), so a future OpenAI image no longer carries another provider's sampler/steps. The table's capability gating covers already-saved ones.
  • Verify: npm run i18n (regenerated en.json + pseudo), lint (0 errors), smoke, gui build (browser glob), test:web 196, test:unit 226 — all green. On feature/single-derive-ancestry; not released.

Finish SPA localization — the two deferred DPL-technical modules (2.15.0, branch feature/i18n-react-intl)

Closed out the one gap left from the 2026-06-28 i18n pass: the two DPL-technical lib modules that were shipped English-only are now localized, so SPA coverage is complete (~480 messages).

  • validateDpl.js — the editor lint/status diagnostics. Localized with defineMessages + react-intl, but as a non-React isomorphic module it takes an optional intl arg: validateDpl(text, intl?). When none is passed it uses a module-level createIntl({locale:"en"}) fallback, so the existing message-asserting tests (tests/validateDpl.test.js, 9) and any non-React caller get byte-identical English with no changes. Literal braces ({list}, {#generator}, '{', '}') are passed as message arguments so ICU substitutes them verbatim instead of trying to parse them as placeholders — the rendered English still matches the tests' substring assertions exactly.
  • dplInserts.js — the DPL insert teaching catalog. Converted from a static DPL_INSERTS array to a getDplInserts(intl) builder: label/hint/desc localized; syntax/template/example stay literal DPL. The one brace-bearing description (call{#name}) passes the token as an arg.
  • Call sites threaded intl through: DplStatus (already had useIntl) now calls dplStatus(value, intl); the DplEditor CodeMirror linter became a makeDplLinter(intlRef) factory reading a live intlRef (the editor is built once on mount, so a ref keeps the locale current); DplInsertBar memoizes getDplInserts(intl).

Verification (all green): npm run i18n (extract + pseudo) → 480 messages, en-XA regenerated; npm --prefix gui run build; npm test at engine-v3 root — lint (0 errors, 18 pre-existing warnings), smoke OK, test:unit 128, test:web 60 (incl. the 9 validateDpl tests unchanged); npm run lint:i18n clean. Still 2.15.0 (same feature) — notes/changelog/status/decisions updated to mark the modules done. Not yet released — awaiting the owner's go-ahead to merge feature/i18n-react-intl to main.