044 provider runtime completion (pre-staged workstream): capacity/operator stores, provider ops/protocol/reconciler/dispatch revalidation, exploration sandbox runtime, alembic 0014-0016, live canary evidence (browser provider 6/6 against the live stand). 050 Phases 0-2: RBAC FastMCP server with a 45-tool explicit catalog, OAuth/DCR transport guards with bounded bodies, per-call provenance (McpToolInvocationRecord), durable ActionApprovalGate + CAS decide + leased/fenced poller, authoring workspace ops, bounded-response discipline, hidden-vs-gated matrices. Parity domains (T012-T014): gated git/deploy/migration/backup/llm tools with reviewed dispatch adapters in explicit poll chains; Superset reads/writes with a dedicated plugin:superset_sql risk class (terminal PROD denial via the canonical execution-policy criterion, hardened danger-SQL guard covering INTO/CALL/SET/REFRESH/file primitives/multi-statement); baseline 037 tools over the shared REST-surface services. Evidence (T016/T023/T028): REST-vs-MCP field parity on shared 037 fixtures; vertical E2E from tools/list through registry revision activation with real scenario:EDIT RBAC; sandbox-to-revision promotion E2E with unsafe-payload and caller-digest rejection; dispatcher soak (three poll cycles, exactly-once). Orthogonal QA+security audit hardening: enforced response_limit fail-closed envelope, poisoned-exploration fail-closed (EXPLORATION_TARGET_UNRESOLVED), sha256 exploration evidence digests, actor-UUID task ownership, is_active guard on baseline consume, 038 resolver description=None selector fix. Phase 3/4 decommission: HandoffSurface behind the MCP_DECOMMISSION flag, then unconditional removal — agent/ service tree, chat components/models/ stores/types, gradio proxies (vite + nginx), agent service in run.sh, docker-compose profiles, build.sh bundles; /agent renders the handoff only. Docs: AGENTS.md/INSTALL.md two-service rewrite; 036-047 drift amendments marked done; WORKSTATE checkpoints with all evidence. Suites: backend 11199 passed / 240 skipped / 1 xpassed; frontend 3454 passed (197 files), lint 0 errors, build OK; browser E2E login+handoff 6/6 twice on the isolated compose stack (no 7860); ruff/compileall clean. Misc: gitignore hardening (tmp/, tool model cache); E2E selector repairs (nav strict-mode, invalid-credentials passthrough detail).
7.0 KiB
@{ Doc.Adr.ADR0010 [C:1] [TYPE ADR]
@STATUS ACCEPTED
@BRIEF Define the model decomposition gate for Svelte 5 Screen Models: maximum size thresholds, split strategy, and enforcement mechanism to prevent god‑object anti‑pattern in model‑first architecture.
@RELATION DEPENDS_ON -> [Doc.Adr.ADR0006]
@RELATION DEPENDS_ON -> [Std.Semantics.Svelte]
@RELATION CALLS -> [Doc.Adr.DashboardHubModel]
@RATIONALE Screen Models ([TYPE Model] in .svelte.ts files) aggregate all screen‑level state and actions. Without a size gate, models accumulate methods indefinitely as features grow — reproducing the same monolith problem that model‑first architecture was designed to solve. The DashboardHubModel (2026‑06‑02, PR 4) is the first model to approach the threshold at 350 lines and 35 public methods. This ADR locks in the decomposition rule before the model crosses the boundary.
@RATIONALE 400‑line threshold mirrors INV_7 (module <400 lines) from semantics‑core §I, creating a consistent rule across both page files and model files.
@RATIONALE 40‑method threshold is based on practical cognitive load: beyond ~40 public methods, an AI agent or human developer cannot hold the full model API in working memory when reasoning about invariants.
@REJECTED No size gate — rejected because DashboardHubModel already demonstrates the accumulation pattern (35 atoms, 35 methods from one refactoring pass). Without a gate, future features (batch‑delete, export, saved‑filters) would push it past 600 lines.
@REJECTED Arbitrary split (e.g. file‑size only) — rejected because splitting by line count alone ignores method cohesion. The split MUST follow responsibility boundaries (filters, selection, git) to keep invariants within each submodel locally verifiable.
ADR-0010: Model Decomposition Gate
Status
ACTIVE — enforced for all C4+ Screen Models as of 2026‑06‑02.
Context
Screen Models (defined in semantics‑svelte §IIIa) are the single source of truth for screen‑level state. They use Svelte 5 runes ($state, $derived) in .svelte.ts files and declare @STATE, @ACTION, and @INVARIANT annotations.
The first wave of model extraction (PR 4, June 2026) produced DashboardHubModel: 350 lines, 35 $state atoms, and 35 public methods. While this is a 51% reduction from the original 2765‑line page, the model already bundles four distinct responsibility domains:
- Core data — pagination, loading/error state, environment context
- Filters & sort — column filter dropdowns, search, sort direction
- Selection & bulk actions — checkboxes, select‑all, migrate/backup modals
- Git operations — init, sync, commit, pull, push, status batch
Adding future features (batch‑delete, saved‑filters, export) to the same model would push it past 600 lines — reproducing the monolith anti‑pattern at the model layer.
Decision
Thresholds
| Criterion | Threshold | Action |
|---|---|---|
| Model file > 400 lines | Mandatory | MUST decompose before adding new methods |
Model > 40 public methods (excluding private _ helpers) |
Mandatory | MUST split into submodels by responsibility |
| Either threshold crossed | Hard gate | New PRs adding methods to the model MUST include the split |
Split Strategy
When a model crosses either threshold, split into submodels by responsibility domain. Each submodel:
- Lives in its own
.svelte.tsfile:<Domain>Model.svelte.ts - Declares
@RELATION DEPENDS_ON -> [ParentModel]on the parent model - Exports a class with
$stateatoms and public methods for its domain only - Is instantiated as a property of the parent model
Concrete split plan for DashboardHubModel (when threshold is crossed):
lib/models/
├── DashboardHubModel.svelte.ts # Core: pagination, load, environment, composes submodels
├── DashboardFiltersModel.svelte.ts # Search, column filters, sort
├── DashboardSelectionModel.svelte.ts # Checkbox, select all/visible, bulk modals
├── DashboardGitModel.svelte.ts # Git init, sync, commit, pull, push, status batch
Parent model composition pattern:
// #region Example.Dashboardhubmodel [C:4] [TYPE Model] [SEMANTICS dashboard,hub,core]
// @RELATION DEPENDS_ON -> [DashboardFiltersModel]
// @RELATION DEPENDS_ON -> [DashboardSelectionModel]
// @RELATION DEPENDS_ON -> [DashboardGitModel]
export class DashboardHubModel {
readonly filters = new DashboardFiltersModel();
readonly selection = new DashboardSelectionModel();
readonly git = new DashboardGitModel();
// Core atoms only:
selectedEnv = $state<string | null>(null);
allDashboards = $state<DashboardRow[]>([]);
isLoading = $state(true);
// ... < 10 atoms
}
// #endregion Example.Dashboardhubmodel
Enforcement
- Documentation — every C4+ model MUST declare
@INVARIANT DECOMPOSITION GATE: 400 lines, 40 methodswith a comment referencing this ADR. - Guardrail —
scripts/audit-frontend-style.mjsalready checks page size; extend with model size check (>400 lines OR >40 public methods). - Code review — new PRs adding methods to a model near 300+ lines MUST include either a split plan in the PR description or a
@RATIONALEexplaining why split is deferred.
Relationship to Page Size Gate
| Level | Threshold | Documented in |
|---|---|---|
Page file (+page.svelte) |
400 lines (INV_7) | semantics‑core §I |
Screen Model (.svelte.ts) |
400 lines OR 40 methods | This ADR |
A page that exceeds 400 lines is decomposed into Model + Components. A Model that exceeds 400 lines is decomposed into submodels. The invariant cascades down.
Consequences
Positive
- Models stay reviewable and testable (unit tests for each submodel in isolation).
- AI agents receive a clear instruction via
@INVARIANT DECOMPOSITION GATEin the model header. - Future features map directly to submodels — no ambiguity about where to add code.
Negative
- Cross‑submodel invariants become harder to declare (e.g., "changing filter resets selection" spans two submodels). Mitigation: the parent model composes submodels and declares cross‑cutting invariants.
- Slightly more indirection (
m.filters.searchQueryvsm.searchQuery). Mitigation: TypeScript autocomplete resolves this in IDEs.
Migration
- Done —
DashboardHubModelcreated (350 lines, below threshold).@INVARIANT DECOMPOSITION GATEadded with split plan. - Next — when model crosses 400 lines or 40 methods, execute the split plan documented in the model header.
- Future models — all new C4+ Screen Models created after this ADR MUST include the
@INVARIANT DECOMPOSITION GATEannotation.
References
semantics‑core§I INV_7 — module < 400 linessemantics‑svelte§IIIa — Screen Model contract templatesemantics‑svelte§IIIa.1 — Model Decomposition Gate (added 2026‑06‑02)ai‑native‑design‑audit.md§10 — Lessons learned from PR 4