agents ai native front

This commit is contained in:
2026-05-29 22:16:39 +03:00
parent 9ef9fae206
commit c528a37987
6 changed files with 14167 additions and 12493 deletions

View File

@@ -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.

View File

@@ -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 topk / 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 P1P3:
@@ -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: [...]

View File

@@ -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.*<keyword>"` + `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.*<keyword>` across `frontend/src/` to find existing models
- Use `axiom_semantic_discovery search_contracts query="<keyword>" 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`.

View File

@@ -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

View File

@@ -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 topk 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
<!-- #region UserListPage [C:3] [TYPE Component] [SEMANTICS users,list,page] -->
<!-- @BRIEF User list page — renders UserListModel state, delegates all logic to the model. -->
<!-- @RELATION BINDS_TO -> [UserListModel] -->
<!-- @UX_TEST: Loaded -> {click: "delete", expected: User removed, count decremented}. -->
<script>
import { UserListModel } from "./UserListModel.js";
const model = new UserListModel();
// Initial load
$effect(() => { model.loadPage(1); });
</script>
<div class="max-w-7xl mx-auto px-4 py-6">
{#if model.screenState === "error"}
<div role="alert" class="text-red-600">{model.error}</div>
<button onclick={() => model.retry()}>Retry</button>
{:else if model.screenState === "empty"}
<p class="text-gray-500">No users found.</p>
{:else}
<ul>
{#each model.users as user (user.id)}
<li>
{user.name}
<button onclick={() => model.deleteUser(user.id)}>Delete</button>
</li>
{/each}
</ul>
<nav>Page {model.page} of {model.totalPages}</nav>
{/if}
</div>
<!-- #endregion UserListPage -->
```
### 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]