@plures/praxis 1.2.13 → 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.
Files changed (85) hide show
  1. package/README.md +44 -0
  2. package/dist/browser/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
  3. package/dist/browser/{chunk-K377RW4V.js → chunk-FCEH7WMH.js} +1 -1
  4. package/dist/browser/{engine-YJZV4SLD.js → engine-65QDGCAN.js} +1 -1
  5. package/dist/browser/index.d.ts +104 -2
  6. package/dist/browser/index.js +181 -5
  7. package/dist/browser/integrations/svelte.d.ts +2 -2
  8. package/dist/browser/integrations/svelte.js +2 -2
  9. package/dist/browser/{reactive-engine.svelte-9aS0kTa8.d.ts → reactive-engine.svelte-Cqd8Mod2.d.ts} +56 -1
  10. package/dist/node/{chunk-PRPQO6R5.js → chunk-32YFEEML.js} +1 -1
  11. package/dist/node/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
  12. package/dist/node/{chunk-5RH7UAQC.js → chunk-PTH6MD6P.js} +1 -0
  13. package/dist/node/cli/index.cjs +1553 -839
  14. package/dist/node/cli/index.js +39 -2
  15. package/dist/node/cloud/index.d.cts +1 -1
  16. package/dist/node/cloud/index.d.ts +1 -1
  17. package/dist/node/components/index.d.cts +2 -2
  18. package/dist/node/components/index.d.ts +2 -2
  19. package/dist/node/conversations-KQBXTP3N.js +596 -0
  20. package/dist/node/{engine-2DQBKBJC.js → engine-7CXQV6RC.js} +1 -1
  21. package/dist/node/index.cjs +408 -3
  22. package/dist/node/index.d.cts +308 -7
  23. package/dist/node/index.d.ts +308 -7
  24. package/dist/node/index.js +336 -6
  25. package/dist/node/integrations/svelte.cjs +70 -1
  26. package/dist/node/integrations/svelte.d.cts +3 -3
  27. package/dist/node/integrations/svelte.d.ts +3 -3
  28. package/dist/node/integrations/svelte.js +2 -2
  29. package/dist/node/{protocol-Qek7ebBl.d.ts → protocol-BocKczNv.d.cts} +1 -1
  30. package/dist/node/{protocol-Qek7ebBl.d.cts → protocol-BocKczNv.d.ts} +1 -1
  31. package/dist/node/{reactive-engine.svelte-CRNqHlbv.d.ts → reactive-engine.svelte-CGe8SpVE.d.cts} +57 -2
  32. package/dist/node/{reactive-engine.svelte-BFIZfawz.d.cts → reactive-engine.svelte-D-xTDxT5.d.ts} +57 -2
  33. package/dist/node/{terminal-adapter-B-UK_Vdz.d.ts → terminal-adapter-CvIvgTo4.d.ts} +1 -1
  34. package/dist/node/{terminal-adapter-BQSIF5bf.d.cts → terminal-adapter-Db-snPJ3.d.cts} +1 -1
  35. package/dist/node/{validate-CNHUULQE.js → validate-EN3M4FUR.js} +1 -1
  36. package/dist/node/{verify-KLJRXVJS.js → verify-7VZRP2WS.js} +2 -2
  37. package/docs/BOT_UPDATE_POLICY.md +125 -0
  38. package/docs/DOGFOODING_CHECKLIST.md +254 -0
  39. package/docs/DOGFOODING_INDEX.md +169 -0
  40. package/docs/DOGFOODING_QUICK_START.md +140 -0
  41. package/docs/KNO_ENG_EXTRACTION_PLAN.md +577 -0
  42. package/docs/PLURES_TOOLS_INVENTORY.md +170 -0
  43. package/docs/README.md +12 -0
  44. package/docs/TESTING_BOT_WORKFLOWS.md +154 -0
  45. package/docs/conversations/INTEGRATION_POINTS.md +719 -0
  46. package/docs/conversations/README.md +168 -0
  47. package/docs/core/extending-praxis-core.md +604 -0
  48. package/docs/core/praxis-core-api.md +385 -0
  49. package/docs/decision-ledger/contract-index.json +2 -2
  50. package/docs/decision-ledger/decisions/2026-02-01-monorepo-organization.md +130 -0
  51. package/docs/examples/DOGFOODING_WORKFLOW_EXAMPLE.md +295 -0
  52. package/docs/examples/README.md +41 -0
  53. package/docs/workflows/pr-overlap-guard.md +50 -0
  54. package/package.json +7 -2
  55. package/src/__tests__/chronicle.test.ts +512 -0
  56. package/src/__tests__/conversations.test.ts +312 -0
  57. package/src/__tests__/edge-cases.test.ts +1 -1
  58. package/src/__tests__/engine-dx.test.ts +355 -0
  59. package/src/cli/commands/conversations.ts +252 -0
  60. package/src/cli/index.ts +73 -0
  61. package/src/conversations/README.md +230 -0
  62. package/src/conversations/candidate.schema.json +123 -0
  63. package/src/conversations/candidates.ts +114 -0
  64. package/src/conversations/capture.ts +56 -0
  65. package/src/conversations/classify.ts +110 -0
  66. package/src/conversations/conversation.schema.json +106 -0
  67. package/src/conversations/emitters/fs.ts +65 -0
  68. package/src/conversations/emitters/github.ts +115 -0
  69. package/src/conversations/gate.ts +102 -0
  70. package/src/conversations/index.ts +28 -0
  71. package/src/conversations/normalize.ts +51 -0
  72. package/src/conversations/redact.ts +57 -0
  73. package/src/conversations/types.ts +96 -0
  74. package/src/core/chronicle/chronicle.ts +227 -0
  75. package/src/core/chronicle/context.ts +80 -0
  76. package/src/core/chronicle/index.ts +53 -0
  77. package/src/core/chronicle/mcp.ts +135 -0
  78. package/src/core/chronicle/types.ts +61 -0
  79. package/src/core/engine.ts +99 -1
  80. package/src/core/pluresdb/index.ts +22 -0
  81. package/src/core/pluresdb/store.ts +162 -5
  82. package/src/core/rules.ts +12 -0
  83. package/src/dsl/index.ts +6 -0
  84. package/src/index.ts +18 -0
  85. package/src/integrations/pluresdb.ts +22 -0
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Chronicle Types
3
+ *
4
+ * Core types for the Chronicle causal tracking system.
5
+ * Records state transitions as a causal graph stored in PluresDB.
6
+ */
7
+
8
+ /**
9
+ * Direction for traversing causal chains.
10
+ */
11
+ export type TraceDirection = 'backward' | 'forward' | 'both';
12
+
13
+ /**
14
+ * Causal relationship type between Chronicle nodes.
15
+ * - `causes`: node A caused node B (explicit causal link)
16
+ * - `context`: node B belongs to the same session/request as node A
17
+ * - `follows`: node B happened after node A in the same context
18
+ */
19
+ export type EdgeType = 'causes' | 'context' | 'follows';
20
+
21
+ /**
22
+ * A recorded state transition event passed to Chronicle.
23
+ */
24
+ export interface ChronicleEvent {
25
+ /** Path to the changed value (fact or event stream path) */
26
+ path: string;
27
+ /** Value before the change (undefined for creates) */
28
+ before?: unknown;
29
+ /** Value after the change */
30
+ after?: unknown;
31
+ /** Parent span/node ID that caused this change */
32
+ cause?: string;
33
+ /** Session or request ID grouping related changes */
34
+ context?: string;
35
+ /** Additional metadata key-value pairs */
36
+ metadata: Record<string, string>;
37
+ }
38
+
39
+ /**
40
+ * A Chronicle node representing a single recorded state transition.
41
+ */
42
+ export interface ChronicleNode {
43
+ /** Unique node ID: `chronos:{timestamp}-{counter}` */
44
+ id: string;
45
+ /** Timestamp (ms since epoch) when this node was recorded */
46
+ timestamp: number;
47
+ /** The recorded state transition */
48
+ event: ChronicleEvent;
49
+ }
50
+
51
+ /**
52
+ * A directed edge in the causal graph connecting two Chronicle nodes.
53
+ */
54
+ export interface ChronicleEdge {
55
+ /** Source node ID */
56
+ from: string;
57
+ /** Target node ID */
58
+ to: string;
59
+ /** Type of causal relationship */
60
+ type: EdgeType;
61
+ }
@@ -28,6 +28,20 @@ export interface PraxisEngineOptions<TContext = unknown> {
28
28
  initialFacts?: PraxisFact[];
29
29
  /** Initial metadata (optional) */
30
30
  initialMeta?: Record<string, unknown>;
31
+ /**
32
+ * Fact deduplication strategy (default: 'last-write-wins').
33
+ *
34
+ * - 'none': facts accumulate without dedup (original behavior)
35
+ * - 'last-write-wins': only keep the latest fact per tag (most common)
36
+ * - 'append': keep all facts but cap at maxFacts
37
+ */
38
+ factDedup?: 'none' | 'last-write-wins' | 'append';
39
+ /**
40
+ * Maximum number of facts to retain (default: 1000).
41
+ * When exceeded, oldest facts are evicted (FIFO).
42
+ * Set to 0 for unlimited (not recommended).
43
+ */
44
+ maxFacts?: number;
31
45
  }
32
46
 
33
47
  /**
@@ -69,9 +83,13 @@ function safeClone<T>(value: T): T {
69
83
  export class LogicEngine<TContext = unknown> {
70
84
  private state: PraxisState & { context: TContext };
71
85
  private readonly registry: PraxisRegistry<TContext>;
86
+ private readonly factDedup: 'none' | 'last-write-wins' | 'append';
87
+ private readonly maxFacts: number;
72
88
 
73
89
  constructor(options: PraxisEngineOptions<TContext>) {
74
90
  this.registry = options.registry;
91
+ this.factDedup = options.factDedup ?? 'last-write-wins';
92
+ this.maxFacts = options.maxFacts ?? 1000;
75
93
  this.state = {
76
94
  context: options.initialContext,
77
95
  facts: options.initialFacts ?? [],
@@ -134,6 +152,7 @@ export class LogicEngine<TContext = unknown> {
134
152
 
135
153
  // Apply rules
136
154
  const newFacts: PraxisFact[] = [];
155
+ const eventTags = new Set(events.map(e => e.tag));
137
156
  for (const ruleId of config.ruleIds) {
138
157
  const rule = this.registry.getRule(ruleId);
139
158
  if (!rule) {
@@ -145,6 +164,15 @@ export class LogicEngine<TContext = unknown> {
145
164
  continue;
146
165
  }
147
166
 
167
+ // Event type filtering: if rule declares eventTypes, skip unless
168
+ // at least one event in the batch matches.
169
+ if (rule.eventTypes) {
170
+ const filterTags = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
171
+ if (!filterTags.some(t => eventTags.has(t))) {
172
+ continue; // No matching events — skip this rule
173
+ }
174
+ }
175
+
148
176
  try {
149
177
  const ruleFacts = rule.impl(newState, events);
150
178
  newFacts.push(...ruleFacts);
@@ -157,10 +185,35 @@ export class LogicEngine<TContext = unknown> {
157
185
  }
158
186
  }
159
187
 
188
+ // Merge new facts with deduplication
189
+ let mergedFacts: PraxisFact[];
190
+ switch (this.factDedup) {
191
+ case 'last-write-wins': {
192
+ // Build a map keyed by tag — new facts overwrite old ones with same tag
193
+ const factMap = new Map<string, PraxisFact>();
194
+ for (const f of newState.facts) factMap.set(f.tag, f);
195
+ for (const f of newFacts) factMap.set(f.tag, f);
196
+ mergedFacts = Array.from(factMap.values());
197
+ break;
198
+ }
199
+ case 'append':
200
+ mergedFacts = [...newState.facts, ...newFacts];
201
+ break;
202
+ case 'none':
203
+ default:
204
+ mergedFacts = [...newState.facts, ...newFacts];
205
+ break;
206
+ }
207
+
208
+ // Enforce maxFacts limit (evict oldest first)
209
+ if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
210
+ mergedFacts = mergedFacts.slice(mergedFacts.length - this.maxFacts);
211
+ }
212
+
160
213
  // Add new facts to state
161
214
  newState = {
162
215
  ...newState,
163
- facts: [...newState.facts, ...newFacts],
216
+ facts: mergedFacts,
164
217
  };
165
218
 
166
219
  // Check constraints
@@ -221,6 +274,35 @@ export class LogicEngine<TContext = unknown> {
221
274
  };
222
275
  }
223
276
 
277
+ /**
278
+ * Atomically update context AND process events in a single call.
279
+ *
280
+ * This avoids the fragile pattern of calling updateContext() then step()
281
+ * separately, where rules could see stale context if the ordering is wrong.
282
+ *
283
+ * @param updater Function that produces new context from old context
284
+ * @param events Events to process after context is updated
285
+ * @returns Result with new state and diagnostics
286
+ *
287
+ * @example
288
+ * engine.stepWithContext(
289
+ * ctx => ({ ...ctx, sprintName: sprint.name, items: sprint.items }),
290
+ * [{ tag: 'sprint.update', payload: { name: sprint.name } }]
291
+ * );
292
+ */
293
+ stepWithContext(
294
+ updater: (context: TContext) => TContext,
295
+ events: PraxisEvent[]
296
+ ): PraxisStepResult {
297
+ // Update context first — rules see fresh data
298
+ this.state = {
299
+ ...this.state,
300
+ context: updater(this.state.context),
301
+ };
302
+ // Then step with the now-current context
303
+ return this.step(events);
304
+ }
305
+
224
306
  /**
225
307
  * Add facts directly (for exceptional cases).
226
308
  * Generally, facts should be added through rules.
@@ -234,6 +316,22 @@ export class LogicEngine<TContext = unknown> {
234
316
  };
235
317
  }
236
318
 
319
+ /**
320
+ * Check all constraints without processing any events.
321
+ *
322
+ * Useful for validation-only scenarios (e.g., form validation,
323
+ * pre-save checks) where you want constraint diagnostics without
324
+ * triggering any rules.
325
+ *
326
+ * @returns Array of constraint violation diagnostics (empty = all passing)
327
+ */
328
+ checkConstraints(): PraxisDiagnostics[] {
329
+ return this.stepWithConfig([], {
330
+ ruleIds: [],
331
+ constraintIds: this.registry.getConstraintIds(),
332
+ }).diagnostics;
333
+ }
334
+
237
335
  /**
238
336
  * Clear all facts
239
337
  */
@@ -32,3 +32,25 @@ export {
32
32
  // Config Generator - Generate PluresDB config from schemas
33
33
  export type { PluresDBGeneratorOptions, GeneratedPluresDBFile } from './generator.js';
34
34
  export { PluresDBGenerator, createPluresDBGenerator } from './generator.js';
35
+
36
+ // Chronicle - Causal graph tracking for state transitions
37
+ export type {
38
+ TraceDirection,
39
+ EdgeType,
40
+ ChronicleEvent,
41
+ ChronicleNode,
42
+ ChronicleEdge,
43
+ Chronicle,
44
+ ChronosTraceParams,
45
+ ChronosSearchParams,
46
+ McpToolResult,
47
+ ChronosMcpTools,
48
+ } from '../chronicle/index.js';
49
+ export {
50
+ ChronicleContext,
51
+ PluresDbChronicle,
52
+ createChronicle,
53
+ CHRONICLE_PATHS,
54
+ createChronosMcpTools,
55
+ } from '../chronicle/index.js';
56
+ export type { ChronicleSpan } from '../chronicle/index.js';
@@ -11,6 +11,8 @@ declare const process: { env: { [key: string]: string | undefined } } | undefine
11
11
  import type { PraxisDB, UnsubscribeFn } from './adapter.js';
12
12
  import type { PraxisRegistry } from '../rules.js';
13
13
  import type { PraxisFact, PraxisEvent, PraxisState } from '../protocol.js';
14
+ import type { Chronicle } from '../chronicle/chronicle.js';
15
+ import { ChronicleContext } from '../chronicle/context.js';
14
16
 
15
17
  /**
16
18
  * Key paths for Praxis data in PluresDB
@@ -110,6 +112,7 @@ export class PraxisDBStore<TContext = unknown> {
110
112
  private subscriptions: UnsubscribeFn[] = [];
111
113
  private factWatchers = new Map<string, Set<(facts: PraxisFact[]) => void>>();
112
114
  private onRuleError: RuleErrorHandler;
115
+ private chronicle?: Chronicle;
113
116
 
114
117
  constructor(options: PraxisDBStoreOptions<TContext> & { onRuleError?: RuleErrorHandler }) {
115
118
  this.db = options.db;
@@ -118,6 +121,26 @@ export class PraxisDBStore<TContext = unknown> {
118
121
  this.onRuleError = options.onRuleError ?? defaultErrorHandler;
119
122
  }
120
123
 
124
+ /**
125
+ * Attach a Chronicle observer to this store.
126
+ *
127
+ * Every subsequent `storeFact` and `appendEvent` call will be recorded as a
128
+ * causal graph node in PluresDB, enabling full observability for free.
129
+ *
130
+ * @param chronicle Chronicle implementation to attach
131
+ * @returns `this` for fluent chaining
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const store = createPraxisDBStore(db, registry)
136
+ * .withChronicle(createChronicle(db));
137
+ * ```
138
+ */
139
+ withChronicle(chronicle: Chronicle): this {
140
+ this.chronicle = chronicle;
141
+ return this;
142
+ }
143
+
121
144
  /**
122
145
  * Store a fact in PluresDB
123
146
  *
@@ -134,8 +157,37 @@ export class PraxisDBStore<TContext = unknown> {
134
157
  throw new Error(`Constraint violation: ${constraintResult.errors.join(', ')}`);
135
158
  }
136
159
 
160
+ // Capture before state for Chronicle (best-effort; ignore errors)
161
+ let before: unknown;
162
+ if (this.chronicle) {
163
+ const payload = fact.payload as Record<string, unknown> | undefined;
164
+ const id = payload?.id as string | undefined;
165
+ if (id) {
166
+ before = await this.getFact(fact.tag, id);
167
+ }
168
+ }
169
+
137
170
  await this.persistFact(fact);
138
171
 
172
+ // Record state transition in Chronicle
173
+ if (this.chronicle) {
174
+ const payload = fact.payload as Record<string, unknown> | undefined;
175
+ const id = (payload?.id as string | undefined) ?? '';
176
+ const span = ChronicleContext.current;
177
+ try {
178
+ await this.chronicle.record({
179
+ path: getFactPath(fact.tag, id),
180
+ before,
181
+ after: fact,
182
+ cause: span?.spanId,
183
+ context: span?.contextId,
184
+ metadata: { factTag: fact.tag, operation: 'storeFact' },
185
+ });
186
+ } catch {
187
+ // Chronicle errors must never fail the primary operation
188
+ }
189
+ }
190
+
139
191
  // Trigger rule evaluation - facts stored directly may trigger derived computations
140
192
  await this.triggerRules([fact]);
141
193
  }
@@ -153,7 +205,36 @@ export class PraxisDBStore<TContext = unknown> {
153
205
  }
154
206
 
155
207
  for (const fact of facts) {
208
+ // Capture before state for Chronicle (best-effort)
209
+ let before: unknown;
210
+ if (this.chronicle) {
211
+ const payload = fact.payload as Record<string, unknown> | undefined;
212
+ const id = payload?.id as string | undefined;
213
+ if (id) {
214
+ before = await this.getFact(fact.tag, id);
215
+ }
216
+ }
217
+
156
218
  await this.persistFact(fact);
219
+
220
+ // Record state transition in Chronicle
221
+ if (this.chronicle) {
222
+ const payload = fact.payload as Record<string, unknown> | undefined;
223
+ const id = (payload?.id as string | undefined) ?? '';
224
+ const span = ChronicleContext.current;
225
+ try {
226
+ await this.chronicle.record({
227
+ path: getFactPath(fact.tag, id),
228
+ before,
229
+ after: fact,
230
+ cause: span?.spanId,
231
+ context: span?.contextId,
232
+ metadata: { factTag: fact.tag, operation: 'storeFacts' },
233
+ });
234
+ } catch {
235
+ // Chronicle errors must never fail the primary operation
236
+ }
237
+ }
157
238
  }
158
239
 
159
240
  // Trigger rule evaluation
@@ -207,8 +288,37 @@ export class PraxisDBStore<TContext = unknown> {
207
288
  const newEvents = [...existingEvents, entry];
208
289
  await this.db.set(path, newEvents);
209
290
 
210
- // Trigger rules with this event
211
- await this.triggerRulesForEvents([event]);
291
+ // Record event in Chronicle and capture node ID so derived facts can link back to it
292
+ let eventNodeId: string | undefined;
293
+ if (this.chronicle) {
294
+ const span = ChronicleContext.current;
295
+ try {
296
+ const node = await this.chronicle.record({
297
+ path,
298
+ before: existingEvents.length > 0 ? existingEvents[existingEvents.length - 1] : undefined,
299
+ after: entry,
300
+ cause: span?.spanId,
301
+ context: span?.contextId,
302
+ metadata: { eventTag: event.tag, sequence: String(entry.sequence), operation: 'appendEvent' },
303
+ });
304
+ eventNodeId = node.id;
305
+ } catch {
306
+ // Chronicle errors must never fail the primary operation
307
+ }
308
+ }
309
+
310
+ // Trigger rules within a causal span attributed to this event node,
311
+ // so any derived facts automatically link back to this event via 'causes' edges.
312
+ const outerSpan = ChronicleContext.current;
313
+ const ruleSpan = eventNodeId
314
+ ? { spanId: eventNodeId, contextId: outerSpan?.contextId }
315
+ : outerSpan;
316
+
317
+ if (ruleSpan && this.chronicle) {
318
+ await ChronicleContext.runAsync(ruleSpan, () => this.triggerRulesForEvents([event]));
319
+ } else {
320
+ await this.triggerRulesForEvents([event]);
321
+ }
212
322
  }
213
323
 
214
324
  /**
@@ -224,7 +334,8 @@ export class PraxisDBStore<TContext = unknown> {
224
334
  eventsByTag.set(event.tag, [...existing, event]);
225
335
  }
226
336
 
227
- // Append each group
337
+ // Append each group and collect the last recorded event node ID for causal linking
338
+ let lastEventNodeId: string | undefined;
228
339
  for (const [tag, tagEvents] of eventsByTag) {
229
340
  const path = getEventPath(tag);
230
341
  const existingEvents = (await this.db.get<EventStreamEntry[]>(path)) ?? [];
@@ -237,10 +348,38 @@ export class PraxisDBStore<TContext = unknown> {
237
348
  }));
238
349
 
239
350
  await this.db.set(path, [...existingEvents, ...newEntries]);
351
+
352
+ // Record each appended event in Chronicle
353
+ if (this.chronicle) {
354
+ const span = ChronicleContext.current;
355
+ for (const entry of newEntries) {
356
+ try {
357
+ const node = await this.chronicle.record({
358
+ path,
359
+ after: entry,
360
+ cause: span?.spanId,
361
+ context: span?.contextId,
362
+ metadata: { eventTag: tag, sequence: String(entry.sequence), operation: 'appendEvents' },
363
+ });
364
+ lastEventNodeId = node.id;
365
+ } catch {
366
+ // Chronicle errors must never fail the primary operation
367
+ }
368
+ }
369
+ }
240
370
  }
241
371
 
242
- // Trigger rules
243
- await this.triggerRulesForEvents(events);
372
+ // Trigger rules in a causal span attributed to the last event node
373
+ const outerSpan = ChronicleContext.current;
374
+ const ruleSpan = lastEventNodeId
375
+ ? { spanId: lastEventNodeId, contextId: outerSpan?.contextId }
376
+ : outerSpan;
377
+
378
+ if (ruleSpan && this.chronicle) {
379
+ await ChronicleContext.runAsync(ruleSpan, () => this.triggerRulesForEvents(events));
380
+ } else {
381
+ await this.triggerRulesForEvents(events);
382
+ }
244
383
  }
245
384
 
246
385
  /**
@@ -386,6 +525,24 @@ export class PraxisDBStore<TContext = unknown> {
386
525
  if (constraintResult.valid) {
387
526
  for (const fact of derivedFacts) {
388
527
  await this.persistFact(fact);
528
+
529
+ // Record derived fact in Chronicle using the current causal span
530
+ if (this.chronicle) {
531
+ const payload = fact.payload as Record<string, unknown> | undefined;
532
+ const id = (payload?.id as string | undefined) ?? '';
533
+ const span = ChronicleContext.current;
534
+ try {
535
+ await this.chronicle.record({
536
+ path: getFactPath(fact.tag, id),
537
+ after: fact,
538
+ cause: span?.spanId,
539
+ context: span?.contextId,
540
+ metadata: { factTag: fact.tag, operation: 'derivedFact' },
541
+ });
542
+ } catch {
543
+ // Chronicle errors must never fail the primary operation
544
+ }
545
+ }
389
546
  }
390
547
  }
391
548
  }
package/src/core/rules.ts CHANGED
@@ -61,6 +61,18 @@ export interface RuleDescriptor<TContext = unknown> {
61
61
  description: string;
62
62
  /** Implementation function */
63
63
  impl: RuleFn<TContext>;
64
+ /**
65
+ * Optional event type filter — only evaluate this rule when at least one
66
+ * event in the batch has a matching `tag`. When omitted, the rule runs on
67
+ * every step (catch-all).
68
+ *
69
+ * Accepts a single tag string or an array of tags.
70
+ *
71
+ * @example
72
+ * { id: 'sprint-behind', eventTypes: ['sprint.update'], impl: ... }
73
+ * { id: 'note-check', eventTypes: 'note.update', impl: ... }
74
+ */
75
+ eventTypes?: string | string[];
64
76
  /** Optional contract for rule behavior */
65
77
  contract?: Contract;
66
78
  /** Optional metadata */
package/src/dsl/index.ts CHANGED
@@ -85,6 +85,11 @@ export interface DefineRuleOptions<TContext = unknown> {
85
85
  id: string;
86
86
  description: string;
87
87
  impl: RuleFn<TContext>;
88
+ /**
89
+ * Optional event type filter — only evaluate this rule when at least one
90
+ * event in the batch has a matching `tag`. Accepts a single tag or array.
91
+ */
92
+ eventTypes?: string | string[];
88
93
  contract?: Contract;
89
94
  meta?: Record<string, unknown>;
90
95
  }
@@ -115,6 +120,7 @@ export function defineRule<TContext = unknown>(
115
120
  id: options.id,
116
121
  description: options.description,
117
122
  impl: options.impl,
123
+ eventTypes: options.eventTypes,
118
124
  contract,
119
125
  meta,
120
126
  };
package/src/index.ts CHANGED
@@ -208,6 +208,18 @@ export type {
208
208
  GeneratedPluresDBFile,
209
209
  PluresDBAdapter,
210
210
  PluresDBAdapterOptions,
211
+ // Chronicle
212
+ TraceDirection,
213
+ EdgeType,
214
+ ChronicleEvent,
215
+ ChronicleNode,
216
+ ChronicleEdge,
217
+ Chronicle,
218
+ ChronicleSpan,
219
+ ChronosTraceParams,
220
+ ChronosSearchParams,
221
+ McpToolResult,
222
+ ChronosMcpTools,
211
223
  } from './integrations/pluresdb.js';
212
224
  export {
213
225
  InMemoryPraxisDB,
@@ -229,6 +241,12 @@ export {
229
241
  createPluresDBGenerator,
230
242
  createPluresDBAdapter,
231
243
  attachToEngine,
244
+ // Chronicle
245
+ ChronicleContext,
246
+ PluresDbChronicle,
247
+ createChronicle,
248
+ CHRONICLE_PATHS,
249
+ createChronosMcpTools,
232
250
  } from './integrations/pluresdb.js';
233
251
 
234
252
  // Unum Integration (Identity & Channels)
@@ -57,6 +57,28 @@ export type {
57
57
  GeneratedPluresDBFile,
58
58
  } from '../core/pluresdb/generator.js';
59
59
 
60
+ // Chronicle - Causal graph tracking for Praxis state transitions
61
+ export type {
62
+ TraceDirection,
63
+ EdgeType,
64
+ ChronicleEvent,
65
+ ChronicleNode,
66
+ ChronicleEdge,
67
+ Chronicle,
68
+ ChronicleSpan,
69
+ ChronosTraceParams,
70
+ ChronosSearchParams,
71
+ McpToolResult,
72
+ ChronosMcpTools,
73
+ } from '../core/chronicle/index.js';
74
+ export {
75
+ ChronicleContext,
76
+ PluresDbChronicle,
77
+ createChronicle,
78
+ CHRONICLE_PATHS,
79
+ createChronosMcpTools,
80
+ } from '../core/chronicle/index.js';
81
+
60
82
  /**
61
83
  * PluresDB adapter interface for engine integration
62
84
  *