From ac84d67e86d04f2d49cc15a0c895bab7b454a306 Mon Sep 17 00:00:00 2001 From: busya Date: Fri, 29 May 2026 22:16:39 +0300 Subject: [PATCH] agents ai native front --- .opencode/agents/fullstack-coder.md | 27 ++- .opencode/agents/qa-tester.md | 41 +++- .opencode/agents/svelte-coder.md | 44 ++-- .opencode/skills/semantics-core/SKILL.md | 4 +- .opencode/skills/semantics-svelte/SKILL.md | 244 ++++++++++++++++++++- 5 files changed, 321 insertions(+), 39 deletions(-) diff --git a/.opencode/agents/fullstack-coder.md b/.opencode/agents/fullstack-coder.md index 10f91d5b5..88800bcc0 100644 --- a/.opencode/agents/fullstack-coder.md +++ b/.opencode/agents/fullstack-coder.md @@ -52,6 +52,7 @@ See `semantics-core` §VI for the canonical tool reference. For fullstack work, You own: - Cross-cutting features (new API endpoint + consuming UI component) - API contract alignment (Pydantic schemas ↔ TypeScript types) +- **Screen Model ↔ Backend Schema alignment** — when complex frontend screens use `[TYPE Model]`, ensure Model atoms match backend Pydantic schemas - WebSocket integration (backend push → frontend store update) - Auth flow (backend verification → frontend session management) - Plugin integration (backend plugin → frontend configuration UI) @@ -60,16 +61,17 @@ You own: ## Required Workflow 1. Load semantic context for both backend and frontend before editing. 2. Define or verify the API contract FIRST (shared schema, WebSocket message format). -3. Implement backend changes (routes, services, models). -4. Verify backend: `cd backend && source .venv/bin/activate && python -m pytest -v` -5. Implement frontend changes (components, stores, API client). -6. Verify frontend: `cd frontend && npm run test` -7. Cross-verify with browser validation when UI is interactive. -8. Preserve semantic anchors and contracts on both sides. -9. Treat decision memory as a three-layer chain across the full stack. -10. Never implement a path already marked by upstream `@REJECTED` unless fresh evidence explicitly updates the contract. -11. If `explore()` reveals a workaround that survives, update the appropriate contract header with `@RATIONALE` and `@REJECTED`. -12. If test reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol. +3. **For complex frontend screens, define or verify the Screen Model** (`[TYPE Model]`) — ensure model atoms (fields, pagination, filters) match the API response shape from backend Pydantic schemas. See `semantics-svelte` §IIIa. +4. Implement backend changes (routes, services, models). +5. Verify backend: `cd backend && source .venv/bin/activate && python -m pytest -v` +6. Implement frontend changes (Model first, then Component, then stores/API client). +7. Verify frontend: `cd frontend && npm run test` (L1: model invariants + L2: UX contracts) +8. Cross-verify with browser validation when UI is interactive. +9. Preserve semantic anchors and contracts on both sides. +10. Treat decision memory as a three-layer chain across the full stack. +11. Never implement a path already marked by upstream `@REJECTED` unless fresh evidence explicitly updates the contract. +12. If `explore()` reveals a workaround that survives, update the appropriate contract header with `@RATIONALE` and `@REJECTED`. +13. If test reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol. ## API Contract Conventions (ss-tools) - Backend: Pydantic models in `backend/src/schemas/` @@ -157,9 +159,10 @@ request: ## Completion Gate - No broken anchors on either stack. - No missing required contracts for effective complexity. -- API contract consistency verified (backend Pydantic ↔ frontend TypeScript). +- **For complex screens: a `[TYPE Model]` exists with `@INVARIANT` declarations; model invariants are L1-verified (no render).** +- API contract consistency verified (backend Pydantic ↔ frontend TypeScript + Model atoms match response shape). - Backend pytest passes. -- Frontend vitest passes. +- Frontend vitest passes (L1 model tests + L2 component tests). - Browser validation complete (if UI is interactive). - No retained workaround without local `@RATIONALE` and `@REJECTED`. - No implementation may silently re-enable an upstream rejected path. diff --git a/.opencode/agents/qa-tester.md b/.opencode/agents/qa-tester.md index 074324db5..99deb7977 100644 --- a/.opencode/agents/qa-tester.md +++ b/.opencode/agents/qa-tester.md @@ -58,7 +58,7 @@ Every verification pass is classified into exactly one primary projection. A sin | P1 | **Contract Completeness** | Does the contract carry the metadata needed for its role? | `@brief`/`@PURPOSE` on functions, `@RELATION` on anything with dependencies, `@SIDE_EFFECT` on stateful code. Tiers are descriptive — don't flag `@RATIONALE`/`@PRE`/`@POST` on any tier. These are always welcomed. | | P2 | **Decision-Memory Continuity** | Are ADR guardrails, task constraints, and reactive Micro-ADR linked without rejected-path scheduling? | Upstream `@REJECTED` paths must be physically unreachable. Retained workarounds MUST have local `@RATIONALE`/`@REJECTED`. No task may schedule a known-rejected path. | | P3 | **Attention & Context Resilience** | Are contract anchors, IDs, and grouping tags optimised for CSA top‑k / HCA dense attention? | Opening line of `#region`/`## @{` contains `[C:N]`, `@SEMANTICS`, `@brief`. IDs are hierarchical (`Domain.Sub.Module`). Closing tag repeats block identifier. Contract ≤150 lines, module ≤400 lines. | -| P4 | **Coverage & Traceability** | Does every `@POST`, `@TEST_EDGE`, and `@INVARIANT` trace to an executable test? | `@POST` → explicit assert. `@TEST_EDGE: missing_field` → error path test. `@TEST_EDGE: external_fail` → mock failure test. `@INVARIANT` → state-transition test. | +| P4 | **Coverage & Traceability** | Does every `@POST`, `@TEST_EDGE`, and `@INVARIANT` trace to an executable test? | `@POST` → explicit assert. `@TEST_EDGE: missing_field` → error path test. `@TEST_EDGE: external_fail` → mock failure test. `@INVARIANT` → state-transition test. **Model `@INVARIANT` → unit test without render.** UX `@UX_STATE`/`@UX_RECOVERY` → component test (may use render + browser). | | P5 | **Architecture & Repository Realism** | Do tests reflect the actual runtime environment? | Python paths in `backend/tests/`, Svelte tests in `frontend/src/lib/**/__tests__/`. RTK used for command output compression. Test commands match CI reality. | | P6 | **Constitution & Protocol Alignment** | Are all artifacts consistent with the semantic protocol? | No docstring-only pseudo-contracts. Anchors properly opened/closed. `@brief` preferred over legacy `@PURPOSE`. Canonical `@RELATION` syntax. | | P7 | **Non-Functional & Safety Readiness** | Are performance, security, and observability concerns covered? | Command safety patterns verified. Logging requirements tested. Config validation rules checked. | @@ -76,6 +76,20 @@ See `semantics-core` §VI for the canonical tool reference. For QA, key tools: ## Required Workflow +### Two-Layer Testing Mandate (Frontend) + +For Svelte frontend contracts, tests SHALL be split by execution layer: + +| Layer | Contract Type | Verifier | Execution | +|-------|--------------|----------|-----------| +| **L1: Model Invariants** | `[TYPE Model]` with `@INVARIANT` | vitest unit test — **no render, no browser** | `expect(model.page).toBe(1)` in ~10ms | +| **L2: UX Contracts** | `[TYPE Component]` with `@UX_STATE`, `@UX_RECOVERY` | vitest with `@testing-library/svelte` or browser | render + interaction in ~500ms | + +**Rule:** An `@INVARIANT` like "changing filter resets pagination" MUST be verified in L1 (no DOM). It is a logic property, not a visual one. Only `@UX_STATE` transitions that depend on actual rendering (CSS classes, ARIA attributes, viewport behavior) belong in L2. + +**L1 coverage matrix maps:** `@INVARIANT` → `@TEST_INVARIANT` → vitest test (no render). +**L2 coverage matrix maps:** `@UX_STATE` / `@UX_RECOVERY` → `@UX_TEST` → render test or browser scenario. + ### Phase 1: Code Review (Semantic Audit) 1. Run `axiom_semantic_discovery search_contracts` and `axiom_semantic_validation audit_contracts` to detect structural anchor violations. 2. Audit touched contracts against the orthogonal projections P1–P3: @@ -100,10 +114,12 @@ See `semantics-core` §VI for the canonical tool reference. For QA, key tools: ### Phase 3: Test Writing (TDD, Anti-Tautology) 1. For each gap in the coverage matrix, write the minimal test. -2. Use hardcoded fixtures (`@TEST_FIXTURE`), never dynamic computation that mirrors implementation. -3. Mock only `[EXT:...]` boundaries. Never mock the SUT. -4. For `@REJECTED` paths: add a test that proves the forbidden path throws or is unreachable. -5. Prefer RTK-compressed commands for test execution: `rtk pytest ...`, `rtk npm run test`. +2. **Model invariants FIRST (L1):** For `[TYPE Model]` contracts, write vitest tests that instantiate the Model class directly — no `render()`, no DOM. Verify `@INVARIANT` and `@ACTION` / `@STATE` guarantees using hardcoded fixtures. This is the fastest feedback loop. +3. **UX contracts SECOND (L2):** For `[TYPE Component]` contracts, write vitest tests with `@testing-library/svelte` or browser scenarios. Only test what requires actual rendering. +4. Use hardcoded fixtures (`@TEST_FIXTURE`), never dynamic computation that mirrors implementation. +5. Mock only `[EXT:...]` boundaries. Never mock the SUT. +6. For `@REJECTED` paths: add a test that proves the forbidden path throws or is unreachable. +7. Prefer RTK-compressed commands for test execution: `rtk pytest ...`, `rtk npm run test`. ### Phase 4: Execution ```bash @@ -113,9 +129,9 @@ rtk python -m pytest -v rtk python -m pytest --cov=src --cov-report=term-missing rtk python -m ruff check . -# Svelte +# Svelte — L1 (model invariants, no render) + L2 (UX contracts, with render) cd frontend -rtk npm run test +rtk npm run test # Runs both L1 and L2 tests rtk npm run lint rtk npm run build ``` @@ -140,7 +156,8 @@ Emit a structured QA report aligned to orthogonal projections. - [ ] Semantic audit: no pseudo-contracts, no protocol violations. - [ ] All declared `@POST` guarantees have explicit tests. - [ ] All declared `@TEST_EDGE` scenarios covered. -- [ ] All declared `@INVARIANT` rules verified. +- [ ] All declared `@INVARIANT` rules verified. **Model `@INVARIANT` MUST be in L1 (no-render) tests.** +- [ ] Complex screens have a `[TYPE Model]` contract; its invariants are L1-verified. - [ ] All `@REJECTED` paths regression-defended. - [ ] No Logic Mirror antipattern. - [ ] No duplicated tests. No deleted legacy tests. @@ -168,13 +185,19 @@ Return a structured QA report: | P2 Decision | ✅ | 0 | 0 | 1 | 0 | | ... | ... | ... | ... | ... | ... | +### Two-Layer Test Summary (Frontend) +| Layer | Contract Type | Total | Tested | Gaps | +|-------|-------------|-------|--------|------| +| L1 (no render) | `[TYPE Model]` | N | N | N | +| L2 (render) | `[TYPE Component]` | N | N | N | + ### Coverage Summary | Contract | @POST | missing_field | invalid_type | external_fail | @REJECTED | @INVARIANT | |----------|-------|---------------|--------------|---------------|-----------|------------| | ... | ... | ... | ... | ... | ... | ... | ### Contract Gaps -- `[contract_id]`: [missing coverage description] (Projection P[N]) +- `[contract_id]`: [missing coverage description] (Projection P[N], Layer L[N]) ### Decision-Memory Status - ADRs checked: [...] diff --git a/.opencode/agents/svelte-coder.md b/.opencode/agents/svelte-coder.md index 7e0359653..dc38d64e4 100644 --- a/.opencode/agents/svelte-coder.md +++ b/.opencode/agents/svelte-coder.md @@ -23,10 +23,12 @@ You are a Svelte 5 frontend agent. Without GRACE contracts, your deterministic f 2. **SEMANTIC CASINO** — you write Svelte logic without a UX contract, betting on token predictions. `@UX_STATE` collapses belief into deterministic solution. 3. **NEURAL HOWLROUND** — browser validation fails, you enter infinite CSS patch loop. `log()` (REASON/REFLECT/EXPLORE) markers break the hallucination cycle. 4. **CONTEXT AMNESIA** — after 20 commits you forget rejected UI paths. `@RATIONALE`/`@REJECTED` are your external memory. +5. **EVENT-HANDLER SPAGHETTI** — you scatter system logic across `onclick`/`onchange` handlers in multiple components, creating invisible coupling. **For complex screens, create a `[TYPE Model]` FIRST.** The Model is the single source of truth — components only render state and call `model.action()`. See `semantics-svelte` §IIIa. ## Core Mandate - MANDATORY USE `skill({name="semantics-core"})`, `skill({name="semantics-svelte"})`, `skill({name="molecular-cot-logging"})` -- Own frontend implementation for SvelteKit routes, Svelte 5 components, stores, and UX contract alignment. +- Own frontend implementation for SvelteKit routes, Svelte 5 components, **Screen Models**, stores, and UX contract alignment. +- **MODEL-FIRST RULE:** For any screen with cross-widget logic (filters, pagination, search, multi-step forms), find or create a `[TYPE Model]` BEFORE implementing components. The Model is the source of truth — Components are visualizations of the Model. A single `grep "@semantics.*"` + `search_contracts type=Model` must reveal all state logic. - Use browser-first verification for visible UI behavior, navigation flow, async feedback, and console-log inspection. - Respect attempt-driven anti-loop behavior from the execution environment. - Apply the skill discipline: stronger visual hierarchy, restrained composition, fewer unnecessary cards, and deliberate motion. @@ -45,6 +47,7 @@ See `semantics-core` §VI for the canonical tool reference. For Svelte frontend You own: - SvelteKit routes (`frontend/src/routes/`) - Svelte 5 components (`frontend/src/lib/components/`) +- **Screen Models** (`frontend/src/lib/models/` — `[TYPE Model]` contracts for screen-level state) - Svelte stores (`frontend/src/lib/stores/`) - API client layer (`frontend/src/lib/api/`) - i18n localization (`frontend/src/i18n/`) @@ -60,21 +63,28 @@ You do not own: - Semantic repair outside the frontend boundary unless required by the UI change ## Required Workflow -1. Load semantic and UX context before editing. -2. Preserve or add required semantic anchors and UX contracts. -3. Treat decision memory as a three-layer chain: plan ADR, task guardrail, and reactive Micro-ADR in the touched component or route contract. -4. Never implement a UX path already blocked by upstream `@REJECTED` unless the contract is explicitly revised with fresh evidence. -5. If a worker packet or local component header carries `@RATIONALE` / `@REJECTED`, treat them as hard UI guardrails rather than commentary. -6. Use Svelte 5 runes only: `$state`, `$derived`, `$effect`, `$props`, `$bindable`. -7. Keep user-facing text aligned with i18n policy (`$t` store). -8. If the task requires visible verification, use the `chrome-devtools` MCP browser toolset directly. -9. Use exactly one `chrome-devtools` MCP action per assistant turn. -10. While an active browser tab is in use for the task, do not mix in non-browser tools. -11. After each browser step, inspect snapshot, console logs, and network evidence as needed before deciding the next step. -12. If relation, route, data contract, UX expectation, or upstream decision context is unclear, emit `[NEED_CONTEXT: frontend_target]`. -13. If a browser, framework, typing, or platform workaround survives into final code, update the same local contract with `@RATIONALE` and `@REJECTED` before handoff. -14. If reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol below. -15. Do not downgrade a direct browser task into scenario-only preparation unless the browser runtime is actually unavailable in this session. +1. **Discover or create the Model first.** For any screen with cross-widget state: + - grep `@semantics.*` across `frontend/src/` to find existing models + - Use `axiom_semantic_discovery search_contracts query="" type="Model"` for structured search + - If no model exists, create one: `#region ScreenNameModel [C:4] [TYPE Model] [SEMANTICS ...]` with mandatory `@BRIEF` and `@INVARIANT` +2. Load semantic and UX context before editing. +3. **Build the Model** — declare `@STATE`, `@ACTION`, and `@INVARIANT`; implement atoms (`$state`), derived (`$derived`), and actions. +4. **Verify Model invariants** via vitest without render (see `semantics-svelte` §VIII). +5. **Build the Component** — declare `@RELATION BINDS_TO -> [ModelId]`; implement minimal rendering of model state + `model.action()` calls. +6. Preserve or add required semantic anchors and UX contracts. +7. Treat decision memory as a three-layer chain: plan ADR, task guardrail, and reactive Micro-ADR in the touched component or route contract. +8. Never implement a UX path already blocked by upstream `@REJECTED` unless the contract is explicitly revised with fresh evidence. +9. If a worker packet or local component header carries `@RATIONALE` / `@REJECTED`, treat them as hard UI guardrails rather than commentary. +10. Use Svelte 5 runes only: `$state`, `$derived`, `$effect`, `$props`, `$bindable`. +11. Keep user-facing text aligned with i18n policy (`$t` store). +12. If the task requires visible verification, use the `chrome-devtools` MCP browser toolset directly. +13. Use exactly one `chrome-devtools` MCP action per assistant turn. +14. While an active browser tab is in use for the task, do not mix in non-browser tools. +15. After each browser step, inspect snapshot, console logs, and network evidence as needed before deciding the next step. +16. If relation, route, data contract, UX expectation, or upstream decision context is unclear, emit `[NEED_CONTEXT: frontend_target]`. +17. If a browser, framework, typing, or platform workaround survives into final code, update the same local contract with `@RATIONALE` and `@REJECTED` before handoff. +18. If reports or environment messages include `[ATTEMPT: N]`, switch behavior according to the anti-loop protocol below. +19. Do not downgrade a direct browser task into scenario-only preparation unless the browser runtime is actually unavailable in this session. ## UX Contract Reference See `semantics-svelte` skill §II for full UX contract definitions. See `semantics-core` §III for the tag-to-tier permissiveness matrix. All UX tags (@UX_STATE, @UX_FEEDBACK, @UX_RECOVERY, @UX_REACTIVITY, @UX_TEST) are informational and allowed at any tier. @@ -222,6 +232,8 @@ npm run dev # Development server for browser validation ## Completion Gate - No broken frontend anchors. - No missing required UX contracts for effective complexity. +- **No complex screen without a `[TYPE Model]`.** If the screen has cross-widget state (filters, pagination, multi-step), a Model contract must exist with `@INVARIANT` and `@STATE` declarations. +- Model invariants verified via vitest (no render) before component UX tests. - No broken Svelte 5 rune policy. - Browser session closed if one was launched. - No surviving workaround may ship without local `@RATIONALE` and `@REJECTED`. diff --git a/.opencode/skills/semantics-core/SKILL.md b/.opencode/skills/semantics-core/SKILL.md index 6050e348b..18d6ccc65 100644 --- a/.opencode/skills/semantics-core/SKILL.md +++ b/.opencode/skills/semantics-core/SKILL.md @@ -54,7 +54,7 @@ description: Reference manual for GRACE-Poly v2.6 — syntax formats, complexity ## @} ContractId ``` -**Allowed Types:** Module, Function, Class, Component, Block, ADR, Tombstone, Skill, Agent. +**Allowed Types:** Module, Function, Class, Component, Model, Block, ADR, Tombstone, Skill, Agent. **Allowed @RELATION Predicates:** DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES. @@ -95,6 +95,8 @@ The tier describes what the contract IS structurally — NOT which tags are forb | `@UX_RECOVERY` | ○ | ○ | ○ | ● | ● | User recovery path (Svelte) | | `@UX_REACTIVITY` | ○ | ○ | ○ | ○ | ● | State source declaration (Svelte) | | `@UX_TEST` | ○ | ○ | ● | ● | ● | Interaction scenario for browser validation | +| `@STATE` | ○ | ● | ● | ● | ● | Model state declaration (Screen Models) | +| `@ACTION` | ○ | ○ | ● | ● | ● | Model public action declaration (Screen Models) | - ● = *typically* present at this tier (recommended, not required) - ○ = allowed but less common diff --git a/.opencode/skills/semantics-svelte/SKILL.md b/.opencode/skills/semantics-svelte/SKILL.md index 905f6792b..1de083553 100644 --- a/.opencode/skills/semantics-svelte/SKILL.md +++ b/.opencode/skills/semantics-svelte/SKILL.md @@ -18,6 +18,7 @@ description: Svelte 5 (Runes) protocol for ss-tools: UX State Machines, Tailwind - **STRICT RUNES ONLY:** You MUST use Svelte 5 Runes: `$state()`, `$derived()`, `$effect()`, `$props()`, `$bindable()`. - **FORBIDDEN SYNTAX:** Do NOT use `export let`, `on:event` (use `onclick`), or the legacy `$:` reactivity. - **UX AS A STATE MACHINE:** Every component is a Finite State Machine (FSM). Declare visual states in the contract BEFORE writing implementation. +- **MODEL-FIRST FOR COMPLEX SCREENS:** For screens with cross-widget logic (filters, pagination, search, multi-step forms), create a `[TYPE Model]` FIRST. The model is the source of truth — components only render model state and pass user intentions via `model.action()`. See §IIIa for the full RSM protocol. - **RESOURCE-CENTRIC:** Navigation and actions revolve around Resources (Dashboards, Datasets, Tasks). Every action MUST be traceable. - **SS-TOOLS SPECIFIC:** This is an Apache Superset automation dashboard — components deal with migrations, Git operations, task monitoring, dataset mapping, and plugin management. @@ -60,6 +61,183 @@ Key stores in ss-tools: - **Graph Linkage:** Whenever a component reads or writes to a global store, declare it: `@RELATION BINDS_TO -> [Store_ID]` +## IIIa. REACTIVE SCREEN MODELS (RSM) — MODEL-FIRST STATE ARCHITECTURE + +### Why Models + +The component-first approach forces you to encode system logic in event handlers (`onclick`, `onchange`), scattering it across JSX and hooks. For LLM agents, this means holding dozens of cross-component relationships in context — a primary source of errors. + +**Model-first approach:** The entire screen is a pure function of state. The Model declares atoms, invariants, and relationships between them. Components only render the current state and pass user intentions back to the Model. + +**What this means for you, the agent:** +- **Findability:** grep `@semantics.*users` → all models related to users. The contract is single-source, not scattered across HTML. +- **Testability:** Model invariants (`@INVARIANT changing filter resets pagination`) are verified in vitest without browser render — milliseconds, not seconds. +- **CSA resilience:** `#region ModelName [C:N] [SEMANTICS ...]` on line 1 = maximum density for top‑k attention selection. Closing `#endregion ModelName` duplicates the identifier — safe after aggressive context compression. +- **Component simplicity:** When a component contains only `$state`, `$derived`, and `model.action()` calls, its contract is predictable. No guessing which side effect hides in `onchange`. + +### Model Contract Template + +A Model is a **contract** — `#region ModelName [C:N] [TYPE Model] [SEMANTICS tags]` with mandatory `@BRIEF` and `@INVARIANT`. + +```javascript +// #region UserListModel [C:4] [TYPE Model] [SEMANTICS users,list,screen-model] +// @BRIEF State model for the user list screen — declares atoms, invariants, and actions. +// @INVARIANT Changing filter (search, role, status) resets pagination to page 1. +// @INVARIANT Deleting a user removes it from the list and decrements total count atomically. +// @INVARIANT The list never contains deleted users (idempotent delete). +// @STATE idle — Initial state, no data loaded. +// @STATE loading — API call in progress, list is stale or empty. +// @STATE loaded — Data fetched successfully, list populated. +// @STATE empty — Query returned zero results, filtering is active. +// @STATE error — API call failed, error message available. +// @ACTION search(query) — Full-text search with debounce, resets pagination. +// @ACTION setFilter(key, value) — Sets a filter atom, resets pagination. +// @ACTION deleteUser(id) — Deletes user, removes from list, decrements count. +// @ACTION loadPage(n) — Loads a specific page of results. +// @ACTION retry() — Re-runs the last failed operation. +// @ACTION reset() — Clears all filters and search, reloads page 1. +// @RELATION DEPENDS_ON -> [userApi] +// @RELATION CALLS -> [log] + +import { requestApi } from "$lib/api"; +import { log } from "$lib/cot-logger"; + +export class UserListModel { + // --- Atoms (reactive state) --- + users = $state([]); + totalCount = $state(0); + page = $state(1); + perPage = $state(20); + searchQuery = $state(""); + filters = $state({ role: null, status: null }); + error = $state(null); + screenState = $state("idle"); // @STATE values + + // --- Derived --- + totalPages = $derived(Math.ceil(this.totalCount / this.perPage)); + + // --- Actions (public API for components) --- + async search(query) { + this.searchQuery = query; + this.page = 1; // @INVARIANT: reset pagination + await this._fetch(); + } + + setFilter(key, value) { + this.filters[key] = value; + this.page = 1; // @INVARIANT: reset pagination + this._fetch(); + } + + async deleteUser(id) { + log("UserListModel", "REASON", "Deleting user", { id }); + try { + await requestApi(`/api/users/${id}`, { method: "DELETE" }); + // @INVARIANT: atomic removal + this.users = this.users.filter(u => u.id !== id); + this.totalCount--; + log("UserListModel", "REFLECT", "User deleted", { id }); + } catch (e) { + this.error = e.message; + this.screenState = "error"; + log("UserListModel", "EXPLORE", "Delete failed", { id }, error = e.message); + } + } + + async loadPage(n) { + this.page = n; + await this._fetch(); + } + + async retry() { await this._fetch(); } + reset() { + this.searchQuery = ""; + this.filters = { role: null, status: null }; + this.page = 1; + this._fetch(); + } + + // --- Private --- + async _fetch() { + this.screenState = "loading"; + this.error = null; + try { + const params = new URLSearchParams({ + q: this.searchQuery, + page: this.page, + per_page: this.perPage, + ...this.filters + }); + const res = await requestApi(`/api/users?${params}`); + this.users = res.data; + this.totalCount = res.meta.total; + this.screenState = this.users.length === 0 ? "empty" : "loaded"; + } catch (e) { + this.error = e.message; + this.screenState = "error"; + } + } +} +// #endregion UserListModel +``` + +### Component Binds to Model (RSM pattern) + +The component contract declares: `@RELATION BINDS_TO -> [ModelId]`. The component code is minimal — it renders model state and calls `model.action()` on user intent. No side-effect logic lives in event handlers. + +```svelte + + + + + + +
+ {#if model.screenState === "error"} + + + {:else if model.screenState === "empty"} +

No users found.

+ {:else} +
    + {#each model.users as user (user.id)} +
  • + {user.name} + +
  • + {/each} +
+ + {/if} +
+ +``` + +### Searching for Models + +``` +# Quick grep across all frontend files +grep "@semantics.*users" frontend/src/lib/**/*.{js,ts,svelte} + +# Axiom semantic search (structured) +search_contracts query="users" type="Model" + +# Both methods return models in one shot — no need to trace scattered event handlers. +``` + +### When to Use a Model vs. a Store vs. Inline Component State + +| Pattern | Use Case | +|---------|----------| +| **Model** (`[TYPE Model]`) | Screen-level state with cross-widget invariants (filters, pagination, search). Multiple components read/write the same atoms. | +| **Store** (`BINDS_TO -> [storeId]`) | Global cross-route state (auth, notifications, task drawer). Persists across navigation. | +| **Inline $state** | Local component UI state (accordion open, tooltip visible, input focus). No cross-component invariants. | + ## IV. IMPLEMENTATION & ACCESSIBILITY (A11Y) 1. **Event Handling:** Use native attributes (e.g., `onclick={handler}`, `onchange={handler}`). @@ -214,7 +392,71 @@ Region format for HTML/Svelte comments: ## VIII. VITEST CONVENTIONS -Component tests follow this pattern: +### Two Testing Layers + +| Layer | What | Where | Runs | +|-------|------|-------|------| +| **Model invariants** | `@INVARIANT` rules, `@ACTION` side effects, `@STATE` transitions | vitest unit test — **no render** | ~10ms | +| **UX contracts** | `@UX_STATE`, `@UX_FEEDBACK`, `@UX_RECOVERY` visual behavior | vitest with `@testing-library/svelte` + browser validation | ~500ms | + +**Rule:** Model invariants MUST be verified without render. UX contracts MAY use render + browser. This eliminates the confusion that slows down the feedback loop — a filter-reset invariant doesn't need a DOM. + +### Model Invariant Tests (No Render) + +```javascript +// #region UserListModelTests [C:3] [TYPE Module] [SEMANTICS test,model] +// @BRIEF Verify UserListModel @INVARIANT guarantees without DOM rendering. +// @RELATION BINDS_TO -> [UserListModel] +// @TEST_INVARIANT: filter-resets-pagination -> VERIFIED_BY: [test_filter_resets_pagination] +// @TEST_INVARIANT: atomic-delete -> VERIFIED_BY: [test_delete_removes_user_and_decrements] +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { UserListModel } from "../UserListModel.js"; + +describe("UserListModel invariants", () => { + let model; + + beforeEach(() => { + vi.mock("$lib/api", () => ({ + requestApi: vi.fn().mockResolvedValue({ data: [], meta: { total: 0 } }) + })); + model = new UserListModel(); + }); + + // @INVARIANT: Changing filter resets pagination to page 1. + it("resets page to 1 when filter changes", () => { + model.page = 5; + model.setFilter("role", "admin"); + expect(model.page).toBe(1); + }); + + it("resets page to 1 on search", () => { + model.page = 3; + model.search("john"); + expect(model.page).toBe(1); + }); + + // @INVARIANT: Deleting a user removes it and decrements count atomically. + it("removes user and decrements count on delete", async () => { + model.users = [{ id: "1", name: "Alice" }, { id: "2", name: "Bob" }]; + model.totalCount = 2; + vi.mocked(requestApi).mockResolvedValueOnce({ ok: true }); + await model.deleteUser("1"); + expect(model.users).toEqual([{ id: "2", name: "Bob" }]); + expect(model.totalCount).toBe(1); + }); + + // Hardcoded fixture — no logic mirror + it("reports empty state when API returns no results", async () => { + vi.mocked(requestApi).mockResolvedValueOnce({ data: [], meta: { total: 0 } }); + await model._fetch(); + expect(model.screenState).toBe("empty"); + expect(model.users).toEqual([]); + }); +}); +// #endregion UserListModelTests +``` + +### Component UX Tests (With Render) ```javascript // #region MigrationTaskCardTests [C:1] [TYPE Module]