@sanity/agent-schema 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +253 -121
  2. package/package.json +2 -10
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # @sanity/agent-schema
2
2
 
3
- Schema processing utilities for Sanity Agent. Parses Lexicon schemas, compresses them for LLM context, provides full JSON views for schema explorers, and navigates schema trees for validation and mutation agents.
3
+ Schema utilities for Sanity Agent. Fetches Lexicon schemas, compresses them for LLM context windows, provides full JSON views for schema explorers, and resolves field definitions for validation and mutation agents.
4
4
 
5
5
  ## Install
6
6
 
7
- ```bash
8
- pnpm add @sanity/agent-schema
7
+ ```sh
8
+ pnpm add @sanity/agent-schema @sanity/client groq-js
9
9
  ```
10
10
 
11
- ## Quick start
11
+ ## Getting started
12
+
13
+ The most common workflow: fetch a schema, optionally enrich it with dataset stats, and compress it into text for an LLM.
12
14
 
13
15
  ```ts
14
16
  import {
@@ -17,10 +19,13 @@ import {
17
19
  compressedSchema,
18
20
  } from "@sanity/agent-schema";
19
21
 
20
- // 1. Fetch and parse the schema
22
+ // 1. Fetch and parse the schema from Lexicon
23
+ // The `lexiconClient` should target the Lexicon API host.
24
+ // `descriptorId` identifies a specific schema version (typically from an application resource).
21
25
  const types = await fetchSchemaByDescriptorId(lexiconClient, descriptorId);
22
26
 
23
- // 2. Optionally fetch dataset stats (enriches output with counts)
27
+ // 2. Optionally fetch dataset stats (document/reference counts)
28
+ // The `agentClient` should target the agent API host with `useProjectHostname: false`.
24
29
  const statsResult = await fetchDatasetStats(agentClient, {
25
30
  organizationId,
26
31
  projectId,
@@ -34,7 +39,128 @@ const text = await compressedSchema(types, {
34
39
  });
35
40
  ```
36
41
 
37
- ## API
42
+ The result is a compact text summary of your document types, fields, references, and counts — ready to drop into an LLM system prompt.
43
+
44
+ ## Usage
45
+
46
+ ### Compress a schema for LLM context
47
+
48
+ `compressedSchema` is the main entry point for generating LLM-ready text. It accepts parsed schema types and returns a compact summary.
49
+
50
+ ```ts
51
+ import { compressedSchema } from "@sanity/agent-schema";
52
+
53
+ // Filter by GROQ expression
54
+ const text = await compressedSchema(types, {
55
+ filter: { read: '_type in ["post", "author"]' },
56
+ });
57
+
58
+ // Filter by explicit type list
59
+ const text = await compressedSchema(types, {
60
+ filter: ["post", "author"],
61
+ });
62
+
63
+ // All options
64
+ const text = await compressedSchema(types, {
65
+ filter: { read: '_type == "post"', write: '_type == "draft"' },
66
+ datasetStats,
67
+ fields: { maxCount: 5, includeDescription: true },
68
+ });
69
+ ```
70
+
71
+ ### Browse full schema JSON
72
+
73
+ `fullSchema` returns the complete JSON structure of a type — every field, nested object, validation rule, and option. Used by schema explorer tools.
74
+
75
+ ```ts
76
+ import { fullSchema } from "@sanity/agent-schema";
77
+
78
+ // Get the entire type
79
+ fullSchema(types, { type: "post" });
80
+ // => { name: "post", type: "document", fields: [...] }
81
+
82
+ // Zoom into a specific field path
83
+ fullSchema(types, { type: "post", path: "content[].codeBlock" });
84
+ // => { name: "codeBlock", type: "object", fields: [...] }
85
+ ```
86
+
87
+ Large types automatically return a navigator object with child paths to drill into, instead of blowing up the response. Control this with `sizeThreshold` (default 200KB).
88
+
89
+ ### Resolve a field at a path
90
+
91
+ `resolveFieldSchema` answers "what type is the value at this path?" — used by validators and mutation code.
92
+
93
+ ```ts
94
+ import { resolveFieldSchema } from "@sanity/agent-schema";
95
+
96
+ resolveFieldSchema("content[0].text", postSchema, allTypes);
97
+ // => { name: "text", type: "string" }
98
+
99
+ resolveFieldSchema("tags", postSchema, allTypes);
100
+ // => { name: "tags", type: "array", of: [...] }
101
+
102
+ resolveFieldSchema("nonexistent.path", postSchema, allTypes);
103
+ // => null
104
+ ```
105
+
106
+ For union arrays where the same path exists in multiple item types, use `resolveFieldSchemaAllUnions` to get all matches:
107
+
108
+ ```ts
109
+ import { resolveFieldSchemaAllUnions } from "@sanity/agent-schema";
110
+
111
+ resolveFieldSchemaAllUnions("content", postSchema, allTypes);
112
+ // => [blockSchema, imageSchema, codeSchema]
113
+ ```
114
+
115
+ ### Process schema for custom tooling
116
+
117
+ `processSchema` is the core transformation step. It categorizes types, expands inline object references, computes bidirectional reference graphs, and identifies title fields. This is what `compressedSchema` uses internally.
118
+
119
+ ```ts
120
+ import { processSchema } from "@sanity/agent-schema";
121
+
122
+ const { documentTypes, objectTypes } = processSchema(types, {
123
+ filter: ["post", "author"],
124
+ });
125
+
126
+ // documentTypes: Map<string, ProcessedDocumentType>
127
+ // objectTypes: Map<string, ManifestSchemaType>
128
+
129
+ for (const [name, doc] of documentTypes) {
130
+ console.log(doc.titleField); // "title"
131
+ console.log(doc.outgoingReferences); // ["author", "category"]
132
+ console.log(doc.incomingReferences); // ["comment"]
133
+ console.log(doc.outgoingReferencePaths); // [{ type: "author", path: "author" }]
134
+ }
135
+ ```
136
+
137
+ Use this when you need the processed data structures without the text formatting — for custom schema visualizations, reference analysis, or building infrastructure like document counters.
138
+
139
+ ## Architecture
140
+
141
+ ```
142
+ fetchSchemaByDescriptorId() fetchDatasetStats()
143
+ │ │
144
+ ▼ ▼
145
+ ManifestSchemaType[] DatasetStatsResult
146
+ │ │
147
+ ├──► compressedSchema() ◄─────────────────┘
148
+ │ ──► text summary of all types
149
+ │
150
+ ├──► fullSchema()
151
+ │ ──► JSON subtree of one type (all fields expanded)
152
+ │
153
+ ├──► resolveFieldSchema()
154
+ │ ──► single field definition at a path
155
+ │
156
+ ├──► resolveFieldSchemaAllUnions()
157
+ │ ──► same, all union branches
158
+ │
159
+ └──► processSchema()
160
+ ──► categorize types, expand fields, compute refs
161
+ ```
162
+
163
+ ## API reference
38
164
 
39
165
  ### Fetch
40
166
 
@@ -42,11 +168,13 @@ const text = await compressedSchema(types, {
42
168
 
43
169
  Fetches a schema from the Lexicon API and returns parsed `ManifestSchemaType[]`.
44
170
 
171
+ The `client` should be a `@sanity/client` instance configured for the Lexicon API host. The `descriptorId` identifies a specific schema version — typically obtained from an application resource.
172
+
45
173
  ```ts
46
174
  const types = await fetchSchemaByDescriptorId(client, descriptorId);
47
175
  ```
48
176
 
49
- Enable caching with `{ cache: true }` (in-memory, 5 min TTL) or a custom adapter:
177
+ **Caching:** Pass `{ cache: true }` for a built-in in-memory cache (5 min TTL), or provide a custom adapter:
50
178
 
51
179
  ```ts
52
180
  // Built-in memory cache
@@ -61,9 +189,16 @@ const types = await fetchSchemaByDescriptorId(client, id, {
61
189
  });
62
190
  ```
63
191
 
192
+ | Option | Type | Default | Description |
193
+ | ------- | ------------------------ | -------- | --------------------------------------------------------- |
194
+ | `cache` | `boolean \| SchemaCache` | | Enable caching. `true` for in-memory, or a custom adapter |
195
+ | `ttlMs` | `number` | `300000` | Cache TTL in ms (only applies to built-in memory cache) |
196
+
64
197
  #### `fetchDatasetStats(client, options)`
65
198
 
66
- Fetches document and reference counts from the agent API. Returns `{ status: 'ready', stats }` or `{ status: 'processing' }` if still being computed.
199
+ Fetches document and reference counts from the agent API. Returns `{ status: 'ready', stats }` or `{ status: 'processing' }` if stats are still being computed.
200
+
201
+ The `client` should be configured to point at the agent API host (e.g. `api.sanity.io`) with `useProjectHostname: false`.
67
202
 
68
203
  ```ts
69
204
  const result = await fetchDatasetStats(client, {
@@ -82,28 +217,7 @@ if (result.status === "ready") {
82
217
 
83
218
  #### `compressedSchema(types, options?)`
84
219
 
85
- The main entry point. Takes parsed types and returns compact text for agents.
86
-
87
- The `filter` option controls which document types to include. Pass a GROQ filter (requires `groq-js` peer dep) or an explicit list of type names:
88
-
89
- ```ts
90
- // GROQ filter — types are inferred from the expression
91
- await compressedSchema(types, {
92
- filter: { read: '_type in ["post", "author"]' },
93
- });
94
-
95
- // Explicit type list
96
- await compressedSchema(types, {
97
- filter: ["post", "author"],
98
- });
99
-
100
- // All options
101
- await compressedSchema(types, {
102
- filter: { read: '_type == "post"', write: '_type == "draft"' },
103
- datasetStats,
104
- fields: { maxCount: 5, includeDescription: true },
105
- });
106
- ```
220
+ Generates compressed text for agent consumption. Processes the schema, applies filters, and formats the output.
107
221
 
108
222
  | Option | Type | Description |
109
223
  | -------------- | --------------------------- | --------------------------------------------- |
@@ -127,78 +241,76 @@ await compressedSchema(types, {
127
241
 
128
242
  #### `fullSchema(types, options)`
129
243
 
130
- Returns a **JSON subtree** of a schema type — everything inside it (all fields, nested objects, validation rules, options). Used by schema explorer tools to let the LLM browse type structures.
244
+ Returns the complete JSON structure for a schema type, optionally scoped to a field path.
131
245
 
132
- Without `path`, you get the entire type. With `path`, you get the subtree rooted at that field — still the full structure from that point down, just scoped to a smaller starting point. This is useful because some types are huge; `path` lets you zoom in.
246
+ Without `path`, you get the entire type. With `path`, you get the subtree rooted at that field. This is useful because some types are huge — `path` lets you zoom in without fetching the whole thing.
133
247
 
134
- ```ts
135
- import { fullSchema } from "@sanity/agent-schema";
248
+ When the output exceeds `sizeThreshold`, a navigator object is returned instead with child paths to drill into:
136
249
 
137
- fullSchema(types, { type: "post" });
138
- // Returns: { name: "post", type: "document", fields: [
139
- // { name: "title", type: "string" },
140
- // { name: "content", type: "array", of: [
141
- // { name: "block", type: "block", marks: {...}, styles: [...], ... },
142
- // { name: "codeBlock", type: "object", fields: [
143
- // { name: "language", type: "string", options: { list: [...] } },
144
- // { name: "code", type: "text" },
145
- // ]},
146
- // ]},
147
- // ... every field, fully expanded ...
148
- // ]}
149
-
150
- // Same structure, just starting from content[].codeBlock instead of the root
151
- fullSchema(types, { type: "post", path: "content[].codeBlock" });
152
- // Returns: { name: "codeBlock", type: "object", fields: [
153
- // { name: "language", type: "string", options: { list: [...] } },
154
- // { name: "code", type: "text" },
155
- // ]}
250
+ ```ts
251
+ // Navigator shape (returned when output is too large)
252
+ {
253
+ _rawLargeNode: true,
254
+ name: "post",
255
+ type: "document",
256
+ children: [
257
+ { type: "array", path: "content[]" },
258
+ { type: "object", path: "metadata" },
259
+ ],
260
+ hint: "This node is large. Fetch a specific child path to drill in."
261
+ }
156
262
  ```
157
263
 
158
- If the output exceeds `sizeThreshold`, a navigator object is returned instead with child paths to drill into.
159
-
160
264
  | Option | Type | Default | Description |
161
265
  | --------------- | -------- | -------- | ----------------------------------------------------- |
162
266
  | `type` | `string` | required | Document type name to return |
163
267
  | `path` | `string` | | Field path — returns the subtree from this point down |
164
268
  | `sizeThreshold` | `number` | `200KB` | Byte limit before returning a navigator instead |
165
269
 
166
- ### Navigate
270
+ ### Process
167
271
 
168
- #### `resolveFieldSchema(path, typeSchema, schema)`
272
+ #### `processSchema(types, options?)`
273
+
274
+ Processes raw `ManifestSchemaType[]` into categorized, enriched data structures. This is the core transformation that `compressedSchema` uses internally.
169
275
 
170
- Returns the **single field definition** at the end of a path. No subtree, no nested fields — just the node itself. Used by validators and mutation code that need to answer "what type is the value at this path?"
276
+ Returns a `SchemaTypes` object:
171
277
 
172
278
  ```ts
173
- import { resolveFieldSchema } from "@sanity/agent-schema";
279
+ type SchemaTypes = {
280
+ documentTypes: Map<string, ProcessedDocumentType>;
281
+ objectTypes: Map<string, ManifestSchemaType>;
282
+ };
283
+ ```
174
284
 
175
- resolveFieldSchema("content[0].text", postSchema, allTypes);
176
- // Returns: { name: "text", type: "string" }
177
- //
178
- // Compare with fullSchema — that would return the entire content array
179
- // with all its item types expanded. This just tells you "it's a string."
285
+ Each `ProcessedDocumentType` contains:
286
+
287
+ | Property | Type | Description |
288
+ | ------------------------ | ------------------ | ---------------------------------- |
289
+ | `name` | `string` | Document type name |
290
+ | `title` | `string?` | Human-readable title |
291
+ | `description` | `string?` | Type description |
292
+ | `deprecated` | `object?` | Deprecation info with reason |
293
+ | `titleField` | `string` | Best candidate for display title |
294
+ | `fields` | `ManifestField[]` | Expanded field definitions |
295
+ | `outgoingReferences` | `string[]` | Types this document references |
296
+ | `incomingReferences` | `string[]` | Types that reference this document |
297
+ | `outgoingReferencePaths` | `ReferenceInfo[]?` | Outgoing refs with field paths |
298
+ | `incomingReferencePaths` | `ReferenceInfo[]?` | Incoming refs with field paths |
299
+
300
+ | Option | Type | Description |
301
+ | -------- | --------------------------- | --------------------------------------- |
302
+ | `filter` | `SchemaFilter` | GROQ filter or `string[]` of type names |
303
+ | `fields` | `SchemaFieldDisplayOptions` | Field display options |
180
304
 
181
- resolveFieldSchema("tags", postSchema, allTypes);
182
- // Returns: { name: "tags", type: "array", of: [...] }
183
- //
184
- // You get the field node, not the expanded contents of every array item.
305
+ ### Navigate
185
306
 
186
- resolveFieldSchema("nonexistent.path", postSchema, allTypes);
187
- // Returns: null
188
- ```
307
+ #### `resolveFieldSchema(path, typeSchema, schema)`
189
308
 
190
- Handles nested objects, array indices, built-in type fields (`slug.current`, `block.children`), and type references.
309
+ Returns the single field definition at the end of a path. Handles nested objects, array indices, built-in type fields (`slug.current`, `block.children`), and type references. Returns `null` if the path doesn't resolve.
191
310
 
192
311
  #### `resolveFieldSchemaAllUnions(path, typeSchema, schema)`
193
312
 
194
- Same as `resolveFieldSchema`, but when the path crosses a union array, returns the field definition from **every** item type instead of just the first match.
195
-
196
- ```ts
197
- import { resolveFieldSchemaAllUnions } from "@sanity/agent-schema";
198
-
199
- resolveFieldSchemaAllUnions("content", postSchema, allTypes);
200
- // Returns: [blockSchema, imageSchema, codeSchema]
201
- ```
313
+ Same as `resolveFieldSchema`, but when the path crosses a union array, returns the field definition from every item type instead of just the first match.
202
314
 
203
315
  #### `filterToPath(type, path, schema)`
204
316
 
@@ -221,57 +333,77 @@ toSchemaPath('content[_key=="abc"].children[0].text');
221
333
  // "content.children.text"
222
334
  ```
223
335
 
224
- #### Type helpers
336
+ ### Type helpers
337
+
338
+ #### `isPrimitiveType(type, options?)`
339
+
340
+ Returns `true` for primitive types (`string`, `number`, `boolean`, `text`, `url`, `date`, `datetime`). Pass `{ includeBlockTypes: true }` to also include `block` and `span`.
341
+
342
+ #### `isBuiltInType(type)`
343
+
344
+ Returns `true` for Sanity built-in types (`image`, `file`, `slug`, `geopoint`, `reference`, `block`, `span`).
345
+
346
+ #### `isGenericObject(node)`
347
+
348
+ Returns `true` for object-like schema nodes without a `fields` array.
349
+
350
+ #### `shouldSkipType(typeName)`
351
+
352
+ Returns `true` for internal types that should be excluded from processing (types matching `system.*`, `sanity.*`, `assist.*`, `media.*`, `mux.*`).
353
+
354
+ ### Constants
355
+
356
+ #### `BUILT_IN_TYPE_FIELDS`
357
+
358
+ Maps built-in type names to their implicit field definitions:
225
359
 
226
360
  ```ts
227
- import {
228
- isPrimitiveType,
229
- isBuiltInType,
230
- isGenericObject,
231
- } from "@sanity/agent-schema";
361
+ import { BUILT_IN_TYPE_FIELDS } from "@sanity/agent-schema";
232
362
 
233
- isPrimitiveType("string"); // true
234
- isPrimitiveType("block", { includeBlockTypes: true }); // true
235
- isBuiltInType("image"); // true
236
- isGenericObject(node); // true for object-like nodes without fields array
363
+ BUILT_IN_TYPE_FIELDS.slug.current; // { name: "current", type: "string" }
364
+ BUILT_IN_TYPE_FIELDS.image.asset; // { name: "asset", type: "reference" }
365
+ BUILT_IN_TYPE_FIELDS.block.children; // { name: "children", type: "array", of: [...] }
237
366
  ```
238
367
 
239
- #### Constants
368
+ Supported types: `slug`, `geopoint`, `reference`, `image`, `file`, `block`, `span`.
369
+
370
+ #### `ARRAY_ITEM_SYSTEM_FIELDS`
371
+
372
+ System fields present on all array items:
240
373
 
241
374
  ```ts
242
- import {
243
- BUILT_IN_TYPE_FIELDS,
244
- ARRAY_ITEM_SYSTEM_FIELDS,
245
- } from "@sanity/agent-schema";
375
+ import { ARRAY_ITEM_SYSTEM_FIELDS } from "@sanity/agent-schema";
246
376
 
247
- BUILT_IN_TYPE_FIELDS.slug.current; // { name: "current", type: "string" }
248
377
  ARRAY_ITEM_SYSTEM_FIELDS._key; // { name: "_key", type: "string" }
378
+ ARRAY_ITEM_SYSTEM_FIELDS._type; // { name: "_type", type: "string" }
249
379
  ```
250
380
 
251
- ## Types
381
+ #### `DEFAULT_LINK_ANNOTATION`
252
382
 
253
- | Type | Description |
254
- | --------------------------- | --------------------------------------------------------------- |
255
- | `ManifestSchemaType` | A parsed schema type (document, object, array, etc.) |
256
- | `ManifestField` | A field definition (ManifestSchemaType with optional fieldset) |
257
- | `DatasetStats` | `{ documentCount?, referenceCount? }` |
258
- | `CompressedSchemaOptions` | Options for `compressedSchema()` (filter, datasetStats, fields) |
259
- | `SchemaFilter` | `{ read?, write? }` GROQ filter or `string[]` of type names |
260
- | `FullSchemaOptions` | Options for `fullSchema()` |
261
- | `SchemaFieldDisplayOptions` | Options for field display in `compressedSchema` |
262
- | `SchemaCache` | Cache adapter for `fetchSchemaByDescriptorId` |
383
+ The default link annotation Sanity adds to Portable Text blocks when no `marks.annotations` are declared.
263
384
 
264
- ## Architecture
385
+ #### `SCHEMA_CONSTANTS`
265
386
 
266
- ```
267
- fetchSchemaByDescriptorId() fetchDatasetStats()
268
- │ │
269
- ▼ │
270
- ManifestSchemaType[] │
271
- │ │
272
- ├──► compressedSchema() ──► text summary of all types
273
- ├──► fullSchema() ──► JSON subtree of one type (all fields expanded)
274
- ├──► resolveFieldSchema() ──► single field definition at a path
275
- ├──► resolveFieldSchemaAllUnions() ──► same, all union branches
276
- └──► processSchema() ──► categorize types, expand fields, compute refs
277
- ```
387
+ Internal constants used by the schema processor (skip patterns, title field candidates, default limits). Exported for consumers that need to extend the defaults.
388
+
389
+ ## Types
390
+
391
+ | Type | Description |
392
+ | --------------------------- | --------------------------------------------------------------------- |
393
+ | `ManifestSchemaType` | A parsed schema type (document, object, array, etc.) |
394
+ | `ManifestField` | A field definition (`ManifestSchemaType` with optional `fieldset`) |
395
+ | `DatasetStats` | `{ documentCount?, referenceCount? }` |
396
+ | `DatasetStatsSchema` | Zod schema for runtime validation of `DatasetStats` |
397
+ | `CompressedSchemaOptions` | Options for `compressedSchema()` |
398
+ | `SchemaFilter` | `{ read?, write? }` GROQ filter or `string[]` of type names |
399
+ | `SchemaFieldDisplayOptions` | Field display options for `compressedSchema` |
400
+ | `FullSchemaOptions` | Options for `fullSchema()` |
401
+ | `SchemaCache` | Cache adapter for `fetchSchemaByDescriptorId` |
402
+ | `FetchSchemaOptions` | Options for `fetchSchemaByDescriptorId` |
403
+ | `ProcessedDocumentType` | A document type after processing (fields expanded, refs computed) |
404
+ | `SchemaTypes` | Return type of `processSchema` (`documentTypes` + `objectTypes` Maps) |
405
+ | `SchemaProcessorOptions` | Options for `processSchema()` |
406
+
407
+ ### Deprecated aliases
408
+
409
+ `navigateToFieldSchema` and `navigateToFieldSchemaAllUnions` are deprecated aliases for `resolveFieldSchema` and `resolveFieldSchemaAllUnions`. They will be removed in the next minor version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/agent-schema",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Schema processing utilities for Sanity Agent",
5
5
  "keywords": [
6
6
  "sanity",
@@ -31,18 +31,10 @@
31
31
  "@sanity/client": ">=7.0.0",
32
32
  "groq-js": ">=1.0.0"
33
33
  },
34
- "peerDependenciesMeta": {
35
- "@sanity/client": {
36
- "optional": true
37
- },
38
- "groq-js": {
39
- "optional": true
40
- }
41
- },
42
34
  "devDependencies": {
43
35
  "@sanity/client": "7.14.1",
44
36
  "@sanity/pkg-utils": "^10.4.1",
45
- "groq-js": "^1.26.0",
37
+ "groq-js": "^1.28.0",
46
38
  "@types/node": "^22.19.7",
47
39
  "typescript": "^5.9.3",
48
40
  "vitest": "^4.0.18",