Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/spec/src/ai/solution-blueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ describe('SolutionBlueprintSchema', () => {
expect(parsed.views?.[0].type).toBe('list');
});

it('accepts gallery and gantt as first-class view kinds (not only list/form/kanban/calendar)', () => {
// A build agent that wants a poster wall or a schedule must be able to PROPOSE
// a gallery/gantt view directly — otherwise the closest allowed enum value
// (list) wins and the view silently downgrades to a grid.
const parsed = SolutionBlueprintSchema.parse({
summary: 'events',
objects: [{ name: 'event', fields: [{ name: 'name', type: 'text' }, { name: 'poster', type: 'image' }] }],
views: [
{ object: 'event', name: 'poster_wall', type: 'gallery', columns: ['poster', 'name'] },
{ object: 'event', name: 'schedule', type: 'gantt', columns: ['name'] },
],
});
expect(parsed.views?.map((v) => v.type)).toEqual(['gallery', 'gantt']);
});

it('rejects a missing summary', () => {
const { summary: _drop, ...noSummary } = validBlueprint;
expect(() => SolutionBlueprintSchema.parse(noSummary)).toThrow();
Expand Down Expand Up @@ -204,6 +219,20 @@ describe('SolutionBlueprintStrictSchema (OpenAI strict mirror)', () => {
expect(parsed.app).toBeNull();
});

it('accepts gallery/gantt view kinds in the strict mirror (the structured-output contract)', () => {
// This is the schema the build agent's structured output is validated against,
// so the gallery/gantt enum values MUST live here too — else the model can
// never emit them and a requested gallery degrades to a list/grid.
const parsed = SolutionBlueprintStrictSchema.parse({
...strictBp,
views: [
{ object: 'event', name: 'wall', label: null, type: 'gallery', columns: null, groupBy: null },
{ object: 'event', name: 'plan', label: null, type: 'gantt', columns: null, groupBy: null },
],
});
expect(parsed.views?.map((v) => v.type)).toEqual(['gallery', 'gantt']);
});

it('requires every top-level key to be present (OpenAI strict needs all in `required`)', () => {
const { views: _v, ...missingViews } = strictBp;
expect(() => SolutionBlueprintStrictSchema.parse(missingViews)).toThrow();
Expand Down
15 changes: 8 additions & 7 deletions packages/spec/src/ai/solution-blueprint.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ export const BlueprintObjectSchema = lazySchema(() => z.object({
}));
export type BlueprintObject = z.infer<typeof BlueprintObjectSchema>;

/** A proposed list/form/kanban/calendar view over an object. */
/** A proposed list/form/kanban/calendar/gallery/gantt view over an object. */
export const BlueprintViewSchema = lazySchema(() => z.object({
object: z.string().regex(SNAKE_CASE).describe('Object this view displays (snake_case)'),
name: z.string().regex(SNAKE_CASE).describe('View machine name (snake_case)'),
label: z.string().optional().describe('Human-readable view label'),
type: z.enum(['list', 'form', 'kanban', 'calendar']).default('list').describe('View kind'),
type: z.enum(['list', 'form', 'kanban', 'calendar', 'gallery', 'gantt']).default('list')
.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".'),
columns: z.array(z.string().regex(SNAKE_CASE)).optional()
.describe('Field names shown as columns (in order)'),
.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.'),
groupBy: z.string().regex(SNAKE_CASE).optional()
.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.'),
.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).'),
}));
export type BlueprintView = z.infer<typeof BlueprintViewSchema>;

Expand Down Expand Up @@ -203,9 +204,9 @@ const StrictView = z.object({
object: z.string().describe('Object this view displays (snake_case)'),
name: z.string().describe('View machine name (snake_case)'),
label: z.string().nullable().describe('Human-readable view label, or null'),
type: z.enum(['list', 'form', 'kanban', 'calendar']).nullable().describe('View kind, or null for list'),
columns: z.array(z.string()).nullable().describe('Field names shown as columns, or null'),
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.'),
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.'),
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.'),
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.'),
});

const StrictDashboard = z.object({
Expand Down