Contributing

Stash Curator is primarily generated with AI coding agents under human direction, review, and testing. Contributions—human-written or agent-assisted—must still be understood, reviewed, and verified by a person responsible for the change.

Set up and verify

Requirements are Python 3.12+, uv, Node.js for browser code checks, and Go 1.26.5+ for full verification or packaging. Packaging also uses Zig; the build script downloads its pinned version if it is not already installed.

uv sync --locked
scripts/verify changed

Use scripts/verify changed tests/path/to/test_file.py while implementing a focused change. For a broad native test pass, run scripts/verify test: it builds the host core and runs lint, type checks, Go tests and Python compatibility tests. It omits cross-platform packaging and the performance benchmark. Run scripts/verify perf for that benchmark. Focused verify changed checks run Go tests when needed and only the Python tests requested; broad differential coverage runs in verify core, verify test and verify full.

Before release, run the CI-equivalent suite with all five platform builds:

scripts/verify full

Tests use unique directories in system temporary storage; set TMPDIR to override that location. Prefer local storage for SQLite-heavy tests. Non-integration tests run on four pytest-xdist workers, keeping each file’s fixtures together and limiting numerical libraries to one thread per worker. Set CURATOR_TEST_WORKERS=0 for serial debugging, or another worker count for your machine. The full suite reports its 20 slowest tests. Test-only Go edits and unchanged generated fingerprints do not invalidate packaged binaries. macOS linker stubs use a content-addressed cache, so moving between checkouts does not invalidate SQLite’s C compilation when the stubs are unchanged.

Build the plugin source with uv run --frozen python scripts/build_plugin.py. It writes dist/stash-curator.zip and a checksummed dist/index.yml. Repository tests use synthetic data and need no live Stash or StashDB access.

Windows coverage

The Windows workflow runs go vet and go test for core/ on windows-latest, then installs a Windows-only package built from the same commit into native Stash v0.31.1 (scripts/windows_smoke.py). The smoke test seeds a synthetic library, runs Sync and build, calls the status and connection-check operations, and keeps the Curator page open. It fails on task failures, browser exceptions, Curator errors in Stash’s log, request failures in the daemon log, or more new TCP connections than its budget. It runs for pull requests that touch core/, plugin/ or packaging, for pushes to main, nightly, and on demand. Its logs and a page screenshot are uploaded as artifacts.

Plugin compatibility coverage

tests/plugin/test_patch_composition.cjs exercises every registered Stash patch with rewritten argument counts and both plugin registration orders. It preserves renderer context, native content, and element, null and array results. Its surface inventory must be extended when adding a patch.

tests/integration/test_patch_composition.py loads a synthetic companion plugin before Curator in the isolated Stash browser environment. It checks scene and performer grids and detail pages, card details/popovers/overlays, player and popover hook execution, retained navbar entries, and JavaScript errors. Run it through scripts/verify integration; it does not require access to a personal library. The destructive fresh-install reset test runs last, after pytest groups fixture parameters, so it cannot invalidate later model-dependent pages or background tasks.

Code map

Area Purpose
core/ Production Go core: Stash integration, SQLite, model, discovery, and tasks
curator/, plugin/backend.py Development and differential-test oracle; not production runtime
curator/explanations/realizations.json Explanation catalog included with the plugin
plugin/ Launcher, manifest, and browser UI
tests/ Synthetic unit and integration coverage

Privacy rules

Never commit databases, reports, GraphQL payloads, local URLs, credentials, real entity IDs, library facts, or personal evaluation notes. Live Stash and StashDB access is read-only unless a user explicitly authorizes testing the reversible Prune tag mutation. Curator must never delete media.

Migrations and packaging

SQLite migrations are ordered, immutable, checksummed, and transactional. Add a new migration; never edit one that may have been applied, reset a sidecar to hide a migration defect, or expose readers to partially published model state.

The plugin archive contains the plugin UI and launcher, explanation realization catalog, per-platform Go binaries, manifest, and license—not the full Python curator package or plugin/backend.py. Documentation is built independently from docs/ with GitHub Pages’ native Jekyll action, then the archive and index are copied into the same deployment so docs and install source go live atomically.

See the retained backend runtime decision for the deployment rationale.

Releases

Releases are automated with release-please and Conventional Commits. The one thing to keep consistent is commit subjects: prefix them with a conventional type (feat:, fix:, docs:, perf:, chore:, …) and merge with squash so the PR title becomes the commit subject. A workflow checks every PR title, and a local .githooks/commit-msg hook checks commits before they are made (the same hook directory as the pre-push verifier; enable it with git config core.hooksPath .githooks). Use --no-verify only when a subject is truly exempt.

On every push to main, release-please compares the merged commits against the last release, opens a chore(main): release vX.Y.Z pull request that bumps pyproject.toml and curator/__init__.py, and regenerates CHANGELOG.md. Merging that pull request creates the version tag and the GitHub Release; a follow-up job builds the plugin archive and attaches dist/stash-curator.zip and dist/index.yml to the release, then dispatches the Pages workflow.

GitHub Pages rebuilds on every push to main and serves two plugin sources. The development source under /dev/ is built from main by scripts/build_plugin.py --dev, versioned X.Y.Z+dev.N.<sha> (N commits after release X.Y.Z; the release commit itself is plain X.Y.Z). The regular source serves the latest release’s attached archives unchanged. Stash offers a plugin update only when the index date is newer, so regular-source users see an update only when a release lands. The development source is for maintainers and is deliberately not in the user guide.

To test what the next release will contain, point an existing installation’s plugin source at https://mrx-31415.github.io/stash-curator/dev/index.yml, keeping its Local Path, and update Curator. Edit the source rather than adding a second one: a second source with another Local Path installs a second copy of the plugin. The development source also carries every release, so an installation can stay on it. To return to the regular source, switch it back and reinstall. Going back to a release older than a development build that added a migration is not supported, so back up the plugin folder first.

The version is defined once in pyproject.toml ([project] version) and again in curator/__init__.py; release-please updates both together. Everything else derives from pyproject.toml: scripts/build_plugin.py reads it at build time and injects the value into the staged plugin/stash-curator.yml and the generated index.yml. Never edit those three outputs by hand. feat: bumps minor, fix:/docs: and the other types bump patch, and a BREAKING CHANGE: footer bumps major.