Doctrine for designing and evolving any **SDK** Grida ships — TypeScript, Rust, or otherwise. "SDK" here means a surface that crosses a foreign-or-foreign-treated boundary: published packages, separately-versioned consumers, FFI bindings, public-by-design modules. An SDK's job is to refuse; a strict, honest surface rejects the wrong contents and keeps the package testable in isolation. Default is "core, not customizable"; customization is the exception, defended by a deciding table. Use when authoring or evolving any such surface — `@grida/*` published packages, engine crates (gridaco/nothing `crates/*`) published or FFI-exported, intent/message vocabularies, any contract a second author will compile against. Internal-only helper packages are welcome to follow, not forced. Companion skill for two-sided contract work: $sdk-seam. Critique partners: $pedantic, $etiology. Related: $naming.
72
88%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This is not a style guide. Style and language-specific code shape are downstream (see $code-ts, $code-react for the TS sides). This is about what an SDK refuses to do — the discipline that keeps a package small, legible, and replaceable, regardless of language.
An SDK lives or dies by what it refuses to expose. Default is core; customization is the exception. Every public knob is a contract you cannot retract without a semver break and a coordinated migration across every downstream call site.
A library with too few knobs is easy to grow. A library with too many is impossible to retire. The asymmetry is brutal — design from it.
This doctrine applies whether the package ships as an npm scope, a crate, a header-only library, a WASM module, a Python wheel, a hosted service with an SDK, or a pair of microservices defining a shared message vocabulary. The mechanics of "publish" differ; the discipline of "refuse the wrong contents" does not.
This is the gate. The skill says "SDK," not "package," because the two are different. An SDK is:
npm, crates.io, PyPI); linked by a separately-versioned consumer (a desktop binary against a crate, a generated WASM/FFI binding); or authored as if a foreign consumer existed even if one doesn't yet (any package whose README documents it as a public surface, anything tagged for publication, anything in a *-hosted suffix family).What this excludes — where the doctrine is welcome but not load-bearing:
packages/. Adopt the parts of this skill that pay; skip the rest without apology.crates/. Same logic.Don't extend the doctrine to internal-only utilities just because the file structure looks like an SDK. The discipline costs something — designed views over raw streams, anti-goals as perimeters, promotion-on-dogfooding — and that cost is paid by the foreign-callers it protects. If there are no foreign callers (now or planned), the strictness doesn't pay back.
sdk-seam triggers on the same gate from the other angle: any
boundary that meets the SDK bar above, where the same author
writes both sides. Include FFI bindings to internal crates here —
binding regeneration cost makes the boundary foreign-treated even
when the crate is same-repo.
When a new decision lands — "should this be a provider hook? a built-in toggle? a sibling package? a public type or an internal seam?" — walk these in order. First match wins.
| Question | If yes → | Why |
|---|---|---|
| Would customization let a consumer break the invariant this package exists to protect? | Core, non-customizable | Sovereignty |
| Is this genuinely a host-owned concern (I/O, locale, surface, credentials, clock)? | Provider at construction | Host knows what you can't |
| Is this per-variant edit/parse/render semantics, complex but bounded by a spec or schema? | Internal seam, no public API | Code organization, not API |
| Does the candidate have ≥2 internal consumers AND can be tested without mounting the SDK? | Separate layer (own module/package) | Earned its separation |
| Have ≥2 internal consumers shaped the contract already? | Eligible for public | Public only after dogfooding |
| Otherwise | Core, internally modular | Default-in, not default-out |
The third rung — "complex but internal" — is where most "extension-point" mistakes get caught. A real spec or schema (SVG element table, MIDI event types, OpenType tables, USB device classes) is the registry; the SDK implements against it. Don't re-invite the spec to be re-implemented at runtime by consumers.
The public observation surface is designed, not raw. It exposes purpose-built views — selection, mode, dirty/version, computed property — each handling multi-target, capability variance, and bookkeeping internally. Consumers never receive raw input events, reducer actions, or internal state frames.
If a needed view doesn't exist, that's an API gap to close, not an internals hatch to open. Exposing the internal stream because "the consumer can compose it themselves" is how you wake up six months later unable to refactor the core.
The same rule applies to the other direction: emit named outcomes
(intents, commands, requests), not "the user moved their pointer."
If your outputs carry phase markers (preview / commit, begin /
progress / end), the consumer wraps history/transactions without
guessing internal state.
One-directional dependency, layered:
primitives / math ← logic core ← adapter shell ← hostThe math/logic core has no I/O, no DOM, no canvas, no UI runtime,
no global clock. Plain function over plain inputs returns plain
output. Runnable under the language's basic test runner with zero
mocks. A Rust crate's core compiles under no_std where feasible;
a TS package's core has no window / document import; a Python
package's core does not touch the filesystem.
The shell is a thin wire: lifecycle, draw loop, host wiring. Its own logic should be trivial enough to verify by inspection, because it's the part you can't test headlessly.
Why this matters: when a shell grows logic, that logic ships
unguarded. Common failure: the shell holds a switch (render,
dispatch, route) and a new core variant is added without
updating the switch — the core's tests pass; the shell silently
drops the variant; downstreams hit it in production. Push logic
into the core. Tests follow.
Don't conflate outputs that exist to satisfy different constraints. Every paired-but-asymmetric surface — render vs. hit-test, read vs. write, declared vs. computed, preview vs. commit, encode vs. decode — earns its asymmetry from a real disagreement in requirements. When you find yourself unifying them "for elegance," you're about to break one.
Concrete pattern: a UI surface that draws and hit-tests as two separate outputs. Drawing optimizes for legibility at any zoom; hit-testing optimizes for Fitts'-reach (fat targets, virtual regions that extend past the visible shape). Collapsing them — sizing the visual to match the hit AABB, or shrinking the hit region to match the visual — breaks one of the two; each side has to compromise to satisfy the other.
The generalization: tests assert each side separately, and — where they intentionally differ — assert the direction of difference (e.g., the hit region strictly contains the rendered bbox).
Every published SDK ships an explicit Anti-goals section in its README. It is not aspirational; it is the perimeter that lets the package stay small. Examples that have already prevented bloat across various Grida packages:
When a feature request arrives, the first question is which anti-goal it would violate. If it violates one, the right answer is "this is the wrong tool." If it threatens one without crossing it, write the anti-goal sharper.
Adding an anti-goal is the cheapest design work an SDK author does.
See $naming for the full treatment. The SDK-specific corollary:
git mv; published-name rename is a coordinated downstream migration. Invest heavily before a name escapes its file.Surface, Encoder, Intent, Paint in a package asserts "nothing else competes for this slot here." If a peer could be added later, qualify now.<consumer> for <feature>" is leaking the consumer's problem into the contract. The field name should justify itself in producer-only terms.Internal seams stay internal until ≥2 internal consumers have shaped the contract. This is not a bureaucratic gate — it's the only way to avoid public APIs designed against one use case.
Promoting too early produces:
Promoting too late costs little. Internal callers reach into internals; you tighten when the second consumer arrives. Default direction of pressure is inward, not outward.
When you do promote, the contract test is: "could a stranger build the next caller against this API alone, without reading the SDK's source?" If no, it's not promoted; it's exposed.
For very new packages without a second internal consumer yet, the honest move is to mark the surface as unstable in its README ("v0.x.y — no compatibility guarantees") and let the second consumer's needs shape the contract before locking it.
For any extensibility request, walk this ladder. Reach down only when the rung above doesn't fit.
What's absent from this ladder is a generic plugin / widget / middleware registry. That's the point. A registry is the path that turns small packages into god-classes — the lesson is repeated across the industry (jQuery plugins, Babel plugins of the early era, Webpack loaders) and locally (the Grida main editor's 6,800-line god-class grew partly from this).
For an SDK, tests carry double weight:
Discipline: every default behavior is locked by a test whose description names the behavior in plain language. The test name is the spec. The body proves the code obeys it. A comment above explains why — the design intent that the code itself can't carry.
Where applicable, embed scenario names verbatim in test text so "did we drop a rule?" is grep-able across implementations and ports. This matters most for SDKs that ship parallel implementations (TS + Rust + WASM bindings) of the same contract.
A PR that touches a public behavior without touching the matching test is a smell; a PR that flips a test's assertion without changing the test name is a near-certain regression.
$pedantic — before drafting a public API, run the design through pedantic. The probes for unfalsifiability, vague quantifiers, and assumed-bedrock catch the "this feels finished but isn't grounded" failure mode that produces APIs you can't retract.$etiology — before patching across an SDK boundary, walk the diagnostic ladder. Most "quick fixes" at a boundary are API-contract bugs (rung 3), not call-site bugs (rung 2). Treating one as the other is how contracts rot.Work that touches more than one SDK — your producer and its
consumer, two sibling packages, a published surface and its tests,
two crates on either side of an FFI boundary — has a specific
failure mode of its own: when you control both sides, you shotgun
changes across them in a single edit and the contract silently
degrades. The joint between the two sides is a seam; keeping
seams clean has its own discipline. See $sdk-seam.
$sdk-seam.2e0d276
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.