Composer

A catalog is a vocabulary, and vocabularies should fit the product. Pick components from both catalogs and the composer builds the mix: one prompt-pack (the primary catalog carries the surface; the other's components ride the protocol's explicit-catalogId rule, with their fixtures rewritten into mixed examples), one contract per catalog for validation, and the registration for svelte-a2ui. The wire never changes — a composition narrows what the agent is taught and what the renderer accepts, nothing else.

4 components · primary ops · pack 231 lines
ops — dashboards, data display, and human-in-the-loop decisions
forms — agent-composed forms that collect answers from humans
insight — present a finding and reach its evidence — insight cards, drill paths, source audit
my-console.pack.md — paste into your agent's system prompt
# my-console — prompt-pack (composed from auri ops)

> Composed vocabulary: Stat, Callout.

> System-prompt snippet teaching an agent the `auri ops` vocabulary. Everything below the rule is
> the pack.

---

You can render live UI for the user by emitting A2UI v1.0 messages as JSONL — one complete JSON
object per line, no surrounding markdown or prose. You describe components from a fixed catalog;
you never write markup or code.

Catalog id: `https://chaliceforauri.github.io/auri/catalogs/ops/v2.json`

## The wire in 30 seconds

Three message kinds. A minimal complete stream:

```
{"version":"v1.0","createSurface":{"surfaceId":"s1","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"s1","value":{"p95":342}}}
{"version":"v1.0","updateComponents":{"surfaceId":"s1","components":[{"id":"root","component":"Stat","label":"Checkout p95","value":{"path":"/p95"},"unit":"ms"}]}}
```

- Every line has the shape `{"version":"v1.0","<messageKind>":{...}}` — it ends with **two**
  closing braces minimum: one for the message, one for the envelope. Balance every line before
  the newline; a dropped final `}` is the most common emission mistake.
- `createSurface` comes first; after it, data and components may arrive in any order.
- Components form a flat list addressed by `id`. Nothing paints until a component with the id
  `root` exists, and only components reachable from `root` render.
- Any displayable property takes either a literal (`"value": 342`) or a data binding
  (`"value": {"path": "/p95"}`) — an RFC 6901 JSON Pointer into the surface's data model.
- **To change what's on screen, change the data, not the components.** Bind values you expect to
  update, then send:

```
{"version":"v1.0","updateDataModel":{"surfaceId":"s1","path":"/p95","value":329}}
```

## Rules

1. **Raw values only in data props.** Emit `12400`, never `"12,400"`, `"$12.4K"` or `"98%"` — the
   renderer formats numbers, dates and units in the user's locale. Units go in the `unit` prop.
   Prose is the opposite: in callout text and summaries write times and numbers for humans
   ("yesterday at 22:14 UTC"), never raw ISO strings.
2. **`intent` judges, `trend` describes.** They are independent axes: latency rising is
   `"trend": "up"` with `"intent": "bad"`; error rate falling is `"trend": "down"` with
   `"intent": "good"`.
3. **One intent scale everywhere**: `good` (healthy, succeeding) · `bad` (failing, critical) ·
   `warning` (needs attention, degraded) · `info` (informational) · `neutral` (no judgment).
   Omit `intent` when you aren't making a claim.
4. **No icons, colors, or sizes.** Intent implies the iconography; the host theme decides the look.
5. **Send data in small slices.** Several short `updateDataModel` messages beat one giant nested
   one — each line must be a complete, balanced JSON object, and small messages paint sooner.
   After the initial send, always include a `path`: an `updateDataModel` without one **replaces
   the entire data model**, blanking every other binding on the surface.

**Everything an action carries lives INSIDE `event`.** `name`, `context` and
`userMessage` are all keys of `event` — never siblings of it:

```
CORRECT  {"event":{"name":"saved","context":{"id":"a1"},"userMessage":"Saved the draft"}}
INVALID  {"event":{"name":"saved"},"context":{"id":"a1"}}
```

Hoisting any of them out of `event` makes the action invalid and it will be
rejected. This is the single most common shape mistake observed in live
emissions across every auri catalog.

`event` accepts **only** those three keys. To send a result back to the surface
after handling an action, reply with `updateDataModel` on the path you want
written — you authored the surface, so you already know the path.

## Components

### Stat — a KPI tile

One metric: label, current value, and optionally its change, direction and judgment.

| prop      | type                       | required | notes                                                     |
| --------- | -------------------------- | -------- | --------------------------------------------------------- |
| `label`   | string                     | yes      | short metric name, e.g. `"Error rate"`                    |
| `value`   | number \| string           | yes      | raw reading; bind with `{"path"}` if it updates           |
| `unit`    | string                     | no       | `"ms"`, `"%"`, `"req/s"`, or a currency code like `"USD"` |
| `delta`   | number                     | no       | signed change vs the comparison period                    |
| `caption` | string                     | no       | one line of context, e.g. `"vs previous hour"`            |
| `trend`   | `"up" \| "down" \| "flat"` | no       | direction only — never a judgment                         |
| `intent`  | intent scale               | no       | judgment of the reading; default `"neutral"`              |

```
{"id":"latency","component":"Stat","label":"Checkout p95","value":{"path":"/p95"},"unit":"ms","delta":{"path":"/p95Delta"},"trend":"down","intent":"good","caption":"vs previous hour"}
```

### Callout — an alert/note block

The agent telling the user something in prose: a heads-up, a caveat, a status note.

| prop     | type         | required | notes                                                          |
| -------- | ------------ | -------- | -------------------------------------------------------------- |
| `title`  | string       | no       | short heading                                                  |
| `text`   | string       | yes      | body; inline markdown allowed: `**bold**`, `` `code` ``, links |
| `intent` | intent scale | no       | default `"info"` — a callout's resting state is informational  |

```
{"id":"deploy_note","component":"Callout","title":"Deploy window tonight","text":"Payments API deploys **21:00–21:30 UTC**. Expect brief elevated latency.","intent":"info"}
```

## Mixing with the basic catalog

Layout containers (`Row`, `Column`, `Card`, `List`) come from the A2UI basic catalog. When the
surface's `catalogId` is the ops catalog, give basic-catalog components an explicit `catalogId`:

```
{"id":"root","component":"Column","catalogId":"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json","children":["status","latency"]}
```

## The forms catalog — mixed onto this surface

The components below come from a second catalog. Emit each of them with an explicit
`"catalogId": "https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"` — the surface's default catalog stays
`https://chaliceforauri.github.io/auri/catalogs/ops/v2.json`.

### Rules for forms components

1. **Every field's `value` is a binding — never a literal.** `"value": {"path": "/signup/email"}`
   points at where the answer lives in the data model. Seed defaults by writing that path with
   `updateDataModel`; the user's input is written back to the same path. Give every field its own
   path — two fields sharing one is a bug.
2. **Read answers from the data model.** After submission you can read any field's path, or
   hand-pick the answers you need into the submit action's `context` as `{"path": ...}` bindings —
   the decision payload, not the world.
3. **Validation is the `checks` array**, evaluated live by the renderer:
   `{"condition": {"call": "...", "args": {...}}, "message": "human text"}`. The call and
   its args go INSIDE `condition`; `message` sits beside it. Only five calls exist —
   `required` · `email` · `regex` (args `{"pattern"}`) · `length` (args `{"min"}`/`{"max"}`) ·
   `numeric` (args `{"min"}`/`{"max"}`). The renderer supplies the field's current value; you
   never pass it. `message` is what the user reads when it fails — always plain human text.
4. **Raw values on the wire.** Dates are ISO 8601 (`"2026-08-19"`), numbers unformatted — the
   renderer localizes. Prose (labels, hints, messages) is the opposite: written for humans.
5. **Send data in small slices.** After the initial send, always include a `path`: an
   `updateDataModel` without one **replaces the entire data model**, blanking every binding on
   the surface — including the user's half-typed answers.
6. **Send components in small batches too.** `updateComponents` may be sent repeatedly — each
   message merges into the flat list by `id`. Emit the root and two or three fields per line, then
   the next batch. Never pack a whole form into one line: long lines are where braces get lost,
   and shorter lines paint sooner.

**Everything an action carries lives INSIDE `event`.** `name`, `context` and
`userMessage` are all keys of `event` — never siblings of it:

```
CORRECT  {"event":{"name":"saved","context":{"id":"a1"},"userMessage":"Saved the draft"}}
INVALID  {"event":{"name":"saved"},"context":{"id":"a1"}}
```

Hoisting any of them out of `event` makes the action invalid and it will be
rejected. This is the single most common shape mistake observed in live
emissions across every auri catalog.

`event` accepts **only** those three keys. To send a result back to the surface
after handling an action, reply with `updateDataModel` on the path you want
written — you authored the surface, so you already know the path.

### TextField — one line of text

| prop          | type                                              | required | notes                                               |
| ------------- | ------------------------------------------------- | -------- | --------------------------------------------------- |
| `label`       | string                                            | yes      | visible label and accessible name                   |
| `value`       | `{"path"}`                                        | yes      | where the answer lives; always a binding            |
| `kind`        | `"text" \| "email" \| "url" \| "tel" \| "secret"` | no       | input treatment + mobile keyboard; default `"text"` |
| `placeholder` | string                                            | no       | example content, never a label substitute           |
| `hint`        | string                                            | no       | one line of help under the field                    |
| `checks`      | check[]                                           | no       | see rule 3                                          |

```
{"id":"email","component":"TextField","label":"Work email","kind":"email","value":{"path":"/contact/email"},"hint":"We only use this for receipts.","checks":[{"condition":{"call":"required"},"message":"Enter your email."},{"condition":{"call":"email"},"message":"That doesn't look like an email address."}]}
```

### SubmitBar — submit (and cancel) with pending state

Submission fires only when every check on the surface passes. Hand-pick the answers you need into
`context`. To report a server-side verdict afterwards, reply with `updateDataModel` on the path you
want written — bind a `Callout` or the field's own error to that path when you build the form.

| prop           | type       | required | notes                                                          |
| -------------- | ---------- | -------- | -------------------------------------------------------------- |
| `submitAction` | action     | yes      | `{"event":{"name","context",...}}`; name it after what happens |
| `submitLabel`  | string     | no       | e.g. `"Create account"`; default localized "Submit"            |
| `cancelAction` | action     | no       | renders a quiet cancel button                                  |
| `cancelLabel`  | string     | no       |                                                                |
| `pending`      | `{"path"}` | no       | bind and set true while you process; the bar disables          |

```
{"id":"submit","component":"SubmitBar","submitLabel":"File report","pending":{"path":"/report/pending"},"submitAction":{"event":{"name":"report_filed","context":{"severity":{"path":"/report/severity"},"details":{"path":"/report/details"}}}}}
```

## Examples

One Stat stream:

```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"checkout_health","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"checkout_health","value":{"p95":342,"p95Delta":-38}}}
{"version":"v1.0","updateComponents":{"surfaceId":"checkout_health","components":[{"id":"root","component":"Stat","label":"Checkout p95 latency","value":{"path":"/p95"},"unit":"ms","delta":{"path":"/p95Delta"},"trend":"down","intent":"good","caption":"vs previous hour"}]}}
{"version":"v1.0","updateDataModel":{"surfaceId":"checkout_health","path":"/p95","value":329}}
```

One Callout stream:

```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"deploy_note","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateComponents":{"surfaceId":"deploy_note","components":[{"id":"root","component":"Callout","title":"Deploy window tonight","text":"Payments API deploys **21:00–21:30 UTC**. Expect brief elevated latency on `/charge`.","intent":"info"}]}}
```

One mixed TextField stream (note the explicit catalogId):

```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"f","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"f","value":{"email":""}}}
{"version":"v1.0","updateComponents":{"surfaceId":"f","components":[{"id":"root","component":"Column","catalogId":"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json","children":["email"]},{"id":"email","component":"TextField","label":"Work email","kind":"email","value":{"path":"/email"},"hint":"We only use this for receipts.","checks":[{"condition":{"call":"required"},"message":"Enter your email."},{"condition":{"call":"email"},"message":"That doesn't look like an email address."}],"catalogId":"https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"}]}}
```

One mixed SubmitBar stream (note the explicit catalogId):

```jsonl
{"version":"v1.0","createSurface":{"surfaceId":"f","catalogId":"https://chaliceforauri.github.io/auri/catalogs/ops/v2.json"}}
{"version":"v1.0","updateDataModel":{"surfaceId":"f","value":{"email":"","pending":false}}}
{"version":"v1.0","updateComponents":{"surfaceId":"f","components":[{"id":"root","component":"Column","catalogId":"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json","children":["email","submit"]},{"id":"email","component":"TextField","label":"Work email","kind":"email","value":{"path":"/email"},"checks":[{"condition":{"call":"required"},"message":"Enter your email."}],"catalogId":"https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"},{"id":"submit","component":"SubmitBar","submitLabel":"Subscribe","pending":{"path":"/pending"},"submitAction":{"event":{"name":"subscribed","context":{"email":{"path":"/email"}}}},"catalogId":"https://chaliceforauri.github.io/auri/catalogs/forms/v2.json"}]}}
```
my-console.ops.contract.json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"$id": "https://chaliceforauri.github.io/auri/catalogs/ops/v2.json",
	"protocolVersion": "v1.0",
	"title": "my-console",
	"description": "Composed from auri ops: Stat, Callout. Source contract: https://chaliceforauri.github.io/auri/catalogs/ops/v2.json",
	"catalogId": "https://chaliceforauri.github.io/auri/catalogs/ops/v2.json",
	"instructions": "Rules for emitting this catalog. 1) Emit raw values: 12400, never \"12,400\" or \"$12.4K\" — the renderer formats numbers, dates and units in the user's locale. 2) `intent` judges, `trend` describes: latency rising is trend \"up\" with intent \"bad\"; error rate falling is trend \"down\" with intent \"good\". 3) One shared intent scale: good (healthy) | bad (failing) | warning (needs attention) | info (informational) | neutral (no judgment). Omit intent when you are not making a claim. 4) No icons, colors, or sizes on the wire — intent implies iconography and the theme decides the look. 5) Bind values you expect to change with {\"path\": \"/json/pointer\"} and update them via updateDataModel instead of re-sending components. 6) Layout containers (Row, Column, Card, List) come from the basic catalog: give those components an explicit \"catalogId\": \"https://a2ui.org/specification/v1_0/catalogs/basic/catalog.json\".",
	"components": {
		"Stat": {
			"type": "object",
			"description": "A KPI tile: one metric with its label, current value, and optionally its change, direction and judgment.",
			"properties": {
				"component": {
					"const": "Stat"
				},
				"label": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "Short name of the metric, e.g. 'Error rate'. Required — it is also the tile's accessible name."
				},
				"value": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicValue",
					"description": "The current reading, raw and unformatted (a number like 12400, or a short string like 'Healthy'). Bind with {\"path\": ...} when it will update."
				},
				"unit": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "Unit rendered beside the value: 'ms', '%', 'req/s', or an ISO 4217 currency code like 'USD' (currencies are formatted in the user's locale). Omit for unitless values."
				},
				"delta": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicNumber",
					"description": "Signed change versus the comparison period, as a raw number (e.g. -38). The renderer formats the sign."
				},
				"caption": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "One small line of context under the value, e.g. 'vs previous hour'."
				},
				"trend": {
					"oneOf": [
						{
							"type": "string",
							"enum": [
								"up",
								"down",
								"flat"
							]
						},
						{
							"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding"
						},
						{
							"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCall"
						}
					],
					"description": "Direction of movement: 'up', 'down' or 'flat'. Describes, never judges — pair with intent for the judgment."
				},
				"intent": {
					"oneOf": [
						{
							"type": "string",
							"enum": [
								"good",
								"bad",
								"warning",
								"info",
								"neutral"
							]
						},
						{
							"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding"
						},
						{
							"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCall"
						}
					],
					"description": "Judgment of the current reading on the shared scale. Default: 'neutral'.",
					"default": "neutral"
				}
			},
			"required": [
				"component",
				"label",
				"value"
			]
		},
		"Callout": {
			"type": "object",
			"description": "An alert/note block the agent uses to tell the user something in prose: a heads-up, a caveat, a status note.",
			"properties": {
				"component": {
					"const": "Callout"
				},
				"title": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "Optional short heading for the callout."
				},
				"text": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "The callout body. Required. Supports inline markdown: **bold**, *italics*, `code`, [links](https://...)."
				},
				"intent": {
					"oneOf": [
						{
							"type": "string",
							"enum": [
								"good",
								"bad",
								"warning",
								"info",
								"neutral"
							]
						},
						{
							"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding"
						},
						{
							"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/FunctionCall"
						}
					],
					"description": "The callout's register on the shared scale. Default: 'info' — a callout exists to call attention; informational is its resting state.",
					"default": "info"
				}
			},
			"required": [
				"component",
				"text"
			]
		}
	},
	"$defs": {
		"anyComponent": {
			"oneOf": [
				{
					"$ref": "#/components/Stat"
				},
				{
					"$ref": "#/components/Callout"
				}
			],
			"discriminator": {
				"propertyName": "component"
			}
		},
		"anyFunction": {
			"not": {}
		}
	}
}
my-console.forms.contract.json
{
	"$schema": "https://json-schema.org/draft/2020-12/schema",
	"$id": "https://chaliceforauri.github.io/auri/catalogs/forms/v2.json",
	"protocolVersion": "v1.0",
	"title": "my-console",
	"description": "Composed from auri forms: TextField, SubmitBar. Source contract: https://chaliceforauri.github.io/auri/catalogs/forms/v2.json",
	"catalogId": "https://chaliceforauri.github.io/auri/catalogs/forms/v2.json",
	"instructions": "Rules for emitting this catalog. 1) Every field's `value` is a data binding {\"path\": \"/json/pointer\"} — that pointer is where the user's answer lives. Never emit a literal value; seed defaults by writing the data model with updateDataModel. 2) Give each field its own path; two fields sharing a path is a bug. 3) Validation is the `checks` array: {\"call\", \"args\", \"message\"} using only the built-ins required | email | regex | length | numeric. The renderer supplies the field's current value; args carry the rest (e.g. {\"min\": 8}). `message` is always human text. 4) Raw values on the wire: dates are ISO 8601 (\"2026-08-19\"), numbers unformatted — the renderer localizes. 5) Read answers from the data model, or hand-pick them into submitAction context as {\"path\": ...} bindings. 6) Labels are required — a field without an accessible name cannot be emitted. 7) Send components in small batches — updateComponents merges by id, so emit two or three components per message rather than a whole form in one line.",
	"components": {
		"TextField": {
			"type": "object",
			"properties": {
				"component": {
					"const": "TextField"
				},
				"label": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "Visible field label, e.g. 'Work email'. Required — it is the field's accessible name."
				},
				"value": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DataBinding",
					"description": "Data binding for the answer, always {\"path\": ...}. Seed a default by writing that path with updateDataModel; the user's input is written back to it."
				},
				"kind": {
					"enum": [
						"text",
						"email",
						"url",
						"tel",
						"secret"
					],
					"description": "Input treatment and mobile keyboard. 'secret' masks input. Default 'text'."
				},
				"placeholder": {
					"type": "string",
					"description": "Example content shown while empty. Never a substitute for label."
				},
				"hint": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "One line of help under the field, e.g. 'We only use this for receipts.'"
				},
				"checks": {
					"type": "array",
					"items": {
						"type": "object",
						"description": "A validation check. Spec-shaped — `condition` is a FunctionCall — but narrowed to the renderer's built-ins, and `message` is required because a check with no human text fails silently.",
						"properties": {
							"condition": {
								"type": "object",
								"properties": {
									"call": {
										"enum": [
											"required",
											"email",
											"regex",
											"length",
											"numeric"
										],
										"description": "One of the protocol's built-in validation functions. The renderer supplies the field's current value automatically."
									},
									"args": {
										"type": "object",
										"description": "Arguments beyond the value: regex takes {\"pattern\"}, length and numeric take {\"min\"} and/or {\"max\"}. required and email take none."
									}
								},
								"required": [
									"call"
								],
								"additionalProperties": false
							},
							"message": {
								"type": "string",
								"description": "Human text shown when the check fails. Always required."
							}
						},
						"required": [
							"condition",
							"message"
						],
						"additionalProperties": false
					},
					"description": "Validation rules, evaluated by the renderer as the user types. Submission is blocked while any check fails."
				}
			},
			"required": [
				"label",
				"value"
			]
		},
		"SubmitBar": {
			"type": "object",
			"properties": {
				"component": {
					"const": "SubmitBar"
				},
				"submitAction": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/Action",
					"description": "Fired on submit, only when every check on the surface passes. Hand-pick the answers you need into context as {\"path\": ...} bindings."
				},
				"submitLabel": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString",
					"description": "Names the act of submitting, e.g. 'Create account'. Default: localized 'Submit'."
				},
				"cancelAction": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/Action",
					"description": "Optional secondary action; renders a quiet cancel button."
				},
				"cancelLabel": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicString"
				},
				"pending": {
					"$ref": "https://a2ui.org/specification/v1_0/common_types.json#/$defs/DynamicBoolean",
					"description": "Bind to a path and set true while processing the submission — the bar disables and shows progress. Set false (or write a server error) when done."
				}
			},
			"required": [
				"submitAction"
			]
		}
	},
	"$defs": {
		"anyComponent": {
			"oneOf": [
				{
					"$ref": "#/components/TextField"
				},
				{
					"$ref": "#/components/SubmitBar"
				}
			],
			"discriminator": {
				"propertyName": "component"
			}
		},
		"anyFunction": {
			"not": {}
		}
	}
}
registration — svelte-a2ui
import { createCatalogRegistry, basicCatalog } from 'svelte-a2ui';
import { opsCatalog } from '@aurilabs/ops';
import { formsCatalog } from '@aurilabs/forms';

const pick = (catalog, names) =>
	({ id: catalog.id, components: Object.fromEntries(names.map((n) => [n, catalog.components[n]])) });

export const catalog = createCatalogRegistry([
	pick(opsCatalog, ['Stat', 'Callout']),
	pick(formsCatalog, ['TextField', 'SubmitBar']),
	basicCatalog
]);
run the gate against your composition
# save the pack and each contract, then from packages/ops:
node scripts/emission-eval.js \
  --models openai:gpt-5.6 \
  --pack my-console.pack.md \
  --contract my-console.ops.contract.json \
  --scenarios-file your-scenarios.json

node scripts/emission-eval.js \
  --models openai:gpt-5.6 \
  --pack my-console.pack.md \
  --contract my-console.forms.contract.json \
  --scenarios-file your-scenarios.json

# validation is per-catalog: each run validates its own catalog's components
# and skips the other's (they carry an explicit foreign catalogId on the wire).
# a model emitting a component you cut still fails its catalog's run.