Check locally. just check is the single gate: format + lint + code rules, scoped
to your changes vs origin/main, every finding printed as file:line [rule-id] with
the command that fixes it. just check full adds tsc + clippy.
Format. One rule per line: <id> [scope] rule (evidence · enforcement · docs).
The id prefix is the domain:
CS-##— Rust backend (crates/,services/,tooling/)FE-##— frontend and shared TypeScript (apps/web,packages/)
IDs are stable — cite them in review comments ("see CS-30", "FE-12"), never renumber or reuse them; deletions leave gaps and new rules append with the next free number. Scope tags make the list greppable.
CS scopes: [db] database & migrations · [types] type design · [cfg] config, env,
secrets · [err] errors & observability · [arch] architecture & boundaries · [api]
API & handler design · [sec] security & permissions · [rust] Rust idioms · [perf]
performance · [test] testing
FE scopes: [data] data fetching & queries · [solid] Solid reactivity & state ·
[async] async & error handling · [arch] architecture & module hygiene · [ts]
TypeScript · [ui] UI / UX conventions
- CS-01
[db]Generate ids as UUIDv7 in application code — nevergen_random_uuid()(UUIDv4); v7 ids sort by creation time. (#4296) - CS-02
[db]Make seed/backfill inserts and anything that can race idempotent with anON CONFLICThandler. (#4296, #4498) - CS-03
[db]Prefer natural/composite keys over surrogate ids when the table is queried/sorted by those columns anyway, and key REST routes on the natural id. (#4296, #4498) - CS-04
[db]Name user columnsuser_id— notowner_user_idor other variants; follow existing tables. (#4498) - CS-05
[db]New tables need a deliberate cascade/cleanup story, not whatever the default happens to be. (#4296) - CS-06
[db]Don't add redundant indexes — a column already covered as the leading part of the primary key does not need its own index. (#3961) - CS-07
[db]Don't store columns that are implicit in context (e.g. a scoping id already fixed per durable object / per tenant). (#3961) - CS-08
[db]Usesqlx::query!/query_as!(compile-time checked) by default; the non-macro form is only for queries that genuinely cannot be statically known. (#4156 · enforced: clippydisallowed-methods· workflow: database development) - CS-09
[db]The.sqlxcache lives at the workspace root — runnix develop --command just prepare_dbfrom the repository root; never commit a.sqlxdirectory inside an individual crate. (#4577 · workflow: database development) - CS-10
[types]Newtype your identifiers and tokens — wrap rawStringids/tokens/model-ids in a validated newtype that checks shape at construction. (#4020, #4077, #4276) - CS-11
[types]A closed set of string values is an enum, not aString— implDisplay/FromStras needed. (#4296, #4410) - CS-12
[types]Optional meansOption<T>— don't rely on sentinel values or convention to express absence. (#4296) - CS-13
[types]Model tri-state data (not-loaded / missing / present) as one flat enum, not nestedOptions or ad-hoc flags. (#4527) - CS-14
[cfg]All env access goes throughmacro_env_var/macro_config— neverstd::env::var, never hand-rolled wrappers; useMaybeEnvVarfor optional vars. The same goes for AWS config instantiation (macro_aws_config) and tracing subscriber setup (macro_entrypoint): use the shared crates. (#4306, #4334, #4380 · enforced: clippydisallowed-methods) - CS-15
[cfg]Fail fast: validate config at service instantiation, not deep inside request handling — a missing env var should kill startup, not a request. (#4077, #4156) - CS-16
[cfg]Don't add.context()to env-var macro errors — the macro error already statically names the missing variable. (#4156) - CS-17
[cfg]Doppler secret key names must exactly match the env var name referenced in code. (#4525) - CS-18
[cfg]Register new environment variables in Doppler. They are plain env vars, notLocalOrRemote/doppler-wrapped; non-secret config goes in Doppler as raw values, not AWS Secrets Manager secrets. (#4305, #4525) - CS-19
[err]Give third-party errors their own variant — don't collapse e.g. ajsonwebtokenfailure into a generic internal error. (#4020) - CS-20
[err]Depending on a rate-limited external provider requires a fallback (fallback model, retry story, or documented degradation). (#4296) - CS-21
[err]Wire usage metering on every invocation path — MCP-triggered tool calls count too, not just the primary path. (#4296) - CS-22
[err]When adding#[tracing::instrument], includeerronResultfunctions and omit it on other return types. Never setlevel = "info"on the attribute. Log errors as structured fields (tracing::error!(error=?e, "msg")); prefer.inspect_erroverif let Err(e)for logging. - CS-23
[arch]Do not growmacro_db_client— new domain logic gets a new crate; the catch-all crates must shrink, not accumulate. (#4380) - CS-24
[arch]Keep source files under ~1000 lines — split before a reviewer has to ask. (#4364) - CS-25
[arch]mod.rsdeclares submodules; it doesn't host logic — prefer thefoo.rs+foo/file-and-directory style for modules with logic. (#4175 · enforced: ast-greprust-mod-rs-declarations-only, error) - CS-26
[arch]Reuse before reimplementing — if the logic plausibly exists (service clients, permission checks, oauth utils, theagentcrate), find it and reuse/extract it instead of writing a second copy. (#3692, #4020, #4380, #4485) - CS-27
[arch]Shared domain tables are only touched by their owning crate — e.g.entity_accessmutations go throughentity_access/entity_access_db_utils, never raw SQL elsewhere. (#3769) - CS-28
[arch]Don't extract single-use code into shared crates prematurely, and watch dependency direction: general-purpose crates must not import from specific ones. (#4410) - CS-29
[arch]Group proliferating root files (e.g. Dockerfiles) into a dedicated folder. (#4380) - CS-30
[api]Axum handlers take shared services viaState, notExtension. (#4556 · enforced: ast-greprust-no-axum-extension-param, warning) - CS-31
[api]Attach cross-cutting services to the owning domain service, not ad hoc at the router/handler layer — e.g.EntityAccessManagementServicehangs off the email/document service itself, the way the documents crate does. (#4572) - CS-32
[api]New API/soup models mirror the shape of their existing counterpart: omit fields derivable from a nested field, keep lazily-loaded collections lazy, include only fields relevant to the new context. (#4165) - CS-33
[api]Trait methods every implementor must consciously declare get no default impl (e.g. schema version) — defaults are for genuine defaults, not escape hatches. (#4276) - CS-34
[api]Design generic abstractions to mapT -> U, not justT -> T, when mapping is the point of the abstraction. (#4396) - CS-35
[api]Keep sibling endpoints on a resource using the same DTO shape; migrate them together rather than changing one in isolation. (#4386) - CS-36
[sec]Permission grants are stateless HTTP endpoints, not channel messages — in-memory channel flows don't survive reconnects. (#4201, #4296) - CS-37
[sec]Mint narrowly-scoped tokens instead of forwarding the user's full JWT downstream — least privilege by construction. (#4296) - CS-38
[sec]Tool responses must be valid members of the message chain — includetool_call_idand required chain metadata. (#4296) - CS-39
[sec]Pin third-party GitHub Actions to a commit SHA, not a movable tag. (#4276) - CS-40
[rust]Repeated literals become named consts. (#4020) - CS-41
[rust]Don't use#[allow(...)]— use#[expect(..., reason = "...")]on the narrowest item it applies to;allowsilently rots when the lint stops firing,expectwarns. (#4396, #4647) - CS-42
[rust]Don't re-state trait bounds already implied by a supertrait. (#4276) - CS-43
[rust]Use the smallest sufficient integer type — a version counter that can't plausibly pass 255 is au8. (#4396) - CS-44
[rust]Large inline strings belong in files — useinclude_str!. (#4156) - CS-45
[rust]CLI binaries useclap, not hand-rolled arg parsing. (#3678) - CS-46
[rust]Userootcausefor error handling in new code — it's preferred overanyhowthese days. In code that's still on anyhow, preferbail!for early error returns. - CS-47
[perf]Keep latency-critical services thin: push bytes directly instead of round-tripping through presigned URLs or extra services; dispatch non-blocking background work withwait_until. (#3781) - CS-48
[perf]Don't do per-message work on hot websocket paths — accumulate and flush on a timer/alarm. (#3961) - CS-49
[test]Tests live in a siblingtest.rs, not inline#[cfg(test)]blocks in the implementation file. (#4647 · example: Rust development) - CS-50
[test]Update affected tests for db-crate changes. Refresh SQLx metadata when queries/schema change or required cache data is missing, not for unrelated Rust-only edits. Run affected tests withSQLX_OFFLINEunset. (workflow: database development) - CS-51
[arch]Domain modules reference no infrastructure or transport: no AWS SDKs, redis, reqwest, opensearch, kafka, axum, or http types undersrc/domain/**— wrap clients in outbound adapters behind ports; response mapping lives in inbound. (enforced: ast-greprust-no-infra-in-domainwarning,rust-no-transport-in-domainerror · also: cloud-storage-hexagonal-architecture skill) - CS-52
[arch]Dependencies point inward:src/domain/**never importscrate::inbound/crate::outbound, andsrc/outbound/**never importscrate::inbound— define a port in domain and implement it in the adapter. (enforced: ast-greprust-domain-no-adapter-importswarning,rust-outbound-no-inbound-importserror) - CS-53
[arch]Inbound adapters run no database queries — handlers, tools, and listeners call a domain service backed by an outbound repository, never sqlx directly. (enforced: ast-greprust-no-sqlx-in-inbound, warning) - CS-54
[rust]New crates put#![deny(missing_docs)]inlib.rsand document all public items. Do not mark documentation code blocksignoreto skip doctests unless explicitly directed. - CS-55
[err]AI spans follow the OpenTelemetry GenAI conventions (gen_ai.*, seecrates/genai_telemetry). Agent sessions of every harness are traced in one place: the session actor projects each session's ACP frames ontoinvoke_agent/execute_toolspans (crates/agent_session/src/domain/session/telemetry.rs), so do not add GenAI spans to a harness runtime —agent_inmemruns its agent loop withwith_genai_telemetry(false)for that reason. Outside sessions (chat indocument_cognition_service), the agent loop andai_toolset::ToolSet::try_tool_callemit and enrich theinvoke_agent/chat/execute_toolspans; do not open tool spans in tools or toolsets, and never record prompts, tool arguments or tool results as span fields or logs — they are content, recorded on the spans under theOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENTpolicy and a size budget.
- FE-01
[data]Never call a service client outside thequeriespackage — UI code calling an endpoint directly is usually re-fetching data an existing query already caches. (#3750, #3961 · enforced: ast-grepts-no-service-client-outside-queries+tsx-no-service-client-outside-queries, warning · also: AGENTS.md) - FE-02
[data]Every query module has akeys.tsstructured like the existing ones. (#3710) - FE-03
[data]Conditional fetching uses a debounced signal passed to TanStack Query'senabled, not a custom resource. (#3961) - FE-04
[data]Pre-populating data means exposing a cache-seeding method on the query, not an ad hoc manual cache. (#4020) - FE-05
[data]Data-dependent UI that must not suspend usesqueryReadyGate. (#4077) - FE-06
[data]ExtendQUERY_FILTERS_BASEinstead of re-deriving filter exclusions per query, and prefer explicit include lists — exclude lists silently break when a new entity type is added. (#3947, #4260) - FE-07
[data]Don't hardcode backend-owned config in the frontend (e.g. system bot config) — fetch it dynamically or generate types from the backend source of truth. (#3692) - FE-08
[solid]No ad-hoc global state modules — shared state for a subtree lives in a Context scoped to a clear ownership boundary. (#3750 · also: AGENTS.md) - FE-09
[solid]Derive, don't sync — an effect that only copies one signal into another similarly-shaped signal should be a derived signal at the appropriate level. (#3898) - FE-10
[solid]createEffectis for external/imperative systems only (DOM APIs, third-party libs, navigation events) — never for deriving state; useon()to make dependencies explicit when an effect is warranted. Prefer wrapping the setter overcreateEffect(() => { if (signal()) sideEffect() })when setting a value should also clear related UI, blur a control, or scroll. (#3750, #3898, #6038 · also: AGENTS.md) - FE-11
[solid]Checksolid-primitivesbefore writing a custom reactive utility. (also: AGENTS.md) - FE-12
[async]async/awaitwithtry/catch, not.then()/.catch()chains. (#3716, #3781 · enforced: oxlintpromise/prefer-await-to-then—bun run lint:oxlintfrom the repository root) - FE-13
[async]Extract multi-step async coordination into named helper functions, and make intentionally-unawaited promises explicit — no bare floating promises. (#3781) - FE-14
[async]Keep neverthrowResult/ResultAsyncintact end-to-end — use the existing helpers (ResultAsync,throwOnErrorinqueryOptions,catchToResult) instead of ad hocPromise <-> Resultconversions. (#3781, #4373) - FE-15
[async]Guard once, at the top — missing/invalid state gets an early return at the start of the function or hook, not a repeated check in every branch or a silent fall-through. (#3750, #4057) - FE-16
[arch]Keep modules single-purpose: feature-specific logic out of generic util files, generic/collaboration logic decoupled from feature specifics, styling out of core business logic. (#3750, #3781, #3947) - FE-17
[arch]Reuse existing shared utilities and primitives before hand-rolling (removeNodeAndRestoreSelection, Kobalte components) — re-implementations reintroduce bugs that were already fixed. (#4281, #4321 · also: AGENTS.md) - FE-18
[arch]Within a package, use relative imports — don't route through the package's own barrel file; it adds indirection and invites circular dependencies. (#3692) - FE-19
[ts]Trustmatchnarrowing — an exhaustivets-patternmatch already narrows the type inside each closure; a manualExtract<>alias is redundant. (#4201) - FE-20
[ts]Exhaustive branching usesmatchfromts-pattern. (also: AGENTS.md) - FE-21
[ts]Noany— proper types orunknown+ type guards. (also: AGENTS.md) - FE-22
[ui]Pending or permission-gated actions render as a dimmed version of the real UI with inline accept/reject controls — not a generic placeholder icon. (#4201) - FE-23
[ui]Disclosure carets rotate 180° to point down when expanded, matching Discussion, SplitFileMenu, CollapsibleMessage, etc. (#4582) - FE-24
[ui]Truncated/collapsed controls get a tooltip (matching the app's tooltip pattern) so the lost label stays discoverable. (#4492) - FE-25
[ui]Semantic color tokens, not raw Tailwind palette classes — the default palette is disabled via--color-*: initialinapps/web/src/index.css, so classes liketext-red-500silently render nothing. (enforced: ast-greptsx-no-raw-tailwind-palette, CI error · also: AGENTS.md) - FE-26
[ui]Prefer composition over configurability; keep reusable components small and free of queries/complex state. (also: AGENTS.md) - FE-27
[ui]Don't addcursor-pointerto clickable elements. (enforced: ast-greptsx-no-cursor-pointer, warning · also: AGENTS.md) - FE-28
[ui]Dialogs rely on Kobalte's default autofocus: make the intended target the first tabbable element and preserve focus ownership for restoration. OverrideonOpenAutoFocusonly for a proven lifecycle requirement, and verifydocument.activeElementafter both opening and reopening in the live app. - FE-29
[data]Every new query call site (useQuery/useInfiniteQueryor a hook fromsrc/lib/queries/**) needs a deliberateSuspenseboundary: readingquery.datasuspends to the nearest ancestor boundary. When reviewing, walk up the component tree from the call site and determine which boundary would catch it. If none exists below the app root, or the nearest one is far outside the component's own UI scope (e.g. the route-level boundary inapps/web/src/routes/Root.tsx, whose fallback blanks unrelated UI), flag it and ask which boundary is intended. - FE-30
[ui]Never hoist Tailwind class strings into named constants (const DAY_CELL_CLASS = '...') — shared markup+styling is a component; a class string can't carry structure, props, or behavior. Extract a component, or inline the literal at its single use; styling variants are component props, not exported strings. (enforced: ast-greptsx-no-class-string-consts, warning) - FE-31
[ts]Never cast a string toItemType— parse it withstringToItemTypefrom@service-storage/client, the one owner of entity-type spellings (email threads alone are stored asemail,thread, andemail_thread). (#6043 · enforced: ast-grepts-no-item-type-cast+tsx-no-item-type-cast, CI error) - FE-32
[ui]Prefer styling in the component (Tailwind on the markup). Reserve@utilityinapps/web/src/index.cssfor styles widely shared across many components — not one-off or two-callsite layouts. (#6038 · also: apps/web/AGENTS.md) - FE-33
[arch]New features and feature restructures use the layered feature architecture, withfeatures/activityas the reference. Usecore/for pure feature types and functions,queries/for wire adapters and query factories,primitives/for reactive state/actions without JSX,components/for presentation from props, andviews/for use-case composition.context/holds feature-owned capability contracts and the provider/consumer, without production imports or fallback. An app-facing entry point constructs real adapters and supplies the provider; missing provider setup fails clearly. Meaningful reactive decisions depend on narrow feature sources, not concrete query adapters or raw clients. Adapters implement those contracts using existing query/cache infrastructure; Solid accessors and ownership remain part of the reactive foundation. Per-host behavior belongs in callback props, and flags gate mounting outside the capability contract. Primitives and adapters depend on feature-owned contracts; views depend on primitives and components, and core stays independent. Components may import context display types only. Keep data availability in source adapters and presentation decisions in primitives; avoid interfaces with no independent behavior to protect. Controllers receive only their named capabilities; production environments and presentation wiring stay in views. Reuse query/mutation infrastructure rather than adding a generic async wrapper. Keep request failures distinct from errors after a successful write, and require promises when callers depend on completion. Use view-state unions when the use case has distinct states; small helpers can return accessors. Test feature behavior through injected capabilities, with shared helpers intests/. Use fake sources for primitive unit tests and fake clients for adapter tests, retaining integration coverage of both. Activity still has combined context/production wiring, raw-client injection, and UI/import-side-effect stubs; these are documented migration gaps. The detailed document covers layer responsibilities, narrow reference exceptions, adoption, and review. Register adopting feature paths in both language variants of every feature rule. (enforced: ast-grepts-/tsx-feature-core-pure,ts-/tsx-feature-components-presentational,ts-/tsx-feature-data-no-ui,ts-/tsx-feature-layers-use-context, warning; currently scoped to activity. Separate production composition and source-contract inversion still require explicit review; the existing rules do not enforce them.) - FE-34
[arch]Treat the legacy block state system as adapter-boundary code, not state management for new or refactored surfaces. Do not add newuseBlock*,createBlock*, block-scoped signal/store/memo/resource, or block permission dependencies. An existing block adapter may read legacy state once and translate it into a feature-owned context or explicit props; descendants and reusable components receive identity, permissions, state, and lifecycle owners through those contracts and must remain mountable without<Block>. Never add a<Block>wrapper merely to satisfy a missing dependency. If shared code needs an owner or other host capability, expose it through the owning surface context instead of borrowingBlockContext. (also: apps/web/AGENTS.md)