Skip to main content
ClaudeWave
Install in Claude Code
Copy
git clone --depth 1 https://github.com/TryCaspian/caspian-sdk /tmp/taste && cp -r /tmp/taste/.cursor/skills/taste ~/.claude/skills/taste
Then start a new Claude Code session; the skill loads automatically.

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