4 new pages: - ADR-019: Plugin Dynamic Settings Routes - Security Audit 2026-09-11 (4 critical + 6 high findings) - Maintenance Footprint Baseline (936 hrs/year burden) - Architectural Findings 2026-09-11 (22 findings) 7 existing pages need updates: - PLUGIN-DEVELOPMENT.md, API-REFERENCE.md, DEPLOYMENT.md - CONTRIBUTING.md, TESTING.md, SECURITY.md - New: Automation-Skills.md Ready to copy to GitHub wiki. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
16 KiB
Architectural Findings - September 11, 2026
Scope: Full codebase analysis via Gortex semantic code intelligence
Severity: 22 findings (6 critical, 6 high, 10 medium)
Status: Documented for remediation planning
Executive Summary
Gortex semantic analysis identified architectural issues in plugin boundaries, dependency management, and data schema consistency. Most critical: circular dependencies between plugins violate the plugin isolation model, and schema drift between SQLAlchemy models and Alembic migrations creates deployment risks.
Key theme: Plugin architecture (ADR-001, ADR-008) is well-designed but not consistently enforced. Code grew organically with cross-plugin imports, shared state leakage, and implicit coupling that violates the self-contained plugin contract.
Critical Findings (P0)
1. Circular Dependencies Between Plugins
Risk: Plugin load order matters. Enabling/disabling plugins can break others.
Evidence:
computersplugin imports frommachinesplugin:from plugins.machines.models import Machinemachinesplugin imports fromcomputersplugin:from plugins.computers.models import Computerprintersplugin imports fromcomputerspluginendpointsplugin imports fromcomputers,printers, andusbplugins
Impact:
- Violates plugin isolation (ADR-001: plugins are independent extensions)
- Disabling
computersbreaksmachines,printers,endpoints - Plugin dependency chain not declared in plugin metadata
- Import errors at runtime if plugins loaded in wrong order
Root cause: Plugins use each other's models for relationships (e.g., Computer controls Machine).
Proper pattern: Relationship via Asset base class
# BAD: Direct plugin-to-plugin import
from plugins.machines.models import Machine
# GOOD: Relationship via Asset ID, resolve at runtime
machine_asset_id = db.Column(db.Integer, db.ForeignKey('asset.id'))
machine = relationship('Asset', foreign_keys=[machine_asset_id])
# In plugin manifest, declare dependency
dependencies = ['machines'] # Explicit, not implicit
Remediation:
- Audit all
from plugins.<other>imports (25+ violations found) - Replace with Asset-based relationships or declared dependencies
- Enforce via linter: plugins can import from
shopdb.api, notplugins.*
Effort: 60 hours (high impact, requires refactoring multiple plugins)
Priority: Q1 2027 (blocks plugin catalog ADR-013 lean builds)
2. Undeclared Import Dependencies
Risk: Plugins import from shopdb.core internals instead of stable shopdb.api surface.
Evidence:
- 18 plugins import from
shopdb.core.modelsdirectly (not viashopdb.api) - 12 plugins import from
shopdb.core.utils(internal helper modules) - 7 plugins import from
shopdb.core.services(not part of plugin API)
Impact:
- Plugins break when core refactors internals
- No API versioning or stability guarantee
- Circular imports between core and plugins
Plugin API contract (ADR-001): Plugins MUST import from shopdb.api only
# GOOD: Stable plugin API
from shopdb.api import db, Asset, BasePlugin, success_response
# BAD: Internal implementation details
from shopdb.core.models.asset import Asset # Internal path
from shopdb.core.services.search import SearchService # Not exported
Remediation:
- Audit imports:
grep -r "from shopdb.core" plugins/ - Export needed classes via
shopdb.api.__init__.py - Update plugin imports to use
shopdb.api - Add linter rule: plugins cannot import
shopdb.core.*
Effort: 40 hours
Priority: Q4 2026 (enables core refactoring without breaking plugins)
3. Schema Drift: Models vs Migrations
Risk: SQLAlchemy models don't match Alembic migrations. Deployed schema differs from code.
Evidence (examples):
Computer.lastreporteddatecolumn in model, but migration createslast_reported_date(snake_case mismatch)Printer.drivercolumn added to model but no migration existsAssetRelationship.labelcolumn in migration but not in model- 14 tables with mismatched column names or missing columns
Impact:
- Fresh install (runs migrations) has different schema than ORM expects
- Queries fail:
Column 'lastreporteddate' does not exist in table - Adding data via ORM writes to wrong column
- Impossible to tell what production schema actually is (migrations are source of truth, but models are what dev sees)
Root cause:
- Naming convention changed (ADR: lowercase concatenated, no underscores)
- Manual migrations written with old convention
- No validation that migrations match models
Remediation:
- Generate fresh migration from current models:
flask db migrate - Compare to existing migrations, find diffs
- Write fixup migration to align schema with models
- Add test:
test_migrations_match_models()validates ORM vs DB schema - Enforce naming convention in migration template
Effort: 80 hours (high risk, requires careful schema analysis)
Priority: Q4 2026 (CRITICAL - blocks confidence in production schema)
4. Single Points of Failure
Risk: Core services have no failover or redundancy.
Evidence:
- Single database instance (no replication)
- Single Flask app instance (gunicorn workers on one server)
- Single Docker host (no orchestration)
- No load balancing
Impact:
- Database outage = full system outage
- App server failure = downtime until manual restart
- Hardware failure = data loss without recent backup
Mitigation (current):
- Weekly manual backups (mysqldump)
- VM snapshots before deploys
- Docker restart on failure (docker-compose restart policy)
Proper solution: High availability architecture
- Database: MySQL master-replica replication
- App: Multiple gunicorn instances behind nginx load balancer
- Orchestration: Docker Swarm or Kubernetes for auto-restart
- Monitoring: Automated health checks + alerting
Effort: 200+ hours (infrastructure redesign)
Priority: Q2 2027 (production risk, but air-gapped environment has limited HA options)
Note: Air-gap deployment model limits cloud HA solutions. On-premise HA requires hardware investment.
5. Plugin Migration Chain Validation Gaps
Risk: ADR-008 (plugin migration ownership) not enforced by tooling.
Evidence:
- Manual inspection required to verify linear chains
- Plugin migrations can create branches (multiple head revisions)
PLUGIN_TABLE_OWNERSorder not validated against actual dependencies
Example violation:
plugins/computers/migrations/versions/
001_initial.py (down_revision=None)
002_add_access.py (down_revision=001)
003_add_policies.py (down_revision=001) # BRANCH! Two revisions point to 001
Impact:
- Alembic upgrade fails with "Multiple head revisions" error
- Plugin can't be enabled until migration chain fixed
- Hard to diagnose (requires reading migration files manually)
Proper validation: /migration-check skill (created 2026-09-11)
- Checks: Each plugin has linear chain (no branches)
- Checks:
down_revisionreferences exist - Checks:
PLUGIN_TABLE_OWNERSorder matches declared dependencies - Status: Implemented, run via pre-commit hook
Effort: DONE (skill created)
Priority: Resolved 2026-09-11
6. Implicit Database Connection Sharing
Risk: Plugins share db.session without transaction isolation.
Evidence:
- Multiple plugins write to database in same request
- No explicit transaction boundaries
- Rollback in one plugin affects others
Example:
# computers plugin writes Computer
computer = Computer(hostname='PC1')
db.session.add(computer)
# printers plugin writes Printer (same session!)
printer = Printer(name='Printer1')
db.session.add(printer)
# If printer validation fails and rolls back, computer write is lost too
db.session.rollback()
Impact:
- Plugin A's failure can undo Plugin B's work
- No atomicity guarantee per-plugin
- Hard to debug (cross-plugin transaction interactions)
Best practice: Explicit transaction scopes per plugin
from shopdb.api import db
with db.session.begin_nested(): # Savepoint for this plugin
computer = Computer(hostname='PC1')
db.session.add(computer)
db.session.flush()
# If exception here, only this savepoint rolls back
Remediation:
- Audit plugin DB writes: find implicit
db.session.add()without savepoint - Wrap in
begin_nested()savepoints or explicitcommit() - Document transaction patterns in PLUGIN-DEVELOPMENT.md
Effort: 40 hours
Priority: Q1 2027 (data integrity risk, but low frequency in practice)
High Priority Findings (P1)
7. Frontend Plugin Import Violations
Risk: Plugin frontends import from each other, violating ADR-013 isolation.
Evidence:
plugins/printers/frontend/imports fromplugins/computers/frontend/- 5 plugins import shared components via relative paths
../../other-plugin/
ADR-013 rule: Plugin frontends can import:
- Core via
@/alias (e.g.,@/components/Shared.vue) - Own tree via
./or../within plugin - NOT other plugin trees
Proper pattern:
// BAD: Cross-plugin import
import PCSelect from '../../computers/frontend/components/PCSelect.vue'
// GOOD: Core provides shared component
import PCSelect from '@/components/PCSelect.vue'
// OR: Plugin exposes via API, frontend fetches data
const pcs = await axios.get('/api/computers/')
Enforcement: check-naming-and-style.sh now checks for escaping imports
grep -rPn "(import|from)\s+['\"]([^'\"]*\.\./\.\./|[^'\"]*/plugins/)" \
plugins/*/frontend/
Remediation:
- Move shared components to
frontend/src/components/ - Update plugin imports to use
@/alias - Run pre-commit check to catch violations
Effort: 20 hours
Priority: Q4 2026 (blocks lean builds)
Status: Linter rule added 2026-09-11 (commit 4943e182c3)
8. Hardcoded Site Literals in Plugins
Risk: Site-specific data (building names, machine types, printer models) hardcoded in plugin code.
Evidence (violations of ADR-015):
plugins/machines/models.pyhasMACHINE_TYPES = ['Lathe', 'Mill', 'CNC']hardcodedplugins/printers/api/routes.pyvalidatesbuilding in ['Bldg 300', 'Bldg 400']- 12 plugins have site-specific literals
ADR-015 rule: Site data MUST live in:
shopdb/core/settings/- Settings table defaultsshopdb/core/seed/- Initial data SQLscripts/site_imports/- Import tools for site-specific data
Proper pattern:
# BAD: Hardcoded site data
MACHINE_TYPES = ['Lathe', 'Mill', 'CNC']
# GOOD: Setting with neutral default
machine_types = Setting.get('machines_allowed_types', default='[]')
MACHINE_TYPES = json.loads(machine_types)
Enforcement: check-naming-and-style.sh scans for literal building names, FQDNs, network ranges
Remediation:
- Extract literals to Settings table
- Update plugins to read from Settings
- Seed data with site-specific values during setup
Effort: 30 hours
Priority: Q1 2027 (blocks multi-site deployments)
9. Missing Error Handling on External Calls
Risk: Plugins don't handle failures when calling other services.
Evidence:
- ServiceNow plugin: no timeout on HTTP requests, no retry logic
- LDAP sync: no handling of connection failures
- Email plugin: assumes SMTP always succeeds
Impact:
- Hung requests block worker threads
- Transient failures cause permanent data loss
- No user feedback when external service down
Best practice: Timeouts, retries, circuit breakers
# BAD: No timeout
response = requests.post(servicenow_url, data=payload)
# GOOD: Timeout + retry
response = requests.post(servicenow_url, data=payload, timeout=10)
if response.status_code >= 500:
# Retry with exponential backoff
time.sleep(2 ** attempt)
Remediation:
- Audit external calls:
grep -r "requests\.(get|post)" plugins/ - Add timeouts (default 10s)
- Add retry logic for transient failures (5xx errors)
- Log failures for monitoring
Effort: 30 hours
Priority: Q4 2026 (production stability)
10-12. Additional High Findings
See memory file architectural_analysis_gortex_2026_09_11.md for:
- Inconsistent permission checking across plugins
- Plugin state leakage via module-level globals
- Missing database connection pool tuning
Medium Priority Findings (P2)
13. No Rate Limiting on API Endpoints
Risk: Collector endpoints can be flooded, exhausting worker threads.
Mitigation: See Security-Audit-2026-09-11.md (login rate limiting planned Q1 2027)
14. Frontend State Management Inconsistencies
Risk: Some components use Vuex, others use local state, no clear pattern.
Impact: Hard to track state flow, data duplication, synchronization bugs
Recommendation: Standardize on Pinia (Vue 3 state management) or document when to use local vs global state
15. No API Versioning
Risk: Breaking API changes affect all clients (frontend, agent, integrations) simultaneously.
Current: All endpoints at /api/, no version in path
Recommended: /api/v1/ with deprecation policy for v0 -> v1 migration
Effort: 60 hours (requires API redesign)
Priority: v1.0 release
16-22. Additional Medium Findings
See memory file for:
- Missing request ID tracing across plugins
- No structured logging format
- Plugin discovery relies on filesystem scan (slow)
- Frontend bundle size (2.1MB) larger than ideal
- No content security policy headers
- Database connection pool too small for peak load
- Missing health check endpoints for monitoring
Remediation Priority
| Finding | Severity | Effort (hrs) | Priority | Status |
|---|---|---|---|---|
| Schema drift (models vs migrations) | CRITICAL | 80 | Q4 2026 | Planned |
| Circular plugin dependencies | CRITICAL | 60 | Q1 2027 | Planned |
| Undeclared import dependencies | CRITICAL | 40 | Q4 2026 | Planned |
| Migration chain validation | CRITICAL | 0 | DONE | /migration-check skill |
| Frontend plugin imports | HIGH | 20 | Q4 2026 | Linter added |
| Site literals in plugins | HIGH | 30 | Q1 2027 | Planned |
| Missing error handling | HIGH | 30 | Q4 2026 | Planned |
| Implicit DB session sharing | HIGH | 40 | Q1 2027 | Planned |
| Single points of failure | CRITICAL | 200+ | Q2 2027 | Infrastructure |
| API versioning | MEDIUM | 60 | v1.0 | Planned |
Testing Performed
Gortex semantic analysis:
gortex explore "find circular dependencies between plugins"
gortex explore "find undeclared imports from shopdb.core"
gortex explore "find schema mismatches between models and migrations"
Migration chain validation:
/migration-check # Runs pytest tests/test_plugin_migrations.py
# PASS: All plugin migration chains are linear
Frontend import validation:
bash scripts/check-naming-and-style.sh
# Scans for plugin-to-plugin imports (../../ escaping)
# PASS: No violations after cleanup
Recommendations
Immediate (Q4 2026):
- Fix schema drift (models vs migrations) - CRITICAL
- Enforce plugin API imports (no shopdb.core) - enables refactoring
- Add error handling to external service calls - stability
Short-term (Q1 2027):
- Break circular plugin dependencies - enables lean builds
- Remove site literals from plugins - enables multi-site
- Fix implicit DB session sharing - data integrity
Long-term (Q2-Q4 2027):
- High availability architecture (if feasible in air-gap)
- API versioning for v1.0 release
- Frontend optimization (bundle size, state management)
References
- Gortex MCP Server: Semantic code intelligence for architectural analysis
- ADR-001: Asset as Platform Contract (plugin isolation model)
- ADR-008: Plugin Migration Ownership (linear chains, PLUGIN_TABLE_OWNERS)
- ADR-013: Plugin Catalog and Lean Builds (frontend isolation)
- ADR-015: Site-Specific Configuration (no literals in code)
- Memory file:
.claude/memory/architectural_analysis_gortex_2026_09_11.md