@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.
- package/dist/browser/{chunk-BBP2F7TT.js → chunk-MJK3IYTJ.js} +123 -5
- package/dist/browser/{chunk-FCEH7WMH.js → chunk-N63K4KWS.js} +1 -1
- package/dist/browser/{engine-65QDGCAN.js → engine-YIEGSX7U.js} +1 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.js +10 -5
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -2
- package/dist/browser/{reactive-engine.svelte-Cqd8Mod2.d.ts → reactive-engine.svelte-DjynI82A.d.ts} +83 -4
- package/dist/node/{chunk-32YFEEML.js → chunk-5JQJZADT.js} +1 -1
- package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
- package/dist/node/cloud/index.d.cts +1 -1
- package/dist/node/cloud/index.d.ts +1 -1
- package/dist/node/{engine-7CXQV6RC.js → engine-FEN5IYZ5.js} +1 -1
- package/dist/node/index.cjs +522 -59
- package/dist/node/index.d.cts +271 -5
- package/dist/node/index.d.ts +271 -5
- package/dist/node/index.js +355 -39
- package/dist/node/integrations/svelte.cjs +123 -5
- package/dist/node/integrations/svelte.d.cts +3 -3
- package/dist/node/integrations/svelte.d.ts +3 -3
- package/dist/node/integrations/svelte.js +2 -2
- package/dist/node/{protocol-BocKczNv.d.ts → protocol-DcyGMmWY.d.cts} +7 -0
- package/dist/node/{protocol-BocKczNv.d.cts → protocol-DcyGMmWY.d.ts} +7 -0
- package/dist/node/{reactive-engine.svelte-D-xTDxT5.d.ts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
- package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
- package/package.json +2 -2
- package/src/__tests__/engine-v2.test.ts +532 -0
- package/src/core/completeness.ts +274 -0
- package/src/core/engine.ts +47 -5
- package/src/core/pluresdb/store.ts +9 -3
- package/src/core/protocol.ts +7 -0
- package/src/core/rule-result.ts +130 -0
- package/src/core/rules.ts +12 -5
- package/src/core/ui-rules.ts +340 -0
- package/src/index.ts +27 -0
- 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
|
|
103
|
-
|
|
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
|
|
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 = [...
|
|
240
|
+
mergedFacts = [...existingFacts, ...newFacts];
|
|
123
241
|
break;
|
|
124
242
|
case "none":
|
|
125
243
|
default:
|
|
126
|
-
mergedFacts = [...
|
|
244
|
+
mergedFacts = [...existingFacts, ...newFacts];
|
|
127
245
|
break;
|
|
128
246
|
}
|
|
129
247
|
if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
/**
|
package/dist/browser/index.js
CHANGED
|
@@ -2,12 +2,12 @@ import {
|
|
|
2
2
|
PraxisRegistry,
|
|
3
3
|
ReactiveLogicEngine,
|
|
4
4
|
createReactiveEngine
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-N63K4KWS.js";
|
|
6
6
|
import {
|
|
7
7
|
LogicEngine,
|
|
8
8
|
PRAXIS_PROTOCOL_VERSION,
|
|
9
9
|
createPraxisEngine
|
|
10
|
-
} from "./chunk-
|
|
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
|
|
1286
|
-
|
|
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-
|
|
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-
|
|
2
|
-
export { o as ReactiveEngineOptions, p as ReactiveLogicEngine, s as createReactiveEngine } from '../reactive-engine.svelte-
|
|
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
|
package/dist/browser/{reactive-engine.svelte-Cqd8Mod2.d.ts → reactive-engine.svelte-DjynI82A.d.ts}
RENAMED
|
@@ -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
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|
-
|
|
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.
|
|
@@ -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
|
|
103
|
-
|
|
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
|
|
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 = [...
|
|
243
|
+
mergedFacts = [...existingFacts, ...newFacts];
|
|
123
244
|
break;
|
|
124
245
|
case "none":
|
|
125
246
|
default:
|
|
126
|
-
mergedFacts = [...
|
|
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
|
};
|