@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
@@ -39,6 +39,99 @@ var init_protocol = __esm({
39
39
  }
40
40
  });
41
41
 
42
+ // src/core/rule-result.ts
43
+ function fact(tag, payload) {
44
+ return { tag, payload };
45
+ }
46
+ var RuleResult;
47
+ var init_rule_result = __esm({
48
+ "src/core/rule-result.ts"() {
49
+ "use strict";
50
+ RuleResult = class _RuleResult {
51
+ /** The kind of result */
52
+ kind;
53
+ /** Facts produced (only for 'emit') */
54
+ facts;
55
+ /** Fact tags to retract (only for 'retract') */
56
+ retractTags;
57
+ /** Optional reason (for noop/skip/retract — useful for debugging) */
58
+ reason;
59
+ /** The rule ID that produced this result (set by engine) */
60
+ ruleId;
61
+ constructor(kind, facts, retractTags, reason) {
62
+ this.kind = kind;
63
+ this.facts = facts;
64
+ this.retractTags = retractTags;
65
+ this.reason = reason;
66
+ }
67
+ /**
68
+ * Rule produced facts.
69
+ *
70
+ * @example
71
+ * return RuleResult.emit([
72
+ * { tag: 'sprint.behind', payload: { deficit: 5 } }
73
+ * ]);
74
+ */
75
+ static emit(facts) {
76
+ if (facts.length === 0) {
77
+ throw new Error(
78
+ "RuleResult.emit() requires at least one fact. Use RuleResult.noop() or RuleResult.skip() when a rule has nothing to say."
79
+ );
80
+ }
81
+ return new _RuleResult("emit", facts, []);
82
+ }
83
+ /**
84
+ * Rule evaluated but had nothing to report.
85
+ * Unlike returning [], this is explicit and traceable.
86
+ *
87
+ * @example
88
+ * if (ctx.completedHours >= expectedHours) {
89
+ * return RuleResult.noop('Sprint is on pace');
90
+ * }
91
+ */
92
+ static noop(reason) {
93
+ return new _RuleResult("noop", [], [], reason);
94
+ }
95
+ /**
96
+ * Rule decided to skip because preconditions were not met.
97
+ * Distinct from noop: skip means "I can't evaluate", noop means "I evaluated and found nothing".
98
+ *
99
+ * @example
100
+ * if (!ctx.sprintName) {
101
+ * return RuleResult.skip('No active sprint');
102
+ * }
103
+ */
104
+ static skip(reason) {
105
+ return new _RuleResult("skip", [], [], reason);
106
+ }
107
+ /**
108
+ * Rule retracts previously emitted facts by tag.
109
+ * Used when a condition that previously produced facts is no longer true.
110
+ *
111
+ * @example
112
+ * // Sprint was behind, but caught up
113
+ * if (ctx.completedHours >= expectedHours) {
114
+ * return RuleResult.retract(['sprint.behind'], 'Sprint caught up');
115
+ * }
116
+ */
117
+ static retract(tags, reason) {
118
+ if (tags.length === 0) {
119
+ throw new Error("RuleResult.retract() requires at least one tag.");
120
+ }
121
+ return new _RuleResult("retract", [], tags, reason);
122
+ }
123
+ /** Whether this result produced facts */
124
+ get hasFacts() {
125
+ return this.facts.length > 0;
126
+ }
127
+ /** Whether this result retracts facts */
128
+ get hasRetractions() {
129
+ return this.retractTags.length > 0;
130
+ }
131
+ };
132
+ }
133
+ });
134
+
42
135
  // src/core/engine.ts
43
136
  var engine_exports = {};
44
137
  __export(engine_exports, {
@@ -68,6 +161,7 @@ var init_engine = __esm({
68
161
  "src/core/engine.ts"() {
69
162
  "use strict";
70
163
  init_protocol();
164
+ init_rule_result();
71
165
  LogicEngine = class {
72
166
  state;
73
167
  registry;
@@ -131,7 +225,13 @@ var init_engine = __esm({
131
225
  stepWithConfig(events, config) {
132
226
  const diagnostics = [];
133
227
  let newState = { ...this.state };
228
+ const stateWithEvents = {
229
+ ...newState,
230
+ events
231
+ // current batch — rules can read state.events
232
+ };
134
233
  const newFacts = [];
234
+ const retractions = [];
135
235
  const eventTags = new Set(events.map((e) => e.tag));
136
236
  for (const ruleId of config.ruleIds) {
137
237
  const rule = this.registry.getRule(ruleId);
@@ -150,8 +250,31 @@ var init_engine = __esm({
150
250
  }
151
251
  }
152
252
  try {
153
- const ruleFacts = rule.impl(newState, events);
154
- newFacts.push(...ruleFacts);
253
+ const rawResult = rule.impl(stateWithEvents, events);
254
+ if (rawResult instanceof RuleResult) {
255
+ rawResult.ruleId = ruleId;
256
+ switch (rawResult.kind) {
257
+ case "emit":
258
+ newFacts.push(...rawResult.facts);
259
+ break;
260
+ case "retract":
261
+ retractions.push(...rawResult.retractTags);
262
+ break;
263
+ case "noop":
264
+ case "skip":
265
+ if (rawResult.reason) {
266
+ diagnostics.push({
267
+ kind: "rule-error",
268
+ // reused kind — could add 'rule-trace' in protocol v2
269
+ message: `[${rawResult.kind}] ${ruleId}: ${rawResult.reason}`,
270
+ data: { ruleId, resultKind: rawResult.kind, reason: rawResult.reason }
271
+ });
272
+ }
273
+ break;
274
+ }
275
+ } else if (Array.isArray(rawResult)) {
276
+ newFacts.push(...rawResult);
277
+ }
155
278
  } catch (error) {
156
279
  diagnostics.push({
157
280
  kind: "rule-error",
@@ -160,21 +283,26 @@ var init_engine = __esm({
160
283
  });
161
284
  }
162
285
  }
286
+ let existingFacts = newState.facts;
287
+ if (retractions.length > 0) {
288
+ const retractSet = new Set(retractions);
289
+ existingFacts = existingFacts.filter((f) => !retractSet.has(f.tag));
290
+ }
163
291
  let mergedFacts;
164
292
  switch (this.factDedup) {
165
293
  case "last-write-wins": {
166
294
  const factMap = /* @__PURE__ */ new Map();
167
- for (const f of newState.facts) factMap.set(f.tag, f);
295
+ for (const f of existingFacts) factMap.set(f.tag, f);
168
296
  for (const f of newFacts) factMap.set(f.tag, f);
169
297
  mergedFacts = Array.from(factMap.values());
170
298
  break;
171
299
  }
172
300
  case "append":
173
- mergedFacts = [...newState.facts, ...newFacts];
301
+ mergedFacts = [...existingFacts, ...newFacts];
174
302
  break;
175
303
  case "none":
176
304
  default:
177
- mergedFacts = [...newState.facts, ...newFacts];
305
+ mergedFacts = [...existingFacts, ...newFacts];
178
306
  break;
179
307
  }
180
308
  if (this.maxFacts > 0 && mergedFacts.length > this.maxFacts) {
@@ -494,6 +622,7 @@ __export(src_exports, {
494
622
  PraxisSchemaRegistry: () => PraxisSchemaRegistry,
495
623
  ReactiveLogicEngine: () => ReactiveLogicEngine,
496
624
  RegistryIntrospector: () => RegistryIntrospector,
625
+ RuleResult: () => RuleResult,
497
626
  StateDocsGenerator: () => StateDocsGenerator,
498
627
  TerminalAdapter: () => TerminalAdapter,
499
628
  ValidateContracts: () => ValidateContracts,
@@ -501,6 +630,7 @@ __export(src_exports, {
501
630
  attachTauriToEngine: () => attachTauriToEngine,
502
631
  attachToEngine: () => attachToEngine,
503
632
  attachUnumToEngine: () => attachUnumToEngine,
633
+ auditCompleteness: () => auditCompleteness,
504
634
  canvasToMermaid: () => canvasToMermaid,
505
635
  canvasToSchema: () => canvasToSchema,
506
636
  canvasToYaml: () => canvasToYaml,
@@ -526,6 +656,7 @@ __export(src_exports, {
526
656
  createTauriPraxisAdapter: () => createTauriPraxisAdapter,
527
657
  createTerminalAdapter: () => createTerminalAdapter,
528
658
  createTimerActor: () => createTimerActor,
659
+ createUIModule: () => createUIModule,
529
660
  createUnifiedApp: () => createUnifiedApp,
530
661
  createUnumAdapter: () => createUnumAdapter,
531
662
  defineConstraint: () => defineConstraint,
@@ -534,10 +665,14 @@ __export(src_exports, {
534
665
  defineFact: () => defineFact,
535
666
  defineModule: () => defineModule,
536
667
  defineRule: () => defineRule,
668
+ dirtyGuardRule: () => dirtyGuardRule,
669
+ errorDisplayRule: () => errorDisplayRule,
670
+ fact: () => fact,
537
671
  filterEvents: () => filterEvents,
538
672
  filterFacts: () => filterFacts,
539
673
  findEvent: () => findEvent,
540
674
  findFact: () => findFact,
675
+ formatReport: () => formatReport,
541
676
  formatValidationReport: () => formatValidationReport,
542
677
  formatValidationReportJSON: () => formatValidationReportJSON,
543
678
  formatValidationReportSARIF: () => formatValidationReportSARIF,
@@ -548,18 +683,28 @@ __export(src_exports, {
548
683
  getEventPath: () => getEventPath,
549
684
  getFactPath: () => getFactPath,
550
685
  getSchemaPath: () => getSchemaPath,
686
+ initGateRule: () => initGateRule,
551
687
  isContract: () => isContract,
552
688
  loadSchema: () => loadSchema,
553
689
  loadSchemaFromFile: () => loadSchemaFromFile,
554
690
  loadSchemaFromJson: () => loadSchemaFromJson,
555
691
  loadSchemaFromYaml: () => loadSchemaFromYaml,
692
+ loadingGateRule: () => loadingGateRule,
693
+ mustBeInitializedConstraint: () => mustBeInitializedConstraint,
694
+ navigationRequest: () => navigationRequest,
695
+ noInteractionWhileLoadingConstraint: () => noInteractionWhileLoadingConstraint,
696
+ offlineIndicatorRule: () => offlineIndicatorRule,
556
697
  registerSchema: () => registerSchema,
698
+ resizeEvent: () => resizeEvent,
557
699
  runTerminalCommand: () => runTerminalCommand,
558
700
  schemaToCanvas: () => schemaToCanvas,
701
+ uiModule: () => uiModule,
702
+ uiStateChanged: () => uiStateChanged,
559
703
  validateContracts: () => validateContracts,
560
704
  validateForGeneration: () => validateForGeneration,
561
705
  validateSchema: () => validateSchema,
562
- validateWithGuardian: () => validateWithGuardian
706
+ validateWithGuardian: () => validateWithGuardian,
707
+ viewportRule: () => viewportRule
563
708
  });
564
709
  module.exports = __toCommonJS(src_exports);
565
710
  init_protocol();
@@ -1336,8 +1481,8 @@ function defineFact(tag) {
1336
1481
  create(payload) {
1337
1482
  return { tag, payload };
1338
1483
  },
1339
- is(fact) {
1340
- return fact.tag === tag;
1484
+ is(fact2) {
1485
+ return fact2.tag === tag;
1341
1486
  }
1342
1487
  };
1343
1488
  }
@@ -2090,16 +2235,16 @@ function validateSchema(schema) {
2090
2235
  if (schema.logic) {
2091
2236
  schema.logic.forEach((logic, logicIndex) => {
2092
2237
  if (logic.facts) {
2093
- logic.facts.forEach((fact, factIndex) => {
2094
- if (!fact.tag) {
2238
+ logic.facts.forEach((fact2, factIndex) => {
2239
+ if (!fact2.tag) {
2095
2240
  errors.push({
2096
2241
  path: `logic[${logicIndex}].facts[${factIndex}].tag`,
2097
2242
  message: "Fact tag is required"
2098
2243
  });
2099
- } else if (!isValidIdentifier(fact.tag)) {
2244
+ } else if (!isValidIdentifier(fact2.tag)) {
2100
2245
  errors.push({
2101
2246
  path: `logic[${logicIndex}].facts[${factIndex}].tag`,
2102
- message: `Fact tag "${fact.tag}" is not a valid JavaScript identifier. Use only letters, numbers, underscores, and dollar signs, and do not start with a number.`
2247
+ message: `Fact tag "${fact2.tag}" is not a valid JavaScript identifier. Use only letters, numbers, underscores, and dollar signs, and do not start with a number.`
2103
2248
  });
2104
2249
  }
2105
2250
  });
@@ -2484,37 +2629,37 @@ var PraxisDBStore = class {
2484
2629
  * @param fact The fact to store
2485
2630
  * @returns Promise that resolves when the fact is stored
2486
2631
  */
2487
- async storeFact(fact) {
2488
- const constraintResult = await this.checkConstraints([fact]);
2632
+ async storeFact(fact2) {
2633
+ const constraintResult = await this.checkConstraints([fact2]);
2489
2634
  if (!constraintResult.valid) {
2490
2635
  throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
2491
2636
  }
2492
2637
  let before;
2493
2638
  if (this.chronicle) {
2494
- const payload = fact.payload;
2639
+ const payload = fact2.payload;
2495
2640
  const id = payload?.id;
2496
2641
  if (id) {
2497
- before = await this.getFact(fact.tag, id);
2642
+ before = await this.getFact(fact2.tag, id);
2498
2643
  }
2499
2644
  }
2500
- await this.persistFact(fact);
2645
+ await this.persistFact(fact2);
2501
2646
  if (this.chronicle) {
2502
- const payload = fact.payload;
2647
+ const payload = fact2.payload;
2503
2648
  const id = payload?.id ?? "";
2504
2649
  const span = ChronicleContext.current;
2505
2650
  try {
2506
2651
  await this.chronicle.record({
2507
- path: getFactPath(fact.tag, id),
2652
+ path: getFactPath(fact2.tag, id),
2508
2653
  before,
2509
- after: fact,
2654
+ after: fact2,
2510
2655
  cause: span?.spanId,
2511
2656
  context: span?.contextId,
2512
- metadata: { factTag: fact.tag, operation: "storeFact" }
2657
+ metadata: { factTag: fact2.tag, operation: "storeFact" }
2513
2658
  });
2514
2659
  } catch {
2515
2660
  }
2516
2661
  }
2517
- await this.triggerRules([fact]);
2662
+ await this.triggerRules([fact2]);
2518
2663
  }
2519
2664
  /**
2520
2665
  * Store multiple facts in PluresDB
@@ -2526,28 +2671,28 @@ var PraxisDBStore = class {
2526
2671
  if (!constraintResult.valid) {
2527
2672
  throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
2528
2673
  }
2529
- for (const fact of facts) {
2674
+ for (const fact2 of facts) {
2530
2675
  let before;
2531
2676
  if (this.chronicle) {
2532
- const payload = fact.payload;
2677
+ const payload = fact2.payload;
2533
2678
  const id = payload?.id;
2534
2679
  if (id) {
2535
- before = await this.getFact(fact.tag, id);
2680
+ before = await this.getFact(fact2.tag, id);
2536
2681
  }
2537
2682
  }
2538
- await this.persistFact(fact);
2683
+ await this.persistFact(fact2);
2539
2684
  if (this.chronicle) {
2540
- const payload = fact.payload;
2685
+ const payload = fact2.payload;
2541
2686
  const id = payload?.id ?? "";
2542
2687
  const span = ChronicleContext.current;
2543
2688
  try {
2544
2689
  await this.chronicle.record({
2545
- path: getFactPath(fact.tag, id),
2690
+ path: getFactPath(fact2.tag, id),
2546
2691
  before,
2547
- after: fact,
2692
+ after: fact2,
2548
2693
  cause: span?.spanId,
2549
2694
  context: span?.contextId,
2550
- metadata: { factTag: fact.tag, operation: "storeFacts" }
2695
+ metadata: { factTag: fact2.tag, operation: "storeFacts" }
2551
2696
  });
2552
2697
  } catch {
2553
2698
  }
@@ -2559,11 +2704,11 @@ var PraxisDBStore = class {
2559
2704
  * Internal method to persist a fact without constraint checking
2560
2705
  * Used by both storeFact and derived fact storage
2561
2706
  */
2562
- async persistFact(fact) {
2563
- const payload = fact.payload;
2707
+ async persistFact(fact2) {
2708
+ const payload = fact2.payload;
2564
2709
  const id = payload?.id ?? generateId();
2565
- const path3 = getFactPath(fact.tag, id);
2566
- await this.db.set(path3, fact);
2710
+ const path3 = getFactPath(fact2.tag, id);
2711
+ await this.db.set(path3, fact2);
2567
2712
  }
2568
2713
  /**
2569
2714
  * Get a fact by tag and id
@@ -2700,8 +2845,8 @@ var PraxisDBStore = class {
2700
2845
  if (watchers) {
2701
2846
  watchers.add(callback);
2702
2847
  }
2703
- const unsubscribe = this.db.watch(path3, (fact) => {
2704
- callback([fact]);
2848
+ const unsubscribe = this.db.watch(path3, (fact2) => {
2849
+ callback([fact2]);
2705
2850
  });
2706
2851
  this.subscriptions.push(unsubscribe);
2707
2852
  return () => {
@@ -2759,13 +2904,18 @@ var PraxisDBStore = class {
2759
2904
  const state2 = {
2760
2905
  context: this.context,
2761
2906
  facts: [],
2907
+ events,
2762
2908
  meta: {}
2763
2909
  };
2764
2910
  const derivedFacts = [];
2765
2911
  for (const rule of rules) {
2766
2912
  try {
2767
- const facts = rule.impl(state2, events);
2768
- derivedFacts.push(...facts);
2913
+ const result = rule.impl(state2, events);
2914
+ if (Array.isArray(result)) {
2915
+ derivedFacts.push(...result);
2916
+ } else if (result && "kind" in result && result.kind === "emit") {
2917
+ derivedFacts.push(...result.facts);
2918
+ }
2769
2919
  } catch (error) {
2770
2920
  this.onRuleError(rule.id, error);
2771
2921
  }
@@ -2773,19 +2923,19 @@ var PraxisDBStore = class {
2773
2923
  if (derivedFacts.length > 0) {
2774
2924
  const constraintResult = await this.checkConstraints(derivedFacts);
2775
2925
  if (constraintResult.valid) {
2776
- for (const fact of derivedFacts) {
2777
- await this.persistFact(fact);
2926
+ for (const fact2 of derivedFacts) {
2927
+ await this.persistFact(fact2);
2778
2928
  if (this.chronicle) {
2779
- const payload = fact.payload;
2929
+ const payload = fact2.payload;
2780
2930
  const id = payload?.id ?? "";
2781
2931
  const span = ChronicleContext.current;
2782
2932
  try {
2783
2933
  await this.chronicle.record({
2784
- path: getFactPath(fact.tag, id),
2785
- after: fact,
2934
+ path: getFactPath(fact2.tag, id),
2935
+ after: fact2,
2786
2936
  cause: span?.spanId,
2787
2937
  context: span?.contextId,
2788
- metadata: { factTag: fact.tag, operation: "derivedFact" }
2938
+ metadata: { factTag: fact2.tag, operation: "derivedFact" }
2789
2939
  });
2790
2940
  } catch {
2791
2941
  }
@@ -3398,7 +3548,7 @@ async function createUnumAdapter(config) {
3398
3548
  type: "event"
3399
3549
  });
3400
3550
  }
3401
- async function broadcastFact(channelId, fact) {
3551
+ async function broadcastFact(channelId, fact2) {
3402
3552
  const channel = channels.get(channelId);
3403
3553
  if (!channel) {
3404
3554
  throw new Error(`Not joined to channel ${channelId}`);
@@ -3406,7 +3556,7 @@ async function createUnumAdapter(config) {
3406
3556
  await channel.publish({
3407
3557
  id: generateId2(),
3408
3558
  sender: currentIdentity || { id: "anonymous", createdAt: Date.now() },
3409
- content: fact,
3559
+ content: fact2,
3410
3560
  type: "fact"
3411
3561
  });
3412
3562
  }
@@ -3549,17 +3699,17 @@ function schemaToCanvas(schema, _options = {}) {
3549
3699
  yOffset += schema.events.length * ySpacing + 30;
3550
3700
  }
3551
3701
  if (schema.facts) {
3552
- schema.facts.forEach((fact, index) => {
3553
- const pos = fact.position && (fact.position.x !== 0 || fact.position.y !== 0) ? fact.position : { x: 50 + xSpacing * 3, y: yOffset + index * ySpacing };
3702
+ schema.facts.forEach((fact2, index) => {
3703
+ const pos = fact2.position && (fact2.position.x !== 0 || fact2.position.y !== 0) ? fact2.position : { x: 50 + xSpacing * 3, y: yOffset + index * ySpacing };
3554
3704
  const node = {
3555
- id: fact.id || `fact-${nodeId++}`,
3705
+ id: fact2.id || `fact-${nodeId++}`,
3556
3706
  type: "fact",
3557
- label: fact.tag,
3707
+ label: fact2.tag,
3558
3708
  x: pos.x,
3559
3709
  y: pos.y,
3560
3710
  width: 150,
3561
3711
  height: 50,
3562
- data: fact,
3712
+ data: fact2,
3563
3713
  style: {
3564
3714
  backgroundColor: "#fce4ec",
3565
3715
  borderColor: "#c2185b"
@@ -4014,8 +4164,8 @@ var StateDocsGenerator = class {
4014
4164
  if (logic.facts && logic.facts.length > 0) {
4015
4165
  lines.push("**Facts:**");
4016
4166
  lines.push("");
4017
- for (const fact of logic.facts) {
4018
- lines.push(`- \`${fact.tag}\`: ${fact.description || ""}`);
4167
+ for (const fact2 of logic.facts) {
4168
+ lines.push(`- \`${fact2.tag}\`: ${fact2.description || ""}`);
4019
4169
  }
4020
4170
  lines.push("");
4021
4171
  }
@@ -4147,9 +4297,9 @@ var StateDocsGenerator = class {
4147
4297
  lines.push("");
4148
4298
  lines.push("| Fact | Description | Payload |");
4149
4299
  lines.push("|------|-------------|---------|");
4150
- for (const fact of logic.facts) {
4151
- const payload = fact.payload ? Object.entries(fact.payload).map(([k, v]) => `${k}: ${v}`).join(", ") : "-";
4152
- lines.push(`| \`${fact.tag}\` | ${fact.description || "-"} | ${payload} |`);
4300
+ for (const fact2 of logic.facts) {
4301
+ const payload = fact2.payload ? Object.entries(fact2.payload).map(([k, v]) => `${k}: ${v}`).join(", ") : "-";
4302
+ lines.push(`| \`${fact2.tag}\` | ${fact2.description || "-"} | ${payload} |`);
4153
4303
  }
4154
4304
  lines.push("");
4155
4305
  }
@@ -4201,8 +4351,8 @@ var StateDocsGenerator = class {
4201
4351
  for (const event of logic.events) {
4202
4352
  lines.push(` Processing --> ${event.tag.replace(/[^a-zA-Z0-9]/g, "")}: ${event.tag}`);
4203
4353
  }
4204
- for (const fact of logic.facts) {
4205
- lines.push(` ${fact.tag.replace(/[^a-zA-Z0-9]/g, "")} --> [*]`);
4354
+ for (const fact2 of logic.facts) {
4355
+ lines.push(` ${fact2.tag.replace(/[^a-zA-Z0-9]/g, "")} --> [*]`);
4206
4356
  }
4207
4357
  }
4208
4358
  return {
@@ -4653,6 +4803,302 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4653
4803
  }
4654
4804
  };
4655
4805
  }
4806
+
4807
+ // src/index.ts
4808
+ init_rule_result();
4809
+
4810
+ // src/core/ui-rules.ts
4811
+ init_rule_result();
4812
+ var loadingGateRule = {
4813
+ id: "ui/loading-gate",
4814
+ description: "Signals when the app is in a loading state",
4815
+ eventTypes: ["ui.state-change", "app.init"],
4816
+ impl: (state2) => {
4817
+ const ctx = state2.context;
4818
+ if (ctx.loading) {
4819
+ return RuleResult.emit([fact("ui.loading-gate", { active: true })]);
4820
+ }
4821
+ return RuleResult.retract(["ui.loading-gate"], "Not loading");
4822
+ },
4823
+ contract: {
4824
+ ruleId: "RULE_ID_PLACEHOLDER",
4825
+ behavior: "Emits ui.loading-gate when context.loading is true, retracts when false",
4826
+ examples: [
4827
+ { given: "loading is true", when: "ui state changes", then: "ui.loading-gate emitted" },
4828
+ { given: "loading is false", when: "ui state changes", then: "ui.loading-gate retracted" }
4829
+ ],
4830
+ invariants: ["Loading gate must reflect context.loading exactly"]
4831
+ }
4832
+ };
4833
+ var errorDisplayRule = {
4834
+ id: "ui/error-display",
4835
+ description: "Signals when an error should be displayed to the user",
4836
+ eventTypes: ["ui.state-change", "app.error"],
4837
+ impl: (state2) => {
4838
+ const ctx = state2.context;
4839
+ if (ctx.error) {
4840
+ return RuleResult.emit([fact("ui.error-display", { message: ctx.error, severity: "error" })]);
4841
+ }
4842
+ return RuleResult.retract(["ui.error-display"], "Error cleared");
4843
+ },
4844
+ contract: {
4845
+ ruleId: "RULE_ID_PLACEHOLDER",
4846
+ behavior: "Emits ui.error-display when context.error is non-null, retracts when cleared",
4847
+ examples: [
4848
+ { given: "error is set", when: "ui state changes", then: "ui.error-display emitted with message" },
4849
+ { given: "error is null", when: "ui state changes", then: "ui.error-display retracted" }
4850
+ ],
4851
+ invariants: ["Error display must clear when error is null"]
4852
+ }
4853
+ };
4854
+ var offlineIndicatorRule = {
4855
+ id: "ui/offline-indicator",
4856
+ description: "Signals when the app is offline",
4857
+ eventTypes: ["ui.state-change", "network.change"],
4858
+ impl: (state2) => {
4859
+ if (state2.context.offline) {
4860
+ return RuleResult.emit([fact("ui.offline", { message: "You are offline. Changes will sync when reconnected." })]);
4861
+ }
4862
+ return RuleResult.retract(["ui.offline"], "Back online");
4863
+ },
4864
+ contract: {
4865
+ ruleId: "RULE_ID_PLACEHOLDER",
4866
+ behavior: "Emits ui.offline when context.offline is true, retracts when back online",
4867
+ examples: [
4868
+ { given: "offline is true", when: "network changes", then: "ui.offline emitted" },
4869
+ { given: "offline is false", when: "network changes", then: "ui.offline retracted" }
4870
+ ],
4871
+ invariants: ["Offline indicator must match actual connectivity"]
4872
+ }
4873
+ };
4874
+ var dirtyGuardRule = {
4875
+ id: "ui/dirty-guard",
4876
+ description: "Warns when there are unsaved changes",
4877
+ eventTypes: ["ui.state-change", "navigation.request"],
4878
+ impl: (state2) => {
4879
+ if (state2.context.dirty) {
4880
+ return RuleResult.emit([fact("ui.unsaved-warning", {
4881
+ message: "You have unsaved changes",
4882
+ blocking: true
4883
+ })]);
4884
+ }
4885
+ return RuleResult.retract(["ui.unsaved-warning"], "No unsaved changes");
4886
+ },
4887
+ contract: {
4888
+ ruleId: "RULE_ID_PLACEHOLDER",
4889
+ behavior: "Emits ui.unsaved-warning when context.dirty is true, retracts when saved",
4890
+ examples: [
4891
+ { given: "dirty is true", when: "ui state changes", then: "ui.unsaved-warning emitted with blocking=true" },
4892
+ { given: "dirty is false", when: "ui state changes", then: "ui.unsaved-warning retracted" }
4893
+ ],
4894
+ invariants: ["Dirty guard must clear after save"]
4895
+ }
4896
+ };
4897
+ var initGateRule = {
4898
+ id: "ui/init-gate",
4899
+ description: "Signals whether the app has completed initialization",
4900
+ eventTypes: ["ui.state-change", "app.init"],
4901
+ impl: (state2) => {
4902
+ if (!state2.context.initialized) {
4903
+ return RuleResult.emit([fact("ui.init-pending", {
4904
+ message: "App is initializing..."
4905
+ })]);
4906
+ }
4907
+ return RuleResult.retract(["ui.init-pending"], "App initialized");
4908
+ },
4909
+ contract: {
4910
+ ruleId: "RULE_ID_PLACEHOLDER",
4911
+ behavior: "Emits ui.init-pending until context.initialized is true",
4912
+ examples: [
4913
+ { given: "initialized is false", when: "app starts", then: "ui.init-pending emitted" },
4914
+ { given: "initialized is true", when: "init completes", then: "ui.init-pending retracted" }
4915
+ ],
4916
+ invariants: ["Init gate must clear exactly once, when initialization completes"]
4917
+ }
4918
+ };
4919
+ var viewportRule = {
4920
+ id: "ui/viewport-class",
4921
+ description: "Classifies viewport size for responsive layout decisions",
4922
+ eventTypes: ["ui.state-change", "ui.resize"],
4923
+ impl: (state2) => {
4924
+ const vp = state2.context.viewport;
4925
+ if (!vp) return RuleResult.skip("No viewport data");
4926
+ return RuleResult.emit([fact("ui.viewport-class", {
4927
+ viewport: vp,
4928
+ compact: vp === "mobile",
4929
+ showSidebar: vp !== "mobile"
4930
+ })]);
4931
+ },
4932
+ contract: {
4933
+ ruleId: "RULE_ID_PLACEHOLDER",
4934
+ behavior: "Classifies viewport into responsive layout hints",
4935
+ examples: [
4936
+ { given: "viewport is mobile", when: "resize event", then: "compact=true, showSidebar=false" },
4937
+ { given: "viewport is desktop", when: "resize event", then: "compact=false, showSidebar=true" }
4938
+ ],
4939
+ invariants: ["Viewport class must update on every resize event"]
4940
+ }
4941
+ };
4942
+ var noInteractionWhileLoadingConstraint = {
4943
+ id: "ui/no-interaction-while-loading",
4944
+ description: "Prevents data mutations while a load operation is in progress",
4945
+ impl: (state2) => {
4946
+ if (state2.context.loading) {
4947
+ return "Cannot perform action while data is loading";
4948
+ }
4949
+ return true;
4950
+ },
4951
+ contract: {
4952
+ ruleId: "RULE_ID_PLACEHOLDER",
4953
+ behavior: "Fails when context.loading is true",
4954
+ examples: [
4955
+ { given: "loading is true", when: "action attempted", then: "violation" },
4956
+ { given: "loading is false", when: "action attempted", then: "pass" }
4957
+ ],
4958
+ invariants: ["Must always fail during loading"]
4959
+ }
4960
+ };
4961
+ var mustBeInitializedConstraint = {
4962
+ id: "ui/must-be-initialized",
4963
+ description: "Requires app initialization before user interactions",
4964
+ impl: (state2) => {
4965
+ if (!state2.context.initialized) {
4966
+ return "App must be initialized before performing this action";
4967
+ }
4968
+ return true;
4969
+ },
4970
+ contract: {
4971
+ ruleId: "RULE_ID_PLACEHOLDER",
4972
+ behavior: "Fails when context.initialized is false",
4973
+ examples: [
4974
+ { given: "initialized is false", when: "action attempted", then: "violation" },
4975
+ { given: "initialized is true", when: "action attempted", then: "pass" }
4976
+ ],
4977
+ invariants: ["Must always fail before init completes"]
4978
+ }
4979
+ };
4980
+ var uiModule = {
4981
+ rules: [
4982
+ loadingGateRule,
4983
+ errorDisplayRule,
4984
+ offlineIndicatorRule,
4985
+ dirtyGuardRule,
4986
+ initGateRule,
4987
+ viewportRule
4988
+ ],
4989
+ constraints: [
4990
+ noInteractionWhileLoadingConstraint,
4991
+ mustBeInitializedConstraint
4992
+ ],
4993
+ meta: {
4994
+ name: "praxis-ui",
4995
+ version: "1.0.0",
4996
+ description: "Predefined UI rules and constraints \u2014 separate from business logic"
4997
+ }
4998
+ };
4999
+ function createUIModule(options) {
5000
+ const allRules = uiModule.rules;
5001
+ const allConstraints = uiModule.constraints;
5002
+ const selectedRules = options.rules ? allRules.filter((r) => options.rules.includes(r.id)) : allRules;
5003
+ const selectedConstraints = options.constraints ? allConstraints.filter((c) => options.constraints.includes(c.id)) : allConstraints;
5004
+ return {
5005
+ rules: [...selectedRules, ...options.extraRules ?? []],
5006
+ constraints: [...selectedConstraints, ...options.extraConstraints ?? []],
5007
+ meta: { ...uiModule.meta, customized: true }
5008
+ };
5009
+ }
5010
+ function uiStateChanged(changes) {
5011
+ return { tag: "ui.state-change", payload: changes ?? {} };
5012
+ }
5013
+ function navigationRequest(to) {
5014
+ return { tag: "navigation.request", payload: { to } };
5015
+ }
5016
+ function resizeEvent(width, height) {
5017
+ return { tag: "ui.resize", payload: { width, height } };
5018
+ }
5019
+
5020
+ // src/core/completeness.ts
5021
+ function auditCompleteness(manifest, registryRuleIds, registryConstraintIds, rulesWithContracts, config) {
5022
+ const threshold = config?.threshold ?? 90;
5023
+ const domainBranches = manifest.branches.filter((b) => b.kind === "domain");
5024
+ const coveredDomain = domainBranches.filter((b) => b.coveredBy && registryRuleIds.includes(b.coveredBy));
5025
+ const uncoveredDomain = domainBranches.filter((b) => !b.coveredBy || !registryRuleIds.includes(b.coveredBy));
5026
+ const invariantBranches = manifest.branches.filter((b) => b.kind === "invariant");
5027
+ const coveredInvariants = invariantBranches.filter((b) => b.coveredBy && registryConstraintIds.includes(b.coveredBy));
5028
+ const uncoveredInvariants = invariantBranches.filter((b) => !b.coveredBy || !registryConstraintIds.includes(b.coveredBy));
5029
+ const needContracts = manifest.rulesNeedingContracts;
5030
+ const haveContracts = needContracts.filter((id) => rulesWithContracts.includes(id));
5031
+ const missingContracts = needContracts.filter((id) => !rulesWithContracts.includes(id));
5032
+ const neededFields = manifest.stateFields.filter((f) => f.usedByRule);
5033
+ const coveredFields = neededFields.filter((f) => f.inContext);
5034
+ const missingFields = neededFields.filter((f) => !f.inContext);
5035
+ const coveredTransitions = manifest.transitions.filter((t) => t.eventTag);
5036
+ const missingTransitions = manifest.transitions.filter((t) => !t.eventTag);
5037
+ const ruleScore = domainBranches.length > 0 ? coveredDomain.length / domainBranches.length * 40 : 40;
5038
+ const constraintScore = invariantBranches.length > 0 ? coveredInvariants.length / invariantBranches.length * 20 : 20;
5039
+ const contractScore = needContracts.length > 0 ? haveContracts.length / needContracts.length * 15 : 15;
5040
+ const contextScore = neededFields.length > 0 ? coveredFields.length / neededFields.length * 15 : 15;
5041
+ const eventScore = manifest.transitions.length > 0 ? coveredTransitions.length / manifest.transitions.length * 10 : 10;
5042
+ const score = Math.round(ruleScore + constraintScore + contractScore + contextScore + eventScore);
5043
+ const rating = score >= 90 ? "complete" : score >= 70 ? "good" : score >= 50 ? "partial" : "incomplete";
5044
+ const report = {
5045
+ score,
5046
+ rating,
5047
+ rules: { total: domainBranches.length, covered: coveredDomain.length, uncovered: uncoveredDomain },
5048
+ constraints: { total: invariantBranches.length, covered: coveredInvariants.length, uncovered: uncoveredInvariants },
5049
+ contracts: { total: needContracts.length, withContracts: haveContracts.length, missing: missingContracts },
5050
+ context: { total: neededFields.length, covered: coveredFields.length, missing: missingFields },
5051
+ events: { total: manifest.transitions.length, covered: coveredTransitions.length, missing: missingTransitions }
5052
+ };
5053
+ if (config?.strict && score < threshold) {
5054
+ throw new Error(`Praxis completeness ${score}/100 (${rating}) \u2014 below threshold ${threshold}. ${uncoveredDomain.length} uncovered rules, ${uncoveredInvariants.length} uncovered invariants, ${missingContracts.length} missing contracts.`);
5055
+ }
5056
+ return report;
5057
+ }
5058
+ function formatReport(report) {
5059
+ const lines = [];
5060
+ const icon = report.rating === "complete" ? "\u2705" : report.rating === "good" ? "\u{1F7E2}" : report.rating === "partial" ? "\u{1F7E1}" : "\u{1F534}";
5061
+ lines.push(`${icon} Praxis Completeness: ${report.score}/100 (${report.rating})`);
5062
+ lines.push("");
5063
+ lines.push(`Rules: ${report.rules.covered}/${report.rules.total} domain branches covered (${pct(report.rules.covered, report.rules.total)})`);
5064
+ lines.push(`Constraints: ${report.constraints.covered}/${report.constraints.total} invariants covered (${pct(report.constraints.covered, report.constraints.total)})`);
5065
+ lines.push(`Contracts: ${report.contracts.withContracts}/${report.contracts.total} rules have contracts (${pct(report.contracts.withContracts, report.contracts.total)})`);
5066
+ lines.push(`Context: ${report.context.covered}/${report.context.total} state fields in context (${pct(report.context.covered, report.context.total)})`);
5067
+ lines.push(`Events: ${report.events.covered}/${report.events.total} transitions have events (${pct(report.events.covered, report.events.total)})`);
5068
+ if (report.rules.uncovered.length > 0) {
5069
+ lines.push("");
5070
+ lines.push("Uncovered domain logic:");
5071
+ for (const b of report.rules.uncovered) {
5072
+ lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
5073
+ }
5074
+ }
5075
+ if (report.constraints.uncovered.length > 0) {
5076
+ lines.push("");
5077
+ lines.push("Uncovered invariants:");
5078
+ for (const b of report.constraints.uncovered) {
5079
+ lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
5080
+ }
5081
+ }
5082
+ if (report.contracts.missing.length > 0) {
5083
+ lines.push("");
5084
+ lines.push("Rules missing contracts:");
5085
+ for (const id of report.contracts.missing) {
5086
+ lines.push(` \u{1F4DD} ${id}`);
5087
+ }
5088
+ }
5089
+ if (report.events.missing.length > 0) {
5090
+ lines.push("");
5091
+ lines.push("State transitions without events:");
5092
+ for (const t of report.events.missing) {
5093
+ lines.push(` \u26A1 ${t.location}: ${t.description}`);
5094
+ }
5095
+ }
5096
+ return lines.join("\n");
5097
+ }
5098
+ function pct(a, b) {
5099
+ if (b === 0) return "100%";
5100
+ return Math.round(a / b * 100) + "%";
5101
+ }
4656
5102
  // Annotate the CommonJS export names for ESM import in node:
4657
5103
  0 && (module.exports = {
4658
5104
  AcknowledgeContractGap,
@@ -4678,6 +5124,7 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4678
5124
  PraxisSchemaRegistry,
4679
5125
  ReactiveLogicEngine,
4680
5126
  RegistryIntrospector,
5127
+ RuleResult,
4681
5128
  StateDocsGenerator,
4682
5129
  TerminalAdapter,
4683
5130
  ValidateContracts,
@@ -4685,6 +5132,7 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4685
5132
  attachTauriToEngine,
4686
5133
  attachToEngine,
4687
5134
  attachUnumToEngine,
5135
+ auditCompleteness,
4688
5136
  canvasToMermaid,
4689
5137
  canvasToSchema,
4690
5138
  canvasToYaml,
@@ -4710,6 +5158,7 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4710
5158
  createTauriPraxisAdapter,
4711
5159
  createTerminalAdapter,
4712
5160
  createTimerActor,
5161
+ createUIModule,
4713
5162
  createUnifiedApp,
4714
5163
  createUnumAdapter,
4715
5164
  defineConstraint,
@@ -4718,10 +5167,14 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4718
5167
  defineFact,
4719
5168
  defineModule,
4720
5169
  defineRule,
5170
+ dirtyGuardRule,
5171
+ errorDisplayRule,
5172
+ fact,
4721
5173
  filterEvents,
4722
5174
  filterFacts,
4723
5175
  findEvent,
4724
5176
  findFact,
5177
+ formatReport,
4725
5178
  formatValidationReport,
4726
5179
  formatValidationReportJSON,
4727
5180
  formatValidationReportSARIF,
@@ -4732,16 +5185,26 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4732
5185
  getEventPath,
4733
5186
  getFactPath,
4734
5187
  getSchemaPath,
5188
+ initGateRule,
4735
5189
  isContract,
4736
5190
  loadSchema,
4737
5191
  loadSchemaFromFile,
4738
5192
  loadSchemaFromJson,
4739
5193
  loadSchemaFromYaml,
5194
+ loadingGateRule,
5195
+ mustBeInitializedConstraint,
5196
+ navigationRequest,
5197
+ noInteractionWhileLoadingConstraint,
5198
+ offlineIndicatorRule,
4740
5199
  registerSchema,
5200
+ resizeEvent,
4741
5201
  runTerminalCommand,
4742
5202
  schemaToCanvas,
5203
+ uiModule,
5204
+ uiStateChanged,
4743
5205
  validateContracts,
4744
5206
  validateForGeneration,
4745
5207
  validateSchema,
4746
- validateWithGuardian
5208
+ validateWithGuardian,
5209
+ viewportRule
4747
5210
  });