@jini-ai/cms 0.5.0 → 0.5.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 — 2026-10-06
4
+
5
+ - Navigation: the `menus_update_menu_tree` schema declares the `entryType`/`lastKnownHref` hints on entry targets, and `updateMenuTree` carries a stored hint forward when the same item still targets the same entry and the submitted target omits it, so echoing a read no longer marks the menu changed.
6
+
3
7
  ## 0.5.0 — 2026-10-05
4
8
 
5
9
  ### BREAKING
@@ -84,7 +84,15 @@ const NAV_TARGET_SCHEMA = {
84
84
  type: "object",
85
85
  additionalProperties: false,
86
86
  required: ["kind", "entryId"],
87
- properties: { kind: { const: "entryRef" }, entryId: { type: "string", description: "Target entry id (e.g. a page or post)." } },
87
+ properties: {
88
+ kind: { const: "entryRef" },
89
+ entryId: { type: "string", description: "Target entry id (e.g. a page or post)." },
90
+ // Declared so a read can be echoed back unchanged: these were stored on page links but absent
91
+ // here, so every agent save dropped them (dry run 2026-10-05). The save also keeps the stored
92
+ // value when omitted for the same item id + entryId (`carryForwardEntryHints`).
93
+ entryType: { type: "string", description: "Content-type hint, e.g. 'page' or 'post'. Copy it unchanged from the read; omit for a new link." },
94
+ lastKnownHref: { type: "string", description: "The entry's last known public URL, e.g. '/about'. Copy it unchanged from the read; omit for a new link." },
95
+ },
88
96
  },
89
97
  {
90
98
  type: "object",
@@ -207,7 +215,7 @@ export const menusAgentToolCatalog = [
207
215
  name: "menus_update_menu_tree",
208
216
  description: "Replaces an existing menu's whole item tree (and optionally its title/slug) in one version-guarded write. " +
209
217
  "Always a whole-tree replace — there is no per-item patch operation, so the submitted tree must include every " +
210
- "item the menu keeps. To delete a menu, use trash_item with entityType 'menu' (it moves to the Trash and can be restored).",
218
+ "item the menu keeps, each with its fields copied unchanged from the read. To delete a menu, use trash_item with entityType 'menu' (it moves to the Trash and can be restored).",
211
219
  sideEffects: "mutates-durable-state",
212
220
  authorization: { permission: "admin.menus.update" },
213
221
  inputSchema: {
@@ -198,6 +198,18 @@ export interface UpdateMenuTreeRequired {
198
198
  export interface UpdateMenuTreeOptional {
199
199
  limits?: TreeValidationLimits | undefined;
200
200
  }
201
+ /**
202
+ * Whole-tree saves arrive from callers that may not know an item's stored hint fields (an agent
203
+ * echoing a read through a schema, an older editor build). Dropping them on every save made an
204
+ * untouched menu look changed (dry run 2026-10-05: ~30 header page links lost `entryType: "page"`
205
+ * on one assistant save and the menu showed pending-to-live). So a hint is kept when the same item
206
+ * id still points at the same entry and the submitted target leaves that hint out; a re-pointed
207
+ * link or an explicitly submitted hint is taken as sent.
208
+ */
209
+ export declare function carryForwardEntryHints({ previous, next }: {
210
+ previous: readonly NavItemNode[];
211
+ next: readonly NavItemNode[];
212
+ }, _optional?: {}): NavItemNode[];
201
213
  /**
202
214
  * Replaces a menu's whole item tree, guarded by optimistic concurrency on the
203
215
  * entry `version` (matches the host's `updatePost` pattern): `expectedVersion` must equal the version
@@ -327,6 +327,44 @@ export async function createMenu(required, optional = {}) {
327
327
  }));
328
328
  return { menu };
329
329
  }
330
+ /** The stored `entryRef` hint fields a whole-tree save carries forward when the submitted target omits them. */
331
+ const ENTRY_HINT_KEYS = ["entryType", "lastKnownHref"];
332
+ function entryTargetsById(nodes, into = new Map()) {
333
+ for (const node of nodes) {
334
+ if (node.target?.kind === "entryRef")
335
+ into.set(node.id, node.target);
336
+ if (node.children)
337
+ entryTargetsById(node.children, into);
338
+ }
339
+ return into;
340
+ }
341
+ function withStoredHints(target, stored) {
342
+ if (target.kind !== "entryRef" || !stored || stored.entryId !== target.entryId)
343
+ return target;
344
+ const merged = { ...target };
345
+ for (const key of ENTRY_HINT_KEYS) {
346
+ if (merged[key] === undefined && stored[key] !== undefined)
347
+ merged[key] = stored[key];
348
+ }
349
+ return merged;
350
+ }
351
+ /**
352
+ * Whole-tree saves arrive from callers that may not know an item's stored hint fields (an agent
353
+ * echoing a read through a schema, an older editor build). Dropping them on every save made an
354
+ * untouched menu look changed (dry run 2026-10-05: ~30 header page links lost `entryType: "page"`
355
+ * on one assistant save and the menu showed pending-to-live). So a hint is kept when the same item
356
+ * id still points at the same entry and the submitted target leaves that hint out; a re-pointed
357
+ * link or an explicitly submitted hint is taken as sent.
358
+ */
359
+ export function carryForwardEntryHints({ previous, next }, _optional = {}) {
360
+ const stored = entryTargetsById(previous);
361
+ const walk = (nodes) => nodes.map((node) => ({
362
+ ...node,
363
+ target: withStoredHints(node.target, stored.get(node.id)),
364
+ children: node.children ? walk(node.children) : undefined,
365
+ }));
366
+ return walk(next);
367
+ }
330
368
  /**
331
369
  * Replaces a menu's whole item tree, guarded by optimistic concurrency on the
332
370
  * entry `version` (matches the host's `updatePost` pattern): `expectedVersion` must equal the version
@@ -369,7 +407,10 @@ export async function updateMenuTree(required, optional = {}) {
369
407
  throw new MenuConflictError({ message: `slug '${slug}' already exists` });
370
408
  }
371
409
  }
372
- const items = validateAndCloneTree({ items: input.items }, optional.limits);
410
+ const items = carryForwardEntryHints({
411
+ previous: existing.doc.items,
412
+ next: validateAndCloneTree({ items: input.items }, optional.limits),
413
+ });
373
414
  const menu = {
374
415
  ...existing,
375
416
  title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/cms",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {