Skip to content

Commit f4aee81

Browse files
os-zhuangclaude
andauthored
feat(spec): blueprint dashboard widget condition slot (threshold/status filter) (#2381)
* feat(spec): blueprint dashboard widget `condition` slot (threshold/status filter) A widget whose title restricts WHICH records it counts — "库存低于10的备件预警", "overdue work orders", "金额超过 2000" — has no way to say so: the `measure`/ `groupBy` slots choose the NUMBER and the axis, not the row set. So the builder emits a plain count and a "low stock" card counts EVERY part. Add an optional `condition {field, op, value}` (op ∈ lt|lte|gt|gte|eq|ne) to the blueprint widget — lenient + strict variants — that the builder compiles to the framework widget's presentation-scope `filter` (runtimeFilter). Kept to a single comparison so the model emits it reliably and a bad guess can be dropped rather than producing an empty card. Pairs with the cloud dashboardBody mapping + prompt guidance (separate PR). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(spec): cover widget `condition` in strict mirror + refresh api-surface The strict SolutionBlueprintStrictSchema makes every widget key required-present (nullable for optionality), so the new `condition` slot must appear in the strict widget fixtures — one null, one a real {field,op,value}. Regenerate api-surface.json to register the new BlueprintWidgetCondition export. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ce0b4f6 commit f4aee81

3 files changed

Lines changed: 28 additions & 4 deletions

File tree

packages/spec/api-surface.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1731,6 +1731,8 @@
17311731
"BlueprintSeedSchema (const)",
17321732
"BlueprintView (type)",
17331733
"BlueprintViewSchema (const)",
1734+
"BlueprintWidgetCondition (type)",
1735+
"BlueprintWidgetConditionSchema (const)",
17341736
"CodeContentSchema (const)",
17351737
"ConversationAnalytics (type)",
17361738
"ConversationAnalyticsSchema (const)",

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -224,22 +224,23 @@ describe('SolutionBlueprintStrictSchema (OpenAI strict mirror)', () => {
224224
expect('seedData' in SolutionBlueprintStrictSchema.shape).toBe(false);
225225
});
226226

227-
it('accepts a dashboard widget carrying the (nullable) measure + groupBy keys', () => {
227+
it('accepts a dashboard widget carrying the (nullable) measure + groupBy + condition keys', () => {
228228
const parsed = SolutionBlueprintStrictSchema.parse({
229229
...strictBp,
230230
dashboards: [
231231
{
232232
name: 'overview',
233233
label: null,
234234
widgets: [
235-
{ id: 'revenue', title: 'Total revenue', object: 'project', chart: 'metric', measure: 'amount', groupBy: null },
236-
{ id: 'w2', title: null, object: null, chart: null, measure: null, groupBy: null },
235+
{ id: 'revenue', title: 'Total revenue', object: 'project', chart: 'metric', measure: 'amount', groupBy: null, condition: null },
236+
{ id: 'low_stock', title: 'Low stock', object: 'project', chart: 'table', measure: null, groupBy: null, condition: { field: 'qty', op: 'lt', value: 10 } },
237237
],
238238
},
239239
],
240240
});
241241
expect(parsed.dashboards?.[0].widgets?.[0]).toMatchObject({ measure: 'amount', groupBy: null });
242-
expect(parsed.dashboards?.[0].widgets?.[1].measure).toBeNull();
242+
expect(parsed.dashboards?.[0].widgets?.[0].condition).toBeNull();
243+
expect(parsed.dashboards?.[0].widgets?.[1].condition).toMatchObject({ field: 'qty', op: 'lt', value: 10 });
243244
});
244245

245246
it('requires the (nullable) measure + groupBy widget keys to be present (OpenAI strict)', () => {

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,19 @@ export const BlueprintViewSchema = lazySchema(() => z.object({
6363
}));
6464
export type BlueprintView = z.infer<typeof BlueprintViewSchema>;
6565

66+
/**
67+
* A single comparison that scopes WHICH records a dashboard widget
68+
* counts/aggregates — kept deliberately simple (one field op value) so the
69+
* builder can compile it to a widget `runtimeFilter`, and the model can emit it
70+
* reliably, instead of leaving a "low stock" / "overdue" card counting every row.
71+
*/
72+
export const BlueprintWidgetConditionSchema = lazySchema(() => z.object({
73+
field: z.string().regex(SNAKE_CASE).describe('Field on the widget object to filter by (e.g. "stock_quantity", "status")'),
74+
op: z.enum(['lt', 'lte', 'gt', 'gte', 'eq', 'ne']).describe('Comparison operator'),
75+
value: z.union([z.number(), z.string(), z.boolean()]).describe('Comparison value (e.g. 10, "open")'),
76+
}));
77+
export type BlueprintWidgetCondition = z.infer<typeof BlueprintWidgetConditionSchema>;
78+
6679
/** A proposed dashboard with a few widgets (kept intentionally light). */
6780
export const BlueprintDashboardSchema = lazySchema(() => z.object({
6881
name: z.string().regex(SNAKE_CASE).describe('Dashboard machine name (snake_case)'),
@@ -76,6 +89,8 @@ export const BlueprintDashboardSchema = lazySchema(() => z.object({
7689
.describe('The field this widget aggregates (e.g. "amount", "probability"), or "count" to count records. The aggregation is chosen automatically from the field type — a money field SUMs, a percentage/rate AVERAGEs — so name the FIELD, not "total_amount". A "total revenue" widget sets measure:"amount"; an "average win rate" widget sets measure:"win_rate"; a "number of deals" widget sets measure:"count". Omit to let the builder infer from the title.'),
7790
groupBy: z.string().regex(SNAKE_CASE).optional()
7891
.describe('The field to break the widget down by — the category or time axis (e.g. "stage", "created_at"). A "by status" chart MUST set this to the status field; the title and this field MUST name the SAME field. Omit for a single-number metric.'),
92+
condition: BlueprintWidgetConditionSchema.optional()
93+
.describe('Restrict WHICH records the widget counts/aggregates when its title implies a threshold or status (e.g. "stock below 10" → {field:"stock_quantity", op:"lt", value:10}; "open tickets" → {field:"status", op:"eq", value:"open"}). Without it the widget covers ALL records — so a "低于10的备件预警" / "overdue" card would wrongly count everything. Omit when the widget genuinely spans every record.'),
7994
})).optional().describe('Widgets to place on the dashboard'),
8095
}));
8196
export type BlueprintDashboard = z.infer<typeof BlueprintDashboardSchema>;
@@ -205,6 +220,12 @@ const StrictDashboard = z.object({
205220
.describe('The field this widget aggregates (e.g. "amount", "probability"), or "count" to count records, or null to infer from the title. The aggregation (sum vs average) is chosen automatically from the field type — name the FIELD, not "total_amount". "total revenue" → "amount"; "average win rate" → "win_rate"; "number of deals" → "count".'),
206221
groupBy: z.string().nullable()
207222
.describe('The field to break the widget down by — the category or time axis (e.g. "stage", "created_at"), or null for a single-number metric. A "by status" chart MUST set this to the status field; the title and this field MUST name the SAME field.'),
223+
condition: z.object({
224+
field: z.string().describe('Field on the widget object to filter by (e.g. "stock_quantity", "status")'),
225+
op: z.enum(['lt', 'lte', 'gt', 'gte', 'eq', 'ne']).describe('Comparison operator'),
226+
value: z.union([z.number(), z.string(), z.boolean()]).describe('Comparison value (e.g. 10, "open")'),
227+
}).nullable()
228+
.describe('Restrict WHICH records the widget counts/aggregates when its title implies a threshold or status (e.g. "stock below 10" → {field:"stock_quantity",op:"lt",value:10}; "open tickets" → {field:"status",op:"eq",value:"open"}), or null when the widget covers every record. Without it a "低于10的预警" / "overdue" card wrongly counts ALL rows.'),
208229
})).nullable().describe('Widgets to place on the dashboard, or null'),
209230
});
210231

0 commit comments

Comments
 (0)