Install in Claude Code
Copygit clone --depth 1 https://github.com/TryCaspian/caspian-sdk /tmp/taste && cp -r /tmp/taste/.cursor/skills/taste ~/.claude/skills/tasteThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Domain-first stores
Portable agent law for persistence, cache, policy documents, and similar ports.
If the repo has its own `AGENTS.md` overlay, read that after this file and
prefer project overrides for paths and house libraries.
## When this applies
Designing or changing shared domain surfaces: stores, registries, policy
resolve, CAS/publish, desired-set membership, or multi-service clients that
must not fork wire APIs.
## Taste (abstract)
1. **One fact, one place.** Vocabulary and policy live in small catalogs
(dimensions, fields, families). Everything else is **derived**. If adding
one concept means editing three sibling types (`FooConfig` + `FooContext` +
`FooDims`), delete the twins.
2. **Indexed ideas over parallel nouns.** Prefer catalog row + allowed dims →
context/result over re-authored per-family Structs. Types should cascade
from the catalog.
3. **HOF means shared ritual, not twin wrappers.** Three helpers that only
inject the same default are copy. Lift once when compose actually repeats;
otherwise **inline**. Prefer `decodeFamily(family, …)` over `decodeFoo` /
`decodeBar` / `decodeBaz`.
4. **Call sites stay obvious.** Pass raw fields; schema owns null/empty. Don’t
hide a one-liner behind a named helper. One context type: scalar or
non-empty array per dim; match with equality or ∈. Closed dims declare
enums on the registry; open dims stay free strings.
5. **Engine stays dumb; boundaries stay sharp.** Cascade/match with a simple
priority rule — no expand/desugar pass. Allowlists and IntelliSense live at
catalog / publish / family resolve — not a second “pass a Policy bag” API.
Resolve **on use** (ports); never freeze knobs at `create*` / `make*` boot.
6. **Challenge early, cut ruthlessly.** Prefer deleting a layer to polishing a
wrong one. Redundant Structs and family-specific context exports are smell,
not thoroughness.
7. **DX is part of correctness.** If IntelliSense doesn’t follow a new dim or
field, the type model failed — even when runtime works. Adding a dim =
registry row; adding a field = one catalog row (+ family key list if needed).
## Taste (call sites & policy)
1. **Call site stays clean.** Decode at the family/resolve boundary — not
scattered `??` / `=== "" ? null` / dual APIs for the same policy.
2. **Invariants at the boundary.** Decode once; the rest of the system only
sees a clean shape. Schema bounds are the ceiling — don’t re-clamp the same
knobs via env at boot.
3. **Resolve from real context.** Hot path uses identity dims that exist
(`fooId` / `bar` / `region` / …) against defaults + overrides — not a bare
env-only context when richer identity is available, and not a parallel
Policy injection. Example:
`resolve.foo({ env: "published", fooId, bar })`.
4. **Pass the whole port, not plucked fields.** Wire `resolve.foo` (or the
whole resolve bag) into consumers — don’t extract `fooLimit` at `main` /
`server` and pass a number.
5. **One concern, one return.** Boot/open helpers return the store (or one
domain value) — not `{ store, resolveEverything }`. Callers compose resolve
ports at the edge.
6. **Share the ritual, don’t twin it.** One module per compose; no
service-local copies of the same decode/match/apply.
7. **Defaults everywhere, scoped when stated.** Defaults are the base case;
dimensions/overrides only matter when specified.
8. **Ugly in adapters/scripts, not the server.** Seeds and fixtures are
scripts; empty required config → fail loud.
9. **Explain by flow, not jargon.** Show a concrete resolve example
(`defaults` + matching override → result).
10. **Live day one, minimal now.** Production-minded (version/revert, HA) but
ruthlessly cut scope. Ship the flexible core; defer authoring UI / extra
domains until needed.
11. **House schema library owns Encoded/Type.** Prefer derived
`FamilyContextEncoded<"foo">` over hand-rolled `{ fooId: string; … }` bags.
12. **Policy resolve drives the query — don’t filter the world in JS.**
Derive a **conservative** DB/search plan from the same resolve port
(probe closed dims like `bar` from the registry), push
`WHERE` / `ORDER` / `LIMIT` (or equivalent), then re-resolve with full row
context (`fooId`, …) only on the small result for open-dim overrides.
Example: `planFooLoad(resolveFoo)` → SQL filters + `LIMIT` → JS finish for
per-`fooId` keep/drop.
13. **No hardcodes when the row or closed enum already names it.** Don’t bake
`bar = 'qux'` into loaders that should follow the registry. Prefer
`bar IN (enabled…)` from resolve, or no `bar` predicate when the plan is
“all enabled bars.” Hardcodes fight multi-value dims and make admin
overrides lie.
14. **Prefilter is conservative; finish pass is exact.** Under-fetching because
the plan guessed wrong is worse than a slightly larger `LIMIT`. Closed-dim
floors/size from resolve are safe bounds; open dims (`fooId`) stay in the
post-load pass. Skip the query when every probed context resolves disabled.
## Hard rules
1. **Domain API first.** Public names describe product concepts
(`getFields`, `writeAll`, `publish`, `retain`), never wire commands
(`hgetall`, `INSERT`, `GetItem`) or vendor products in types, factories,
errors, or domain comments.
2. **Backends are swappable.** Runtime adapters (cache, SQL, HTTP, SDK) vs
in-process backends for unit tests. Do not bake wire commands into the Tag /
port interface.
3. **Service shape.** Prefer an explicit service interface + tagged errors.
Factories return the service / Layer — not `createVendorX(client)`.
4. **Wire only at the edge.** Adapters map infrastructure → domain. Scripts,
pipelines, and SDK calls stay inside adapters — not on public contract
exports’ names.
5. **One shared module per concern.** Do not fork the same store into two
services. Put it in a shared package (`packages/domain`, `packages/con