@plures/praxis 1.2.13 → 1.3.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 (93) hide show
  1. package/README.md +44 -0
  2. package/dist/browser/chunk-MJK3IYTJ.js +384 -0
  3. package/dist/browser/{chunk-K377RW4V.js → chunk-N63K4KWS.js} +1 -1
  4. package/dist/browser/{engine-YJZV4SLD.js → engine-YIEGSX7U.js} +1 -1
  5. package/dist/browser/index.d.ts +104 -2
  6. package/dist/browser/index.js +188 -7
  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-DjynI82A.d.ts} +139 -5
  10. package/dist/node/{chunk-PRPQO6R5.js → chunk-5JQJZADT.js} +1 -1
  11. package/dist/node/chunk-KMJWAFZV.js +389 -0
  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-FEN5IYZ5.js} +1 -1
  21. package/dist/node/index.cjs +911 -43
  22. package/dist/node/index.d.cts +574 -7
  23. package/dist/node/index.d.ts +574 -7
  24. package/dist/node/index.js +672 -26
  25. package/dist/node/integrations/svelte.cjs +190 -3
  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-DcyGMmWY.d.cts} +8 -1
  30. package/dist/node/{protocol-Qek7ebBl.d.cts → protocol-DcyGMmWY.d.ts} +8 -1
  31. package/dist/node/{reactive-engine.svelte-CRNqHlbv.d.ts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +145 -6
  32. package/dist/node/{reactive-engine.svelte-BFIZfawz.d.cts → reactive-engine.svelte-DekxqFu0.d.ts} +145 -6
  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 +8 -3
  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/__tests__/engine-v2.test.ts +532 -0
  60. package/src/cli/commands/conversations.ts +252 -0
  61. package/src/cli/index.ts +73 -0
  62. package/src/conversations/README.md +230 -0
  63. package/src/conversations/candidate.schema.json +123 -0
  64. package/src/conversations/candidates.ts +114 -0
  65. package/src/conversations/capture.ts +56 -0
  66. package/src/conversations/classify.ts +110 -0
  67. package/src/conversations/conversation.schema.json +106 -0
  68. package/src/conversations/emitters/fs.ts +65 -0
  69. package/src/conversations/emitters/github.ts +115 -0
  70. package/src/conversations/gate.ts +102 -0
  71. package/src/conversations/index.ts +28 -0
  72. package/src/conversations/normalize.ts +51 -0
  73. package/src/conversations/redact.ts +57 -0
  74. package/src/conversations/types.ts +96 -0
  75. package/src/core/chronicle/chronicle.ts +227 -0
  76. package/src/core/chronicle/context.ts +80 -0
  77. package/src/core/chronicle/index.ts +53 -0
  78. package/src/core/chronicle/mcp.ts +135 -0
  79. package/src/core/chronicle/types.ts +61 -0
  80. package/src/core/completeness.ts +274 -0
  81. package/src/core/engine.ts +143 -3
  82. package/src/core/pluresdb/index.ts +22 -0
  83. package/src/core/pluresdb/store.ts +171 -8
  84. package/src/core/protocol.ts +7 -0
  85. package/src/core/rule-result.ts +130 -0
  86. package/src/core/rules.ts +24 -5
  87. package/src/core/ui-rules.ts +340 -0
  88. package/src/dsl/index.ts +6 -0
  89. package/src/index.ts +45 -0
  90. package/src/integrations/pluresdb.ts +22 -0
  91. package/src/vite/completeness-plugin.ts +72 -0
  92. package/dist/browser/chunk-VOMLVI6V.js +0 -197
  93. package/dist/node/chunk-VOMLVI6V.js +0 -197
@@ -196,6 +196,90 @@ var PraxisRegistry = class {
196
196
  // src/core/protocol.ts
197
197
  var PRAXIS_PROTOCOL_VERSION = "1.0.0";
198
198
 
199
+ // src/core/rule-result.ts
200
+ var RuleResult = class _RuleResult {
201
+ /** The kind of result */
202
+ kind;
203
+ /** Facts produced (only for 'emit') */
204
+ facts;
205
+ /** Fact tags to retract (only for 'retract') */
206
+ retractTags;
207
+ /** Optional reason (for noop/skip/retract — useful for debugging) */
208
+ reason;
209
+ /** The rule ID that produced this result (set by engine) */
210
+ ruleId;
211
+ constructor(kind, facts, retractTags, reason) {
212
+ this.kind = kind;
213
+ this.facts = facts;
214
+ this.retractTags = retractTags;
215
+ this.reason = reason;
216
+ }
217
+ /**
218
+ * Rule produced facts.
219
+ *
220
+ * @example
221
+ * return RuleResult.emit([
222
+ * { tag: 'sprint.behind', payload: { deficit: 5 } }
223
+ * ]);
224
+ */
225
+ static emit(facts) {
226
+ if (facts.length === 0) {
227
+ throw new Error(
228
+ "RuleResult.emit() requires at least one fact. Use RuleResult.noop() or RuleResult.skip() when a rule has nothing to say."
229
+ );
230
+ }
231
+ return new _RuleResult("emit", facts, []);
232
+ }
233
+ /**
234
+ * Rule evaluated but had nothing to report.
235
+ * Unlike returning [], this is explicit and traceable.
236
+ *
237
+ * @example
238
+ * if (ctx.completedHours >= expectedHours) {
239
+ * return RuleResult.noop('Sprint is on pace');
240
+ * }
241
+ */
242
+ static noop(reason) {
243
+ return new _RuleResult("noop", [], [], reason);
244
+ }
245
+ /**
246
+ * Rule decided to skip because preconditions were not met.
247
+ * Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
248
+ *
249
+ * @example
250
+ * if (!ctx.sprintName) {
251
+ * return RuleResult.skip('No active sprint');
252
+ * }
253
+ */
254
+ static skip(reason) {
255
+ return new _RuleResult("skip", [], [], reason);
256
+ }
257
+ /**
258
+ * Rule retracts previously emitted facts by tag.
259
+ * Used when a condition that previously produced facts is no longer true.
260
+ *
261
+ * @example
262
+ * // Sprint was behind, but caught up
263
+ * if (ctx.completedHours >= expectedHours) {
264
+ * return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
265
+ * }
266
+ */
267
+ static retract(tags, reason) {
268
+ if (tags.length === 0) {
269
+ throw new Error("RuleResult.retract() requires at least one tag.");
270
+ }
271
+ return new _RuleResult("retract", [], tags, reason);
272
+ }
273
+ /** Whether this result produced facts */
274
+ get hasFacts() {
275
+ return this.facts.length > 0;
276
+ }
277
+ /** Whether this result retracts facts */
278
+ get hasRetractions() {
279
+ return this.retractTags.length > 0;
280
+ }
281
+ };
282
+
199
283
  // src/core/engine.ts
200
284
  function safeClone(value) {
201
285
  if (value === null || typeof value !== "object") {
@@ -215,8 +299,12 @@ function safeClone(value) {
215
299
  var LogicEngine = class {
216
300
  state;
217
301
  registry;
302
+ factDedup;
303
+ maxFacts;
218
304
  constructor(options) {
219
305
  this.registry = options.registry;
306
+ this.factDedup = options.factDedup ?? "last-write-wins";
307
+ this.maxFacts = options.maxFacts ?? 1e3;
220
308
  this.state = {
221
309
  context: options.initialContext,
222
310
  facts: options.initialFacts ?? [],
@@ -271,7 +359,14 @@ var LogicEngine = class {
271
359
  stepWithConfig(events, config) {
272
360
  const diagnostics = [];
273
361
  let newState = { ...this.state };
362
+ const stateWithEvents = {
363
+ ...newState,
364
+ events
365
+ // current batch — rules can read state.events
366
+ };
274
367
  const newFacts = [];
368
+ const retractions = [];
369
+ const eventTags = new Set(events.map((e) => e.tag));
275
370
  for (const ruleId of config.ruleIds) {
276
371
  const rule = this.registry.getRule(ruleId);
277
372
  if (!rule) {
@@ -282,9 +377,38 @@ var LogicEngine = class {
282
377
  });
283
378
  continue;
284
379
  }
380
+ if (rule.eventTypes) {
381
+ const filterTags = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
382
+ if (!filterTags.some((t) => eventTags.has(t))) {
383
+ continue;
384
+ }
385
+ }
285
386
  try {
286
- const ruleFacts = rule.impl(newState, events);
287
- newFacts.push(...ruleFacts);
387
+ const rawResult = rule.impl(stateWithEvents, events);
388
+ if (rawResult instanceof RuleResult) {
389
+ rawResult.ruleId = ruleId;
390
+ switch (rawResult.kind) {
391
+ case "emit":
392
+ newFacts.push(...rawResult.facts);
393
+ break;
394
+ case "retract":
395
+ retractions.push(...rawResult.retractTags);
396
+ break;
397
+ case "noop":
398
+ case "skip":
399
+ if (rawResult.reason) {
400
+ diagnostics.push({
401
+ kind: "rule-error",
402
+ // reused kind — could add 'rule-trace' in protocol v2
403
+ message: `[${rawResult.kind}] ${ruleId}: ${rawResult.reason}`,
404
+ data: { ruleId, resultKind: rawResult.kind, reason: rawResult.reason }
405
+ });
406
+ }
407
+ break;
408
+ }
409
+ } else if (Array.isArray(rawResult)) {
410
+ newFacts.push(...rawResult);
411
+ }
288
412
  } catch (error) {
289
413
  diagnostics.push({
290
414
  kind: "rule-error",
@@ -293,9 +417,34 @@ var LogicEngine = class {
293
417
  });
294
418
  }
295
419
  }
420
+ let existingFacts = newState.facts;
421
+ if (retractions.length > 0) {
422
+ const retractSet = new Set(retractions);
423
+ existingFacts = existingFacts.filter((f) => !retractSet.has(f.tag));
424
+ }
425
+ let mergedFacts;
426
+ switch (this.factDedup) {
427
+ case "last-write-wins": {
428
+ const factMap = /* @__PURE__ */ new Map();
429
+ for (const f of existingFacts) factMap.set(f.tag, f);
430
+ for (const f of newFacts) factMap.set(f.tag, f);
431
+ mergedFacts = Array.from(factMap.values());
432
+ break;
433
+ }
434
+ case "append":
435
+ mergedFacts = [...existingFacts, ...newFacts];
436
+ break;
437
+ case "none":
438
+ default:
439
+ mergedFacts = [...existingFacts, ...newFacts];
440
+ break;
441
+ }
442
+ if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
443
+ mergedFacts = mergedFacts.slice(mergedFacts.length - this.maxFacts);
444
+ }
296
445
  newState = {
297
446
  ...newState,
298
- facts: [...newState.facts, ...newFacts]
447
+ facts: mergedFacts
299
448
  };
300
449
  for (const constraintId of config.constraintIds) {
301
450
  const constraint = this.registry.getConstraint(constraintId);
@@ -348,6 +497,29 @@ var LogicEngine = class {
348
497
  context: updater(this.state.context)
349
498
  };
350
499
  }
500
+ /**
501
+ * Atomically update context AND process events in a single call.
502
+ *
503
+ * This avoids the fragile pattern of calling updateContext() then step()
504
+ * separately, where rules could see stale context if the ordering is wrong.
505
+ *
506
+ * @param updater Function that produces new context from old context
507
+ * @param events Events to process after context is updated
508
+ * @returns Result with new state and diagnostics
509
+ *
510
+ * @example
511
+ * engine.stepWithContext(
512
+ * ctx => ({ ...ctx, sprintName: sprint.name, items: sprint.items }),
513
+ * [{ tag: 'sprint.update', payload: { name: sprint.name } }]
514
+ * );
515
+ */
516
+ stepWithContext(updater, events) {
517
+ this.state = {
518
+ ...this.state,
519
+ context: updater(this.state.context)
520
+ };
521
+ return this.step(events);
522
+ }
351
523
  /**
352
524
  * Add facts directly (for exceptional cases).
353
525
  * Generally, facts should be added through rules.
@@ -360,6 +532,21 @@ var LogicEngine = class {
360
532
  facts: [...this.state.facts, ...facts]
361
533
  };
362
534
  }
535
+ /**
536
+ * Check all constraints without processing any events.
537
+ *
538
+ * Useful for validation-only scenarios (e.g., form validation,
539
+ * pre-save checks) where you want constraint diagnostics without
540
+ * triggering any rules.
541
+ *
542
+ * @returns Array of constraint violation diagnostics (empty = all passing)
543
+ */
544
+ checkConstraints() {
545
+ return this.stepWithConfig([], {
546
+ ruleIds: [],
547
+ constraintIds: this.registry.getConstraintIds()
548
+ }).diagnostics;
549
+ }
363
550
  /**
364
551
  * Clear all facts
365
552
  */
@@ -1,6 +1,6 @@
1
- import { L as LogicEngine } from '../reactive-engine.svelte-BFIZfawz.cjs';
2
- export { a as ReactiveEngineOptions, R as ReactiveLogicEngine, c as createReactiveEngine } from '../reactive-engine.svelte-BFIZfawz.cjs';
3
- import { P as PraxisState, a as PraxisEvent } from '../protocol-Qek7ebBl.cjs';
1
+ import { L as LogicEngine } from '../reactive-engine.svelte-Cg0Yc2Hs.cjs';
2
+ export { R as ReactiveEngineOptions, a as ReactiveLogicEngine, c as createReactiveEngine } from '../reactive-engine.svelte-Cg0Yc2Hs.cjs';
3
+ import { P as PraxisState, a as PraxisEvent } from '../protocol-DcyGMmWY.cjs';
4
4
 
5
5
  /**
6
6
  * Svelte v5 Integration
@@ -1,6 +1,6 @@
1
- import { L as LogicEngine } from '../reactive-engine.svelte-CRNqHlbv.js';
2
- export { a as ReactiveEngineOptions, R as ReactiveLogicEngine, c as createReactiveEngine } from '../reactive-engine.svelte-CRNqHlbv.js';
3
- import { P as PraxisState, a as PraxisEvent } from '../protocol-Qek7ebBl.js';
1
+ import { L as LogicEngine } from '../reactive-engine.svelte-DekxqFu0.js';
2
+ export { R as ReactiveEngineOptions, a as ReactiveLogicEngine, c as createReactiveEngine } from '../reactive-engine.svelte-DekxqFu0.js';
3
+ import { P as PraxisState, a as PraxisEvent } from '../protocol-DcyGMmWY.js';
4
4
 
5
5
  /**
6
6
  * Svelte v5 Integration
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  ReactiveLogicEngine,
3
3
  createReactiveEngine
4
- } from "../chunk-PRPQO6R5.js";
4
+ } from "../chunk-5JQJZADT.js";
5
5
  import "../chunk-R2PSBPKQ.js";
6
- import "../chunk-VOMLVI6V.js";
6
+ import "../chunk-KMJWAFZV.js";
7
7
  import "../chunk-QGM4M3NI.js";
8
8
 
9
9
  // src/integrations/svelte.ts
@@ -71,6 +71,13 @@ interface PraxisState {
71
71
  context: unknown;
72
72
  /** Current facts about the domain */
73
73
  facts: PraxisFact[];
74
+ /**
75
+ * Events currently being processed in this step.
76
+ * Available to rules during execution — guaranteed to contain the exact
77
+ * events passed to step()/stepWithContext().
78
+ * Empty outside of step execution.
79
+ */
80
+ events?: PraxisEvent[];
74
81
  /** Optional metadata (timestamps, version, etc.) */
75
82
  meta?: Record<string, unknown>;
76
83
  /** Protocol version (for cross-language compatibility) */
@@ -119,4 +126,4 @@ interface PraxisStepResult {
119
126
  */
120
127
  type PraxisStepFn = (state: PraxisState, events: PraxisEvent[], config: PraxisStepConfig) => PraxisStepResult;
121
128
 
122
- export { type PraxisState as P, type PraxisEvent as a, type PraxisFact as b, type PraxisStepResult as c, type PraxisStepConfig as d, type PraxisDiagnostics as e, type PraxisStepFn as f, PRAXIS_PROTOCOL_VERSION as g };
129
+ export { type PraxisState as P, type PraxisEvent as a, type PraxisFact as b, type PraxisStepResult as c, type PraxisStepConfig as d, type PraxisDiagnostics as e, PRAXIS_PROTOCOL_VERSION as f, type PraxisStepFn as g };
@@ -71,6 +71,13 @@ interface PraxisState {
71
71
  context: unknown;
72
72
  /** Current facts about the domain */
73
73
  facts: PraxisFact[];
74
+ /**
75
+ * Events currently being processed in this step.
76
+ * Available to rules during execution — guaranteed to contain the exact
77
+ * events passed to step()/stepWithContext().
78
+ * Empty outside of step execution.
79
+ */
80
+ events?: PraxisEvent[];
74
81
  /** Optional metadata (timestamps, version, etc.) */
75
82
  meta?: Record<string, unknown>;
76
83
  /** Protocol version (for cross-language compatibility) */
@@ -119,4 +126,4 @@ interface PraxisStepResult {
119
126
  */
120
127
  type PraxisStepFn = (state: PraxisState, events: PraxisEvent[], config: PraxisStepConfig) => PraxisStepResult;
121
128
 
122
- export { type PraxisState as P, type PraxisEvent as a, type PraxisFact as b, type PraxisStepResult as c, type PraxisStepConfig as d, type PraxisDiagnostics as e, type PraxisStepFn as f, PRAXIS_PROTOCOL_VERSION as g };
129
+ export { type PraxisState as P, type PraxisEvent as a, type PraxisFact as b, type PraxisStepResult as c, type PraxisStepConfig as d, type PraxisDiagnostics as e, PRAXIS_PROTOCOL_VERSION as f, type PraxisStepFn as g };
@@ -1,4 +1,4 @@
1
- import { P as PraxisState, a as PraxisEvent, b as PraxisFact, c as PraxisStepResult, d as PraxisStepConfig } from './protocol-Qek7ebBl.js';
1
+ import { b as PraxisFact, P as PraxisState, a as PraxisEvent, c as PraxisStepResult, d as PraxisStepConfig, e as PraxisDiagnostics } from './protocol-DcyGMmWY.cjs';
2
2
 
3
3
  /**
4
4
  * Decision Ledger - Contract Types
@@ -162,6 +162,83 @@ interface ValidationReport {
162
162
  timestamp: string;
163
163
  }
164
164
 
165
+ /**
166
+ * The result of evaluating a rule. Every rule MUST return one of:
167
+ * - `RuleResult.emit(facts)` — rule produced facts
168
+ * - `RuleResult.noop(reason?)` — rule evaluated but had nothing to say
169
+ * - `RuleResult.skip(reason?)` — rule decided to skip (preconditions not met)
170
+ * - `RuleResult.retract(tags)` — rule retracts previously emitted facts
171
+ */
172
+ declare class RuleResult {
173
+ /** The kind of result */
174
+ readonly kind: 'emit' | 'noop' | 'skip' | 'retract';
175
+ /** Facts produced (only for 'emit') */
176
+ readonly facts: PraxisFact[];
177
+ /** Fact tags to retract (only for 'retract') */
178
+ readonly retractTags: string[];
179
+ /** Optional reason (for noop/skip/retract — useful for debugging) */
180
+ readonly reason?: string;
181
+ /** The rule ID that produced this result (set by engine) */
182
+ ruleId?: string;
183
+ private constructor();
184
+ /**
185
+ * Rule produced facts.
186
+ *
187
+ * @example
188
+ * return RuleResult.emit([
189
+ * { tag: 'sprint.behind', payload: { deficit: 5 } }
190
+ * ]);
191
+ */
192
+ static emit(facts: PraxisFact[]): RuleResult;
193
+ /**
194
+ * Rule evaluated but had nothing to report.
195
+ * Unlike returning [], this is explicit and traceable.
196
+ *
197
+ * @example
198
+ * if (ctx.completedHours >= expectedHours) {
199
+ * return RuleResult.noop('Sprint is on pace');
200
+ * }
201
+ */
202
+ static noop(reason?: string): RuleResult;
203
+ /**
204
+ * Rule decided to skip because preconditions were not met.
205
+ * Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
206
+ *
207
+ * @example
208
+ * if (!ctx.sprintName) {
209
+ * return RuleResult.skip('No active sprint');
210
+ * }
211
+ */
212
+ static skip(reason?: string): RuleResult;
213
+ /**
214
+ * Rule retracts previously emitted facts by tag.
215
+ * Used when a condition that previously produced facts is no longer true.
216
+ *
217
+ * @example
218
+ * // Sprint was behind, but caught up
219
+ * if (ctx.completedHours >= expectedHours) {
220
+ * return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
221
+ * }
222
+ */
223
+ static retract(tags: string[], reason?: string): RuleResult;
224
+ /** Whether this result produced facts */
225
+ get hasFacts(): boolean;
226
+ /** Whether this result retracts facts */
227
+ get hasRetractions(): boolean;
228
+ }
229
+ /**
230
+ * A rule function that returns a typed RuleResult.
231
+ * New API — replaces the old PraxisFact[] return type.
232
+ */
233
+ type TypedRuleFn<TContext = unknown> = (state: PraxisState & {
234
+ context: TContext;
235
+ events: PraxisEvent[];
236
+ }, events: PraxisEvent[]) => RuleResult;
237
+ /**
238
+ * Convenience: create a fact object (just a shorthand)
239
+ */
240
+ declare function fact(tag: string, payload: unknown): PraxisFact;
241
+
165
242
  /**
166
243
  * Rules and Constraints System
167
244
  *
@@ -182,13 +259,20 @@ type ConstraintId = string;
182
259
  * A rule function derives new facts or transitions from context + input facts/events.
183
260
  * Rules must be pure - no side effects.
184
261
  *
185
- * @param state Current Praxis state
186
- * @param events Events to process
187
- * @returns Array of new facts to add to the state
262
+ * Returns either:
263
+ * - `RuleResult` (new API — typed, traceable, supports retraction)
264
+ * - `PraxisFact[]` (legacy backward compatible, will be deprecated)
265
+ *
266
+ * The state parameter includes `events` — the current batch being processed.
267
+ *
268
+ * @param state Current Praxis state (includes state.events for current batch)
269
+ * @param events Events to process (same as state.events, provided for convenience)
270
+ * @returns RuleResult or array of new facts
188
271
  */
189
272
  type RuleFn<TContext = unknown> = (state: PraxisState & {
190
273
  context: TContext;
191
- }, events: PraxisEvent[]) => PraxisFact[];
274
+ events: PraxisEvent[];
275
+ }, events: PraxisEvent[]) => RuleResult | PraxisFact[];
192
276
  /**
193
277
  * A constraint function checks that an invariant holds.
194
278
  * Constraints must be pure - no side effects.
@@ -209,6 +293,18 @@ interface RuleDescriptor<TContext = unknown> {
209
293
  description: string;
210
294
  /** Implementation function */
211
295
  impl: RuleFn<TContext>;
296
+ /**
297
+ * Optional event type filter — only evaluate this rule when at least one
298
+ * event in the batch has a matching `tag`. When omitted, the rule runs on
299
+ * every step (catch-all).
300
+ *
301
+ * Accepts a single tag string or an array of tags.
302
+ *
303
+ * @example
304
+ * { id: 'sprint-behind', eventTypes: ['sprint.update'], impl: ... }
305
+ * { id: 'note-check', eventTypes: 'note.update', impl: ... }
306
+ */
307
+ eventTypes?: string | string[];
212
308
  /** Optional contract for rule behavior */
213
309
  contract?: Contract;
214
310
  /** Optional metadata */
@@ -337,6 +433,20 @@ interface PraxisEngineOptions<TContext = unknown> {
337
433
  initialFacts?: PraxisFact[];
338
434
  /** Initial metadata (optional) */
339
435
  initialMeta?: Record<string, unknown>;
436
+ /**
437
+ * Fact deduplication strategy (default: 'last-write-wins').
438
+ *
439
+ * - 'none': facts accumulate without dedup (original behavior)
440
+ * - 'last-write-wins': only keep the latest fact per tag (most common)
441
+ * - 'append': keep all facts but cap at maxFacts
442
+ */
443
+ factDedup?: 'none' | 'last-write-wins' | 'append';
444
+ /**
445
+ * Maximum number of facts to retain (default: 1000).
446
+ * When exceeded, oldest facts are evicted (FIFO).
447
+ * Set to 0 for unlimited (not recommended).
448
+ */
449
+ maxFacts?: number;
340
450
  }
341
451
  /**
342
452
  * The Praxis Logic Engine
@@ -347,6 +457,8 @@ interface PraxisEngineOptions<TContext = unknown> {
347
457
  declare class LogicEngine<TContext = unknown> {
348
458
  private state;
349
459
  private readonly registry;
460
+ private readonly factDedup;
461
+ private readonly maxFacts;
350
462
  constructor(options: PraxisEngineOptions<TContext>);
351
463
  /**
352
464
  * Get the current state (immutable copy)
@@ -385,6 +497,23 @@ declare class LogicEngine<TContext = unknown> {
385
497
  * @param updater Function that produces new context from old context
386
498
  */
387
499
  updateContext(updater: (context: TContext) => TContext): void;
500
+ /**
501
+ * Atomically update context AND process events in a single call.
502
+ *
503
+ * This avoids the fragile pattern of calling updateContext() then step()
504
+ * separately, where rules could see stale context if the ordering is wrong.
505
+ *
506
+ * @param updater Function that produces new context from old context
507
+ * @param events Events to process after context is updated
508
+ * @returns Result with new state and diagnostics
509
+ *
510
+ * @example
511
+ * engine.stepWithContext(
512
+ * ctx => ({ ...ctx, sprintName: sprint.name, items: sprint.items }),
513
+ * [{ tag: 'sprint.update', payload: { name: sprint.name } }]
514
+ * );
515
+ */
516
+ stepWithContext(updater: (context: TContext) => TContext, events: PraxisEvent[]): PraxisStepResult;
388
517
  /**
389
518
  * Add facts directly (for exceptional cases).
390
519
  * Generally, facts should be added through rules.
@@ -392,6 +521,16 @@ declare class LogicEngine<TContext = unknown> {
392
521
  * @param facts Facts to add
393
522
  */
394
523
  addFacts(facts: PraxisFact[]): void;
524
+ /**
525
+ * Check all constraints without processing any events.
526
+ *
527
+ * Useful for validation-only scenarios (e.g., form validation,
528
+ * pre-save checks) where you want constraint diagnostics without
529
+ * triggering any rules.
530
+ *
531
+ * @returns Array of constraint violation diagnostics (empty = all passing)
532
+ */
533
+ checkConstraints(): PraxisDiagnostics[];
395
534
  /**
396
535
  * Clear all facts
397
536
  */
@@ -495,4 +634,4 @@ declare class ReactiveLogicEngine<TContext extends object> {
495
634
  */
496
635
  declare function createReactiveEngine<TContext extends object>(options: ReactiveEngineOptions<TContext>): ReactiveLogicEngine<TContext>;
497
636
 
498
- export { type Assumption as A, type ConstraintDescriptor as C, type DefineContractOptions as D, type Example as E, LogicEngine as L, type MissingArtifact as M, PraxisRegistry as P, ReactiveLogicEngine as R, type Severity as S, type ValidationReport as V, type ReactiveEngineOptions as a, type RuleDescriptor as b, createReactiveEngine as c, type RuleFn as d, type Contract as e, type ConstraintFn as f, type PraxisModule as g, type RuleId as h, type ConstraintId as i, type PraxisEngineOptions as j, createPraxisEngine as k, defineContract as l, getContract as m, isContract as n, type Reference as o, type ContractGap as p };
637
+ export { type Assumption as A, type ConstraintDescriptor as C, type DefineContractOptions as D, type Example as E, LogicEngine as L, type MissingArtifact as M, PraxisRegistry as P, type ReactiveEngineOptions as R, type Severity as S, type TypedRuleFn as T, type ValidationReport as V, ReactiveLogicEngine as a, type RuleDescriptor as b, createReactiveEngine as c, type ConstraintFn as d, type Contract as e, type RuleFn as f, type PraxisModule as g, type ConstraintId as h, type ContractGap as i, type PraxisEngineOptions as j, type Reference as k, type RuleId as l, RuleResult as m, createPraxisEngine as n, defineContract as o, fact as p, getContract as q, isContract as r };