@avocadostudio-ai/site-sdk 0.15.0 → 0.16.0

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.
@@ -183,19 +183,62 @@ export function createLens(options) {
183
183
  const byId = new Map(sourceRows.map((row) => [row[primitives.rowIdKey], row]));
184
184
  const seen = new Set();
185
185
  const merged = [];
186
- for (const item of items) {
186
+ /*
187
+ * A list whose source rows carry no identity at all.
188
+ *
189
+ * It is not a rare shape: a CMS query written before Avocado existed
190
+ * selects the fields a template renders, and a row's `_key` is not one of
191
+ * them. Matching then finds nothing, and the old code read "matches
192
+ * nothing" as "every row is new" — so a projection merged straight back
193
+ * appended a second copy of every row and kept the originals. Two buttons
194
+ * became four on a no-op, and the fabricated rows were built from the
195
+ * projection, so they carried one language and none of the fields the
196
+ * table does not declare.
197
+ *
198
+ * Position is the only identity left, and using it is better than the
199
+ * alternatives: refusing makes the list uneditable, and fabricating makes
200
+ * it grow. It is also *wrong* for a reorder, silently — the rows swap
201
+ * their undeclared fields — so this writes and warns rather than writing
202
+ * quietly. The remedy is one line in the integrator's query, and the
203
+ * warning names it.
204
+ */
205
+ const keyless = sourceRows.length > 0 && sourceRows.every((row) => row[primitives.rowIdKey] === undefined);
206
+ let usedPositionalMatch = false;
207
+ const rowWarnings = [];
208
+ for (const [index, item] of items.entries()) {
187
209
  const rowId = item[primitives.rowIdKey];
188
- const source = rowId === undefined ? undefined : byId.get(rowId);
210
+ let source = rowId === undefined ? undefined : byId.get(rowId);
189
211
  if (source)
190
212
  seen.add(rowId);
213
+ if (!source && keyless && index < sourceRows.length) {
214
+ source = sourceRows[index];
215
+ usedPositionalMatch = true;
216
+ }
191
217
  if ("itemFields" in spec) {
192
- const base = source ? { ...source } : { [primitives.rowIdKey]: primitives.newRowId() };
218
+ /*
219
+ * A row matched by position keeps the identity it had, which is none.
220
+ * Stamping a fresh `rowIdKey` onto it would be this codec inventing a
221
+ * field the document never had — a schema change smuggled in by an
222
+ * edit to a label.
223
+ */
224
+ const base = source
225
+ ? { ...source }
226
+ : keyless
227
+ ? {}
228
+ : { [primitives.rowIdKey]: primitives.newRowId() };
193
229
  let rowChanged = !source;
194
230
  for (const [itemKey, itemSpec] of Object.entries(spec.itemFields)) {
195
231
  const itemCtx = { spec: itemSpec, lang: ctx.lang, type: ctx.type, where: `${ctx.where}[] > ${itemKey}` };
196
232
  const before = source ? readField(source, itemKey, ctx.lang, itemSpec, ctx.type) : undefined;
197
233
  const outcome = codecFor(itemSpec).merge(itemKey, item, before, itemCtx);
198
- if (!outcome || "warning" in outcome)
234
+ if (!outcome)
235
+ continue;
236
+ // A row's own hazards travel up with the list's. A nested list is
237
+ // the case that matters: its rows are keyless far more often than a
238
+ // top-level list's, and it is a level deeper than anyone looks.
239
+ if (outcome.warning)
240
+ rowWarnings.push(outcome.warning);
241
+ if (!("value" in outcome))
199
242
  continue;
200
243
  writeField(base, itemKey, ctx.lang, itemSpec, ctx.type, outcome.value);
201
244
  rowChanged = true;
@@ -221,16 +264,27 @@ export function createLens(options) {
221
264
  const result = merge(source ?? { [typeKey]: rowType, [primitives.rowIdKey]: primitives.newRowId() }, item, rowType, ctx.lang, `${ctx.where}[]`);
222
265
  merged.push(result.doc);
223
266
  }
224
- // Rows the table does not describe keep their place rather than vanishing.
225
- for (const [index, row] of sourceRows.entries()) {
226
- const rowId = row[primitives.rowIdKey];
227
- if (seen.has(rowId))
228
- continue;
229
- const typeKey = primitives.rowTypeKey;
230
- const described = typeKey ? specFor(String(row[typeKey])) !== undefined : true;
231
- if (described)
232
- continue;
233
- merged.splice(Math.min(index, merged.length), 0, row);
267
+ /*
268
+ * Rows the table does not describe keep their place rather than vanishing.
269
+ *
270
+ * Only `of` lists can hold such a row. An `itemFields` list describes
271
+ * every row it has by construction, so there is nothing here to rescue —
272
+ * and running this pass over one was the second half of the duplication
273
+ * bug: `specFor(String(undefined))` is undefined, every row read as
274
+ * undescribed, and each got re-inserted beside the copy the loop above
275
+ * had just built.
276
+ */
277
+ if (!("itemFields" in spec)) {
278
+ for (const [index, row] of sourceRows.entries()) {
279
+ const rowId = row[primitives.rowIdKey];
280
+ if (seen.has(rowId))
281
+ continue;
282
+ const typeKey = primitives.rowTypeKey;
283
+ const described = typeKey ? specFor(String(row[typeKey])) !== undefined : true;
284
+ if (described)
285
+ continue;
286
+ merged.splice(Math.min(index, merged.length), 0, row);
287
+ }
234
288
  }
235
289
  /*
236
290
  * Compared structurally, and deliberately not by counting rows.
@@ -242,7 +296,19 @@ export function createLens(options) {
242
296
  * reorder, add and delete, and says nothing about a list that only looks
243
297
  * shorter from Avocado's side.
244
298
  */
245
- return JSON.stringify(merged) === JSON.stringify(sourceRows) ? null : { value: merged };
299
+ if (JSON.stringify(merged) === JSON.stringify(sourceRows))
300
+ return null;
301
+ const hazards = usedPositionalMatch
302
+ ? [
303
+ `"${key}" holds rows with no "${primitives.rowIdKey}", so they were matched to the stored rows by ` +
304
+ `position. An edit lands correctly, but a reorder or a deletion moves every row's undeclared fields ` +
305
+ `onto its neighbour. Select "${primitives.rowIdKey}" for this list in the query that reads it.`,
306
+ ...rowWarnings
307
+ ]
308
+ : rowWarnings;
309
+ if (hazards.length === 0)
310
+ return { value: merged };
311
+ return { value: merged, warning: [...new Set(hazards)].join(" ") };
246
312
  }
247
313
  };
248
314
  /**
@@ -294,10 +360,18 @@ export function createLens(options) {
294
360
  const outcome = codecFor(spec).merge(key, props, before, ctx);
295
361
  if (!outcome)
296
362
  continue;
297
- if ("warning" in outcome) {
363
+ if (!("value" in outcome)) {
298
364
  warnings.push({ where: ctx.where, reason: outcome.warning });
299
365
  continue;
300
366
  }
367
+ /*
368
+ * Reported whether or not the write goes on to happen. A codec that
369
+ * returns both has stored something *and* found a hazard in how it had to
370
+ * store it; swallowing the second half was how a list that doubled its
371
+ * rows came back `changed: true, warnings: []`.
372
+ */
373
+ if (outcome.warning)
374
+ warnings.push({ where: ctx.where, reason: outcome.warning });
301
375
  /*
302
376
  * Asked here, about a real change, and deliberately not earlier. The
303
377
  * first version of this re-projected the source and compared JSON before
@@ -1,6 +1,6 @@
1
1
  export { createLens } from "./create-lens.ts";
2
2
  export type { Lens, LensOptions, MergeResult, MergeWarning, ProjectOptions } from "./create-lens.ts";
3
- export { registerFieldTable, topLevelTypes } from "./register.ts";
3
+ export { registerFieldTable, registerLens, topLevelTypes } from "./register.ts";
4
4
  export type { RegisterOptions } from "./register.ts";
5
5
  export { suffixNaming } from "./types.ts";
6
6
  export type { BlockSpec, CodecContext, FieldCodec, FieldSpec, FieldTable, ImageNaming, LocaleLens, MergeOutcome, Primitives } from "./types.ts";
@@ -20,6 +20,6 @@
20
20
  * object you cannot build twice and make the order of two imports matter.
21
21
  */
22
22
  export { createLens } from "./create-lens.js";
23
- export { registerFieldTable, topLevelTypes } from "./register.js";
23
+ export { registerFieldTable, registerLens, topLevelTypes } from "./register.js";
24
24
  export { suffixNaming } from "./types.js";
25
25
  export { changed } from "./scalar-codecs.js";
@@ -23,6 +23,18 @@ export type RegisterOptions = {
23
23
  narrowCatalogue?: boolean;
24
24
  };
25
25
  export declare function registerFieldTable(table: FieldTable, options?: RegisterOptions): void;
26
+ /**
27
+ * Register a lens's own table, with the lens's own primitives.
28
+ *
29
+ * The two-argument call is two declarations that have to agree; this is one
30
+ * that cannot disagree with itself. Prefer it. `registerFieldTable` remains for
31
+ * a table that has no lens — a site that registers blocks without projecting a
32
+ * CMS through them.
33
+ */
34
+ export declare function registerLens(lens: {
35
+ table: FieldTable;
36
+ primitives: Pick<Primitives, "imageNaming"> & Partial<Pick<Primitives, "rowIdKey" | "rowTypeKey">>;
37
+ }, options?: Omit<RegisterOptions, "primitives">): void;
26
38
  /**
27
39
  * The types a page body may hold, which is not every type in the table.
28
40
  *
@@ -177,12 +177,25 @@ function metaForField(key, spec) {
177
177
  meta.imageSpec = spec.imageSpec;
178
178
  return meta;
179
179
  }
180
- /** The same expansion as `shapeForFields`, as panel metadata rather than schema. */
180
+ /**
181
+ * The same expansion as `shapeForFields`, as panel metadata rather than schema.
182
+ *
183
+ * Lists come back separately. `metaForField` renders a `list` as `kind: "text"`,
184
+ * which is harmless for a block's own list — that one travels `listFields` and
185
+ * the kind is never read — and destructive for a row's, which had no second
186
+ * channel to travel and so arrived at the panel as a text input pointed at an
187
+ * array of objects. Now it has one.
188
+ */
181
189
  function metaForFields(fields, ctx) {
182
190
  const out = {};
191
+ const lists = {};
183
192
  if (ctx.rowIdKey)
184
193
  out[ctx.rowIdKey] = { kind: "text", label: humanise(ctx.rowIdKey), internal: true };
185
194
  for (const [key, spec] of Object.entries(fields)) {
195
+ if (spec.kind === "list") {
196
+ lists[key] = listMetaFor(key, spec, ctx);
197
+ continue;
198
+ }
186
199
  if (spec.kind === "image") {
187
200
  out[ctx.naming.url(key)] = metaForField(key, spec);
188
201
  out[ctx.naming.alt(key)] = { kind: "imageAlt", label: `${spec.label ?? humanise(key)} alt` };
@@ -190,7 +203,7 @@ function metaForFields(fields, ctx) {
190
203
  }
191
204
  out[key] = metaForField(key, spec);
192
205
  }
193
- return out;
206
+ return { fields: out, listFields: lists };
194
207
  }
195
208
  /**
196
209
  * A list's panel metadata.
@@ -204,24 +217,40 @@ function metaForFields(fields, ctx) {
204
217
  */
205
218
  function listMetaFor(key, spec, ctx) {
206
219
  const label = spec.label ?? humanise(key);
207
- if ("itemFields" in spec)
208
- return { label, itemFields: metaForFields(spec.itemFields, ctx) };
220
+ if ("itemFields" in spec) {
221
+ const row = metaForFields(spec.itemFields, ctx);
222
+ return {
223
+ label,
224
+ itemFields: row.fields,
225
+ ...(Object.keys(row.listFields).length > 0 ? { itemListFields: row.listFields } : {})
226
+ };
227
+ }
209
228
  const types = rowTypesFor(spec.of, ctx);
210
229
  if (types.length === 0)
211
230
  return { label, itemFields: {} };
212
231
  const itemFieldsByType = {};
213
232
  const merged = {};
233
+ const mergedLists = {};
214
234
  for (const type of types) {
215
- const fields = metaForFields(ctx.table[type].fields, {
235
+ const row = metaForFields(ctx.table[type].fields, {
216
236
  ...ctx,
217
237
  visiting: new Set([...ctx.visiting, type])
218
238
  });
219
- itemFieldsByType[type] = fields;
220
- for (const [fieldKey, meta] of Object.entries(fields))
239
+ itemFieldsByType[type] = row.fields;
240
+ for (const [fieldKey, meta] of Object.entries(row.fields))
221
241
  if (!(fieldKey in merged))
222
242
  merged[fieldKey] = meta;
243
+ for (const [fieldKey, meta] of Object.entries(row.listFields))
244
+ if (!(fieldKey in mergedLists))
245
+ mergedLists[fieldKey] = meta;
223
246
  }
224
- return { label, itemFields: merged, discriminator: ctx.rowTypeKey, itemFieldsByType };
247
+ return {
248
+ label,
249
+ itemFields: merged,
250
+ discriminator: ctx.rowTypeKey,
251
+ itemFieldsByType,
252
+ ...(Object.keys(mergedLists).length > 0 ? { itemListFields: mergedLists } : {})
253
+ };
225
254
  }
226
255
  /**
227
256
  * Register every type in the table as an Avocado block type.
@@ -262,7 +291,47 @@ function assertDrawable(type, key, spec) {
262
291
  * no `FieldKind` spelling.
263
292
  */
264
293
  const KNOWN_KINDS = [...FIELD_KINDS, "list"];
294
+ /** Does this field set declare an image anywhere, including inside a row? */
295
+ function declaresAnImage(fields, seen = new Set()) {
296
+ for (const spec of Object.values(fields)) {
297
+ if (spec.kind === "image" || spec.kind === "imageList")
298
+ return true;
299
+ if (spec.kind === "list" && "itemFields" in spec && declaresAnImage(spec.itemFields, seen))
300
+ return true;
301
+ }
302
+ return false;
303
+ }
304
+ /**
305
+ * Refuse a table with images when nobody said what an image's props are called.
306
+ *
307
+ * `registerFieldTable` defaulted to `suffixNaming("", "_alt")` — Storyblok's
308
+ * answer — for every CMS, while `sanityPrimitives()` defaults to
309
+ * `suffixNaming("Url", "Alt")`. Same table, same package, two calls, and the
310
+ * defaults disagree: the panel drew `image` and `image_alt`, the projection
311
+ * emitted `imageUrl` and `imageAlt`, and every image on the site was both
312
+ * invisible and uneditable. Nothing errored, because neither half is wrong on
313
+ * its own.
314
+ *
315
+ * `RegisterOptions` has documented this exact symptom for as long as the
316
+ * option has existed, which is the point: an integration that never reads that
317
+ * doc gets the broken pairing by default, and the only evidence is an asset
318
+ * picker that never appears. So the default is withdrawn where it can do harm.
319
+ * A table with no image is unaffected.
320
+ */
321
+ function assertImageNamingDeclared(table, options) {
322
+ if (options?.primitives?.imageNaming)
323
+ return;
324
+ for (const [type, blockSpec] of Object.entries(table)) {
325
+ if (!declaresAnImage(blockSpec.fields))
326
+ continue;
327
+ throw new Error(`registerFieldTable: ${type} declares an image, but no image naming was given. The panel and the ` +
328
+ `projection have to agree on what an image field's two props are called, and their defaults do not ` +
329
+ `match. Pass the same pack you gave createLens — registerFieldTable(table, { primitives: ` +
330
+ `sanityPrimitives() }) — or call registerLens(lens), which cannot get this wrong.`);
331
+ }
332
+ }
265
333
  export function registerFieldTable(table, options) {
334
+ assertImageNamingDeclared(table, options);
266
335
  const naming = options?.primitives?.imageNaming ?? suffixNaming("", "_alt");
267
336
  const ctx = {
268
337
  naming,
@@ -309,6 +378,17 @@ export function registerFieldTable(table, options) {
309
378
  .map(([type]) => type));
310
379
  }
311
380
  }
381
+ /**
382
+ * Register a lens's own table, with the lens's own primitives.
383
+ *
384
+ * The two-argument call is two declarations that have to agree; this is one
385
+ * that cannot disagree with itself. Prefer it. `registerFieldTable` remains for
386
+ * a table that has no lens — a site that registers blocks without projecting a
387
+ * CMS through them.
388
+ */
389
+ export function registerLens(lens, options) {
390
+ registerFieldTable(lens.table, { ...options, primitives: lens.primitives });
391
+ }
312
392
  /**
313
393
  * The types a page body may hold, which is not every type in the table.
314
394
  *
@@ -8,7 +8,24 @@ import type { ImageNaming, LocaleLens, Primitives } from "./types.ts";
8
8
  * precisely the overwrite a field-level publish diff exists to prevent.
9
9
  */
10
10
  export declare function imageUrl(image: unknown): string;
11
- export declare function imageAlt(image: unknown): string;
11
+ /**
12
+ * An image's alt text, in one language.
13
+ *
14
+ * Alt text is content, so a field-level-i18n CMS localises it like any other
15
+ * string: a tri-lingual Sanity site models it as `localeString`, i.e. `{de,fr,en}` on
16
+ * the image object. This used to be `String(image.alt)`, which rendered every
17
+ * such alt as the literal `"[object Object]"` in the property panel — and then
18
+ * an editor who corrected that garbage wrote a flat string over the container,
19
+ * deleting the other two languages with no warning.
20
+ *
21
+ * The container sits *inside* a field the table declares `localized: false`,
22
+ * because the asset reference really is shared across languages. So no
23
+ * declaration could have expressed this and none is asked for: an object here
24
+ * is read as a locale container, always. The one thing never done again is
25
+ * coercing it — an unreadable alt projects as empty, which is true, rather than
26
+ * as a string that looks like text and is not.
27
+ */
28
+ export declare function imageAlt(image: unknown, lang?: string): string;
12
29
  /**
13
30
  * The locale lens for a site whose translatable fields are `{de, fr, en}`.
14
31
  *
@@ -34,8 +34,33 @@ export function imageUrl(image) {
34
34
  const asset = isObj(img.asset) ? img.asset : undefined;
35
35
  return str(asset?.url);
36
36
  }
37
- export function imageAlt(image) {
38
- return isObj(image) ? str(image.alt) : "";
37
+ /**
38
+ * An image's alt text, in one language.
39
+ *
40
+ * Alt text is content, so a field-level-i18n CMS localises it like any other
41
+ * string: a tri-lingual Sanity site models it as `localeString`, i.e. `{de,fr,en}` on
42
+ * the image object. This used to be `String(image.alt)`, which rendered every
43
+ * such alt as the literal `"[object Object]"` in the property panel — and then
44
+ * an editor who corrected that garbage wrote a flat string over the container,
45
+ * deleting the other two languages with no warning.
46
+ *
47
+ * The container sits *inside* a field the table declares `localized: false`,
48
+ * because the asset reference really is shared across languages. So no
49
+ * declaration could have expressed this and none is asked for: an object here
50
+ * is read as a locale container, always. The one thing never done again is
51
+ * coercing it — an unreadable alt projects as empty, which is true, rather than
52
+ * as a string that looks like text and is not.
53
+ */
54
+ export function imageAlt(image, lang) {
55
+ if (!isObj(image))
56
+ return "";
57
+ const alt = image.alt;
58
+ if (typeof alt === "string")
59
+ return alt;
60
+ if (!isObj(alt))
61
+ return "";
62
+ const value = lang === undefined ? undefined : alt[lang];
63
+ return typeof value === "string" ? value : "";
39
64
  }
40
65
  /**
41
66
  * An image whose alt text is editable and whose asset is not.
@@ -45,19 +70,26 @@ export function imageAlt(image) {
45
70
  * repoint a reference at something the CMS has no record of.
46
71
  */
47
72
  const imageCodec = (naming) => ({
48
- project(key, raw) {
49
- return { [naming.url(key)]: imageUrl(raw), [naming.alt(key)]: imageAlt(raw) };
73
+ project(key, raw, ctx) {
74
+ return { [naming.url(key)]: imageUrl(raw), [naming.alt(key)]: imageAlt(raw, ctx.lang) };
50
75
  },
51
- merge(key, props, before) {
76
+ merge(key, props, before, ctx) {
52
77
  const altKey = naming.alt(key);
53
78
  if (!(altKey in props))
54
79
  return null;
55
80
  const nextAlt = str(props[altKey]);
56
- if (!changed(nextAlt, imageAlt(before)))
81
+ if (!changed(nextAlt, imageAlt(before, ctx.lang)))
57
82
  return null;
58
83
  if (!isObj(before))
59
84
  return null;
60
- return { value: { ...before, alt: nextAlt } };
85
+ /*
86
+ * Written back into the language's slot, never over the container. The
87
+ * shape the source had is the shape it keeps: a localised alt stays
88
+ * localised and the untouched languages ride along, exactly as every other
89
+ * localised field in this lens behaves.
90
+ */
91
+ const alt = isObj(before.alt) ? { ...before.alt, [ctx.lang]: nextAlt } : nextAlt;
92
+ return { value: { ...before, alt } };
61
93
  }
62
94
  });
63
95
  const fileCodec = {
@@ -119,16 +151,16 @@ const richTextCodec = {
119
151
  }
120
152
  };
121
153
  const imageListCodec = () => ({
122
- project(key, raw) {
154
+ project(key, raw, ctx) {
123
155
  return {
124
156
  [key]: (Array.isArray(raw) ? raw : []).map((img) => ({
125
157
  image: imageUrl(img),
126
- alt: imageAlt(img),
158
+ alt: imageAlt(img, ctx.lang),
127
159
  _key: isObj(img) ? img._key : undefined
128
160
  }))
129
161
  };
130
162
  },
131
- merge(key, props, before) {
163
+ merge(key, props, before, ctx) {
132
164
  if (!(key in props))
133
165
  return null;
134
166
  const items = Array.isArray(props[key]) ? props[key] : [];
@@ -137,9 +169,11 @@ const imageListCodec = () => ({
137
169
  const merged = items.map((item, i) => {
138
170
  const src = (item._key !== undefined ? byKey.get(item._key) : undefined) ?? sourceItems[i] ?? {};
139
171
  const alt = str(item.alt);
140
- if (!isObj(src) || alt === imageAlt(src))
172
+ if (!isObj(src) || alt === imageAlt(src, ctx.lang))
141
173
  return src;
142
- return { ...src, alt };
174
+ // Same rule as the single image: into the language's slot, never over the
175
+ // container that holds the other languages.
176
+ return { ...src, alt: isObj(src.alt) ? { ...src.alt, [ctx.lang]: alt } : alt };
143
177
  });
144
178
  if (JSON.stringify(merged) === JSON.stringify(sourceItems))
145
179
  return null;
@@ -212,8 +212,20 @@ export type CodecContext = {
212
212
  * showed, so nothing looks wrong. A codec returns a value only when the value
213
213
  * really differs from what the source holds.
214
214
  */
215
- export type MergeOutcome = null | {
215
+ export type MergeOutcome = null
216
+ /**
217
+ * A value to store, and optionally a reason the integrator is owed anyway.
218
+ *
219
+ * A merge that writes is not the same question as a merge that is safe. A
220
+ * list whose source rows carry no `rowIdKey` can only be matched by position,
221
+ * which is the right answer often enough to be worth doing and wrong in a way
222
+ * nobody can see — so it writes *and* says so. Without this the only way to
223
+ * report a hazard was to refuse the edit, so the codec that knew stayed
224
+ * silent and the corruption shipped.
225
+ */
226
+ | {
216
227
  value: unknown;
228
+ warning?: string;
217
229
  }
218
230
  /** The edit cannot be stored, and the editor is owed the reason. */
219
231
  | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avocadostudio-ai/site-sdk",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -147,17 +147,17 @@
147
147
  ],
148
148
  "dependencies": {
149
149
  "zod": "^4.3.6",
150
- "@avocadostudio-ai/blocks": "^0.15.0",
151
- "@avocadostudio-ai/richtext": "^0.15.0",
152
- "@avocadostudio-ai/preview-adapter": "^0.15.0",
153
- "@avocadostudio-ai/shared": "^0.15.0"
150
+ "@avocadostudio-ai/blocks": "^0.16.0",
151
+ "@avocadostudio-ai/richtext": "^0.16.0",
152
+ "@avocadostudio-ai/preview-adapter": "^0.16.0",
153
+ "@avocadostudio-ai/shared": "^0.16.0"
154
154
  },
155
155
  "peerDependencies": {
156
156
  "next": ">=15.0.0",
157
157
  "react": ">=19.0.0",
158
158
  "react-dom": ">=19.0.0",
159
159
  "better-sqlite3": ">=12.0.0",
160
- "@avocadostudio-ai/orchestrator-core": "^0.15.0"
160
+ "@avocadostudio-ai/orchestrator-core": "^0.16.0"
161
161
  },
162
162
  "peerDependenciesMeta": {
163
163
  "@avocadostudio-ai/orchestrator-core": {