agents ai native front
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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: [...]
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
<!-- #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]
|
||||
|
||||
Reference in New Issue
Block a user