Quick table of errors hit and how they were resolved. Add a row whenever you fix something reusable.
| Error / symptom | Cause | Fix |
|---|---|---|
ERR_MODULE_NOT_FOUND for a local import |
ESM relative import without a file extension | Add .js: import x from "./foo.js". |
require is not defined in ES module scope |
Leftover CommonJS require in an ESM file |
Convert to import, or — only for synchronous config-driven plugin loading / legacy migration — const require = createRequire(import.meta.url). See esm-patterns.md. |
__dirname is not defined |
__dirname/__filename don't exist in ESM |
Use import.meta.dirname / import.meta.url. |
A block loaded but fn is not a function |
require(esm) returns a namespace, not the function |
Call .default(...); read .full / .suggestion_exclude as named exports. |
| Settings read from the wrong directory after ESM conversion | Imports evaluate before top-level process.chdir |
Move the chdir into chdir.js and import it first in common.js. |
Cannot find package 'node-fetch' |
Dependency removed in 2.0.0 | Delete the import; use the global fetch. |
ESLint: 'console' is not defined in a stray .mjs |
A scratch file got no Node globals from the flat config | It's a temp file — delete it, or add Node globals / ignore it. |
ESLint errors no-useless-escape / no-dupe-else-if in prompt files |
Pre-existing redundant escapes / duplicate else if branches |
Left as warnings on purpose (changing them can change generated prompts). Review deliberately; don't bulk-rewrite. |
Tauri updater build: failed to decode base64 secret key: Invalid symbol 239, offset 0 |
The TAURI_SIGNING_PRIVATE_KEY CI secret has a UTF-8 BOM (0xEF) — from setting it via Get-Content -Raw key | gh secret set in PowerShell. Installers still build; only the final signing step fails. |
Re-set BOM-free: gh secret set TAURI_SIGNING_PRIVATE_KEY --body ([System.IO.File]::ReadAllText("$env:USERPROFILE\.tauri\rap-updater.key").Trim()), then gh run rerun <id> --failed. See desktop-updater.md. |
stylelint fails in CI but npm run format/tests passed locally |
npm run lint = eslint . && stylelint …; running only format/tests skips stylelint (modern rgb() notation, rule-empty-line-before, etc.) |
Run the full npm run lint (or stylelint --fix) before pushing CSS. |
Bash sandbox reports package.json truncated / "Unterminated string" |
The Cowork bash mount returned a false, truncated view of a file the host had written correctly (risk of acting on bad data / data loss) | Don't use the bash sandbox for file work here. Use PowerShell + the Read/Edit/Write tools, which see the real host files. |
npm run lint / format suddenly flooded with errors after adding files under assets/ |
assets/ is gitignored but the tools walk the filesystem — ESLint/Prettier indexed the local reference snapshot (old CommonJS/jQuery → 100s of no-undef). Gitignored ≠ tool-ignored. |
Exclude assets/ in eslint.config.js ignores and .prettierignore (JSDoc only reads its configured source.include, so it's unaffected). Restore a tool-modified snapshot from its own .git (git checkout -- . inside it). |
Doc generator: a doc comment never closes (e.g. Reached end of file while still inside a (nested) comment) |
A /** … */ JSDoc block whose text contains /* (e.g. a glob like presets/*.json or output/*.json) — the /* opens a nested comment that breaks the block |
Reword to avoid /* and */ sequences (drop the *: write data/presets or the output/ sidecars). |
Source files show mojibake (em-dash → ‗,  , é) after a PowerShell edit script |
Windows PowerShell Get-Content -Raw reads as ANSI (Windows-1252), not UTF-8; writing back as UTF-8 double-encodes any non-ASCII char |
Read/write UTF-8 explicitly: [IO.File]::ReadAllText(p,[Text.Encoding]::UTF8) / WriteAllText(p,s,(New-Object Text.UTF8Encoding $false)) — or just use the Read/Edit tools. Repair a single round-trip: [Text.Encoding]::UTF8.GetString([Text.Encoding]::GetEncoding(1252).GetBytes($s)). |
gh api --method PUT … --input - fails with Problems parsing JSON (HTTP 400) |
PowerShell pipes a string to a native process's stdin as UTF-16LE; gh expects UTF-8 and chokes |
Write the JSON body to a UTF-8 (no BOM) temp file and pass --input <file> (not -): [IO.File]::WriteAllText($f,$json,(New-Object Text.UTF8Encoding $false)); gh api --method PUT … --input $f. |
| A third-party widget (CodeMirror tooltip / autocomplete) ignores our theme colours — renders its own default (light in dark mode, unreadable) | The library injects its base theme as an UNLAYERED <style>; our CSS is in @layer components. Unlayered normal rules beat layered ones regardless of specificity, so the library defaults win. |
Force our surface colours with !important (background/border/text) — important declarations win over the library's unlayered normal ones. (Or move the overrides to an unlayered sheet.) See generic-code-highlighting.css (.cm-tooltip*). |
A Vitest suite intermittently FAILs with all tests skipped and a duration pinned at ~10000ms (e.g. promptEngine.integration — 18 skipped, 10020ms) — but passes in isolation |
Its beforeAll does real async work (ensureCatalog() glob-imports the whole data corpus, ~9s) and races the default 10s hook timeout; a heavier parallel suite tips it over, so Vitest fails the suite and skips its tests. Not a code bug. |
Give the slow warmup hook explicit headroom: beforeAll(() => ensureCatalog(), 30000). Confirm it's load-driven by running the file alone (passes) vs. the full suite (fails). |
The CodeMirror line-number gutter is a bright-white slab and the active line is a milky blue wash in dark mode (computed .cm-gutters bg = rgb(245,245,245), .cm-activeLine = rgba(204,238,255,.267)) |
CM 6's gutter baseTheme ships BOTH &light .cm-gutters {#f5f5f5} and a &dark variant, chosen by whether the view's theme sets { dark: true }. Our editors never did → CM stayed light and injected the light chrome at the SAME specificity as .dpl-editor .cm-gutters, beating it on source order (CM injects late). Plain CSS can't win. |
Theme the chrome via an EditorView.theme({...}) extension (StyleModule priority > baseTheme) — targets/web/frontend/lib/editorChrome.js, shared by both editors. Use CSS vars for colours so it stays light/dark adaptive (no hard dark flag). NOT plain CSS. |
| The CodeMirror editor (DPL box) freezes the whole renderer when it mounts (Chrome tab unresponsive, CDP screenshots time out; navigating away recovers it) | scrollbar-gutter: stable (or similar layout property) on the .cm-scroller fights CM's own scrollbar/size measure loop — CM re-measures, the gutter changes the metrics, CM re-measures again, forever. |
Don't put scrollbar-gutter on the CM scroller. To keep an overlay control off the scrollbar, inset the control instead (right: 1.25rem); CM already wraps long lines inside the scrollbar, so the text stays clear. |
A prop is passed by the parent but the child silently ignores it (e.g. App.js passed onOpenImage to GenerateScreen, which never destructured it) → the UI renders fine but the control is DEAD (generated thumbs weren't tappable) |
JS destructuring makes an unused prop a silent no-op — no error, no warning. Marker/regex "surface parity" checks and render-only tests can NEVER catch this: the element is present, it just does nothing. This is exactly how it shipped. | Guard BOTH ends with interaction tests: (1) in the child's test, fireEvent.press the control and assert the callback fired with the right payload; (2) in the parent's test, capture the mocked child's props and assert the callback is a function (typeof props.onOpenImage === "function"). Prove the test by re-introducing the bug — it must FAIL. See targets/mobile/screens/__tests__/GenerateScreen.test.jsx + __tests__/App.test.jsx. |
| A generated/derived image can't be opened in the detail view even though its thumbnail renders | The row stored the raw provider source (data: / https:) instead of the saved gallery item. SingleScreen resolves by saved gallery uri (items.find(it => it.uri === image.uri)), so a raw source can never match. |
Store what saveImageSrc() RETURNS ({ name, uri }) in the row, not the provider's source, and pass that object to onOpenImage. |
An absolutely-positioned overlay pinned over a CodeMirror editor (e.g. Manage's Modify/Draft corner) overlaps the code on a narrow pane, even though a padding-top gutter was added to .cm-content to clear it |
CM's baseTheme sets .cm-content { padding: 4px 0 } unlayered, at a specificity our .dpl-editor-wrap .dpl-editor .cm-content rule can't beat on source order — so the intended gutter (2.5rem) computes as 4px and the overlay lands on the text. Wide panes hide it (short lines never reach the corner); it only bites on the phone width. Measure the computed padding-top to confirm (4px, not 40px). |
Don't rely on the CM gutter for overlay clearance. On the narrow breakpoint, un-pin the overlay: make the wrapper a flex column and set the overlay position: static; order: -1; align-self: flex-end so it sits above the editor. If its popover is clipped by an overflow: hidden ancestor, float the popover to a position: fixed bottom sheet (see mobile-sheets.css). Example: manage-responsive.css. |
A shared module can't be imported by a target at all (Metro: import.meta undefined / a Vite import.meta.glob is meaningless; Node: same) -- so the target hand-ports it and the copies drift |
A plugin pool needs discovery, and every runtime discovers differently: Vite import.meta.glob, Node fs.readdirSync, Metro neither (static module graph, no fs). A shared module that reaches for one locks the other targets out. |
Discover through a generated static index -- plain import statements, the one construct all three understand (scripts/build-provider-registry.mjs -> targets/shared/registry.generated.js; a --check mode in npm test fails if stale). Keep shared modules free of import.meta / node:, and inject anything platform-specific (see _shared/transport/config.js). |
A PowerShell-driven node -e "..." codemod reports success but silently corrupts the files (here: it ate the label: line in 15 provider configs and wrote a literal \n) |
PowerShell interpolates $ inside the double-quoted command string, so a regex backreference (\/``) never reaches Node -- the replacement drops the captured text. The script still prints its happy changed N message. |
Never inline node -e through PowerShell when the code contains $ -- write a real .mjs script file and run node script.mjs. And always read a file back after a scripted bulk edit instead of trusting the script's own success message (cf. the [IO.File]::ReadAllText relative-path landmine). |
Jest (jest-expo / React Native) throws A dynamic import callback was invoked without --experimental-vm-modules as soon as a test imports a module that lazy-loads with import() |
Jest runs in Node's CommonJS VM, where a native import() can't work. babel-preset-expo deliberately LEAVES import() alone (Metro uses it for code-splitting), so it reaches the VM intact. The shared provider manifests lazy-load their code/ + settings.js exactly this way. |
Add babel-plugin-dynamic-import-node to babel.config.js under env.test only -- it compiles import() down to require() for tests while the real Metro bundle keeps its dynamic imports. Also give Jest a moduleNameMapper for any Metro extraNodeModules alias (^shared/(.*)$) AND for ^@babel/runtime/(.*)$ (helpers can't resolve from a file outside the package). |
A whole RN screen renders BLANK (white); the minified bundle logs Cannot access 'Ge' before initialization. Every unit test still passes. |
A hook/const referenced a value declared LOWER in the component (here a useRef(settings) placed above const settings = useMemo(...)) -> temporal-dead-zone ReferenceError, which React turns into an empty tree. The component tests never caught it because they mock the engine and the theme, so the real module/declaration order is never exercised. |
Declare-before-use inside the component body (move the block below the value it reads). And treat this as the proof that render-only + mocked unit tests cannot replace LOOKING at the app -- the screenshot was the only thing that saw it. Wire a page.on('pageerror') listener into any screenshot harness so a crashed render is reported, not silently shot as a white PNG. |
| A "surface parity" marker check passes while the feature is entirely missing from the target | The marker regex was satisfied by an UNRELATED identifier that happened to share a word -- /suggestion/ matched the caret-completion suggestions array, so the web's rotating random-suggestion + shuffle control was reported present on mobile for months while it did not exist. A later attempt to fix it with /ShuffleIcon/ was ALSO wrong: the import line satisfies it even with the button deleted. |
A marker must match the wiring, not a name or an import (/onPress=\{useSuggestion\}/). Prove it: delete the feature, watch the gate go RED, restore. A marker that can be satisfied by an unrelated identifier is worse than no marker -- it buys false confidence. |
| A "supported load" number (1000 prompts, 100k gallery) gets implemented as a hard CAP -- the web silently truncated every roll to 50, mobile clamped at 1000 -- so the app refuses work it advertises, with no message | Someone read a performance promise ("this much with no degradation") as permission ("no more than this"). It then got locked in by tests that asserted the cap (expect(len(999)).toBe(50) // capped). A test that asserts a bug is the bug's best defender -- it converts a defect into a specification and makes the fix look like a regression. |
Documented capacities are a floor for what must stay FAST, never a ceiling on what the user may ask for. Keep validity constraints (>= 1, integer); delete limits. When a test says // capped, ask who decided that -- and check the product intent before believing the assertion. |
CI (or a fresh clone) fails to build with [UNRESOLVED_IMPORT] Could not resolve '<old path>' after a file was MOVED -- while every gate passes locally |
The importer updates exist in the working tree but were never committed: a multi-path git add silently staged only some of the listed files (13 of 17). Tests/build/parity all read the working tree, so they went green over a commit that doesn't compile. A working tree is not evidence. |
Run npm run check:committed after committing (fails when tracked source differs from HEAD), and for a release build from a clean git worktree at HEAD. Always diff git diff --cached --numstat against the files you actually touched -- never assume a multi-path git add took. |
A git commit -m "…" (or any command) run through PowerShell executes text that was only meant to be quoted — here, a commit message describing an injection payload spawned calc |
PowerShell parses the whole command line: &, ` |
, `` ``, $(…) inside a double-quoted argument are still operators. The message was about x" & calc & ".png, and PowerShell obligingly ran it. |
A backend endpoint runs a shell string built from a request value (exec(\cmd /c start "" "${fp}"`)`) — and the path validator "already sanitizes" it |
The validator (resolveOutputFile) blocked path traversal and nothing else. A filename may legally contain " and &; in a shell string those end the argument and start a command (CodeQL js/command-line-injection, critical). "Local-only backend" is one --host flag from a LAN. |
Remove the shell, don't improve the escaping — escaping is a losing game played on the attacker's board. Build {cmd, args} and call execFile; argv goes straight to the OS, so a quote in a filename is a quote in a filename. Keep the command builders pure so the invariant is testable (backend/osCommands.js + tests/regression/commandInjection.test.js). Not cmd /c either — cmd re-parses its own arguments (CVE-2024-27980). |
Every SonarCloud scan fails with ERROR Organization key '<new-name>' does not exist. after the GitHub account was renamed |
A repo-wide rename of the old GitHub login swept up sonar.organization / sonar.projectKey in sonar-project.properties. Those are SonarCloud's own keys, minted when the project was bound; they do not follow a GitHub login change. |
Restore the Sonar keys to what SonarCloud shows on the project's Information page (here: junebug12851…). A find-and-replace across "current-state files" does not get to redefine a third party's identifiers — exclude external service keys (Sonar, Codecov, Netlify, app IDs) from any rename sweep, and confirm the scan passes before believing it's fixed. |
| An e2e test "proves" a serious app defect (the app never renders 1000 prompts) — but the app's own instrumentation says it finished long ago | The test waited for "1000 generated", and the app accumulates results across rolls, so the label actually read "1220 generated". The assertion could never come true; the 10-minute timeout then dressed a test bug up as an app failure — and I chased a render bug for hours while the device log said committed 1220 result rows, 34 ms after the roll. |
Reset the app's state at the start of each measured interaction (here: tap Clear all), and assert on something that can only mean what you think it means. And when a test's verdict contradicts the app's own telemetry, suspect the test first: it is the newer, less-exercised code. Read the instrument you built before you distrust the thing it measures. |
| An expensive CI job (native build + emulator, 30–45 min) runs on every push, so every one-line fix costs 40 minutes and the gate becomes something to route around | Slow gates were wired to push like the fast ones. Cost is not the same as value: this one protects the release, not the commit. |
Two tiers: fast checks on every push; expensive ones if: gated to the release path (PR into main, pushes to main, workflow_dispatch). main is still protected — the release PR must be green — and dev iteration stays fast. |
CI is RED on Format check while the local npm test is green (9 files, none of them yours) |
npm test ran lint but not format:check; CI runs both. So a session can end "green" on a commit CI rejects — which is exactly what happened: dev sat red for an hour on files the previous session wrote and never formatted. A gate you don't run is not a gate — and a local gate that is a SUBSET of the CI gate is a lie about what green means. |
format:check is now inside npm test. When you add a check to CI, add it to npm test in the same change (and vice-versa); the two must be the same gate. |
| Prettier says a generated file is unformatted, but formatting it makes its freshness check say STALE | Two gates demanding opposite things: format:check wants Prettier's output; check:registry compares the file byte-for-byte against what the generator would emit — and the generator hand-assembled strings that weren't Prettier-clean. Fixing either one breaks the other. |
Format at the SOURCE: the generator runs its rendered text through Prettier (format(src, {...await resolveConfig(out), parser:"babel"})) before writing and before comparing. Now the file it writes is the file both gates expect. Never hand-format a generated file. |
node_modules is suddenly EMPTY (Cannot find package 'vite') after cleaning up a temporary git worktree |
A junction/symlink was created from the throwaway worktree to the real node_modules to avoid a reinstall; git worktree remove --force then followed the link and deleted the TARGET's contents. |
Never link node_modules into a worktree you intend to force-remove. Recover with npm install (+ npm --prefix targets/mobile install) -- nothing is lost, it's derived. |