Skip to content

Commit 3e20294

Browse files
committed
feat: enhance state machine support with parallel lifecycles
- Updated JSON schema for CreateObjectOperation and MigrationOperation to support multiple state machines via `stateMachines` property. - Added detailed structure for state machines including states, transitions, and actions. - Modified ObjectSchema to allow coexistence of legacy single state machine and new parallel state machines. - Enhanced tests to validate the new state machine functionality, ensuring correct initialization and transitions for multiple machines.
1 parent a62655a commit 3e20294

12 files changed

Lines changed: 6608 additions & 1444 deletions

File tree

content/docs/objectql/state-machine.mdx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,49 @@ import { LeadStateMachine } from './lead.state';
145145
export const Lead = ObjectSchema.create({
146146
name: 'lead',
147147
// ... fields ...
148-
stateMachine: LeadStateMachine // Imported config
148+
stateMachines: {
149+
lifecycle: LeadStateMachine, // Named key for the primary lifecycle
150+
}
149151
});
150152
```
153+
154+
## Multiple State Machines (Parallel Lifecycles)
155+
156+
In real enterprise systems, a single object often has **multiple independent state lines**. For example, an `Order` has:
157+
158+
- **`lifecycle`**`draft → submitted → confirmed → shipped → delivered`
159+
- **`payment`**`unpaid → partial → paid → refunded`
160+
- **`approval`**`pending → approved → rejected`
161+
162+
Use the `stateMachines` (plural) property to define them:
163+
164+
```typescript
165+
// src/domains/sales/order.object.ts
166+
import { ObjectSchema } from '@objectstack/spec/data';
167+
import { OrderLifecycle } from './order-lifecycle.state';
168+
import { OrderPayment } from './order-payment.state';
169+
import { OrderApproval } from './order-approval.state';
170+
171+
export const Order = ObjectSchema.create({
172+
name: 'order',
173+
fields: {
174+
status: Field.select({ options: ['draft', 'submitted', 'confirmed', 'shipped', 'delivered'] }),
175+
payment_status: Field.select({ options: ['unpaid', 'partial', 'paid', 'refunded'] }),
176+
approval_status: Field.select({ options: ['pending', 'approved', 'rejected'] }),
177+
},
178+
stateMachines: {
179+
lifecycle: OrderLifecycle,
180+
payment: OrderPayment,
181+
approval: OrderApproval,
182+
}
183+
});
184+
```
185+
186+
### `stateMachine` vs `stateMachines`
187+
188+
| Property | Type | Use Case |
189+
|:---|:---|:---|
190+
| `stateMachine` | `StateMachineConfig` | Simple objects with a single lifecycle (shorthand) |
191+
| `stateMachines` | `Record<string, StateMachineConfig>` | Complex objects with parallel state lines |
192+
193+
Both can coexist on the same object. The kernel merges them at runtime.

examples/app-crm/src/domains/sales/lead.object.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,13 @@ export const Lead = ObjectSchema.create({
161161
}),
162162
},
163163

164-
// Lifecycle State Machine
164+
// Lifecycle State Machine(s)
165165
// Enforces valid status transitions to prevent AI hallucinations
166-
stateMachine: LeadStateMachine,
166+
// Using `stateMachines` (plural) for future extensibility.
167+
// For simple objects with one lifecycle, `stateMachine` (singular) is also supported.
168+
stateMachines: {
169+
lifecycle: LeadStateMachine,
170+
},
167171

168172
// Database indexes for performance
169173
indexes: [

examples/app-crm/test/lead.test.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@ describe('CRM Domain - Lead', () => {
99
expect(() => ObjectSchema.parse(Lead)).not.toThrow();
1010
});
1111

12-
it('should have a configured state machine', () => {
13-
expect(Lead.stateMachine).toBeDefined();
14-
expect(Lead.stateMachine?.id).toBe('lead_process');
15-
expect(Lead.stateMachine?.initial).toBe('new');
16-
expect(Lead.stateMachine?.states['new']).toBeDefined();
17-
expect(Lead.stateMachine?.states['converted'].type).toBe('final');
12+
it('should have a configured state machine via stateMachines (plural)', () => {
13+
expect(Lead.stateMachines).toBeDefined();
14+
15+
const lifecycle = Lead.stateMachines!.lifecycle;
16+
expect(lifecycle).toBeDefined();
17+
expect(lifecycle.id).toBe('lead_process');
18+
expect(lifecycle.initial).toBe('new');
19+
expect(lifecycle.states['new']).toBeDefined();
20+
expect(lifecycle.states['converted'].type).toBe('final');
1821
});
1922

2023
it('should have strict AI instructions in states', () => {
21-
const newMeta = Lead.stateMachine?.states['new'].meta;
24+
const lifecycle = Lead.stateMachines!.lifecycle;
25+
26+
const newMeta = lifecycle.states['new'].meta;
2227
expect(newMeta?.aiInstructions).toContain('Verify email');
2328

24-
const qualifiedMeta = Lead.stateMachine?.states['qualified'].meta;
29+
const qualifiedMeta = lifecycle.states['qualified'].meta;
2530
expect(qualifiedMeta?.aiInstructions).toContain('Prepare for conversion');
2631
});
2732
});

packages/spec/json-schema/ai/FeedbackLoop.json

Lines changed: 1480 additions & 476 deletions
Large diffs are not rendered by default.

packages/spec/json-schema/ai/Resolution.json

Lines changed: 1480 additions & 476 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)