diff --git a/CLAUDE.md b/CLAUDE.md index 188f0928..a30c899d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -395,6 +395,28 @@ enough, so the next breakage is a prompt to check. in the locked input, then the file at that rev) or against `~/.agents/skills//` after a switch, and give the skill a fallback for anything that can lag +- **For a library, look inside the package before writing a skill for it.** + Effect v4 ships `AGENTS.md` and `ai-docs/` in its npm tarball, and the + Effect team's own `effect-ts` skill is only a pointer to them. A local API + reference would restate that content, and it would go stale at each + release. The repo's `effect` skill therefore carries what the package + cannot: a gate on the *installed* major version (the v3 tarball ships no + agent docs, and `effect-ts` starts with `pnpm add effect@latest`, which + since v4 GA on 2026-10-01 is an unrequested major upgrade in a v3 project; + during the RC the same step said `effect@rc`), plus Alyssa's Rust + mapping. Get version-specific claims from a typecheck against the real + package, not from memory: typechecking effect-playground against + 4.0.0-rc.117 found `Schema.Date`, a name that v4 keeps with a different + meaning, and a v4 name written from memory (`Schema.TaggedErrorClass`) + that did not exist. A source you trust needs the same check. The + playground's own comment (a `Data.TaggedError` field cannot be named + `cause`) went into the skill as a rule, and it was false: the renamed + field typechecks and passes the lesson's tests. Where the guides ship + also differs by project. Topcoat embeds its guides in the published crates + (`~/.cargo/registry/src/*/topcoat-/docs/`), but its `AGENTS.md` + index exists only in the repo, and `main` drifts from the release. The + `topcoat` skill reads the crate at the `Cargo.lock` version and the index + at the matching tag ### When nixpkgs lags: prefer the vendor's own Nix repo over NUR @@ -868,6 +890,8 @@ This document should evolve as patterns emerge. When you: --- *Last updated: 2026-09-29 - The scheduled flake update broke on `nix/build-skill` not existing: nix-skills was renamed to agents.nix, history rewritten and restructured. `lib/skills-sh.nix` now goes through upstream's public overlay, which the restructure made cheap (measured), instead of the internals it used to avoid a then-18s eval; supabase-postgres-best-practices is pinned by hand because the index dropped its repo* +*Last updated: 2026-09-22 - Added the `topcoat` skill (tokio-rs/topcoat, with the Effect <-> Topcoat pairing), pinned Rhys Sullivan's `effect-client-wrapper`, and reviewed create-epoch-app; corrected a `cause`-field rule the `effect` skill had copied unverified from effect-playground* +*Last updated: 2026-09-22 - Added the `effect` skill as a layer over the Effect team's pinned `effect-ts` / `effect-v3-to-v4` skills, and recorded the lesson: check whether a library ships its own agent docs in the package (Effect v4 does, v3 does not), gate on the installed version, and verify v3/v4 claims by typechecking against the real package* *Last updated: 2026-09-21 - Ghost (#56): ghost.build is shutting down, so venari now runs a server for its OpenAPI contract from the alycda/ghost fork, with the CLI packaged from that fork and driven by an env-setting wrapper; recorded the two client-side assumptions (`tsdb` dbname, viper's empty-env handling) that the "no CLI patch needed" plan missed* *Last updated: 2026-09-21 - Added "prebuilt binaries in a persisted `$HOME` are image-scoped state" to Tools Nix Can't Fully Manage, after a rustup toolchain in the devhome volume survived an image rebuild and left `cargo` erroring ENOENT for a loader that no longer existed — with the corollary that an activation step guarded on "is it installed" can never repair state that went bad in place (#80)* *Last updated: 2026-09-16 - Verified inspect against a real binary and found its declared install route could never have worked: the ataraxy-labs/tap formula pins a checksum upstream invalidated by moving the v0.1.1 tag, so the brew fails and would abort activation. Replaced it with `lib/inspect.nix` (release binary, tart-style). Two lessons, both already in the tap write-up and both nearly repeated: a tap's risk is its maintenance, so check the formula's age and hash before declaring it, not after; and a prebuilt binary that runs on *this* machine proves little — this one linked Homebrew's openssl by absolute path, so `otool -L` is part of verifying any fetched macOS binary* diff --git a/home-manager/modules/tools/agent-skills.nix b/home-manager/modules/tools/agent-skills.nix index 9855cef0..e607bb2e 100644 --- a/home-manager/modules/tools/agent-skills.nix +++ b/home-manager/modules/tools/agent-skills.nix @@ -36,6 +36,11 @@ # brag-doc - promo-packet impact entries from raw work notes # cf-now - private file sharing from Cloudflare R2 (pre-signed URLs) # commit-craft - commit-message craft + jj describe/push workflow +# effect - Effect (TypeScript) with a v3/v4 version gate and +# the effect-playground Rust mapping; carries no v4 +# API text, defers to the AGENTS.md the v4 package +# ships and to the external effect-ts / +# effect-v3-to-v4 skills # entity-level-git - Ataraxy Labs stack: sem (entity diff/blame/impact), # weave (semantic merge driver), inspect (review triage) # failure-doc - failures as deliberate-learning records @@ -50,6 +55,10 @@ # surfaces; carries no rule text, delegates to the # external asd-ste100-skill and reads CONCEPTS.md as # the project's technical-name dictionary +# topcoat - tokio-rs/topcoat (server-rendered Rust web apps): +# reads the guides shipped in the crates at the +# locked version, the two contracts the compiler +# does not check, and the Effect <-> Topcoat pairing # # One of them is vendored rather than authored: # hackmd-cli - upstream's own skill, extracted from the hackmd-cli.skill zip @@ -90,6 +99,7 @@ let "brag-doc" "cf-now" "commit-craft" + "effect" "entity-level-git" "failure-doc" "hackmd-cli" @@ -98,10 +108,14 @@ let "jujutsu" "power-of-ten" "ste100" + "topcoat" ]; externalSkills = { asd-ste100-skill = pkgs.skills-sh.asd-ste100-skill; + effect-client-wrapper = pkgs.skills-sh.effect-client-wrapper; + effect-ts = pkgs.skills-sh.effect-ts; + effect-v3-to-v4 = pkgs.skills-sh.effect-v3-to-v4; here-now = pkgs.skills-sh.here-now; supabase-postgres-best-practices = pkgs.skills-sh.supabase-postgres-best-practices; }; diff --git a/lib/skills-sh.nix b/lib/skills-sh.nix index 5468b35c..0f34b85d 100644 --- a/lib/skills-sh.nix +++ b/lib/skills-sh.nix @@ -26,6 +26,14 @@ nix-skills: final: prev: let github = (nix-skills.overlays.default final prev).agent-skills.github; + + # effect-ts/skills, pinned ahead of the index; see effect-ts below. + effect-skills = final.fetchFromGitHub { + owner = "effect-ts"; + repo = "skills"; + rev = "155c50c911f9c99c294ef8a4461981c5bbe6453e"; # 2026-10-09 + hash = "sha256-GSmGLOQSp4taxckrg7vfMTD/mbkfBYjv7oBsvDJdyck="; + }; in { skills-sh = { @@ -63,6 +71,48 @@ in name = "asd-ste100-skill"; }; + # https://www.skills.sh/effect-ts/skills + # The Effect team's own skills. Both are deliberately thin: `effect-ts` + # tells an agent to read node_modules/effect/AGENTS.md, which the v4 npm + # tarball ships alongside ai-docs/ (the v3 tarball ships neither), and + # `effect-v3-to-v4` drives a migration from the generated + # migration/v3-to-v4.md in Effect-TS/effect. The repo's `effect` skill + # (tools/agents/skills/effect) layers on both: a version gate, because + # `effect-ts` step 1 is `pnpm add effect@latest`, and in a v3 project that + # is an unrequested major upgrade; and the Rust mapping from + # effect-playground. effect-v3-to-v4 sets disable-model-invocation, so it + # only runs when invoked by name. + # + # Pinned here by hand, ahead of the index. agents.nix 0b4d80e still has + # 2309e6f, written while v4 was a release candidate: its `effect-ts` says + # `pnpm add effect@rc`, which now installs 4.0.0-rc.118, older than the + # stable 4.0.x on `latest`; and its `effect-v3-to-v4` sends imports to + # `effect/unstable/*` paths that v4 GA removed with no compatibility + # exports. 155c50c ("align Effect skills with v4 GA", 2026-10-09) fixes + # both. One fetch serves both skills, so they cannot drift apart. When + # the index reaches 155c50c or later, switch back to + # github.effect-ts.skills. and delete the pin. Until then + # `nix flake update nix-skills` does not move it; bump rev + hash by hand. + # The hash was computed without nix (a NAR serialization of `git archive + # `), a method that reproduced the index's hash for 2309e6f and the + # supabase pin below exactly; `nix flake prefetch + # github:effect-ts/skills/` is the normal way. + # + # Subdirectories, no builder: neither skill has symlinks, and each + # SKILL.md name already matches its deploy directory. + effect-ts = "${effect-skills}/skills/effect-ts"; + effect-v3-to-v4 = "${effect-skills}/skills/effect-v3-to-v4"; + + # https://www.skills.sh/rhyssullivan/skills/effect-client-wrapper + # Wrap a Promise-based SDK as an Effect service behind one `use(fn)` that + # maps rejections to a tagged error and opens a span. By the author of + # create-epoch-app. Its code is Effect v3 (Context.Tag, a static Default + # layer, Schedule.intersect); the repo's `effect` skill says to keep the + # pattern and translate the code on v4 rather than copy it. From + # rhyssullivan/skills (indexed at ab3a455), not the older standalone + # rhyssullivan/effect-client-wrapper-skill repo the index also carries. + effect-client-wrapper = github.rhyssullivan.skills.effect-client-wrapper; + # https://www.skills.sh/supabase/agent-skills/supabase-postgres-best-practices # Postgres performance/schema/RLS guidance across 8 priority categories. # diff --git a/tools/agents/skills/effect/SKILL.md b/tools/agents/skills/effect/SKILL.md new file mode 100644 index 00000000..6d69448c --- /dev/null +++ b/tools/agents/skills/effect/SKILL.md @@ -0,0 +1,224 @@ +--- +name: effect +description: > + Write, read, review, or explain TypeScript that uses the Effect library + (`effect` package: Effect.gen, Layer, Context, Schema, Scope, Fiber, + Schedule). Trigger when a file imports from "effect" or "effect/*", when + package.json lists `effect`, or on "effect-ts", "effectts", "Effect v4", + "typed errors in TypeScript", or "how does this map to Rust". Detects the + installed major version first, because v3 and v4 code do not compile + against each other. On v4 it defers to the guidance the package ships + (node_modules/effect/AGENTS.md); on both versions it adds a Rust + translation table from Alyssa's effect-playground. Carries no v4 API + reference of its own. Does not migrate: a v3-to-v4 upgrade is the pinned + effect-v3-to-v4 skill, run only when Alyssa asks for it. +--- + +# Effect + +This skill is a thin layer. The rules for writing v4 Effect code come from the +Effect team, not from here. The skill adds three things upstream does not +have: a version gate, the Rust mapping from +[effect-playground](https://tangled.org/alycda.tngl.sh/effect-playground), and +the v3/v4 traps found by typechecking that playground against v4. + +## Step 0: find the installed version + +Do this before you write a line of Effect code. Do not infer the version from +examples in your memory or on the web; most of both are v3. + +```sh +node -p "require('effect/package.json').version" # resolved version +grep '"effect"' package.json # declared range +``` + +| Result | Do this | +|---|---| +| `4.x` | Read `node_modules/effect/AGENTS.md` completely, then follow its links into `node_modules/effect/ai-docs/src/`. For an API it does not cover, search `node_modules/effect/src/`. Use the v4 column below. | +| `4.0.0-rc.N` / `-beta.N` | A pre-GA build. Treat it as v4, and also apply the import-path trap below: a pre-GA install has the `effect/unstable/*` paths that GA removed. Say that it is a pre-release, and do not upgrade it unasked. | +| `3.x` | The v3 package ships no `AGENTS.md`. Search `node_modules/effect/src/` for signatures. Use the v3 column below. Do not copy code from v4 docs, from `AGENTS.md` in another project, or from the pinned `effect-ts` skill; it will not compile. | +| not installed, new project | v4: npm `latest` is 4.x, the Effect team's skill installs it, and it ships agent docs in the package. v3 needs a stated reason, for example code that must match an existing v3 codebase. Say which one you chose in one line. | +| not installed, existing code | Find out why before you install anything. | + +As of 2026-10-16: npm `latest` = `4.0.5` (4.0.0 went out 2026-10-01), `prev` = +`3.22.3` (v3 still gets releases), and `rc` has stayed at `4.0.0-rc.118` since +2026-09-28, behind the stable releases. Re-check with `npm view effect +dist-tags` before you state it. + +**Do not upgrade the major version unasked.** The pinned `effect-ts` skill +(`~/.agents/skills/effect-ts/`) starts with `pnpm add effect@latest`. Since +`latest` became 4.x, that command is a v3-to-v4 migration in a v3 project. +Use `effect-ts` only to set up a v4 repo. In a v3 project, add or update with +a range, not a tag. `npm install effect` and `pnpm add effect` with no version +keep the `^3` range already in `package.json`; `effect@latest` rewrites it to +`^4` (checked with npm 10 and pnpm 10 on 2026-10-16). Use `effect@3` when you +need to name it. For a migration, Alyssa runs `/effect-v3-to-v4`, which has +`disable-model-invocation: true` and so never starts automatically. + +If `~/.agents/skills/effect-ts/` does not exist, the v4 path still works: +`node_modules/effect/AGENTS.md` is the content that skill points at. + +## Rust translation + +Same table as the playground README, with a v4 column. Every v4 entry is either +unchanged or resolved against `migration/v3-to-v4.md` in Effect-TS/effect. On +2026-10-16, every v4 name in this table and in the trap list below was +re-typechecked against the stable `4.0.5`. The entries marked "checked" were +also run, first on `4.0.0-rc.117` and again on `4.0.5`, with the same results. + +| Rust | v3 | v4 | Lesson | +|---|---|---|---| +| `Result` | `E` in `Effect` | same | 00, 01 | +| `?` | `yield*` in `Effect.gen` | same; `Effect.fn("name")` for reusable functions | 00 | +| `enum Error` + `thiserror` | `Data.TaggedError` | `Data.TaggedError` still exists; `AGENTS.md` prefers `Schema.TaggedError()("Tag", {...})` (checked) | 01 | +| `match` on the error enum | `Effect.catchTags` | same (checked) | 01 | +| catch-all `Err(_)` | `Effect.catchAll` | `Effect.catch` | 01 | +| `Result` as a value | `Effect.either` → `Either` | `Effect.result` → `Result` | 01, 06 | +| `panic!` | defect (`Effect.die`) | same; `Cause.isDie` → `Cause.hasDies` | 01 | +| `Drop` / RAII | `Scope` + `Effect.acquireRelease` | same | 02 | +| `std::thread::scope` | `Effect.scoped` | same | 02 | +| trait / `&dyn Trait` | `Context.Tag("id")()` | `Context.Service()("id")`, a structural rewrite and not a rename (checked) | 03 | +| impl at the call site | `Layer` + `Effect.provide` | same; build the value with `Service.of({...})` | 03 | +| `tokio::spawn` | `Effect.fork` | `Effect.forkChild` (checked); `forkDaemon` → `forkDetach` | 04 | +| `tokio::select!` | `Effect.race` | same | 04 | +| `Semaphore` over a task set | `{ concurrency: n }` | same | 04, 06 | +| `serde` derive | `Schema.Struct` | same | 05 | +| `#[serde(with = ...)]` | `Schema.Date` (string → `Date`) | **`Schema.DateFromString`**; see the traps below | 05 | +| `backoff` crate | `Schedule.intersect`, `whileInput` | `Schedule.max([...])`, `Schedule.while`; see the traps below | 06 | +| `Arc>` | `Ref` | same | — | +| `impl Iterator` | `Stream` | same | — | + +For a symbol that is not in this table, do not guess the v4 name. Search +`node_modules/effect/src/` first. For a v3 name, look it up in the generated +migration reference. The package does not ship that reference, so if +`.repos/effect` does not exist, fetch only the reference (6.5 MB) with: + +```sh +git clone --depth 1 --filter=blob:none --sparse https://github.com/Effect-TS/effect .repos/effect +git -C .repos/effect sparse-checkout set migration +rg -n '^- `Effect\.catchAll`' .repos/effect/migration/v3-to-v4.md # one symbol at a time +``` + +Never read `v3-to-v4.md` whole; it is about 17,000 lines. The other search +recipes are in `~/.agents/skills/effect-v3-to-v4/SKILL.md`; they expect this +same `.repos/effect` path, so they work after the clone above. This table was +checked, and one guess still got through: `Schema.TaggedErrorClass` does not +exist. + +## v3 → v4 traps that a rename table hides + +These come from typechecking effect-playground (v3, strict tsconfig) against +v4: first `4.0.0-rc.117`, then `4.0.5`, which produced the same 71 errors. +They matter when you read v3 code while you write v4 code, or the other way +round. + +- **Pre-GA import paths: `effect/unstable/*` is gone.** Before 4.0.0, modules + such as http, rpc and ai lived under `effect/unstable/*`. At GA they moved to + plain paths (`effect/unstable/http` → `effect/http`), with no compatibility + exports: on `4.0.5` the old path fails with TS2307 (checked). Code, examples + and answers written against a v4 beta or RC use the old paths, so drop the + `unstable` segment. Moving a module did not make its API stable. Stability is + now a JSDoc tag: `@stability unstable` APIs can break in a minor release, and + `@stability experimental` APIs in a patch release. + +- **Same name, different meaning: `Schema.Date`.** v3 `Schema.Date` decodes an + ISO string to a `Date`. v4 `Schema.Date` is the identity schema for a `Date`, + and it rejects the string (the decode returns `Failure`; checked). The v3 + meaning is now called `Schema.DateFromString`. Code that keeps the v3 name + still compiles and fails at runtime on every real payload. Lesson 05 exists + to teach this boundary. +- **Different shape: `Schedule.intersect` → `Schedule.max`.** v4 `max` takes an + array (`Schedule.max([a, b])`), so it cannot go in a `.pipe(...)` chain, and + it outputs a `Duration`, not a tuple. v3 `whileInput(pred)` becomes + `Schedule.while(({ input }) => pred(input))`. Lesson 06's backoff, ported + this way, still makes 5 attempts on a persistent transient error and 1 on a + permanent one (checked). +- **`Cause` is no longer a union you branch on.** `isFailType`, `isDie`, and + the other variant guards now apply to entries of `cause.reasons` + (`Cause.isFailReason`), or become cause-level predicates (`Cause.hasDies`). + `Cause.TimeoutException` → `Cause.TimeoutError`, and the `_tag` + discriminant changes with it. +- **Schema filters became checks.** `Schema.int()`, `between`, and `pattern` + become `isInt`, `isBetween`, and `isPattern`, applied with `Schema.check` or + a schema's `.check`. `Schema.annotations` → `Schema.annotate`. + `Schema.decodeUnknown` → `Schema.decodeUnknownEffect`. `ParseResult` is + gone. `Schema.Schema.Encoded` has no counterpart, so infer the type from the + constructor. +- **Misleading cascade under `exactOptionalPropertyTypes`.** One unresolved + symbol makes `R` infer as `unknown`, and `runPromise` then reports TS2379: + "`Effect` is not assignable to `Effect` ... Consider adding 'undefined'". Do not add `undefined`. Fix the + first TS2339/TS2551 in the file, and the TS2379 goes away. + +## Writing rules for both versions + +- An `Effect` is a description. Building one runs nothing. Retry, timeout, and + interruption are combinators because the value can run twice or never. +- Use `Effect.gen` for sequential logic. Use `.pipe(...)` to decorate one + effect (retry, timeout, catch). On v4, use `Effect.fn("name")` for a + reusable function, and pass decorators as extra arguments, not `.pipe` + (see `AGENTS.md`). +- To fail from a generator, write `return yield* new MyError(...)`. Without the + `return`, TypeScript thinks execution continues. +- Expected failures go in `E` as tagged errors. Broken invariants are defects + (`Effect.die`, `orDie`) and stay out of `E`. A payload field named `cause` + is fine, including `readonly cause: unknown`, on v3 and v4 (checked: + 3.22.2, rc.117 and 4.0.5). The playground's lesson 06 comment says it conflicts + with `Error.cause`. It does not: renaming that field to `cause` still + typechecks and passes 6/6 tests. Do not repeat that comment. +- Ask for a dependency with `yield* Service` inside the function. Do not pass + it in as an argument. The requirement then shows in `R`, and a missing + Layer is a compile error. Pass a service as an argument only at a non-Effect + boundary. (create-epoch-app's `effect-dependency-injection` rule; Topcoat's + "functions, not middlewares" is the same rule without the compile-time + check, see the `topcoat` skill.) +- Decode untrusted input with `Schema`. Do not cast. +- Keep the playground's tsconfig strictness: `strict`, + `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`. Without `strict`, + the error channel can degrade to `any`. +- Do not use `as` or `any` to silence an Effect type error. The error usually + means the shape is different, and a cast deletes that information. + +## Verify + +Done means the code typechecks and runs. Hover inference alone is not proof. + +```sh +npx tsc --noEmit # or the project's typecheck script +npx tsx path/to/file.ts # run it; Effect code that typechecks can still fail at runtime (Schema.Date) +``` + +In effect-playground itself, `npm run check && npm test` is the gate. Lesson 06 +calls a live API, so CI does not run it. + +## Hand-offs + +- Set up a new v4 repo: the pinned `effect-ts` skill. +- Migrate v3 → v4: `/effect-v3-to-v4`, and only on request. That skill forbids + compat shims and casts. This one agrees. +- Wrap a Promise-based SDK (Stripe, Resend, AWS) as a service: the pinned + `effect-client-wrapper` skill (Rhys Sullivan). Its code is v3: + `Context.Tag` and a hand-written `static Default` layer, and a + `Schedule.intersect` retry. On v4, keep the pattern (one `use(fn)` that + turns a rejected promise into a tagged error and a span) and convert the + code with the table above. +- The same app in Rust, or a comparison of a full-stack Effect design with + one: the `topcoat` skill, which holds the Effect ↔ Topcoat pairing. +- Rust→WASM through Effect (playground lesson 07): the Rust side is ordinary + FFI work, so `power-of-ten` applies to any `unsafe` in it. + +## Reference projects, and what not to take from them + +- **effect-playground** (Alyssa's): the Rust lens above. v3, strict tsconfig. +- **create-epoch-app** (github.com/RhysSullivan/create-epoch-app): a + full-stack starter (Effect + Convex + Next.js + effect-atom, bun). On + 2026-09-22 it was on Effect `^3.19.8`, and its last commit was + 2026-01-11. Its agent rules are Cursor `.mdc` files. The dependency rule + above comes from them. Do not import two of the others unchanged: + `effect-primitives.mdc` prefers `Either` and `catchAll`, which v4 renames, + and it prefers `Chunk`/`HashMap`/`List` over native collections by + default. That is a style choice: it costs a conversion at every interop + boundary, and the rule gives no benchmark for it (Alyssa's review, not an + upstream position). `never-use-as-any` + also bans `unknown`, but `Schema.decodeUnknown*` takes `unknown` input by + design. The ban on `as` and `any` is right. The ban on `unknown` is not. diff --git a/tools/agents/skills/topcoat/SKILL.md b/tools/agents/skills/topcoat/SKILL.md new file mode 100644 index 00000000..913a18bc --- /dev/null +++ b/tools/agents/skills/topcoat/SKILL.md @@ -0,0 +1,135 @@ +--- +name: topcoat +description: > + Write, read, or review a web app built on Topcoat (tokio-rs/topcoat), the + server-rendered Rust framework: `view!`, `#[component]`, `#[page]`, + `#[layout]`, `#[procedure]`, `#[shard]`, `live!`, `signal`, `app_context`, + `Cx`, `topcoat dev`, `topcoat ui`. Trigger when Cargo.toml depends on + `topcoat`, on "topcoat", or when Alyssa compares a full-stack Effect app + (create-epoch-app, Confect, effect-atom) with its Rust equivalent. Finds + the installed version first and reads the guides that ship inside the + crates at that version. Carries no API reference of its own. Adds the + Effect ↔ Topcoat pairing and two contracts the compiler does not check. + Not for work inside the Topcoat repository itself, which has its own + contributor skills in `.agents/skills/`. +--- + +# Topcoat + +Topcoat is pre-1.0; its README says "Early-stage and experimental. Expect +breaking changes." And the guides on `main` drift from the release (13 guide files +changed between v0.8.1 and `main` in the nine days after the tag). Read the +docs for the version in `Cargo.lock`, never `main`. + +This skill is a thin layer. The guides are upstream's. It adds a version gate, +two runtime contracts that the compiler does not check, and the pairing with +the Effect stack, which is where Alyssa is coming from. + +## Step 0: version and docs + +```sh +grep -A1 '^name = "topcoat"$' Cargo.lock # resolved version +rustc --version # 0.8.1 needs >= 1.98 +ls -d ~/.cargo/registry/src/*/topcoat-*/docs # every crate that has guides +``` + +The guides are embedded in the published crates (`include_str!` into the API +docs), so after a `cargo build` they are on disk at the resolved version, in +three kinds of crate (the `ls` above lists all of them): +`topcoat-/docs/` holds the facade guides (router, context, app_context, +runtime, session, ui, …); `topcoat-router-/docs/` holds the router's own +(content types, errors, module_router, tower); and +`topcoat--macro-/docs/` holds the macro references (`view.md`, +`live.md`, `procedure.md`, `shard.md`, …). The repository keeps macro docs at +`crates//macro/docs/`; that path does not exist in the registry. +`cargo doc --open -p topcoat` renders the same text. + +The guide *index* is `AGENTS.md` in the repository, and it does **not** ship in +the crate. Read it at the matching tag: +`https://github.com/tokio-rs/topcoat/blob/v/AGENTS.md`. Its "Project +structure" section and the `.agents/skills/` it references are for Topcoat +contributors; only the "Documentation" index applies to an app. + +Before you write code in an area, read its guide. Start with +`functions_not_middlewares.md`; it is the framework's design argument, and +the rest assumes it. + +## Contracts the compiler does not check + +**`app_context::(cx)` is a runtime lookup.** It is keyed by `TypeId`. A type +that was never registered with `.app_context(value)` compiles, then panics on +every request that reaches the call. Topcoat catches the panic and responds +500; other routes keep serving (checked on 0.8.1: the page returned +`internal server error [500]` and the log named the missing type). So: + +- Register every app-context value in the one `Router::builder()` chain, and + treat that chain as the list of what the app depends on. +- Use `try_app_context::(cx)` when a value is optional; it returns `None`. +- A green `cargo build` says nothing about registration. Smoke-test by + requesting each route once. This is the same lesson as "passing activation + is not proof a tool works" in this repo's CLAUDE.md. +- One value per type. Registering a type twice panics at startup; wrap in + newtypes (`struct PrimaryDb(Database)`). + +**Procedures and shards are public HTTP endpoints.** Their arguments come from +the browser and "must not be trusted" (upstream's words, both guides). Do the +auth check inside the body with a `cx` function (`require_auth(cx).await?`), +not by assuming the page that renders the button was guarded. A procedure's +`Err` becomes an error response that the calling expression cannot observe; +if the browser must react to a failure, return it as data +(`Result>`). + +## Coming from Effect: the pairing + +create-epoch-app (Rhys Sullivan: Effect + Convex + Next.js + effect-atom, on +Effect 3.19, last commit 2026-01-11) is the full-stack Effect shape; Topcoat is +the nearest Rust shape. Rows marked "checked" were run; the rest come from the +0.8.1 guides and the epoch-app source. + +| Concern | create-epoch-app / Effect | Topcoat 0.8.1 | +|---|---|---| +| Ask for a dependency | `const db = yield* Database`; epoch-app's rule is "prefer `yield*` over passing deps as arguments" | `app_context::(cx)` inside a small `fn(cx: &Cx)`; "functions, not middlewares" is the same rule | +| Missing dependency | **Compile error**: the service stays in `R`, and `runPromise` rejects `Effect` (checked on 4.0.5: TS2345, or TS2379 under `exactOptionalPropertyTypes`) | **Runtime panic → 500** on that request only (checked) | +| Wire implementations | `Layer` + `Effect.provide`; swap a Layer for tests | `Router::builder().app_context(value)`; swap the value in a test router | +| Request-scoped dedupe | no one-to-one; `RequestResolver` dedupes and batches | `#[memoize]` on a `cx` function, per request | +| Error type | typed `E` channel, tagged errors, `catchTags` | `topcoat::Error` wraps `anyhow::Error` (type-erased); router error types map to status codes, anything else is 500. Keep `thiserror` enums in the domain and convert at the handler | +| Auth guard | Better Auth on Convex; a service you `yield*` | `topcoat-session` (bring your own storage) plus `require_auth(cx)` functions called where the data is used | +| Server call from UI | Confect RPC (`factory.query/mutation`), typed errors cross the wire via Schema | `#[procedure]` async fn called in a runtime expression; `Err` is opaque to the client | +| Client state | effect-atom (`useAtom`, `useAtomValue`) | `signal(cx, init)` + runtime expressions (`@click`, `:value`) | +| Re-render on input change | atom-derived React component | `#[shard]`: the browser tracks the signals, the server re-renders, the DOM swaps | +| Slow data | React Suspense | `live!` / `emit!` stream regions into the same response; `suspense`/`error_boundary` components | +| Push after load | Convex live queries | no DB subscription; `Sse` response (`sse` feature), or Datastar over SSE | +| Test database | `convex-test` (in-memory Convex) | a Ghost fork per test run or agent task (`ghost fork seeded run-42`) on venari's Ghost server. Ghost is the *test* database, not the Convex replacement: its databases share one role, use unverified TLS, and are disposable by design (`tools/venari/README.md`, "Ghost") | +| Validate input | `Schema.decodeUnknown` | `FromRequest` extractors (`content.md`) and serde | +| UI kit | `@packages/ui` (shadcn / Radix) + Tailwind via Node | `topcoat ui` copies component source from a registry (the shadcn model) + standalone Tailwind CLI, no Node | +| Client build | Next.js bundle, bun, turbo | none: no wasm, no JS build step | +| Tracing | `Effect.withSpan`, `@effect/opentelemetry` | a router layer (tower), which the guide reserves for transport concerns like tracing | + +The row that decides most design arguments is the second. Effect moves the +"did you wire it" check into the type system; Topcoat keeps it a startup and +request-time invariant, and buys a smaller, plainer API with it. Say which +side of that trade a design relies on. + +The "Server call from UI" row is where the two differ most on errors: a +Confect endpoint declares an error schema, so the client gets a typed +failure; a Topcoat procedure cannot deliver one unless the error is part of +the `Ok` value. + +## Tooling + +- `topcoat dev` (from `cargo install topcoat-cli`): build, watch, restart. + Pages that include `topcoat::dev::script()` reload themselves. +- `topcoat fmt`: formats macro bodies (`view!` and others) alongside + `rustfmt`. Run it before you commit view code. +- `HOST` / `PORT` set the bind address; the default is `127.0.0.1:3000`. + +## Unsafe + +Topcoat's own workspace sets `unsafe_code = "deny"`. Suggestion, not a +requirement: an app on it rarely needs `unsafe` either, so the same lint in +`[lints.rust]` costs nothing. Where FFI does need it, `power-of-ten` applies. + +## Hand-offs + +- The Effect side of any comparison: the `effect` skill (version gate first). +- A third-party SDK wrapped as an Effect service: `effect-client-wrapper`.