5 Commits

Author SHA1 Message Date
cproudlock
da654944dc Phase 6: simplify-pass policy, plugins listing, roadmap to 1.0
End-of-pipeline cleanup. No structural changes; documents what is
done, what is left, and the discipline for future cleanup commits.

Skill:
- simplifying-python: end-of-session cleanup discipline. Inspired by
  Anthropic's open-sourced code-simplifier. Targets duplication, dead
  code, voodoo constants, stale comments, misnamed identifiers,
  unnecessary abstraction. Anti-targets: architecture, public API
  surface, schema, sweeping rewrites. Behavior preservation enforced
  by running pytest before and after.

Simplify pass on shopdb/utils/responses.py:
- error_response docstring claimed the error info lives at top level
  `error`. Implementation puts it under `data.error` (consistent with
  the success envelope). Implementation is correct; docstring updated.
- paginated_response docstring used snake_case keys (`per_page`,
  `total_pages`, etc). Implementation uses lowercase concatenated per
  CONTRIBUTING.md. Docstring updated to match.

Documentation:
- docs/PLUGINS.md: bundled plugins (six, with what they track and
  caveats per ADR), planned plugins (measuringtools as the scaffold
  canary per ADR-005), distribution conventions for sister-site
  plugins per ADR-003, naming policy.
- docs/ROADMAP.md: phase status table (0-5 done, 6 in progress),
  must-have work for 1.0 (Asset.mapx/mapy, equipment migration,
  printers retirement, frontend hook contract, per-plugin Alembic
  chains), nice-to-have (measuringtools plugin, frontend scaffolding,
  marketplace listing, surface-diff tooling), deferred (multi-tenancy,
  pip-installable plugins, event bus). Defines what 1.0.0 means as a
  contract.

Test count unchanged: 101 passing. Naming/style check green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:02:11 -04:00
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
cproudlock
8eb9362452 Phase 4: plugin scaffolding (flask plugin new) with canary tests
Lowers the barrier for sister sites to build their own plugins.
Generated output satisfies the framework contract out of the box.

CLI command (shopdb/plugins/cli.py):
- `flask plugin new <name> --description "..."` generates a plugin
  skeleton under plugins/<name>/. Validates the name against
  CONTRIBUTING.md rules (lowercase letters/digits only, no
  underscores or hyphens, not in the reserved list) and refuses to
  overwrite existing plugins unless --overwrite is passed.
- Output prints the next steps (install, migrate, test).

Scaffolder (shopdb/plugins/scaffolder.py):
- validate_name: enforces the naming rules
- pascal_case: lowercase-to-PascalCase for class names
- scaffold_plugin: copies templates with string.Template
  substitution. Three placeholders: $name, $Name, $description.
  Files with `model.py` in the path get renamed to <name>.py.

Templates (shopdb/plugins/templates/):
- manifest.json.tmpl: name, version 0.1.0, description, core_version
  range >=0.1.0,<1.0.0 (broad enough to survive minor framework bumps)
- plugin.py.tmpl: <Name>Plugin class extending BasePlugin with all
  required hooks implemented (meta from manifest, get_blueprint
  returning the bp, get_models returning the example model). Includes
  on_install hook that seeds the AssetType row.
- models/__init__.py.tmpl + models/model.py.tmpl: Asset extension
  table keyed by assetid with one example field. TODO comment marks
  it as a placeholder.
- api/__init__.py.tmpl + api/routes.py.tmpl: Blueprint with list and
  detail endpoints using the framework's pagination + response helpers.
- schemas/__init__.py.tmpl: marshmallow schema stub.
- tests/__init__.py.tmpl + tests/test_plugin.py.tmpl: smoke tests
  asserting plugin loads, get_blueprint returns Blueprint, get_models
  returns at least one model.
- README.md.tmpl: one-pager for plugin authors with common edits and
  next-step references.

Canary tests (tests/test_plugin_scaffold.py):
- 14 tests asserting the scaffold output passes contract checks.
- Validates name rules (lowercase, reserved, hyphens, digits, etc.)
- Verifies all expected files generated, manifest fields present.
- Loads the generated plugin via PluginLoader (spec_from_file_location
  bypasses the real `plugins` package shadowing).
- Asserts subclasses BasePlugin, get_blueprint returns Blueprint,
  get_models returns model with __tablename__.
- Module-scoped fixture; cleans up sys.modules + SQLAlchemy metadata
  on teardown to avoid cross-test contamination.

Quickstart docs (docs/PLUGIN-QUICKSTART.md):
- 30-minute walkthrough: scaffold -> edit model -> add routes ->
  install -> verify -> add hooks. Cross-links to PLUGIN-HOOKS.md and
  the ADRs. Includes common-errors table.

Test count: 87 -> 101 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:13:46 -04:00
cproudlock
6f085a175d Phase 3 (part 1): manifest-first loader, shopdb.api namespace, auto-register
Hardens the plugin framework so sister-site adoption is safe.

Loader rewrite (shopdb/plugins/loader.py):
- Reads manifest.json directly. Dependency sort and version checks
  no longer instantiate plugin classes (avoids __init__ side effects).
- Fail-loud policy: in dev/test (DEBUG or TESTING true), plugin
  errors re-raise. In production, errors log with full context and
  the plugin is excluded from registration. Framework keeps booting.
- Contract-version range check via packaging.SpecifierSet. Plugin's
  manifest.core_version must include the framework's
  __contract_version__ or load fails per the policy above.
- Manifest validation: required fields (name, version, description),
  name matches directory, JSON parseable.

Exceptions (shopdb/exceptions.py):
- PluginNotFoundError, PluginContractError, PluginVersionError,
  PluginDependencyError. Specific types replace generic Exception
  swallowing.

Auto-register core blueprints (shopdb/__init__.py):
- CORE_BLUEPRINT_NAMES tuple drives registration. Adding a core
  resource is one entry, not three lines (import + register call).
- Replaces 27 hand-coded register_blueprint calls.
- Asserts each blueprint is exported by shopdb.core.api at boot.

Public API namespace (shopdb/api/__init__.py):
- audit_log: thin wrapper over AuditLog.log() with stable signature.
- resolve_asset_position: implements ADR-001 position resolution
  (asset > related > location). Asset.mapx/mapy and
  AssetRelationship.inheritsposition columns are part of the locked
  contract surface but not yet in models; helper degrades gracefully
  to location-only fallback until the migration lands.

BasePlugin helpers (shopdb/plugins/base.py):
- get_setting(key, default), set_setting(key, value, ...). Settings
  namespaced as plugin.<pluginname>.<key> so two plugins can use the
  same key without colliding.

Manifest version compatibility (plugins/*/manifest.json):
- Bumped core_version from ">=1.0.0" to ">=0.1.0,<1.0.0" so all
  bundled plugins satisfy the new range check.

Contract version bump (shopdb/__init__.py):
- 0.1.0 -> 0.2.0. Additive surface change (Setting helpers,
  shopdb.api namespace) per ADR-002 minor-bump rules.

Tests (tests/test_plugin_loader.py, tests/test_api_namespace.py):
- 13 loader tests: manifest validation failures, version range
  checks, plugin.py import errors, strict-vs-isolate behavior under
  TESTING vs production-like config, manifest-first dependency sort.
- 8 api-namespace tests: audit_log roundtrip, resolve position
  fallback chain, plugin.get_setting/set_setting roundtrip with
  per-plugin namespacing.

Test count: 66 -> 87 passing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 16:15:28 -04:00
cproudlock
5fefb53bca Phase 2: define plugin contract surface and add compliance tests
Locks the public surface plugin authors at sister sites depend on.

Contract version (shopdb/__init__.py):
- __contract_version__ = '0.1.0'. Per ADR-002, plugins declare a
  compatible range in manifest.json `core_version`. Pre-1.0 signals
  the contract is still settling; sister sites should pin tightly.

BasePlugin hook changes (shopdb/plugins/base.py):
- Add get_collector_schema() per ADR-006. Returns JSON Schema (with
  identityfield + fields) describing the payload of an external
  collector pushing to /api/collector/<pluginname>. Defaults to None
  (no auto-registered endpoint).
- Remove get_event_handlers(). Event bus deferred indefinitely per
  ADR-001 (no real use case yet; add via new ADR if it appears).

Hook reference (docs/PLUGIN-HOOKS.md):
- Canonical reference for the contract: required hooks (meta,
  get_blueprint, get_models), optional hooks (init_app,
  get_cli_commands, get_services, get_dashboard_widgets,
  get_navigation_items, get_searchable_fields, get_collector_schema),
  lifecycle hooks (on_install, on_uninstall, on_enable, on_disable),
  helpers exposed in shopdb.api (audit_log, Setting,
  resolve_asset_position).
- Versioning rules + change-classification guidance.

Compliance tests (tests/test_plugin_contract.py):
- 8 distinct contract assertions parametrized over 6 bundled plugins
  (computers, equipment, network, notifications, printers, usb).
- Asserts: subclasses BasePlugin; manifest has required fields; meta
  returns valid PluginMeta; get_blueprint returns Blueprint or None;
  get_models returns model classes; get_collector_schema returns
  None or {identityfield, fields}; get_navigation_items and
  get_searchable_fields return list.
- Plus 3 framework-level: __contract_version__ is valid semver,
  get_event_handlers absent, get_collector_schema present.

Test count: 15 -> 66 passing.

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