diff --git a/.claude/skills/refine/SKILL.md b/.claude/skills/refine/SKILL.md new file mode 100644 index 0000000000..aaf747cd29 --- /dev/null +++ b/.claude/skills/refine/SKILL.md @@ -0,0 +1,39 @@ +--- +name: refine +user_invocable: true +description: End-of-session reflection. Reviews friction encountered during the session and proposes updates to docs/ to capture lessons learned. +--- + +# Refine Skill + +Reflect on the current session and update documentation with lessons learned. + +## Instructions + +1. **Identify friction**: Review what happened in this session: + - Run `git diff main...HEAD --stat` to see what files were touched + - Think about: what was slow, what failed, what required multiple attempts, what information was missing or hard to find + +2. **Read current docs**: Read the docs that were relevant to this session: + - `docs/validation.md` + - `docs/enterprise.md` + - `docs/autonomous-mode.md` + - Any skills that were invoked + +3. **Propose updates**: For each piece of friction, decide if it warrants a doc update: + - **Missing knowledge**: Information you had to discover that should be documented + - **Wrong guidance**: Instructions that led you astray + - **Missing validation rule**: A check that should be in the validation matrix + - **New pattern**: A codebase pattern worth capturing for next time + +4. **Apply updates**: Edit the relevant `docs/` files. Keep changes minimal and specific — add only what would have saved time this session. + +5. **Report**: Summarize what was added/changed and why. + +## Rules + +- Only add knowledge confirmed by this session — no speculative additions +- Keep docs concise — add a line or two, not a paragraph +- If a whole new doc is needed, create it in `docs/` and add a pointer in `CLAUDE.md` +- Don't update skills unless a coding pattern was genuinely wrong +- Don't add things Claude already knows — only Windmill-specific knowledge diff --git a/.claude/skills/rust-backend/SKILL.md b/.claude/skills/rust-backend/SKILL.md index c2253319a6..f0c52002bc 100644 --- a/.claude/skills/rust-backend/SKILL.md +++ b/.claude/skills/rust-backend/SKILL.md @@ -3,493 +3,105 @@ name: rust-backend description: Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory. --- -# Rust Backend Coding Guidelines +# Windmill Rust Patterns -Apply these patterns when writing or modifying Rust code in the `backend/` directory. - -## Data Structure Design - -Choose between `struct`, `enum`, or `newtype` based on domain needs: - -- Use `enum` for state machines instead of boolean flags or loosely related fields -- Model invariants explicitly using types (e.g., `NonZeroU32`, `Duration`, custom enums) -- Consider ownership of each field: - - Use `&str` vs `String`, slices vs vectors - - Use `Arc` when sharing across threads - - Use `Cow<'a, T>` for flexible ownership - -```rust -// State machine with enum -enum JobState { - Pending { scheduled_for: DateTime }, - Running { started_at: DateTime, worker: String }, - Completed { result: JobResult, duration_ms: i64 }, - Failed { error: String, retries: u32 }, -} - -// Avoid multiple booleans -struct Job { - is_pending: bool, // Don't do this - is_running: bool, - is_completed: bool, -} -``` - -## Impl Block Organization - -Place `impl` blocks immediately below the struct/enum they modify. Group methods logically: - -```rust -struct JobQueue { - jobs: Vec, - capacity: usize, -} - -impl JobQueue { - // Constructors first - pub fn new(capacity: usize) -> Self { ... } - pub fn with_jobs(jobs: Vec) -> Self { ... } - - // Getters - pub fn len(&self) -> usize { ... } - pub fn is_empty(&self) -> bool { ... } - - // Mutation methods - pub fn push(&mut self, job: Job) -> Result<()> { ... } - pub fn pop(&mut self) -> Option { ... } - - // Domain logic - pub fn next_scheduled(&self) -> Option<&Job> { ... } -} -``` - -## Iterator Chains Over For-Loops - -Prefer functional iterator chains (`.filter().map().collect()`) over imperative for-loops: - -```rust -// Preferred -let results: Vec<_> = items - .iter() - .filter(|item| item.is_valid()) - .map(|item| item.transform()) - .collect(); - -// Avoid -let mut results = Vec::new(); -for item in items.iter() { - if item.is_valid() { - results.push(item.transform()); - } -} -``` +Apply these Windmill-specific patterns when writing Rust code in `backend/`. ## Error Handling -Use the `Error` type from `windmill_common::error`. Return `Result` or `JsonResult` for fallible functions: +Use `Error` from `windmill_common::error`. Return `Result` or `JsonResult`: ```rust use windmill_common::error::{Error, Result}; -// Use ? operator for propagation pub async fn get_job(db: &DB, id: Uuid) -> Result { - let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id) + sqlx::query_as!(Job, "SELECT id, workspace_id FROM v2_job WHERE id = $1", id) .fetch_optional(db) .await? .ok_or_else(|| Error::NotFound("job not found".to_string()))?; - Ok(job) } ``` -Prefer `if let` for optional handling. Use `let...else` when early return makes code clearer: +Never panic in library code. Reserve `.unwrap()` for compile-time guarantees. + +## SQLx Patterns + +**Never use `SELECT *`** — always list columns explicitly. Critical for backwards compatibility when workers lag behind API version: ```rust -let Some(config) = get_config() else { - return Err(Error::MissingConfig); -}; +// Correct +sqlx::query_as!(Job, "SELECT id, workspace_id, path FROM v2_job WHERE id = $1", id) + +// Wrong — breaks when columns are added +sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", id) ``` -Never panic in library code. Reserve `.unwrap()` for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity. - -## Early Returns - -Return early to avoid deep nesting. Handle error cases and edge conditions first: +Use batch operations to avoid N+1: ```rust -// Preferred - early returns -fn process_job(job: Option) -> Result { - let Some(job) = job else { - return Ok(Output::default()); - }; - - if !job.is_valid() { - return Err(Error::InvalidJob); - } - - if job.is_cached() { - return Ok(job.cached_result()); - } - - // Main logic at the end, not nested - execute_job(job) -} - -// Avoid - deep nesting -fn process_job(job: Option) -> Result { - if let Some(job) = job { - if job.is_valid() { - if !job.is_cached() { - execute_job(job) - } else { - Ok(job.cached_result()) - } - } else { - Err(Error::InvalidJob) - } - } else { - Ok(Output::default()) - } -} +// Preferred — single query with IN clause +sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await? ``` -## Variable Shadowing - -Shadow variables instead of creating new names with prefixes: - -```rust -// Preferred -let data = fetch_raw_data(); -let data = parse(data); -let data = validate(data)?; - -// Avoid -let raw_data = fetch_raw_data(); -let parsed_data = parse(raw_data); -let validated_data = validate(parsed_data)?; -``` - -## Minimal Comments - -- No inline comments explaining obvious code -- No TODO/FIXME comments in committed code -- Doc comments (`///`) only on public items -- Let code be self-documenting through clear naming - -## Type Safety - -Use enums over boolean flags for clarity: - -```rust -// Preferred -enum JobStatus { - Pending, - Running, - Completed, -} - -// Avoid -struct Job { - is_running: bool, - is_completed: bool, -} -``` - -## Pattern Matching - -Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields: - -```rust -// Explicit matching preferred -match status { - JobStatus::Pending => handle_pending(), - JobStatus::Running => handle_running(), - JobStatus::Completed => handle_completed(), -} - -// Wildcards OK for fallback -match result { - Ok(value) => process(value), - Err(_) => return default_value(), -} - -// Wildcards OK for ignoring fields in destructuring -let Point { x, y, .. } = point; -``` - -## Destructuring in Function Signatures - -Destructure structs directly in function parameters: - -```rust -// Preferred -async fn process_job( - Extension(db): Extension, - Path((workspace, job_id)): Path<(String, Uuid)>, - Query(pagination): Query, -) -> Result> { - // ... -} - -// Avoid -async fn process_job( - db_ext: Extension, - path: Path<(String, Uuid)>, - query: Query, -) -> Result> { - let Extension(db) = db_ext; - let Path((workspace, job_id)) = path; - // ... -} -``` - -## Trait Implementations - -Use standard trait implementations to simplify conversions and reduce boilerplate: - -```rust -// Implement From/Into for type conversions -impl From for ApiJob { - fn from(db: DbJob) -> Self { - ApiJob { - id: db.id, - status: db.status.into(), - } - } -} - -// Use TryFrom for fallible conversions -impl TryFrom for JobKind { - type Error = Error; - fn try_from(s: String) -> Result { ... } -} -``` - -Apply `derive` macros to reduce boilerplate: - -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Job { ... } -``` - -## Module Structure - -- Use `pub(crate)` instead of `pub` when possible; expose only what needs exposing -- Keep APIs small and expressive; avoid leaking internal types -- Organize code into modules reflecting ownership and domain boundaries - -```rust -// Prefer restricted visibility -pub(crate) fn internal_helper() { ... } - -// Only pub for external API -pub fn create_job(...) -> Result { ... } -``` - -## Code Navigation - -Always use rust-analyzer LSP for: -- Go to definition -- Find references -- Type information -- Import resolution - -Do not guess at module paths or type definitions. +Use transactions for multi-step operations. Parameterize all queries. ## JSON Handling -Prefer `Box` over `serde_json::Value` when: -- Storing JSON in the database (JSONB columns) -- Passing JSON through without modification -- The JSON structure doesn't need inspection +Prefer `Box` over `serde_json::Value` when storing/passing JSON without inspection: ```rust -// Preferred - avoids parsing/serialization overhead pub struct Job { - pub id: Uuid, pub args: Option>, } - -// Only use Value when you need to inspect/modify JSON -let value: serde_json::Value = serde_json::from_str(&json)?; -if let Some(field) = value.get("field") { - // modify or inspect -} ``` -## Serde Optimizations +Only use `serde_json::Value` when you need to inspect or modify the JSON. -Use serde attributes to optimize serialization: +## Serde Optimizations ```rust #[derive(Serialize, Deserialize)] pub struct Job { - #[serde(rename = "jobId")] - pub id: Uuid, - - #[serde(default)] - pub priority: i32, - #[serde(skip_serializing_if = "Option::is_none")] pub parent_job: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] pub tags: Vec, + #[serde(default)] + pub priority: i32, } ``` -Prefer borrowing for zero-copy deserialization when lifetimes allow: +## Async & Concurrency + +Never block the async runtime. Use `spawn_blocking` for CPU-intensive work: ```rust -#[derive(Deserialize)] -pub struct JobInput<'a> { - #[serde(borrow)] - pub workspace_id: Cow<'a, str>, - - #[serde(borrow)] - pub script_path: &'a str, -} +let result = tokio::task::spawn_blocking(move || expensive_computation(&data)).await?; ``` -## SQLx Patterns +**Mutex selection**: Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) for data protection. Only use `tokio::sync::Mutex` when holding locks across `.await` points. -**Never use `SELECT *`** - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version: +Use `tokio::sync::mpsc` (bounded) for channels. Avoid `std::thread::sleep` in async contexts. + +## Module Structure & Visibility + +- Use `pub(crate)` instead of `pub` when possible +- Place new code in the appropriate crate based on functionality +- API endpoints go in `windmill-api/src/` organized by domain +- Shared functionality goes in `windmill-common/src/` + +## Code Navigation + +Always use rust-analyzer LSP for go-to-definition, find-references, and type info. Do not guess at module paths. + +## Axum Handlers + +Destructure extractors directly in function signatures: ```rust -// Preferred - explicit columns -sqlx::query_as!( - Job, - "SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1", - job_id -) - -// Avoid - breaks when columns are added -sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id) +async fn process_job( + Extension(db): Extension, + Path((workspace, job_id)): Path<(String, Uuid)>, + Query(pagination): Query, +) -> Result> { ... } ``` - -Use batch operations to minimize round trips: - -```rust -// Preferred - single query with multiple values -sqlx::query!( - "INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)", - id1, log1, id2, log2 -) - -// Avoid N+1 queries -for id in ids { - sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?; -} - -// Preferred - single query with IN clause -sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await? -``` - -Use transactions for multi-step operations and parameterize all queries. - -## Async & Tokio Patterns - -Never block the async runtime. Use `spawn_blocking` for CPU-intensive or blocking I/O: - -```rust -// Preferred - offload blocking work -let result = tokio::task::spawn_blocking(move || { - expensive_computation(&data) -}).await?; - -// Avoid - blocks the runtime -let result = expensive_computation(&data); // Don't do this in async -``` - -Use tokio primitives for sleep and channels: - -```rust -use tokio::sync::mpsc; -use tokio::time::sleep; - -// Avoid in async contexts -use std::thread::sleep; // Blocks the runtime -``` - -Use bounded channels for backpressure: - -```rust -// Preferred - bounded channel prevents overwhelming -let (tx, rx) = tokio::sync::mpsc::channel(100); - -// Be careful with unbounded -let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); -``` - -## Mutex Selection in Async Code - -**Prefer `std::sync::Mutex` (or `parking_lot::Mutex`) over `tokio::sync::Mutex`** for protecting data in async code. The async mutex is more expensive and only needed when holding locks across `.await` points. - -```rust -// Preferred for data protection - std mutex is faster -use std::sync::Mutex; - -struct Cache { - data: Mutex>, -} - -impl Cache { - fn get(&self, key: &str) -> Option { - self.data.lock().unwrap().get(key).cloned() - } - - fn insert(&self, key: String, value: Value) { - self.data.lock().unwrap().insert(key, value); - } -} -``` - -**Use `tokio::sync::Mutex` only when you must hold the lock across `.await` points**, typically for IO resources like database connections: - -```rust -use tokio::sync::Mutex; -use std::sync::Arc; - -// Async mutex for IO resources held across await points -let conn = Arc::new(Mutex::new(db_connection)); - -async fn execute_query(conn: Arc>, query: &str) { - let mut lock = conn.lock().await; - lock.execute(query).await; // Lock held across .await -} -``` - -**Common pattern**: Wrap `Arc>` in a struct with non-async methods that lock internally, keeping lock scope minimal: - -```rust -struct SharedState { - inner: std::sync::Mutex, -} - -impl SharedState { - fn update(&self, value: i32) { - self.inner.lock().unwrap().value = value; - } - - fn get(&self) -> i32 { - self.inner.lock().unwrap().value - } -} -``` - -**Alternative for IO resources**: Spawn a dedicated task to manage the resource and communicate via message passing: - -```rust -let (tx, mut rx) = tokio::sync::mpsc::channel(32); - -tokio::spawn(async move { - while let Some(cmd) = rx.recv().await { - handle_io_command(&mut resource, cmd).await; - } -}); -``` - -## Build & Tooling - -Build speed tips: -- Use `cargo check` during rapid iteration over `cargo build` -- Minimize unnecessary dependencies and feature flags \ No newline at end of file diff --git a/.claude/skills/svelte-frontend/SKILL.md b/.claude/skills/svelte-frontend/SKILL.md index f51d36672a..57cac70302 100644 --- a/.claude/skills/svelte-frontend/SKILL.md +++ b/.claude/skills/svelte-frontend/SKILL.md @@ -3,316 +3,78 @@ name: svelte-frontend description: Svelte coding guidelines for the Windmill frontend. MUST use when writing or modifying code in the frontend directory. --- -# Svelte 5 Best Practices +# Windmill Svelte Patterns -This guide outlines best practices for developing with Svelte 5, incorporating the new Runes API and other modern Svelte features. These rules MUST NOT be applied on svelte 4 files unless explicitly asked to do so. +Apply these Windmill-specific patterns when writing Svelte code in `frontend/`. For general Svelte 5 syntax (runes, snippets, event handling), use the Svelte MCP server. -## Reactivity with Runes +## Windmill UI Components (MUST use) -Svelte 5 introduces Runes for more explicit and flexible reactivity. +Always use Windmill's design-system components. Never use raw HTML elements. -1. **Embrace Runes for State Management**: - * Use `$state` for reactive local component state. - ```svelte - - - - ``` - * Use `$derived` for computed values based on other reactive state. - ```svelte - - -

{count} * 2 = {doubled}

- ``` - * Use `$effect` for side effects that need to run when reactive values change (e.g., logging, manual DOM manipulation, data fetching). Remember `$effect` does not run on the server. - ```svelte - - ``` - -2. **Props with `$props`**: - * Declare component props using `$props()`. This offers better clarity and flexibility compared to `export let`. - ```svelte - - -

Name: {name}

-

Age: {age}

- ``` - * For bindable props, use `$bindable`. - ```svelte - - - - ``` - -## Event Handling - -* **Use direct event attributes**: Svelte 5 moves away from `on:` directives for DOM events. - * **Do**: `` - * **Don't**: `` -* **For component events, prefer callback props**: Instead of `createEventDispatcher`, pass functions as props. - ```svelte - - - -

Message from child: {message}

- - - - - ``` - -## Snippets for Content Projection - -* **Use `{#snippet ...}` and `{@render ...}` instead of slots**: Snippets are more powerful and flexible. - ```svelte - - - - - {#snippet title()} - My Awesome Title - {/snippet} - {#snippet content()} -

Some interesting content here.

- {/snippet} -
- - - - -
-
{@render title()}
-
{@render content()}
-
- ``` -* Default content is passed via the `children` prop (which is a snippet). - ```svelte - - -
- {@render children?.()} -
- ``` - -## Component Design - -1. **Create Small, Reusable Components**: Break down complex UIs into smaller, focused components. Each component should have a single responsibility. This also aids performance by limiting the scope of reactivity updates. -2. **Descriptive Naming**: Use clear and descriptive names for variables, functions, and components. -3. **Minimize Logic in Components**: Move complex business logic to utility functions or services. Keep components focused on presentation and interaction. - -## State Management (Stores) - -1. **Segment Stores**: Avoid a single global store. Create multiple stores, each responsible for a specific piece of global state (e.g., `userStore.js`, `themeStore.js`). This can help limit reactivity updates to only the parts of the UI that depend on specific state segments. -2. **Use Custom Stores for Complex Logic**: For stores with related methods, create custom stores. - ```javascript - // counterStore.js - import { writable } from 'svelte/store'; - - function createCounter() { - const { subscribe, set, update } = writable(0); - - return { - subscribe, - increment: () => update(n => n + 1), - decrement: () => update(n => n - 1), - reset: () => set(0) - }; - } - export const counter = createCounter(); - ``` -3. **Use Context API for Localized State**: For state shared within a component subtree, consider Svelte's context API (`setContext`, `getContext`) instead of global stores when the state doesn't need to be truly global. - -## Performance Optimizations (Svelte 5) - -When generating Svelte 5 code, prioritize frontend performance by applying the following principles: - -### General Svelte 5 Principles - -- **Leverage the Compiler:** Trust Svelte's compiler to generate optimized JavaScript. Avoid manual DOM manipulation (`document.querySelector`, etc.) unless absolutely necessary for integrating third-party libraries that lack Svelte adapters. -- **Keep Components Small and Focused:** Reinforcing from Component Design, smaller components lead to less complex reactivity graphs and more targeted, efficient updates. - -### Reactivity & State Management - -- **Optimize Computations with `$derived`:** Always use `$derived` for computed values that depend on other state. This ensures the computation only runs when its specific dependencies change, avoiding unnecessary work compared to recomputing derived values in `$effect` or less efficient methods. -- **Minimize `$effect` Usage:** Use `$effect` sparingly and only for true side effects that interact with the outside world or non-Svelte state. Avoid putting complex logic or state updates *within* an `$effect` unless those updates are explicitly intended as a reaction to external changes or non-Svelte state. Excessive or complex effects can impact rendering performance. -- **Structure State for Fine-Grained Updates:** Design your `$state` objects or variables such that updates affect only the necessary parts of the UI. Avoid putting too much unrelated state into a single large object that gets frequently updated, as this can potentially trigger broader updates than necessary. Consider normalizing complex, nested state. - -### List Rendering (`{#each}`) - -- **Mandate `key` Attribute:** Always use a `key` attribute (`{#each items as item (item.id)}`) that refers to a unique, stable identifier for each item in a list. This is critical for allowing Svelte to efficiently update, reorder, add, or remove list items without destroying and re-creating unnecessary DOM elements and component instances. - -### Component Loading & Bundling - -- **Implement Lazy Loading/Code Splitting:** For routes, components, or modules that are not immediately needed on page load, use dynamic imports (`import(...)`) to split the code bundle. SvelteKit handles this automatically for routes, but it can be applied manually to components using helper patterns if needed. -- **Be Mindful of Third-Party Libraries:** When incorporating external libraries, import only the necessary functions or components to minimize the final bundle size. Prefer libraries designed to be tree-shakeable. - -### Rendering & DOM - -- **Use CSS for Animations/Transitions:** Prefer CSS animations or transitions where possible for performance. Svelte's built-in `transition:` directive is also highly optimized and should be used for complex state-driven transitions, but simple cases can often use plain CSS. -- **Optimize Image Loading:** Implement best practices for images: use optimized formats (WebP, AVIF), lazy loading (`loading="lazy"`), and responsive images (``, `srcset`) to avoid loading unnecessarily large images. - -### Server-Side Rendering (SSR) & Hydration - -- **Ensure SSR Compatibility:** Write components that can be rendered on the server for faster initial page loads. Avoid relying on browser-specific APIs (like `window` or `document`) in the main ` - -
- Hello -
- ``` -5. **Stay Updated**: Keep Svelte and its related packages up to date to benefit from the latest features, performance improvements, and security fixes. - -## Windmill UI Component Rules (MUST follow) - -Always use Windmill's own design-system components instead of raw HTML elements. Using raw HTML elements produces inconsistent styling and breaks the design language. - -### Icons — use `lucide-svelte` - -**Never** write inline SVGs. Import icons from `lucide-svelte`. - -```svelte - - - -``` - -### Buttons — use ` - - -