The shell entry points got this in 99aa194; running the .py directly still
died with ModuleNotFoundError, which is the command the file's own docstring
had been telling people to use for months.
tool/venv_bootstrap.py creates .venv, installs the project and re-executes
the same command line through it. It is stdlib-only by necessity: it runs on
an interpreter where this package does not import yet, so it cannot route
through signal_bot.external, the one module allowed to start a process. That
is why it gets the second scoped per-file-ignore rather than the rule being
bent -- kuhy's call. test_external.py now asserts a closed list of two, so a
third file still fails the gate.
Two things that are less obvious than they look:
It runs on import rather than exposing a function to call. A call would have
to happen before the caller's own third-party imports, and code before
imports is E402 -- so a call-based design needs a lint suppression in every
file it protects. Importing the module is the whole interface.
And isort would sort "venv_bootstrap" after "fastapi", silently breaking the
ordering the module depends on. It gets its own isort section ahead of the
standard library, so the formatter enforces the order instead of undoing it.
Caught while verifying: ensure() used one check to decide both whether to
re-exec and whether to install. The interpreter you typed never grows a
fastapi, so every launch reinstalled the project first. Splitting them
(ready() probes the venv, missing() probes the caller) takes a warm run from
a full pip install to 0.47s. Pinned by a test.
Verified on a clone with no .venv: first run installs and prints one line,
second and third print nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBrE1b9HvFw3xvG9bJiDFt
tool/tagger.py did not run at all: 48957a1 renamed pack.default_dir to
default_dirs and missed the caller, so the tagger raised ImportError at
module scope through two releases. Nothing could have caught it -- tool/ is
outside --cov and mypy ran with [signal_bot, fake_signal]. Both are widened
here, and tests/test_tool_imports.py imports every script under tool/.
--import was a filename check and shutil.copy2. Pointed at a phone export
that overlaps the album it re-imported the same photo under whatever name
the exporter used that time. It now identifies files by magic bytes, strips
them through the same media_strip path !add_photo uses, and dedups on exact
bytes twice: the source hash, then -- only for what survives -- the hash
after stripping. sources.json records both, so a second run on the same
folder is a no-op. What the album refuses is moved to _skipped/<reason>/
beside the source, never deleted.
Two things that made "strip on import" impossible until now:
media_tags claims -all= zeroes the QuickTime date tags and allows them by
value. It does not; measured, it leaves CreateDate at its real value, and
every mp4 was being refused. -time:all= is what zeroes them.
And a tag allowlist cannot see the worst class at all. IMG_6924.mp4 carries
mebx timed-metadata holding per-frame face bounding boxes and a face id,
and those survive a complete tag strip because they live in mdat as opaque
samples. media_tracks.py drops the track with ffmpeg and verifies with
ffprobe that only video and audio survived. This is not the ffmpeg mistake
media_strip warns about: that was ffmpeg replacing exiftool for tags, this
is track removal with exiftool still doing every tag, and the gate is a
positive read-back rather than an exit code.
The roster was write-once. The setup form only appeared on an empty album,
Album.setup overwrote the file whole, and keys were positional (p{len+1}),
so inserting somebody renumbered everyone after them and repointed every
tags.json entry at the wrong person. Keys now come from the highest number
in use, the form merges, and a person can be added mid-session.
--untagged filters the page and never the server's list. /media/{index} and
/api/tag both index the full album, so a shorter list server-side would
show item i of the album while tagging item i of the filtered set.
Measured on the live album: 174 source files -> 1 not media, 82 already in
pack, 24 duplicate in batch, 67 imported, and re-running imports nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WBrE1b9HvFw3xvG9bJiDFt