@plures/praxis 1.2.41 → 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 (36) hide show
  1. package/dist/browser/{chunk-BBP2F7TT.js → chunk-MJK3IYTJ.js} +123 -5
  2. package/dist/browser/{chunk-FCEH7WMH.js → chunk-N63K4KWS.js} +1 -1
  3. package/dist/browser/{engine-65QDGCAN.js → engine-YIEGSX7U.js} +1 -1
  4. package/dist/browser/index.d.ts +2 -2
  5. package/dist/browser/index.js +10 -5
  6. package/dist/browser/integrations/svelte.d.ts +2 -2
  7. package/dist/browser/integrations/svelte.js +2 -2
  8. package/dist/browser/{reactive-engine.svelte-Cqd8Mod2.d.ts → reactive-engine.svelte-DjynI82A.d.ts} +83 -4
  9. package/dist/node/{chunk-32YFEEML.js → chunk-5JQJZADT.js} +1 -1
  10. package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
  11. package/dist/node/cloud/index.d.cts +1 -1
  12. package/dist/node/cloud/index.d.ts +1 -1
  13. package/dist/node/{engine-7CXQV6RC.js → engine-FEN5IYZ5.js} +1 -1
  14. package/dist/node/index.cjs +522 -59
  15. package/dist/node/index.d.cts +271 -5
  16. package/dist/node/index.d.ts +271 -5
  17. package/dist/node/index.js +355 -39
  18. package/dist/node/integrations/svelte.cjs +123 -5
  19. package/dist/node/integrations/svelte.d.cts +3 -3
  20. package/dist/node/integrations/svelte.d.ts +3 -3
  21. package/dist/node/integrations/svelte.js +2 -2
  22. package/dist/node/{protocol-BocKczNv.d.ts → protocol-DcyGMmWY.d.cts} +7 -0
  23. package/dist/node/{protocol-BocKczNv.d.cts → protocol-DcyGMmWY.d.ts} +7 -0
  24. package/dist/node/{reactive-engine.svelte-D-xTDxT5.d.ts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
  25. package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
  26. package/package.json +2 -2
  27. package/src/__tests__/engine-v2.test.ts +532 -0
  28. package/src/core/completeness.ts +274 -0
  29. package/src/core/engine.ts +47 -5
  30. package/src/core/pluresdb/store.ts +9 -3
  31. package/src/core/protocol.ts +7 -0
  32. package/src/core/rule-result.ts +130 -0
  33. package/src/core/rules.ts +12 -5
  34. package/src/core/ui-rules.ts +340 -0
  35. package/src/index.ts +27 -0
  36. package/src/vite/completeness-plugin.ts +72 -0
@@ -1,6 +1,90 @@
1
1
  // src/core/protocol.ts
2
2
  var PRAXIS_PROTOCOL_VERSION = "1.0.0";
3
3
 
4
+ // src/core/rule-result.ts
5
+ var RuleResult = class _RuleResult {
6
+ /** The kind of result */
7
+ kind;
8
+ /** Facts produced (only for 'emit') */
9
+ facts;
10
+ /** Fact tags to retract (only for 'retract') */
11
+ retractTags;
12
+ /** Optional reason (for noop/skip/retract — useful for debugging) */
13
+ reason;
14
+ /** The rule ID that produced this result (set by engine) */
15
+ ruleId;
16
+ constructor(kind, facts, retractTags, reason) {
17
+ this.kind = kind;
18
+ this.facts = facts;
19
+ this.retractTags = retractTags;
20
+ this.reason = reason;
21
+ }
22
+ /**
23
+ * Rule produced facts.
24
+ *
25
+ * @example
26
+ * return RuleResult.emit([
27
+ * { tag: 'sprint.behind', payload: { deficit: 5 } }
28
+ * ]);
29
+ */
30
+ static emit(facts) {
31
+ if (facts.length === 0) {
32
+ throw new Error(
33
+ "RuleResult.emit() requires at least one fact. Use RuleResult.noop() or RuleResult.skip() when a rule has nothing to say."
34
+ );
35
+ }
36
+ return new _RuleResult("emit", facts, []);
37
+ }
38
+ /**
39
+ * Rule evaluated but had nothing to report.
40
+ * Unlike returning [], this is explicit and traceable.
41
+ *
42
+ * @example
43
+ * if (ctx.completedHours >= expectedHours) {
44
+ * return RuleResult.noop('Sprint is on pace');
45
+ * }
46
+ */
47
+ static noop(reason) {
48
+ return new _RuleResult("noop", [], [], reason);
49
+ }
50
+ /**
51
+ * Rule decided to skip because preconditions were not met.
52
+ * Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
53
+ *
54
+ * @example
55
+ * if (!ctx.sprintName) {
56
+ * return RuleResult.skip('No active sprint');
57
+ * }
58
+ */
59
+ static skip(reason) {
60
+ return new _RuleResult("skip", [], [], reason);
61
+ }
62
+ /**
63
+ * Rule retracts previously emitted facts by tag.
64
+ * Used when a condition that previously produced facts is no longer true.
65
+ *
66
+ * @example
67
+ * // Sprint was behind, but caught up
68
+ * if (ctx.completedHours >= expectedHours) {
69
+ * return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
70
+ * }
71
+ */
72
+ static retract(tags, reason) {
73
+ if (tags.length === 0) {
74
+ throw new Error("RuleResult.retract() requires at least one tag.");
75
+ }
76
+ return new _RuleResult("retract", [], tags, reason);
77
+ }
78
+ /** Whether this result produced facts */
79
+ get hasFacts() {
80
+ return this.facts.length > 0;
81
+ }
82
+ /** Whether this result retracts facts */
83
+ get hasRetractions() {
84
+ return this.retractTags.length > 0;
85
+ }
86
+ };
87
+
4
88
  // src/core/engine.ts
5
89
  function safeClone(value) {
6
90
  if (value === null || typeof value !== "object") {
@@ -80,7 +164,13 @@ var LogicEngine = class {
80
164
  stepWithConfig(events, config) {
81
165
  const diagnostics = [];
82
166
  let newState = { ...this.state };
167
+ const stateWithEvents = {
168
+ ...newState,
169
+ events
170
+ // current batch — rules can read state.events
171
+ };
83
172
  const newFacts = [];
173
+ const retractions = [];
84
174
  const eventTags = new Set(events.map((e) => e.tag));
85
175
  for (const ruleId of config.ruleIds) {
86
176
  const rule = this.registry.getRule(ruleId);
@@ -99,8 +189,31 @@ var LogicEngine = class {
99
189
  }
100
190
  }
101
191
  try {
102
- const ruleFacts = rule.impl(newState, events);
103
- newFacts.push(...ruleFacts);
192
+ const rawResult = rule.impl(stateWithEvents, events);
193
+ if (rawResult instanceof RuleResult) {
194
+ rawResult.ruleId = ruleId;
195
+ switch (rawResult.kind) {
196
+ case "emit":
197
+ newFacts.push(...rawResult.facts);
198
+ break;
199
+ case "retract":
200
+ retractions.push(...rawResult.retractTags);
201
+ break;
202
+ case "noop":
203
+ case "skip":
204
+ if (rawResult.reason) {
205
+ diagnostics.push({
206
+ kind: "rule-error",
207
+ // reused kind — could add 'rule-trace' in protocol v2
208
+ message: `[${rawResult.kind}] ${ruleId}: ${rawResult.reason}`,
209
+ data: { ruleId, resultKind: rawResult.kind, reason: rawResult.reason }
210
+ });
211
+ }
212
+ break;
213
+ }
214
+ } else if (Array.isArray(rawResult)) {
215
+ newFacts.push(...rawResult);
216
+ }
104
217
  } catch (error) {
105
218
  diagnostics.push({
106
219
  kind: "rule-error",
@@ -109,21 +222,26 @@ var LogicEngine = class {
109
222
  });
110
223
  }
111
224
  }
225
+ let existingFacts = newState.facts;
226
+ if (retractions.length > 0) {
227
+ const retractSet = new Set(retractions);
228
+ existingFacts = existingFacts.filter((f) => !retractSet.has(f.tag));
229
+ }
112
230
  let mergedFacts;
113
231
  switch (this.factDedup) {
114
232
  case "last-write-wins": {
115
233
  const factMap = /* @__PURE__ */ new Map();
116
- for (const f of newState.facts) factMap.set(f.tag, f);
234
+ for (const f of existingFacts) factMap.set(f.tag, f);
117
235
  for (const f of newFacts) factMap.set(f.tag, f);
118
236
  mergedFacts = Array.from(factMap.values());
119
237
  break;
120
238
  }
121
239
  case "append":
122
- mergedFacts = [...newState.facts, ...newFacts];
240
+ mergedFacts = [...existingFacts, ...newFacts];
123
241
  break;
124
242
  case "none":
125
243
  default:
126
- mergedFacts = [...newState.facts, ...newFacts];
244
+ mergedFacts = [...existingFacts, ...newFacts];
127
245
  break;
128
246
  }
129
247
  if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createPraxisEngine
3
- } from "./chunk-BBP2F7TT.js";
3
+ } from "./chunk-MJK3IYTJ.js";
4
4
 
5
5
  // src/core/rules.ts
6
6
  var PraxisRegistry = class {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LogicEngine,
3
3
  createPraxisEngine
4
- } from "./chunk-BBP2F7TT.js";
4
+ } from "./chunk-MJK3IYTJ.js";
5
5
  export {
6
6
  LogicEngine,
7
7
  createPraxisEngine
@@ -1,5 +1,5 @@
1
- import { L as LogicEngine, P as PraxisState, a as PraxisEvent, b as PraxisRegistry, R as RuleDescriptor, C as ConstraintDescriptor, c as ConstraintFn, d as Contract, e as RuleFn, f as PraxisFact, g as PraxisModule } from './reactive-engine.svelte-Cqd8Mod2.js';
2
- export { h as ConstraintId, i as PRAXIS_PROTOCOL_VERSION, j as PraxisDiagnostics, k as PraxisEngineOptions, l as PraxisStepConfig, m as PraxisStepFn, n as PraxisStepResult, o as ReactiveEngineOptions, p as ReactiveLogicEngine, q as RuleId, r as createPraxisEngine, s as createReactiveEngine } from './reactive-engine.svelte-Cqd8Mod2.js';
1
+ import { L as LogicEngine, P as PraxisState, a as PraxisEvent, b as PraxisRegistry, R as RuleDescriptor, C as ConstraintDescriptor, c as ConstraintFn, d as Contract, e as RuleFn, f as PraxisFact, g as PraxisModule } from './reactive-engine.svelte-DjynI82A.js';
2
+ export { h as ConstraintId, i as PRAXIS_PROTOCOL_VERSION, j as PraxisDiagnostics, k as PraxisEngineOptions, l as PraxisStepConfig, m as PraxisStepFn, n as PraxisStepResult, o as ReactiveEngineOptions, p as ReactiveLogicEngine, q as RuleId, r as createPraxisEngine, s as createReactiveEngine } from './reactive-engine.svelte-DjynI82A.js';
3
3
  import { LocalFirstOptions } from '@plures/pluresdb/local-first';
4
4
 
5
5
  /**
@@ -2,12 +2,12 @@ import {
2
2
  PraxisRegistry,
3
3
  ReactiveLogicEngine,
4
4
  createReactiveEngine
5
- } from "./chunk-FCEH7WMH.js";
5
+ } from "./chunk-N63K4KWS.js";
6
6
  import {
7
7
  LogicEngine,
8
8
  PRAXIS_PROTOCOL_VERSION,
9
9
  createPraxisEngine
10
- } from "./chunk-BBP2F7TT.js";
10
+ } from "./chunk-MJK3IYTJ.js";
11
11
  import {
12
12
  InMemoryPraxisDB,
13
13
  PluresDBPraxisAdapter,
@@ -1277,13 +1277,18 @@ var PraxisDBStore = class {
1277
1277
  const state = {
1278
1278
  context: this.context,
1279
1279
  facts: [],
1280
+ events,
1280
1281
  meta: {}
1281
1282
  };
1282
1283
  const derivedFacts = [];
1283
1284
  for (const rule of rules) {
1284
1285
  try {
1285
- const facts = rule.impl(state, events);
1286
- 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
+ }
1287
1292
  } catch (error) {
1288
1293
  this.onRuleError(rule.id, error);
1289
1294
  }
@@ -2889,7 +2894,7 @@ function generateTauriConfig(config) {
2889
2894
 
2890
2895
  // src/integrations/unified.ts
2891
2896
  async function createUnifiedApp(config) {
2892
- const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-65QDGCAN.js");
2897
+ const { createPraxisEngine: createPraxisEngine2 } = await import("./engine-YIEGSX7U.js");
2893
2898
  const { createInMemoryDB: createInMemoryDB2 } = await import("./adapter-CIMBGDC7.js");
2894
2899
  const db = config.db || createInMemoryDB2();
2895
2900
  const pluresdb = createPluresDBAdapter({
@@ -1,5 +1,5 @@
1
- import { P as PraxisState, a as PraxisEvent, L as LogicEngine } from '../reactive-engine.svelte-Cqd8Mod2.js';
2
- export { o as ReactiveEngineOptions, p as ReactiveLogicEngine, s as createReactiveEngine } from '../reactive-engine.svelte-Cqd8Mod2.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-FCEH7WMH.js";
5
- import "../chunk-BBP2F7TT.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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-R2PSBPKQ.js";
4
4
  import {
5
5
  createPraxisEngine
6
- } from "./chunk-BBP2F7TT.js";
6
+ } from "./chunk-KMJWAFZV.js";
7
7
 
8
8
  // src/core/reactive-engine.svelte.ts
9
9
  import * as $ from "svelte/internal/client";
@@ -1,6 +1,93 @@
1
1
  // src/core/protocol.ts
2
2
  var PRAXIS_PROTOCOL_VERSION = "1.0.0";
3
3
 
4
+ // src/core/rule-result.ts
5
+ var RuleResult = class _RuleResult {
6
+ /** The kind of result */
7
+ kind;
8
+ /** Facts produced (only for 'emit') */
9
+ facts;
10
+ /** Fact tags to retract (only for 'retract') */
11
+ retractTags;
12
+ /** Optional reason (for noop/skip/retract — useful for debugging) */
13
+ reason;
14
+ /** The rule ID that produced this result (set by engine) */
15
+ ruleId;
16
+ constructor(kind, facts, retractTags, reason) {
17
+ this.kind = kind;
18
+ this.facts = facts;
19
+ this.retractTags = retractTags;
20
+ this.reason = reason;
21
+ }
22
+ /**
23
+ * Rule produced facts.
24
+ *
25
+ * @example
26
+ * return RuleResult.emit([
27
+ * { tag: 'sprint.behind', payload: { deficit: 5 } }
28
+ * ]);
29
+ */
30
+ static emit(facts) {
31
+ if (facts.length === 0) {
32
+ throw new Error(
33
+ "RuleResult.emit() requires at least one fact. Use RuleResult.noop() or RuleResult.skip() when a rule has nothing to say."
34
+ );
35
+ }
36
+ return new _RuleResult("emit", facts, []);
37
+ }
38
+ /**
39
+ * Rule evaluated but had nothing to report.
40
+ * Unlike returning [], this is explicit and traceable.
41
+ *
42
+ * @example
43
+ * if (ctx.completedHours >= expectedHours) {
44
+ * return RuleResult.noop('Sprint is on pace');
45
+ * }
46
+ */
47
+ static noop(reason) {
48
+ return new _RuleResult("noop", [], [], reason);
49
+ }
50
+ /**
51
+ * Rule decided to skip because preconditions were not met.
52
+ * Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
53
+ *
54
+ * @example
55
+ * if (!ctx.sprintName) {
56
+ * return RuleResult.skip('No active sprint');
57
+ * }
58
+ */
59
+ static skip(reason) {
60
+ return new _RuleResult("skip", [], [], reason);
61
+ }
62
+ /**
63
+ * Rule retracts previously emitted facts by tag.
64
+ * Used when a condition that previously produced facts is no longer true.
65
+ *
66
+ * @example
67
+ * // Sprint was behind, but caught up
68
+ * if (ctx.completedHours >= expectedHours) {
69
+ * return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
70
+ * }
71
+ */
72
+ static retract(tags, reason) {
73
+ if (tags.length === 0) {
74
+ throw new Error("RuleResult.retract() requires at least one tag.");
75
+ }
76
+ return new _RuleResult("retract", [], tags, reason);
77
+ }
78
+ /** Whether this result produced facts */
79
+ get hasFacts() {
80
+ return this.facts.length > 0;
81
+ }
82
+ /** Whether this result retracts facts */
83
+ get hasRetractions() {
84
+ return this.retractTags.length > 0;
85
+ }
86
+ };
87
+ function fact(tag, payload) {
88
+ return { tag, payload };
89
+ }
90
+
4
91
  // src/core/engine.ts
5
92
  function safeClone(value) {
6
93
  if (value === null || typeof value !== "object") {
@@ -80,7 +167,13 @@ var LogicEngine = class {
80
167
  stepWithConfig(events, config) {
81
168
  const diagnostics = [];
82
169
  let newState = { ...this.state };
170
+ const stateWithEvents = {
171
+ ...newState,
172
+ events
173
+ // current batch — rules can read state.events
174
+ };
83
175
  const newFacts = [];
176
+ const retractions = [];
84
177
  const eventTags = new Set(events.map((e) => e.tag));
85
178
  for (const ruleId of config.ruleIds) {
86
179
  const rule = this.registry.getRule(ruleId);
@@ -99,8 +192,31 @@ var LogicEngine = class {
99
192
  }
100
193
  }
101
194
  try {
102
- const ruleFacts = rule.impl(newState, events);
103
- newFacts.push(...ruleFacts);
195
+ const rawResult = rule.impl(stateWithEvents, events);
196
+ if (rawResult instanceof RuleResult) {
197
+ rawResult.ruleId = ruleId;
198
+ switch (rawResult.kind) {
199
+ case "emit":
200
+ newFacts.push(...rawResult.facts);
201
+ break;
202
+ case "retract":
203
+ retractions.push(...rawResult.retractTags);
204
+ break;
205
+ case "noop":
206
+ case "skip":
207
+ if (rawResult.reason) {
208
+ diagnostics.push({
209
+ kind: "rule-error",
210
+ // reused kind — could add 'rule-trace' in protocol v2
211
+ message: `[${rawResult.kind}] ${ruleId}: ${rawResult.reason}`,
212
+ data: { ruleId, resultKind: rawResult.kind, reason: rawResult.reason }
213
+ });
214
+ }
215
+ break;
216
+ }
217
+ } else if (Array.isArray(rawResult)) {
218
+ newFacts.push(...rawResult);
219
+ }
104
220
  } catch (error) {
105
221
  diagnostics.push({
106
222
  kind: "rule-error",
@@ -109,21 +225,26 @@ var LogicEngine = class {
109
225
  });
110
226
  }
111
227
  }
228
+ let existingFacts = newState.facts;
229
+ if (retractions.length > 0) {
230
+ const retractSet = new Set(retractions);
231
+ existingFacts = existingFacts.filter((f) => !retractSet.has(f.tag));
232
+ }
112
233
  let mergedFacts;
113
234
  switch (this.factDedup) {
114
235
  case "last-write-wins": {
115
236
  const factMap = /* @__PURE__ */ new Map();
116
- for (const f of newState.facts) factMap.set(f.tag, f);
237
+ for (const f of existingFacts) factMap.set(f.tag, f);
117
238
  for (const f of newFacts) factMap.set(f.tag, f);
118
239
  mergedFacts = Array.from(factMap.values());
119
240
  break;
120
241
  }
121
242
  case "append":
122
- mergedFacts = [...newState.facts, ...newFacts];
243
+ mergedFacts = [...existingFacts, ...newFacts];
123
244
  break;
124
245
  case "none":
125
246
  default:
126
- mergedFacts = [...newState.facts, ...newFacts];
247
+ mergedFacts = [...existingFacts, ...newFacts];
127
248
  break;
128
249
  }
129
250
  if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
@@ -261,6 +382,8 @@ function createPraxisEngine(options) {
261
382
 
262
383
  export {
263
384
  PRAXIS_PROTOCOL_VERSION,
385
+ RuleResult,
386
+ fact,
264
387
  LogicEngine,
265
388
  createPraxisEngine
266
389
  };
@@ -1,4 +1,4 @@
1
- import { b as PraxisFact, a as PraxisEvent } from '../protocol-BocKczNv.cjs';
1
+ import { b as PraxisFact, a as PraxisEvent } from '../protocol-DcyGMmWY.cjs';
2
2
 
3
3
  /**
4
4
  * Cloud Relay Types
@@ -1,4 +1,4 @@
1
- import { b as PraxisFact, a as PraxisEvent } from '../protocol-BocKczNv.js';
1
+ import { b as PraxisFact, a as PraxisEvent } from '../protocol-DcyGMmWY.js';
2
2
 
3
3
  /**
4
4
  * Cloud Relay Types
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  LogicEngine,
3
3
  createPraxisEngine
4
- } from "./chunk-BBP2F7TT.js";
4
+ } from "./chunk-KMJWAFZV.js";
5
5
  import "./chunk-QGM4M3NI.js";
6
6
  export {
7
7
  LogicEngine,