Files
shopdb-flask/docs/adr/ADR-002-plugin-versioning.md
cproudlock d4e3ac9fc8 Phase 5: Alembic baseline, per-site deploy, ADRs to docs/adr
Migration runner ready and a sister site can deploy from a clean
checkout with one .env file.

ADRs relocated (migrations/adr/ -> docs/adr/):
- migrations/ is now Alembic territory, not docs.
- All cross-references updated: CLAUDE.md, docs/PLUGIN-HOOKS.md,
  docs/PLUGIN-QUICKSTART.md.

Alembic initialized (migrations/):
- env.py, script.py.mako, alembic.ini copied from Flask-Migrate
  templates so `flask db migrate` and `flask db upgrade` work without
  a one-time `flask db init` (which would clash with the existing
  migrations/ directory).
- Baseline migration generated via autogenerate, captures all 47
  tables (core models + 6 plugins) as the upgrade target. Ready for
  per-site `flask db upgrade` from an empty schema.

Deploy artifacts:
- Dockerfile: python:3.12-slim base, gunicorn server, non-root user,
  healthcheck against /api/auth/login. Single image bundles all six
  plugins; sites enable via `flask plugin install <name>`.
- docker-compose.yml: MySQL 8 + API container, healthcheck-gated
  startup, env-driven secrets that fail loud on missing values
  (`${SECRET_KEY:?}` form).
- .env.example: full env-var inventory with comments. Calls out
  required vs optional. Matches what ProductionConfig.validate
  enforces.

docs/DEPLOY.md:
- Step-by-step per-site runbook: clone, configure .env, bring up
  stack, run migrations, seed reference data, install plugins,
  create admin, front with TLS, backups, updates.
- Common-issues table.
- Cross-links to ADR-004 (per-site rationale), ADR-003 (plugin
  distribution), and the config source.

Skills:
- migrating-asset-schema: Alembic + one-shot data migration policy.
  Rules: additive first, renames are three steps, destructive ops
  need rollback, equipment migration filter per ADR-001 + ADR-005.
- hardening-flask-config: production validation, CORS allowlist
  policy, JWT cookie hardening, per-site deploy isolation per ADR-004.

CLAUDE.md updated to reflect the post-Phase-5 state. No tests added
this commit; the Alembic baseline is exercised by the existing
db.create_all-based test suite (tests do not touch the migration
runner; that's by design until per-plugin migrations land).

Test count unchanged: 101 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:56:19 -04:00

3.8 KiB

ADR-002: Plugin contract versioning

  • Status: ACCEPTED
  • Date: 2026-05-08
  • Deciders: cproudlock
  • Supersedes: none

Context

Once sister sites start writing their own plugins (or pulling community plugins), the framework's plugin contract becomes a public API. Without a versioning story, any change to BasePlugin or the core platform models can silently break installed plugins at remote sites.

The existing BasePlugin and PluginMeta already declare a core_version field (default ">=1.0.0"), but it is not enforced anywhere. The plugin loader does not check it before instantiation.

Decision

The framework adopts semantic versioning for the plugin contract, declared in two places:

  1. Framework version (shopdb/__init__.py): a single __contract_version__ constant. This is the version of the platform contract as defined in ADR-001. Bumped according to semver:
    • Major: breaking change to BasePlugin ABC, PluginMeta schema, or any model in the platform contract (Asset, AssetType, AssetStatus, AssetRelationship, Vendor, Location, BusinessUnit, Model, OperatingSystem).
    • Minor: additive change (new optional hook, new field on a contract model with default).
    • Patch: bug fix, no contract surface change.
  2. Plugin requirement (plugins/<name>/manifest.json): the existing core_version field, expressed as a semver range (e.g., ">=1.0.0,<2.0.0").

The plugin loader (shopdb/plugins/loader.py) checks core_version against __contract_version__ at load time. Mismatch in dev = re-raise (fail loud). Mismatch in prod = log error, mark plugin as incompatible, exclude from registration.

The __contract_version__ starts at 1.0.0 when ADR-001 is accepted and the Machine retirement migration is complete (whichever comes later). Until then, the framework is pre-1.0; plugins should declare core_version: ">=0.1.0,<1.0.0".

Consequences

Positive

  • Sister sites can pin a known-good framework version. They will not be silently broken when the framework is upgraded.
  • Plugin authors know what counts as a breaking change because the contract surface is enumerated in ADR-001.
  • The loader fails predictably: a mismatched plugin is reported, not silently disabled.

Negative / cost

  • Discipline required: every change to the contract surface must be classified (major / minor / patch). Adding a version-bump skill (or a check in code review) reduces the chance of mis-classification.
  • __contract_version__ becomes a coupling point. Forgetting to bump it after a breaking change means downstream plugins crash silently at runtime instead of failing at install.

Neutral

  • Existing plugins (plugins/printers/, etc.) ship as part of the framework, so their core_version is always the current __contract_version__. The discipline matters mostly for external / sister-site plugins.

Alternatives considered

  1. No versioning, just trust. Works for an in-tree-only world. Fails the moment a sister site ships its own plugin. Rejected.
  2. Calendar versioning (e.g., 2026.05.0). Easier to bump, harder to communicate breaking changes. Rejected; semver is the industry standard for library-like contracts.
  3. Per-hook versioning. Each hook has its own version. Too granular; plugins still couple to multiple hooks. Rejected.

Open questions

  • When does the framework declare 1.0.0? Tied to ADR-001 (Asset retirement of Machine) and the framework being deemed "ready for sister sites". Best-effort target: end of Phase 5 in the refactor plan.
  • Should core_version accept commercial-grade ranges (^1.0.0) or stick to PEP 440 / npm-style ranges? Recommend pip-style (>=,<) to match Python ecosystem.

References

  • shopdb/plugins/base.py (PluginMeta declaration)
  • shopdb/plugins/loader.py (where the version check belongs)
  • ADR-001 (defines what is in the contract)