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

444 lines
16 KiB
Markdown

# 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
```python
# 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
```python
# 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:**
```python
# 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
```python
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:**
```javascript
// 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
```bash
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:**
```python
# 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
```python
# 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:**
```bash
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:**
```bash
/migration-check # Runs pytest tests/test_plugin_migrations.py
# PASS: All plugin migration chains are linear
```
**Frontend import validation:**
```bash
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`