@plures/praxis 1.2.12 → 1.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/browser/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
- package/dist/browser/{chunk-K377RW4V.js → chunk-FCEH7WMH.js} +1 -1
- package/dist/browser/{engine-YJZV4SLD.js → engine-65QDGCAN.js} +1 -1
- package/dist/browser/index.d.ts +104 -2
- package/dist/browser/index.js +181 -5
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -2
- package/dist/browser/{reactive-engine.svelte-9aS0kTa8.d.ts → reactive-engine.svelte-Cqd8Mod2.d.ts} +56 -1
- package/dist/node/{chunk-PRPQO6R5.js → chunk-32YFEEML.js} +1 -1
- package/dist/node/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
- package/dist/node/{chunk-5RH7UAQC.js → chunk-PTH6MD6P.js} +1 -0
- package/dist/node/cli/index.cjs +1553 -839
- package/dist/node/cli/index.js +39 -2
- package/dist/node/cloud/index.d.cts +1 -1
- package/dist/node/cloud/index.d.ts +1 -1
- package/dist/node/components/index.d.cts +2 -2
- package/dist/node/components/index.d.ts +2 -2
- package/dist/node/conversations-KQBXTP3N.js +596 -0
- package/dist/node/{engine-2DQBKBJC.js → engine-7CXQV6RC.js} +1 -1
- package/dist/node/index.cjs +408 -3
- package/dist/node/index.d.cts +308 -7
- package/dist/node/index.d.ts +308 -7
- package/dist/node/index.js +336 -6
- package/dist/node/integrations/svelte.cjs +70 -1
- package/dist/node/integrations/svelte.d.cts +3 -3
- package/dist/node/integrations/svelte.d.ts +3 -3
- package/dist/node/integrations/svelte.js +2 -2
- package/dist/node/{protocol-Qek7ebBl.d.ts → protocol-BocKczNv.d.cts} +1 -1
- package/dist/node/{protocol-Qek7ebBl.d.cts → protocol-BocKczNv.d.ts} +1 -1
- package/dist/node/{reactive-engine.svelte-CRNqHlbv.d.ts → reactive-engine.svelte-CGe8SpVE.d.cts} +57 -2
- package/dist/node/{reactive-engine.svelte-BFIZfawz.d.cts → reactive-engine.svelte-D-xTDxT5.d.ts} +57 -2
- package/dist/node/{terminal-adapter-B-UK_Vdz.d.ts → terminal-adapter-CvIvgTo4.d.ts} +1 -1
- package/dist/node/{terminal-adapter-BQSIF5bf.d.cts → terminal-adapter-Db-snPJ3.d.cts} +1 -1
- package/dist/node/{validate-CNHUULQE.js → validate-EN3M4FUR.js} +1 -1
- package/dist/node/{verify-KLJRXVJS.js → verify-7VZRP2WS.js} +2 -2
- package/docs/BOT_UPDATE_POLICY.md +125 -0
- package/docs/DOGFOODING_CHECKLIST.md +254 -0
- package/docs/DOGFOODING_INDEX.md +169 -0
- package/docs/DOGFOODING_QUICK_START.md +140 -0
- package/docs/KNO_ENG_EXTRACTION_PLAN.md +577 -0
- package/docs/PLURES_TOOLS_INVENTORY.md +170 -0
- package/docs/README.md +12 -0
- package/docs/TESTING_BOT_WORKFLOWS.md +154 -0
- package/docs/conversations/INTEGRATION_POINTS.md +719 -0
- package/docs/conversations/README.md +168 -0
- package/docs/core/extending-praxis-core.md +604 -0
- package/docs/core/praxis-core-api.md +385 -0
- package/docs/decision-ledger/contract-index.json +2 -2
- package/docs/decision-ledger/decisions/2026-02-01-monorepo-organization.md +130 -0
- package/docs/examples/DOGFOODING_WORKFLOW_EXAMPLE.md +295 -0
- package/docs/examples/README.md +41 -0
- package/docs/workflows/pr-overlap-guard.md +50 -0
- package/package.json +7 -2
- package/src/__tests__/chronicle.test.ts +512 -0
- package/src/__tests__/conversations.test.ts +312 -0
- package/src/__tests__/edge-cases.test.ts +1 -1
- package/src/__tests__/engine-dx.test.ts +355 -0
- package/src/cli/commands/conversations.ts +252 -0
- package/src/cli/index.ts +73 -0
- package/src/conversations/README.md +230 -0
- package/src/conversations/candidate.schema.json +123 -0
- package/src/conversations/candidates.ts +114 -0
- package/src/conversations/capture.ts +56 -0
- package/src/conversations/classify.ts +110 -0
- package/src/conversations/conversation.schema.json +106 -0
- package/src/conversations/emitters/fs.ts +65 -0
- package/src/conversations/emitters/github.ts +115 -0
- package/src/conversations/gate.ts +102 -0
- package/src/conversations/index.ts +28 -0
- package/src/conversations/normalize.ts +51 -0
- package/src/conversations/redact.ts +57 -0
- package/src/conversations/types.ts +96 -0
- package/src/core/chronicle/chronicle.ts +227 -0
- package/src/core/chronicle/context.ts +80 -0
- package/src/core/chronicle/index.ts +53 -0
- package/src/core/chronicle/mcp.ts +135 -0
- package/src/core/chronicle/types.ts +61 -0
- package/src/core/engine.ts +99 -1
- package/src/core/pluresdb/index.ts +22 -0
- package/src/core/pluresdb/store.ts +162 -5
- package/src/core/rules.ts +12 -0
- package/src/dsl/index.ts +6 -0
- package/src/index.ts +18 -0
- package/src/integrations/pluresdb.ts +22 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
# Extending Praxis-Core
|
|
2
|
+
|
|
3
|
+
**Audience:** Library authors, plugin developers, and advanced users
|
|
4
|
+
**Status:** Stable guidance
|
|
5
|
+
**Last Updated:** 2026-02-01
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
This guide explains how to extend Praxis-Core safely and effectively without breaking existing applications. It covers best practices for creating custom rules, constraints, modules, and integrations.
|
|
10
|
+
|
|
11
|
+
## Table of Contents
|
|
12
|
+
|
|
13
|
+
- [Principles of Extension](#principles-of-extension)
|
|
14
|
+
- [Creating Custom Rules](#creating-custom-rules)
|
|
15
|
+
- [Creating Custom Constraints](#creating-custom-constraints)
|
|
16
|
+
- [Building Modules](#building-modules)
|
|
17
|
+
- [Contract Requirements](#contract-requirements)
|
|
18
|
+
- [Testing Extensions](#testing-extensions)
|
|
19
|
+
- [Publishing Extensions](#publishing-extensions)
|
|
20
|
+
- [Breaking Change Policy](#breaking-change-policy)
|
|
21
|
+
|
|
22
|
+
## Principles of Extension
|
|
23
|
+
|
|
24
|
+
### 1. Purity First
|
|
25
|
+
|
|
26
|
+
All rules and constraints must be pure functions:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// ✅ GOOD: Pure function
|
|
30
|
+
const goodRule = defineRule({
|
|
31
|
+
id: 'counter.increment',
|
|
32
|
+
description: 'Increment counter',
|
|
33
|
+
impl: (state, events) => {
|
|
34
|
+
const incrementEvent = events.find(Increment.is);
|
|
35
|
+
if (!incrementEvent) return [];
|
|
36
|
+
return [CountUpdated.create({ value: state.context.count + 1 })];
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// ❌ BAD: Side effects
|
|
41
|
+
const badRule = defineRule({
|
|
42
|
+
id: 'counter.increment',
|
|
43
|
+
description: 'Increment counter',
|
|
44
|
+
impl: (state, events) => {
|
|
45
|
+
console.log('Incrementing!'); // Side effect: logging
|
|
46
|
+
fetch('/api/increment'); // Side effect: network
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Why?** Pure functions are:
|
|
53
|
+
- Testable
|
|
54
|
+
- Predictable
|
|
55
|
+
- Serializable
|
|
56
|
+
- Cross-language compatible
|
|
57
|
+
|
|
58
|
+
### 2. Stable Identifiers
|
|
59
|
+
|
|
60
|
+
Rule and constraint IDs are permanent. Once published, they should never change.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// ✅ GOOD: Stable, namespaced ID
|
|
64
|
+
const rule = defineRule({
|
|
65
|
+
id: 'myapp.auth.login', // namespace.domain.action
|
|
66
|
+
description: 'Handle user login',
|
|
67
|
+
impl: loginImpl
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// ❌ BAD: Generic, likely to conflict
|
|
71
|
+
const rule = defineRule({
|
|
72
|
+
id: 'login', // Too generic, no namespace
|
|
73
|
+
description: 'Handle user login',
|
|
74
|
+
impl: loginImpl
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**ID Conventions:**
|
|
79
|
+
- Format: `{namespace}.{domain}.{action}`
|
|
80
|
+
- Namespace: Your package/org name
|
|
81
|
+
- Domain: Feature area (auth, cart, etc.)
|
|
82
|
+
- Action: What the rule does
|
|
83
|
+
|
|
84
|
+
### 3. Type Safety
|
|
85
|
+
|
|
86
|
+
Always provide explicit types for context and payloads:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// ✅ GOOD: Explicit types
|
|
90
|
+
interface AppContext {
|
|
91
|
+
userId: string | null;
|
|
92
|
+
sessionToken: string | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const LoginSuccess = defineFact<'LoginSuccess', { userId: string; token: string }>('LoginSuccess');
|
|
96
|
+
|
|
97
|
+
const loginRule = defineRule<AppContext>({
|
|
98
|
+
id: 'myapp.auth.login',
|
|
99
|
+
description: 'Process successful login',
|
|
100
|
+
impl: (state, events) => {
|
|
101
|
+
// state.context is typed as AppContext
|
|
102
|
+
// Full type safety throughout
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ❌ BAD: No types, relies on 'any'
|
|
107
|
+
const loginRule = defineRule({
|
|
108
|
+
id: 'myapp.auth.login',
|
|
109
|
+
description: 'Process successful login',
|
|
110
|
+
impl: (state, events) => {
|
|
111
|
+
// state.context is unknown
|
|
112
|
+
// No type safety
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### 4. Immutability
|
|
118
|
+
|
|
119
|
+
Never mutate state directly. Always return new values:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// ✅ GOOD: Immutable updates
|
|
123
|
+
const rule = defineRule<AppContext>({
|
|
124
|
+
id: 'myapp.user.update',
|
|
125
|
+
description: 'Update user data',
|
|
126
|
+
impl: (state, events) => {
|
|
127
|
+
const updateEvent = events.find(UpdateUser.is);
|
|
128
|
+
if (!updateEvent) return [];
|
|
129
|
+
|
|
130
|
+
// Create new context, don't mutate
|
|
131
|
+
const newContext = {
|
|
132
|
+
...state.context,
|
|
133
|
+
userId: updateEvent.payload.userId
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return [UserUpdated.create(newContext)];
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ❌ BAD: Mutates state
|
|
141
|
+
const rule = defineRule<AppContext>({
|
|
142
|
+
id: 'myapp.user.update',
|
|
143
|
+
description: 'Update user data',
|
|
144
|
+
impl: (state, events) => {
|
|
145
|
+
const updateEvent = events.find(UpdateUser.is);
|
|
146
|
+
if (!updateEvent) return [];
|
|
147
|
+
|
|
148
|
+
// WRONG: Mutates existing state
|
|
149
|
+
state.context.userId = updateEvent.payload.userId;
|
|
150
|
+
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Creating Custom Rules
|
|
157
|
+
|
|
158
|
+
### Basic Rule
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { defineRule, defineFact, defineEvent } from '@plures/praxis';
|
|
162
|
+
|
|
163
|
+
// 1. Define your events
|
|
164
|
+
const AddItem = defineEvent<'AddItem', { itemId: string; quantity: number }>('AddItem');
|
|
165
|
+
|
|
166
|
+
// 2. Define your facts
|
|
167
|
+
const ItemAdded = defineFact<'ItemAdded', { itemId: string; quantity: number }>('ItemAdded');
|
|
168
|
+
|
|
169
|
+
// 3. Define your context type
|
|
170
|
+
interface CartContext {
|
|
171
|
+
items: Array<{ itemId: string; quantity: number }>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 4. Define the rule
|
|
175
|
+
const addItemRule = defineRule<CartContext>({
|
|
176
|
+
id: 'myapp.cart.addItem',
|
|
177
|
+
description: 'Add item to shopping cart',
|
|
178
|
+
impl: (state, events) => {
|
|
179
|
+
const addEvent = events.find(AddItem.is);
|
|
180
|
+
if (!addEvent) return [];
|
|
181
|
+
|
|
182
|
+
return [ItemAdded.create({
|
|
183
|
+
itemId: addEvent.payload.itemId,
|
|
184
|
+
quantity: addEvent.payload.quantity
|
|
185
|
+
})];
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Rule with Guards
|
|
191
|
+
|
|
192
|
+
Use guard clauses for clean, readable rules:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const processOrderRule = defineRule<OrderContext>({
|
|
196
|
+
id: 'myapp.order.process',
|
|
197
|
+
description: 'Process order if valid',
|
|
198
|
+
impl: (state, events) => {
|
|
199
|
+
const orderEvent = events.find(PlaceOrder.is);
|
|
200
|
+
|
|
201
|
+
// Guard: No event
|
|
202
|
+
if (!orderEvent) return [];
|
|
203
|
+
|
|
204
|
+
// Guard: Empty cart
|
|
205
|
+
if (state.context.items.length === 0) {
|
|
206
|
+
return [OrderRejected.create({ reason: 'Cart is empty' })];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Guard: Insufficient funds
|
|
210
|
+
if (state.context.balance < state.context.cartTotal) {
|
|
211
|
+
return [OrderRejected.create({ reason: 'Insufficient funds' })];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Happy path
|
|
215
|
+
return [OrderProcessed.create({
|
|
216
|
+
orderId: generateId(),
|
|
217
|
+
items: state.context.items,
|
|
218
|
+
total: state.context.cartTotal
|
|
219
|
+
})];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Multi-Event Rules
|
|
225
|
+
|
|
226
|
+
Rules can respond to multiple event types:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const sessionRule = defineRule<SessionContext>({
|
|
230
|
+
id: 'myapp.session.manage',
|
|
231
|
+
description: 'Manage user session lifecycle',
|
|
232
|
+
impl: (state, events) => {
|
|
233
|
+
const loginEvent = events.find(Login.is);
|
|
234
|
+
const logoutEvent = events.find(Logout.is);
|
|
235
|
+
const renewEvent = events.find(RenewSession.is);
|
|
236
|
+
|
|
237
|
+
if (loginEvent) {
|
|
238
|
+
return [SessionStarted.create({
|
|
239
|
+
userId: loginEvent.payload.userId,
|
|
240
|
+
expiresAt: Date.now() + 3600000 // 1 hour
|
|
241
|
+
})];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (logoutEvent) {
|
|
245
|
+
return [SessionEnded.create({
|
|
246
|
+
userId: state.context.userId
|
|
247
|
+
})];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (renewEvent && state.context.sessionActive) {
|
|
251
|
+
return [SessionRenewed.create({
|
|
252
|
+
expiresAt: Date.now() + 3600000
|
|
253
|
+
})];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Creating Custom Constraints
|
|
262
|
+
|
|
263
|
+
### Basic Constraint
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { defineConstraint } from '@plures/praxis';
|
|
267
|
+
|
|
268
|
+
interface CartContext {
|
|
269
|
+
items: Array<{ itemId: string; quantity: number }>;
|
|
270
|
+
maxItems: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const maxCartItemsConstraint = defineConstraint<CartContext>({
|
|
274
|
+
id: 'myapp.cart.maxItems',
|
|
275
|
+
description: 'Cart cannot exceed maximum item limit',
|
|
276
|
+
impl: (state) => {
|
|
277
|
+
const itemCount = state.context.items.length;
|
|
278
|
+
const maxItems = state.context.maxItems;
|
|
279
|
+
|
|
280
|
+
if (itemCount > maxItems) {
|
|
281
|
+
return `Cart has ${itemCount} items, maximum is ${maxItems}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Constraint with Context
|
|
290
|
+
|
|
291
|
+
Constraints can validate complex business rules:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const orderValidConstraint = defineConstraint<OrderContext>({
|
|
295
|
+
id: 'myapp.order.valid',
|
|
296
|
+
description: 'Order must have valid items and payment',
|
|
297
|
+
impl: (state) => {
|
|
298
|
+
// Check items
|
|
299
|
+
if (state.context.items.length === 0) {
|
|
300
|
+
return 'Order must contain at least one item';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check quantities
|
|
304
|
+
const invalidQuantity = state.context.items.some(item => item.quantity <= 0);
|
|
305
|
+
if (invalidQuantity) {
|
|
306
|
+
return 'All items must have positive quantity';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check payment
|
|
310
|
+
if (!state.context.paymentMethod) {
|
|
311
|
+
return 'Payment method required';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check total
|
|
315
|
+
if (state.context.total <= 0) {
|
|
316
|
+
return 'Order total must be positive';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Building Modules
|
|
325
|
+
|
|
326
|
+
Group related rules and constraints into modules:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { defineModule } from '@plures/praxis';
|
|
330
|
+
|
|
331
|
+
interface CartContext {
|
|
332
|
+
items: Array<{ itemId: string; quantity: number }>;
|
|
333
|
+
maxItems: number;
|
|
334
|
+
total: number;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export const cartModule = defineModule<CartContext>({
|
|
338
|
+
id: 'myapp.cart',
|
|
339
|
+
description: 'Shopping cart logic',
|
|
340
|
+
rules: [
|
|
341
|
+
addItemRule,
|
|
342
|
+
removeItemRule,
|
|
343
|
+
updateQuantityRule,
|
|
344
|
+
clearCartRule
|
|
345
|
+
],
|
|
346
|
+
constraints: [
|
|
347
|
+
maxCartItemsConstraint,
|
|
348
|
+
validQuantitiesConstraint,
|
|
349
|
+
validTotalConstraint
|
|
350
|
+
]
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Usage
|
|
354
|
+
const registry = new PraxisRegistry<CartContext>();
|
|
355
|
+
registry.registerModule(cartModule);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Contract Requirements
|
|
359
|
+
|
|
360
|
+
**Important:** All rules and constraints in praxis-core must have contracts when used in the Praxis repository itself (dogfooding requirement).
|
|
361
|
+
|
|
362
|
+
### Defining Contracts
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { defineContract } from '@plures/praxis';
|
|
366
|
+
|
|
367
|
+
const addItemContract = defineContract({
|
|
368
|
+
ruleId: 'myapp.cart.addItem',
|
|
369
|
+
behavior: 'When an AddItem event is received, create an ItemAdded fact with the same itemId and quantity',
|
|
370
|
+
examples: [
|
|
371
|
+
{
|
|
372
|
+
given: 'Empty cart',
|
|
373
|
+
when: 'AddItem event with itemId="abc" and quantity=2',
|
|
374
|
+
then: 'ItemAdded fact emitted with itemId="abc" and quantity=2'
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
given: 'Cart with existing items',
|
|
378
|
+
when: 'AddItem event with itemId="xyz" and quantity=1',
|
|
379
|
+
then: 'ItemAdded fact emitted with itemId="xyz" and quantity=1'
|
|
380
|
+
}
|
|
381
|
+
],
|
|
382
|
+
invariants: [
|
|
383
|
+
'ItemAdded.itemId === AddItem.itemId',
|
|
384
|
+
'ItemAdded.quantity === AddItem.quantity',
|
|
385
|
+
'Exactly one ItemAdded fact per AddItem event'
|
|
386
|
+
],
|
|
387
|
+
references: [
|
|
388
|
+
{ type: 'doc', url: 'https://example.com/cart-spec', description: 'Cart specification' }
|
|
389
|
+
]
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const addItemRule = defineRule<CartContext>({
|
|
393
|
+
id: 'myapp.cart.addItem',
|
|
394
|
+
description: 'Add item to shopping cart',
|
|
395
|
+
impl: addItemImpl,
|
|
396
|
+
contract: addItemContract // Attach contract
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Contract Testing
|
|
401
|
+
|
|
402
|
+
All contract examples should have corresponding tests:
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
import { describe, it, expect } from 'vitest';
|
|
406
|
+
|
|
407
|
+
describe('myapp.cart.addItem', () => {
|
|
408
|
+
it('Example 1: Empty cart + AddItem -> ItemAdded', () => {
|
|
409
|
+
// Given: Empty cart
|
|
410
|
+
const registry = new PraxisRegistry<CartContext>();
|
|
411
|
+
registry.registerRule(addItemRule);
|
|
412
|
+
const engine = createPraxisEngine({
|
|
413
|
+
initialContext: { items: [], maxItems: 100, total: 0 },
|
|
414
|
+
registry
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// When: AddItem event with itemId="abc" and quantity=2
|
|
418
|
+
const result = engine.step([
|
|
419
|
+
AddItem.create({ itemId: 'abc', quantity: 2 })
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
// Then: ItemAdded fact emitted
|
|
423
|
+
const itemAddedFacts = result.state.facts.filter(ItemAdded.is);
|
|
424
|
+
expect(itemAddedFacts).toHaveLength(1);
|
|
425
|
+
expect(itemAddedFacts[0].payload.itemId).toBe('abc');
|
|
426
|
+
expect(itemAddedFacts[0].payload.quantity).toBe(2);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// More tests for other examples...
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Testing Extensions
|
|
434
|
+
|
|
435
|
+
### Unit Tests
|
|
436
|
+
|
|
437
|
+
Test rules and constraints in isolation:
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
describe('addItemRule', () => {
|
|
441
|
+
it('should emit ItemAdded fact when AddItem event received', () => {
|
|
442
|
+
const state: PraxisState & { context: CartContext } = {
|
|
443
|
+
context: { items: [], maxItems: 100, total: 0 },
|
|
444
|
+
facts: [],
|
|
445
|
+
protocolVersion: '1.0.0'
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const events = [AddItem.create({ itemId: 'test', quantity: 1 })];
|
|
449
|
+
|
|
450
|
+
const result = addItemRule.impl(state, events);
|
|
451
|
+
|
|
452
|
+
expect(result).toHaveLength(1);
|
|
453
|
+
expect(ItemAdded.is(result[0])).toBe(true);
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### Integration Tests
|
|
459
|
+
|
|
460
|
+
Test modules with the full engine:
|
|
461
|
+
|
|
462
|
+
```typescript
|
|
463
|
+
describe('cartModule integration', () => {
|
|
464
|
+
it('should handle complete cart workflow', () => {
|
|
465
|
+
const registry = new PraxisRegistry<CartContext>();
|
|
466
|
+
registry.registerModule(cartModule);
|
|
467
|
+
|
|
468
|
+
const engine = createPraxisEngine({
|
|
469
|
+
initialContext: { items: [], maxItems: 10, total: 0 },
|
|
470
|
+
registry
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Add item
|
|
474
|
+
engine.step([AddItem.create({ itemId: 'item1', quantity: 2 })]);
|
|
475
|
+
|
|
476
|
+
// Update quantity
|
|
477
|
+
engine.step([UpdateQuantity.create({ itemId: 'item1', quantity: 3 })]);
|
|
478
|
+
|
|
479
|
+
// Verify final state
|
|
480
|
+
const context = engine.getContext();
|
|
481
|
+
expect(context.items).toHaveLength(1);
|
|
482
|
+
expect(context.items[0].quantity).toBe(3);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
## Publishing Extensions
|
|
488
|
+
|
|
489
|
+
### Package Structure
|
|
490
|
+
|
|
491
|
+
```
|
|
492
|
+
my-praxis-extension/
|
|
493
|
+
├── src/
|
|
494
|
+
│ ├── index.ts # Main exports
|
|
495
|
+
│ ├── rules.ts # Rule definitions
|
|
496
|
+
│ ├── constraints.ts # Constraint definitions
|
|
497
|
+
│ ├── contracts.ts # Contract definitions
|
|
498
|
+
│ └── types.ts # Type definitions
|
|
499
|
+
├── dist/ # Compiled output
|
|
500
|
+
├── package.json
|
|
501
|
+
├── tsconfig.json
|
|
502
|
+
└── README.md
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Package.json
|
|
506
|
+
|
|
507
|
+
```json
|
|
508
|
+
{
|
|
509
|
+
"name": "@yourorg/praxis-extension",
|
|
510
|
+
"version": "1.0.0",
|
|
511
|
+
"type": "module",
|
|
512
|
+
"main": "./dist/index.js",
|
|
513
|
+
"types": "./dist/index.d.ts",
|
|
514
|
+
"exports": {
|
|
515
|
+
".": {
|
|
516
|
+
"types": "./dist/index.d.ts",
|
|
517
|
+
"import": "./dist/index.js"
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
"peerDependencies": {
|
|
521
|
+
"@plures/praxis": "^1.0.0"
|
|
522
|
+
},
|
|
523
|
+
"keywords": ["praxis", "praxis-extension"]
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### Version Compatibility
|
|
528
|
+
|
|
529
|
+
Specify which praxis-core versions your extension supports:
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
export const EXTENSION_METADATA = {
|
|
533
|
+
name: '@yourorg/praxis-extension',
|
|
534
|
+
version: '1.0.0',
|
|
535
|
+
praxisVersions: {
|
|
536
|
+
min: '1.0.0',
|
|
537
|
+
max: '1.x.x' // Supports all 1.x versions
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Breaking Change Policy
|
|
543
|
+
|
|
544
|
+
### For Extension Authors
|
|
545
|
+
|
|
546
|
+
When publishing extensions:
|
|
547
|
+
|
|
548
|
+
1. **Follow SemVer**: Major for breaking, minor for features, patch for fixes
|
|
549
|
+
2. **Document Changes**: Maintain a CHANGELOG.md
|
|
550
|
+
3. **Deprecation Path**: Deprecate before removing (one minor version notice)
|
|
551
|
+
4. **Migration Guide**: Provide upgrade instructions for breaking changes
|
|
552
|
+
|
|
553
|
+
### Avoiding Breaking Changes
|
|
554
|
+
|
|
555
|
+
Safe additions (non-breaking):
|
|
556
|
+
- ✅ New rules or constraints
|
|
557
|
+
- ✅ New optional fields in context types
|
|
558
|
+
- ✅ New examples in contracts
|
|
559
|
+
- ✅ Performance improvements
|
|
560
|
+
- ✅ Bug fixes
|
|
561
|
+
|
|
562
|
+
Breaking changes:
|
|
563
|
+
- ❌ Changing rule IDs
|
|
564
|
+
- ❌ Changing rule behavior without deprecation
|
|
565
|
+
- ❌ Removing rules or constraints
|
|
566
|
+
- ❌ Changing context type requirements
|
|
567
|
+
- ❌ Changing event/fact payload structures
|
|
568
|
+
|
|
569
|
+
## Best Practices Summary
|
|
570
|
+
|
|
571
|
+
1. **Pure Functions**: Rules and constraints must be pure
|
|
572
|
+
2. **Stable IDs**: Never change rule/constraint IDs
|
|
573
|
+
3. **Type Safety**: Use explicit types for all contexts and payloads
|
|
574
|
+
4. **Immutability**: Never mutate state
|
|
575
|
+
5. **Contracts**: Document behavior with contracts
|
|
576
|
+
6. **Tests**: Test all contract examples
|
|
577
|
+
7. **Namespacing**: Use namespaced IDs to avoid conflicts
|
|
578
|
+
8. **Documentation**: Document your extension thoroughly
|
|
579
|
+
9. **Versioning**: Follow semantic versioning
|
|
580
|
+
10. **Compatibility**: Test against supported praxis-core versions
|
|
581
|
+
|
|
582
|
+
## Examples
|
|
583
|
+
|
|
584
|
+
See these examples for reference:
|
|
585
|
+
|
|
586
|
+
- [Cart Module](../../examples/todo/) - Simple module example
|
|
587
|
+
- [Auth Module](../../examples/hero-shop/) - Complex module with multiple rules
|
|
588
|
+
- [Form Builder](../../examples/form-builder/) - Dynamic constraint validation
|
|
589
|
+
|
|
590
|
+
## Support
|
|
591
|
+
|
|
592
|
+
- **Questions**: [GitHub Discussions](https://github.com/plures/praxis/discussions)
|
|
593
|
+
- **Issues**: [GitHub Issues](https://github.com/plures/praxis/issues)
|
|
594
|
+
- **Documentation**: [Core API Docs](./praxis-core-api.md)
|
|
595
|
+
|
|
596
|
+
## References
|
|
597
|
+
|
|
598
|
+
- [Praxis-Core API](./praxis-core-api.md)
|
|
599
|
+
- [Decision Ledger Dogfooding](../decision-ledger/DOGFOODING.md)
|
|
600
|
+
- [Contributing Guide](../../CONTRIBUTING.md)
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
**Next:** [Decision Ledger](../decision-ledger/LATEST.md)
|