Skip to content

Commit 66d5932

Browse files
os-zhuangclaude
andcommitted
feat(spec): gallery + gantt are first-class blueprint view kinds
The AI build agent's blueprint proposal could only express list/form/kanban/ calendar views — both the lenient schema and the strict mirror (the structured-output contract the model is validated against) capped `view.type` at those four. So a user asking for a 画廊/gallery or 甘特图/gantt could never get one from a blueprint: the closest allowed enum value (list) won and the view silently downgraded to a grid, even though the view spec (ListViewSchema) and the objectui renderers (ObjectGallery, plugin-gantt) fully support both. Widen BlueprintViewSchema.type and the StrictView mirror to include 'gallery' and 'gantt', with guidance in the field descriptions on when to pick each and which columns to include (the image field as the gallery cover; the start date before the end date for a gantt). No new blueprint slots — the cover image and the gantt date fields are inferred downstream from the object's fields, exactly as kanban's groupBy already is. Consumed by @objectstack/cloud's service-ai-studio (blueprint LIST_TYPE + viewBody honor the new kinds; the build skill teaches them). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent d710092 commit 66d5932

2 files changed

Lines changed: 37 additions & 7 deletions

File tree

packages/spec/src/ai/solution-blueprint.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ describe('SolutionBlueprintSchema', () => {
5353
expect(parsed.views?.[0].type).toBe('list');
5454
});
5555

56+
it('accepts gallery and gantt as first-class view kinds (not only list/form/kanban/calendar)', () => {
57+
// A build agent that wants a poster wall or a schedule must be able to PROPOSE
58+
// a gallery/gantt view directly — otherwise the closest allowed enum value
59+
// (list) wins and the view silently downgrades to a grid.
60+
const parsed = SolutionBlueprintSchema.parse({
61+
summary: 'events',
62+
objects: [{ name: 'event', fields: [{ name: 'name', type: 'text' }, { name: 'poster', type: 'image' }] }],
63+
views: [
64+
{ object: 'event', name: 'poster_wall', type: 'gallery', columns: ['poster', 'name'] },
65+
{ object: 'event', name: 'schedule', type: 'gantt', columns: ['name'] },
66+
],
67+
});
68+
expect(parsed.views?.map((v) => v.type)).toEqual(['gallery', 'gantt']);
69+
});
70+
5671
it('rejects a missing summary', () => {
5772
const { summary: _drop, ...noSummary } = validBlueprint;
5873
expect(() => SolutionBlueprintSchema.parse(noSummary)).toThrow();
@@ -204,6 +219,20 @@ describe('SolutionBlueprintStrictSchema (OpenAI strict mirror)', () => {
204219
expect(parsed.app).toBeNull();
205220
});
206221

222+
it('accepts gallery/gantt view kinds in the strict mirror (the structured-output contract)', () => {
223+
// This is the schema the build agent's structured output is validated against,
224+
// so the gallery/gantt enum values MUST live here too — else the model can
225+
// never emit them and a requested gallery degrades to a list/grid.
226+
const parsed = SolutionBlueprintStrictSchema.parse({
227+
...strictBp,
228+
views: [
229+
{ object: 'event', name: 'wall', label: null, type: 'gallery', columns: null, groupBy: null },
230+
{ object: 'event', name: 'plan', label: null, type: 'gantt', columns: null, groupBy: null },
231+
],
232+
});
233+
expect(parsed.views?.map((v) => v.type)).toEqual(['gallery', 'gantt']);
234+
});
235+
207236
it('requires every top-level key to be present (OpenAI strict needs all in `required`)', () => {
208237
const { views: _v, ...missingViews } = strictBp;
209238
expect(() => SolutionBlueprintStrictSchema.parse(missingViews)).toThrow();

packages/spec/src/ai/solution-blueprint.zod.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,17 @@ export const BlueprintObjectSchema = lazySchema(() => z.object({
5050
}));
5151
export type BlueprintObject = z.infer<typeof BlueprintObjectSchema>;
5252

53-
/** A proposed list/form/kanban/calendar view over an object. */
53+
/** A proposed list/form/kanban/calendar/gallery/gantt view over an object. */
5454
export const BlueprintViewSchema = lazySchema(() => z.object({
5555
object: z.string().regex(SNAKE_CASE).describe('Object this view displays (snake_case)'),
5656
name: z.string().regex(SNAKE_CASE).describe('View machine name (snake_case)'),
5757
label: z.string().optional().describe('Human-readable view label'),
58-
type: z.enum(['list', 'form', 'kanban', 'calendar']).default('list').describe('View kind'),
58+
type: z.enum(['list', 'form', 'kanban', 'calendar', 'gallery', 'gantt']).default('list')
59+
.describe('View kind. Pick the surface that fits the data: "gallery" for a visual card/cover browse when the user asks for a 画廊/相册/卡片墙/封面/海报/图集 (a gallery / card wall / cover / poster grid) or the object has an image/avatar/file field worth showing as a card cover; "gantt" for a 甘特图/时间线/排期 (timeline / schedule) when the object has BOTH a start and an end date field; "kanban" for a board grouped by a status/select field; "calendar" for a single-date schedule; "form" for a record editor; else "list".'),
5960
columns: z.array(z.string().regex(SNAKE_CASE)).optional()
60-
.describe('Field names shown as columns (in order)'),
61+
.describe('Field names shown as columns (in order). For a gallery, INCLUDE the image/avatar/file field (it becomes the card cover); for a gantt, INCLUDE the start date column before the end date column.'),
6162
groupBy: z.string().regex(SNAKE_CASE).optional()
62-
.describe('REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list.'),
63+
.describe('REQUIRED for kanban views: the select/status field whose options become the board columns (e.g. "stage", "status"). Without it a kanban renders as a plain list. Optional for gantt (groups leaf tasks into summary rows).'),
6364
}));
6465
export type BlueprintView = z.infer<typeof BlueprintViewSchema>;
6566

@@ -203,9 +204,9 @@ const StrictView = z.object({
203204
object: z.string().describe('Object this view displays (snake_case)'),
204205
name: z.string().describe('View machine name (snake_case)'),
205206
label: z.string().nullable().describe('Human-readable view label, or null'),
206-
type: z.enum(['list', 'form', 'kanban', 'calendar']).nullable().describe('View kind, or null for list'),
207-
columns: z.array(z.string()).nullable().describe('Field names shown as columns, or null'),
208-
groupBy: z.string().nullable().describe('REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Null for non-kanban views.'),
207+
type: z.enum(['list', 'form', 'kanban', 'calendar', 'gallery', 'gantt']).nullable().describe('View kind, or null for list. "gallery" = visual card/cover browse (画廊/相册/卡片墙/封面/海报, or an object with an image/avatar/file field); "gantt" = timeline/schedule (甘特图/时间线/排期, object with BOTH a start and an end date field); "kanban" = board grouped by a status/select field; "calendar" = single-date schedule; "form" = record editor.'),
208+
columns: z.array(z.string()).nullable().describe('Field names shown as columns, or null. For a gallery, INCLUDE the image/avatar/file field (becomes the card cover); for a gantt, INCLUDE the start date column before the end date column.'),
209+
groupBy: z.string().nullable().describe('REQUIRED for kanban: the select/status field whose options become the board columns (e.g. "stage"). Optional for gantt (groups leaf tasks). Null for list/form/calendar/gallery.'),
209210
});
210211

211212
const StrictDashboard = z.object({

0 commit comments

Comments
 (0)