feat(translate): move write settings to Target Config tab + fix datasource name lookup
Fixes: - Move batch size, upsert strategy, include source reference from Config tab to Target Config tab (Write Settings section) - Fix 'Dataset #26' fallback — look up real datasource name via API when source_table is not saved in job data - Persist include_source_reference in save payload and restore from job data on load (was a pre-existing gap) QA review fixes: - Update @BRIEF contracts for both tabs - Fix import indentation in +page.svelte Other: help tooltips on translate pages, flow hint on list page, git migration manager components, dashboard hub pages, migration settings pages
This commit is contained in:
@@ -76,6 +76,8 @@ You own:
|
||||
## API Contract Conventions (ss-tools)
|
||||
- Backend: Pydantic models in `backend/src/schemas/`
|
||||
- Frontend: TypeScript types in `frontend/src/types/`
|
||||
- **Frontend DTOs MUST match backend Pydantic schemas** — agent must verify type alignment across the stack boundary. Model `.svelte.ts` files use typed atoms conforming to frontend DTOs.
|
||||
- `any` is forbidden at the API boundary — use `unknown` with runtime validation/narrowing.
|
||||
- URL prefix: `/api/` for REST, `/ws/` for WebSocket
|
||||
- Response envelope: `{ status, data, error, meta }`
|
||||
- Error codes: Consistent across backend and frontend
|
||||
|
||||
@@ -24,11 +24,13 @@ You are a Svelte 5 frontend agent. Without GRACE contracts, your deterministic f
|
||||
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.
|
||||
6. **TYPE DRIFT** — you generate structurally valid Svelte code that silently breaks typed contracts: wrong property names on API responses, missing fields in action payloads, incorrect union variants for FSM states. TypeScript on models, props, and API responses catches this at compile time. Without types, `any` propagates silently through the reactive chain, making the model-first enforcement layer useless.
|
||||
|
||||
## 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, **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.
|
||||
- **TYPESCRIPT-FIRST RULE:** All frontend code MUST use TypeScript. Components via `<script lang="ts">`. Models via `.svelte.ts` extension (Svelte-aware TS modules for `$state`/`$derived`/`$effect`). API DTOs typed via `types/` directory. Types are the enforcement layer for model-first architecture — `$state` atoms, action payloads, and component props without type annotations are incomplete. `any` is forbidden at external boundaries; use `unknown` with explicit narrowing. See `semantics-svelte` §IIIb.
|
||||
- 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.
|
||||
@@ -67,6 +69,13 @@ You do not own:
|
||||
- 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`
|
||||
1.5. **Define types FIRST before implementing the model:**
|
||||
- FSM state union type (e.g., `type ScreenState = "idle" | "loading" | "loaded" | "error"`)
|
||||
- Model atom interfaces (atoms shape, derived value types)
|
||||
- Action payload interfaces
|
||||
- API response DTOs matching backend Pydantic schemas
|
||||
- Component props interface
|
||||
- All `.svelte.ts` model files start with type declarations before the class body
|
||||
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).
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"experimental": {
|
||||
"mcp_timeout": 300000
|
||||
},
|
||||
"mcp": {
|
||||
"chrome-devtools": {
|
||||
"type": "local",
|
||||
|
||||
@@ -58,6 +58,8 @@ description: Reference manual for GRACE-Poly v2.6 — syntax formats, complexity
|
||||
|
||||
**Allowed @RELATION Predicates:** DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES.
|
||||
|
||||
**Canonical Model format:** Model contracts that use Svelte reactive primitives (`$state`, `$derived`, `$effect`) MUST use the `.svelte.ts` file extension. The Svelte compiler processes `.svelte.ts` files and transforms runes into proper reactive code. Plain `.ts`/`.js` files cannot host Svelte reactive primitives.
|
||||
|
||||
## III. COMPLEXITY SCALE (descriptive signal)
|
||||
|
||||
The tier describes what the contract IS structurally — NOT which tags are forbidden at that tier. All `@`-tags are informational documentation and are **universally allowed at every tier (C1-C5).**
|
||||
|
||||
@@ -15,8 +15,11 @@ description: Svelte 5 (Runes) protocol for ss-tools: UX State Machines, Tailwind
|
||||
|
||||
## 0. SVELTE 5 PARADIGM & UX PHILOSOPHY (SS-TOOLS)
|
||||
|
||||
- **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.
|
||||
- **TYPESCRIPT-FIRST:** TypeScript is the default language for ALL frontend code. Components use `<script lang="ts">`. Screen Models use `.svelte.ts` extension. API DTOs live in `frontend/src/types/` with explicit interfaces. Types are the contract between the model and the component — without them, the model-first architecture has no enforcement layer. `any` is forbidden at external boundaries; use `unknown` with runtime narrowing.
|
||||
- **STRICT RUNES ONLY (PROJECT RULE):** Our project deliberately chooses Svelte 5 runes exclusively — `$state()`, `$derived()`, `$effect()`, `$props()`, `$bindable()`. This is a codebase-wide architectural decision, not a Svelte 5 limitation. Every component follows the same reactive pattern; every model is a predictable `$state` container. Mixed styles create agent confusion and verification gaps.
|
||||
- **FORBIDDEN SYNTAX (PROJECT RULE):** Do NOT use `export let`, `on:event`, `createEventDispatcher`, or the legacy `$:` reactivity. These are banned for architectural consistency — not because Svelte 5 cannot run them.
|
||||
- **EVENT ARCHITECTURE:** DOM events use native attributes (`onclick`, `onchange`, `onsubmit`). Component-to-component communication uses typed callback props — NEVER `createEventDispatcher` or `on:event` directives. This keeps component interfaces explicit, type-checkable, and free of runtime event bus ambiguity.
|
||||
- **$effect SCOPE:** `$effect` is for browser-side side effects only — DOM measurements, WebSocket subscriptions, external library bindings, synchronising state that cannot be expressed as `$derived`. For route-level data loading, use SvelteKit `load` functions in `+page.ts`. Using `$effect` for initial data fetch breaks SSR and creates unpredictable loading order.
|
||||
- **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.
|
||||
@@ -79,7 +82,10 @@ The component-first approach forces you to encode system logic in event handlers
|
||||
|
||||
A Model is a **contract** — `#region ModelName [C:N] [TYPE Model] [SEMANTICS tags]` with mandatory `@BRIEF` and `@INVARIANT`.
|
||||
|
||||
```javascript
|
||||
Models use the **`.svelte.ts`** extension (not plain `.ts`) because they rely on Svelte 5 reactive primitives (`$state`, `$derived`). The Svelte compiler processes `.svelte.ts` files and transforms these runes into proper reactive code.
|
||||
|
||||
```typescript
|
||||
// frontend/src/lib/models/UserListModel.svelte.ts
|
||||
// #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.
|
||||
@@ -100,80 +106,97 @@ A Model is a **contract** — `#region ModelName [C:N] [TYPE Model] [SEMANTICS t
|
||||
// @RELATION CALLS -> [log]
|
||||
|
||||
import { requestApi } from "$lib/api";
|
||||
import { log } from "$lib/cot-logger";
|
||||
|
||||
// ── Type definitions (boundary types) ───────────────────────────
|
||||
|
||||
type ScreenState = "idle" | "loading" | "loaded" | "empty" | "error";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface UserFilters {
|
||||
role: string | null;
|
||||
status: string | null;
|
||||
}
|
||||
|
||||
interface UserListResponse {
|
||||
data: User[];
|
||||
meta: { total: number };
|
||||
}
|
||||
|
||||
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
|
||||
// ── Atoms (reactive state, all typed) ──────────────────────────
|
||||
users: User[] = $state([]);
|
||||
totalCount: number = $state(0);
|
||||
page: number = $state(1);
|
||||
perPage: number = $state(20);
|
||||
searchQuery: string = $state("");
|
||||
filters: UserFilters = $state({ role: null, status: null });
|
||||
error: string | null = $state(null);
|
||||
screenState: ScreenState = $state("idle");
|
||||
|
||||
// --- Derived ---
|
||||
totalPages = $derived(Math.ceil(this.totalCount / this.perPage));
|
||||
// ── Derived ────────────────────────────────────────────────────
|
||||
totalPages: number = $derived(Math.ceil(this.totalCount / this.perPage));
|
||||
|
||||
// --- Actions (public API for components) ---
|
||||
async search(query) {
|
||||
// ── Actions (typed public API for components) ──────────────────
|
||||
async search(query: string): Promise<void> {
|
||||
this.searchQuery = query;
|
||||
this.page = 1; // @INVARIANT: reset pagination
|
||||
await this._fetch();
|
||||
}
|
||||
|
||||
setFilter(key, value) {
|
||||
setFilter<K extends keyof UserFilters>(key: K, value: UserFilters[K]): void {
|
||||
this.filters[key] = value;
|
||||
this.page = 1; // @INVARIANT: reset pagination
|
||||
this._fetch();
|
||||
}
|
||||
|
||||
async deleteUser(id) {
|
||||
log("UserListModel", "REASON", "Deleting user", { id });
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
try {
|
||||
await requestApi(`/api/users/${id}`, { method: "DELETE" });
|
||||
// @INVARIANT: atomic removal
|
||||
this.users = this.users.filter(u => u.id !== id);
|
||||
this.users = this.users.filter((u: User) => u.id !== id);
|
||||
this.totalCount--;
|
||||
log("UserListModel", "REFLECT", "User deleted", { id });
|
||||
} catch (e) {
|
||||
this.error = e.message;
|
||||
} catch (e: unknown) {
|
||||
this.error = e instanceof Error ? e.message : "Delete failed";
|
||||
this.screenState = "error";
|
||||
log("UserListModel", "EXPLORE", "Delete failed", { id }, error = e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async loadPage(n) {
|
||||
async loadPage(n: number): Promise<void> {
|
||||
this.page = n;
|
||||
await this._fetch();
|
||||
}
|
||||
|
||||
async retry() { await this._fetch(); }
|
||||
reset() {
|
||||
async retry(): Promise<void> { await this._fetch(); }
|
||||
|
||||
reset(): void {
|
||||
this.searchQuery = "";
|
||||
this.filters = { role: null, status: null };
|
||||
this.page = 1;
|
||||
this._fetch();
|
||||
}
|
||||
|
||||
// --- Private ---
|
||||
async _fetch() {
|
||||
// ── Private ────────────────────────────────────────────────────
|
||||
private async _fetch(): Promise<void> {
|
||||
this.screenState = "loading";
|
||||
this.error = null;
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
q: this.searchQuery,
|
||||
page: this.page,
|
||||
per_page: this.perPage,
|
||||
page: String(this.page),
|
||||
per_page: String(this.perPage),
|
||||
...this.filters
|
||||
});
|
||||
const res = await requestApi(`/api/users?${params}`);
|
||||
} as Record<string, string>);
|
||||
const res: UserListResponse = 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;
|
||||
} catch (e: unknown) {
|
||||
this.error = e instanceof Error ? e.message : "Fetch failed";
|
||||
this.screenState = "error";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user