@plures/praxis 1.2.41 → 1.4.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 (64) 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-2IUFZBH3.js +87 -0
  10. package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
  11. package/dist/node/{chunk-32YFEEML.js → chunk-7M3HV4XR.js} +4 -4
  12. package/dist/node/{chunk-PTH6MD6P.js → chunk-FWOXU4MM.js} +1 -1
  13. package/dist/node/{chunk-BBP2F7TT.js → chunk-KMJWAFZV.js} +128 -5
  14. package/dist/node/chunk-PGVSB6NR.js +59 -0
  15. package/dist/node/cli/index.cjs +1078 -211
  16. package/dist/node/cli/index.js +21 -2
  17. package/dist/node/cloud/index.d.cts +1 -1
  18. package/dist/node/cloud/index.d.ts +1 -1
  19. package/dist/node/{engine-7CXQV6RC.js → engine-FEN5IYZ5.js} +1 -1
  20. package/dist/node/index.cjs +1633 -59
  21. package/dist/node/index.d.cts +769 -5
  22. package/dist/node/index.d.ts +769 -5
  23. package/dist/node/index.js +1375 -45
  24. package/dist/node/integrations/svelte.cjs +123 -5
  25. package/dist/node/integrations/svelte.d.cts +3 -3
  26. package/dist/node/integrations/svelte.d.ts +3 -3
  27. package/dist/node/integrations/svelte.js +3 -3
  28. package/dist/node/{protocol-BocKczNv.d.ts → protocol-DcyGMmWY.d.cts} +7 -0
  29. package/dist/node/{protocol-BocKczNv.d.cts → protocol-DcyGMmWY.d.ts} +7 -0
  30. package/dist/node/{reactive-engine.svelte-CGe8SpVE.d.cts → reactive-engine.svelte-Cg0Yc2Hs.d.cts} +90 -6
  31. package/dist/node/{reactive-engine.svelte-D-xTDxT5.d.ts → reactive-engine.svelte-DekxqFu0.d.ts} +90 -6
  32. package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
  33. package/dist/node/rules-4DAJ4Z4N.js +7 -0
  34. package/dist/node/server-SYZPDULV.js +361 -0
  35. package/dist/node/{validate-EN3M4FUR.js → validate-TQGVIG7G.js} +4 -3
  36. package/package.json +29 -3
  37. package/src/__tests__/engine-v2.test.ts +532 -0
  38. package/src/__tests__/expectations.test.ts +364 -0
  39. package/src/__tests__/factory.test.ts +426 -0
  40. package/src/__tests__/mcp-server.test.ts +310 -0
  41. package/src/__tests__/project.test.ts +396 -0
  42. package/src/cli/index.ts +28 -0
  43. package/src/core/completeness.ts +274 -0
  44. package/src/core/engine.ts +47 -5
  45. package/src/core/pluresdb/store.ts +9 -3
  46. package/src/core/protocol.ts +7 -0
  47. package/src/core/rule-result.ts +130 -0
  48. package/src/core/rules.ts +12 -5
  49. package/src/core/ui-rules.ts +340 -0
  50. package/src/expectations/expectations.ts +471 -0
  51. package/src/expectations/index.ts +29 -0
  52. package/src/expectations/types.ts +95 -0
  53. package/src/factory/factory.ts +634 -0
  54. package/src/factory/index.ts +27 -0
  55. package/src/factory/types.ts +64 -0
  56. package/src/index.ts +84 -0
  57. package/src/mcp/index.ts +33 -0
  58. package/src/mcp/server.ts +485 -0
  59. package/src/mcp/types.ts +161 -0
  60. package/src/project/index.ts +31 -0
  61. package/src/project/project.ts +423 -0
  62. package/src/project/types.ts +87 -0
  63. package/src/vite/completeness-plugin.ts +72 -0
  64. /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -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) {
@@ -481,6 +609,8 @@ __export(src_exports, {
481
609
  ContractMissing: () => ContractMissing,
482
610
  ContractUpdated: () => ContractUpdated,
483
611
  ContractValidated: () => ContractValidated,
612
+ Expectation: () => Expectation,
613
+ ExpectationSet: () => ExpectationSet,
484
614
  FrameworkAgnosticReactiveEngine: () => ReactiveLogicEngine2,
485
615
  InMemoryPraxisDB: () => InMemoryPraxisDB,
486
616
  LogicEngine: () => LogicEngine,
@@ -494,6 +624,7 @@ __export(src_exports, {
494
624
  PraxisSchemaRegistry: () => PraxisSchemaRegistry,
495
625
  ReactiveLogicEngine: () => ReactiveLogicEngine,
496
626
  RegistryIntrospector: () => RegistryIntrospector,
627
+ RuleResult: () => RuleResult,
497
628
  StateDocsGenerator: () => StateDocsGenerator,
498
629
  TerminalAdapter: () => TerminalAdapter,
499
630
  ValidateContracts: () => ValidateContracts,
@@ -501,9 +632,12 @@ __export(src_exports, {
501
632
  attachTauriToEngine: () => attachTauriToEngine,
502
633
  attachToEngine: () => attachToEngine,
503
634
  attachUnumToEngine: () => attachUnumToEngine,
635
+ auditCompleteness: () => auditCompleteness,
636
+ branchRules: () => branchRules,
504
637
  canvasToMermaid: () => canvasToMermaid,
505
638
  canvasToSchema: () => canvasToSchema,
506
639
  canvasToYaml: () => canvasToYaml,
640
+ commitFromState: () => commitFromState,
507
641
  createBehaviorLedger: () => createBehaviorLedger,
508
642
  createCanvasEditor: () => createCanvasEditor,
509
643
  createChronicle: () => createChronicle,
@@ -526,21 +660,33 @@ __export(src_exports, {
526
660
  createTauriPraxisAdapter: () => createTauriPraxisAdapter,
527
661
  createTerminalAdapter: () => createTerminalAdapter,
528
662
  createTimerActor: () => createTimerActor,
663
+ createUIModule: () => createUIModule,
529
664
  createUnifiedApp: () => createUnifiedApp,
530
665
  createUnumAdapter: () => createUnumAdapter,
666
+ dataRules: () => dataRules,
531
667
  defineConstraint: () => defineConstraint,
532
668
  defineContract: () => defineContract,
533
669
  defineEvent: () => defineEvent,
534
670
  defineFact: () => defineFact,
671
+ defineGate: () => defineGate,
535
672
  defineModule: () => defineModule,
536
673
  defineRule: () => defineRule,
674
+ dirtyGuardRule: () => dirtyGuardRule,
675
+ errorDisplayRule: () => errorDisplayRule,
676
+ expectBehavior: () => expectBehavior,
677
+ expectationGate: () => expectationGate,
678
+ fact: () => fact,
537
679
  filterEvents: () => filterEvents,
538
680
  filterFacts: () => filterFacts,
539
681
  findEvent: () => findEvent,
540
682
  findFact: () => findFact,
683
+ formRules: () => formRules,
684
+ formatGate: () => formatGate,
685
+ formatReport: () => formatReport,
541
686
  formatValidationReport: () => formatValidationReport,
542
687
  formatValidationReportJSON: () => formatValidationReportJSON,
543
688
  formatValidationReportSARIF: () => formatValidationReportSARIF,
689
+ formatVerificationReport: () => formatVerificationReport,
544
690
  generateDocs: () => generateDocs,
545
691
  generateId: () => generateId,
546
692
  generateTauriConfig: () => generateTauriConfig,
@@ -548,18 +694,34 @@ __export(src_exports, {
548
694
  getEventPath: () => getEventPath,
549
695
  getFactPath: () => getFactPath,
550
696
  getSchemaPath: () => getSchemaPath,
697
+ initGateRule: () => initGateRule,
698
+ inputRules: () => inputRules,
551
699
  isContract: () => isContract,
700
+ lintGate: () => lintGate,
552
701
  loadSchema: () => loadSchema,
553
702
  loadSchemaFromFile: () => loadSchemaFromFile,
554
703
  loadSchemaFromJson: () => loadSchemaFromJson,
555
704
  loadSchemaFromYaml: () => loadSchemaFromYaml,
705
+ loadingGateRule: () => loadingGateRule,
706
+ mustBeInitializedConstraint: () => mustBeInitializedConstraint,
707
+ navigationRequest: () => navigationRequest,
708
+ navigationRules: () => navigationRules,
709
+ noInteractionWhileLoadingConstraint: () => noInteractionWhileLoadingConstraint,
710
+ offlineIndicatorRule: () => offlineIndicatorRule,
556
711
  registerSchema: () => registerSchema,
712
+ resizeEvent: () => resizeEvent,
557
713
  runTerminalCommand: () => runTerminalCommand,
558
714
  schemaToCanvas: () => schemaToCanvas,
715
+ semverContract: () => semverContract,
716
+ toastRules: () => toastRules,
717
+ uiModule: () => uiModule,
718
+ uiStateChanged: () => uiStateChanged,
559
719
  validateContracts: () => validateContracts,
560
720
  validateForGeneration: () => validateForGeneration,
561
721
  validateSchema: () => validateSchema,
562
- validateWithGuardian: () => validateWithGuardian
722
+ validateWithGuardian: () => validateWithGuardian,
723
+ verify: () => verify,
724
+ viewportRule: () => viewportRule
563
725
  });
564
726
  module.exports = __toCommonJS(src_exports);
565
727
  init_protocol();
@@ -1336,8 +1498,8 @@ function defineFact(tag) {
1336
1498
  create(payload) {
1337
1499
  return { tag, payload };
1338
1500
  },
1339
- is(fact) {
1340
- return fact.tag === tag;
1501
+ is(fact2) {
1502
+ return fact2.tag === tag;
1341
1503
  }
1342
1504
  };
1343
1505
  }
@@ -2090,16 +2252,16 @@ function validateSchema(schema) {
2090
2252
  if (schema.logic) {
2091
2253
  schema.logic.forEach((logic, logicIndex) => {
2092
2254
  if (logic.facts) {
2093
- logic.facts.forEach((fact, factIndex) => {
2094
- if (!fact.tag) {
2255
+ logic.facts.forEach((fact2, factIndex) => {
2256
+ if (!fact2.tag) {
2095
2257
  errors.push({
2096
2258
  path: `logic[${logicIndex}].facts[${factIndex}].tag`,
2097
2259
  message: "Fact tag is required"
2098
2260
  });
2099
- } else if (!isValidIdentifier(fact.tag)) {
2261
+ } else if (!isValidIdentifier(fact2.tag)) {
2100
2262
  errors.push({
2101
2263
  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.`
2264
+ 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
2265
  });
2104
2266
  }
2105
2267
  });
@@ -2484,37 +2646,37 @@ var PraxisDBStore = class {
2484
2646
  * @param fact The fact to store
2485
2647
  * @returns Promise that resolves when the fact is stored
2486
2648
  */
2487
- async storeFact(fact) {
2488
- const constraintResult = await this.checkConstraints([fact]);
2649
+ async storeFact(fact2) {
2650
+ const constraintResult = await this.checkConstraints([fact2]);
2489
2651
  if (!constraintResult.valid) {
2490
2652
  throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
2491
2653
  }
2492
2654
  let before;
2493
2655
  if (this.chronicle) {
2494
- const payload = fact.payload;
2656
+ const payload = fact2.payload;
2495
2657
  const id = payload?.id;
2496
2658
  if (id) {
2497
- before = await this.getFact(fact.tag, id);
2659
+ before = await this.getFact(fact2.tag, id);
2498
2660
  }
2499
2661
  }
2500
- await this.persistFact(fact);
2662
+ await this.persistFact(fact2);
2501
2663
  if (this.chronicle) {
2502
- const payload = fact.payload;
2664
+ const payload = fact2.payload;
2503
2665
  const id = payload?.id ?? "";
2504
2666
  const span = ChronicleContext.current;
2505
2667
  try {
2506
2668
  await this.chronicle.record({
2507
- path: getFactPath(fact.tag, id),
2669
+ path: getFactPath(fact2.tag, id),
2508
2670
  before,
2509
- after: fact,
2671
+ after: fact2,
2510
2672
  cause: span?.spanId,
2511
2673
  context: span?.contextId,
2512
- metadata: { factTag: fact.tag, operation: "storeFact" }
2674
+ metadata: { factTag: fact2.tag, operation: "storeFact" }
2513
2675
  });
2514
2676
  } catch {
2515
2677
  }
2516
2678
  }
2517
- await this.triggerRules([fact]);
2679
+ await this.triggerRules([fact2]);
2518
2680
  }
2519
2681
  /**
2520
2682
  * Store multiple facts in PluresDB
@@ -2526,28 +2688,28 @@ var PraxisDBStore = class {
2526
2688
  if (!constraintResult.valid) {
2527
2689
  throw new Error(`Constraint violation: ${constraintResult.errors.join(", ")}`);
2528
2690
  }
2529
- for (const fact of facts) {
2691
+ for (const fact2 of facts) {
2530
2692
  let before;
2531
2693
  if (this.chronicle) {
2532
- const payload = fact.payload;
2694
+ const payload = fact2.payload;
2533
2695
  const id = payload?.id;
2534
2696
  if (id) {
2535
- before = await this.getFact(fact.tag, id);
2697
+ before = await this.getFact(fact2.tag, id);
2536
2698
  }
2537
2699
  }
2538
- await this.persistFact(fact);
2700
+ await this.persistFact(fact2);
2539
2701
  if (this.chronicle) {
2540
- const payload = fact.payload;
2702
+ const payload = fact2.payload;
2541
2703
  const id = payload?.id ?? "";
2542
2704
  const span = ChronicleContext.current;
2543
2705
  try {
2544
2706
  await this.chronicle.record({
2545
- path: getFactPath(fact.tag, id),
2707
+ path: getFactPath(fact2.tag, id),
2546
2708
  before,
2547
- after: fact,
2709
+ after: fact2,
2548
2710
  cause: span?.spanId,
2549
2711
  context: span?.contextId,
2550
- metadata: { factTag: fact.tag, operation: "storeFacts" }
2712
+ metadata: { factTag: fact2.tag, operation: "storeFacts" }
2551
2713
  });
2552
2714
  } catch {
2553
2715
  }
@@ -2559,11 +2721,11 @@ var PraxisDBStore = class {
2559
2721
  * Internal method to persist a fact without constraint checking
2560
2722
  * Used by both storeFact and derived fact storage
2561
2723
  */
2562
- async persistFact(fact) {
2563
- const payload = fact.payload;
2724
+ async persistFact(fact2) {
2725
+ const payload = fact2.payload;
2564
2726
  const id = payload?.id ?? generateId();
2565
- const path3 = getFactPath(fact.tag, id);
2566
- await this.db.set(path3, fact);
2727
+ const path3 = getFactPath(fact2.tag, id);
2728
+ await this.db.set(path3, fact2);
2567
2729
  }
2568
2730
  /**
2569
2731
  * Get a fact by tag and id
@@ -2700,8 +2862,8 @@ var PraxisDBStore = class {
2700
2862
  if (watchers) {
2701
2863
  watchers.add(callback);
2702
2864
  }
2703
- const unsubscribe = this.db.watch(path3, (fact) => {
2704
- callback([fact]);
2865
+ const unsubscribe = this.db.watch(path3, (fact2) => {
2866
+ callback([fact2]);
2705
2867
  });
2706
2868
  this.subscriptions.push(unsubscribe);
2707
2869
  return () => {
@@ -2759,13 +2921,18 @@ var PraxisDBStore = class {
2759
2921
  const state2 = {
2760
2922
  context: this.context,
2761
2923
  facts: [],
2924
+ events,
2762
2925
  meta: {}
2763
2926
  };
2764
2927
  const derivedFacts = [];
2765
2928
  for (const rule of rules) {
2766
2929
  try {
2767
- const facts = rule.impl(state2, events);
2768
- derivedFacts.push(...facts);
2930
+ const result = rule.impl(state2, events);
2931
+ if (Array.isArray(result)) {
2932
+ derivedFacts.push(...result);
2933
+ } else if (result && "kind" in result && result.kind === "emit") {
2934
+ derivedFacts.push(...result.facts);
2935
+ }
2769
2936
  } catch (error) {
2770
2937
  this.onRuleError(rule.id, error);
2771
2938
  }
@@ -2773,19 +2940,19 @@ var PraxisDBStore = class {
2773
2940
  if (derivedFacts.length > 0) {
2774
2941
  const constraintResult = await this.checkConstraints(derivedFacts);
2775
2942
  if (constraintResult.valid) {
2776
- for (const fact of derivedFacts) {
2777
- await this.persistFact(fact);
2943
+ for (const fact2 of derivedFacts) {
2944
+ await this.persistFact(fact2);
2778
2945
  if (this.chronicle) {
2779
- const payload = fact.payload;
2946
+ const payload = fact2.payload;
2780
2947
  const id = payload?.id ?? "";
2781
2948
  const span = ChronicleContext.current;
2782
2949
  try {
2783
2950
  await this.chronicle.record({
2784
- path: getFactPath(fact.tag, id),
2785
- after: fact,
2951
+ path: getFactPath(fact2.tag, id),
2952
+ after: fact2,
2786
2953
  cause: span?.spanId,
2787
2954
  context: span?.contextId,
2788
- metadata: { factTag: fact.tag, operation: "derivedFact" }
2955
+ metadata: { factTag: fact2.tag, operation: "derivedFact" }
2789
2956
  });
2790
2957
  } catch {
2791
2958
  }
@@ -3398,7 +3565,7 @@ async function createUnumAdapter(config) {
3398
3565
  type: "event"
3399
3566
  });
3400
3567
  }
3401
- async function broadcastFact(channelId, fact) {
3568
+ async function broadcastFact(channelId, fact2) {
3402
3569
  const channel = channels.get(channelId);
3403
3570
  if (!channel) {
3404
3571
  throw new Error(`Not joined to channel ${channelId}`);
@@ -3406,7 +3573,7 @@ async function createUnumAdapter(config) {
3406
3573
  await channel.publish({
3407
3574
  id: generateId2(),
3408
3575
  sender: currentIdentity || { id: "anonymous", createdAt: Date.now() },
3409
- content: fact,
3576
+ content: fact2,
3410
3577
  type: "fact"
3411
3578
  });
3412
3579
  }
@@ -3549,17 +3716,17 @@ function schemaToCanvas(schema, _options = {}) {
3549
3716
  yOffset += schema.events.length * ySpacing + 30;
3550
3717
  }
3551
3718
  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 };
3719
+ schema.facts.forEach((fact2, index) => {
3720
+ const pos = fact2.position && (fact2.position.x !== 0 || fact2.position.y !== 0) ? fact2.position : { x: 50 + xSpacing * 3, y: yOffset + index * ySpacing };
3554
3721
  const node = {
3555
- id: fact.id || `fact-${nodeId++}`,
3722
+ id: fact2.id || `fact-${nodeId++}`,
3556
3723
  type: "fact",
3557
- label: fact.tag,
3724
+ label: fact2.tag,
3558
3725
  x: pos.x,
3559
3726
  y: pos.y,
3560
3727
  width: 150,
3561
3728
  height: 50,
3562
- data: fact,
3729
+ data: fact2,
3563
3730
  style: {
3564
3731
  backgroundColor: "#fce4ec",
3565
3732
  borderColor: "#c2185b"
@@ -4014,8 +4181,8 @@ var StateDocsGenerator = class {
4014
4181
  if (logic.facts && logic.facts.length > 0) {
4015
4182
  lines.push("**Facts:**");
4016
4183
  lines.push("");
4017
- for (const fact of logic.facts) {
4018
- lines.push(`- \`${fact.tag}\`: ${fact.description || ""}`);
4184
+ for (const fact2 of logic.facts) {
4185
+ lines.push(`- \`${fact2.tag}\`: ${fact2.description || ""}`);
4019
4186
  }
4020
4187
  lines.push("");
4021
4188
  }
@@ -4147,9 +4314,9 @@ var StateDocsGenerator = class {
4147
4314
  lines.push("");
4148
4315
  lines.push("| Fact | Description | Payload |");
4149
4316
  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} |`);
4317
+ for (const fact2 of logic.facts) {
4318
+ const payload = fact2.payload ? Object.entries(fact2.payload).map(([k, v]) => `${k}: ${v}`).join(", ") : "-";
4319
+ lines.push(`| \`${fact2.tag}\` | ${fact2.description || "-"} | ${payload} |`);
4153
4320
  }
4154
4321
  lines.push("");
4155
4322
  }
@@ -4201,8 +4368,8 @@ var StateDocsGenerator = class {
4201
4368
  for (const event of logic.events) {
4202
4369
  lines.push(` Processing --> ${event.tag.replace(/[^a-zA-Z0-9]/g, "")}: ${event.tag}`);
4203
4370
  }
4204
- for (const fact of logic.facts) {
4205
- lines.push(` ${fact.tag.replace(/[^a-zA-Z0-9]/g, "")} --> [*]`);
4371
+ for (const fact2 of logic.facts) {
4372
+ lines.push(` ${fact2.tag.replace(/[^a-zA-Z0-9]/g, "")} --> [*]`);
4206
4373
  }
4207
4374
  }
4208
4375
  return {
@@ -4653,6 +4820,1379 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4653
4820
  }
4654
4821
  };
4655
4822
  }
4823
+
4824
+ // src/index.ts
4825
+ init_rule_result();
4826
+
4827
+ // src/core/ui-rules.ts
4828
+ init_rule_result();
4829
+ var loadingGateRule = {
4830
+ id: "ui/loading-gate",
4831
+ description: "Signals when the app is in a loading state",
4832
+ eventTypes: ["ui.state-change", "app.init"],
4833
+ impl: (state2) => {
4834
+ const ctx = state2.context;
4835
+ if (ctx.loading) {
4836
+ return RuleResult.emit([fact("ui.loading-gate", { active: true })]);
4837
+ }
4838
+ return RuleResult.retract(["ui.loading-gate"], "Not loading");
4839
+ },
4840
+ contract: {
4841
+ ruleId: "RULE_ID_PLACEHOLDER",
4842
+ behavior: "Emits ui.loading-gate when context.loading is true, retracts when false",
4843
+ examples: [
4844
+ { given: "loading is true", when: "ui state changes", then: "ui.loading-gate emitted" },
4845
+ { given: "loading is false", when: "ui state changes", then: "ui.loading-gate retracted" }
4846
+ ],
4847
+ invariants: ["Loading gate must reflect context.loading exactly"]
4848
+ }
4849
+ };
4850
+ var errorDisplayRule = {
4851
+ id: "ui/error-display",
4852
+ description: "Signals when an error should be displayed to the user",
4853
+ eventTypes: ["ui.state-change", "app.error"],
4854
+ impl: (state2) => {
4855
+ const ctx = state2.context;
4856
+ if (ctx.error) {
4857
+ return RuleResult.emit([fact("ui.error-display", { message: ctx.error, severity: "error" })]);
4858
+ }
4859
+ return RuleResult.retract(["ui.error-display"], "Error cleared");
4860
+ },
4861
+ contract: {
4862
+ ruleId: "RULE_ID_PLACEHOLDER",
4863
+ behavior: "Emits ui.error-display when context.error is non-null, retracts when cleared",
4864
+ examples: [
4865
+ { given: "error is set", when: "ui state changes", then: "ui.error-display emitted with message" },
4866
+ { given: "error is null", when: "ui state changes", then: "ui.error-display retracted" }
4867
+ ],
4868
+ invariants: ["Error display must clear when error is null"]
4869
+ }
4870
+ };
4871
+ var offlineIndicatorRule = {
4872
+ id: "ui/offline-indicator",
4873
+ description: "Signals when the app is offline",
4874
+ eventTypes: ["ui.state-change", "network.change"],
4875
+ impl: (state2) => {
4876
+ if (state2.context.offline) {
4877
+ return RuleResult.emit([fact("ui.offline", { message: "You are offline. Changes will sync when reconnected." })]);
4878
+ }
4879
+ return RuleResult.retract(["ui.offline"], "Back online");
4880
+ },
4881
+ contract: {
4882
+ ruleId: "RULE_ID_PLACEHOLDER",
4883
+ behavior: "Emits ui.offline when context.offline is true, retracts when back online",
4884
+ examples: [
4885
+ { given: "offline is true", when: "network changes", then: "ui.offline emitted" },
4886
+ { given: "offline is false", when: "network changes", then: "ui.offline retracted" }
4887
+ ],
4888
+ invariants: ["Offline indicator must match actual connectivity"]
4889
+ }
4890
+ };
4891
+ var dirtyGuardRule = {
4892
+ id: "ui/dirty-guard",
4893
+ description: "Warns when there are unsaved changes",
4894
+ eventTypes: ["ui.state-change", "navigation.request"],
4895
+ impl: (state2) => {
4896
+ if (state2.context.dirty) {
4897
+ return RuleResult.emit([fact("ui.unsaved-warning", {
4898
+ message: "You have unsaved changes",
4899
+ blocking: true
4900
+ })]);
4901
+ }
4902
+ return RuleResult.retract(["ui.unsaved-warning"], "No unsaved changes");
4903
+ },
4904
+ contract: {
4905
+ ruleId: "RULE_ID_PLACEHOLDER",
4906
+ behavior: "Emits ui.unsaved-warning when context.dirty is true, retracts when saved",
4907
+ examples: [
4908
+ { given: "dirty is true", when: "ui state changes", then: "ui.unsaved-warning emitted with blocking=true" },
4909
+ { given: "dirty is false", when: "ui state changes", then: "ui.unsaved-warning retracted" }
4910
+ ],
4911
+ invariants: ["Dirty guard must clear after save"]
4912
+ }
4913
+ };
4914
+ var initGateRule = {
4915
+ id: "ui/init-gate",
4916
+ description: "Signals whether the app has completed initialization",
4917
+ eventTypes: ["ui.state-change", "app.init"],
4918
+ impl: (state2) => {
4919
+ if (!state2.context.initialized) {
4920
+ return RuleResult.emit([fact("ui.init-pending", {
4921
+ message: "App is initializing..."
4922
+ })]);
4923
+ }
4924
+ return RuleResult.retract(["ui.init-pending"], "App initialized");
4925
+ },
4926
+ contract: {
4927
+ ruleId: "RULE_ID_PLACEHOLDER",
4928
+ behavior: "Emits ui.init-pending until context.initialized is true",
4929
+ examples: [
4930
+ { given: "initialized is false", when: "app starts", then: "ui.init-pending emitted" },
4931
+ { given: "initialized is true", when: "init completes", then: "ui.init-pending retracted" }
4932
+ ],
4933
+ invariants: ["Init gate must clear exactly once, when initialization completes"]
4934
+ }
4935
+ };
4936
+ var viewportRule = {
4937
+ id: "ui/viewport-class",
4938
+ description: "Classifies viewport size for responsive layout decisions",
4939
+ eventTypes: ["ui.state-change", "ui.resize"],
4940
+ impl: (state2) => {
4941
+ const vp = state2.context.viewport;
4942
+ if (!vp) return RuleResult.skip("No viewport data");
4943
+ return RuleResult.emit([fact("ui.viewport-class", {
4944
+ viewport: vp,
4945
+ compact: vp === "mobile",
4946
+ showSidebar: vp !== "mobile"
4947
+ })]);
4948
+ },
4949
+ contract: {
4950
+ ruleId: "RULE_ID_PLACEHOLDER",
4951
+ behavior: "Classifies viewport into responsive layout hints",
4952
+ examples: [
4953
+ { given: "viewport is mobile", when: "resize event", then: "compact=true, showSidebar=false" },
4954
+ { given: "viewport is desktop", when: "resize event", then: "compact=false, showSidebar=true" }
4955
+ ],
4956
+ invariants: ["Viewport class must update on every resize event"]
4957
+ }
4958
+ };
4959
+ var noInteractionWhileLoadingConstraint = {
4960
+ id: "ui/no-interaction-while-loading",
4961
+ description: "Prevents data mutations while a load operation is in progress",
4962
+ impl: (state2) => {
4963
+ if (state2.context.loading) {
4964
+ return "Cannot perform action while data is loading";
4965
+ }
4966
+ return true;
4967
+ },
4968
+ contract: {
4969
+ ruleId: "RULE_ID_PLACEHOLDER",
4970
+ behavior: "Fails when context.loading is true",
4971
+ examples: [
4972
+ { given: "loading is true", when: "action attempted", then: "violation" },
4973
+ { given: "loading is false", when: "action attempted", then: "pass" }
4974
+ ],
4975
+ invariants: ["Must always fail during loading"]
4976
+ }
4977
+ };
4978
+ var mustBeInitializedConstraint = {
4979
+ id: "ui/must-be-initialized",
4980
+ description: "Requires app initialization before user interactions",
4981
+ impl: (state2) => {
4982
+ if (!state2.context.initialized) {
4983
+ return "App must be initialized before performing this action";
4984
+ }
4985
+ return true;
4986
+ },
4987
+ contract: {
4988
+ ruleId: "RULE_ID_PLACEHOLDER",
4989
+ behavior: "Fails when context.initialized is false",
4990
+ examples: [
4991
+ { given: "initialized is false", when: "action attempted", then: "violation" },
4992
+ { given: "initialized is true", when: "action attempted", then: "pass" }
4993
+ ],
4994
+ invariants: ["Must always fail before init completes"]
4995
+ }
4996
+ };
4997
+ var uiModule = {
4998
+ rules: [
4999
+ loadingGateRule,
5000
+ errorDisplayRule,
5001
+ offlineIndicatorRule,
5002
+ dirtyGuardRule,
5003
+ initGateRule,
5004
+ viewportRule
5005
+ ],
5006
+ constraints: [
5007
+ noInteractionWhileLoadingConstraint,
5008
+ mustBeInitializedConstraint
5009
+ ],
5010
+ meta: {
5011
+ name: "praxis-ui",
5012
+ version: "1.0.0",
5013
+ description: "Predefined UI rules and constraints \u2014 separate from business logic"
5014
+ }
5015
+ };
5016
+ function createUIModule(options) {
5017
+ const allRules = uiModule.rules;
5018
+ const allConstraints = uiModule.constraints;
5019
+ const selectedRules = options.rules ? allRules.filter((r) => options.rules.includes(r.id)) : allRules;
5020
+ const selectedConstraints = options.constraints ? allConstraints.filter((c) => options.constraints.includes(c.id)) : allConstraints;
5021
+ return {
5022
+ rules: [...selectedRules, ...options.extraRules ?? []],
5023
+ constraints: [...selectedConstraints, ...options.extraConstraints ?? []],
5024
+ meta: { ...uiModule.meta, customized: true }
5025
+ };
5026
+ }
5027
+ function uiStateChanged(changes) {
5028
+ return { tag: "ui.state-change", payload: changes ?? {} };
5029
+ }
5030
+ function navigationRequest(to) {
5031
+ return { tag: "navigation.request", payload: { to } };
5032
+ }
5033
+ function resizeEvent(width, height) {
5034
+ return { tag: "ui.resize", payload: { width, height } };
5035
+ }
5036
+
5037
+ // src/core/completeness.ts
5038
+ function auditCompleteness(manifest, registryRuleIds, registryConstraintIds, rulesWithContracts, config) {
5039
+ const threshold = config?.threshold ?? 90;
5040
+ const domainBranches = manifest.branches.filter((b) => b.kind === "domain");
5041
+ const coveredDomain = domainBranches.filter((b) => b.coveredBy && registryRuleIds.includes(b.coveredBy));
5042
+ const uncoveredDomain = domainBranches.filter((b) => !b.coveredBy || !registryRuleIds.includes(b.coveredBy));
5043
+ const invariantBranches = manifest.branches.filter((b) => b.kind === "invariant");
5044
+ const coveredInvariants = invariantBranches.filter((b) => b.coveredBy && registryConstraintIds.includes(b.coveredBy));
5045
+ const uncoveredInvariants = invariantBranches.filter((b) => !b.coveredBy || !registryConstraintIds.includes(b.coveredBy));
5046
+ const needContracts = manifest.rulesNeedingContracts;
5047
+ const haveContracts = needContracts.filter((id) => rulesWithContracts.includes(id));
5048
+ const missingContracts = needContracts.filter((id) => !rulesWithContracts.includes(id));
5049
+ const neededFields = manifest.stateFields.filter((f) => f.usedByRule);
5050
+ const coveredFields = neededFields.filter((f) => f.inContext);
5051
+ const missingFields = neededFields.filter((f) => !f.inContext);
5052
+ const coveredTransitions = manifest.transitions.filter((t) => t.eventTag);
5053
+ const missingTransitions = manifest.transitions.filter((t) => !t.eventTag);
5054
+ const ruleScore = domainBranches.length > 0 ? coveredDomain.length / domainBranches.length * 40 : 40;
5055
+ const constraintScore = invariantBranches.length > 0 ? coveredInvariants.length / invariantBranches.length * 20 : 20;
5056
+ const contractScore = needContracts.length > 0 ? haveContracts.length / needContracts.length * 15 : 15;
5057
+ const contextScore = neededFields.length > 0 ? coveredFields.length / neededFields.length * 15 : 15;
5058
+ const eventScore = manifest.transitions.length > 0 ? coveredTransitions.length / manifest.transitions.length * 10 : 10;
5059
+ const score = Math.round(ruleScore + constraintScore + contractScore + contextScore + eventScore);
5060
+ const rating = score >= 90 ? "complete" : score >= 70 ? "good" : score >= 50 ? "partial" : "incomplete";
5061
+ const report = {
5062
+ score,
5063
+ rating,
5064
+ rules: { total: domainBranches.length, covered: coveredDomain.length, uncovered: uncoveredDomain },
5065
+ constraints: { total: invariantBranches.length, covered: coveredInvariants.length, uncovered: uncoveredInvariants },
5066
+ contracts: { total: needContracts.length, withContracts: haveContracts.length, missing: missingContracts },
5067
+ context: { total: neededFields.length, covered: coveredFields.length, missing: missingFields },
5068
+ events: { total: manifest.transitions.length, covered: coveredTransitions.length, missing: missingTransitions }
5069
+ };
5070
+ if (config?.strict && score < threshold) {
5071
+ throw new Error(`Praxis completeness ${score}/100 (${rating}) \u2014 below threshold ${threshold}. ${uncoveredDomain.length} uncovered rules, ${uncoveredInvariants.length} uncovered invariants, ${missingContracts.length} missing contracts.`);
5072
+ }
5073
+ return report;
5074
+ }
5075
+ function formatReport(report) {
5076
+ const lines = [];
5077
+ const icon = report.rating === "complete" ? "\u2705" : report.rating === "good" ? "\u{1F7E2}" : report.rating === "partial" ? "\u{1F7E1}" : "\u{1F534}";
5078
+ lines.push(`${icon} Praxis Completeness: ${report.score}/100 (${report.rating})`);
5079
+ lines.push("");
5080
+ lines.push(`Rules: ${report.rules.covered}/${report.rules.total} domain branches covered (${pct(report.rules.covered, report.rules.total)})`);
5081
+ lines.push(`Constraints: ${report.constraints.covered}/${report.constraints.total} invariants covered (${pct(report.constraints.covered, report.constraints.total)})`);
5082
+ lines.push(`Contracts: ${report.contracts.withContracts}/${report.contracts.total} rules have contracts (${pct(report.contracts.withContracts, report.contracts.total)})`);
5083
+ lines.push(`Context: ${report.context.covered}/${report.context.total} state fields in context (${pct(report.context.covered, report.context.total)})`);
5084
+ lines.push(`Events: ${report.events.covered}/${report.events.total} transitions have events (${pct(report.events.covered, report.events.total)})`);
5085
+ if (report.rules.uncovered.length > 0) {
5086
+ lines.push("");
5087
+ lines.push("Uncovered domain logic:");
5088
+ for (const b of report.rules.uncovered) {
5089
+ lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
5090
+ }
5091
+ }
5092
+ if (report.constraints.uncovered.length > 0) {
5093
+ lines.push("");
5094
+ lines.push("Uncovered invariants:");
5095
+ for (const b of report.constraints.uncovered) {
5096
+ lines.push(` \u274C ${b.location}: ${b.condition}${b.note ? ` \u2014 ${b.note}` : ""}`);
5097
+ }
5098
+ }
5099
+ if (report.contracts.missing.length > 0) {
5100
+ lines.push("");
5101
+ lines.push("Rules missing contracts:");
5102
+ for (const id of report.contracts.missing) {
5103
+ lines.push(` \u{1F4DD} ${id}`);
5104
+ }
5105
+ }
5106
+ if (report.events.missing.length > 0) {
5107
+ lines.push("");
5108
+ lines.push("State transitions without events:");
5109
+ for (const t of report.events.missing) {
5110
+ lines.push(` \u26A1 ${t.location}: ${t.description}`);
5111
+ }
5112
+ }
5113
+ return lines.join("\n");
5114
+ }
5115
+ function pct(a, b) {
5116
+ if (b === 0) return "100%";
5117
+ return Math.round(a / b * 100) + "%";
5118
+ }
5119
+
5120
+ // src/expectations/expectations.ts
5121
+ var Expectation = class {
5122
+ name;
5123
+ _conditions = [];
5124
+ constructor(name) {
5125
+ this.name = name;
5126
+ }
5127
+ /**
5128
+ * Declare that this behavior should ONLY occur when a condition is true.
5129
+ * If the condition is false, the behavior should NOT occur.
5130
+ */
5131
+ onlyWhen(condition) {
5132
+ this._conditions.push({ description: condition, type: "onlyWhen" });
5133
+ return this;
5134
+ }
5135
+ /**
5136
+ * Declare that this behavior should NEVER occur under a given condition.
5137
+ */
5138
+ never(condition) {
5139
+ this._conditions.push({ description: condition, type: "never" });
5140
+ return this;
5141
+ }
5142
+ /**
5143
+ * Declare that this behavior should ALWAYS have a certain property.
5144
+ */
5145
+ always(condition) {
5146
+ this._conditions.push({ description: condition, type: "always" });
5147
+ return this;
5148
+ }
5149
+ /** Get all declared conditions. */
5150
+ get conditions() {
5151
+ return this._conditions;
5152
+ }
5153
+ };
5154
+ var ExpectationSet = class {
5155
+ name;
5156
+ description;
5157
+ _expectations = [];
5158
+ constructor(options) {
5159
+ this.name = options.name;
5160
+ this.description = options.description ?? "";
5161
+ }
5162
+ /** Add an expectation to the set. */
5163
+ add(expectation) {
5164
+ this._expectations.push(expectation);
5165
+ return this;
5166
+ }
5167
+ /** Get all expectations in this set. */
5168
+ get expectations() {
5169
+ return this._expectations;
5170
+ }
5171
+ /** Number of expectations. */
5172
+ get size() {
5173
+ return this._expectations.length;
5174
+ }
5175
+ };
5176
+ function expectBehavior(name) {
5177
+ return new Expectation(name);
5178
+ }
5179
+ function verify(registry, expectations) {
5180
+ const rules = registry.getAllRules();
5181
+ const constraints = registry.getAllConstraints();
5182
+ const ruleIds = registry.getRuleIds();
5183
+ const constraintIds = registry.getConstraintIds();
5184
+ const allDescriptors = [...rules, ...constraints];
5185
+ const expectationResults = [];
5186
+ for (const exp of expectations.expectations) {
5187
+ const result = verifyExpectation(exp, allDescriptors, ruleIds, constraintIds);
5188
+ expectationResults.push(result);
5189
+ }
5190
+ const satisfied = expectationResults.filter((r) => r.status === "satisfied").length;
5191
+ const violated = expectationResults.filter((r) => r.status === "violated").length;
5192
+ const partial = expectationResults.filter((r) => r.status === "partial").length;
5193
+ const allEdgeCases = expectationResults.flatMap((r) => r.edgeCases);
5194
+ const allMitigations = expectationResults.flatMap((r) => r.mitigations);
5195
+ return {
5196
+ setName: expectations.name,
5197
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5198
+ status: violated > 0 ? "violated" : partial > 0 ? "partial" : "satisfied",
5199
+ expectations: expectationResults,
5200
+ summary: {
5201
+ total: expectationResults.length,
5202
+ satisfied,
5203
+ violated,
5204
+ partial
5205
+ },
5206
+ allEdgeCases,
5207
+ allMitigations
5208
+ };
5209
+ }
5210
+ function verifyExpectation(expectation, descriptors, ruleIds, _constraintIds) {
5211
+ const conditionResults = [];
5212
+ const edgeCases = [];
5213
+ const mitigations = [];
5214
+ const related = findRelatedDescriptors(expectation.name, descriptors);
5215
+ for (const condition of expectation.conditions) {
5216
+ const result = verifyCondition(condition, expectation.name, related, ruleIds);
5217
+ conditionResults.push(result);
5218
+ if (result.status === "unverifiable") {
5219
+ edgeCases.push(`Cannot verify "${condition.description}" for "${expectation.name}" \u2014 no covering rule/contract found`);
5220
+ mitigations.push(`Add a rule or constraint that explicitly covers: ${condition.description}`);
5221
+ } else if (result.status === "violated") {
5222
+ edgeCases.push(`"${expectation.name}" may fire incorrectly: ${condition.description}`);
5223
+ mitigations.push(`Review rule logic for "${expectation.name}" regarding: ${condition.description}`);
5224
+ }
5225
+ }
5226
+ const satisfiedCount = conditionResults.filter((r) => r.status === "satisfied").length;
5227
+ const violatedCount = conditionResults.filter((r) => r.status === "violated").length;
5228
+ const total = conditionResults.length;
5229
+ let status;
5230
+ if (total === 0) {
5231
+ status = "satisfied";
5232
+ } else if (violatedCount > 0) {
5233
+ status = "violated";
5234
+ } else if (satisfiedCount === total) {
5235
+ status = "satisfied";
5236
+ } else {
5237
+ status = "partial";
5238
+ }
5239
+ return {
5240
+ name: expectation.name,
5241
+ status,
5242
+ conditions: conditionResults,
5243
+ edgeCases,
5244
+ mitigations
5245
+ };
5246
+ }
5247
+ function textOverlaps(a, b) {
5248
+ const stopWords = /* @__PURE__ */ new Set([
5249
+ "the",
5250
+ "a",
5251
+ "an",
5252
+ "is",
5253
+ "are",
5254
+ "was",
5255
+ "were",
5256
+ "be",
5257
+ "been",
5258
+ "being",
5259
+ "have",
5260
+ "has",
5261
+ "had",
5262
+ "do",
5263
+ "does",
5264
+ "did",
5265
+ "will",
5266
+ "would",
5267
+ "could",
5268
+ "should",
5269
+ "may",
5270
+ "might",
5271
+ "shall",
5272
+ "can",
5273
+ "to",
5274
+ "of",
5275
+ "in",
5276
+ "for",
5277
+ "on",
5278
+ "with",
5279
+ "at",
5280
+ "by",
5281
+ "from",
5282
+ "as",
5283
+ "into",
5284
+ "through",
5285
+ "during",
5286
+ "before",
5287
+ "after",
5288
+ "when",
5289
+ "that",
5290
+ "this",
5291
+ "it",
5292
+ "its",
5293
+ "and",
5294
+ "or",
5295
+ "but",
5296
+ "not",
5297
+ "no",
5298
+ "if",
5299
+ "then",
5300
+ "than",
5301
+ "so",
5302
+ "up",
5303
+ "out",
5304
+ "about",
5305
+ "just",
5306
+ "must"
5307
+ ]);
5308
+ const wordsA = a.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
5309
+ const wordsB = b.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
5310
+ if (wordsA.length === 0 || wordsB.length === 0) return false;
5311
+ const matches = wordsA.filter(
5312
+ (wa) => wordsB.some((wb) => wa.startsWith(wb.slice(0, 4)) || wb.startsWith(wa.slice(0, 4)))
5313
+ );
5314
+ const minWords = Math.min(wordsA.length, wordsB.length);
5315
+ return matches.length >= Math.max(1, Math.ceil(minWords * 0.5));
5316
+ }
5317
+ function verifyCondition(condition, expectationName, relatedDescriptors, _ruleIds) {
5318
+ if (relatedDescriptors.length === 0) {
5319
+ return {
5320
+ condition,
5321
+ status: "unverifiable",
5322
+ explanation: `No rules or constraints found related to "${expectationName}"`,
5323
+ relatedRules: []
5324
+ };
5325
+ }
5326
+ const relatedIds = relatedDescriptors.map((d) => d.id);
5327
+ const condLower = condition.description.toLowerCase();
5328
+ const matches = (target) => target.toLowerCase().includes(condLower) || condLower.includes(target.toLowerCase()) || textOverlaps(condLower, target);
5329
+ switch (condition.type) {
5330
+ case "onlyWhen": {
5331
+ const coveringRule = relatedDescriptors.find(
5332
+ (d) => d.contract?.examples.some(
5333
+ (ex) => matches(ex.given) || matches(ex.when)
5334
+ ) || d.contract?.invariants.some((inv) => matches(inv))
5335
+ );
5336
+ if (coveringRule) {
5337
+ return {
5338
+ condition,
5339
+ status: "satisfied",
5340
+ explanation: `Rule "${coveringRule.id}" contract covers precondition: ${condition.description}`,
5341
+ relatedRules: relatedIds
5342
+ };
5343
+ }
5344
+ const descMatch = relatedDescriptors.find((d) => matches(d.description));
5345
+ if (descMatch) {
5346
+ return {
5347
+ condition,
5348
+ status: "satisfied",
5349
+ explanation: `Rule "${descMatch.id}" description addresses: ${condition.description}`,
5350
+ relatedRules: relatedIds
5351
+ };
5352
+ }
5353
+ return {
5354
+ condition,
5355
+ status: "unverifiable",
5356
+ explanation: `No rule contract explicitly covers the precondition: ${condition.description}`,
5357
+ relatedRules: relatedIds
5358
+ };
5359
+ }
5360
+ case "never": {
5361
+ const preventingDescriptor = relatedDescriptors.find(
5362
+ (d) => d.contract?.invariants.some((inv) => matches(inv)) || d.contract?.examples.some(
5363
+ (ex) => (matches(ex.given) || matches(ex.when)) && (ex.then.toLowerCase().includes("retract") || ex.then.toLowerCase().includes("fail") || ex.then.toLowerCase().includes("violation") || ex.then.toLowerCase().includes("skip") || ex.then.toLowerCase().includes("block") || ex.then.toLowerCase().includes("no "))
5364
+ ) || d.contract?.behavior.toLowerCase().includes("block") || d.contract?.behavior.toLowerCase().includes("prevent")
5365
+ );
5366
+ if (preventingDescriptor) {
5367
+ return {
5368
+ condition,
5369
+ status: "satisfied",
5370
+ explanation: `Constraint/rule "${preventingDescriptor.id}" prevents: ${condition.description}`,
5371
+ relatedRules: relatedIds
5372
+ };
5373
+ }
5374
+ return {
5375
+ condition,
5376
+ status: "unverifiable",
5377
+ explanation: `No rule or constraint explicitly prevents: ${condition.description}`,
5378
+ relatedRules: relatedIds
5379
+ };
5380
+ }
5381
+ case "always": {
5382
+ const guaranteeing = relatedDescriptors.find(
5383
+ (d) => d.contract?.invariants.some((inv) => matches(inv)) || d.contract?.behavior && matches(d.contract.behavior)
5384
+ );
5385
+ if (guaranteeing) {
5386
+ return {
5387
+ condition,
5388
+ status: "satisfied",
5389
+ explanation: `Rule "${guaranteeing.id}" guarantees: ${condition.description}`,
5390
+ relatedRules: relatedIds
5391
+ };
5392
+ }
5393
+ const exampleMatch = relatedDescriptors.find(
5394
+ (d) => d.contract?.examples.some((ex) => matches(ex.then))
5395
+ );
5396
+ if (exampleMatch) {
5397
+ return {
5398
+ condition,
5399
+ status: "satisfied",
5400
+ explanation: `Rule "${exampleMatch.id}" example demonstrates: ${condition.description}`,
5401
+ relatedRules: relatedIds
5402
+ };
5403
+ }
5404
+ return {
5405
+ condition,
5406
+ status: "unverifiable",
5407
+ explanation: `No rule contract guarantees: ${condition.description}`,
5408
+ relatedRules: relatedIds
5409
+ };
5410
+ }
5411
+ }
5412
+ }
5413
+ function findRelatedDescriptors(expectationName, descriptors) {
5414
+ const nameLower = expectationName.toLowerCase();
5415
+ const nameParts = nameLower.split(/[-_./\s]+/);
5416
+ return descriptors.filter((d) => {
5417
+ const idLower = d.id.toLowerCase();
5418
+ const descLower = d.description.toLowerCase();
5419
+ const behaviorLower = d.contract?.behavior?.toLowerCase() ?? "";
5420
+ if (idLower.includes(nameLower) || nameLower.includes(idLower)) return true;
5421
+ if (descLower.includes(nameLower)) return true;
5422
+ if (behaviorLower.includes(nameLower)) return true;
5423
+ const minParts = Math.min(2, nameParts.length);
5424
+ const matchingParts = nameParts.filter(
5425
+ (part) => part.length > 2 && (idLower.includes(part) || descLower.includes(part) || behaviorLower.includes(part))
5426
+ );
5427
+ if (matchingParts.length >= minParts) return true;
5428
+ if (d.eventTypes) {
5429
+ const eventStr = Array.isArray(d.eventTypes) ? d.eventTypes.join(" ") : d.eventTypes;
5430
+ if (eventStr.toLowerCase().includes(nameLower)) return true;
5431
+ }
5432
+ return false;
5433
+ });
5434
+ }
5435
+ function formatVerificationReport(report) {
5436
+ const lines = [];
5437
+ const icon = report.status === "satisfied" ? "\u2705" : report.status === "partial" ? "\u{1F7E1}" : "\u{1F534}";
5438
+ lines.push(`${icon} Expectations: ${report.setName} \u2014 ${report.status.toUpperCase()}`);
5439
+ lines.push(` ${report.summary.satisfied}/${report.summary.total} satisfied, ${report.summary.violated} violated, ${report.summary.partial} partial`);
5440
+ lines.push("");
5441
+ for (const exp of report.expectations) {
5442
+ const expIcon = exp.status === "satisfied" ? "\u2705" : exp.status === "partial" ? "\u{1F7E1}" : "\u{1F534}";
5443
+ lines.push(`${expIcon} ${exp.name}`);
5444
+ for (const cond of exp.conditions) {
5445
+ const condIcon = cond.status === "satisfied" ? " \u2713" : cond.status === "violated" ? " \u2717" : " ?";
5446
+ lines.push(`${condIcon} [${cond.condition.type}] ${cond.condition.description}`);
5447
+ lines.push(` ${cond.explanation}`);
5448
+ }
5449
+ if (exp.edgeCases.length > 0) {
5450
+ lines.push(" Edge cases:");
5451
+ for (const ec of exp.edgeCases) {
5452
+ lines.push(` \u26A0\uFE0F ${ec}`);
5453
+ }
5454
+ }
5455
+ lines.push("");
5456
+ }
5457
+ if (report.allMitigations.length > 0) {
5458
+ lines.push("Suggested mitigations:");
5459
+ for (const m of report.allMitigations) {
5460
+ lines.push(` \u{1F4A1} ${m}`);
5461
+ }
5462
+ }
5463
+ return lines.join("\n");
5464
+ }
5465
+
5466
+ // src/factory/factory.ts
5467
+ init_rule_result();
5468
+ var SANITIZE_PATTERNS = {
5469
+ "sql-injection": /('|"|;|--|\/\*|\*\/|xp_|exec\s|union\s+select|drop\s+table|insert\s+into|delete\s+from)/i,
5470
+ "xss": /(<script|javascript:|on\w+\s*=|<iframe|<object|<embed|<img[^>]+onerror)/i,
5471
+ "path-traversal": /(\.\.[/\\]|~\/|\/etc\/|\/proc\/)/i,
5472
+ "command-injection": /([;&|`$]|\$\(|>\s*\/)/i
5473
+ };
5474
+ function inputRules(config = {}) {
5475
+ const {
5476
+ sanitize = [],
5477
+ maxLength = 0,
5478
+ required = false,
5479
+ fieldName = "input"
5480
+ } = config;
5481
+ const rules = [];
5482
+ const constraints = [];
5483
+ if (sanitize.length > 0) {
5484
+ rules.push({
5485
+ id: `factory/input.sanitize-${fieldName}`,
5486
+ description: `Validates ${fieldName} against ${sanitize.join(", ")} patterns`,
5487
+ eventTypes: [`${fieldName}.submit`, `${fieldName}.change`],
5488
+ contract: {
5489
+ ruleId: `factory/input.sanitize-${fieldName}`,
5490
+ behavior: `Checks ${fieldName} for dangerous patterns: ${sanitize.join(", ")}`,
5491
+ examples: [
5492
+ { given: `${fieldName} contains safe text`, when: "input submitted", then: "input.valid emitted" },
5493
+ { given: `${fieldName} contains <script> tag`, when: "input submitted", then: "input.violation emitted" }
5494
+ ],
5495
+ invariants: [
5496
+ `Dangerous ${fieldName} patterns must never pass validation`,
5497
+ "All violations must include the violation type"
5498
+ ]
5499
+ },
5500
+ impl: (state2, events) => {
5501
+ const inputEvent = events.find(
5502
+ (e) => e.tag === `${fieldName}.submit` || e.tag === `${fieldName}.change`
5503
+ );
5504
+ if (!inputEvent) return RuleResult.skip("No input event");
5505
+ const value = inputEvent.payload?.value ?? state2.context.input?.value ?? "";
5506
+ const violations = [];
5507
+ for (const type of sanitize) {
5508
+ const pattern = SANITIZE_PATTERNS[type];
5509
+ if (pattern && pattern.test(value)) {
5510
+ violations.push(type);
5511
+ }
5512
+ }
5513
+ if (violations.length > 0) {
5514
+ return RuleResult.emit([
5515
+ fact(`${fieldName}.violation`, {
5516
+ field: fieldName,
5517
+ violations,
5518
+ message: `Input failed sanitization: ${violations.join(", ")}`
5519
+ })
5520
+ ]);
5521
+ }
5522
+ return RuleResult.emit([
5523
+ fact(`${fieldName}.valid`, { field: fieldName, sanitized: true })
5524
+ ]);
5525
+ }
5526
+ });
5527
+ }
5528
+ if (maxLength > 0) {
5529
+ constraints.push({
5530
+ id: `factory/input.max-length-${fieldName}`,
5531
+ description: `${fieldName} must not exceed ${maxLength} characters`,
5532
+ contract: {
5533
+ ruleId: `factory/input.max-length-${fieldName}`,
5534
+ behavior: `Enforces max length of ${maxLength} for ${fieldName}`,
5535
+ examples: [
5536
+ { given: `${fieldName} is 10 chars`, when: `maxLength is ${maxLength}`, then: maxLength >= 10 ? "passes" : "violation" }
5537
+ ],
5538
+ invariants: [`${fieldName} length must never exceed ${maxLength}`]
5539
+ },
5540
+ impl: (state2) => {
5541
+ const value = state2.context.input?.value ?? "";
5542
+ if (value.length > maxLength) {
5543
+ return `${fieldName} exceeds maximum length of ${maxLength} (got ${value.length})`;
5544
+ }
5545
+ return true;
5546
+ }
5547
+ });
5548
+ }
5549
+ if (required) {
5550
+ constraints.push({
5551
+ id: `factory/input.required-${fieldName}`,
5552
+ description: `${fieldName} is required and must not be empty`,
5553
+ contract: {
5554
+ ruleId: `factory/input.required-${fieldName}`,
5555
+ behavior: `Enforces that ${fieldName} is non-empty`,
5556
+ examples: [
5557
+ { given: `${fieldName} is "hello"`, when: "checked", then: "passes" },
5558
+ { given: `${fieldName} is empty`, when: "checked", then: "violation" }
5559
+ ],
5560
+ invariants: [`${fieldName} must never be empty when required`]
5561
+ },
5562
+ impl: (state2) => {
5563
+ const value = state2.context.input?.value ?? "";
5564
+ if (value.trim().length === 0) {
5565
+ return `${fieldName} is required but empty`;
5566
+ }
5567
+ return true;
5568
+ }
5569
+ });
5570
+ }
5571
+ return { rules, constraints };
5572
+ }
5573
+ function toastRules(config = {}) {
5574
+ const {
5575
+ requireDiff = false,
5576
+ autoDismissMs = 0,
5577
+ deduplicate = false
5578
+ } = config;
5579
+ const rules = [];
5580
+ const constraints = [];
5581
+ rules.push({
5582
+ id: "factory/toast.show",
5583
+ description: "Emits toast notification with content and config",
5584
+ eventTypes: ["toast.request"],
5585
+ contract: {
5586
+ ruleId: "factory/toast.show",
5587
+ behavior: "Shows toast when requested, respecting diff requirement and auto-dismiss",
5588
+ examples: [
5589
+ { given: "toast requested with message", when: "toast.request fires", then: "toast.show emitted" },
5590
+ ...requireDiff ? [{ given: "no diff present", when: "toast.request fires", then: "toast skipped" }] : []
5591
+ ],
5592
+ invariants: [
5593
+ "Toast message must be non-empty",
5594
+ ...requireDiff ? ["Toast must not appear when diff is empty"] : []
5595
+ ]
5596
+ },
5597
+ impl: (state2, events) => {
5598
+ const toastEvent = events.find((e) => e.tag === "toast.request");
5599
+ if (!toastEvent) return RuleResult.skip("No toast request");
5600
+ const payload = toastEvent.payload;
5601
+ const message = payload.message ?? "";
5602
+ if (!message) return RuleResult.skip("Empty toast message");
5603
+ if (requireDiff) {
5604
+ const diff = state2.context.diff;
5605
+ if (!diff || Object.keys(diff).length === 0) {
5606
+ return RuleResult.skip("No diff \u2014 toast suppressed");
5607
+ }
5608
+ }
5609
+ return RuleResult.emit([
5610
+ fact("toast.show", {
5611
+ message,
5612
+ type: payload.type ?? "info",
5613
+ autoDismissMs: autoDismissMs > 0 ? autoDismissMs : void 0,
5614
+ timestamp: Date.now()
5615
+ })
5616
+ ]);
5617
+ }
5618
+ });
5619
+ if (deduplicate) {
5620
+ constraints.push({
5621
+ id: "factory/toast.no-duplicates",
5622
+ description: "Prevents duplicate toast messages",
5623
+ contract: {
5624
+ ruleId: "factory/toast.no-duplicates",
5625
+ behavior: "Rejects toast if identical message is already showing",
5626
+ examples: [
5627
+ { given: "same toast already visible", when: "duplicate toast requested", then: "violation" }
5628
+ ],
5629
+ invariants: ["No two toasts may have the same message simultaneously"]
5630
+ },
5631
+ impl: (state2) => {
5632
+ const toasts = state2.context.toasts ?? [];
5633
+ const messages = toasts.map((t) => t.message);
5634
+ const uniqueMessages = new Set(messages);
5635
+ if (uniqueMessages.size < messages.length) {
5636
+ return "Duplicate toast detected";
5637
+ }
5638
+ return true;
5639
+ }
5640
+ });
5641
+ }
5642
+ return { rules, constraints };
5643
+ }
5644
+ function formRules(config = {}) {
5645
+ const {
5646
+ validateOnBlur = false,
5647
+ submitGate = false,
5648
+ formName = "form"
5649
+ } = config;
5650
+ const rules = [];
5651
+ const constraints = [];
5652
+ if (validateOnBlur) {
5653
+ rules.push({
5654
+ id: `factory/${formName}.validate-on-blur`,
5655
+ description: `Triggers field validation when a ${formName} field loses focus`,
5656
+ eventTypes: [`${formName}.blur`],
5657
+ contract: {
5658
+ ruleId: `factory/${formName}.validate-on-blur`,
5659
+ behavior: `Validates the blurred field and emits validation result`,
5660
+ examples: [
5661
+ { given: `${formName} field has value`, when: "field loses focus", then: "validation result emitted" }
5662
+ ],
5663
+ invariants: ["Validation must run for every blur event on a registered field"]
5664
+ },
5665
+ impl: (_state, events) => {
5666
+ const blurEvent = events.find((e) => e.tag === `${formName}.blur`);
5667
+ if (!blurEvent) return RuleResult.skip("No blur event");
5668
+ const payload = blurEvent.payload;
5669
+ const field = payload.field ?? "unknown";
5670
+ const value = payload.value;
5671
+ const valid = value !== null && value !== void 0 && value !== "";
5672
+ return RuleResult.emit([
5673
+ fact(`${formName}.field-validated`, {
5674
+ field,
5675
+ valid,
5676
+ error: valid ? null : `${field} is required`
5677
+ })
5678
+ ]);
5679
+ }
5680
+ });
5681
+ }
5682
+ if (submitGate) {
5683
+ constraints.push({
5684
+ id: `factory/${formName}.submit-gate`,
5685
+ description: `Prevents ${formName} submission when validation has not passed`,
5686
+ contract: {
5687
+ ruleId: `factory/${formName}.submit-gate`,
5688
+ behavior: `Blocks form submission until all fields are valid`,
5689
+ examples: [
5690
+ { given: `${formName} is invalid`, when: "submit attempted", then: "violation \u2014 submission blocked" },
5691
+ { given: `${formName} is valid`, when: "submit attempted", then: "passes" }
5692
+ ],
5693
+ invariants: ["Form must not submit while any field has errors"]
5694
+ },
5695
+ impl: (state2) => {
5696
+ const form = state2.context.form;
5697
+ if (!form) return true;
5698
+ if (form.submitting && !form.valid) {
5699
+ return `${formName} cannot submit: validation has not passed`;
5700
+ }
5701
+ return true;
5702
+ }
5703
+ });
5704
+ }
5705
+ rules.push({
5706
+ id: `factory/${formName}.dirty-tracking`,
5707
+ description: `Tracks whether ${formName} has unsaved changes`,
5708
+ eventTypes: [`${formName}.change`, `${formName}.reset`],
5709
+ contract: {
5710
+ ruleId: `factory/${formName}.dirty-tracking`,
5711
+ behavior: "Emits dirty state when form fields change, clears on reset",
5712
+ examples: [
5713
+ { given: "field value changed", when: "form.change fires", then: "form.dirty emitted" },
5714
+ { given: "form reset", when: "form.reset fires", then: "form.dirty retracted" }
5715
+ ],
5716
+ invariants: ["Dirty state must reflect actual field changes"]
5717
+ },
5718
+ impl: (_state, events) => {
5719
+ const resetEvent = events.find((e) => e.tag === `${formName}.reset`);
5720
+ if (resetEvent) {
5721
+ return RuleResult.retract([`${formName}.dirty`], "Form reset");
5722
+ }
5723
+ const changeEvent = events.find((e) => e.tag === `${formName}.change`);
5724
+ if (changeEvent) {
5725
+ return RuleResult.emit([
5726
+ fact(`${formName}.dirty`, { dirty: true })
5727
+ ]);
5728
+ }
5729
+ return RuleResult.skip("No form event");
5730
+ }
5731
+ });
5732
+ return { rules, constraints };
5733
+ }
5734
+ function navigationRules(config = {}) {
5735
+ const {
5736
+ dirtyGuard = false,
5737
+ authRequired = false
5738
+ } = config;
5739
+ const rules = [];
5740
+ const constraints = [];
5741
+ rules.push({
5742
+ id: "factory/navigation.handle",
5743
+ description: "Processes navigation requests and emits navigation facts",
5744
+ eventTypes: ["navigation.request"],
5745
+ contract: {
5746
+ ruleId: "factory/navigation.handle",
5747
+ behavior: "Emits navigation.allowed or navigation.blocked based on guards",
5748
+ examples: [
5749
+ { given: "no guards active", when: "navigation requested", then: "navigation.allowed emitted" },
5750
+ ...dirtyGuard ? [{ given: "form is dirty", when: "navigation requested", then: "navigation.blocked emitted" }] : [],
5751
+ ...authRequired ? [{ given: "user not authenticated", when: "navigation requested", then: "navigation.blocked emitted" }] : []
5752
+ ],
5753
+ invariants: [
5754
+ "Every navigation request must result in either allowed or blocked",
5755
+ ...dirtyGuard ? ["Navigation must be blocked when dirty data exists"] : [],
5756
+ ...authRequired ? ["Navigation must be blocked when not authenticated"] : []
5757
+ ]
5758
+ },
5759
+ impl: (state2, events) => {
5760
+ const navEvent = events.find((e) => e.tag === "navigation.request");
5761
+ if (!navEvent) return RuleResult.skip("No navigation request");
5762
+ const target = navEvent.payload?.target ?? "/";
5763
+ const reasons = [];
5764
+ if (dirtyGuard && state2.context.dirty) {
5765
+ reasons.push("Unsaved changes will be lost");
5766
+ }
5767
+ if (authRequired && !state2.context.authenticated) {
5768
+ reasons.push("Authentication required");
5769
+ }
5770
+ if (reasons.length > 0) {
5771
+ return RuleResult.emit([
5772
+ fact("navigation.blocked", { target, reasons })
5773
+ ]);
5774
+ }
5775
+ return RuleResult.emit([
5776
+ fact("navigation.allowed", { target })
5777
+ ]);
5778
+ }
5779
+ });
5780
+ if (dirtyGuard) {
5781
+ constraints.push({
5782
+ id: "factory/navigation.dirty-guard",
5783
+ description: "Prevents silent navigation when unsaved changes exist",
5784
+ contract: {
5785
+ ruleId: "factory/navigation.dirty-guard",
5786
+ behavior: "Blocks navigation when dirty state is true",
5787
+ examples: [
5788
+ { given: "dirty is true", when: "navigation attempted", then: "violation" },
5789
+ { given: "dirty is false", when: "navigation attempted", then: "passes" }
5790
+ ],
5791
+ invariants: ["Must never silently lose unsaved changes"]
5792
+ },
5793
+ impl: (state2) => {
5794
+ if (state2.context.dirty && state2.facts.some((f) => f.tag === "navigation.allowed")) {
5795
+ return "Navigation allowed while dirty \u2014 unsaved changes may be lost";
5796
+ }
5797
+ return true;
5798
+ }
5799
+ });
5800
+ }
5801
+ return { rules, constraints };
5802
+ }
5803
+ function dataRules(config = {}) {
5804
+ const {
5805
+ optimisticUpdate = false,
5806
+ rollbackOnError = false,
5807
+ cacheInvalidation = false,
5808
+ entityName = "data"
5809
+ } = config;
5810
+ const rules = [];
5811
+ const constraints = [];
5812
+ if (optimisticUpdate) {
5813
+ rules.push({
5814
+ id: `factory/${entityName}.optimistic-update`,
5815
+ description: `Applies optimistic update for ${entityName} while request is pending`,
5816
+ eventTypes: [`${entityName}.mutate`],
5817
+ contract: {
5818
+ ruleId: `factory/${entityName}.optimistic-update`,
5819
+ behavior: `Immediately emits updated ${entityName} state before server confirmation`,
5820
+ examples: [
5821
+ { given: `${entityName} mutation requested`, when: "mutate event fires", then: "optimistic state emitted" }
5822
+ ],
5823
+ invariants: [
5824
+ "Optimistic state must store original for rollback",
5825
+ "Optimistic update must be distinguishable from confirmed state"
5826
+ ]
5827
+ },
5828
+ impl: (_state, events) => {
5829
+ const mutateEvent = events.find((e) => e.tag === `${entityName}.mutate`);
5830
+ if (!mutateEvent) return RuleResult.skip("No mutation event");
5831
+ const payload = mutateEvent.payload;
5832
+ return RuleResult.emit([
5833
+ fact(`${entityName}.optimistic`, {
5834
+ id: payload.id,
5835
+ data: payload.data,
5836
+ pending: true,
5837
+ timestamp: Date.now()
5838
+ })
5839
+ ]);
5840
+ }
5841
+ });
5842
+ }
5843
+ if (rollbackOnError) {
5844
+ rules.push({
5845
+ id: `factory/${entityName}.rollback`,
5846
+ description: `Rolls back optimistic ${entityName} update on error`,
5847
+ eventTypes: [`${entityName}.error`],
5848
+ contract: {
5849
+ ruleId: `factory/${entityName}.rollback`,
5850
+ behavior: `Reverts to original ${entityName} state when mutation fails`,
5851
+ examples: [
5852
+ { given: "optimistic update was applied", when: "server returns error", then: "rollback emitted, optimistic retracted" }
5853
+ ],
5854
+ invariants: [
5855
+ "Rollback must restore original state exactly",
5856
+ "Optimistic facts must be retracted on rollback"
5857
+ ]
5858
+ },
5859
+ impl: (_state, events) => {
5860
+ const errorEvent = events.find((e) => e.tag === `${entityName}.error`);
5861
+ if (!errorEvent) return RuleResult.skip("No error event");
5862
+ const payload = errorEvent.payload;
5863
+ const result = RuleResult.emit([
5864
+ fact(`${entityName}.rollback`, {
5865
+ id: payload.id,
5866
+ error: payload.error,
5867
+ timestamp: Date.now()
5868
+ })
5869
+ ]);
5870
+ return result;
5871
+ }
5872
+ });
5873
+ }
5874
+ if (cacheInvalidation) {
5875
+ rules.push({
5876
+ id: `factory/${entityName}.cache-invalidate`,
5877
+ description: `Invalidates ${entityName} cache when data changes are confirmed`,
5878
+ eventTypes: [`${entityName}.confirmed`, `${entityName}.deleted`],
5879
+ contract: {
5880
+ ruleId: `factory/${entityName}.cache-invalidate`,
5881
+ behavior: `Emits cache invalidation signal when ${entityName} is confirmed or deleted`,
5882
+ examples: [
5883
+ { given: `${entityName} mutation confirmed`, when: "confirmed event fires", then: "cache.invalidate emitted" }
5884
+ ],
5885
+ invariants: ["Stale cache entries must be invalidated after confirmed mutations"]
5886
+ },
5887
+ impl: (_state, events) => {
5888
+ const confirmEvent = events.find(
5889
+ (e) => e.tag === `${entityName}.confirmed` || e.tag === `${entityName}.deleted`
5890
+ );
5891
+ if (!confirmEvent) return RuleResult.skip("No confirmation event");
5892
+ const payload = confirmEvent.payload;
5893
+ return RuleResult.emit([
5894
+ fact(`${entityName}.cache-invalidate`, {
5895
+ id: payload.id,
5896
+ timestamp: Date.now()
5897
+ })
5898
+ ]);
5899
+ }
5900
+ });
5901
+ }
5902
+ constraints.push({
5903
+ id: `factory/${entityName}.integrity`,
5904
+ description: `Ensures ${entityName} state integrity \u2014 no orphaned optimistic updates`,
5905
+ contract: {
5906
+ ruleId: `factory/${entityName}.integrity`,
5907
+ behavior: "Detects orphaned optimistic updates without pending confirmation",
5908
+ examples: [
5909
+ { given: "optimistic update exists without pending request", when: "checked", then: "violation" }
5910
+ ],
5911
+ invariants: [`Every optimistic ${entityName} update must have a corresponding pending request`]
5912
+ },
5913
+ impl: (state2) => {
5914
+ const pending = state2.context.pending ?? {};
5915
+ const optimisticFacts = state2.facts.filter((f) => f.tag === `${entityName}.optimistic`);
5916
+ for (const optFact of optimisticFacts) {
5917
+ const id = optFact.payload?.id;
5918
+ if (id && !pending[id]) {
5919
+ return `Orphaned optimistic update for ${entityName} id=${id} \u2014 no pending request`;
5920
+ }
5921
+ }
5922
+ return true;
5923
+ }
5924
+ });
5925
+ return { rules, constraints };
5926
+ }
5927
+
5928
+ // src/project/project.ts
5929
+ init_rule_result();
5930
+ function defineGate(name, config) {
5931
+ const { expects, onSatisfied, onViolation } = config;
5932
+ const rule = {
5933
+ id: `gate/${name}`,
5934
+ description: `Feature gate: ${name} \u2014 requires: ${expects.join(", ")}`,
5935
+ eventTypes: ["gate.check", `gate.${name}.check`],
5936
+ contract: {
5937
+ ruleId: `gate/${name}`,
5938
+ behavior: `Opens gate "${name}" when all expectations are met: ${expects.join(", ")}`,
5939
+ examples: [
5940
+ {
5941
+ given: `all expectations satisfied: ${expects.join(", ")}`,
5942
+ when: "gate checked",
5943
+ then: `gate.${name}.open emitted${onSatisfied ? ` \u2192 ${onSatisfied}` : ""}`
5944
+ },
5945
+ {
5946
+ given: "one or more expectations unsatisfied",
5947
+ when: "gate checked",
5948
+ then: `gate.${name}.blocked emitted${onViolation ? ` \u2192 ${onViolation}` : ""}`
5949
+ }
5950
+ ],
5951
+ invariants: [
5952
+ `Gate "${name}" must never open with unsatisfied expectations`,
5953
+ "Gate status must reflect current expectation state exactly"
5954
+ ]
5955
+ },
5956
+ impl: (state2, events) => {
5957
+ const gateEvent = events.find(
5958
+ (e) => e.tag === "gate.check" || e.tag === `gate.${name}.check`
5959
+ );
5960
+ if (!gateEvent) return RuleResult.skip("No gate check event");
5961
+ const expectationState = state2.context.expectations ?? {};
5962
+ const satisfied = [];
5963
+ const unsatisfied = [];
5964
+ for (const exp of expects) {
5965
+ if (expectationState[exp]) {
5966
+ satisfied.push(exp);
5967
+ } else {
5968
+ unsatisfied.push(exp);
5969
+ }
5970
+ }
5971
+ const status = unsatisfied.length === 0 ? "open" : "blocked";
5972
+ const gateState = {
5973
+ name,
5974
+ status,
5975
+ satisfied,
5976
+ unsatisfied,
5977
+ lastChanged: Date.now()
5978
+ };
5979
+ const facts = [fact(`gate.${name}.status`, gateState)];
5980
+ if (status === "open" && onSatisfied) {
5981
+ facts.push(fact(`gate.${name}.action`, { action: onSatisfied }));
5982
+ } else if (status === "blocked" && onViolation) {
5983
+ facts.push(fact(`gate.${name}.action`, { action: onViolation }));
5984
+ }
5985
+ return RuleResult.emit(facts);
5986
+ }
5987
+ };
5988
+ const constraint = {
5989
+ id: `gate/${name}/integrity`,
5990
+ description: `Ensures gate "${name}" status matches expectation reality`,
5991
+ contract: {
5992
+ ruleId: `gate/${name}/integrity`,
5993
+ behavior: `Validates that gate "${name}" is not open when expectations are unmet`,
5994
+ examples: [
5995
+ {
5996
+ given: `gate ${name} is open but expectations unmet`,
5997
+ when: "constraint checked",
5998
+ then: "violation"
5999
+ }
6000
+ ],
6001
+ invariants: [`Gate "${name}" must never report open when expectations are unsatisfied`]
6002
+ },
6003
+ impl: (state2) => {
6004
+ const gateState = state2.context.gates?.[name];
6005
+ if (!gateState) return true;
6006
+ if (gateState.status === "open" && gateState.unsatisfied.length > 0) {
6007
+ return `Gate "${name}" is open but has unsatisfied expectations: ${gateState.unsatisfied.join(", ")}`;
6008
+ }
6009
+ return true;
6010
+ }
6011
+ };
6012
+ return { rules: [rule], constraints: [constraint] };
6013
+ }
6014
+ function semverContract(config) {
6015
+ const { sources, invariants } = config;
6016
+ const rule = {
6017
+ id: "project/semver-check",
6018
+ description: `Checks version consistency across: ${sources.join(", ")}`,
6019
+ eventTypes: ["project.version-check"],
6020
+ contract: {
6021
+ ruleId: "project/semver-check",
6022
+ behavior: `Verifies version consistency across ${sources.length} sources`,
6023
+ examples: [
6024
+ {
6025
+ given: "all sources have version 1.2.3",
6026
+ when: "version check runs",
6027
+ then: "semver.consistent emitted"
6028
+ },
6029
+ {
6030
+ given: "package.json has 1.2.3 but README has 1.2.2",
6031
+ when: "version check runs",
6032
+ then: "semver.inconsistent emitted with diff"
6033
+ }
6034
+ ],
6035
+ invariants: invariants.length > 0 ? invariants : ["All version sources must report the same semver string"]
6036
+ },
6037
+ impl: (_state, events) => {
6038
+ const checkEvent = events.find((e) => e.tag === "project.version-check");
6039
+ if (!checkEvent) return RuleResult.skip("No version check event");
6040
+ const versions = checkEvent.payload?.versions ?? {};
6041
+ const versionValues = Object.values(versions);
6042
+ const unique = new Set(versionValues);
6043
+ if (unique.size <= 1) {
6044
+ return RuleResult.emit([
6045
+ fact("semver.consistent", {
6046
+ version: versionValues[0] ?? "unknown",
6047
+ sources: Object.keys(versions)
6048
+ })
6049
+ ]);
6050
+ }
6051
+ const report = {
6052
+ consistent: false,
6053
+ versions,
6054
+ violations: [`Version mismatch: ${JSON.stringify(versions)}`]
6055
+ };
6056
+ return RuleResult.emit([fact("semver.inconsistent", report)]);
6057
+ }
6058
+ };
6059
+ return { rules: [rule], constraints: [] };
6060
+ }
6061
+ function commitFromState(diff) {
6062
+ const parts = [];
6063
+ const bodyParts = [];
6064
+ const totalAdded = diff.rulesAdded.length + diff.contractsAdded.length + diff.expectationsAdded.length;
6065
+ const totalRemoved = diff.rulesRemoved.length + diff.contractsRemoved.length + diff.expectationsRemoved.length;
6066
+ const totalModified = diff.rulesModified.length;
6067
+ const hasGateChanges = diff.gateChanges.length > 0;
6068
+ if (totalAdded > 0 && totalRemoved === 0 && totalModified === 0) {
6069
+ if (diff.rulesAdded.length > 0) {
6070
+ const scope = inferScope(diff.rulesAdded);
6071
+ parts.push(`feat(${scope}): add ${formatIds(diff.rulesAdded)}`);
6072
+ } else if (diff.contractsAdded.length > 0) {
6073
+ parts.push(`feat(contracts): add contracts for ${formatIds(diff.contractsAdded)}`);
6074
+ } else {
6075
+ parts.push(`feat(expectations): add ${formatIds(diff.expectationsAdded)}`);
6076
+ }
6077
+ } else if (totalRemoved > 0 && totalAdded === 0) {
6078
+ if (diff.rulesRemoved.length > 0) {
6079
+ const scope = inferScope(diff.rulesRemoved);
6080
+ parts.push(`refactor(${scope}): remove ${formatIds(diff.rulesRemoved)}`);
6081
+ } else {
6082
+ parts.push(`refactor: remove ${totalRemoved} item(s)`);
6083
+ }
6084
+ } else if (totalModified > 0) {
6085
+ const scope = inferScope(diff.rulesModified);
6086
+ parts.push(`refactor(${scope}): update ${formatIds(diff.rulesModified)}`);
6087
+ } else if (hasGateChanges) {
6088
+ const gateNames = diff.gateChanges.map((g) => g.gate);
6089
+ parts.push(`chore(gates): ${formatIds(gateNames)} state changed`);
6090
+ } else {
6091
+ parts.push("chore: behavioral state update");
6092
+ }
6093
+ if (diff.rulesAdded.length > 0) bodyParts.push(`Rules added: ${diff.rulesAdded.join(", ")}`);
6094
+ if (diff.rulesRemoved.length > 0) bodyParts.push(`Rules removed: ${diff.rulesRemoved.join(", ")}`);
6095
+ if (diff.rulesModified.length > 0) bodyParts.push(`Rules modified: ${diff.rulesModified.join(", ")}`);
6096
+ if (diff.contractsAdded.length > 0) bodyParts.push(`Contracts added: ${diff.contractsAdded.join(", ")}`);
6097
+ if (diff.contractsRemoved.length > 0) bodyParts.push(`Contracts removed: ${diff.contractsRemoved.join(", ")}`);
6098
+ if (diff.expectationsAdded.length > 0) bodyParts.push(`Expectations added: ${diff.expectationsAdded.join(", ")}`);
6099
+ if (diff.expectationsRemoved.length > 0) bodyParts.push(`Expectations removed: ${diff.expectationsRemoved.join(", ")}`);
6100
+ for (const gc of diff.gateChanges) {
6101
+ bodyParts.push(`Gate "${gc.gate}": ${gc.from} \u2192 ${gc.to}`);
6102
+ }
6103
+ const subject = parts[0] || "chore: update";
6104
+ return bodyParts.length > 0 ? `${subject}
6105
+
6106
+ ${bodyParts.join("\n")}` : subject;
6107
+ }
6108
+ function inferScope(ids) {
6109
+ if (ids.length === 0) return "rules";
6110
+ const prefixes = ids.map((id) => {
6111
+ const slash = id.indexOf("/");
6112
+ return slash > 0 ? id.slice(0, slash) : id;
6113
+ });
6114
+ const unique = new Set(prefixes);
6115
+ return unique.size === 1 ? prefixes[0] : "rules";
6116
+ }
6117
+ function formatIds(ids) {
6118
+ if (ids.length <= 3) return ids.join(", ");
6119
+ return `${ids.slice(0, 2).join(", ")} (+${ids.length - 2} more)`;
6120
+ }
6121
+ function branchRules(config) {
6122
+ const { naming, mergeConditions } = config;
6123
+ const namePattern = naming.replace("{name}", "(.+)").replace("{issue}", "(\\d+)");
6124
+ const nameRegex = new RegExp(`^${namePattern}$`);
6125
+ const rule = {
6126
+ id: "project/branch-check",
6127
+ description: `Validates branch naming (${naming}) and merge conditions`,
6128
+ eventTypes: ["project.branch-check"],
6129
+ contract: {
6130
+ ruleId: "project/branch-check",
6131
+ behavior: `Ensures branch follows "${naming}" pattern and merge conditions are met`,
6132
+ examples: [
6133
+ {
6134
+ given: `branch named "${naming.replace("{name}", "my-feature")}"`,
6135
+ when: "branch checked",
6136
+ then: "branch.valid emitted"
6137
+ },
6138
+ {
6139
+ given: 'branch named "random-name"',
6140
+ when: "branch checked",
6141
+ then: "branch.invalid emitted"
6142
+ }
6143
+ ],
6144
+ invariants: [
6145
+ `Branch names must follow pattern: ${naming}`,
6146
+ `Merge requires: ${mergeConditions.join(", ")}`
6147
+ ]
6148
+ },
6149
+ impl: (_state, events) => {
6150
+ const checkEvent = events.find((e) => e.tag === "project.branch-check");
6151
+ if (!checkEvent) return RuleResult.skip("No branch check event");
6152
+ const payload = checkEvent.payload;
6153
+ const branch = payload.branch ?? "";
6154
+ const conditions = payload.conditions ?? {};
6155
+ const validName = nameRegex.test(branch);
6156
+ const unmetConditions = mergeConditions.filter((c) => !conditions[c]);
6157
+ if (validName && unmetConditions.length === 0) {
6158
+ return RuleResult.emit([
6159
+ fact("branch.valid", { branch, mergeReady: true })
6160
+ ]);
6161
+ }
6162
+ const reasons = [];
6163
+ if (!validName) reasons.push(`Branch name "${branch}" doesn't match pattern "${naming}"`);
6164
+ if (unmetConditions.length > 0) reasons.push(`Unmet merge conditions: ${unmetConditions.join(", ")}`);
6165
+ return RuleResult.emit([
6166
+ fact("branch.invalid", { branch, reasons, mergeReady: false })
6167
+ ]);
6168
+ }
6169
+ };
6170
+ return { rules: [rule], constraints: [] };
6171
+ }
6172
+ function lintGate(config = {}) {
6173
+ const expects = ["lint-passes", ...config.additionalExpects ?? []];
6174
+ return defineGate("lint", {
6175
+ expects,
6176
+ onSatisfied: "lint-passed",
6177
+ onViolation: "lint-failed"
6178
+ });
6179
+ }
6180
+ function formatGate(config = {}) {
6181
+ const expects = ["format-passes", ...config.additionalExpects ?? []];
6182
+ return defineGate("format", {
6183
+ expects,
6184
+ onSatisfied: "format-passed",
6185
+ onViolation: "format-failed"
6186
+ });
6187
+ }
6188
+ function expectationGate(config = {}) {
6189
+ const expects = ["expectations-verified", ...config.additionalExpects ?? []];
6190
+ return defineGate("expectations", {
6191
+ expects,
6192
+ onSatisfied: "expectations-passed",
6193
+ onViolation: "expectations-failed"
6194
+ });
6195
+ }
4656
6196
  // Annotate the CommonJS export names for ESM import in node:
4657
6197
  0 && (module.exports = {
4658
6198
  AcknowledgeContractGap,
@@ -4665,6 +6205,8 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4665
6205
  ContractMissing,
4666
6206
  ContractUpdated,
4667
6207
  ContractValidated,
6208
+ Expectation,
6209
+ ExpectationSet,
4668
6210
  FrameworkAgnosticReactiveEngine,
4669
6211
  InMemoryPraxisDB,
4670
6212
  LogicEngine,
@@ -4678,6 +6220,7 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4678
6220
  PraxisSchemaRegistry,
4679
6221
  ReactiveLogicEngine,
4680
6222
  RegistryIntrospector,
6223
+ RuleResult,
4681
6224
  StateDocsGenerator,
4682
6225
  TerminalAdapter,
4683
6226
  ValidateContracts,
@@ -4685,9 +6228,12 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4685
6228
  attachTauriToEngine,
4686
6229
  attachToEngine,
4687
6230
  attachUnumToEngine,
6231
+ auditCompleteness,
6232
+ branchRules,
4688
6233
  canvasToMermaid,
4689
6234
  canvasToSchema,
4690
6235
  canvasToYaml,
6236
+ commitFromState,
4691
6237
  createBehaviorLedger,
4692
6238
  createCanvasEditor,
4693
6239
  createChronicle,
@@ -4710,21 +6256,33 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4710
6256
  createTauriPraxisAdapter,
4711
6257
  createTerminalAdapter,
4712
6258
  createTimerActor,
6259
+ createUIModule,
4713
6260
  createUnifiedApp,
4714
6261
  createUnumAdapter,
6262
+ dataRules,
4715
6263
  defineConstraint,
4716
6264
  defineContract,
4717
6265
  defineEvent,
4718
6266
  defineFact,
6267
+ defineGate,
4719
6268
  defineModule,
4720
6269
  defineRule,
6270
+ dirtyGuardRule,
6271
+ errorDisplayRule,
6272
+ expectBehavior,
6273
+ expectationGate,
6274
+ fact,
4721
6275
  filterEvents,
4722
6276
  filterFacts,
4723
6277
  findEvent,
4724
6278
  findFact,
6279
+ formRules,
6280
+ formatGate,
6281
+ formatReport,
4725
6282
  formatValidationReport,
4726
6283
  formatValidationReportJSON,
4727
6284
  formatValidationReportSARIF,
6285
+ formatVerificationReport,
4728
6286
  generateDocs,
4729
6287
  generateId,
4730
6288
  generateTauriConfig,
@@ -4732,16 +6290,32 @@ async function attachAllIntegrations(engine, registry, options = {}) {
4732
6290
  getEventPath,
4733
6291
  getFactPath,
4734
6292
  getSchemaPath,
6293
+ initGateRule,
6294
+ inputRules,
4735
6295
  isContract,
6296
+ lintGate,
4736
6297
  loadSchema,
4737
6298
  loadSchemaFromFile,
4738
6299
  loadSchemaFromJson,
4739
6300
  loadSchemaFromYaml,
6301
+ loadingGateRule,
6302
+ mustBeInitializedConstraint,
6303
+ navigationRequest,
6304
+ navigationRules,
6305
+ noInteractionWhileLoadingConstraint,
6306
+ offlineIndicatorRule,
4740
6307
  registerSchema,
6308
+ resizeEvent,
4741
6309
  runTerminalCommand,
4742
6310
  schemaToCanvas,
6311
+ semverContract,
6312
+ toastRules,
6313
+ uiModule,
6314
+ uiStateChanged,
4743
6315
  validateContracts,
4744
6316
  validateForGeneration,
4745
6317
  validateSchema,
4746
- validateWithGuardian
6318
+ validateWithGuardian,
6319
+ verify,
6320
+ viewportRule
4747
6321
  });