tasks ready

This commit is contained in:
2026-05-08 18:01:49 +03:00
parent d8df1fff59
commit bdd376595c
32 changed files with 3243 additions and 229 deletions

View File

@@ -0,0 +1,94 @@
# [DEF:ADR-0001:ADR]
# @STATUS ACTIVE
# @PURPOSE Define the canonical project directory layout, module boundaries, and naming conventions for the ss-tools repository. This ADR is the root structural authority — all other ADRs and feature plans derive their file placement from it.
# @RELATION CALLS -> [ADR-0002:ADR]
# @RELATION CALLS -> [ADR-0003:ADR]
# @RELATION CALLS -> [ADR-0004:ADR]
# @RELATION CALLS -> [ADR-0005:ADR]
# @RELATION CALLS -> [ADR-0006:ADR]
# @RATIONALE A single authoritative layout prevents module sprawl, cyclic dependencies, and agent confusion during longhorizon speckit workflows. Without this, every feature spec must redebate where to place files, leading to inconsistent structures and broken tooling assumptions.
# @REJECTED Monorepo with perfeature packages (e.g. `packages/llmplugin/`) — rejected because the repository has two toplevel runtime targets (Python backend, Svelte frontend) with shared Docker/configuration infrastructure. Packagestyle nesting would double the directory depth without adding isolation value.
# @REJECTED Flat `src/` root — rejected because Python and JavaScript toolchains have incompatible project roots (`pyproject.toml` vs `package.json`), and mixing them in one toplevel `src/` would force crosstoolchain contamination.
## Decision
The repository uses a **twoplatform, toplevel separation**:
```
ss-tools/ # Repository root
├── backend/ # Python 3.13+ / FastAPI application
│ ├── src/
│ │ ├── api/ # FastAPI route modules (one file per route group)
│ │ ├── core/ # Core services: task_manager, auth, migration, plugin_loader
│ │ ├── models/ # SQLAlchemy ORM models (one file per entity group)
│ │ ├── services/ # Business logic (orchestration, validators, transformers)
│ │ ├── schemas/ # Pydantic request/response schemas
│ │ └── app.py # FastAPI application factory
│ ├── tests/ # pytest test suite (mirrors src/ structure)
│ ├── pyproject.toml # Build & tool configuration
│ └── requirements.txt # Production dependencies
├── frontend/ # SvelteKit application
│ ├── src/
│ │ ├── routes/ # SvelteKit page routes (filebased routing)
│ │ ├── lib/
│ │ │ ├── components/ # Reusable Svelte 5 components
│ │ │ ├── stores/ # Svelte 5 rune stores ($state)
│ │ │ └── api/ # API client modules (typed fetch wrappers)
│ │ └── i18n/ # Internationalization dictionaries
│ ├── tests/ # vitest test suite
│ ├── package.json # Runtime + dev dependencies
│ ├── svelte.config.js # SvelteKit configuration
│ └── vite.config.ts # Vite build configuration
├── docker/ # Dockerfiles for backend, frontend, nginx
├── docker-compose.yml # Development Docker Compose
├── docs/
│ ├── adr/ # Architecture Decision Records (this file)
│ ├── design/ # Detailed design documents
│ └── ... # Operational docs (installation, settings, etc.)
├── specs/ # Feature specifications (speckit artifacts)
│ └── NNNfeaturename/
│ ├── spec.md
│ ├── plan.md
│ ├── research.md
│ ├── data-model.md
│ ├── quickstart.md
│ ├── contracts/
│ │ └── modules.md
│ └── tasks.md
├── scripts/ # Shell utility scripts
├── .specify/ # Speckit workflow framework
│ ├── memory/constitution.md # Repository constitution
│ ├── templates/ # Speckit artifact templates
│ └── scripts/bash/ # Speckit helper scripts
├── .opencode/ # OpenCode AI agent configuration
│ ├── command/ # Speckit command definitions
│ ├── skills/ # GRACE semantic protocol skills
│ └── agents/ # Specialized agent definitions
└── .github/ # GitHub Actions workflows
```
## Module Boundary Rules
1. **`backend/src/api/`** — route handlers only. Must not contain business logic, ORM queries, or schema definitions. Each file = one FastAPI `APIRouter` group.
2. **`backend/src/core/`** — singleton services with applicationscoped lifetime (auth manager, task scheduler, plugin loader, migration engine). Must not import from `api/`.
3. **`backend/src/services/`** — stateless or requestscoped business logic. May depend on `models/`, `schemas/`, and `core/`. Must not depend on `api/`.
4. **`backend/src/models/`** — SQLAlchemy ORM declarations only. No business logic, no API dependencies.
5. **`backend/src/schemas/`** — Pydantic models for validation/serialization. No ORM dependencies, no business logic.
6. **`frontend/src/routes/`** — SvelteKit pages. Import components from `lib/components/`, state from `lib/stores/`, API clients from `lib/api/`.
7. **`frontend/src/lib/components/`** — reusable components. Must not import from `routes/` (prevents circular dependencies).
8. **`frontend/src/lib/stores/`** — Svelte 5 `$state` rune stores. Must not import from `routes/` or `components/`.
## File Naming Conventions
- **Python modules**: `snake_case.py` (e.g., `task_manager.py`, `auth_provider.py`)
- **Svelte components**: `PascalCase.svelte` (e.g., `DashboardGrid.svelte`, `MappingTable.svelte`)
- **Test files**: `test_<module_name>.py` or `<ComponentName>.test.ts`
- **ADR files**: `ADR-NNNN-short-description.md` (zeropadded, 4 digits, kebabcase)
## Enforcement
- Speckit `/speckit.plan` command reads this ADR to validate proposed module placement.
- Speckit `/speckit.implement` rejects files written outside canonical boundaries unless justified in featurelocal `research.md`.
- CI static verification (`python3 scripts/static_verify.py`) may optionally enforce module import direction rules.
# [/DEF:ADR-0001:ADR]

View File

@@ -0,0 +1,39 @@
# [DEF:ADR-0002:ADR]
# @STATUS ACTIVE
# @PURPOSE Mandate the GRACE skill set (`.opencode/skills/`) as the single semantic governance authority for all source files and AIagent workflows in this repository. This ADR is the adoption decision — the protocol mechanics live exclusively in the skills; this ADR records *why* the skills are nonoptional for this project.
# @RELATION DEPENDS_ON -> [ADR-0001:ADR]
# @RATIONALE A multiplatform repository (Python/FastAPI + SvelteKit) served by multiple AI agents (backendcoder, frontendcoder, qatester, semanticcurator, …) requires a single, machineparseable contract language that works identically across both platforms. Without it, agents produce inconsistent annotations, the semantic index (`semantic_map.json`) degrades, and longhorizon agent sessions (50+ commits) accumulate invisible architectural drift.
# @RATIONALE GRACE was chosen over lightweight alternatives because this project has specific stressors that only a full protocol addresses: (a) two platforms with different comment syntax — GRACE provides platformspecific anchor forms under one unified graph, (b) longhorizon agent sessions — GRACE Decision Memory (`@RATIONALE`/`@REJECTED`) prevents agents from reexploring alreadyrejected paths, (c) complex orchestration flows (plugin execution, backup pipelines) — GRACE beliefstate markers make sideeffectheavy code auditable and debuggable, (d) enterprise deployment requirements — fractal limits (module <400 lines, `[DEF]` <150 lines) enforce structural hygiene critical for auditready code.
# @RATIONALE Skills are the canonical source rather than this ADR because protocol details (complexity scale, tag inventory, syntax variants) evolve. Duplicating them here creates a fork that will inevitably desynchronise — agents would face two competing versions and inevitably read the stale one. Skills are versioncontrolled in the same repo and loaded dynamically via `skill({name="..."})`, ensuring agents always receive the current protocol.
# @REJECTED Plain docstring conventions (Google/NumPy style) — rejected because freetext docstrings are invisible to the semantic index and cannot express relations, pre/post conditions, or rejected paths in a machinequeryable form.
# @REJECTED Decoratorbased contracts (`@contract`, `@pre`, `@post`) — rejected because they are Pythononly, cannot annotate Svelte components or TypeScript, and break the unified semantic graph spanning both platforms.
# @REJECTED JSDoc/TSDoc for the frontend — rejected because it would create a second annotation language, fragmenting the semantic graph into two incompatible halves and forcing agents to master two different contract systems.
# @REJECTED Embedding protocol rules directly in ADRs (the previous version of this document) — rejected because it duplicates the skill content and inevitably diverges. Agents receiving both the skill and the ADR would face conflicting versions; the skill is the single source of truth.
## Decision
This repository adopts the **GRACE-Poly v2.4 protocol** as implemented by five skills:
| Skill | File | Role in this project |
|-------|------|---------------------|
| `semantics-core` | `.opencode/skills/semantics-core/SKILL.md` | Anchor syntax, complexity scale (C1C5), global invariants, tag inventory |
| `semantics-contracts` | `.opencode/skills/semantics-contracts/SKILL.md` | Design by Contract, Decision Memory (ADR chain, `@RATIONALE`/`@REJECTED`), antierosion rules |
| `semantics-belief` | `.opencode/skills/semantics-belief/SKILL.md` | Beliefstate runtime markers (`belief_scope`, `reason`, `reflect`, `explore`) for C4/C5 Python flows |
| `semantics-testing` | `.opencode/skills/semantics-testing/SKILL.md` | Test constraints, invariant traceability, antitautology rules |
| `semantics-frontend` | `.opencode/skills/semantics-frontend/SKILL.md` | Svelte 5 UX contracts (`@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY`, `@UX_REACTIVITY`) |
**Key principle:** Skills are the protocol. This ADR is the adoption record. When an agent needs to know *what tags are required at C4*, it reads `semantics-core`. When it needs to know *why this project chose C4 annotations at all*, it reads this ADR.
## Enforcement
All speckit commands (`.opencode/command/speckit.*.md`) load the skill set as part of their execution flow:
- **`/speckit.plan`** — reads skills to validate contract designs against the complexity scale.
- **`/speckit.implement`** — requires `[DEF]` anchors and complexityappropriate metadata; rejects naked code.
- **`/speckit.test`** — audits semantic compliance and decisionmemory continuity per skill rules.
- **`/speckit.semantics`** — reindexes and audits the entire workspace against the skilldefined protocol.
- **`/speckit.analyze`** — checks for decisionmemory drift and rejectedpath scheduling.
Static verification (`python3 scripts/static_verify.py`) performs offline checks for broken anchors, missing complexity metadata, and orphan relations.
# [/DEF:ADR-0002:ADR]

View File

@@ -0,0 +1,57 @@
# [DEF:ADR-0003:ADR]
# @STATUS ACTIVE
# @PURPOSE Definitively establish ss-tools as a standalone orchestrator service operating *above* Apache Superset environments rather than integrating into them. This is a reposhaping architectural decision that governs deployment topology, security boundary, release cycle independence, and technology stack freedom.
# @RELATION DEPENDS_ON -> [ADR-0001:ADR]
# @RELATION CALLS -> [ADR-0004:ADR]
# @RELATION CALLS -> [ADR-0005:ADR]
# @RATIONALE The core value proposition of ss-tools is multienvironment orchestration: moving dashboards, datasets, and configurations between independent Superset instances (Dev → Stage → Prod). Embedding this logic inside any single Superset instance would immediately create a "master controller" dependency — if that instance fails, all environment management is lost. An external orchestrator treats all Superset instances as equal endpoints.
# @RATIONALE Technology stack mismatch is irreversible: Superset uses Flask + React, ss-tools uses FastAPI + SvelteKit. Integration would require rewriting the entire Svelte frontend into React components and refactoring FastAPI dependency injection into Flask-AppBuilder patterns — a multimonth effort with zero business value and high regression risk.
# @RATIONALE Release cycle independence is nonnegotiable: Superset has a slow, complex release cadence. ss-tools must ship LLM plugins, Git integrations, and backup features on its own schedule without waiting for upstream Superset releases or maintaining a fork with perpetual merge conflicts.
# @RATIONALE Security isolation: ss-tools manages DevOps privileges (deployment, backup, migration) while Superset manages BI privileges (data viewing, chart creation). Mixing these in one system expands the blast radius of any security incident.
# @REJECTED Integration as a Superset plugin/extension — rejected for the four reasons above (orchestrator topology break, 100% frontend rewrite, Superset release cycle coupling, privilege boundary collapse).
# @REJECTED Superset fork with ss-tools baked in — rejected because maintaining a fork requires rebase on every Superset release, creating perpetual merge conflicts and making security patch adoption dangerously slow.
# @REJECTED Shared database with Superset — rejected because it couples ss-tools migrations to Superset's Alembic migration chain, preventing independent schema evolution and creating a single point of failure for both systems.
## Decision
ss-tools operates as a **standalone orchestrator microservice** with these defined boundaries:
### Deployment Topology
```
┌─────────────┐
│ ss-tools │ ← FastAPI + SvelteKit
│ PostgreSQL │ ← Own database
└──────┬──────┘
│ REST API calls
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│Superset │ │Superset │ │Superset │
│ (Dev) │ │ (Stage) │ │ (Prod) │
└──────────┘ └──────────┘ └──────────┘
```
### Key Architectural Properties
| Property | Decision |
|----------|----------|
| **Topology** | External orchestrator above all Superset instances |
| **Communication** | Superset REST API (bearer token auth per environment) |
| **Database** | Dedicated PostgreSQL 16 instance (not Superset metadata DB) |
| **Frontend** | SvelteKit SPA (not integrated into Superset UI) |
| **Release cadence** | Independent Docker image releases |
| **Auth** | Own RBAC system with optional ADFS SSO (see ADR-0005) |
| **Plugins** | Own plugin lifecycle (see ADR-0004) |
### Consequences
- **Positive:** Fast independent development, modern stack, no Superset coupling, clear security boundary.
- **Negative:** Users have two separate UIs (Superset for BI, ss-tools for DevOps). Mitigated by consistent UX design and crosslinking.
- **Risk:** API compatibility if Superset changes its REST API. Mitigated by versioned API client with automated compatibility tests.
## Migration from Existing Document
This ADR supersedes and formalizes `docs/architecture_decision_superset_migration.md`. The original document contained the same recommendation and rationale but lacked the `[DEF:id:ADR]` contract structure, making it invisible to the semantic index and decisionmemory audit chain.
# [/DEF:ADR-0003:ADR]

View File

@@ -0,0 +1,78 @@
# [DEF:ADR-0004:ADR]
# @STATUS ACTIVE
# @PURPOSE Define the plugin architecture for ss-tools — the loading mechanism, lifecycle contract, isolation guarantees, and the boundary between core services and pluggable extensions.
# @RELATION DEPENDS_ON -> [ADR-0001:ADR]
# @RELATION DEPENDS_ON -> [ADR-0003:ADR]
# @RELATION CALLS -> [ADR-0005:ADR]
# @RATIONALE Extensibility is a core architectural value: the system must support LLMdriven analysis, custom data transformations, and environmentspecific logic without modifying core code. A plugin system prevents the monolith from accumulating every domainspecific feature and enables thirdparty (or futureself) contributions without forking.
# @RATIONALE Process isolation was chosen over inprocess imports because: (a) plugins may use incompatible library versions, (b) a crashing plugin must not take down the orchestrator, (c) security boundary — plugins should not access the orchestrator's database connection directly.
# @REJECTED Inprocess Python `importlib` plugin loading — rejected because a misbehaving plugin can corrupt global state, exhaust memory, or crash the server. Process isolation provides a hard boundary.
# @REJECTED Dockercontainer per plugin — rejected because it adds excessive orchestration complexity and startup latency for plugins that are mostly lightweight LLM prompt chains. Subprocess isolation is sufficient.
# @REJECTED WebAssembly (WASI) sandbox — rejected because the Python AI/LLM ecosystem (langchain, transformers) does not yet reliably compile to WASM. Premature optimization.
## Decision
### Plugin Architecture
```
ss-tools Core
├── core/plugin_loader.py # Plugin discovery, loading, lifecycle
├── core/plugin_executor.py # Subprocess execution, timeout, error boundary
├── core/plugin_registry.py # Registered plugins, metadata, health
└── plugins/ # Plugin packages (each = one directory)
├── llm_analysis/ # LLMdriven Superset data analysis
├── dataset_orchestration/ # LLM dataset operations
└── git_integration/ # Gitbased version control for dashboards
```
### Plugin Contract
Every plugin MUST provide:
1. **`plugin.toml`** — metadata manifest at the plugin root
```toml
[plugin]
id = "llm_analysis"
name = "LLM Data Analysis"
version = "1.0.0"
entrypoint = "plugin.py"
timeout_sec = 300
max_memory_mb = 512
requires = ["superset-api>=1.0", "openai>=1.0"]
```
2. **`plugin.py`** — entrypoint with two required functions:
- `def register(registry: PluginRegistry) -> PluginInfo` — declare capabilities
- `def execute(task: TaskContext) -> TaskResult` — run the plugin
3. **Task context contract** (Pydantic `TaskContext`):
- `task_id: str`, `plugin_id: str`, `action: str`, `params: dict`, `superset_env: SupersetConnection`, `auth_token: str` (scoped, shortlived)
4. **Result envelope** (Pydantic `TaskResult`):
- `status: Literal["success", "warning", "error"]`, `data: dict | None`, `error_message: str | None`, `execution_time_ms: int`, `artifacts: list[str]` (file paths to saved artifacts)
### Plugin Lifecycle
```
Discover → Validate → Register → [Execute] → Report
│ │ │ │
│ Check TOML Store in Spawn subprocess
│ schema, registry with timeout +
│ dependencies in DB memory limit
```
### Isolation Guarantees
- **Subprocess**: `subprocess.run(..., timeout=timeout_sec)`, killed on timeout.
- **Memory**: `resource.setrlimit(RLIMIT_AS, max_memory_mb * 1024 * 1024)` before exec.
- **No DB access**: Plugins receive only a scoped REST API token, never a database connection.
- **No filesystem writes outside allowed dirs**: Configurable artifact directory per plugin.
### RBAC Integration
Plugin access is governed by ADR-0005 (RBAC):
- Each plugin declares `required_roles: ["admin", "analyst"]` in `plugin.toml`.
- Plugin executor checks the user's role set before allowing execution.
- Forbidden access returns `403` with audit log entry.
# [/DEF:ADR-0004:ADR]

View File

@@ -0,0 +1,84 @@
# [DEF:ADR-0005:ADR]
# @STATUS ACTIVE
# @PURPOSE Define the authentication and authorization architecture for ss-tools: local auth, optional ADFS SSO federation, RBAC model, session management, and the security boundary between ss-tools (DevOps privileges) and Superset (BI privileges).
# @RELATION DEPENDS_ON -> [ADR-0001:ADR]
# @RELATION DEPENDS_ON -> [ADR-0003:ADR]
# @RELATION CALLS -> [ADR-0004:ADR]
# @RATIONALE ss-tools manages operations that can modify production Superset instances (dashboard migration, backup, deployment). Unauthenticated or underauthorized access to these operations is a critical security risk. A dedicated RBAC system ensures that DevOps privileges (manage deployments) are cleanly separated from BI privileges (view dashboards) and that actions are auditable.
# @RATIONALE Local auth (bcrypt + JWT) is the primary path because ss-tools must work in airgapped enterprise deployments where external identity providers are unavailable. ADFS SSO is an optional federation layer for organizations that already have Active Directory.
# @RATIONALE RoleBased Access Control (RBAC) was chosen over AttributeBased Access Control (ABAC) because: (a) the system has a small, welldefined set of operations (deploy, backup, migrate, view), (b) RBAC maps naturally to organizational roles (admin, analyst, operator), (c) ABAC adds complexity without proportional benefit for this scope.
# @REJECTED Delegating all auth to Superset — rejected because ss-tools must operate when Superset is down, and Superset's auth model is designed for BI users, not DevOps operators with crossenvironment privileges.
# @REJECTED OAuth2 social login (Google, GitHub) as primary path — rejected because enterprise deployments require airgapped operation. External OAuth providers are unavailable in offline mode.
# @REJECTED Simple API key (no RBAC) — rejected because it cannot express granular permissions (admin vs analyst vs viewer), making auditability and leastprivilege impossible.
## Decision
### Authentication Flow
```
┌──────────┐ POST /api/auth/login ┌──────────────┐
│ User │ ──────────────────────────────│ FastAPI │
│ (Browser)│ {username, password} │ Backend │
│ │ ◄──────────────────────────── │ │
│ │ {access_token, refresh} │ bcrypt + │
│ │ │ JWT (HS256) │
└──────────┘ └──────┬───────┘
┌───────▼───────┐
│ PostgreSQL │
│ users table │
│ roles table │
└───────────────┘
```
### ADFS Federation (Optional)
- Enabled via `config.json: auth.adfs_enabled = true`
- SAML 2.0 flow through `python3-saml`
- ADFS users mapped to local roles via `adfs_group → ss_tools_role` mapping table
- Federated sessions still receive JWTs (stateless after initial SAML handshake)
### RBAC Model
| Role | Permissions |
|------|-------------|
| `admin` | All: manage users, manage roles, manage plugins, deploy, backup, migrate, view dashboards, view logs |
| `analyst` | View dashboards, run LLM analysis plugins, view reports, view logs (own) |
| `operator` | Deploy dashboards, run migrations, manage backups, view logs |
| `viewer` | View dashboards, view reports |
### Token Design
- **Access token**: JWT (HS256), 15minute expiry, contains `user_id`, `roles: list[str]`
- **Refresh token**: opaque random string (SHA256), 7day expiry, stored hashed in DB
- **Superset API token per environment**: AES256GCM encrypted, stored in `connection_configs` table
- **Plugin execution token**: JWT, scoped to a single `task_id`, 15minute expiry
### Security Constraints
1. Passwords: bcrypt with cost factor 12 (minimum).
2. Rate limiting: 5 failed login attempts per IP per 15 minutes → temporary IP block.
3. Token revocation: admin can revoke all sessions for a user (delete refresh tokens).
4. Audit log: all auth events (login success/failure, role change, token revoke) written to `audit_log` table.
5. Enterprise clean mode: local auth only, ADFS disabled, no external network calls for identity.
### RBAC Enforcement Pattern
```python
# backend/src/api/dependencies.py
from fastapi import Depends, HTTPException
def require_role(required_role: str):
def dependency(current_user: User = Depends(get_current_user)):
if required_role not in current_user.roles:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return current_user
return dependency
# Usage in route:
@router.post("/api/dashboards/deploy")
async def deploy(..., user: User = Depends(require_role("operator"))):
...
```
# [/DEF:ADR-0005:ADR]

View File

@@ -0,0 +1,79 @@
# [DEF:ADR-0006:ADR]
# @STATUS ACTIVE
# @PURPOSE Define the frontend architecture for ss-tools: Svelte 5 runes reactivity model, SvelteKit routing, component composition rules, state management patterns, API client conventions, and UX contract expectations for C4/C5 components.
# @RELATION DEPENDS_ON -> [ADR-0001:ADR]
# @RELATION DEPENDS_ON -> [ADR-0002:ADR]
# @RATIONALE Svelte 5 introduces a fundamentally different reactivity model (runes: `$state`, `$derived`, `$effect`, `$props`) compared to Svelte 4's `$:` reactive statements. This ADR locks in the Svelte 5 runes approach and prevents backsliding into legacy patterns when new developers or AI agents contribute code.
# @RATIONALE SvelteKit with static adapter (`@sveltejs/adapter-static`) was chosen over SvelteKit SSR because: (a) ss-tools is a Dockerdeployed SPA behind nginx — serverside rendering provides no latency benefit, (b) static SPA simplifies Docker deployment (no Node.js server needed, just nginx serving static files), (c) all data is fetched via REST API, not serverside load functions.
# @RATIONALE Svelte 5 runes (`$state`, `$derived`, `$effect`) are mandated over Svelte 4 `$:` reactivity and `writable` stores because: (a) runes are the forward path — Svelte 4 patterns are deprecated, (b) runes provide finegrained reactivity without subscription boilerplate, (c) `$state` can be used outside `.svelte` files in `.svelte.js` modules, enabling reusable reactive logic.
# @REJECTED React/Next.js — rejected by ADR-0003 (technology stack independence from Superset). Svelte 5 was chosen for its compiletime approach, smaller bundle size, and superior DX for this project's scale.
# @REJECTED Svelte 4 legacy patterns (`$:`, `writable`/`readable` stores) — rejected because Svelte 5 runes are the actively maintained reactivity model. Mixing two models creates confusion and potential reactivity bugs.
# @REJECTED ServerSide Rendering (SSR) with SvelteKit — rejected because the data is entirely APIdriven (no serverside page data loading), and SSR adds deployment complexity (Node.js process) without latency benefit for authenticated SPA users.
## Decision
### Technology Stack
| Layer | Choice | Version |
|-------|--------|---------|
| Framework | SvelteKit | 2.x |
| UI Library | Svelte | 5.x (runes mode) |
| Build | Vite | 7.x |
| Styling | Tailwind CSS | 3.x |
| Adapter | @sveltejs/adapter-static | 3.x (SPA mode) |
| Testing | vitest + @testing-library/svelte | 4.x / 5.x |
### Reactivity Model (Svelte 5 Runes)
```
Legacy (FORBIDDEN) Svelte 5 Runes (REQUIRED)
──────────────────────── ──────────────────────────
let count = 0; let count = $state(0);
$: doubled = count * 2; let doubled = $derived(count * 2);
$: { /* side effect */ } $effect(() => { /* side effect */ });
export let name; let { name } = $props();
import { writable } from ... ❌ stores are .svelte.js modules with $state
```
### State Management Pattern
Three tiers of state, strictly separated:
1. **Componentlocal state**: `$state()` inside `.svelte` files. Never exported.
2. **Shared reactive state**: `.svelte.js` modules exporting `$state` objects. Imported by components.
```js
// frontend/src/lib/stores/dashboard.svelte.js
export const dashboardState = $state({
dashboards: [],
selectedId: null,
isLoading: false
});
```
3. **Server state**: Fetched via API client, held in `$state` variables locally. No global cache — each route fetches what it needs.
### API Client Convention
API client modules live under `frontend/src/lib/api/`. Each module wraps `fetch` calls to the FastAPI backend, attaches the JWT access token from the auth store, and normalises errors into a typed `ApiError`. Contract annotations follow the rules defined in the `semantics-core` and `semantics-contracts` skills.
### Component Complexity & UX Contracts
Svelte components with side effects (API calls, WebSocket subscriptions, file operations) MUST carry UX contract tags as defined by the `semantics-frontend` skill (`.opencode/skills/semantics-frontend/SKILL.md`). The skill is the canonical source for tag inventory, syntax, and percomplexity requirements.
### Component File Structure
```
frontend/src/lib/components/
├── DashboardGrid.svelte # C4: complex data grid with WebSocket updates
├── MappingTable.svelte # C3: migration mapping table
├── PluginExecutionCard.svelte # C4: plugin runner with progress feedback
├── RoleBadge.svelte # C1: simple role display
└── ...
```
### Routing
- SvelteKit filebased routing under `frontend/src/routes/`
- Protected routes check auth in `+layout.svelte` (redirect to `/login` if no token)
- Route structure: `/` (dashboard list), `/envs/[id]` (environment detail), `/migration` (migration wizard), `/plugins` (plugin management), `/admin` (user/role management)
# [/DEF:ADR-0006:ADR]