diff --git a/.changeset/solution-design-process-guardrail.md b/.changeset/solution-design-process-guardrail.md new file mode 100644 index 000000000..716f5e210 --- /dev/null +++ b/.changeset/solution-design-process-guardrail.md @@ -0,0 +1,15 @@ +--- +'@objectstack/service-ai': patch +--- + +fix(ai): solution_design no longer models a process/approval as a table + +Asking the assistant to "design expense reimbursement" made it, by default, invent an `approval_record` TABLE to represent the approval process — a non-functional "process-as-data" anti-pattern. It only switched to a flow after the user pushed back. + +Hardens the default in two places: + +- **`propose_blueprint` generation prompt** (the "metadata architect" system message): status/lifecycle is modeled as a `select` field, never a table; it must NOT create `approval` / `approval_record` / `approval_step` / `workflow` / `process` objects (a process is a flow, its trail comes from platform history); the people a process references (approver/reviewer/owner) are `lookup` fields; and if the goal implies a process it adds an assumption that the approval *flow* is a separate step. + +- **`solution_design` skill instructions**: the same modeling rules, plus — when the goal involves a process — the agent now PROACTIVELY drafts the approval flow after `apply_blueprint` (call `get_metadata_schema('flow')`, then `create_metadata(type:'flow', …)` with the approval node(s), bound to the same app package) instead of waiting for the user to ask "now create the flow". Optionally adds a `state_machine` rule to block illegal status transitions. + +Regression-tested: `solution-design-guardrail.test.ts` asserts the skill instructions carry the no-process-as-table rule, status-as-select, and the proactive-flow step. diff --git a/packages/services/service-ai/src/__tests__/solution-design-guardrail.test.ts b/packages/services/service-ai/src/__tests__/solution-design-guardrail.test.ts new file mode 100644 index 000000000..414834d6e --- /dev/null +++ b/packages/services/service-ai/src/__tests__/solution-design-guardrail.test.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { SOLUTION_DESIGN_SKILL } from '../skills/solution-design-skill.js'; + +/** + * Regression guard for the "don't model a process as data" rule. The default + * behaviour was that asking the agent to "design expense reimbursement" made it + * invent an `approval_record` TABLE instead of a flow. The fix lives in the + * skill instructions (and the propose_blueprint generation prompt): status is a + * select field, an approval process is a FLOW, and the agent proactively drafts + * that flow instead of waiting to be asked. These assertions keep that guidance + * from silently regressing. + */ +describe('solution_design — process/state guardrail', () => { + const text = SOLUTION_DESIGN_SKILL.instructions.toLowerCase(); + + it('tells the agent NOT to model a process/approval as a table', () => { + expect(text).toContain('do not model a process as data'); + expect(text).toMatch(/never create objects for approvals/); + expect(text).toMatch(/is a flow, not a table/); + }); + + it('tells the agent to model status as a select field', () => { + expect(text).toMatch(/status\b.*\bselect\b|\bselect\b.*\bstatus/); + }); + + it('tells the agent to proactively draft the approval flow (not wait to be asked)', () => { + expect(text).toContain('get_metadata_schema'); + expect(text).toMatch(/create_metadata\(type:'flow'/); + expect(text).toMatch(/do not wait for the user to ask/); + }); + + it('still drives the plan-first propose -> apply flow', () => { + expect(SOLUTION_DESIGN_SKILL.tools).toEqual(['propose_blueprint', 'apply_blueprint']); + }); +}); diff --git a/packages/services/service-ai/src/skills/solution-design-skill.ts b/packages/services/service-ai/src/skills/solution-design-skill.ts index 151380b63..e4577baea 100644 --- a/packages/services/service-ai/src/skills/solution-design-skill.ts +++ b/packages/services/service-ai/src/skills/solution-design-skill.ts @@ -30,6 +30,15 @@ Hard rules: - Seed data in a blueprint is a suggestion only; it is not auto-applied. - Always answer in the same language the user is using. +Process & state modeling — do NOT model a process as data: +- Record STATUS / lifecycle is a \`select\` field on the main object (e.g. status = draft/submitted/approved/rejected/paid), never a separate table. +- NEVER create objects for approvals, approval steps/records, workflows, routing, sign-offs, or audit trails. An approval/automation process is a FLOW, not a table; its trail comes from platform history. Model only the people the process references (approver/reviewer/owner) as lookup fields to the user object. +- If the goal involves a process (approval / 审批 / review / routing / multi-step state transitions / sign-off / escalation), the blueprint covers only the DATA model. After apply_blueprint drafts it, PROACTIVELY draft the approval flow yourself — do NOT wait for the user to ask "now create the flow": + 1. Call get_metadata_schema('flow') to get the exact shape (it supports an \`approval\` node). + 2. Call create_metadata(type:'flow', ...) with the approval node(s) wired to the status field and approver lookups, binding it to the SAME app package (pass the packageId from apply_blueprint's result, or set_active_package first) so it is not orphaned. + 3. Optionally add a state_machine validation rule on the object so illegal status transitions are blocked. + Then tell the user you have ALSO drafted the approval flow for their review (it is a draft, like everything else). + For small, specific changes ("add a status field to account") use the metadata_authoring tools directly instead of a blueprint.`, tools: [ 'propose_blueprint', diff --git a/packages/services/service-ai/src/tools/blueprint-tools.ts b/packages/services/service-ai/src/tools/blueprint-tools.ts index d271a3cb0..aa068a794 100644 --- a/packages/services/service-ai/src/tools/blueprint-tools.ts +++ b/packages/services/service-ai/src/tools/blueprint-tools.ts @@ -131,6 +131,19 @@ function createProposeBlueprintHandler(ctx: BlueprintToolContext): ToolHandler { 'Rules:\n' + '- Use snake_case for every object, field, and view name.\n' + '- Prefer a small, sensible field set per object over an exhaustive one.\n' + + '- Model record STATUS / lifecycle stage as a single `select` field on the ' + + 'main object (e.g. a `status` field with options like draft/submitted/approved/' + + 'rejected/paid), NOT as a separate table.\n' + + '- A PROCESS is not data. Do NOT create objects for approvals, approval ' + + 'steps/records, workflows, routing, sign-offs, or audit trails (no ' + + '`approval`, `approval_record`, `approval_step`, `workflow`, `process` tables). ' + + 'Approval/automation logic belongs in a separate FLOW authored after this ' + + 'blueprint, and the trail comes from platform history — never a hand-built table. ' + + 'Model only the PEOPLE the process references (approver / reviewer / owner) as ' + + '`lookup` fields to the user object.\n' + + '- If the goal implies an approval or automation process, add an `assumption` ' + + 'stating the approval *flow* will be drafted as a separate step (it is not part ' + + 'of this data blueprint).\n' + '- State the design choices you made as `assumptions`.\n' + '- If (and only if) a genuinely structure-deciding choice is unclear, put at most 1-2 ' + 'short `questions`; otherwise pick the most likely interpretation and proceed.\n' +