Files
ss-tools/docs/adr/ADR-0010-model-decomposition-gate.md
2026-06-02 16:36:00 +03:00

6.9 KiB
Raw Permalink Blame History

[DEF:ADR-0010:ADR]

@STATUS ACTIVE

@PURPOSE Define the model decomposition gate for Svelte 5 Screen Models: maximum size thresholds, split strategy, and enforcement mechanism to prevent godobject antipattern in modelfirst architecture.

@RELATION DEPENDS_ON -> [ADR-0006:ADR]

@RELATION REFINES -> [Std.Semantics.Svelte]

@RELATION CALLS -> [DashboardHubModel]

@RATIONALE Screen Models ([TYPE Model] in .svelte.ts files) aggregate all screenlevel state and actions. Without a size gate, models accumulate methods indefinitely as features grow — reproducing the same monolith problem that modelfirst architecture was designed to solve. The DashboardHubModel (20260602, 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 400line threshold mirrors INV_7 (module <400 lines) from semanticscore §I, creating a consistent rule across both page files and model files.

@RATIONALE 40method 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 (batchdelete, export, savedfilters) would push it past 600 lines.

@REJECTED Arbitrary split (e.g. filesize 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 20260602.

Context

Screen Models (defined in semanticssvelte §IIIa) are the single source of truth for screenlevel 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 2765line page, the model already bundles four distinct responsibility domains:

  1. Core data — pagination, loading/error state, environment context
  2. Filters & sort — column filter dropdowns, search, sort direction
  3. Selection & bulk actions — checkboxes, selectall, migrate/backup modals
  4. Git operations — init, sync, commit, pull, push, status batch

Adding future features (batchdelete, savedfilters, export) to the same model would push it past 600 lines — reproducing the monolith antipattern 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.ts file: <Domain>Model.svelte.ts
  • Declares @RELATION COMPOSED_BY -> [ParentModel] on the parent model
  • Exports a class with $state atoms 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 DashboardHubModel [C:4] [TYPE Model] [SEMANTICS dashboard,hub,core]
// @RELATION COMPOSED_BY -> [DashboardFiltersModel]
// @RELATION COMPOSED_BY -> [DashboardSelectionModel]
// @RELATION COMPOSED_BY -> [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
}

Enforcement

  1. Documentation — every C4+ model MUST declare @INVARIANT DECOMPOSITION GATE: 400 lines, 40 methods with a comment referencing this ADR.
  2. Guardrailscripts/audit-frontend-style.mjs already checks page size; extend with model size check (>400 lines OR >40 public methods).
  3. Code review — new PRs adding methods to a model near 300+ lines MUST include either a split plan in the PR description or a @RATIONALE explaining why split is deferred.

Relationship to Page Size Gate

Level Threshold Documented in
Page file (+page.svelte) 400 lines (INV_7) semanticscore §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 GATE in the model header.
  • Future features map directly to submodels — no ambiguity about where to add code.

Negative

  • Crosssubmodel invariants become harder to declare (e.g., "changing filter resets selection" spans two submodels). Mitigation: the parent model composes submodels and declares crosscutting invariants.
  • Slightly more indirection (m.filters.searchQuery vs m.searchQuery). Mitigation: TypeScript autocomplete resolves this in IDEs.

Migration

  1. DoneDashboardHubModel created (350 lines, below threshold). @INVARIANT DECOMPOSITION GATE added with split plan.
  2. Next — when model crosses 400 lines or 40 methods, execute the split plan documented in the model header.
  3. Future models — all new C4+ Screen Models created after this ADR MUST include the @INVARIANT DECOMPOSITION GATE annotation.

References

  • semanticscore §I INV_7 — module < 400 lines
  • semanticssvelte §IIIa — Screen Model contract template
  • semanticssvelte §IIIa.1 — Model Decomposition Gate (added 20260602)
  • ainativedesignaudit.md §10 — Lessons learned from PR 4

[/DEF:ADR-0010:ADR]