stitch-sdk-domain-design
google-labs-code/stitch-sdk/.agents/skills/stitch-sdk-domain-design/SKILL.md
Design the domain model for the Stitch SDK. Use when mapping MCP tools to domain classes and bindings in domain-map.json. This is Stage 2 of the generation pipeline.
Skill1.8k starsChanged 8 days ago
What's in it
- Stitch SDK Domain Design
- Your Inputs
- Your Output
- Designing Classes
- Key decisions:
- Designing Bindings
- Arg routing
- Response Projections
- Return class wrapping
- Cache-aware methods
- Decision Framework
- Validation
---
name: stitch-sdk-domain-design
description: Design the domain model for the Stitch SDK. Use when mapping MCP tools to domain classes and bindings in domain-map.json. This is Stage 2 of the generation pipeline.
---
# Stitch SDK Domain Design
This skill teaches you how to perform **Stage 2** of the generation pipeline: reading tool schemas and producing `domain-map.json` — the intermediate representation that drives codegen.
---
## Your Inputs
1. **`tools-manifest.json`** — raw MCP tool schemas captured from the server (includes `outputSchema`)
2. **`ir-schema.ts`** — Zod schema defining valid domain-map structure (the canonical contract)
3. **Existing `domain-map.json`** — the current IR (if extending, not starting fresh)
4. **The `stitch-sdk-development` skill** — for understanding the pipeline context
## Your Output
A valid `domain-map.json` with two sections: `classes` and `bindings`, validated by `ir-schema.ts`.
> [!IMPORTANT]
> Your output is validated **twice** by the codegen: structurally (Zod IR schema) and semantically (projection steps verified against `outputSchema` from the tools-manifest).
---
## Designing Classes
Each class represents a domain entity. Ask: "What noun does the user interact with?"
```json
{
"Stitch": {
"description": "Main entry point. Manages projects.",
"constructorParams": [],
"isRoot": true,
"factories": [
{
"method": "project",
"returns": "Project",
"description": "Create a Project handle from an ID."
}
]
}
}
```
### Key decisions:
| Field | Purpose | Example |
| ------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `constructorParams` | Fields stored on the instance | `["projectId", "screenId"]` |
| `reference` | Identity-map keys (`.id` aliases the LAST key) | `{ "keys": ["projectId", "screenId"] }` |
| `parentField` | Which param is injected from a parent class | `"projectId"` |
| `factories` | Local factory methods (no API call) | `[{ "method": "project", "returns": "Project" }]` |
| `sideEffects` | Handwritten extension methods (declared, never generated) | `[{ "method": "upload", "reason": "private_rest", "specPath": "src/spec/upload.ts" }]` |
| `extensionPath` | Module re-exporting the class with handwritten methods | `"../../src/project-ext.js"` |
> [!IMPORTANT]
> **The IR schema is STRICT.** Unknown keys are hard validation errors, not
> silently ignored. Per-field source-mapping helpers described in older docs
> were never implemented and do not exist — identity fields are populated by
> the EntityManager from `reference.keys`, resource `name` parsing, and `id`
> fallback. If validation rejects a key you expected to exist, the feature
> does not exist — do not work around it.
---
## Designing Bindings
Each binding maps one MCP tool to one class method. Ask: "Who owns this action?"
### Arg routing
| Type | Meaning | Code generated |
| ----------- | --------------------------------------------------------------- | ---------------------------------------------------------- |
| `self` | From `this.field` | `projectId: this.projectId` |
| `param` | From method parameter | `prompt: prompt` |
| `computed` | `template` interpolation (`"template": "projects/{projectId}"`) | `name: \`projects/${this.projectId}/screens/${screenId}\`` |
| `selfArray` | Wrap self field as array | `selectedScreenIds: [this.screenId]` |
Optional params use `"optional": true`. Renamed params use `"rename": "newName"`.
A `"default": "VALUE"` (requires `optional: true`) is sent when the caller
omits the value: `deviceType: options?.deviceType ?? "DESKTOP"`.
> [!IMPORTANT]
> **Method signature shape (D12):** required params are positional, in IR
> order; ALL optional params are emitted into a single trailing
> `options?: { ... }` object. `generate(prompt, options?)`, never
> `generate(prompt, deviceType?)`.
### Response Projections
The `returns.projection` array tells codegen how to navigate the API response. Each step is a `ProjectionStep`:
```typescript
{ prop: string; index?: number; each?: boolean; find?: string; acknowledgeSingle?: boolean }
```
| Projection | Generated code | Use when |
| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------- | ------------------------------- |
| `[]` (empty) | `raw` | Direct return (whole response) |
| `[{ "prop": "projects" }]` | `raw.projects` | Array inside object |
| `[{ "prop": "outputComponents", "index": 0 }, { "prop": "design" }, { "prop": "screens", "index": 0 }]` | `raw.outputComponents[0].design.screens[0]` | Deeply nested single item |
| `[{ "prop": "outputComponents", "each": true }, { "prop": "design" }, { "prop": "screens", "each": true }]` | `flatMap` chain | Collect all items across arrays |
| `[{ "prop": "screenshot" }, { "prop": "downloadUrl" }]` | `raw.screenshot.downloadUrl` | Navigate nested properties |
**Decision**: Use `"index": 0` when extracting a single item. Use `"each": true` when collecting all items (array result). You **cannot** use both on the same step. `"find": "a.b"` scans an array for the first element whose nested path is non-null.
> [!TIP]
> Every `prop` in a projection is validated against the tool's `outputSchema` at codegen time. If you typo a property name, codegen will fail with a diagnostic listing the available properties. Stepping THROUGH an array without `index`/`each`/`find` is also a codegen error — the emitted chain would be `undefined` at runtime.
> [!WARNING]
> **Truncation lint:** using `index` or `find` on an UNBOUNDED array emits a
> warning — every other element is silently dropped (this exact pattern caused
> `project.generate()` to return one screen of many). Prefer `"each": true` +
> `"array": true`. Only if the result is semantically singular, acknowledge it
> with `"acknowledgeSingle": true` on the step.
### Return class wrapping
When `returns.class` is set, the extracted data is wrapped in a domain class constructor:
```json
{
"returns": {
"class": "Screen",
"projection": [{ "prop": "screens" }],
"array": true
}
}
```
The codegen automatically spreads `parentField` into the data if the child class declares one.
### Cache-aware methods
Add a `cache` field with a structured `projection` to check `this.data` before calling the API:
```json
{
"cache": {
"projection": [{ "prop": "htmlCode" }, { "prop": "downloadUrl" }],
"description": "Use cached HTML download URL from generation response if available"
}
}
```
When the cached property is a nested object (like `File` with a `downloadUrl`), use multiple projection steps to drill into it.
Generated code:
```typescript
if (this.data?.htmlCode?.downloadUrl) return this.data?.htmlCode?.downloadUrl;
// ... else call API
```
---
## Decision Framework
When mapping a new tool, answer these questions:
1. **Which class?** Look at which fields the tool requires. If it needs `projectId` from `self`, it belongs on `Project` or `Screen`. If it needs nothing from self, it belongs on `Stitch`.
2. **Which method name?** Use the verb from the tool name, simplified. `generate_screen_from_text` → `generate`. `edit_screens` → `edit`.
3. **Arguments from self or param?** If the caller already has the data (because they're calling a method on themselves), use `self`. If they need to provide it, use `param`.
4. **How deep is the return?** Check the tool's `outputSchema` in `tools-manifest.json`. Build the `projection` array step-by-step to navigate to the useful data.
5. **Should it cache?** If the data is available from a previous response (like generation), add a cache field with the projection path.
---
## Validation
After editing `domain-map.json`:
```bash
bun scripts/generate-sdk.ts # Validates IR + projections, then generates
npx tsc --noEmit # Type check
npx vitest run # Unit tests
bun scripts/e2e-test.ts # E2E tests
bun scripts/validate-generated.ts # Lock integrity
```
If a projection is invalid, you'll see:
```
❌ Binding "Project.generate" projection step 2:
property "screenz" not found in outputSchema.
Available properties: screens, components, metadata
```
More agent context in google-labs-code/stitch-sdk
8 other files this repository gives its agents.
Skill
- github-codebase-briefing.agents/skills/repo-review/SKILL.md
- stitch-sdk-bug-bash.agents/skills/stitch-sdk-bug-bash/SKILL.md
- stitch-sdk-development.agents/skills/stitch-sdk-development/SKILL.md
- stitch-sdk-pipeline.agents/skills/stitch-sdk-pipeline/SKILL.md
- stitch-sdk-readme.agents/skills/stitch-sdk-readme/SKILL.md
- stitch-sdk-usage.agents/skills/stitch-sdk-usage/SKILL.md
- tdd-red-green-refactor.agents/skills/tdd/SKILL.md
- typed-service-contracts.agents/skills/typed-service-contract/skill.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

