@plures/praxis 1.4.4 → 2.0.0

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.
Files changed (38) hide show
  1. package/dist/browser/chunk-6MVRT7CK.js +363 -0
  2. package/dist/browser/factory/index.d.ts +2 -1
  3. package/dist/browser/index.d.ts +7 -4
  4. package/dist/browser/index.js +18 -6
  5. package/dist/browser/integrations/svelte.d.ts +4 -3
  6. package/dist/browser/project/index.d.ts +2 -1
  7. package/dist/browser/{reactive-engine.svelte-DgVTqHLc.d.ts → reactive-engine.svelte-BwWadvAW.d.ts} +2 -1
  8. package/dist/browser/rule-result-DcXWe9tn.d.ts +206 -0
  9. package/dist/browser/{rules-i1LHpnGd.d.ts → rules-BaWMqxuG.d.ts} +2 -205
  10. package/dist/browser/unified/index.d.ts +239 -0
  11. package/dist/browser/unified/index.js +20 -0
  12. package/dist/node/chunk-6MVRT7CK.js +363 -0
  13. package/dist/node/cli/index.js +1 -1
  14. package/dist/node/index.cjs +367 -0
  15. package/dist/node/index.d.cts +4 -2
  16. package/dist/node/index.d.ts +4 -2
  17. package/dist/node/index.js +19 -7
  18. package/dist/node/integrations/svelte.d.cts +3 -2
  19. package/dist/node/integrations/svelte.d.ts +3 -2
  20. package/dist/node/integrations/svelte.js +2 -2
  21. package/dist/node/{reactive-engine.svelte-DekxqFu0.d.ts → reactive-engine.svelte-BBZLMzus.d.ts} +3 -79
  22. package/dist/node/{reactive-engine.svelte-Cg0Yc2Hs.d.cts → reactive-engine.svelte-Cbq_V20o.d.cts} +3 -79
  23. package/dist/node/rule-result-B9GMivAn.d.cts +80 -0
  24. package/dist/node/rule-result-Bo3sFMmN.d.ts +80 -0
  25. package/dist/node/unified/index.cjs +484 -0
  26. package/dist/node/unified/index.d.cts +240 -0
  27. package/dist/node/unified/index.d.ts +240 -0
  28. package/dist/node/unified/index.js +21 -0
  29. package/package.json +7 -1
  30. package/src/index.browser.ts +20 -0
  31. package/src/index.ts +21 -0
  32. package/src/unified/__tests__/unified.test.ts +396 -0
  33. package/src/unified/core.ts +517 -0
  34. package/src/unified/index.ts +32 -0
  35. package/src/unified/rules.ts +66 -0
  36. package/src/unified/types.ts +148 -0
  37. package/dist/node/{chunk-ZO2LU4G4.js → chunk-WFRHXZBP.js} +3 -3
  38. package/dist/node/{validate-5PSWJTIC.js → validate-BY7JNY7H.js} +1 -1
@@ -0,0 +1,517 @@
1
+ /**
2
+ * Praxis Unified Reactive Layer — Core
3
+ *
4
+ * createApp() → query() / mutate()
5
+ *
6
+ * The developer defines a schema + rules. Praxis handles:
7
+ * - Reactive state (backed by Unum graph DB)
8
+ * - Automatic rule evaluation on state changes
9
+ * - Constraint enforcement on mutations
10
+ * - Chronos logging for every state change
11
+ * - Liveness monitoring (detect broken plumbing)
12
+ * - Svelte-compatible store contract
13
+ */
14
+
15
+ import type {
16
+ PraxisAppConfig,
17
+ PathSchema,
18
+ QueryOptions,
19
+ ReactiveRef,
20
+ MutationResult,
21
+ UnifiedRule,
22
+ UnifiedConstraint,
23
+ } from './types.js';
24
+ import type { PraxisFact, PraxisDiagnostics } from '../core/protocol.js';
25
+ import { RuleResult } from '../core/rule-result.js';
26
+
27
+ // ── Internal State ──────────────────────────────────────────────────────────
28
+
29
+ interface PathState<T = unknown> {
30
+ schema: PathSchema<T>;
31
+ value: T;
32
+ subscribers: Set<(value: T) => void>;
33
+ lastUpdated: number;
34
+ updateCount: number;
35
+ }
36
+
37
+ interface RuleState {
38
+ rule: UnifiedRule;
39
+ lastResult: RuleResult | null;
40
+ }
41
+
42
+ // ── Timeline Entry (Chronos-compatible) ─────────────────────────────────────
43
+
44
+ interface TimelineEntry {
45
+ id: string;
46
+ timestamp: number;
47
+ path: string;
48
+ kind: 'mutation' | 'rule-eval' | 'constraint-check' | 'liveness';
49
+ data: Record<string, unknown>;
50
+ }
51
+
52
+ // ── Praxis App Instance ─────────────────────────────────────────────────────
53
+
54
+ export interface PraxisApp {
55
+ /** Reactive query — returns a Svelte-compatible store */
56
+ query: <T>(path: string, opts?: QueryOptions<T>) => ReactiveRef<T>;
57
+ /** Write to the graph — validates through constraints first */
58
+ mutate: (path: string, value: unknown) => MutationResult;
59
+ /** Batch multiple mutations atomically */
60
+ batch: (fn: (mutate: (path: string, value: unknown) => void) => void) => MutationResult;
61
+ /** Current facts */
62
+ facts: () => PraxisFact[];
63
+ /** Current constraint violations */
64
+ violations: () => PraxisDiagnostics[];
65
+ /** Timeline (Chronos entries) */
66
+ timeline: () => TimelineEntry[];
67
+ /** Force re-evaluate all rules */
68
+ evaluate: () => void;
69
+ /** Cleanup */
70
+ destroy: () => void;
71
+ /** Liveness status — which paths are stale */
72
+ liveness: () => Record<string, { stale: boolean; lastUpdated: number; elapsed: number }>;
73
+ }
74
+
75
+ // ── Implementation ──────────────────────────────────────────────────────────
76
+
77
+ let _idCounter = 0;
78
+ function nextId(): string {
79
+ return `px:${Date.now()}-${++_idCounter}`;
80
+ }
81
+
82
+ /**
83
+ * Create a Praxis application.
84
+ *
85
+ * This is the single entry point. It creates the reactive graph,
86
+ * wires rules and constraints, starts Chronos logging, and returns
87
+ * query() and mutate() — the only two functions a developer needs.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * import { createApp, definePath, defineRule } from '@plures/praxis';
92
+ *
93
+ * const Sprint = definePath<SprintInfo | null>('sprint/current', null);
94
+ * const Loading = definePath<boolean>('sprint/loading', false);
95
+ *
96
+ * const app = createApp({
97
+ * name: 'sprint-log',
98
+ * schema: [Sprint, Loading],
99
+ * rules: [sprintBehindRule, capacityRule],
100
+ * constraints: [noCloseWithoutHoursConstraint],
101
+ * });
102
+ *
103
+ * // In a Svelte component:
104
+ * const sprint = app.query<SprintInfo | null>('sprint/current');
105
+ * // $sprint is reactive — updates automatically
106
+ *
107
+ * // To write:
108
+ * app.mutate('sprint/current', sprintData);
109
+ * // Constraints validated, rules re-evaluated, Chronos logged — all automatic
110
+ * ```
111
+ */
112
+ export function createApp(config: PraxisAppConfig): PraxisApp {
113
+ // ── Path Registry ──
114
+ const paths = new Map<string, PathState>();
115
+ for (const schema of config.schema) {
116
+ paths.set(schema.path, {
117
+ schema,
118
+ value: schema.initial,
119
+ subscribers: new Set(),
120
+ lastUpdated: 0,
121
+ updateCount: 0,
122
+ });
123
+ }
124
+
125
+ // ── Facts ──
126
+ let facts: PraxisFact[] = [];
127
+ const factMap = new Map<string, PraxisFact>(); // tag → latest fact (LWW)
128
+
129
+ // ── Timeline ──
130
+ const timeline: TimelineEntry[] = [];
131
+ const maxTimeline = 10000;
132
+
133
+ function recordTimeline(path: string, kind: TimelineEntry['kind'], data: Record<string, unknown>) {
134
+ const entry: TimelineEntry = {
135
+ id: nextId(),
136
+ timestamp: Date.now(),
137
+ path,
138
+ kind,
139
+ data,
140
+ };
141
+ timeline.push(entry);
142
+ if (timeline.length > maxTimeline) {
143
+ timeline.splice(0, timeline.length - maxTimeline);
144
+ }
145
+ }
146
+
147
+ // ── Rules ──
148
+ const ruleStates: RuleState[] = (config.rules ?? []).map(rule => ({
149
+ rule,
150
+ lastResult: null,
151
+ }));
152
+
153
+ // ── Constraints ──
154
+ const constraints: UnifiedConstraint[] = config.constraints ?? [];
155
+
156
+ // ── Liveness ──
157
+ const livenessConfig = config.liveness;
158
+ const initTime = Date.now();
159
+ let livenessTimer: ReturnType<typeof setTimeout> | null = null;
160
+
161
+ if (livenessConfig) {
162
+ const timeout = livenessConfig.timeoutMs ?? 5000;
163
+ livenessTimer = setTimeout(() => {
164
+ for (const expectedPath of livenessConfig.expect) {
165
+ const state = paths.get(expectedPath);
166
+ if (!state || state.updateCount === 0) {
167
+ const elapsed = Date.now() - initTime;
168
+ recordTimeline(expectedPath, 'liveness', {
169
+ stale: true,
170
+ elapsed,
171
+ message: `Path "${expectedPath}" never updated after ${elapsed}ms`,
172
+ });
173
+ livenessConfig.onStale?.(expectedPath, elapsed);
174
+ }
175
+ }
176
+ }, timeout);
177
+ }
178
+
179
+ // ── Helpers ──
180
+
181
+ function getPathValues(watchPaths: string[]): Record<string, unknown> {
182
+ const values: Record<string, unknown> = {};
183
+ for (const p of watchPaths) {
184
+ const state = paths.get(p);
185
+ values[p] = state ? state.value : undefined;
186
+ }
187
+ return values;
188
+ }
189
+
190
+ function notify(path: string, value: unknown) {
191
+ const state = paths.get(path);
192
+ if (!state) return;
193
+ for (const cb of state.subscribers) {
194
+ try {
195
+ cb(value as any);
196
+ } catch (err) {
197
+ console.error(`[praxis] Subscriber error for "${path}":`, err);
198
+ }
199
+ }
200
+ }
201
+
202
+ function checkConstraints(path: string, value: unknown): PraxisDiagnostics[] {
203
+ const violations: PraxisDiagnostics[] = [];
204
+ for (const c of constraints) {
205
+ if (!c.watch.includes(path)) continue;
206
+ // Build values with the proposed new value
207
+ const values = getPathValues(c.watch);
208
+ values[path] = value;
209
+ try {
210
+ const result = c.validate(values);
211
+ if (result !== true) {
212
+ violations.push({
213
+ kind: 'constraint-violation',
214
+ message: result,
215
+ data: { constraintId: c.id, path, description: c.description },
216
+ });
217
+ recordTimeline(path, 'constraint-check', {
218
+ constraintId: c.id,
219
+ violated: true,
220
+ message: result,
221
+ });
222
+ }
223
+ } catch (err) {
224
+ violations.push({
225
+ kind: 'constraint-violation',
226
+ message: `Constraint "${c.id}" threw: ${err instanceof Error ? err.message : String(err)}`,
227
+ data: { constraintId: c.id, error: err },
228
+ });
229
+ }
230
+ }
231
+ return violations;
232
+ }
233
+
234
+ function evaluateRules() {
235
+ const newFacts: PraxisFact[] = [];
236
+ const retractions: string[] = [];
237
+
238
+ for (const rs of ruleStates) {
239
+ const values = getPathValues(rs.rule.watch);
240
+ try {
241
+ const result = rs.rule.evaluate(values, [...facts]);
242
+ rs.lastResult = result;
243
+
244
+ recordTimeline(rs.rule.watch[0] ?? '*', 'rule-eval', {
245
+ ruleId: rs.rule.id,
246
+ kind: result.kind,
247
+ reason: result.reason,
248
+ });
249
+
250
+ switch (result.kind) {
251
+ case 'emit':
252
+ newFacts.push(...result.facts);
253
+ break;
254
+ case 'retract':
255
+ retractions.push(...result.retractTags);
256
+ break;
257
+ case 'noop':
258
+ case 'skip':
259
+ break;
260
+ }
261
+ } catch (err) {
262
+ console.error(`[praxis] Rule "${rs.rule.id}" error:`, err);
263
+ }
264
+ }
265
+
266
+ // Apply retractions
267
+ if (retractions.length > 0) {
268
+ const retractSet = new Set(retractions);
269
+ for (const tag of retractSet) {
270
+ factMap.delete(tag);
271
+ }
272
+ }
273
+
274
+ // Merge new facts (LWW)
275
+ for (const f of newFacts) {
276
+ factMap.set(f.tag, f);
277
+ }
278
+
279
+ facts = Array.from(factMap.values());
280
+ }
281
+
282
+ // ── Public API ──
283
+
284
+ function query<T>(path: string, opts?: QueryOptions<T>): ReactiveRef<T> {
285
+ // Ensure path exists
286
+ let state = paths.get(path);
287
+ if (!state) {
288
+ // Auto-create with undefined initial — allows querying unschema'd paths
289
+ state = {
290
+ schema: { path, initial: undefined as T },
291
+ value: undefined as T,
292
+ subscribers: new Set(),
293
+ lastUpdated: 0,
294
+ updateCount: 0,
295
+ };
296
+ paths.set(path, state);
297
+ }
298
+
299
+ // Create a subscribable that filters/maps if opts provided
300
+ const ref: ReactiveRef<T> = {
301
+ get current() {
302
+ const s = paths.get(path);
303
+ return applyQueryOpts((s?.value ?? state!.schema.initial) as T, opts);
304
+ },
305
+
306
+ subscribe(cb: (value: T) => void): () => void {
307
+ // Wrap callback to apply query opts
308
+ const wrappedCb = (rawValue: unknown) => {
309
+ const processed = applyQueryOpts(rawValue as T, opts);
310
+ cb(processed);
311
+ };
312
+ const s = paths.get(path)!;
313
+ s.subscribers.add(wrappedCb as any);
314
+
315
+ // Immediate callback with current value (Svelte store contract)
316
+ try {
317
+ cb(ref.current);
318
+ } catch (err) {
319
+ console.error(`[praxis] query("${path}") subscriber init error:`, err);
320
+ }
321
+
322
+ return () => {
323
+ s.subscribers.delete(wrappedCb as any);
324
+ };
325
+ },
326
+ };
327
+
328
+ return ref;
329
+ }
330
+
331
+ function applyQueryOpts<T>(value: T, opts?: QueryOptions<T>): T {
332
+ if (!opts || !Array.isArray(value)) return value;
333
+ let result: any[] = [...(value as any[])];
334
+ if (opts.where) result = result.filter(opts.where as any);
335
+ if (opts.sort) result.sort(opts.sort);
336
+ if (opts.select) result = result.map(opts.select as any);
337
+ if (opts.limit) result = result.slice(0, opts.limit);
338
+ return result as unknown as T;
339
+ }
340
+
341
+ function mutateInternal(path: string, value: unknown): { violations: PraxisDiagnostics[]; emittedFacts: PraxisFact[] } {
342
+ // 1. Check constraints
343
+ const violations = checkConstraints(path, value);
344
+ if (violations.length > 0) {
345
+ return { violations, emittedFacts: [] };
346
+ }
347
+
348
+ // 2. Write to graph
349
+ const state = paths.get(path);
350
+ if (!state) {
351
+ // Auto-create path
352
+ paths.set(path, {
353
+ schema: { path, initial: value },
354
+ value,
355
+ subscribers: new Set(),
356
+ lastUpdated: Date.now(),
357
+ updateCount: 1,
358
+ });
359
+ } else {
360
+ const before = state.value;
361
+ state.value = value;
362
+ state.lastUpdated = Date.now();
363
+ state.updateCount++;
364
+
365
+ // 3. Log to timeline
366
+ recordTimeline(path, 'mutation', {
367
+ before: summarize(before),
368
+ after: summarize(value),
369
+ });
370
+ }
371
+
372
+ // 4. Notify subscribers
373
+ notify(path, value);
374
+
375
+ // 5. Re-evaluate rules
376
+ const factsBefore = facts.length;
377
+ evaluateRules();
378
+ const emittedFacts = facts.slice(factsBefore);
379
+
380
+ return { violations: [], emittedFacts };
381
+ }
382
+
383
+ function mutate(path: string, value: unknown): MutationResult {
384
+ const { violations, emittedFacts } = mutateInternal(path, value);
385
+ return {
386
+ accepted: violations.length === 0,
387
+ violations,
388
+ facts: emittedFacts,
389
+ };
390
+ }
391
+
392
+ function batchMutate(fn: (m: (path: string, value: unknown) => void) => void): MutationResult {
393
+ const allViolations: PraxisDiagnostics[] = [];
394
+ const pendingWrites: Array<{ path: string; value: unknown }> = [];
395
+
396
+ // Collect all mutations
397
+ fn((path, value) => {
398
+ // Check constraints eagerly
399
+ const violations = checkConstraints(path, value);
400
+ if (violations.length > 0) {
401
+ allViolations.push(...violations);
402
+ } else {
403
+ pendingWrites.push({ path, value });
404
+ }
405
+ });
406
+
407
+ // If any constraint failed, reject the whole batch
408
+ if (allViolations.length > 0) {
409
+ return { accepted: false, violations: allViolations, facts: [] };
410
+ }
411
+
412
+ // Apply all writes
413
+ for (const { path, value } of pendingWrites) {
414
+ const state = paths.get(path);
415
+ if (state) {
416
+ const before = state.value;
417
+ state.value = value;
418
+ state.lastUpdated = Date.now();
419
+ state.updateCount++;
420
+ recordTimeline(path, 'mutation', {
421
+ before: summarize(before),
422
+ after: summarize(value),
423
+ });
424
+ } else {
425
+ paths.set(path, {
426
+ schema: { path, initial: value },
427
+ value,
428
+ subscribers: new Set(),
429
+ lastUpdated: Date.now(),
430
+ updateCount: 1,
431
+ });
432
+ }
433
+ }
434
+
435
+ // Notify all at once
436
+ for (const { path, value } of pendingWrites) {
437
+ notify(path, value);
438
+ }
439
+
440
+ // Single rule evaluation pass
441
+ const factsBefore = facts.length;
442
+ evaluateRules();
443
+
444
+ return {
445
+ accepted: true,
446
+ violations: [],
447
+ facts: facts.slice(factsBefore),
448
+ };
449
+ }
450
+
451
+ function getLiveness(): Record<string, { stale: boolean; lastUpdated: number; elapsed: number }> {
452
+ const result: Record<string, { stale: boolean; lastUpdated: number; elapsed: number }> = {};
453
+ const now = Date.now();
454
+ const watchPaths = livenessConfig?.expect ?? [];
455
+ for (const p of watchPaths) {
456
+ const state = paths.get(p);
457
+ const lastUpdated = state?.lastUpdated ?? 0;
458
+ const elapsed = lastUpdated > 0 ? now - lastUpdated : now - initTime;
459
+ const timeout = livenessConfig?.timeoutMs ?? 5000;
460
+ result[p] = {
461
+ stale: state?.updateCount === 0 || elapsed > timeout,
462
+ lastUpdated,
463
+ elapsed,
464
+ };
465
+ }
466
+ return result;
467
+ }
468
+
469
+ function destroy() {
470
+ if (livenessTimer) clearTimeout(livenessTimer);
471
+ for (const state of paths.values()) {
472
+ state.subscribers.clear();
473
+ }
474
+ paths.clear();
475
+ facts = [];
476
+ factMap.clear();
477
+ timeline.length = 0;
478
+ }
479
+
480
+ return {
481
+ query,
482
+ mutate,
483
+ batch: batchMutate,
484
+ facts: () => [...facts],
485
+ violations: () => {
486
+ const allViolations: PraxisDiagnostics[] = [];
487
+ for (const c of constraints) {
488
+ const values = getPathValues(c.watch);
489
+ try {
490
+ const result = c.validate(values);
491
+ if (result !== true) {
492
+ allViolations.push({
493
+ kind: 'constraint-violation',
494
+ message: result,
495
+ data: { constraintId: c.id },
496
+ });
497
+ }
498
+ } catch { /* skip */ }
499
+ }
500
+ return allViolations;
501
+ },
502
+ timeline: () => [...timeline],
503
+ evaluate: evaluateRules,
504
+ destroy,
505
+ liveness: getLiveness,
506
+ };
507
+ }
508
+
509
+ /** Summarize a value for timeline logging (avoid huge payloads) */
510
+ function summarize(value: unknown): unknown {
511
+ if (value === null || value === undefined) return value;
512
+ if (typeof value !== 'object') return value;
513
+ if (Array.isArray(value)) return `[Array(${value.length})]`;
514
+ const keys = Object.keys(value as object);
515
+ if (keys.length > 10) return `{Object(${keys.length} keys)}`;
516
+ return value;
517
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Praxis Unified — Public API
3
+ *
4
+ * This is the only import developers need:
5
+ *
6
+ * import { createApp, definePath, defineRule, defineConstraint } from '@plures/praxis/unified';
7
+ *
8
+ * Everything else — stores, subscriptions, logging, validation — is automatic.
9
+ */
10
+
11
+ export { createApp } from './core.js';
12
+ export type { PraxisApp } from './core.js';
13
+
14
+ export {
15
+ definePath,
16
+ type PathSchema,
17
+ type QueryOptions,
18
+ type ReactiveRef,
19
+ type MutationResult,
20
+ type UnifiedRule,
21
+ type UnifiedConstraint,
22
+ type LivenessConfig,
23
+ type PraxisAppConfig,
24
+ } from './types.js';
25
+
26
+ export {
27
+ defineRule,
28
+ defineConstraint,
29
+ defineModule,
30
+ RuleResult,
31
+ fact,
32
+ } from './rules.js';
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Praxis Unified — Rule DSL helpers
3
+ *
4
+ * Developers define rules as plain objects with watch paths.
5
+ * No manual subscriptions, no wirePraxis(), no context mapping.
6
+ */
7
+
8
+ import type { UnifiedRule, UnifiedConstraint } from './types.js';
9
+ import { RuleResult, fact } from '../core/rule-result.js';
10
+
11
+ export { RuleResult, fact };
12
+
13
+ /**
14
+ * Define a rule that watches graph paths and auto-evaluates.
15
+ *
16
+ * @example
17
+ * const sprintBehind = defineRule({
18
+ * id: 'sprint.behind',
19
+ * watch: ['sprint/current'],
20
+ * evaluate: (values) => {
21
+ * const sprint = values['sprint/current'];
22
+ * if (!sprint) return RuleResult.skip('No sprint');
23
+ * const pace = sprint.currentDay / sprint.totalDays;
24
+ * const work = sprint.completedHours / sprint.totalHours;
25
+ * if (work >= pace) return RuleResult.retract(['sprint.behind']);
26
+ * return RuleResult.emit([fact('sprint.behind', { pace, work })]);
27
+ * }
28
+ * });
29
+ */
30
+ export function defineRule(rule: UnifiedRule): UnifiedRule {
31
+ return rule;
32
+ }
33
+
34
+ /**
35
+ * Define a constraint that validates mutations before they're applied.
36
+ *
37
+ * @example
38
+ * const noCloseWithoutHours = defineConstraint({
39
+ * id: 'no-close-without-hours',
40
+ * description: 'Cannot close a work item with 0 completed hours',
41
+ * watch: ['sprint/items'],
42
+ * validate: (values) => {
43
+ * const items = values['sprint/items'] ?? [];
44
+ * const bad = items.find(i => i.state === 'Closed' && !i.completedWork);
45
+ * if (bad) return `Item #${bad.id} cannot be closed with 0 hours`;
46
+ * return true;
47
+ * }
48
+ * });
49
+ */
50
+ export function defineConstraint(constraint: UnifiedConstraint): UnifiedConstraint {
51
+ return constraint;
52
+ }
53
+
54
+ /**
55
+ * Compose multiple rules into a named module.
56
+ *
57
+ * @example
58
+ * const sprintModule = defineModule('sprint-health', [
59
+ * sprintBehindRule,
60
+ * capacityRule,
61
+ * endNearRule,
62
+ * ]);
63
+ */
64
+ export function defineModule(name: string, rules: UnifiedRule[]): { name: string; rules: UnifiedRule[] } {
65
+ return { name, rules };
66
+ }