Files
shopdb-flask/docs/wiki-drafts/Architectural-Findings-2026-09-11.md
Proudlock 3d041da233 Add wiki page drafts for Q3 2026 work
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>
2026-09-11 17:04:39 -04:00

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:

  • computers plugin imports from machines plugin: from plugins.machines.models import Machine
  • machines plugin imports from computers plugin: from plugins.computers.models import Computer
  • printers plugin imports from computers plugin
  • endpoints plugin imports from computers, printers, and usb plugins

Impact:

  • Violates plugin isolation (ADR-001: plugins are independent extensions)
  • Disabling computers breaks machines, 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:

  1. Audit all from plugins.<other> imports (25+ violations found)
  2. Replace with Asset-based relationships or declared dependencies
  3. Enforce via linter: plugins can import from shopdb.api, not plugins.*

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.models directly (not via shopdb.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:

  1. Audit imports: grep -r "from shopdb.core" plugins/
  2. Export needed classes via shopdb.api.__init__.py
  3. Update plugin imports to use shopdb.api
  4. 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.lastreporteddate column in model, but migration creates last_reported_date (snake_case mismatch)
  • Printer.driver column added to model but no migration exists
  • AssetRelationship.label column 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:

  1. Generate fresh migration from current models: flask db migrate
  2. Compare to existing migrations, find diffs
  3. Write fixup migration to align schema with models
  4. Add test: test_migrations_match_models() validates ORM vs DB schema
  5. 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_OWNERS order 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_revision references exist
  • Checks: PLUGIN_TABLE_OWNERS order 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:

  1. Audit plugin DB writes: find implicit db.session.add() without savepoint
  2. Wrap in begin_nested() savepoints or explicit commit()
  3. 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 from plugins/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:

  1. Move shared components to frontend/src/components/
  2. Update plugin imports to use @/ alias
  3. 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.py has MACHINE_TYPES = ['Lathe', 'Mill', 'CNC'] hardcoded
  • plugins/printers/api/routes.py validates building in ['Bldg 300', 'Bldg 400']
  • 12 plugins have site-specific literals

ADR-015 rule: Site data MUST live in:

  • shopdb/core/settings/ - Settings table defaults
  • shopdb/core/seed/ - Initial data SQL
  • scripts/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:

  1. Extract literals to Settings table
  2. Update plugins to read from Settings
  3. 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:

  1. Audit external calls: grep -r "requests\.(get|post)" plugins/
  2. Add timeouts (default 10s)
  3. Add retry logic for transient failures (5xx errors)
  4. 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):

  1. Fix schema drift (models vs migrations) - CRITICAL
  2. Enforce plugin API imports (no shopdb.core) - enables refactoring
  3. Add error handling to external service calls - stability

Short-term (Q1 2027):

  1. Break circular plugin dependencies - enables lean builds
  2. Remove site literals from plugins - enables multi-site
  3. Fix implicit DB session sharing - data integrity

Long-term (Q2-Q4 2027):

  1. High availability architecture (if feasible in air-gap)
  2. API versioning for v1.0 release
  3. 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