@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
@@ -2,12 +2,12 @@ import {
2
2
  PraxisRegistry,
3
3
  ReactiveLogicEngine,
4
4
  createReactiveEngine
5
- } from "./chunk-K377RW4V.js";
5
+ } from "./chunk-N63K4KWS.js";
6
6
  import {
7
7
  LogicEngine,
8
8
  PRAXIS_PROTOCOL_VERSION,
9
9
  createPraxisEngine
10
- } from "./chunk-VOMLVI6V.js";
10
+ } from "./chunk-MJK3IYTJ.js";
11
11
  import {
12
12
  InMemoryPraxisDB,
13
13
  PluresDBPraxisAdapter,
@@ -586,6 +586,7 @@ function defineRule(options) {
586
586
  id: options.id,
587
587
  description: options.description,
588
588
  impl: options.impl,
589
+ eventTypes: options.eventTypes,
589
590
  contract,
590
591
  meta
591
592
  };
@@ -884,6 +885,53 @@ function validateForGeneration(schema) {
884
885
  };
885
886
  }
886
887
 
888
+ // src/core/chronicle/context.ts
889
+ var ChronicleContext = class {
890
+ static _stack = [];
891
+ /**
892
+ * Get the current active span, if any.
893
+ */
894
+ static get current() {
895
+ return this._stack[this._stack.length - 1];
896
+ }
897
+ /**
898
+ * Run a synchronous function within a causal span.
899
+ * The span is automatically popped when the function returns.
900
+ */
901
+ static run(span, fn) {
902
+ this._stack.push(span);
903
+ try {
904
+ return fn();
905
+ } finally {
906
+ this._stack.pop();
907
+ }
908
+ }
909
+ /**
910
+ * Run an async function within a causal span.
911
+ * The span is popped after the promise settles.
912
+ */
913
+ static async runAsync(span, fn) {
914
+ this._stack.push(span);
915
+ try {
916
+ return await fn();
917
+ } finally {
918
+ this._stack.pop();
919
+ }
920
+ }
921
+ /**
922
+ * Create a child span that inherits the current contextId.
923
+ *
924
+ * @param spanId ID for the new span
925
+ * @returns A new ChronicleSpan with the current contextId
926
+ */
927
+ static childSpan(spanId) {
928
+ return {
929
+ spanId,
930
+ contextId: this.current?.contextId
931
+ };
932
+ }
933
+ };
934
+
887
935
  // src/core/pluresdb/store.ts
888
936
  var PRAXIS_PATHS = {
889
937
  /** Base path for all Praxis data */
@@ -919,12 +967,32 @@ var PraxisDBStore = class {
919
967
  subscriptions = [];
920
968
  factWatchers = /* @__PURE__ */ new Map();
921
969
  onRuleError;
970
+ chronicle;
922
971
  constructor(options) {
923
972
  this.db = options.db;
924
973
  this.registry = options.registry;
925
974
  this.context = options.initialContext ?? {};
926
975
  this.onRuleError = options.onRuleError ?? defaultErrorHandler;
927
976
  }
977
+ /**
978
+ * Attach a Chronicle observer to this store.
979
+ *
980
+ * Every subsequent `storeFact` and `appendEvent` call will be recorded as a
981
+ * causal graph node in PluresDB, enabling full observability for free.
982
+ *
983
+ * @param chronicle Chronicle implementation to attach
984
+ * @returns `this` for fluent chaining
985
+ *
986
+ * @example
987
+ * ```typescript
988
+ * const store = createPraxisDBStore(db, registry)
989
+ * .withChronicle(createChronicle(db));
990
+ * ```
991
+ */
992
+ withChronicle(chronicle) {
993
+ this.chronicle = chronicle;
994
+ return this;
995
+ }
928
996
  /**
929
997
  * Store a fact in PluresDB
930
998
  *
@@ -939,7 +1007,31 @@ var PraxisDBStore = class {
939
1007
  if (!constraintResult.valid) {
940
1008
  throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
941
1009
  }
1010
+ let before;
1011
+ if (this.chronicle) {
1012
+ const payload = fact.payload;
1013
+ const id = payload?.id;
1014
+ if (id) {
1015
+ before = await this.getFact(fact.tag, id);
1016
+ }
1017
+ }
942
1018
  await this.persistFact(fact);
1019
+ if (this.chronicle) {
1020
+ const payload = fact.payload;
1021
+ const id = payload?.id ?? "";
1022
+ const span = ChronicleContext.current;
1023
+ try {
1024
+ await this.chronicle.record({
1025
+ path: getFactPath(fact.tag, id),
1026
+ before,
1027
+ after: fact,
1028
+ cause: span?.spanId,
1029
+ context: span?.contextId,
1030
+ metadata: { factTag: fact.tag, operation: "storeFact" }
1031
+ });
1032
+ } catch {
1033
+ }
1034
+ }
943
1035
  await this.triggerRules([fact]);
944
1036
  }
945
1037
  /**
@@ -953,7 +1045,31 @@ var PraxisDBStore = class {
953
1045
  throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
954
1046
  }
955
1047
  for (const fact of facts) {
1048
+ let before;
1049
+ if (this.chronicle) {
1050
+ const payload = fact.payload;
1051
+ const id = payload?.id;
1052
+ if (id) {
1053
+ before = await this.getFact(fact.tag, id);
1054
+ }
1055
+ }
956
1056
  await this.persistFact(fact);
1057
+ if (this.chronicle) {
1058
+ const payload = fact.payload;
1059
+ const id = payload?.id ?? "";
1060
+ const span = ChronicleContext.current;
1061
+ try {
1062
+ await this.chronicle.record({
1063
+ path: getFactPath(fact.tag, id),
1064
+ before,
1065
+ after: fact,
1066
+ cause: span?.spanId,
1067
+ context: span?.contextId,
1068
+ metadata: { factTag: fact.tag, operation: "storeFacts" }
1069
+ });
1070
+ } catch {
1071
+ }
1072
+ }
957
1073
  }
958
1074
  await this.triggerRules(facts);
959
1075
  }
@@ -995,7 +1111,29 @@ var PraxisDBStore = class {
995
1111
  };
996
1112
  const newEvents = [...existingEvents, entry];
997
1113
  await this.db.set(path, newEvents);
998
- await this.triggerRulesForEvents([event]);
1114
+ let eventNodeId;
1115
+ if (this.chronicle) {
1116
+ const span = ChronicleContext.current;
1117
+ try {
1118
+ const node = await this.chronicle.record({
1119
+ path,
1120
+ before: existingEvents.length > 0 ? existingEvents[existingEvents.length - 1] : void 0,
1121
+ after: entry,
1122
+ cause: span?.spanId,
1123
+ context: span?.contextId,
1124
+ metadata: { eventTag: event.tag, sequence: String(entry.sequence), operation: "appendEvent" }
1125
+ });
1126
+ eventNodeId = node.id;
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ const outerSpan = ChronicleContext.current;
1131
+ const ruleSpan = eventNodeId ? { spanId: eventNodeId, contextId: outerSpan?.contextId } : outerSpan;
1132
+ if (ruleSpan && this.chronicle) {
1133
+ await ChronicleContext.runAsync(ruleSpan, () => this.triggerRulesForEvents([event]));
1134
+ } else {
1135
+ await this.triggerRulesForEvents([event]);
1136
+ }
999
1137
  }
1000
1138
  /**
1001
1139
  * Append multiple events to their respective streams
@@ -1008,6 +1146,7 @@ var PraxisDBStore = class {
1008
1146
  const existing = eventsByTag.get(event.tag) ?? [];
1009
1147
  eventsByTag.set(event.tag, [...existing, event]);
1010
1148
  }
1149
+ let lastEventNodeId;
1011
1150
  for (const [tag, tagEvents] of eventsByTag) {
1012
1151
  const path = getEventPath(tag);
1013
1152
  const existingEvents = await this.db.get(path) ?? [];
@@ -1018,8 +1157,30 @@ var PraxisDBStore = class {
1018
1157
  sequence: sequence++
1019
1158
  }));
1020
1159
  await this.db.set(path, [...existingEvents, ...newEntries]);
1160
+ if (this.chronicle) {
1161
+ const span = ChronicleContext.current;
1162
+ for (const entry of newEntries) {
1163
+ try {
1164
+ const node = await this.chronicle.record({
1165
+ path,
1166
+ after: entry,
1167
+ cause: span?.spanId,
1168
+ context: span?.contextId,
1169
+ metadata: { eventTag: tag, sequence: String(entry.sequence), operation: "appendEvents" }
1170
+ });
1171
+ lastEventNodeId = node.id;
1172
+ } catch {
1173
+ }
1174
+ }
1175
+ }
1176
+ }
1177
+ const outerSpan = ChronicleContext.current;
1178
+ const ruleSpan = lastEventNodeId ? { spanId: lastEventNodeId, contextId: outerSpan?.contextId } : outerSpan;
1179
+ if (ruleSpan && this.chronicle) {
1180
+ await ChronicleContext.runAsync(ruleSpan, () => this.triggerRulesForEvents(events));
1181
+ } else {
1182
+ await this.triggerRulesForEvents(events);
1021
1183
  }
1022
- await this.triggerRulesForEvents(events);
1023
1184
  }
1024
1185
  /**
1025
1186
  * Get events from a stream
@@ -1116,13 +1277,18 @@ var PraxisDBStore = class {
1116
1277
  const state = {
1117
1278
  context: this.context,
1118
1279
  facts: [],
1280
+ events,
1119
1281
  meta: {}
1120
1282
  };
1121
1283
  const derivedFacts = [];
1122
1284
  for (const rule of rules) {
1123
1285
  try {
1124
- const facts = rule.impl(state, events);
1125
- derivedFacts.push(...facts);
1286
+ const result = rule.impl(state, events);
1287
+ if (Array.isArray(result)) {
1288
+ derivedFacts.push(...result);
1289
+ } else if (result && "kind" in result && result.kind === "emit") {
1290
+ derivedFacts.push(...result.facts);
1291
+ }
1126
1292
  } catch (error) {
1127
1293
  this.onRuleError(rule.id, error);
1128
1294
  }
@@ -1132,6 +1298,21 @@ var PraxisDBStore = class {
1132
1298
  if (constraintResult.valid) {
1133
1299
  for (const fact of derivedFacts) {
1134
1300
  await this.persistFact(fact);
1301
+ if (this.chronicle) {
1302
+ const payload = fact.payload;
1303
+ const id = payload?.id ?? "";
1304
+ const span = ChronicleContext.current;
1305
+ try {
1306
+ await this.chronicle.record({
1307
+ path: getFactPath(fact.tag, id),
1308
+ after: fact,
1309
+ cause: span?.spanId,
1310
+ context: span?.contextId,
1311
+ metadata: { factTag: fact.tag, operation: "derivedFact" }
1312
+ });
1313
+ } catch {
1314
+ }
1315
+ }
1135
1316
  }
1136
1317
  }
1137
1318
  }
@@ -2713,7 +2894,7 @@ function generateTauriConfig(config) {
2713
2894
 
2714
2895
  // src/integrations/unified.ts
2715
2896
  async function createUnifiedApp(config) {
2716
- const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-YJZV4SLD.js");
2897
+ const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-YIEGSX7U.js");
2717
2898
  const { createInMemoryDB: createInMemoryDB2 } = await import("./adapter-CIMBGDC7.js");
2718
2899
  const db = config.db || createInMemoryDB2();
2719
2900
  const pluresdb = createPluresDBAdapter({
@@ -1,5 +1,5 @@
1
- import { L as LogicEngine, P as PraxisState, a as PraxisEvent } from '../reactive-engine.svelte-9aS0kTa8.js';
2
- export { q as ReactiveEngineOptions, r as ReactiveLogicEngine, s as createReactiveEngine } from '../reactive-engine.svelte-9aS0kTa8.js';
1
+ import { P as PraxisState, a as PraxisEvent, L as LogicEngine } from '../reactive-engine.svelte-DjynI82A.js';
2
+ export { o as ReactiveEngineOptions, p as ReactiveLogicEngine, s as createReactiveEngine } from '../reactive-engine.svelte-DjynI82A.js';
3
3
 
4
4
  /**
5
5
  * Svelte v5 Integration
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  ReactiveLogicEngine,
3
3
  createReactiveEngine
4
- } from "../chunk-K377RW4V.js";
5
- import "../chunk-VOMLVI6V.js";
4
+ } from "../chunk-N63K4KWS.js";
5
+ import "../chunk-MJK3IYTJ.js";
6
6
 
7
7
  // src/integrations/svelte.ts
8
8
  function createPraxisStore(engine) {
@@ -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) */
@@ -218,6 +225,71 @@ interface ContractGap {
218
225
  message?: string;
219
226
  }
220
227
 
228
+ /**
229
+ * The result of evaluating a rule. Every rule MUST return one of:
230
+ * - `RuleResult.emit(facts)` — rule produced facts
231
+ * - `RuleResult.noop(reason?)` — rule evaluated but had nothing to say
232
+ * - `RuleResult.skip(reason?)` — rule decided to skip (preconditions not met)
233
+ * - `RuleResult.retract(tags)` — rule retracts previously emitted facts
234
+ */
235
+ declare class RuleResult {
236
+ /** The kind of result */
237
+ readonly kind: 'emit' | 'noop' | 'skip' | 'retract';
238
+ /** Facts produced (only for 'emit') */
239
+ readonly facts: PraxisFact[];
240
+ /** Fact tags to retract (only for 'retract') */
241
+ readonly retractTags: string[];
242
+ /** Optional reason (for noop/skip/retract — useful for debugging) */
243
+ readonly reason?: string;
244
+ /** The rule ID that produced this result (set by engine) */
245
+ ruleId?: string;
246
+ private constructor();
247
+ /**
248
+ * Rule produced facts.
249
+ *
250
+ * @example
251
+ * return RuleResult.emit([
252
+ * { tag: 'sprint.behind', payload: { deficit: 5 } }
253
+ * ]);
254
+ */
255
+ static emit(facts: PraxisFact[]): RuleResult;
256
+ /**
257
+ * Rule evaluated but had nothing to report.
258
+ * Unlike returning [], this is explicit and traceable.
259
+ *
260
+ * @example
261
+ * if (ctx.completedHours >= expectedHours) {
262
+ * return RuleResult.noop('Sprint is on pace');
263
+ * }
264
+ */
265
+ static noop(reason?: string): RuleResult;
266
+ /**
267
+ * Rule decided to skip because preconditions were not met.
268
+ * Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
269
+ *
270
+ * @example
271
+ * if (!ctx.sprintName) {
272
+ * return RuleResult.skip('No active sprint');
273
+ * }
274
+ */
275
+ static skip(reason?: string): RuleResult;
276
+ /**
277
+ * Rule retracts previously emitted facts by tag.
278
+ * Used when a condition that previously produced facts is no longer true.
279
+ *
280
+ * @example
281
+ * // Sprint was behind, but caught up
282
+ * if (ctx.completedHours >= expectedHours) {
283
+ * return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
284
+ * }
285
+ */
286
+ static retract(tags: string[], reason?: string): RuleResult;
287
+ /** Whether this result produced facts */
288
+ get hasFacts(): boolean;
289
+ /** Whether this result retracts facts */
290
+ get hasRetractions(): boolean;
291
+ }
292
+
221
293
  /**
222
294
  * Rules and Constraints System
223
295
  *
@@ -238,13 +310,20 @@ type ConstraintId = string;
238
310
  * A rule function derives new facts or transitions from context + input facts/events.
239
311
  * Rules must be pure - no side effects.
240
312
  *
241
- * @param state Current Praxis state
242
- * @param events Events to process
243
- * @returns Array of new facts to add to the state
313
+ * Returns either:
314
+ * - `RuleResult` (new API — typed, traceable, supports retraction)
315
+ * - `PraxisFact[]` (legacy backward compatible, will be deprecated)
316
+ *
317
+ * The state parameter includes `events` — the current batch being processed.
318
+ *
319
+ * @param state Current Praxis state (includes state.events for current batch)
320
+ * @param events Events to process (same as state.events, provided for convenience)
321
+ * @returns RuleResult or array of new facts
244
322
  */
245
323
  type RuleFn<TContext = unknown> = (state: PraxisState & {
246
324
  context: TContext;
247
- }, events: PraxisEvent[]) => PraxisFact[];
325
+ events: PraxisEvent[];
326
+ }, events: PraxisEvent[]) => RuleResult | PraxisFact[];
248
327
  /**
249
328
  * A constraint function checks that an invariant holds.
250
329
  * Constraints must be pure - no side effects.
@@ -265,6 +344,18 @@ interface RuleDescriptor<TContext = unknown> {
265
344
  description: string;
266
345
  /** Implementation function */
267
346
  impl: RuleFn<TContext>;
347
+ /**
348
+ * Optional event type filter — only evaluate this rule when at least one
349
+ * event in the batch has a matching `tag`. When omitted, the rule runs on
350
+ * every step (catch-all).
351
+ *
352
+ * Accepts a single tag string or an array of tags.
353
+ *
354
+ * @example
355
+ * { id: 'sprint-behind', eventTypes: ['sprint.update'], impl: ... }
356
+ * { id: 'note-check', eventTypes: 'note.update', impl: ... }
357
+ */
358
+ eventTypes?: string | string[];
268
359
  /** Optional contract for rule behavior */
269
360
  contract?: Contract;
270
361
  /** Optional metadata */
@@ -393,6 +484,20 @@ interface PraxisEngineOptions<TContext = unknown> {
393
484
  initialFacts?: PraxisFact[];
394
485
  /** Initial metadata (optional) */
395
486
  initialMeta?: Record<string, unknown>;
487
+ /**
488
+ * Fact deduplication strategy (default: 'last-write-wins').
489
+ *
490
+ * - 'none': facts accumulate without dedup (original behavior)
491
+ * - 'last-write-wins': only keep the latest fact per tag (most common)
492
+ * - 'append': keep all facts but cap at maxFacts
493
+ */
494
+ factDedup?: 'none' | 'last-write-wins' | 'append';
495
+ /**
496
+ * Maximum number of facts to retain (default: 1000).
497
+ * When exceeded, oldest facts are evicted (FIFO).
498
+ * Set to 0 for unlimited (not recommended).
499
+ */
500
+ maxFacts?: number;
396
501
  }
397
502
  /**
398
503
  * The Praxis Logic Engine
@@ -403,6 +508,8 @@ interface PraxisEngineOptions<TContext = unknown> {
403
508
  declare class LogicEngine<TContext = unknown> {
404
509
  private state;
405
510
  private readonly registry;
511
+ private readonly factDedup;
512
+ private readonly maxFacts;
406
513
  constructor(options: PraxisEngineOptions<TContext>);
407
514
  /**
408
515
  * Get the current state (immutable copy)
@@ -441,6 +548,23 @@ declare class LogicEngine<TContext = unknown> {
441
548
  * @param updater Function that produces new context from old context
442
549
  */
443
550
  updateContext(updater: (context: TContext) => TContext): void;
551
+ /**
552
+ * Atomically update context AND process events in a single call.
553
+ *
554
+ * This avoids the fragile pattern of calling updateContext() then step()
555
+ * separately, where rules could see stale context if the ordering is wrong.
556
+ *
557
+ * @param updater Function that produces new context from old context
558
+ * @param events Events to process after context is updated
559
+ * @returns Result with new state and diagnostics
560
+ *
561
+ * @example
562
+ * engine.stepWithContext(
563
+ * ctx => ({ ...ctx, sprintName: sprint.name, items: sprint.items }),
564
+ * [{ tag: 'sprint.update', payload: { name: sprint.name } }]
565
+ * );
566
+ */
567
+ stepWithContext(updater: (context: TContext) => TContext, events: PraxisEvent[]): PraxisStepResult;
444
568
  /**
445
569
  * Add facts directly (for exceptional cases).
446
570
  * Generally, facts should be added through rules.
@@ -448,6 +572,16 @@ declare class LogicEngine<TContext = unknown> {
448
572
  * @param facts Facts to add
449
573
  */
450
574
  addFacts(facts: PraxisFact[]): void;
575
+ /**
576
+ * Check all constraints without processing any events.
577
+ *
578
+ * Useful for validation-only scenarios (e.g., form validation,
579
+ * pre-save checks) where you want constraint diagnostics without
580
+ * triggering any rules.
581
+ *
582
+ * @returns Array of constraint violation diagnostics (empty = all passing)
583
+ */
584
+ checkConstraints(): PraxisDiagnostics[];
451
585
  /**
452
586
  * Clear all facts
453
587
  */
@@ -551,4 +685,4 @@ declare class ReactiveLogicEngine<TContext extends object> {
551
685
  */
552
686
  declare function createReactiveEngine<TContext extends object>(options: ReactiveEngineOptions<TContext>): ReactiveLogicEngine<TContext>;
553
687
 
554
- export { type ConstraintDescriptor as C, LogicEngine as L, type PraxisState as P, type RuleDescriptor as R, type PraxisEvent as a, PraxisRegistry as b, type PraxisFact as c, type RuleFn as d, type Contract as e, type ConstraintFn as f, type PraxisModule as g, type PraxisDiagnostics as h, type PraxisStepConfig as i, type PraxisStepResult as j, type PraxisStepFn as k, PRAXIS_PROTOCOL_VERSION as l, type RuleId as m, type ConstraintId as n, type PraxisEngineOptions as o, createPraxisEngine as p, type ReactiveEngineOptions as q, ReactiveLogicEngine as r, createReactiveEngine as s };
688
+ export { type ConstraintDescriptor as C, LogicEngine as L, type PraxisState as P, type RuleDescriptor as R, type PraxisEvent as a, PraxisRegistry as b, type ConstraintFn as c, type Contract as d, type RuleFn as e, type PraxisFact as f, type PraxisModule as g, type ConstraintId as h, PRAXIS_PROTOCOL_VERSION as i, type PraxisDiagnostics as j, type PraxisEngineOptions as k, type PraxisStepConfig as l, type PraxisStepFn as m, type PraxisStepResult as n, type ReactiveEngineOptions as o, ReactiveLogicEngine as p, type RuleId as q, createPraxisEngine as r, createReactiveEngine as s };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-R2PSBPKQ.js";
4
4
  import {
5
5
  createPraxisEngine
6
- } from "./chunk-VOMLVI6V.js";
6
+ } from "./chunk-KMJWAFZV.js";
7
7
 
8
8
  // src/core/reactive-engine.svelte.ts
9
9
  import * as $ from "svelte/internal/client";