Skip to content

fix(build): move appstate.py + routers/ under lib/ so the packaged desktop app ships them - #836

Merged
byrongamatos merged 1 commit into
mainfrom
fix/r3-bundle-appstate-routers
Jul 10, 2026
Merged

fix(build): move appstate.py + routers/ under lib/ so the packaged desktop app ships them#836
byrongamatos merged 1 commit into
mainfrom
fix/r3-bundle-appstate-routers

Conversation

@byrongamatos

@byrongamatos byrongamatos commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The break

The packaged desktop app dies at startup on current main:

File ".../Resources/slopsmith/server.py", line 71, in <module>
    import appstate
ModuleNotFoundError: No module named 'appstate'

feedback-desktop's scripts/bundle-slopsmith.sh:51-53 copies a hardcoded list of core files into the bundle:

cp "$SLOPSMITH_DIR/server.py" "$BUNDLE_DIR/"
cp "$SLOPSMITH_DIR/VERSION"   "$BUNDLE_DIR/"
cp -r "$SLOPSMITH_DIR/lib"    "$BUNDLE_DIR/"

The root-level appstate.py (#833) and routers/ (#834) ship correctly in Docker, pass the whole suite, and are then silently dropped from the packaged app. I verified the Dockerfile and docker-compose.yml when I added them; I never checked the desktop bundler. My miss.

The fix

Both move under lib/ — the one core directory every packaging path already copies wholesale, and that all three put on sys.path:

path copies lib/ puts it on sys.path
Dockerfile COPY lib/ /app/lib/ ENV PYTHONPATH=/app/lib:/app
docker-compose.yml - ./lib:/app/lib PYTHONPATH=/app/lib:/app
feedback-desktop cp -r "$SLOPSMITH_DIR/lib" python.ts:520 PYTHONPATH; on Windows build-windows.sh writes ../slopsmith + ../slopsmith/lib into the embeddable ._pth, since PYTHONPATH is ignored in isolated mode

No feedback-desktop change and no new release are required for this to take effect. The Dockerfile / .dockerignore / docker-compose.yml entries I added for the root layout are reverted. git mv, so --follow survives. Nothing else in core changes.

lib/ is also the correct home under Principle V, and always was once the design settled: with the injection seam, appstate.py constructs nothing and does no import-time IO, and a route module only builds an APIRouter. The premise that forced root placement — "appstate opens sqlite at import" — stopped being true the moment configure() replaced ownership. I didn't revisit the placement decision after the design changed.

The guard

tests/test_packaging.py walks server.py's module-level imports, keeps the ones that resolve inside this repo, and fails if any lives outside a directory the packagers copy:

AssertionError: server.py imports `appstate` from appstate.py, which no packager copies.
The desktop bundler (scripts/bundle-slopsmith.sh) copies only ('server.py', 'main.py')
and ('lib', 'plugins', 'data', 'static')/, so the packaged app would die at startup with
ModuleNotFoundError: No module named 'appstate'.
Move it under lib/, or teach bundle-slopsmith.sh + Dockerfile + .dockerignore +
docker-compose.yml about it and update BUNDLED_ROOTS.

Negative-checked: restoring appstate.py to the root fails it. It also has to skip origin in {"built-in", "frozen"} — on Python 3.14 the frozen stdlib reports origin="frozen", and Path("frozen").resolve() lands inside the repo, which flagged os and stat as first-party.

Verification

  • Replicated bundle-slopsmith.sh's copy exactly into a temp dir and booted it with PYTHONPATH=<bundle>:<bundle>/lib: import server, import appstate, import routers.audio_effects all resolve, appstate.meta_db is server.meta_db, 143 routes. The same simulation against origin/main reproduces the production ModuleNotFoundError. That's the check that was missing.
  • pytest2398 passed (2348 + 50 new), 4 skipped.
  • Route table still identical to origin/main — 143 routes, same paths, methods, registration order.
  • Docker build context reaches lib/appstate.py and lib/routers/, with no __pycache__ (verified against the real daemon).
  • Native uvicorn boot smoke: /api/version, /api/library, the moved audio-effects router, and all three migrated plugins' src/ module graphs.
  • pyflakes clean; npm run lint 0 errors; Codex preflight 0 findings.

One small follow-up for feedback-desktop (not blocking)

bundle-slopsmith.sh:54 does rm -rf "$BUNDLE_DIR/lib/__pycache__" — top level only. lib/routers/__pycache__ now exists in a dev checkout and would ride along in a local build (CI clones fresh, so CI builds are unaffected). Harmless — CPython validates .pyc against source mtime — but find "$BUNDLE_DIR/lib" -name __pycache__ -prune -exec rm -rf {} + would be tidier.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an audio-effects mapping API for listing, saving, deleting, activating, and clearing mappings.
    • Supports filtering mappings by song, filename, tone, and provider.
  • Bug Fixes

    • Fixed desktop application startup failures caused by missing packaged modules.
    • Improved Docker and desktop packaging so required application components are included consistently.
  • Documentation

    • Updated the changelog with the latest routing and packaging improvements.

… ships them

The packaged desktop app died at startup:

    File ".../Resources/slopsmith/server.py", line 71, in <module>
        import appstate
    ModuleNotFoundError: No module named 'appstate'

feedback-desktop's scripts/bundle-slopsmith.sh copies a HARDCODED list of core
files into the bundle -- server.py, VERSION, lib/, data/, static/,
plugins/__init__.py. The root-level appstate.py (#833) and routers/ (#834)
shipped fine in Docker, passed every test, and were silently dropped from the
packaged app.

Both now live under lib/, the one core directory every packaging path already
copies wholesale -- Dockerfile `COPY lib/`, docker-compose.yml, and the desktop
bundler's `cp -r lib` -- and that all three put on sys.path (on Windows via the
embeddable-Python ._pth, where PYTHONPATH is ignored: build-windows.sh writes
`../slopsmith` and `../slopsmith/lib`). No feedback-desktop change and no new
release are needed for this to take effect.

lib/ is also the CORRECT home under Principle V, and always was once the design
settled: with the injection seam, appstate.py constructs nothing and does no
import-time IO, and a route module only builds an APIRouter. The premise that
forced root placement -- "appstate opens sqlite at import" -- stopped being true
when configure() replaced ownership. The Dockerfile / .dockerignore /
docker-compose.yml entries added for the root layout are reverted; nothing else
in core changes (git mv, so --follow survives).

tests/test_packaging.py is the guard: it walks server.py's module-level imports,
keeps the ones resolving inside this repo, and fails if any lives outside a
directory the packagers copy -- with the ModuleNotFoundError spelled out. So the
next root-level core module can't ship broken. Negative-checked: restoring
appstate.py to the root fails it; the message names the file and the four
packaging files a root module would have to teach.
(It also has to skip `origin in {"built-in","frozen"}` -- on 3.14 the frozen
stdlib reports origin="frozen", and Path("frozen").resolve() lands inside the
repo, which flagged `os` and `stat` as first-party.)

Verified: the bundler's copy replicated exactly into a temp dir and booted with
PYTHONPATH=<bundle>:<bundle>/lib -- `import server`, `import appstate`,
`import routers.audio_effects` all resolve, appstate.meta_db is server.meta_db,
143 routes. The same simulation against origin/main reproduces the production
ModuleNotFoundError. pytest 2398 passed (2348 + 50 new); route table still
identical to origin/main (paths, methods, order); docker build context reaches
lib/appstate.py and lib/routers/ with no __pycache__; native uvicorn boot smoke
serves /api/version, /api/library, the moved audio-effects router, and all three
migrated plugins' src/ graphs; eslint 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1543a541-5136-4e6f-90f0-e7cdb663679a

📥 Commits

Reviewing files that changed from the base of the PR and between ebe59d3 and 3c27ca7.

📒 Files selected for processing (9)
  • .dockerignore
  • CHANGELOG.md
  • Dockerfile
  • docker-compose.yml
  • lib/appstate.py
  • lib/routers/__init__.py
  • lib/routers/audio_effects.py
  • server.py
  • tests/test_packaging.py
💤 Files with no reviewable changes (3)
  • .dockerignore
  • docker-compose.yml
  • Dockerfile

📝 Walkthrough

Walkthrough

The PR adds audio-effects mapping routes under lib/routers, removes root-level Docker packaging rules, updates Compose mounts, and adds packaging tests that enforce lib-based module placement.

Changes

Audio-effects routing and packaging

Layer / File(s) Summary
Audio-effects mapping API
lib/routers/audio_effects.py
Adds FastAPI endpoints for listing, upserting, deleting, activating, and clearing audio-effects mappings, with 400 and 404 responses for validation and missing mappings.
lib-based packaging wiring
lib/appstate.py, lib/routers/__init__.py, Dockerfile, .dockerignore, docker-compose.yml, server.py
Removes root-level appstate/router copies and mounts, documents the lib layout, and adds development mounts for packaged runtime resources.
Packaging regression checks
tests/test_packaging.py
Validates that top-level server.py imports resolve to approved bundled roots and that appstate and routers resolve under lib.
Release documentation
CHANGELOG.md
Documents the packaging startup fix, the lib/routers/audio_effects.py location, and the updated router move details.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant audio_effects_router
  participant appstate_audio_effect_mappings
  Client->>audio_effects_router: Send audio-effects mapping request
  audio_effects_router->>appstate_audio_effect_mappings: Execute mapping operation
  appstate_audio_effect_mappings-->>audio_effects_router: Return result or ValueError
  audio_effects_router-->>Client: Return JSON response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: moving appstate.py and routers under lib/ to fix packaging for the desktop app.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/r3-bundle-appstate-routers

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 29-30: Update the changelog heading to use `lib/routers/` instead
of `routers/`, keeping it consistent with the documented implementation path.

In `@lib/routers/audio_effects.py`:
- Line 43: Update upsert_audio_effect_mapping to declare its request body
parameter using Annotated rather than Body(...) as a default value, avoiding
Ruff B008; preserve default_factory=dict on activate_audio_effect_mapping.

In `@tests/test_packaging.py`:
- Around line 33-35: Remove "plugins" from BUNDLED_ROOTS and represent only
plugins/__init__.py in the explicit bundled-file expectations, updating the
packaging guard and related documentation/tests to enforce the desktop bundler’s
exact plugin inclusion rather than allowing the entire directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1543a541-5136-4e6f-90f0-e7cdb663679a

📥 Commits

Reviewing files that changed from the base of the PR and between ebe59d3 and 3c27ca7.

📒 Files selected for processing (9)
  • .dockerignore
  • CHANGELOG.md
  • Dockerfile
  • docker-compose.yml
  • lib/appstate.py
  • lib/routers/__init__.py
  • lib/routers/audio_effects.py
  • server.py
  • tests/test_packaging.py
💤 Files with no reviewable changes (3)
  • .dockerignore
  • docker-compose.yml
  • Dockerfile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 29-30: Update the changelog heading to use `lib/routers/` instead
of `routers/`, keeping it consistent with the documented implementation path.

In `@lib/routers/audio_effects.py`:
- Line 43: Update upsert_audio_effect_mapping to declare its request body
parameter using Annotated rather than Body(...) as a default value, avoiding
Ruff B008; preserve default_factory=dict on activate_audio_effect_mapping.

In `@tests/test_packaging.py`:
- Around line 33-35: Remove "plugins" from BUNDLED_ROOTS and represent only
plugins/__init__.py in the explicit bundled-file expectations, updating the
packaging guard and related documentation/tests to enforce the desktop bundler’s
exact plugin inclusion rather than allowing the entire directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1543a541-5136-4e6f-90f0-e7cdb663679a

📥 Commits

Reviewing files that changed from the base of the PR and between ebe59d3 and 3c27ca7.

📒 Files selected for processing (9)
  • .dockerignore
  • CHANGELOG.md
  • Dockerfile
  • docker-compose.yml
  • lib/appstate.py
  • lib/routers/__init__.py
  • lib/routers/audio_effects.py
  • server.py
  • tests/test_packaging.py
💤 Files with no reviewable changes (3)
  • .dockerignore
  • docker-compose.yml
  • Dockerfile
🛑 Comments failed to post (3)
CHANGELOG.md (1)

29-30: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the actual lib/routers path in the heading.

The heading still says routers/, contradicting the implementation path documented on Line 30. Rename it to lib/routers/ for consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 29 - 30, Update the changelog heading to use
`lib/routers/` instead of `routers/`, keeping it consistent with the documented
implementation path.
lib/routers/audio_effects.py (1)

43-43: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
fd -HI '^(pyproject\.toml|setup\.cfg|tox\.ini)$' -x \
  rg -n 'fastapi|ruff|B008' {}

Repository: got-feedBack/feedBack

Length of output: 159


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files | rg '^(lib/routers/audio_effects\.py|pyproject\.toml|setup\.cfg|tox\.ini|ruff\.toml|requirements.*\.txt|Pipfile|poetry\.lock|uv\.lock)$'

printf '\n== audio_effects outline ==\n'
ast-grep outline lib/routers/audio_effects.py --view expanded || true

printf '\n== relevant lines ==\n'
cat -n lib/routers/audio_effects.py | sed -n '1,220p'

printf '\n== lint config / fastapi pins ==\n'
for f in pyproject.toml setup.cfg tox.ini ruff.toml requirements.txt requirements-dev.txt Pipfile poetry.lock uv.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n '(^\s*(fastapi|ruff)\b|B008|select\s*=|ignore\s*=|per-file-ignores|extend-ignore|line-length|version\s*=|==)' "$f" || true
  fi
done

Repository: got-feedBack/feedBack

Length of output: 4412


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo '== lib/routers/audio_effects.py =='
cat -n lib/routers/audio_effects.py | sed -n '1,220p'

echo
echo '== lint / dependency config =='
for f in pyproject.toml setup.cfg tox.ini ruff.toml requirements.txt requirements-dev.txt Pipfile poetry.lock uv.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    rg -n 'fastapi|ruff|B008|select\s*=|ignore\s*=|extend-ignore|per-file-ignores|version\s*=|==|dependencies' "$f" || true
  fi
done

Repository: got-feedBack/feedBack

Length of output: 3893


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo '== lib/routers/audio_effects.py =='
cat -n lib/routers/audio_effects.py | sed -n '1,220p'

Repository: got-feedBack/feedBack

Length of output: 3681


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo '== fastapi / ruff references =='
rg -n --hidden --glob '!**/.git/**' -S 'fastapi|B008|Body\(' .

echo
echo '== audio_effects file =='
cat -n lib/routers/audio_effects.py | sed -n '1,180p'

Repository: got-feedBack/feedBack

Length of output: 12008


Move these request-body params to Annotated. Body(...) in the default position triggers Ruff B008; keep default_factory=dict on activate_audio_effect_mapping.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 43-43: Do not perform function call Body in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/routers/audio_effects.py` at line 43, Update upsert_audio_effect_mapping
to declare its request body parameter using Annotated rather than Body(...) as a
default value, avoiding Ruff B008; preserve default_factory=dict on
activate_audio_effect_mapping.

Source: Linters/SAST tools

tests/test_packaging.py (1)

33-35: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not model plugins/ as a wholesale bundled root.

The module documentation says the desktop bundler copies only plugins/__init__.py, but BUNDLED_ROOTS allows every module under plugins/. A future server.py import of plugins.some_module would therefore pass this guard while still being omitted from the packaged app.

Proposed fix
-BUNDLED_ROOTS = ("lib", "plugins", "data", "static")
-BUNDLED_FILES = ("server.py", "main.py")
+BUNDLED_ROOTS = ("lib", "data", "static")
+BUNDLED_FILES = ("server.py", "main.py", "plugins/__init__.py")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

# Directories every packaging path copies wholesale, plus the files copied by name.
BUNDLED_ROOTS = ("lib", "data", "static")
BUNDLED_FILES = ("server.py", "main.py", "plugins/__init__.py")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_packaging.py` around lines 33 - 35, Remove "plugins" from
BUNDLED_ROOTS and represent only plugins/__init__.py in the explicit
bundled-file expectations, updating the packaging guard and related
documentation/tests to enforce the desktop bundler’s exact plugin inclusion
rather than allowing the entire directory.

@byrongamatos
byrongamatos merged commit b321569 into main Jul 10, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant