Skip to content

Phase 37: make the built wheel actually work - #24

Merged
thezoid merged 19 commits into
masterfrom
chore/v4.0-milestone-close
Aug 3, 2026
Merged

Phase 37: make the built wheel actually work#24
thezoid merged 19 commits into
masterfrom
chore/v4.0-milestone-close

Conversation

@thezoid

@thezoid thezoid commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Closes the v5.0 Phase 37 requirements PKG-01 through PKG-06.

The problem

The built wheel was not degraded, it was dead. shoppybot --help failed at import on pydantic_settings before reaching any command dispatch. Measured, not inferred.

What changed

Before After
shoppybot --help from clean install ModuleNotFoundError exit 0
Modules importable clean 3 of 8 8 of 8
Data files in wheel 0 9
Declared runtime deps 1 9
  • PKG-01 sounds/ moved into core/sounds/ as a real package. It was structurally unshippable before: utils is a top-level module, so os.path.dirname(__file__) resolved to site-packages/sounds, which cannot be package data of anything. Plus package-data globs for web/static and web/templates.
  • PKG-02/03 9 runtime dependencies declared. colorama and pyyaml were reaching Windows installs only by accident via click's win32 marker and uvicorn[standard], and only under the web extra, so a bare pip install shoppybot failed on every OS.
  • PKG-04 dead selenium and webdriver-manager pins removed.
  • PKG-05 scripts/verify_wheel.py, a five-assertion gate, plus a CI wheel job on ubuntu and windows that installs without requirements.txt.
  • PKG-06 bundled_plugins_dir() added as the named plugin-root seam. Computed from __file__, deliberately not via _repo_root(), which honours a test-monkeypatched override. Three inline call sites collapsed into it.

Phase 43 gate: answered, favorably

PKG-06 was a declared hard gate on Phase 43. The bundled plugin root does survive a wheel install: site-packages/plugins with all 7 shopbot_plugin_*.py. No importlib.resources rewrite needed. Phase 43 is unblocked.

Verification

Local suite 969 passed / 2 skipped. Every packaging claim was checked by building a real wheel and probing it in a clean venv, never by reading pyproject.toml back.

The wheel gate's negative control was run for real, twice, because assertion 3 makes two independent claims. Doctored wheels produced exit 1 naming assertion 3: once for files missing from the wheel, once for files shipping but not resolving at runtime. That second case is exactly what a zip-entry-only check would have reported green.

version = "2.0.0" untouched; release-please owns versioning.

Note

This PR is also what first exercises the new wheel CI job. The ubuntu leg has never run.

thezoid added 18 commits August 2, 2026 15:13
- git mv the three alert wavs from sounds/ to core/sounds/ so they can be package data
- git mv generate_alert_sounds.py to scripts/, out of the shipped runtime tree
- utils.SOUNDS_DIR now resolves via importlib.resources.files("core.sounds"), no fallback
- add tests/test_packaging.py asserting SOUNDS_DIR exists, is core/sounds, holds all 3 cues
- update README.md and CLAUDE.md sound paths
- declare [tool.setuptools.package-data] for core.sounds (*.wav, *.mp3)
- declare web static/*, static/vendor/*, templates/* (vendor needs its own glob)
- set include-package-data = true alongside py-modules
- extend tests/test_packaging.py with the six web data files under the web package

Verified against a real build: wheel grew 61 -> 71 entries, carrying 3 core/sounds
wavs, 5 web/static files, 1 web/templates file, 0 top-level sounds/, 0 generator.
- 37-01-SUMMARY.md records the observed wheel entry counts (61 -> 71)
- PKG-01 marked complete in REQUIREMENTS.md
- ROADMAP.md phase 37 progress 1/4, In Progress
- STATE.md position, decisions, metrics, session continuity
- [project] dependencies goes from 1 entry to 9: colorama, cryptography,
  keyring, nodriver, platformdirs, pydantic, pydantic-settings[yaml],
  pyyaml, requests. Every one is imported unconditionally in the production
  tree; pins match requirements.txt byte for byte.
- colorama and pyyaml are logger.py imports that arrive on Windows only by
  accident (click win32 marker, uvicorn[standard]) and only via the web
  extra, so a bare install fails on any OS without them.
- web extra gains starlette>=0.40 and websockets>=10.4 as floors, not pins,
  so they do not fight fastapi's and uvicorn's own resolution.
- New sound extra holds pygame; utils.py already degrades gracefully and a
  headless install should not pull an audio stack.
- New test extra holds pytest, pytest-asyncio, httpx; nothing on a runtime
  path imports httpx.
- requirements.txt drops the dead selenium and webdriver-manager pins; the
  codebase uses nodriver.
- version = 2.0.0 and all packaging tables left untouched.
- 37-02-SUMMARY.md records the before/after clean-install import matrix
  (3 of 8 -> 8 of 8) and the shoppybot --help exit-0 proof
- PKG-02, PKG-03, PKG-04 marked complete
- STATE.md position advanced to plan 3 of 4
- core/paths.py: bundled_plugins_dir() computed from __file__ directly, never
  via _repo_root(), so the monkeypatchable _REPO_ROOT_OVERRIDE cannot redirect
  a directory whose every .py file is exec_module'd (T-37-10)
- core/orchestrator.py:814 and core/service.py:133,159 call the accessor
  instead of inlining Path(__file__).parent.parent / "plugins"
- drop the now-unused pathlib.Path import from both callers
- tests: behavior preservation against core.orchestrator.__file__ and
  core.service.__file__, override independence, 7-plugin contents, and a
  seam guard so the expression cannot be re-inlined (Phase 43 criterion 5)
- core/registry.py unchanged: it already takes plugins_dir as a parameter
…ch flag

- Phase 43's research flag stated PKG-06 as an open question that could push
  importlib.resources work back into Phase 37; it now states the answer
- bundled_plugins_dir() verified from a clean-venv wheel install: resolves to
  site-packages/plugins with all 7 shopbot_plugin_*.py files present
- retains the scout's namespace caveat, the bundled root lands as a top-level
  site-packages/plugins entry that another distribution could collide with
- EXT-03 unblocked; no other phase entry touched
- 37-03-SUMMARY.md records the PKG-06 answer as the durable input Phase 43 consumes
- STATE.md: position to plan 4 of 4, 3 decisions, 37-03 metric row, 37-04 carry note
- ROADMAP.md: Phase 37 plan progress 3/4, 37-03 checkbox
- REQUIREMENTS.md: PKG-06 marked complete
Encodes the five locked wheel-health assertions in a stdlib-only script so the
facts Phase 37 established cannot silently regress on the next dependency bump.

- installs the one wheel in --wheel-dir into a fresh venv by absolute path,
  never from a pinned dev requirements file and never from source
- runs the assertions in cheapest-failing-first order, stopping at the first
  failure with the offending subprocess output, so CI names the broken layer
- assertion 3 checks both halves of PKG-05: the wheel ships entries under
  web/static, web/templates and core/sounds, AND notification.wav resolves
  from utils.SOUNDS_DIR inside the clean install
- assertion 5 polls real HTTP, because core/cli/web.py prints the dashboard
  URL before create_app() runs, so the printed line proves nothing
- wheel is resolved by glob, never by a versioned filename, so release-please
  bumping the version cannot break the job
Runs on ubuntu-latest and windows-latest with fail-fast disabled, builds the
wheel, and hands it to scripts/verify_wheel.py, which creates its own clean
virtualenv and installs the wheel there by absolute path.

- separate install step from the test job on purpose: the test job installs
  requirements.txt first, deliberately, and doing the same here would hide
  every dependency and data file missing from the wheel itself
- ubuntu is not optional; colorama and pyyaml reach a Windows install
  transitively, so a Windows-only job would keep missing that failure mode
- SHOPBOT_DATA_DIR and WHEEL_VENV stay at step level: the runner context does
  not exist in job-level env and using it there fails the whole workflow to
  compile rather than failing one job
- actions kept on @v6 tags; Phase 38 owns SHA pinning and the permissions block

test job byte-identical to HEAD; diff is 37 additions and 0 deletions.
PKG-05 closed, and Phase 37 with it. All six PKG requirements are now proven
against a real built wheel rather than against pyproject.toml.

- 37-04-SUMMARY.md records the five PASS lines verbatim and both negative
  controls verbatim, with exit codes
- STATE.md: Phase 37 marked complete, 13 of 84 requirements, next is Phase 38
- ROADMAP.md phase-status row 37 to 4/4 Complete
- REQUIREMENTS.md PKG-05 checked off and its traceability row set to Complete

The wheel CI job has NOT run on a runner. It lands with this branch's push.
Comment thread .github/workflows/ci.yml
Comment on lines +56 to +82
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install build tooling
# Only pip and build. Nothing here reaches the clean venv, which the
# verification script creates itself and installs the wheel into.
run: |
python -m pip install --upgrade pip
pip install build
- name: Build wheel
run: python -m build --wheel --outdir dist
- name: Verify wheel
env:
# Same step-level rule as the Test step above: the `runner` context does
# not exist in job-level `env:`, and using it there fails the whole
# workflow to compile rather than failing this one job.
SHOPBOT_DATA_DIR: ${{ runner.temp }}/shopbot-wheel
WHEEL_VENV: ${{ runner.temp }}/wheelenv
run: python scripts/verify_wheel.py --wheel-dir dist --venv "${{ env.WHEEL_VENV }}"
…e-close

# Conflicts:
#	.planning/REQUIREMENTS.md
#	.planning/ROADMAP.md
@thezoid
thezoid merged commit bd70601 into master Aug 3, 2026
8 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.

2 participants