You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 8333a6c
Browse filesBrowse the repository at this point in the historyBrowse files
feat(spec): a blueprint nav entry can name the VIEW it opens (cloud#2150) (#16841)
* feat(spec): a blueprint nav entry can name the VIEW it opens (cloud#2150)
`BlueprintNavItemSchema` gets an optional `viewName`, and the OpenAI-strict
mirror's `StrictNavItem` gets it in lockstep (required-but-nullable, per the
strict convention).
Until now the shape could only say which OBJECT a menu entry opens. A model
that had just designed a kanban and wanted it in the app menu therefore had
exactly one move left: emit a SECOND entry at the same `target` and carry the
intent in `label`/`icon` alone. Both entries then opened the object's default
view, and the cloud applier derives a nav id from the target, so the two
collided — the user clicked 「工单看板」 and got the list. Nothing anywhere
complained: the target object really does exist, so the dangling-target lint
had nothing to say. The runtime nav item could always express this
(`ObjectNavItemSchema.viewName`, "Default list view to open"), so the gap was
the blueprint's alone and the duplicate entry was the reasonable move under
the expressiveness the model was given.
Both halves, deliberately. A key on the lenient side only is a fix on no path
the product runs: `propose_blueprint` validates its structured output against
the mirror, so the design step could never author a board entry while
`apply_blueprint` accepted a key nothing emits — the `seedData` divergence
(cloud#2118) with a different key name.
`viewName` is deliberately un-regexed on BOTH sides, unlike `target`: a view
answers to a bare key (`views[].name`) and to the qualified `<object>.<key>` a
staged view record carries, and the applier normalizes between them.
Constraining the leaf would make one spelling legal to generate and illegal to
apply — the cloud#1967 shape, where an already-approved blueprint was refused
wholesale.
The strict↔lenient key-parity pin is widened one level further out (fields →
objects → NAV ITEMS); ablating the mirror half fails four tests. Generated
authorable-surface and the blueprint reference are regenerated.
Part of objectstack-ai/cloud#2150.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(spec): classify the blueprint nav `viewName` empty state as `scope` (cloud#2150)
The empty-state gate refuses a permissive empty state nobody signed off on, and
the strict mirror's describe states one out loud ('or null for the object's
default list'). It is presentation breadth, not authorization: the entry's
`target` already fixes WHICH OBJECT is reachable, record- and field-level
security are enforced downstream, and the empty state is the behaviour every
existing app already has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
A blueprint nav entry can say WHICH view it opens: `viewName` is added to `BlueprintNavItemSchema` and, in lockstep, to the strict mirror's `StrictNavItem` (required-but-nullable, per the strict convention).
6
+
7
+
Without it the shape could only say which OBJECT an entry opens, so a model that had just designed a kanban and wanted it in the menu had one move left: emit a SECOND entry at the same `target` and carry the intent in `label`/`icon` alone. Both entries then opened the object's default view, and the consumer derived both ids from the target, so they collided — the user clicked 「工单看板」 and got the list, with nothing to see anywhere (the target object really exists, so a dangling-target lint has nothing to say). The runtime nav item could always express this — `ObjectNavItemSchema.viewName` is "Default list view to open" — so the gap was the blueprint's alone, and the model's duplicate entry was the reasonable move under the expressiveness it was given.
8
+
9
+
`viewName` is deliberately NOT `.regex(SNAKE_CASE)` on either side, unlike `target`. A view answers to two interchangeable spellings — the bare key a blueprint's `views[].name` carries and the qualified `<object>.<key>` a staged view record's `name` carries — and consumers normalize between them. Constraining the leaf would make one spelling legal to GENERATE and illegal to APPLY, the failure mode that once refused an already-approved blueprint wholesale.
10
+
11
+
The key-parity pin between the strict mirror and the lenient schema is widened a level further out — fields → objects → NAV ITEMS — so the next nav-level divergence fails a test rather than shipping as "the lenient side accepts a key no proposal can contain".
|**icon**|`string`| optional | Lucide icon for the App Launcher |
33
-
|**nav**|`{ type: Enum<'object' \| 'dashboard'>; target: string; label?: string; icon?: string }[]`| optional | Navigation entries; omit to auto-surface every created object and dashboard |
33
+
|**nav**|`{ type: Enum<'object' \| 'dashboard'>; target: string; label?: string; icon?: string; … }[]`| optional | Navigation entries; omit to auto-surface every created object and dashboard |
34
34
35
35
### Nested Shape: `BlueprintApp.nav[number]`
36
36
@@ -40,6 +40,7 @@ const result = BlueprintAppSchema.parse(data);
40
40
|**target**|`string`| ✅ | Object or dashboard machine name to surface (snake_case) |
41
41
|**label**|`string`| optional | Nav entry label (defaults to the target label/name) |
42
42
|**icon**|`string`| optional | Lucide icon name for the nav entry |
43
+
|**viewName**|`string`| optional | For type:"object" only — the `views[].name` this entry opens (e.g. "ticket_status_board"). Omit for the object's default list. SET it whenever this blueprint authors a kanban/calendar/gallery/gantt view the menu should reach: give the object ONE entry per view (a 「工单列表」 entry with no viewName plus a 「工单看板」 entry with viewName:"ticket_status_board"). Without it every entry on the same target opens the SAME default list, and a label/icon saying otherwise is decoration. |
43
44
44
45
45
46
---
@@ -173,6 +174,7 @@ const result = BlueprintAppSchema.parse(data);
173
174
|**target**|`string`| ✅ | Object or dashboard machine name to surface (snake_case) |
174
175
|**label**|`string`| optional | Nav entry label (defaults to the target label/name) |
175
176
|**icon**|`string`| optional | Lucide icon name for the nav entry |
177
+
|**viewName**|`string`| optional | For type:"object" only — the `views[].name` this entry opens (e.g. "ticket_status_board"). Omit for the object's default list. SET it whenever this blueprint authors a kanban/calendar/gallery/gantt view the menu should reach: give the object ONE entry per view (a 「工单列表」 entry with no viewName plus a 「工单看板」 entry with viewName:"ticket_status_board"). Without it every entry on the same target opens the SAME default list, and a label/icon saying otherwise is decoration. |
176
178
177
179
178
180
---
@@ -323,7 +325,7 @@ const result = BlueprintAppSchema.parse(data);
323
325
|**name**|`string`| ✅ | App machine name (snake_case) |
'Which error a catch node handles; empty catches all of them. Catching broadly is the safe direction for an error handler — the failure mode of the opposite default is an unhandled fault, not an over-grant.',
"Which VIEW a proposed app-menu entry opens; absent/null means the object's default list. It is presentation breadth, not authorization — the entry's `target` already fixes WHICH OBJECT is reachable, and record- and field-level security are enforced downstream, so naming a view can only ever change how the same permitted rows are laid out. The empty state is also the pre-existing behaviour every app already has, which is what makes it the safe default here (cloud#2150 added the key precisely because there was no way to say anything ELSE). Note the failure direction is cosmetic and visible: an entry labelled 「工单看板」 that omits this opens the list — wrong-looking, never over-granting.",
type: z.enum(['object','dashboard']).default('object').describe('What this nav entry opens'),
166
187
target: z.string().regex(SNAKE_CASE).describe('Object or dashboard machine name to surface (snake_case)'),
167
188
label: z.string().optional().describe('Nav entry label (defaults to the target label/name)'),
168
189
icon: z.string().optional().describe('Lucide icon name for the nav entry'),
190
+
viewName: z.string().optional()
191
+
.describe('For type:"object" only — the `views[].name` this entry opens (e.g. "ticket_status_board"). Omit for the object\'s default list. SET it whenever this blueprint authors a kanban/calendar/gallery/gantt view the menu should reach: give the object ONE entry per view (a 「工单列表」 entry with no viewName plus a 「工单看板」 entry with viewName:"ticket_status_board"). Without it every entry on the same target opens the SAME default list, and a label/icon saying otherwise is decoration.'),
target: strictIdent('Object or dashboard machine name to surface (snake_case)'),
348
371
label: z.string().nullable().describe('Nav entry label, or null'),
349
372
icon: z.string().nullable().describe('Lucide icon name, or null'),
373
+
// ⛔ Must stay in lockstep with the lenient `BlueprintNavItemSchema.viewName`
374
+
// (cloud#2150). THIS side is the one that decides whether the design step can
375
+
// author a board entry at all: `propose_blueprint`'s structured output is
376
+
// validated against this mirror, so a key present only in the lenient schema
377
+
// is a fix that exists nowhere on the main path — the applier would accept a
378
+
// `viewName` no proposal can ever contain. That is the `seedData` divergence
379
+
// (cloud#2118) with a different key name; the nav key-parity pin below fails
380
+
// if this line is removed.
381
+
viewName: z.string().nullable()
382
+
.describe('For type:"object" only — the `views[].name` this entry opens (e.g. "ticket_status_board"), or null for the object\'s default list. SET it whenever this blueprint authors a kanban/calendar/gallery/gantt view the menu should reach: give the object ONE entry per view (a 「工单列表」 entry with viewName null plus a 「工单看板」 entry with viewName "ticket_status_board"). Null on every entry means they all open the SAME default list, and a label/icon saying otherwise is decoration.'),
0 commit comments