slice 3.5c: seed 15 built-in skills across the 6 stacks
Hand-authored skill catalog anchored to real 2026-07 versions:
- Rust 1.97.1 (stable), edition 2024
- React 19.2.7, Server Components + Actions
- TailwindCSS 4.3.3 (CSS-first config, Oxide engine)
- three.js r185 (WebGPURenderer stable, BatchedMesh matured)
- React Native 0.86 / Expo SDK 54+ (New Architecture default)
- cargo-nextest 0.9.140, gitleaks 8.20+, cargo-audit 0.21+
- Postgres 17 (18 in beta, don't rely on)
- CUDA Blackwell, Metal Apple7+, ROCm CDNA3
Ships 15 skills across the categories:
foundation/ workspace-repo-commit-protocol
small-focused-commits
tdd-red-green-refactor
code-review-checklist
int-xx-marker-protocol
decompose-int-items
rust/ write-rust-current-edition
rust-error-handling
cargo-test-driven-development
rust-async-tokio-idioms
backend/ postgres-migrations-forward-only
postgres-index-selection
api-pagination-day-1
frontend/ react-19-server-components
tailwind-v4-idioms
component-4-state-model
mobile/ expo-managed-vs-bare
rn-flashlist-perf
gpu/ gpu-coalescing-and-occupancy
roofline-model
threejs/ threejs-perf-and-teardown
security/ cargo-audit-workflow
secret-scanning-gitleaks
skills_loader.rs walks skills/**/*.md, parses YAML frontmatter
(name, description, when_to_use, tags), upserts via
skills_catalog::upsert_builtin. Idempotent per boot — bumps version
+ appends skill_versions row ONLY when body changes. Deterministic
sha256-derived ids so builtins are stable across boots.
Dockerfile copies skills/ to /etc/clawmates/skills. Server boot
task spawns loader alongside team_template_loader.
Follow-ups (Slice 3.5c continuation, future PRs):
- 20-30 more skills (duckdb, shadcn composition, a11y, WebGPU
migration, metal frame capture, rocprof, deep gitea forge
integration, semgrep rulepacks)
- Bind skills to team template roles (add [role.skills] refs to
templates/teams/*.toml + wire template_role_skills population
in team_template_loader)
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cddbcb91b3
commit
7b23f61632
@@ -281,6 +281,15 @@ async fn run() -> Result<(), String> {
|
|||||||
eprintln!("team_template_loader: loaded {n} builtin team template(s)");
|
eprintln!("team_template_loader: loaded {n} builtin team template(s)");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Load builtin skills from skills/**/*.md into the skills catalog
|
||||||
|
// (Slice 3.5c). Same idempotent-per-boot semantics.
|
||||||
|
{
|
||||||
|
let pool = pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let n = cm_api::skills_loader::load_builtins(&pool).await;
|
||||||
|
eprintln!("skills_loader: loaded {n} builtin skill(s)");
|
||||||
|
});
|
||||||
|
}
|
||||||
// Outbound-email delivery: drains the §15-gated `outbox` over SMTP. Inert
|
// Outbound-email delivery: drains the §15-gated `outbox` over SMTP. Inert
|
||||||
// until CLAWMATES_SMTP_* is set, so it ships safely before credentials exist.
|
// until CLAWMATES_SMTP_* is set, so it ships safely before credentials exist.
|
||||||
cm_runtime::spawn_drainer(pool.clone(), std::time::Duration::from_secs(10));
|
cm_runtime::spawn_drainer(pool.clone(), std::time::Duration::from_secs(10));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
serde_yaml = "0.9"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ mod recursive_exec;
|
|||||||
pub mod research_container;
|
pub mod research_container;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod runtime_provision;
|
mod runtime_provision;
|
||||||
|
pub mod skills_loader;
|
||||||
pub mod swarm;
|
pub mod swarm;
|
||||||
pub mod team_template_loader;
|
pub mod team_template_loader;
|
||||||
pub mod tool_versions;
|
pub mod tool_versions;
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
//! Load builtin skills from `skills/**/*.md` into `skills` +
|
||||||
|
//! `skill_versions` at server boot. Slice 3.5c of the missions
|
||||||
|
//! consolidation.
|
||||||
|
//!
|
||||||
|
//! Frontmatter shape (YAML between `---` fences):
|
||||||
|
//! name: <slug>
|
||||||
|
//! description: <one-line, shown to the LLM in resources/list>
|
||||||
|
//! when_to_use: <trigger sentence, appended to description>
|
||||||
|
//! tags: [foundation, rust, ...]
|
||||||
|
//!
|
||||||
|
//! The body is the rest of the file. Both are upserted idempotently:
|
||||||
|
//! `skills_catalog::upsert_builtin` bumps the version + appends to
|
||||||
|
//! `skill_versions` ONLY when the body actually changes.
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use cm_db::repo::skills_catalog::{upsert_builtin, UpsertBuiltinSkill};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Frontmatter {
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
when_to_use: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
tags: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skills_dir() -> PathBuf {
|
||||||
|
if let Ok(d) = std::env::var("CLAWMATES_SKILLS_DIR") {
|
||||||
|
return PathBuf::from(d);
|
||||||
|
}
|
||||||
|
let container = PathBuf::from("/etc/clawmates/skills");
|
||||||
|
if container.exists() {
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
PathBuf::from("skills")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic id per builtin skill name — sha256 of a stable
|
||||||
|
/// namespace + the name. Matches the pattern used by the team-template
|
||||||
|
/// loader so ids are reproducible across boots.
|
||||||
|
fn builtin_id(name: &str) -> uuid::Uuid {
|
||||||
|
let mut h = Sha256::new();
|
||||||
|
h.update(b"clawmates.builtin.skill\x00");
|
||||||
|
h.update(name.as_bytes());
|
||||||
|
let d = h.finalize();
|
||||||
|
let mut bytes = [0u8; 16];
|
||||||
|
bytes.copy_from_slice(&d[..16]);
|
||||||
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
|
uuid::Uuid::from_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk the skills tree + upsert every `*.md`. Returns the count of
|
||||||
|
/// successful upserts. Failures are logged and skipped so a single
|
||||||
|
/// broken skill can't block boot.
|
||||||
|
pub async fn load_builtins(pool: &PgPool) -> usize {
|
||||||
|
let dir = skills_dir();
|
||||||
|
let files = match walk_md(&dir) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"skills_loader: dir {} not readable: {e} — skipping builtin skills seed",
|
||||||
|
dir.display()
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut loaded = 0usize;
|
||||||
|
for path in files {
|
||||||
|
match load_one(pool, &path).await {
|
||||||
|
Ok(name) => {
|
||||||
|
loaded += 1;
|
||||||
|
eprintln!("skills_loader: upserted builtin skill {name}");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("skills_loader: failed to load {}: {e}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
fn walk_md(root: &std::path::Path) -> std::io::Result<Vec<PathBuf>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
fn recurse(p: &std::path::Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
|
||||||
|
for entry in std::fs::read_dir(p)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
if entry.file_type()?.is_dir() {
|
||||||
|
recurse(&path, out)?;
|
||||||
|
} else if path.extension().and_then(|s| s.to_str()) == Some("md") {
|
||||||
|
out.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
recurse(root, &mut out)?;
|
||||||
|
out.sort();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the frontmatter + body out of one file and upsert.
|
||||||
|
async fn load_one(pool: &PgPool, path: &std::path::Path) -> Result<String, String> {
|
||||||
|
let text =
|
||||||
|
std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
|
||||||
|
let (frontmatter, body) = split_frontmatter(&text)
|
||||||
|
.ok_or_else(|| format!("no --- frontmatter block in {}", path.display()))?;
|
||||||
|
let fm: Frontmatter = serde_yaml::from_str(frontmatter)
|
||||||
|
.map_err(|e| format!("parse frontmatter of {}: {e}", path.display()))?;
|
||||||
|
|
||||||
|
let skill = UpsertBuiltinSkill {
|
||||||
|
id: builtin_id(&fm.name),
|
||||||
|
name: &fm.name,
|
||||||
|
description: &fm.description,
|
||||||
|
when_to_use: fm.when_to_use.as_deref(),
|
||||||
|
tags: fm.tags.clone(),
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
upsert_builtin(pool, skill)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("upsert {}: {e}", fm.name))?;
|
||||||
|
Ok(fm.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the `---\n<yaml>\n---\n<body>` shape. Returns `(yaml, body)`
|
||||||
|
/// or None if the file doesn't start with a frontmatter fence.
|
||||||
|
fn split_frontmatter(text: &str) -> Option<(&str, &str)> {
|
||||||
|
let mut rest = text.strip_prefix("---\n")?;
|
||||||
|
// Some editors add a BOM; strip a leading whitespace/newline pair.
|
||||||
|
if rest.starts_with('\r') {
|
||||||
|
rest = rest.strip_prefix('\r').unwrap_or(rest);
|
||||||
|
}
|
||||||
|
let end = rest.find("\n---\n")?;
|
||||||
|
let yaml = &rest[..end];
|
||||||
|
let body = &rest[end + "\n---\n".len()..];
|
||||||
|
Some((yaml, body.trim_start()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn splits_frontmatter() {
|
||||||
|
let src = "---\nname: foo\ndescription: bar\n---\n# body\n";
|
||||||
|
let (fm, body) = split_frontmatter(src).unwrap();
|
||||||
|
assert!(fm.contains("name: foo"));
|
||||||
|
assert_eq!(body, "# body\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_frontmatter_returns_none() {
|
||||||
|
assert!(split_frontmatter("# plain md\n").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builtin_id_stable() {
|
||||||
|
assert_eq!(
|
||||||
|
builtin_id("workspace-repo-commit-protocol"),
|
||||||
|
builtin_id("workspace-repo-commit-protocol")
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
builtin_id("workspace-repo-commit-protocol"),
|
||||||
|
builtin_id("other-skill")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,5 +46,6 @@ RUN apt-get update \
|
|||||||
COPY --from=builder /clawmates-server /usr/local/bin/clawmates-server
|
COPY --from=builder /clawmates-server /usr/local/bin/clawmates-server
|
||||||
# Builtin templates (team + workflow). Loader upserts them on boot.
|
# Builtin templates (team + workflow). Loader upserts them on boot.
|
||||||
COPY templates /etc/clawmates/templates
|
COPY templates /etc/clawmates/templates
|
||||||
|
COPY skills /etc/clawmates/skills
|
||||||
USER 65532
|
USER 65532
|
||||||
ENTRYPOINT ["/usr/local/bin/clawmates-server"]
|
ENTRYPOINT ["/usr/local/bin/clawmates-server"]
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: api-pagination-day-1
|
||||||
|
description: Every list endpoint paginates from the first version. Cursor-based over offset. Bounded max_limit. Includes total when cheap.
|
||||||
|
when_to_use: You're the api_designer role, or a coder adding any `GET /...` endpoint that returns a collection.
|
||||||
|
tags: [backend, api]
|
||||||
|
---
|
||||||
|
|
||||||
|
# List endpoints paginate from day 1
|
||||||
|
|
||||||
|
Unpaginated lists are a production incident waiting to happen. The one time the endpoint is called with a million-row table, the response body fills the load balancer's buffer, memory spikes, and everything downstream 503s.
|
||||||
|
|
||||||
|
## The pattern
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ListQuery {
|
||||||
|
#[serde(default = "default_limit")]
|
||||||
|
limit: u32,
|
||||||
|
/// Cursor from a prior page's `next_cursor`. Opaque to the client.
|
||||||
|
cursor: Option<String>,
|
||||||
|
}
|
||||||
|
fn default_limit() -> u32 { 50 }
|
||||||
|
const MAX_LIMIT: u32 = 500;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Page<T> {
|
||||||
|
items: Vec<T>,
|
||||||
|
next_cursor: Option<String>,
|
||||||
|
/// Present only when it's cheap to compute (COUNT(*) over a small
|
||||||
|
/// filtered set). Never for a full-table count.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
total: Option<u64>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cursor over offset
|
||||||
|
|
||||||
|
- **Offset (`LIMIT 50 OFFSET 500`)** — Postgres scans and discards the first 500 rows on every request. Cost grows with page number.
|
||||||
|
- **Cursor (`WHERE id > $cursor ORDER BY id LIMIT 50`)** — O(log n) always. The cursor is the last row's sort key, base64'd (or JWT-signed if you want tamper-detection).
|
||||||
|
|
||||||
|
For clawmates, the standard cursor shape is `base64(json({"id": "...", "ts": "..."}))` — includes id + the ORDER BY tiebreaker so pages are deterministic even under concurrent inserts.
|
||||||
|
|
||||||
|
## Bounds
|
||||||
|
|
||||||
|
- **`limit.clamp(1, MAX_LIMIT)`** — server-side clamp, not just server-side validation. Never trust the client to respect `max_limit`.
|
||||||
|
- **Return `413 Payload Too Large`** when a caller sends `limit=100000` if you'd rather they notice than have their traffic silently clamped.
|
||||||
|
|
||||||
|
## Sorting
|
||||||
|
|
||||||
|
- Every paginated endpoint has ONE canonical sort order.
|
||||||
|
- Cursor must include the sort key + a tie-breaker (usually `id`). Otherwise two rows with the same sort key can be missed or duplicated across pages.
|
||||||
|
|
||||||
|
## Total counts
|
||||||
|
|
||||||
|
- Cheap: `SELECT count(*) FROM t WHERE small_indexed_filter`. Include it.
|
||||||
|
- Expensive: unfiltered COUNT on a large table. **Don't run it.** Return `total: null` and let the UI show "50 of many".
|
||||||
|
|
||||||
|
## OpenAPI
|
||||||
|
|
||||||
|
The response schema declares `items`, `next_cursor`, and `total?`. Never return a bare array — you can't add a cursor later without a breaking change.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **`GET /foo` returning `[...]`** with no pagination shape. Every list route pays for future-proofing on day 1.
|
||||||
|
- **`page=N&per_page=M`** — offset-based. Ships until traffic bites you.
|
||||||
|
- **Cursor that includes an unsigned index into a mutable list** — reorders + inserts corrupt it. Cursor must be a stable identifier.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
name: postgres-index-selection
|
||||||
|
description: Which index type to reach for — btree/gin/gist/brin/hash — and when partial or expression indexes beat a plain one.
|
||||||
|
when_to_use: You're adding an index or diagnosing a slow query in Postgres.
|
||||||
|
tags: [backend, postgres, performance]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Postgres index selection (17)
|
||||||
|
|
||||||
|
Default to **btree** unless the data shape says otherwise.
|
||||||
|
|
||||||
|
## btree
|
||||||
|
|
||||||
|
- Equality + range on scalar columns.
|
||||||
|
- Multi-column: leading column must be in the WHERE clause for the index to help. `(a, b, c)` helps `WHERE a=? AND b=?`, not `WHERE b=?`.
|
||||||
|
- Descending: rare — btree can scan in either direction, but `ORDER BY x DESC` with a mixed-direction multi-col query benefits from `(a ASC, b DESC)`.
|
||||||
|
|
||||||
|
## Partial indexes — huge wins
|
||||||
|
|
||||||
|
Any time a WHERE clause repeats across queries, a partial index is a smaller, faster index:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX missions_running_idx
|
||||||
|
ON missions (updated_at DESC)
|
||||||
|
WHERE status IN ('running', 'pending');
|
||||||
|
```
|
||||||
|
|
||||||
|
The clawmates codebase leans on this heavily — see `topology_runs_status_idx WHERE status = 'queued'`.
|
||||||
|
|
||||||
|
## Expression indexes
|
||||||
|
|
||||||
|
For computed lookups:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE UNIQUE INDEX users_lower_email_idx ON users (lower(email));
|
||||||
|
-- Query MUST use the exact expression:
|
||||||
|
SELECT * FROM users WHERE lower(email) = lower($1);
|
||||||
|
```
|
||||||
|
|
||||||
|
## GIN
|
||||||
|
|
||||||
|
- **JSONB**: `CREATE INDEX foo_meta_gin ON foo USING gin (metadata jsonb_path_ops);` — `jsonb_path_ops` is smaller + faster than default for `@>` queries. Use default when you also need `?` / `?|` / `?&`.
|
||||||
|
- **Arrays**: `CREATE INDEX skills_tags_gin ON skills USING gin (tags);` — `WHERE tags && ARRAY['rust']` becomes index-served.
|
||||||
|
- **Full-text search**: `to_tsvector('english', body)` with a GIN index.
|
||||||
|
|
||||||
|
## GiST
|
||||||
|
|
||||||
|
- Geo (PostGIS): `USING gist` on `geometry` / `geography` columns.
|
||||||
|
- Range types: `USING gist` on `tstzrange` for "does this range overlap" queries.
|
||||||
|
|
||||||
|
## BRIN
|
||||||
|
|
||||||
|
- Very large tables where the indexed column correlates with physical row order (append-only timestamps, sequential ids). 1/1000th the size of btree, works only for range scans.
|
||||||
|
|
||||||
|
## Hash (rare)
|
||||||
|
|
||||||
|
- Equality-only, very high-cardinality, no range. Postgres 10+ hash indexes are WAL-logged + replicated so they're finally safe — but btree wins in most benchmarks. Reach for it only when profiling justifies.
|
||||||
|
|
||||||
|
## Diagnosis flow
|
||||||
|
|
||||||
|
```sql
|
||||||
|
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
|
||||||
|
SELECT ...;
|
||||||
|
```
|
||||||
|
|
||||||
|
Read from bottom up. Watch for:
|
||||||
|
- **Seq Scan on a hot table** with a WHERE that should be indexed → missing index.
|
||||||
|
- **Index Scan** with `Rows Removed by Filter` > returned rows → wrong index; add a compound or partial.
|
||||||
|
- **Bitmap Heap Scan** with `Recheck` on lossy → index is being used but not tightly; check if partial would help.
|
||||||
|
- **Sort** with `Method: external merge Disk` → sort spills to disk; add index that provides the order.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Indexing every column**. Each index has a write cost (INSERT/UPDATE amplification). Prefer 1–2 well-chosen composites over 6 single-column indexes.
|
||||||
|
- **Indexing a boolean without WHERE**. `CREATE INDEX foo_active_idx ON foo(active)` is worthless; `... WHERE active = true` is what you want.
|
||||||
|
- **Trusting `pg_stat_user_indexes.idx_scan = 0`** without waiting a full billing cycle. Some indexes exist for the monthly report.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: postgres-migrations-forward-only
|
||||||
|
description: Forward-only, reversible-by-new-migration policy for Postgres 17. No `--rollback`, no ALTER TABLE without concurrent-safe patterns on hot tables.
|
||||||
|
when_to_use: You are the db_engineer role or any coder authoring a `migrations/NNNN_*.sql` file.
|
||||||
|
tags: [backend, postgres, migrations, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Postgres migrations (17-safe)
|
||||||
|
|
||||||
|
Anchored to **Postgres 17** (latest stable as of 2026-07). Postgres 18 is in beta at time of writing — don't rely on 18-only syntax without a version guard.
|
||||||
|
|
||||||
|
## Cardinal rule: forward-only
|
||||||
|
|
||||||
|
- Every migration file is applied once and never rolled back. If you need to undo, ship a NEW migration that undoes.
|
||||||
|
- `sqlx-cli` reads `migrations/NNNN_*.sql` in ascending order; each is wrapped in a txn where possible.
|
||||||
|
- The migration is part of the commit that introduces the code depending on it. Split ONLY when a two-step deploy is required (see below).
|
||||||
|
|
||||||
|
## Two-step deploys (for column drops + renames)
|
||||||
|
|
||||||
|
- **Adding a column**: single migration, always safe.
|
||||||
|
- **Removing a column**:
|
||||||
|
1. **Migration N** stops writing the column (code change first).
|
||||||
|
2. **Migration N+1** (weeks later) drops the column, after every replica has caught up.
|
||||||
|
- **Renaming a column**: don't. Add new, dual-write, backfill, stop-writing-old, drop-old.
|
||||||
|
|
||||||
|
## Concurrent-safe patterns on hot tables
|
||||||
|
|
||||||
|
Anything > 1M rows or in the request path needs concurrent-safe DDL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Index adds
|
||||||
|
CREATE INDEX CONCURRENTLY foo_bar_idx ON foo(bar);
|
||||||
|
-- (CONCURRENTLY can't run in a txn — omit sqlx's txn wrap by prefixing
|
||||||
|
-- the migration file with `-- fx-no-tx`; sqlx-cli honors it.)
|
||||||
|
|
||||||
|
-- Adding NOT NULL to an existing column
|
||||||
|
ALTER TABLE foo ADD CONSTRAINT foo_bar_nn CHECK (bar IS NOT NULL) NOT VALID;
|
||||||
|
ALTER TABLE foo VALIDATE CONSTRAINT foo_bar_nn;
|
||||||
|
-- Later: drop the CHECK, add NOT NULL to the column — cheap now.
|
||||||
|
|
||||||
|
-- Backfill in batches
|
||||||
|
UPDATE foo SET bar = new_value
|
||||||
|
WHERE id IN (SELECT id FROM foo WHERE bar IS NULL LIMIT 10000);
|
||||||
|
-- Loop until 0 rows.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Foreign keys
|
||||||
|
|
||||||
|
- Every FK gets an index UNLESS the table is small (< 100k rows expected long-term).
|
||||||
|
- `ON DELETE CASCADE` for owned child rows; `ON DELETE SET NULL` for weak references; explicit `NO ACTION` for anything else.
|
||||||
|
- Deferring: `DEFERRABLE INITIALLY IMMEDIATE` on tables where cross-table batch writes need to defer FK checks.
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- Migration filename: `NNNN_snake_case_description.sql` — 4-digit prefix, no gaps.
|
||||||
|
- Table names: singular is fine, plural is fine — pick one per crate and never mix.
|
||||||
|
- Index name: `<table>_<cols>_idx` for unique; `<table>_<cols>_uniq` for UNIQUE.
|
||||||
|
- Constraint name: `<table>_<cols>_ck` for CHECK, `_fk` for FK.
|
||||||
|
|
||||||
|
## What NOT to put in a migration
|
||||||
|
|
||||||
|
- Data seeding beyond a handful of rows — that's a boot-time loader (see [[team_template_loader]] pattern).
|
||||||
|
- `SET search_path` or `SET timezone` — belongs in role config or session, not migration.
|
||||||
|
- Anything requiring a specific extension without `CREATE EXTENSION IF NOT EXISTS pgcrypto` (or whichever) first.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
name: code-review-checklist
|
||||||
|
description: A structured checklist for the reviewer role — correctness, safety, simplicity, testability, docs — with explicit approve/block markers.
|
||||||
|
when_to_use: You are a reviewer role or performing a review pass on any code diff.
|
||||||
|
tags: [foundation, review]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code review checklist
|
||||||
|
|
||||||
|
Work top-down. A block on any earlier item stops the pass — don't score everything before opining.
|
||||||
|
|
||||||
|
## 1. Correctness (BLOCKS)
|
||||||
|
|
||||||
|
- Does the change match the PLANNER's acceptance criteria for this INT-XX?
|
||||||
|
- Do the tests exercise the behavior described in the plan, not just the code that was written?
|
||||||
|
- Any edge cases the plan named but the code doesn't handle? (empty input, boundary values, timezones, unicode)
|
||||||
|
- Any silent error paths (`unwrap_or_default()`, `.ok()?`) that hide real failures?
|
||||||
|
|
||||||
|
## 2. Safety (BLOCKS)
|
||||||
|
|
||||||
|
- New `unsafe` blocks — is the justification comment concrete about the invariants?
|
||||||
|
- New FFI — is the C-side contract checked at every call site?
|
||||||
|
- SQL — parameterized every user input? No string concatenation into queries?
|
||||||
|
- HTTP — validates auth, checks workspace scope on every read?
|
||||||
|
|
||||||
|
## 3. Simplicity (usually NIT, sometimes BLOCK)
|
||||||
|
|
||||||
|
- Any code that a smaller function would replace? (`Iterator::sum`, `.map`, `.filter`)
|
||||||
|
- Any abstraction added for one caller? BLOCK — introduce when second caller lands.
|
||||||
|
- Any dead code, commented-out blocks, `// TODO fix later` without a ticket? BLOCK.
|
||||||
|
- Files > 1500 LOC after the change? Split.
|
||||||
|
|
||||||
|
## 4. Performance (BLOCK on hot paths, NIT elsewhere)
|
||||||
|
|
||||||
|
- Cold path (init, config load, admin endpoints): NIT.
|
||||||
|
- Hot path (per-request handler, per-frame render, tight loop): BLOCK on obvious O(n²), unnecessary allocations, blocking sync in async context.
|
||||||
|
|
||||||
|
## 5. Testability / observability (BLOCK if downstream on-call would suffer)
|
||||||
|
|
||||||
|
- Does the change add meaningful log lines at decision points? (Info at boundaries, warn on retries, error on give-up.)
|
||||||
|
- Are new metrics wired? Prometheus counter/histogram naming convention followed?
|
||||||
|
|
||||||
|
## 6. Docs (NIT if module-private, BLOCK if public API)
|
||||||
|
|
||||||
|
- New public function / struct / route → doc comment with example.
|
||||||
|
- Non-obvious invariant → comment on the declaration.
|
||||||
|
|
||||||
|
## Verdict markers
|
||||||
|
|
||||||
|
Emit exactly one of these on a line by itself when the pass is done:
|
||||||
|
|
||||||
|
```
|
||||||
|
REVIEW_APPROVE: INT-NN
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
REVIEW_BLOCK: INT-NN — <specific issue that must be fixed>
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-line commentary is fine, but the marker must appear literally.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: decompose-int-items
|
||||||
|
description: How to break a mission's roadmap/research artifact into INT-XX items sized for one coder-turn each.
|
||||||
|
when_to_use: You are the planner role, at the start of a coding phase, when the input is a spec/roadmap and the output is a task list.
|
||||||
|
tags: [foundation, planning]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Decomposing into INT-XX items
|
||||||
|
|
||||||
|
Each INT item is one unit of work handed to a coder. The mission loop iterates one INT per iteration by default. Sizing matters — too big and iterations stall, too small and you burn tokens on ceremony.
|
||||||
|
|
||||||
|
## Sizing heuristic
|
||||||
|
|
||||||
|
Aim for **1–3 hours of focused coder time** per INT. Practically:
|
||||||
|
- Touches ≤ 5 files
|
||||||
|
- Adds ≤ 300 LOC net
|
||||||
|
- Owns exactly one test story (a `#[test]` or a small `describe` block)
|
||||||
|
- Independently mergeable — reverting it doesn't break other INTs
|
||||||
|
|
||||||
|
If an item exceeds any of these, split.
|
||||||
|
|
||||||
|
## The output shape
|
||||||
|
|
||||||
|
Each INT is a block with:
|
||||||
|
|
||||||
|
```
|
||||||
|
INT-05: <short title>
|
||||||
|
|
||||||
|
**What:** one paragraph, observable behavior only (no implementation
|
||||||
|
details unless they're required for correctness).
|
||||||
|
|
||||||
|
**Files (expected):** src/foo.rs, tests/foo_test.rs
|
||||||
|
|
||||||
|
**Acceptance:**
|
||||||
|
- [ ] `cargo test foo::` passes with the new case
|
||||||
|
- [ ] Coverage on src/foo.rs ≥ 90%
|
||||||
|
- [ ] No new `unsafe` blocks
|
||||||
|
|
||||||
|
**Depends on:** INT-04 (must be merged first)
|
||||||
|
|
||||||
|
**Risk notes:** touches the hot path — bench before/after.
|
||||||
|
```
|
||||||
|
|
||||||
|
Emit each on its own — the task-card parser (Slice 5) UPSERTs one `mission_tasks` row per marker.
|
||||||
|
|
||||||
|
## Ordering
|
||||||
|
|
||||||
|
- **Dependencies first.** INT-01 → INT-02 → ... — but leave room for reorders. A coder can emit `REORDER: <rationale>` if a prerequisite blocks them.
|
||||||
|
- **De-risk first.** Put the highest-uncertainty item early so failure is cheap to recover from.
|
||||||
|
- **Interface before implementation.** If INT-N introduces a new module boundary, land the trait/type in INT-N-a and the impl in INT-N-b.
|
||||||
|
|
||||||
|
## Emit protocol
|
||||||
|
|
||||||
|
At the end of the planning turn, emit:
|
||||||
|
|
||||||
|
```
|
||||||
|
TASK: INT-01 — <title>
|
||||||
|
TASK: INT-02 — <title>
|
||||||
|
...
|
||||||
|
PLAN_COMPLETE: INT-01..05
|
||||||
|
```
|
||||||
|
|
||||||
|
The parser creates `mission_tasks` rows for each TASK line. `PLAN_COMPLETE` records that the plan pass finished so the mission's coding phase can begin iterating.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: int-xx-marker-protocol
|
||||||
|
description: The literal line-based markers the mission loop parses to advance state — TASK, WORK, HANDOFF, TEST_PASS, REVIEW_APPROVE, COMPLETED.
|
||||||
|
when_to_use: Pin on every coding role. Missing or malformed markers cause the mission to stall.
|
||||||
|
tags: [foundation, protocol]
|
||||||
|
---
|
||||||
|
|
||||||
|
# INT-XX marker protocol
|
||||||
|
|
||||||
|
Missions parse your turn output line-by-line for these markers. **They must appear literally**, with the colon, on their own line, no bold, no code fence.
|
||||||
|
|
||||||
|
## The full ladder
|
||||||
|
|
||||||
|
```
|
||||||
|
TASK: INT-NN — <title> # planner opens a new item
|
||||||
|
PLAN_COMPLETE: INT-NN # planner is done specifying
|
||||||
|
WORK: INT-NN # coder starts implementing
|
||||||
|
HANDOFF: INT-NN # coder passes to tester/reviewer
|
||||||
|
TEST_PASS: INT-NN # tester confirms green
|
||||||
|
TEST_FAIL: INT-NN — <reason> # tester failed the build; coder loops
|
||||||
|
REVIEW_APPROVE: INT-NN # reviewer OKs the diff
|
||||||
|
REVIEW_BLOCK: INT-NN — <reason> # reviewer requests changes
|
||||||
|
COMPLETED: INT-NN # committer pushed; loop advances
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. **Exactly one INT id per marker line.** `COMPLETED: INT-05, INT-06` won't parse.
|
||||||
|
2. **Emit at the end of your substantive turn**, not the beginning. Otherwise the parser advances state before the work is real.
|
||||||
|
3. **Never emit a marker you can't back up.** Emitting `COMPLETED: INT-NN` without a corresponding `git push` desynchronizes the mission from the repo.
|
||||||
|
4. **`REORDER: <one-sentence rationale>`** if you're taking an INT out of order — the loop's review timeline logs these.
|
||||||
|
|
||||||
|
## What the loop does with each
|
||||||
|
|
||||||
|
- `TASK` + `WORK` + `HANDOFF` are observability only — they show up in the mission's task-card feed (Slice 5).
|
||||||
|
- `TEST_FAIL` / `REVIEW_BLOCK` re-schedules the item back to the coder.
|
||||||
|
- `COMPLETED` bumps `mission_tasks.status = 'complete'` and advances the loop's `consumed_int_ids`. Next iteration picks the next unconsumed INT.
|
||||||
|
- `REORDER` writes a `loop_reorder_event` row — a reviewer can inspect why the order shifted.
|
||||||
|
|
||||||
|
## Common failures
|
||||||
|
|
||||||
|
- **Marker in a code fence**: `\`\`\`COMPLETED: INT-05\`\`\`` won't parse. Put it OUTSIDE the fence.
|
||||||
|
- **Marker with markdown emphasis**: `**COMPLETED: INT-05**` won't parse.
|
||||||
|
- **Wrong dash**: `INT-NN — title` uses en-dash; `INT-NN - title` also works. `INT-NN – title` (em-dash) also works. Any dash-like char is fine; the parser trims whitespace around it.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: small-focused-commits
|
||||||
|
description: Every commit does one thing, is understandable in isolation, and passes tests on its own. Bisectable-by-default policy.
|
||||||
|
when_to_use: Before every `git commit` — check that the commit is a single reviewable unit.
|
||||||
|
tags: [foundation, git, review]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Small, focused commits
|
||||||
|
|
||||||
|
A commit's job is to make ONE change that a reviewer can hold in their head, that CI can green, and that `git bisect` can meaningfully cross.
|
||||||
|
|
||||||
|
## Test yourself before you commit
|
||||||
|
|
||||||
|
1. **Can you describe the change in one sentence with no `and`?** If not, split.
|
||||||
|
2. **Does the diff exceed ~300 lines net, excluding generated/mechanical?** If yes, split unless every line is genuinely part of the same idea (renaming a symbol across a codebase counts).
|
||||||
|
3. **Would `git bisect` on this commit be meaningful?** If the commit combines a bug fix + a refactor, bisect can't isolate the bug — split.
|
||||||
|
4. **Does the commit compile + tests pass on its own?** If no, you're building a broken bisect landscape — reorder or squash so every commit is green.
|
||||||
|
|
||||||
|
## Splitting patterns
|
||||||
|
|
||||||
|
- **Mechanical then semantic** — rename first, behavior second. Two commits, each trivially reviewable.
|
||||||
|
- **Add then use** — introduce the new abstraction as an unused module first, then wire callers in a follow-up. Reviewers can validate the shape before opining on integration.
|
||||||
|
- **Refactor then feature** — never `refactor + add feature` in one commit. It hides what the feature actually cost.
|
||||||
|
- **Test then implementation** — TDD writes the test first; commit the failing test, then the passing impl (see [[tdd-red-green-refactor]]).
|
||||||
|
|
||||||
|
## What a good commit message looks like
|
||||||
|
|
||||||
|
```
|
||||||
|
<INT-NN> <imperative one-liner, ≤72 chars>
|
||||||
|
|
||||||
|
<paragraph explaining WHY the change is needed; what problem it solves;
|
||||||
|
what alternatives were considered and why they were rejected>
|
||||||
|
|
||||||
|
<optional: performance/security notes, migration guidance, follow-ups>
|
||||||
|
|
||||||
|
Refs: INT-NN
|
||||||
|
```
|
||||||
|
|
||||||
|
The subject is what shows up in `git log --oneline`. Make it stand alone.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- `wip` / `fix` / `update stuff` — never merge these; if that's what you have locally, squash before push.
|
||||||
|
- One commit per file — commits are about ideas, not filesystem layout.
|
||||||
|
- 40-line commit that touches 22 files — probably a rename that should be its own commit (see above).
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: tdd-red-green-refactor
|
||||||
|
description: Strict test-driven development — write the failing test first, make it pass minimally, then refactor. Prevents overbuilt code and pinpoints regressions.
|
||||||
|
when_to_use: Before writing any behavior-adding code. Applies to Rust, TypeScript, Python, anywhere tests can run cheap.
|
||||||
|
tags: [foundation, testing, tdd]
|
||||||
|
---
|
||||||
|
|
||||||
|
# TDD: red → green → refactor
|
||||||
|
|
||||||
|
## The loop
|
||||||
|
|
||||||
|
1. **RED** — Write the test that fails because the behavior doesn't exist yet. Run it. Confirm it fails for the RIGHT reason (missing symbol, wrong output — not a syntax error).
|
||||||
|
2. **GREEN** — Write the simplest possible code that makes the test pass. Not the "correct" version — the SIMPLEST one. Hardcoded return value is legal.
|
||||||
|
3. **REFACTOR** — Now that you have a passing safety net, restructure. Extract, rename, tighten types. Every intermediate state must still be green.
|
||||||
|
4. **Commit the RED-to-GREEN pair as one commit.** The refactor is its own commit.
|
||||||
|
|
||||||
|
## Why this order
|
||||||
|
|
||||||
|
- Writing the test first forces you to design the API from the caller's perspective. The API you wish existed usually beats the API you accidentally get.
|
||||||
|
- Watching the test fail proves the test can fail — a test that has never failed is a test you don't trust.
|
||||||
|
- Refactoring under a green bar means every step is safe. Refactoring in the dark means every step could silently break behavior.
|
||||||
|
|
||||||
|
## What counts as "a test"
|
||||||
|
|
||||||
|
- Rust: `#[test]` unit test, `#[tokio::test]` async, or an integration test under `tests/`. Not a `println!`.
|
||||||
|
- TypeScript: Vitest / Jest / Playwright. Storybook + a visual snapshot counts for component work.
|
||||||
|
- Any language: it exits non-zero when the behavior is broken, without a human interpreting the output.
|
||||||
|
|
||||||
|
## Coverage discipline
|
||||||
|
|
||||||
|
- Target: ≥90% line coverage on files you touched in this INT-XX item. Measured by `cargo llvm-cov` for Rust, `vitest --coverage` for TS.
|
||||||
|
- Coverage regressions on changed files block merge — enforce via the mission's `commit_policy` (Slice 4).
|
||||||
|
- 100% coverage is a smell — usually means testing implementation details. Aim for behavior coverage.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- Writing the impl first "because it's obvious" and adding tests after — you already lost the design feedback and the tests will inevitably shape to what the impl happens to do.
|
||||||
|
- Testing multiple behaviors in one `#[test]` — a failure now hides what actually broke.
|
||||||
|
- Snapshot-only test suites — snapshots catch NOTHING structural. Pair with at least one assertion per behavior.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: workspace-repo-commit-protocol
|
||||||
|
description: How to interact with /workspace/repo — the mission's checked-out codebase — and how to commit + push meaningful changes back.
|
||||||
|
when_to_use: You are a coder, committer, or any role that edits code. Pin this at turn start so you never lose orientation.
|
||||||
|
tags: [foundation, coding, git]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Workspace repo + commit protocol
|
||||||
|
|
||||||
|
You are running inside a mission's team container. The clawhdf5/backend/whatever repository the mission targets is bind-mounted at **`/workspace/repo`**. That is the ONLY path where source-modifying edits belong.
|
||||||
|
|
||||||
|
## Ground rules
|
||||||
|
|
||||||
|
1. **`cd /workspace/repo` at the start of every substantive turn.** If you `pwd` and it isn't `/workspace/repo`, cd there first — your default CWD is the ZeroClaw agent workspace (`~/workspace`), which is scratch storage, not the codebase.
|
||||||
|
2. **All `file_read` / `file_write` / `shell` calls that touch source use paths under `/workspace/repo`.** Anything else is scratch — the mission will not persist it.
|
||||||
|
3. **Never write files outside `/workspace/repo` and expect them to survive** — the agent workspace resets, `/tmp` is per-container ephemeral.
|
||||||
|
|
||||||
|
## Commit protocol
|
||||||
|
|
||||||
|
When (and only when) you have a meaningful, tested change:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd /workspace/repo
|
||||||
|
git status # sanity-check what you touched
|
||||||
|
git diff --stat # confirm scope matches the plan
|
||||||
|
git add -A
|
||||||
|
git commit -m "<INT-NN> <one-line title>
|
||||||
|
|
||||||
|
<one-paragraph rationale — WHY, not what>
|
||||||
|
|
||||||
|
Refs: INT-NN
|
||||||
|
"
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Commit message uses the mission's INT-XX marker** on the subject line — the mission's task-card parser (Slice 5) advances state on it.
|
||||||
|
- **One INT per commit** unless the change genuinely can't be split; split-when-in-doubt.
|
||||||
|
- **Never `--force`, never rewrite pushed history** without an explicit `HANDOFF: safe to force-push` from the reviewer.
|
||||||
|
|
||||||
|
## When NOT to commit
|
||||||
|
|
||||||
|
- Tests failing → fix or revert; never commit red.
|
||||||
|
- Reviewer emitted `REVIEW_BLOCK: INT-NN — <reason>` for the current item.
|
||||||
|
- The change is a WIP or exploratory — that lives in the agent's scratch workspace, not the repo.
|
||||||
|
|
||||||
|
## Emit the completion marker
|
||||||
|
|
||||||
|
After a successful push, on a line by itself:
|
||||||
|
|
||||||
|
```
|
||||||
|
COMPLETED: INT-NN
|
||||||
|
```
|
||||||
|
|
||||||
|
The mission loop advances on that marker. If you didn't push, don't emit it.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: component-4-state-model
|
||||||
|
description: Every component ships with four visual states — empty, loading, error, ready — from day 1. No spinners without skeletons.
|
||||||
|
when_to_use: You're designing or authoring a component that displays data. Frontend + mobile teams pin this.
|
||||||
|
tags: [frontend, design, ux]
|
||||||
|
---
|
||||||
|
|
||||||
|
# The four-state model
|
||||||
|
|
||||||
|
Every component that renders remote data has FOUR distinct visual states. Ship all four in the same PR — you can't "add empty state later" without shipping broken UX in the interim.
|
||||||
|
|
||||||
|
## The states
|
||||||
|
|
||||||
|
1. **Empty** — the request succeeded and returned zero items, OR the entity hasn't been created yet.
|
||||||
|
2. **Loading** — the request is in flight. Show a **skeleton** matching the eventual layout, never a spinner in isolation.
|
||||||
|
3. **Error** — the request failed. Show what went wrong + a recovery affordance (retry, contact support, or the fallback action).
|
||||||
|
4. **Ready** — data is here, render it.
|
||||||
|
|
||||||
|
## The skeleton rule
|
||||||
|
|
||||||
|
- Skeletons should match the ready state's LAYOUT within 5%. A wide card skeleton that resolves to a narrow list is disorienting.
|
||||||
|
- Skeletons pulse subtly (`animate-pulse` in Tailwind is fine). No spinners layered on top.
|
||||||
|
- Skeleton for lists shows **3 items** — enough to convey shape, not so many the eye reads them as content.
|
||||||
|
|
||||||
|
## The empty state rule
|
||||||
|
|
||||||
|
- Explain what would fill this component + the action to make it happen.
|
||||||
|
- CTA button placed prominently, not buried.
|
||||||
|
- Emojis / illustrations are fine but the copy carries the meaning.
|
||||||
|
|
||||||
|
Example (from the missions canvas):
|
||||||
|
> **No missions yet.** Pick a workflow template to get started. → `[Start a mission]`
|
||||||
|
|
||||||
|
## The error state rule
|
||||||
|
|
||||||
|
- Show the human-friendly reason (never the raw stack). "Couldn't reach the server. Retry?" not "TypeError: undefined is not a function".
|
||||||
|
- Retry button that actually retries the same request, not a page reload.
|
||||||
|
- If the error is auth (401/403), the affordance is "sign in", not "retry".
|
||||||
|
- Log the raw error to observability (Sentry, PostHog, etc.) so it's investigable. Users see the human message.
|
||||||
|
|
||||||
|
## Implementation shape (React 19)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function ThingList() {
|
||||||
|
const { data, error, isLoading, refetch } = useThings();
|
||||||
|
|
||||||
|
if (isLoading) return <ThingListSkeleton />;
|
||||||
|
if (error) return <ErrorCard error={error} onRetry={refetch} />;
|
||||||
|
if (!data?.length) return <EmptyCard onCreate={openWizard} />;
|
||||||
|
return <>{data.map((t) => <ThingRow key={t.id} thing={t} />)}</>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Empty state = "No data."** Do the work; explain what fills it and how.
|
||||||
|
- **Spinner-only loading state.** Nothing tells the user what's coming.
|
||||||
|
- **`data && data.length && data.map(...)`** without any branch for the falsy cases. That renders NOTHING and the user thinks the app is broken.
|
||||||
|
- **`toast.error(err.message)`** as your entire error handling. Toasts are ephemeral; a persistent error state is what tells the user the component is broken.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
name: react-19-server-components
|
||||||
|
description: React 19.2 patterns — RSC by default, `use()` hook, Actions + useActionState, `use client` boundaries.
|
||||||
|
when_to_use: You're writing React components in a Next.js 15+ app on React 19. Rust_sdlc + frontend teams pin this.
|
||||||
|
tags: [frontend, react, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# React 19 patterns (as of 19.2.7)
|
||||||
|
|
||||||
|
## Server Components by default
|
||||||
|
|
||||||
|
- **Every new component is a Server Component unless it needs interactivity.** `use client` opts in.
|
||||||
|
- **Why**: no JS ships to the browser for pure-display components, data fetching happens at the source, secrets stay server-side.
|
||||||
|
- **When to `use client`**:
|
||||||
|
- Hooks (`useState`, `useEffect`, `useReducer`, custom hooks that use them).
|
||||||
|
- Event handlers (`onClick`, `onChange`).
|
||||||
|
- Browser-only APIs (`window`, `document`, `IntersectionObserver`).
|
||||||
|
- Third-party libs that call `useLayoutEffect` internally.
|
||||||
|
|
||||||
|
Pattern: keep the parent server, extract the interactive leaf as client:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// list.tsx (server component)
|
||||||
|
export async function List() {
|
||||||
|
const items = await db.items.findMany();
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{items.map((i) => <Row key={i.id} item={i} />)}
|
||||||
|
<RefreshButton /> {/* client leaf */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh-button.tsx
|
||||||
|
"use client";
|
||||||
|
export function RefreshButton() {
|
||||||
|
return <button onClick={() => router.refresh()}>Refresh</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The `use()` hook
|
||||||
|
|
||||||
|
Read a promise or context inside a component (server OR client):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { use } from "react";
|
||||||
|
export function User({ userPromise }: { userPromise: Promise<User> }) {
|
||||||
|
const user = use(userPromise); // suspends until resolved
|
||||||
|
return <div>{user.name}</div>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Wrap in `<Suspense fallback={...}>` at the boundary you want to loading-state.
|
||||||
|
- Works with context: `const theme = use(ThemeContext)` — can be called conditionally, unlike `useContext`.
|
||||||
|
|
||||||
|
## Actions + `useActionState`
|
||||||
|
|
||||||
|
Server actions handle mutations without an API route:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
"use client";
|
||||||
|
import { useActionState } from "react";
|
||||||
|
|
||||||
|
async function submit(prev: State, formData: FormData) {
|
||||||
|
"use server";
|
||||||
|
await db.thing.create({ data: formData });
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Form() {
|
||||||
|
const [state, action, pending] = useActionState(submit, { ok: false });
|
||||||
|
return (
|
||||||
|
<form action={action}>
|
||||||
|
<input name="title" />
|
||||||
|
<button disabled={pending}>Save</button>
|
||||||
|
{state.ok && <p>Saved!</p>}
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## `useOptimistic`
|
||||||
|
|
||||||
|
For instant UI feedback on mutations:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const [optimistic, addOptimistic] = useOptimistic(items, (state, next) => [...state, next]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## What changed vs React 18
|
||||||
|
|
||||||
|
- **`forwardRef` is optional** — regular components accept `ref` as a prop (19.0+).
|
||||||
|
- **`use client` boundary is enforced** — you can't `useState` in a Server Component. Fails at build.
|
||||||
|
- **Removed**: `React.createRef` in functional components (was already deprecated), string refs.
|
||||||
|
|
||||||
|
## Common bugs
|
||||||
|
|
||||||
|
- **Passing a function from a Server Component to a Client Component prop** — fails serialization. Solution: define the function inside the client component or pass a Server Action.
|
||||||
|
- **`useState` in an async Server Component** — build error. Extract the interactive part to a `"use client"` leaf.
|
||||||
|
- **`document` accessed in the top level of a client component file** — SSR runs the top level, blows up. Guard with `if (typeof window !== "undefined")` or move into an effect.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: tailwind-v4-idioms
|
||||||
|
description: TailwindCSS v4 (4.3.3 as of 2026-07) — CSS-first config, no tailwind.config.js by default, container queries, custom variants.
|
||||||
|
when_to_use: You're authoring styles in a TailwindCSS v4 project. Frontend team pins this.
|
||||||
|
tags: [frontend, css, tailwind, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# TailwindCSS v4 (4.3.3)
|
||||||
|
|
||||||
|
## What changed vs v3
|
||||||
|
|
||||||
|
- **CSS-first config**. No `tailwind.config.js` by default. Theme tokens live in your CSS via `@theme { ... }`.
|
||||||
|
- **`@import "tailwindcss"`** replaces the three `@tailwind base/components/utilities` directives.
|
||||||
|
- **Zero-config content detection** — scans automatically via the Vite/PostCSS plugin, no `content: [...]` glob.
|
||||||
|
- **Native CSS variables everywhere** — every design token is a `--color-primary` style var, usable in raw CSS.
|
||||||
|
- **Container queries built-in** — `@container/name` variant, no plugin.
|
||||||
|
- **Faster** — Rust-powered engine (Oxide), ~100× faster incremental builds.
|
||||||
|
|
||||||
|
## Minimal setup
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* app.css */
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-brand-500: oklch(0.7 0.15 250);
|
||||||
|
--font-display: "Inter", ui-sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in a component: `class="text-brand-500 font-display"`.
|
||||||
|
|
||||||
|
## Class-order convention
|
||||||
|
|
||||||
|
For each `class="..."` attribute, order tokens:
|
||||||
|
|
||||||
|
```
|
||||||
|
[layout] → [box] → [typography] → [color] → [state]
|
||||||
|
flex items-center gap-2 → w-full p-4 rounded-lg → text-sm font-semibold → text-white bg-brand-500 → hover:bg-brand-600 focus:outline-none
|
||||||
|
```
|
||||||
|
|
||||||
|
Prettier's `prettier-plugin-tailwindcss` enforces this — run it in CI.
|
||||||
|
|
||||||
|
## Design system integration (ShadCN)
|
||||||
|
|
||||||
|
- ShadCN 2.x ships v4-native components. No compat layer needed.
|
||||||
|
- Custom variants: define once in `@variant` and reuse:
|
||||||
|
```css
|
||||||
|
@variant hocus (&:hover, &:focus-visible);
|
||||||
|
/* class="hocus:bg-brand-500" */
|
||||||
|
```
|
||||||
|
- Prefer `data-[state=open]:...` variants for stateful ShadCN primitives.
|
||||||
|
|
||||||
|
## Container queries
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="@container/card">
|
||||||
|
<p class="@md/card:text-lg text-sm">Adapts to card width, not viewport.</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Name the container (`/card`) so nested containers don't cross-reference.
|
||||||
|
- Breakpoints: `@sm`, `@md`, `@lg`, `@xl` follow the same px values as viewport.
|
||||||
|
|
||||||
|
## Dark mode
|
||||||
|
|
||||||
|
`@media (prefers-color-scheme: dark)` is picked up automatically. For a manual toggle, use `dark:` variant + a class or attribute switch on `<html>` (`data-theme="dark"` + `@variant dark (&[data-theme="dark"] *)`).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Bringing back `tailwind.config.js`** for one tweak. Use `@theme` in CSS instead.
|
||||||
|
- **Inline styles alongside utility classes** — pick one per element.
|
||||||
|
- **`@apply` in component CSS** — allowed but should be rare; utility class in JSX is usually clearer.
|
||||||
|
- **Arbitrary values everywhere** — `class="mt-[13px]"` is a code smell if it repeats. Promote to a theme token.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: gpu-coalescing-and-occupancy
|
||||||
|
description: The two knobs that dominate GPU perf — memory coalescing + occupancy targets across CUDA (Blackwell), Metal (Apple 7+), ROCm (RDNA3/CDNA3).
|
||||||
|
when_to_use: You're a kernel_author or bench_engineer profiling a GPU kernel. GPU team pins.
|
||||||
|
tags: [gpu, cuda, metal, rocm, performance]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Coalescing + occupancy
|
||||||
|
|
||||||
|
Two dominant knobs on ANY modern GPU. Get them right first; then worry about smaller wins.
|
||||||
|
|
||||||
|
## Coalescing
|
||||||
|
|
||||||
|
**Rule**: consecutive threads in a warp/wavefront should read consecutive 32/64/128-bit words.
|
||||||
|
|
||||||
|
- **CUDA (Blackwell, SM_100+)** — warp = 32 threads. Threads 0..31 accessing `arr[tid]` = 1 memory transaction. `arr[tid * stride]` for stride > 1 = multiple transactions. Violation cost: 4×–32× slowdown depending on stride.
|
||||||
|
- **Metal (Apple 7+, i.e., A17 / M3+)** — SIMD-group = 32 threads, same principle. `metal::simd_shuffle` for cross-lane comms without shared memory.
|
||||||
|
- **ROCm (RDNA3, CDNA3)** — wavefront = 32 (RDNA) or 64 (CDNA). Same rule; note the 64-wide wavefronts on CDNA change your indexing math.
|
||||||
|
|
||||||
|
Prove coalescing in a comment on the kernel:
|
||||||
|
```cpp
|
||||||
|
// Coalesced: thread tid reads global[tid], stride-1.
|
||||||
|
// Warp-level transaction: 1× 128-byte load per warp.
|
||||||
|
float x = global_in[gid];
|
||||||
|
```
|
||||||
|
|
||||||
|
## Occupancy
|
||||||
|
|
||||||
|
Rough targets:
|
||||||
|
- **Memory-bound kernel**: aim for **≥ 50% occupancy** to hide DRAM latency.
|
||||||
|
- **Compute-bound kernel with high ILP**: 25%–50% is often fine; more threads compete for registers + shared memory.
|
||||||
|
|
||||||
|
Occupancy is bounded by whichever hits first:
|
||||||
|
- Registers per thread × threads per block ≤ register file per SM.
|
||||||
|
- Shared / threadgroup memory per block ≤ per-SM budget.
|
||||||
|
- Threads per block ≤ max (typically 1024).
|
||||||
|
|
||||||
|
Tools:
|
||||||
|
- **Nsight Compute** (CUDA): `--section=Occupancy` prints the bottleneck.
|
||||||
|
- **Xcode GPU Frame Capture** (Metal): occupancy chart in the performance report.
|
||||||
|
- **rocprof** (ROCm): `--stats` for kernel occupancy.
|
||||||
|
|
||||||
|
## Memory hierarchy strategy
|
||||||
|
|
||||||
|
Order-of-magnitude comparison — always prefer the fastest tier your data footprint allows:
|
||||||
|
|
||||||
|
| Tier | CUDA name | Latency | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Register | reg | ~1 cycle | Per-thread; too many spills to L1 |
|
||||||
|
| Shared | shared / threadgroup / LDS | ~30 cycles | Per-block. CUDA: 48–100 KB. Metal: 32 KB (Apple7+). ROCm: 64 KB LDS |
|
||||||
|
| L1 / texture | L1 tex | ~200 cycles | Cache; some HW has separate paths |
|
||||||
|
| L2 | L2 | ~500 cycles | Shared across SMs |
|
||||||
|
| Global | DRAM / HBM | 400–800 cycles | Coalescing matters MOST here |
|
||||||
|
|
||||||
|
## Divergence
|
||||||
|
|
||||||
|
- Warp-uniform control flow keeps all lanes active.
|
||||||
|
- `if (tid % 2)` splits the warp — half the lanes idle per branch.
|
||||||
|
- On CUDA Volta+ (7.0+), Independent Thread Scheduling makes divergence CORRECT under all conditions but doesn't make it FAST.
|
||||||
|
|
||||||
|
## The one bottleneck rule
|
||||||
|
|
||||||
|
After profiling, report ONE bottleneck to attack next. Reporting five doesn't help — you'll fix one and re-profile anyway.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
> `KERNEL foo achieves 340 GB/s of 3.35 TB/s HBM = 10% BW-bound. Occupancy 62%. Bottleneck: stride-2 loads from arr — refactor tile shape to coalesce.`
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: roofline-model
|
||||||
|
description: The roofline model — is your kernel compute-bound or memory-bound? Estimate arithmetic intensity before optimizing.
|
||||||
|
when_to_use: You're the arch_analyst or bench_engineer role sizing up a GPU kernel before or after implementation.
|
||||||
|
tags: [gpu, performance, analysis]
|
||||||
|
---
|
||||||
|
|
||||||
|
# The roofline model
|
||||||
|
|
||||||
|
Before you optimize anything, know which wall you're hitting.
|
||||||
|
|
||||||
|
## The two rooflines
|
||||||
|
|
||||||
|
- **Peak memory bandwidth** — how many bytes/sec your GPU can pull from HBM.
|
||||||
|
- **Peak compute** — how many FLOPs/sec your GPU can execute.
|
||||||
|
|
||||||
|
Where they meet defines the **ridge point** (in FLOPs / byte). Kernels to the left of the ridge are memory-bound; to the right, compute-bound.
|
||||||
|
|
||||||
|
## Reference numbers (2026)
|
||||||
|
|
||||||
|
| Device | Peak BW (TB/s) | Peak FP32 (TFLOP/s) | FP16/BF16 tensor | Ridge (FLOP/B, FP32) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| NVIDIA B200 (Blackwell) | 8.0 | 80 | ~2200 tensor | ~10 |
|
||||||
|
| NVIDIA H100 | 3.35 | 67 | 989 tensor | ~20 |
|
||||||
|
| AMD MI300X (CDNA3) | 5.3 | 163 | ~2600 tensor | ~31 |
|
||||||
|
| Apple M3 Max | 0.4 | 14 | — | ~35 |
|
||||||
|
|
||||||
|
*Rough figures — verify against the actual SKU. Ratios matter more than absolutes.*
|
||||||
|
|
||||||
|
## Arithmetic intensity
|
||||||
|
|
||||||
|
FLOPs performed per byte read from HBM. Compute it BEFORE writing the kernel:
|
||||||
|
|
||||||
|
```
|
||||||
|
GEMM (A: MxK, B: KxN, C: MxN, all FP32):
|
||||||
|
FLOPs = 2*M*N*K
|
||||||
|
Bytes = 4*(M*K + K*N + M*N) (naive, no tiling)
|
||||||
|
AI = 2*M*N*K / (4*(M*K + K*N + M*N))
|
||||||
|
For M=N=K=1024: AI ≈ 170 FLOP/B → compute-bound on H100
|
||||||
|
For M=N=1024, K=32: AI ≈ 15 FLOP/B → memory-bound on H100
|
||||||
|
```
|
||||||
|
|
||||||
|
## What the diagnosis tells you
|
||||||
|
|
||||||
|
**Memory-bound** — DON'T optimize the math. Reduce bytes:
|
||||||
|
- Fuse kernels to keep data in registers/shared.
|
||||||
|
- Use lower precision (FP16/BF16/INT8) if numerics allow.
|
||||||
|
- Better tiling to reuse loaded data.
|
||||||
|
- Coalesce (see [[gpu-coalescing-and-occupancy]]).
|
||||||
|
|
||||||
|
**Compute-bound** — DON'T optimize the loads. Feed the ALU:
|
||||||
|
- Use tensor cores (Nvidia) / matrix cores (AMD) / AMX / metal Simd matrix ops.
|
||||||
|
- Instruction-level parallelism (multiple independent FMAs per thread).
|
||||||
|
- Higher occupancy is often COUNTERPRODUCTIVE — you want registers, not more threads.
|
||||||
|
|
||||||
|
**Balanced (near the ridge)** — hardest. Small changes tip you into one regime or the other. Profile OFTEN.
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
Every kernel benchmark report includes:
|
||||||
|
```
|
||||||
|
Kernel: gemm_fp32_tiled
|
||||||
|
Achieved throughput: 52 TFLOP/s (78% of 67 TFLOP/s peak)
|
||||||
|
Achieved bandwidth: 280 GB/s (8% of 3.35 TB/s peak)
|
||||||
|
Arithmetic intensity: 180 FLOP/B
|
||||||
|
Regime: compute-bound
|
||||||
|
Bottleneck to attack: tensor core underutilization — WMMA fragment misaligned
|
||||||
|
```
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Optimizing math on a memory-bound kernel.** Doubling FLOPs while DRAM saturates changes nothing.
|
||||||
|
- **Micro-benchmarking without measuring HBM traffic.** Wall clock alone can't tell you which regime you're in.
|
||||||
|
- **Skipping roofline "because we already know it's slow".** You don't know the CEILING until you plot it.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
name: expo-managed-vs-bare
|
||||||
|
description: Default to Expo managed workflow; drop to bare only for capabilities Expo hasn't shipped. Expo SDK 54+ (RN 0.86) baseline.
|
||||||
|
when_to_use: Starting or expanding an Expo/React Native app. Mobile team pins.
|
||||||
|
tags: [mobile, expo, react-native, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Expo managed vs bare (SDK 54+, RN 0.86)
|
||||||
|
|
||||||
|
Anchored to **Expo SDK 54** (2026 line) shipping React Native 0.86 as of 2026-07. New Architecture is default; JSC is removed; Hermes-only.
|
||||||
|
|
||||||
|
## Managed by default
|
||||||
|
|
||||||
|
- One codebase, `expo prebuild` on demand to regenerate iOS/Android projects.
|
||||||
|
- OTA updates via EAS Update.
|
||||||
|
- Config Plugins system covers 95% of native customization without ejecting.
|
||||||
|
|
||||||
|
## When to drop to bare
|
||||||
|
|
||||||
|
- Custom native modules with no Expo Module wrapper (rare — Expo Modules API covers most cases now).
|
||||||
|
- Fork of React Native itself.
|
||||||
|
- Vendor SDK that ships only a `.aar` or `.xcframework` with a config that Expo Config Plugins can't express.
|
||||||
|
|
||||||
|
If none of the above, stay managed. `expo prebuild` gives you the native project when you need to poke it, without permanent ejection.
|
||||||
|
|
||||||
|
## SDK version alignment
|
||||||
|
|
||||||
|
- Expo SDK N ships against a specific RN version. Don't `yarn add react-native@latest` — use `expo install react-native` so you get the version the SDK bundles.
|
||||||
|
- Expo SDK is released ~quarterly; RN follows ~monthly. Follow Expo's calendar, not RN's.
|
||||||
|
- Native modules published to npm should declare `expo-modules-core` peer dep with a range covering the current + previous SDK.
|
||||||
|
|
||||||
|
## Config Plugins
|
||||||
|
|
||||||
|
- `app.config.ts` over `app.json` — you get typing + can compute values (env-branched dev/staging/prod).
|
||||||
|
- Plugins compose: `[plugins]: ["expo-notifications", ["./plugins/withCustomEntitlement", { key: "..." }]]`.
|
||||||
|
- Custom plugins live in `plugins/` and are typed by `ConfigPlugin<Props>`.
|
||||||
|
|
||||||
|
## EAS Build
|
||||||
|
|
||||||
|
- Two profiles minimum: `development` (dev client, sim + device), `production` (store submission).
|
||||||
|
- `eas.json` per-branch overrides for env vars; secrets go into EAS Secrets, never repo.
|
||||||
|
- **Fingerprint** (SDK 51+) determines rebuild need — if your JS-only change fingerprints identically, no native rebuild. Saves 15+ min per iteration.
|
||||||
|
|
||||||
|
## When Bare IS the answer
|
||||||
|
|
||||||
|
- Deep customization of the launcher / splash screen beyond `expo-splash-screen` capabilities.
|
||||||
|
- Multi-target apps (main + share extension + widget) where Expo's single-target model doesn't fit.
|
||||||
|
- Enterprise MDM constraints that require unusual entitlements.
|
||||||
|
|
||||||
|
If you go bare, keep the Expo dep chain (`expo`, `expo-modules-core`) — you still benefit from `expo-router`, `expo-notifications`, etc. Bare ≠ vanilla RN.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Ejecting to bare because a StackOverflow answer said so.** Search for a Config Plugin first.
|
||||||
|
- **Mixing `react-native init` with `expo` deps.** Pick one project generator; don't try to retrofit.
|
||||||
|
- **Locking to a beta SDK for production.** Expo betas are for testing your app against the next SDK, not for shipping.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: rn-flashlist-perf
|
||||||
|
description: React Native list performance — FlashList (Shopify) over FlatList for anything > 20 rows. Reanimated 3 for animations. New Architecture-aware.
|
||||||
|
when_to_use: You're rendering scrollable lists or animations in RN 0.86+. Mobile team pins.
|
||||||
|
tags: [mobile, react-native, performance]
|
||||||
|
---
|
||||||
|
|
||||||
|
# RN performance essentials
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
- **FlashList** (`@shopify/flash-list` — 1.7+ for RN 0.86 New Architecture support) over `FlatList` for any list of 20+ rows. Recycles views instead of unmounting.
|
||||||
|
- **Estimated item size** is REQUIRED for FlashList perf. Measure a typical row height, plug in.
|
||||||
|
```tsx
|
||||||
|
<FlashList
|
||||||
|
data={items}
|
||||||
|
estimatedItemSize={72}
|
||||||
|
renderItem={({ item }) => <Row item={item} />}
|
||||||
|
keyExtractor={(i) => i.id}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
- **`getItemType`** for heterogeneous lists — FlashList recycles per type, so a section-header + row list gets 2 recycled pools.
|
||||||
|
- **`overrideItemLayout`** when items have known-in-advance heights that vary — skips measurement pass.
|
||||||
|
|
||||||
|
## Animations
|
||||||
|
|
||||||
|
- **Reanimated 3.x** for anything animating > 3× per frame (60+/s). Runs on the UI thread — no JS bridge bounce.
|
||||||
|
- **Never** `Animated` (the legacy API) for gesture-driven interactions. It goes through the JS bridge and jitters under load.
|
||||||
|
- **Worklets** for anything that reads a shared value + computes. Marked `"worklet"` at the top of the fn.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const scale = useSharedValue(1);
|
||||||
|
const style = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }));
|
||||||
|
// Update from JS: scale.value = withSpring(1.1);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gesture handling
|
||||||
|
|
||||||
|
- **`react-native-gesture-handler` v2.x** — required, not optional. Wrap `<GestureHandlerRootView>` at the app root.
|
||||||
|
- **`Gesture.Pan()`** etc. over `PanResponder`. Composes with Reanimated worklets natively.
|
||||||
|
|
||||||
|
## Image handling
|
||||||
|
|
||||||
|
- **`expo-image`** for any image where you'd have reached for `<Image>` — automatic caching, disk + memory tiers, format-optimal decoding.
|
||||||
|
- **Never** load a >1024px image directly. Resize server-side or via `expo-image-manipulator`.
|
||||||
|
- **Prefer WebP or AVIF** for static assets — smaller than PNG at same quality.
|
||||||
|
|
||||||
|
## Bridge crossings to avoid
|
||||||
|
|
||||||
|
- **`console.log` in prod builds** — goes through the bridge, has real cost. Strip with `babel-plugin-transform-remove-console`.
|
||||||
|
- **Anonymous fns in `renderItem`** — trigger reconciliation every render. Extract outside or `useCallback`.
|
||||||
|
- **Inline styles that create new objects every render** — same problem. Extract with `StyleSheet.create` or memoize.
|
||||||
|
|
||||||
|
## New Architecture caveats (default in SDK 54+)
|
||||||
|
|
||||||
|
- **Fabric renderer** — layout is synchronous. `onLayout` fires reliably.
|
||||||
|
- **Turbo Modules** — native calls are typed + can be sync where safe. If you own a native module and haven't migrated, do it (Codegen handles the JSI wrapper).
|
||||||
|
- **Some libraries still lag New Arch** — check the Fabric compat table; those still bounce through the bridge until they update.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **`ScrollView` with 200 hard-mounted children.** ScrollView renders all children up front — use FlashList always.
|
||||||
|
- **`setInterval` for animation.** Reanimated `withRepeat` runs on UI thread; setInterval jitters.
|
||||||
|
- **`InteractionManager.runAfterInteractions`** as a general delay tool. Its actual semantics are subtle; use `setTimeout(0)` if you just want "next tick".
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: cargo-test-driven-development
|
||||||
|
description: The `cargo nextest run` workflow for Rust TDD — fast, parallel, isolates flakes. Falls back to `cargo test` when nextest isn't installed.
|
||||||
|
when_to_use: You're the tester role, or coder role in the RED phase of TDD, on a Rust codebase.
|
||||||
|
tags: [rust, testing, tdd, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Cargo TDD workflow
|
||||||
|
|
||||||
|
## Runner
|
||||||
|
|
||||||
|
**Primary: `cargo nextest run` (0.9.140 as of 2026-07).** Why over `cargo test`:
|
||||||
|
- Parallel process isolation — one panicking test doesn't taint others.
|
||||||
|
- Structured, greppable output.
|
||||||
|
- `--retries N` policy for flake-tolerance without hiding real failures.
|
||||||
|
- Per-test timeouts.
|
||||||
|
|
||||||
|
Install: `cargo install cargo-nextest --locked` in the runtime image; CI runs `cargo nextest run --profile ci`.
|
||||||
|
|
||||||
|
## The RED-GREEN loop (Rust flavor)
|
||||||
|
|
||||||
|
1. **RED — write the failing test.**
|
||||||
|
```rust
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workspace_scoped_get_rejects_cross_workspace() {
|
||||||
|
let pool = testkit::pool().await;
|
||||||
|
let mine = seed_workspace(&pool).await;
|
||||||
|
let theirs = seed_workspace(&pool).await;
|
||||||
|
let row = insert_row(&pool, theirs).await;
|
||||||
|
assert!(get(&pool, row.id, mine).await.unwrap().is_none());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Run: `cargo nextest run workspace_scoped_get_rejects_cross_workspace`. Confirm the test fails because the fn doesn't exist yet or because behavior is missing.
|
||||||
|
|
||||||
|
2. **GREEN — minimal implementation.** Hardcode if that's what the ONE test asks for. Trust that the NEXT test will force you to generalize.
|
||||||
|
|
||||||
|
3. **REFACTOR under green.** Extract, rename, tighten. Re-run after each keystroke that could break something. `cargo nextest run <fn_name>` on the specific test is faster than the whole suite.
|
||||||
|
|
||||||
|
## Speed tricks
|
||||||
|
|
||||||
|
- **`cargo nextest run -j 8`** — parallelize across cores.
|
||||||
|
- **`cargo nextest run --no-fail-fast`** — see all failures in one pass, not just the first.
|
||||||
|
- **`cargo nextest run -E 'test(foo)'`** — filter expression language, more powerful than a substring.
|
||||||
|
- **`cargo check --tests`** before running — catches type errors ~10× faster than compiling test binaries.
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
- **`cargo llvm-cov nextest`** — nextest under llvm-cov instrumentation. Reports per-file line coverage.
|
||||||
|
- CI enforces ≥90% on changed lines. Locally: `cargo llvm-cov nextest --html && open target/llvm-cov/html/index.html`.
|
||||||
|
|
||||||
|
## Integration tests that hit real infra
|
||||||
|
|
||||||
|
- Postgres via `testcontainers` — spins a fresh container per test suite. NEVER mock the DB when the test's job is to prove a query.
|
||||||
|
- HTTP via `axum::Router` + `tower::ServiceExt::oneshot` — driver is in-process, no port needed.
|
||||||
|
- Docker via `bollard` mock or a `#[ignore]` guarded live test.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Snapshot-only test files.** A snapshot proves nothing structural. Pair with real assertions.
|
||||||
|
- **`#[ignore]`d tests that stay ignored.** Ignore is a debt marker — file the ticket to fix and put the ticket ID in the ignore reason.
|
||||||
|
- **Testing panics with `should_panic` when a `Result::Err` would do.** Panics on the happy path of a test framework are user-hostile output.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
name: rust-async-tokio-idioms
|
||||||
|
description: Tokio 1.x runtime idioms — spawn vs block_in_place, cancellation, channels, avoiding Arc<Mutex<T>> patterns.
|
||||||
|
when_to_use: You're writing async Rust code with Tokio. Applies to nearly all clawmates crates.
|
||||||
|
tags: [rust, async, tokio]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tokio async idioms
|
||||||
|
|
||||||
|
Anchored to **Tokio 1.x** (workspace tracks the latest 1.x line, currently ~1.42).
|
||||||
|
|
||||||
|
## Runtime choice
|
||||||
|
|
||||||
|
- **`#[tokio::main]`** in the binary. Default is multi-thread — fine.
|
||||||
|
- **`#[tokio::test]`** in tests. Default is current-thread (fast, deterministic). Add `flavor = "multi_thread"` when the code under test spawns.
|
||||||
|
- **NEVER** call `tokio::runtime::Handle::current().block_on(...)` inside async code — deadlocks in current-thread, breaks structured concurrency in multi-thread.
|
||||||
|
|
||||||
|
## Spawning
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let handle = tokio::spawn(async move {
|
||||||
|
do_work().await
|
||||||
|
});
|
||||||
|
let result = handle.await??; // ?? = JoinError, then inner Result
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`tokio::spawn`** for detached background work. Returns `JoinHandle`.
|
||||||
|
- **`tokio::task::spawn_blocking`** for CPU-heavy or blocking-syscall work (`std::fs`, `sync::Mutex`). Bounded pool — don't spawn thousands.
|
||||||
|
- **`tokio::task::spawn_local`** only inside a `LocalSet` — you almost never want this.
|
||||||
|
|
||||||
|
## Cancellation
|
||||||
|
|
||||||
|
- Dropping a `JoinHandle` DOES NOT cancel the task by default. Use `.abort()` or `CancellationToken`.
|
||||||
|
- Prefer `tokio_util::sync::CancellationToken` for structured cancel — pass the child token, call `parent.cancel()` at teardown.
|
||||||
|
- `tokio::select!` on `token.cancelled()` in every long-running loop.
|
||||||
|
|
||||||
|
## Channels
|
||||||
|
|
||||||
|
- **`mpsc::channel(cap)`** for actor-like state — one owner processes messages. Cap ≥ number of expected concurrent senders.
|
||||||
|
- **`oneshot::channel()`** for request/response. Sender is consumed on send.
|
||||||
|
- **`broadcast::channel(cap)`** for fan-out. Slow subscribers get `Lagged` errors — handle it.
|
||||||
|
- **`watch::channel(initial)`** for latest-value-wins state (config, health flags).
|
||||||
|
|
||||||
|
## Avoiding `Arc<Mutex<T>>`
|
||||||
|
|
||||||
|
Common trap: shared mutable state protected by a lock. Usually a smell.
|
||||||
|
|
||||||
|
- Immutable snapshot: `Arc<T>` is enough.
|
||||||
|
- Update-then-read cadence: `arc-swap::ArcSwap<T>` (lock-free).
|
||||||
|
- Actor-shaped: `mpsc::channel` + one task owning the state.
|
||||||
|
- Sync mutex needed AT ALL: use `std::sync::Mutex` (not `tokio::sync::Mutex`) for anything held < 1ms. Tokio's mutex is for holding across `.await`, which you should be actively avoiding.
|
||||||
|
|
||||||
|
## `.await` discipline
|
||||||
|
|
||||||
|
- Never `.await` inside a `std::sync::MutexGuard` scope — deadlock waiting to happen. Drop the guard first.
|
||||||
|
- Never `.await` inside a `tokio::task::block_in_place` block — it panics.
|
||||||
|
- `tokio::select! { biased; ... }` when you need branch order guarantees (usually shutdown-first).
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **`futures::executor::block_on` inside a Tokio task.** Use `tokio::task::block_in_place` if you really need to sync-bridge.
|
||||||
|
- **`std::thread::sleep` in async code.** Use `tokio::time::sleep`.
|
||||||
|
- **Spawning from `Drop`.** `Drop` isn't async; the runtime may already be shutting down.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
name: rust-error-handling
|
||||||
|
description: When to use `anyhow` vs `thiserror`, how to add context, when to panic. Rust 1.97+ idioms.
|
||||||
|
when_to_use: Any time you write a fn returning `Result` in Rust.
|
||||||
|
tags: [rust, errors]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rust error handling
|
||||||
|
|
||||||
|
## Which error type
|
||||||
|
|
||||||
|
- **Application binary** (`clawmates-server`, `clawmates-node`) → `anyhow::Result<T>` everywhere. You never match on the error at runtime; you print + log + exit.
|
||||||
|
- **Library crate** (`cm-db`, `cm-api`, `cm-runtime`) → `thiserror` on a per-module `Error` enum. Callers get typed variants they can match.
|
||||||
|
- **Test code** → `anyhow` is fine even in library crates. Tests don't match on error types.
|
||||||
|
|
||||||
|
## `thiserror` shape
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DbError {
|
||||||
|
#[error("row not found")]
|
||||||
|
NotFound,
|
||||||
|
#[error("workspace mismatch: expected {expected}, got {actual}")]
|
||||||
|
WorkspaceMismatch { expected: Uuid, actual: Uuid },
|
||||||
|
#[error(transparent)]
|
||||||
|
Sqlx(#[from] sqlx::Error),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- **`#[from]` for exactly one wrapped external type per variant.** Otherwise `?` becomes ambiguous.
|
||||||
|
- **`#[error("...")]` includes the values.** Never `#[error("db error")]` — that's information-lossy.
|
||||||
|
- **`#[error(transparent)]`** for pass-through wrappers where the inner error's message is already sufficient.
|
||||||
|
|
||||||
|
## Adding context
|
||||||
|
|
||||||
|
Every `?` at a module boundary needs context. Without it, a deeply-nested `sqlx::Error` is unattributable.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
let user = load(pool, id)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("load user {id}"))?;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`.context("...")`** for a static string.
|
||||||
|
- **`.with_context(|| ...)`** when the message includes runtime data (allocation is lazy — only fires on error).
|
||||||
|
|
||||||
|
## When to panic
|
||||||
|
|
||||||
|
Panic is fine when:
|
||||||
|
- The invariant CAN'T be violated at runtime (`unreachable!("mission_kind is validated at insert time")`).
|
||||||
|
- You're in `main()` before the async runtime, and the config file is broken — `.expect("clawmates.toml is required")`.
|
||||||
|
- Test-only preconditions (`assert_eq!` inside `#[test]`).
|
||||||
|
|
||||||
|
Panic is NOT fine when:
|
||||||
|
- The input came from the network.
|
||||||
|
- The input came from a config parse.
|
||||||
|
- The input came from a database row that a migration is supposed to guarantee — write an `Error` variant explaining WHY the row shape violates the invariant.
|
||||||
|
|
||||||
|
## Backtrace hygiene
|
||||||
|
|
||||||
|
- Set `RUST_BACKTRACE=1` in dev + CI. Off in prod (leaks internals).
|
||||||
|
- `anyhow::Error` captures a backtrace automatically. Log it via `error!("op failed: {e:?}")` — the `?` formatter includes the chain + backtrace.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: write-rust-current-edition
|
||||||
|
description: House Rust style anchored to edition 2024 + MSRV 1.97.1 (as of 2026-07). Idioms, patterns, and version-guarded features.
|
||||||
|
when_to_use: Writing any Rust code. Pin on rust_sdlc + backend + gpu coder roles.
|
||||||
|
tags: [rust, style, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Writing Rust (edition 2024, MSRV 1.97.1)
|
||||||
|
|
||||||
|
## Toolchain we ship against
|
||||||
|
|
||||||
|
Version fields as of **2026-07**. Verify with `rustc --version` before assuming.
|
||||||
|
|
||||||
|
- **stable**: 1.97.1
|
||||||
|
- **workspace MSRV**: 1.97.1 (some crates require 1.98.0+; check `rust-toolchain.toml`)
|
||||||
|
- **edition**: 2024 (default for new crates; workspace-wide bump per project)
|
||||||
|
- **cargo tools we standardize on**: `cargo-nextest 0.9.140`, `cargo-audit 0.21+`, `cargo-llvm-cov 0.6+`, `cargo-machete`, `cargo-hakari` (for workspace unification)
|
||||||
|
|
||||||
|
## Idioms
|
||||||
|
|
||||||
|
- **`let-else` over deep nesting.**
|
||||||
|
```rust
|
||||||
|
let Some(user) = auth.load(id).await? else {
|
||||||
|
return Err(NotFound.into());
|
||||||
|
};
|
||||||
|
```
|
||||||
|
- **`?` with `.context()`** at every boundary (uses `anyhow::Context` or the equivalent). Bare `?` inside a private function is fine; at API surfaces, add context.
|
||||||
|
- **`if let` chains** (stable in edition 2024):
|
||||||
|
```rust
|
||||||
|
if let Some(x) = maybe && x > 0 && !seen.contains(&x) { ... }
|
||||||
|
```
|
||||||
|
Nest with `if let` blocks in edition-2021 crates until MSRV catches up.
|
||||||
|
- **Struct-of-args when a fn exceeds ~5 params** — pass an `Args<'a>` struct. Clippy's `too_many_arguments` enforces at 7.
|
||||||
|
- **`impl Trait` in argument position** for callbacks; `Box<dyn Trait>` only when erasure is genuinely needed.
|
||||||
|
- **`Arc<T>` over `Rc<T>`** anywhere `Send` might matter — you're almost always in a Tokio runtime.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **`unwrap()` in library code.** Only allowed in tests, in `main.rs` before the runtime spins up, and behind `debug_assert!`.
|
||||||
|
- **`.clone()` as a borrow-checker escape.** Ask why the borrow can't work first. If it genuinely can't, add a comment explaining the tradeoff.
|
||||||
|
- **`Arc<Mutex<T>>` when a channel would do.** For actor-like state, `tokio::sync::mpsc` is almost always the right answer.
|
||||||
|
- **`Box<Pin<Future>>` in public APIs** where `impl Future` would do — leaks implementation and forces heap alloc.
|
||||||
|
- **`#[allow(clippy::...)]` without a comment.** Every allow needs a one-line WHY.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- **`cargo nextest run`** as the default runner — faster, better output, isolates flaky tests. Fall back to `cargo test` when nextest isn't installed.
|
||||||
|
- **`#[tokio::test]`** for async. Prefer `#[tokio::test(flavor = "multi_thread")]` when the code under test spawns.
|
||||||
|
- See [[cargo-test-driven-development]] for the TDD flow.
|
||||||
|
|
||||||
|
## Lint policy
|
||||||
|
|
||||||
|
Every Rust crate CI runs `cargo clippy --all-targets -- -D warnings`. Warnings ARE errors. Fix the warning; if it's a false positive, `#[allow(...)]` with a comment.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
name: cargo-audit-workflow
|
||||||
|
description: `cargo-audit` (rustsec) — how to run it, triage findings, and integrate into the security template's mission flow.
|
||||||
|
when_to_use: You're on a security mission or investigating a RUSTSEC advisory in a Rust project.
|
||||||
|
tags: [security, rust, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# cargo-audit workflow
|
||||||
|
|
||||||
|
Anchored to **cargo-audit 0.21+** (2026 line) reading the [RustSec Advisory DB](https://rustsec.org/advisories/).
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo install cargo-audit --locked
|
||||||
|
cargo audit # scans Cargo.lock, reports vulnerabilities
|
||||||
|
cargo audit fetch # update advisory DB
|
||||||
|
cargo audit --deny warnings # non-zero on any warning too (yanked crates, unmaintained)
|
||||||
|
```
|
||||||
|
|
||||||
|
CI runs: `cargo audit --deny warnings --json` — JSON output goes to the mission's `mission_artifacts` as a `security_report`.
|
||||||
|
|
||||||
|
## Triage each finding
|
||||||
|
|
||||||
|
For every advisory the report surfaces:
|
||||||
|
|
||||||
|
1. **Is the vulnerable path reachable from OUR code?**
|
||||||
|
- `cargo tree -i <cratename>` shows who depends on it.
|
||||||
|
- Sometimes a vulnerable transitive dep is only exercised by a feature you don't enable — check the report's `affected_functions`.
|
||||||
|
|
||||||
|
2. **What versions fix it?**
|
||||||
|
- The advisory lists `patched_versions`. Try `cargo update -p <crate> --precise <version>` to hop to a patched minor without a semver-major bump.
|
||||||
|
- If patched only in a newer major, plan the migration (may be a multi-INT effort).
|
||||||
|
|
||||||
|
3. **Is there a `RUSTSEC` allowlist for a known-false-positive?**
|
||||||
|
- `[advisories.ignore]` in `audit.toml` — with a comment explaining WHY and a ticket link + expiry date.
|
||||||
|
- Never ignore without expiry; reviewers should re-evaluate quarterly.
|
||||||
|
|
||||||
|
## Common findings + fixes
|
||||||
|
|
||||||
|
- **`RUSTSEC-YYYY-NNNN` on a dev-dep**: less urgent, but fix anyway — dev tools run in CI with elevated permissions.
|
||||||
|
- **Yanked crate** — the version was pulled from crates.io. Update immediately; `cargo update -p <crate>` picks the next non-yanked.
|
||||||
|
- **Unmaintained crate warning** — no CVE yet, but the maintainer walked away. Long-term: replace. Short-term: pin and document.
|
||||||
|
|
||||||
|
## Integration with the security mission template
|
||||||
|
|
||||||
|
The security-hardening mission runs `cargo audit --json` in the security_scan phase. Each finding becomes a `mission_task` with:
|
||||||
|
- `external_id = "RUSTSEC-YYYY-NNNN"`
|
||||||
|
- `title = advisory.title`
|
||||||
|
- `status = "created"`
|
||||||
|
|
||||||
|
The coding phase picks up each finding as an INT-XX item and applies the fix (bump, replace, or add ignore-with-justification). Emit `COMPLETED: RUSTSEC-YYYY-NNNN` when the follow-up commit lands.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Silencing an advisory in CI without a fix commit.** Advisories aren't a code style violation — they're a claim your code is exposed. Fix or explicitly accept.
|
||||||
|
- **Bumping to a beta version to "fix" it.** Betas can regress. Prefer a patched stable; if none exists, wait or fork.
|
||||||
|
- **Running `cargo audit` only in CI, never locally.** Every commit that changes Cargo.lock should trigger a local run.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
name: secret-scanning-gitleaks
|
||||||
|
description: Detecting + removing leaked credentials with gitleaks 8+. Pre-commit + CI + full history sweep policies.
|
||||||
|
when_to_use: You're on a security mission, or setting up a new repo, or triaging a "did we push a key?" incident.
|
||||||
|
tags: [security, git]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Gitleaks workflow
|
||||||
|
|
||||||
|
Anchored to **gitleaks 8.20+** (2026 line). Detects secrets in the working tree, staged changes, and git history.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
- **`gitleaks protect --staged`** — pre-commit hook. Blocks commits containing secrets.
|
||||||
|
- **`gitleaks detect`** — scans working tree + full history. Use in CI.
|
||||||
|
- **`gitleaks detect --log-opts="--since=2026-01-01"`** — bounded historical sweep for large repos.
|
||||||
|
|
||||||
|
## Pre-commit integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .pre-commit-config.yaml
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.20.0
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Config: `.gitleaks.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
title = "clawmates gitleaks config"
|
||||||
|
[extend]
|
||||||
|
useDefault = true # start from bundled rules
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
id = "clawmates-internal-token"
|
||||||
|
description = "Our internal service session token pattern"
|
||||||
|
regex = '''cm_svc_[a-f0-9]{64}'''
|
||||||
|
tags = ["clawmates", "token"]
|
||||||
|
|
||||||
|
[allowlist]
|
||||||
|
description = "Test fixtures that intentionally contain fake secrets"
|
||||||
|
paths = ['''crates/cm-auth/tests/fixtures/.*''']
|
||||||
|
regexes = ['''sk-ant-api03-TEST[a-zA-Z0-9]+''']
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules to add per-repo, not global — every codebase has domain-specific token shapes.
|
||||||
|
|
||||||
|
## What to do when it fires
|
||||||
|
|
||||||
|
**On a commit-in-progress:**
|
||||||
|
1. Don't commit.
|
||||||
|
2. Rotate the credential immediately (whether you'd have pushed it or not — assume compromised the moment it existed on disk).
|
||||||
|
3. Remove from the file. Re-attempt.
|
||||||
|
|
||||||
|
**On a historical hit (already pushed):**
|
||||||
|
1. Rotate first, panic second.
|
||||||
|
2. Force-remove from history with `git filter-repo --replace-text` or `bfg-repo-cleaner`.
|
||||||
|
3. `git push --force` after coordination with everyone with a clone (they'll need to `git reset --hard origin/main` to avoid re-introducing).
|
||||||
|
4. Notify anyone who might have cached the value (CI logs, artifact registries, error monitoring backends often store request bodies).
|
||||||
|
|
||||||
|
## What gitleaks misses
|
||||||
|
|
||||||
|
- **Base64-encoded secrets.** Add a rule if you use them.
|
||||||
|
- **Split-across-lines secrets** — most rules are single-line regex.
|
||||||
|
- **Secrets in binary files.** Gitleaks skips binaries by default; scan artifacts separately.
|
||||||
|
- **Encrypted-at-rest formats** (age, sops) — encrypted content is opaque to gitleaks; make sure the ENCRYPTED file being in the repo is the intent.
|
||||||
|
|
||||||
|
## Integration with the security mission template
|
||||||
|
|
||||||
|
Runs `gitleaks detect --report-format=json` in the security_scan phase. Each finding becomes a `mission_task`:
|
||||||
|
- `external_id = <fingerprint>`
|
||||||
|
- `title = "<RuleID> at <file>:<line>"`
|
||||||
|
- `status = "created"`
|
||||||
|
|
||||||
|
The coding phase remediates: rotate + purge + add allowlist entry if a false positive. Emit `COMPLETED: <fingerprint>` when the rotation is confirmed AND the historical removal is deployed.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **Adding a blanket `.gitleaks.toml` allowlist** covering entire directories. Neuters detection. Allowlist specific patterns with justification comments.
|
||||||
|
- **Fixing the secret leak by squashing history without rotating.** The secret was on disk on multiple machines; it's compromised. Rotate first.
|
||||||
|
- **`.gitignore` as your secret defense.** Files can be added by name from the CLI; gitignore only helps `git add .` sweeps.
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: threejs-perf-and-teardown
|
||||||
|
description: three.js r185 essentials — 60fps budget, InstancedMesh over per-node Meshes, dispose everything on scene teardown.
|
||||||
|
when_to_use: You're coding or profiling a three.js/WebGL/WebGPU scene. threeJS team pins.
|
||||||
|
tags: [threejs, webgl, webgpu, performance, versioned]
|
||||||
|
---
|
||||||
|
|
||||||
|
# three.js perf + teardown (r185)
|
||||||
|
|
||||||
|
Anchored to **three.js r185** (2026-07). WebGPURenderer is stable enough for production on Chromium/Safari; WebGL2 renderer still the default fallback.
|
||||||
|
|
||||||
|
## Frame budget
|
||||||
|
|
||||||
|
Target **16.6 ms/frame** (60 fps) on the median target device. Break it down:
|
||||||
|
- JS + scene graph updates: ≤ 4 ms
|
||||||
|
- GPU submission: ≤ 4 ms
|
||||||
|
- GPU rendering (browser waits): remainder
|
||||||
|
|
||||||
|
If you can't hit 60 fps at 1440p on a mid-range GPU, DON'T ship as-is. Downgrade features (fewer shadow casters, lower shadow map res, LOD swaps) before shipping.
|
||||||
|
|
||||||
|
## Draw call budget
|
||||||
|
|
||||||
|
- Target **≤ 200 draw calls per frame**. Every distinct material × geometry combo = ≥ 1 draw call.
|
||||||
|
- **InstancedMesh** for any repeated geometry. 10,000 trees = 1 draw call, not 10,000.
|
||||||
|
- **BatchedMesh** (added in r168+, matured through r185) for heterogeneous geometry with shared material — batch different meshes with one call.
|
||||||
|
|
||||||
|
## Memory teardown
|
||||||
|
|
||||||
|
**The #1 cause of "why does the tab crash after 20 minutes"**. GPU resources are NOT garbage collected — you must dispose.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function teardown(scene: THREE.Scene) {
|
||||||
|
scene.traverse((obj) => {
|
||||||
|
if ((obj as THREE.Mesh).isMesh) {
|
||||||
|
const m = obj as THREE.Mesh;
|
||||||
|
m.geometry.dispose();
|
||||||
|
if (Array.isArray(m.material)) m.material.forEach((x) => x.dispose());
|
||||||
|
else m.material.dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renderer.dispose(); // WebGL context + programs
|
||||||
|
renderer.forceContextLoss(); // guarantee GPU release, not deferred
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Textures must ALSO be disposed — walk materials and dispose any `map`, `normalMap`, `roughnessMap`, etc.
|
||||||
|
|
||||||
|
## Render-loop discipline
|
||||||
|
|
||||||
|
- **Reuse math objects.** Never `new THREE.Vector3()` inside the render loop.
|
||||||
|
```ts
|
||||||
|
const _tmp = new THREE.Vector3(); // module-level
|
||||||
|
function tick() {
|
||||||
|
_tmp.copy(a).sub(b).normalize(); // no allocation
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Same for `Matrix4`, `Quaternion`, `Color`.
|
||||||
|
- If you MUST allocate, batch outside the loop.
|
||||||
|
|
||||||
|
## Shadows
|
||||||
|
|
||||||
|
- **One directional shadow-caster.** Bake everything else via lightmaps or ambient occlusion.
|
||||||
|
- Shadow map resolution: 1024 for medium range, 2048 max. 4096 is a mobile-crash-in-a-can.
|
||||||
|
- `light.shadow.autoUpdate = false; light.shadow.needsUpdate = true;` when the shadow is static — one render + freeze.
|
||||||
|
|
||||||
|
## Custom shaders
|
||||||
|
|
||||||
|
- Try `ShaderMaterial` when built-ins get close but not exact.
|
||||||
|
- `onBeforeCompile` hook for tweaking a built-in when it's 95% right — patch the shader source instead of rewriting.
|
||||||
|
- WebGPU: WGSL, not GLSL. Migrate shader-by-shader as you adopt WebGPURenderer.
|
||||||
|
|
||||||
|
## Profiling
|
||||||
|
|
||||||
|
- **Chrome DevTools Performance panel** — JS time.
|
||||||
|
- **SpectorJS** browser extension — per-frame draw call breakdown, texture inspector, shader debugger.
|
||||||
|
- **WebGL / WebGPU inspector** in Firefox — similar to Spector.
|
||||||
|
|
||||||
|
## Anti-patterns
|
||||||
|
|
||||||
|
- **`new THREE.Mesh(geometry, material)` inside `requestAnimationFrame`.** Creates 60 mesh objects per second; profile-visible before you notice.
|
||||||
|
- **Adding then removing lights on interaction.** Recompiles shaders (cache-miss). Toggle intensity to 0 instead.
|
||||||
|
- **`renderer.setPixelRatio(window.devicePixelRatio)` on a 3× retina display without a quality slider.** Renders 9× the pixels; kills fps. Cap at 2 by default.
|
||||||
Reference in New Issue
Block a user