@plures/praxis 1.3.0 → 1.4.4

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 (74) hide show
  1. package/dist/browser/{chunk-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
  2. package/dist/browser/chunk-6SJ44Q64.js +473 -0
  3. package/dist/browser/chunk-BQOYZBWA.js +282 -0
  4. package/dist/browser/chunk-IG5BJ2MT.js +91 -0
  5. package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
  6. package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
  7. package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
  8. package/dist/browser/expectations/index.d.ts +180 -0
  9. package/dist/browser/expectations/index.js +14 -0
  10. package/dist/browser/factory/index.d.ts +149 -0
  11. package/dist/browser/factory/index.js +15 -0
  12. package/dist/browser/index.d.ts +274 -3
  13. package/dist/browser/index.js +407 -54
  14. package/dist/browser/integrations/svelte.d.ts +3 -2
  15. package/dist/browser/integrations/svelte.js +3 -2
  16. package/dist/browser/project/index.d.ts +176 -0
  17. package/dist/browser/project/index.js +19 -0
  18. package/dist/browser/reactive-engine.svelte-DgVTqHLc.d.ts +223 -0
  19. package/dist/browser/{reactive-engine.svelte-DjynI82A.d.ts → rules-i1LHpnGd.d.ts} +13 -221
  20. package/dist/node/chunk-2IUFZBH3.js +87 -0
  21. package/dist/node/{chunk-WZ6B3LZ6.js → chunk-7CSWBDFL.js} +3 -56
  22. package/dist/node/chunk-AZLNISFI.js +1690 -0
  23. package/dist/node/chunk-IG5BJ2MT.js +91 -0
  24. package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
  25. package/dist/node/chunk-PGVSB6NR.js +59 -0
  26. package/dist/node/{chunk-5JQJZADT.js → chunk-ZO2LU4G4.js} +4 -4
  27. package/dist/node/cli/index.cjs +1126 -211
  28. package/dist/node/cli/index.js +21 -2
  29. package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
  30. package/dist/node/index.cjs +5623 -2765
  31. package/dist/node/index.d.cts +1181 -1
  32. package/dist/node/index.d.ts +1181 -1
  33. package/dist/node/index.js +1646 -79
  34. package/dist/node/integrations/svelte.js +4 -3
  35. package/dist/node/{reverse-W7THPV45.js → reverse-YD3CWIGM.js} +3 -2
  36. package/dist/node/rules-4DAJ4Z4N.js +7 -0
  37. package/dist/node/server-FKLVY57V.js +363 -0
  38. package/dist/node/{validate-EN3M4FUR.js → validate-5PSWJTIC.js} +5 -3
  39. package/package.json +50 -3
  40. package/src/__tests__/chronos-project.test.ts +799 -0
  41. package/src/__tests__/decision-ledger.test.ts +857 -402
  42. package/src/__tests__/expectations.test.ts +364 -0
  43. package/src/__tests__/factory.test.ts +426 -0
  44. package/src/__tests__/mcp-server.test.ts +310 -0
  45. package/src/__tests__/project.test.ts +396 -0
  46. package/src/chronos/diff.ts +336 -0
  47. package/src/chronos/hooks.ts +227 -0
  48. package/src/chronos/index.ts +83 -0
  49. package/src/chronos/project-chronicle.ts +198 -0
  50. package/src/chronos/timeline.ts +152 -0
  51. package/src/cli/index.ts +28 -0
  52. package/src/decision-ledger/analyzer-types.ts +280 -0
  53. package/src/decision-ledger/analyzer.ts +518 -0
  54. package/src/decision-ledger/contract-verification.ts +456 -0
  55. package/src/decision-ledger/derivation.ts +158 -0
  56. package/src/decision-ledger/index.ts +59 -0
  57. package/src/decision-ledger/report.ts +378 -0
  58. package/src/decision-ledger/suggestions.ts +287 -0
  59. package/src/expectations/expectations.ts +471 -0
  60. package/src/expectations/index.ts +29 -0
  61. package/src/expectations/types.ts +95 -0
  62. package/src/factory/factory.ts +634 -0
  63. package/src/factory/index.ts +27 -0
  64. package/src/factory/types.ts +64 -0
  65. package/src/index.browser.ts +83 -0
  66. package/src/index.ts +134 -0
  67. package/src/mcp/index.ts +33 -0
  68. package/src/mcp/server.ts +485 -0
  69. package/src/mcp/types.ts +161 -0
  70. package/src/project/index.ts +31 -0
  71. package/src/project/project.ts +423 -0
  72. package/src/project/types.ts +87 -0
  73. package/dist/node/chunk-PTH6MD6P.js +0 -487
  74. /package/dist/node/{chunk-R2PSBPKQ.js → chunk-TEMFJOIH.js} +0 -0
@@ -0,0 +1,1690 @@
1
+ import {
2
+ getContractFromDescriptor
3
+ } from "./chunk-PGVSB6NR.js";
4
+ import {
5
+ RuleResult
6
+ } from "./chunk-IG5BJ2MT.js";
7
+
8
+ // src/dsl/index.ts
9
+ function defineFact(tag) {
10
+ return {
11
+ tag,
12
+ create(payload) {
13
+ return { tag, payload };
14
+ },
15
+ is(fact) {
16
+ return fact.tag === tag;
17
+ }
18
+ };
19
+ }
20
+ function defineEvent(tag) {
21
+ return {
22
+ tag,
23
+ create(payload) {
24
+ return { tag, payload };
25
+ },
26
+ is(event) {
27
+ return event.tag === tag;
28
+ }
29
+ };
30
+ }
31
+ function defineRule(options) {
32
+ const contract = options.contract ?? options.meta?.contract;
33
+ const meta = contract ? { ...options.meta ?? {}, contract } : options.meta;
34
+ return {
35
+ id: options.id,
36
+ description: options.description,
37
+ impl: options.impl,
38
+ eventTypes: options.eventTypes,
39
+ contract,
40
+ meta
41
+ };
42
+ }
43
+ function defineConstraint(options) {
44
+ const contract = options.contract ?? options.meta?.contract;
45
+ const meta = contract ? { ...options.meta ?? {}, contract } : options.meta;
46
+ return {
47
+ id: options.id,
48
+ description: options.description,
49
+ impl: options.impl,
50
+ contract,
51
+ meta
52
+ };
53
+ }
54
+ function defineModule(options) {
55
+ return {
56
+ rules: options.rules ?? [],
57
+ constraints: options.constraints ?? [],
58
+ meta: options.meta
59
+ };
60
+ }
61
+ function filterEvents(events, definition) {
62
+ return events.filter(definition.is);
63
+ }
64
+ function filterFacts(facts, definition) {
65
+ return facts.filter(definition.is);
66
+ }
67
+ function findEvent(events, definition) {
68
+ return events.find(definition.is);
69
+ }
70
+ function findFact(facts, definition) {
71
+ return facts.find(definition.is);
72
+ }
73
+
74
+ // src/decision-ledger/facts-events.ts
75
+ var ContractMissing = defineFact("ContractMissing");
76
+ var ContractValidated = defineFact("ContractValidated");
77
+ var AcknowledgeContractGap = defineEvent("ACKNOWLEDGE_CONTRACT_GAP");
78
+ var ValidateContracts = defineEvent("VALIDATE_CONTRACTS");
79
+ var ContractGapAcknowledged = defineFact("ContractGapAcknowledged");
80
+ var ContractAdded = defineEvent("CONTRACT_ADDED");
81
+ var ContractUpdated = defineEvent("CONTRACT_UPDATED");
82
+ var ContractGapEmitted = defineEvent("CONTRACT_GAP_EMITTED");
83
+
84
+ // src/decision-ledger/validation.ts
85
+ function validateContracts(registry, options = {}) {
86
+ const {
87
+ incompleteSeverity = "warning",
88
+ requiredFields = ["behavior", "examples"],
89
+ artifactIndex
90
+ } = options;
91
+ const complete = [];
92
+ const incomplete = [];
93
+ const missing = [];
94
+ for (const rule of registry.getAllRules()) {
95
+ const contract = getContractFromDescriptor(rule);
96
+ if (!contract) {
97
+ missing.push(rule.id);
98
+ if (options.missingSeverity) {
99
+ incomplete.push({
100
+ ruleId: rule.id,
101
+ missing: ["contract"],
102
+ severity: options.missingSeverity,
103
+ message: `Rule '${rule.id}' has no contract`
104
+ });
105
+ }
106
+ continue;
107
+ }
108
+ const gaps = validateContract(contract, requiredFields, artifactIndex);
109
+ if (gaps.length > 0) {
110
+ incomplete.push({
111
+ ruleId: rule.id,
112
+ missing: gaps,
113
+ severity: incompleteSeverity,
114
+ message: `Rule '${rule.id}' contract is incomplete: missing ${gaps.join(", ")}`
115
+ });
116
+ } else {
117
+ complete.push({ ruleId: rule.id, contract });
118
+ }
119
+ }
120
+ for (const constraint of registry.getAllConstraints()) {
121
+ const contract = getContractFromDescriptor(constraint);
122
+ if (!contract) {
123
+ missing.push(constraint.id);
124
+ if (options.missingSeverity) {
125
+ incomplete.push({
126
+ ruleId: constraint.id,
127
+ missing: ["contract"],
128
+ severity: options.missingSeverity,
129
+ message: `Constraint '${constraint.id}' has no contract`
130
+ });
131
+ }
132
+ continue;
133
+ }
134
+ const gaps = validateContract(contract, requiredFields, artifactIndex);
135
+ if (gaps.length > 0) {
136
+ incomplete.push({
137
+ ruleId: constraint.id,
138
+ missing: gaps,
139
+ severity: incompleteSeverity,
140
+ message: `Constraint '${constraint.id}' contract is incomplete: missing ${gaps.join(", ")}`
141
+ });
142
+ } else {
143
+ complete.push({ ruleId: constraint.id, contract });
144
+ }
145
+ }
146
+ const total = registry.getAllRules().length + registry.getAllConstraints().length;
147
+ return {
148
+ complete,
149
+ incomplete,
150
+ missing,
151
+ total,
152
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
153
+ };
154
+ }
155
+ function validateContract(contract, requiredFields, artifactIndex) {
156
+ const missing = [];
157
+ if (requiredFields.includes("behavior") && isFieldEmpty(contract.behavior)) {
158
+ missing.push("behavior");
159
+ }
160
+ if (requiredFields.includes("examples") && (!contract.examples || contract.examples.length === 0)) {
161
+ missing.push("examples");
162
+ }
163
+ if (requiredFields.includes("invariants") && (!contract.invariants || contract.invariants.length === 0)) {
164
+ missing.push("invariants");
165
+ }
166
+ if (artifactIndex?.tests && !artifactIndex.tests.has(contract.ruleId)) {
167
+ missing.push("tests");
168
+ }
169
+ if (artifactIndex?.spec && !artifactIndex.spec.has(contract.ruleId)) {
170
+ missing.push("spec");
171
+ }
172
+ return missing;
173
+ }
174
+ function isFieldEmpty(value) {
175
+ return !value || value.trim() === "";
176
+ }
177
+ function formatValidationReport(report) {
178
+ const lines = [];
179
+ lines.push("Contract Validation Report");
180
+ lines.push("=".repeat(50));
181
+ lines.push("");
182
+ lines.push(`Total: ${report.total}`);
183
+ lines.push(`Complete: ${report.complete.length}`);
184
+ lines.push(`Incomplete: ${report.incomplete.length}`);
185
+ lines.push(`Missing: ${report.missing.length}`);
186
+ lines.push("");
187
+ if (report.complete.length > 0) {
188
+ lines.push("\u2713 Complete Contracts:");
189
+ for (const { ruleId, contract } of report.complete) {
190
+ lines.push(` \u2713 ${ruleId} (v${contract.version || "1.0.0"})`);
191
+ }
192
+ lines.push("");
193
+ }
194
+ if (report.incomplete.length > 0) {
195
+ lines.push("\u2717 Incomplete Contracts:");
196
+ for (const gap of report.incomplete) {
197
+ const icon = gap.severity === "error" ? "\u2717" : gap.severity === "warning" ? "\u26A0" : "\u2139";
198
+ lines.push(` ${icon} ${gap.ruleId} - Missing: ${gap.missing.join(", ")}`);
199
+ if (gap.message) {
200
+ lines.push(` ${gap.message}`);
201
+ }
202
+ }
203
+ lines.push("");
204
+ }
205
+ if (report.missing.length > 0) {
206
+ lines.push("\u2717 No Contract:");
207
+ for (const ruleId of report.missing) {
208
+ lines.push(` \u2717 ${ruleId}`);
209
+ }
210
+ lines.push("");
211
+ }
212
+ lines.push(`Validated at: ${report.timestamp}`);
213
+ return lines.join("\n");
214
+ }
215
+ function formatValidationReportJSON(report) {
216
+ return JSON.stringify(report, null, 2);
217
+ }
218
+ function formatValidationReportSARIF(report) {
219
+ const results = report.incomplete.map((gap) => {
220
+ const primaryMissing = gap.missing.length > 0 ? gap.missing[0] : "contract";
221
+ return {
222
+ ruleId: `decision-ledger/${primaryMissing}`,
223
+ level: gap.severity === "error" ? "error" : gap.severity === "warning" ? "warning" : "note",
224
+ message: {
225
+ text: gap.message || `Missing: ${gap.missing.join(", ")}`
226
+ },
227
+ locations: [
228
+ {
229
+ physicalLocation: {
230
+ artifactLocation: {
231
+ uri: "registry"
232
+ },
233
+ region: {
234
+ startLine: 1
235
+ }
236
+ }
237
+ }
238
+ ],
239
+ properties: {
240
+ ruleId: gap.ruleId,
241
+ missing: gap.missing
242
+ }
243
+ };
244
+ });
245
+ const sarif = {
246
+ version: "2.1.0",
247
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
248
+ runs: [
249
+ {
250
+ tool: {
251
+ driver: {
252
+ name: "Praxis Decision Ledger",
253
+ version: "1.0.0",
254
+ informationUri: "https://github.com/plures/praxis",
255
+ rules: [
256
+ {
257
+ id: "decision-ledger/contract",
258
+ shortDescription: {
259
+ text: "Rule or constraint missing contract"
260
+ }
261
+ },
262
+ {
263
+ id: "decision-ledger/behavior",
264
+ shortDescription: {
265
+ text: "Contract missing behavior description"
266
+ }
267
+ },
268
+ {
269
+ id: "decision-ledger/examples",
270
+ shortDescription: {
271
+ text: "Contract missing examples"
272
+ }
273
+ },
274
+ {
275
+ id: "decision-ledger/invariants",
276
+ shortDescription: {
277
+ text: "Contract missing invariants"
278
+ }
279
+ },
280
+ {
281
+ id: "decision-ledger/tests",
282
+ shortDescription: {
283
+ text: "Contract missing tests"
284
+ }
285
+ },
286
+ {
287
+ id: "decision-ledger/spec",
288
+ shortDescription: {
289
+ text: "Contract missing spec"
290
+ }
291
+ }
292
+ ]
293
+ }
294
+ },
295
+ results
296
+ }
297
+ ]
298
+ };
299
+ return JSON.stringify(sarif, null, 2);
300
+ }
301
+
302
+ // src/decision-ledger/ledger.ts
303
+ var BehaviorLedger = class _BehaviorLedger {
304
+ entries = [];
305
+ entryMap = /* @__PURE__ */ new Map();
306
+ /**
307
+ * Append a new entry to the ledger.
308
+ *
309
+ * @param entry The entry to append
310
+ * @throws Error if entry ID already exists
311
+ */
312
+ append(entry) {
313
+ if (this.entryMap.has(entry.id)) {
314
+ throw new Error(`Ledger entry with ID '${entry.id}' already exists`);
315
+ }
316
+ if (entry.supersedes) {
317
+ const superseded = this.entryMap.get(entry.supersedes);
318
+ if (superseded && superseded.status === "active") {
319
+ const updatedEntry = {
320
+ ...superseded,
321
+ status: "superseded"
322
+ };
323
+ this.entryMap.set(entry.supersedes, updatedEntry);
324
+ }
325
+ }
326
+ this.entries.push(entry);
327
+ this.entryMap.set(entry.id, entry);
328
+ }
329
+ /**
330
+ * Get an entry by ID.
331
+ *
332
+ * @param id The entry ID
333
+ * @returns The entry, or undefined if not found
334
+ */
335
+ getEntry(id) {
336
+ return this.entryMap.get(id);
337
+ }
338
+ /**
339
+ * Get all entries (in order of append) with current status.
340
+ *
341
+ * @returns Array of all entries with current status from the map
342
+ */
343
+ getAllEntries() {
344
+ return this.entries.map((entry) => this.entryMap.get(entry.id));
345
+ }
346
+ /**
347
+ * Get entries for a specific rule ID.
348
+ *
349
+ * @param ruleId The rule ID
350
+ * @returns Array of entries for this rule with current status
351
+ */
352
+ getEntriesForRule(ruleId) {
353
+ return this.entries.map((entry) => this.entryMap.get(entry.id)).filter((entry) => entry.contract.ruleId === ruleId);
354
+ }
355
+ /**
356
+ * Get the latest active entry for a rule.
357
+ *
358
+ * @param ruleId The rule ID
359
+ * @returns The latest active entry, or undefined if none
360
+ */
361
+ getLatestEntry(ruleId) {
362
+ const entries = this.getEntriesForRule(ruleId);
363
+ const activeEntries = entries.filter((entry) => entry.status === "active");
364
+ if (activeEntries.length === 0) {
365
+ return void 0;
366
+ }
367
+ return activeEntries[activeEntries.length - 1];
368
+ }
369
+ /**
370
+ * Get all active assumptions across all entries.
371
+ *
372
+ * @returns Map of assumption ID to assumption
373
+ */
374
+ getActiveAssumptions() {
375
+ const assumptions = /* @__PURE__ */ new Map();
376
+ for (const entry of this.entries) {
377
+ const currentEntry = this.entryMap.get(entry.id);
378
+ if (currentEntry.status !== "active") {
379
+ continue;
380
+ }
381
+ for (const assumption of currentEntry.contract.assumptions || []) {
382
+ if (assumption.status === "active") {
383
+ assumptions.set(assumption.id, assumption);
384
+ }
385
+ }
386
+ }
387
+ return assumptions;
388
+ }
389
+ /**
390
+ * Find assumptions that impact a specific artifact type.
391
+ *
392
+ * @param impactType The artifact type ('spec', 'tests', 'code')
393
+ * @returns Array of assumptions
394
+ */
395
+ findAssumptionsByImpact(impactType) {
396
+ const assumptions = [];
397
+ for (const entry of this.entries) {
398
+ const currentEntry = this.entryMap.get(entry.id);
399
+ if (currentEntry.status !== "active") {
400
+ continue;
401
+ }
402
+ for (const assumption of currentEntry.contract.assumptions || []) {
403
+ if (assumption.status === "active" && assumption.impacts.includes(impactType)) {
404
+ assumptions.push(assumption);
405
+ }
406
+ }
407
+ }
408
+ return assumptions;
409
+ }
410
+ /**
411
+ * Get ledger statistics.
412
+ */
413
+ getStats() {
414
+ const currentEntries = this.entries.map((e) => this.entryMap.get(e.id));
415
+ const active = currentEntries.filter((e) => e.status === "active").length;
416
+ const superseded = currentEntries.filter((e) => e.status === "superseded").length;
417
+ const deprecated = currentEntries.filter((e) => e.status === "deprecated").length;
418
+ const uniqueRules = new Set(currentEntries.map((e) => e.contract.ruleId)).size;
419
+ return {
420
+ totalEntries: this.entries.length,
421
+ activeEntries: active,
422
+ supersededEntries: superseded,
423
+ deprecatedEntries: deprecated,
424
+ uniqueRules
425
+ };
426
+ }
427
+ /**
428
+ * Export ledger as JSON.
429
+ *
430
+ * @returns JSON string with current entry status
431
+ */
432
+ toJSON() {
433
+ return JSON.stringify(
434
+ {
435
+ version: "1.0.0",
436
+ // Export entries with current status from the map
437
+ entries: this.entries.map((entry) => this.entryMap.get(entry.id)),
438
+ stats: this.getStats()
439
+ },
440
+ null,
441
+ 2
442
+ );
443
+ }
444
+ /**
445
+ * Import ledger from JSON.
446
+ *
447
+ * Note: The JSON must contain entries in the order they were originally appended.
448
+ * If a superseding entry appears before the entry it supersedes, the superseding
449
+ * logic will not work correctly. The toJSON method preserves this order.
450
+ *
451
+ * @param json The JSON string
452
+ * @returns A new BehaviorLedger instance
453
+ */
454
+ static fromJSON(json) {
455
+ const data = JSON.parse(json);
456
+ const ledger = new _BehaviorLedger();
457
+ for (const entry of data.entries || []) {
458
+ ledger.append(entry);
459
+ }
460
+ return ledger;
461
+ }
462
+ };
463
+ function createBehaviorLedger() {
464
+ return new BehaviorLedger();
465
+ }
466
+
467
+ // src/decision-ledger/analyzer.ts
468
+ function analyzeDependencyGraph(registry) {
469
+ const facts = /* @__PURE__ */ new Map();
470
+ const edges = [];
471
+ const producers = /* @__PURE__ */ new Map();
472
+ const consumers = /* @__PURE__ */ new Map();
473
+ const rules = registry.getAllRules();
474
+ for (const rule of rules) {
475
+ const ruleId = rule.id;
476
+ const produced = [];
477
+ const consumed = [];
478
+ const probeFacts = probeRuleExecution(rule);
479
+ for (const tag of probeFacts.produced) {
480
+ produced.push(tag);
481
+ }
482
+ for (const tag of probeFacts.consumed) {
483
+ consumed.push(tag);
484
+ }
485
+ if (rule.contract) {
486
+ for (const example of rule.contract.examples) {
487
+ const thenTags = extractFactTagsFromText(example.then);
488
+ for (const tag of thenTags) {
489
+ if (!produced.includes(tag)) produced.push(tag);
490
+ }
491
+ const givenTags = extractFactTagsFromText(example.given);
492
+ for (const tag of givenTags) {
493
+ if (!consumed.includes(tag)) consumed.push(tag);
494
+ }
495
+ }
496
+ }
497
+ producers.set(ruleId, produced);
498
+ consumers.set(ruleId, consumed);
499
+ for (const tag of produced) {
500
+ const node = getOrCreateFactNode(facts, tag);
501
+ if (!node.producedBy.includes(ruleId)) {
502
+ node.producedBy.push(ruleId);
503
+ }
504
+ edges.push({ from: ruleId, to: tag, type: "produces" });
505
+ }
506
+ for (const tag of consumed) {
507
+ const node = getOrCreateFactNode(facts, tag);
508
+ if (!node.consumedBy.includes(ruleId)) {
509
+ node.consumedBy.push(ruleId);
510
+ }
511
+ edges.push({ from: tag, to: ruleId, type: "consumes" });
512
+ }
513
+ }
514
+ return { facts, edges, producers, consumers };
515
+ }
516
+ function findDeadRules(registry, knownEventTypes) {
517
+ const dead = [];
518
+ const known = new Set(knownEventTypes);
519
+ const rules = registry.getAllRules();
520
+ for (const rule of rules) {
521
+ if (!rule.eventTypes) continue;
522
+ const required = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
523
+ const hasMatch = required.some((t) => known.has(t));
524
+ if (!hasMatch) {
525
+ dead.push({
526
+ ruleId: rule.id,
527
+ description: rule.description,
528
+ requiredEventTypes: required,
529
+ reason: `Rule requires event types [${required.join(", ")}] but none are in the known event types [${knownEventTypes.join(", ")}]`
530
+ });
531
+ }
532
+ }
533
+ return dead;
534
+ }
535
+ function findUnreachableStates(registry) {
536
+ const graph = analyzeDependencyGraph(registry);
537
+ const unreachable = [];
538
+ for (const [tag, node] of graph.facts) {
539
+ if (node.producedBy.length === 0 && node.consumedBy.length > 0) {
540
+ unreachable.push({
541
+ factTags: [tag],
542
+ reason: `Fact "${tag}" is consumed by rules [${node.consumedBy.join(", ")}] but never produced by any rule`
543
+ });
544
+ }
545
+ }
546
+ const allProducedTags = Array.from(graph.facts.keys()).filter(
547
+ (tag) => graph.facts.get(tag).producedBy.length > 0
548
+ );
549
+ for (let i = 0; i < allProducedTags.length; i++) {
550
+ for (let j = i + 1; j < allProducedTags.length; j++) {
551
+ const tagA = allProducedTags[i];
552
+ const tagB = allProducedTags[j];
553
+ const producersA = graph.facts.get(tagA).producedBy;
554
+ const producersB = graph.facts.get(tagB).producedBy;
555
+ const sharedProducer = producersA.find((p) => producersB.includes(p));
556
+ if (sharedProducer) continue;
557
+ const rules = registry.getAllRules();
558
+ const getEventTypes = (ruleId) => {
559
+ const rule = rules.find((r) => r.id === ruleId);
560
+ if (!rule?.eventTypes) return [];
561
+ return Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
562
+ };
563
+ const eventTypesA = new Set(producersA.flatMap(getEventTypes));
564
+ const eventTypesB = new Set(producersB.flatMap(getEventTypes));
565
+ if (eventTypesA.size > 0 && eventTypesB.size > 0 && ![...eventTypesA].some((t) => eventTypesB.has(t))) {
566
+ const consumersA = graph.facts.get(tagA).consumedBy;
567
+ const consumersB = graph.facts.get(tagB).consumedBy;
568
+ const sharedConsumer = consumersA.find((c) => consumersB.includes(c));
569
+ if (sharedConsumer) {
570
+ unreachable.push({
571
+ factTags: [tagA, tagB],
572
+ reason: `Facts "${tagA}" and "${tagB}" are both consumed by rule "${sharedConsumer}" but are produced by rules with non-overlapping event types`
573
+ });
574
+ }
575
+ }
576
+ }
577
+ }
578
+ return unreachable;
579
+ }
580
+ function findShadowedRules(registry) {
581
+ const shadowed = [];
582
+ const graph = analyzeDependencyGraph(registry);
583
+ const rules = registry.getAllRules();
584
+ for (let i = 0; i < rules.length; i++) {
585
+ for (let j = 0; j < rules.length; j++) {
586
+ if (i === j) continue;
587
+ const ruleA = rules[i];
588
+ const ruleB = rules[j];
589
+ const typesA = normalizeEventTypes(ruleA.eventTypes);
590
+ const typesB = normalizeEventTypes(ruleB.eventTypes);
591
+ if (typesA.length === 0 || typesB.length === 0) continue;
592
+ const shared = typesA.filter((t) => typesB.includes(t));
593
+ if (shared.length === 0) continue;
594
+ const producedA = graph.producers.get(ruleA.id) ?? [];
595
+ const producedB = graph.producers.get(ruleB.id) ?? [];
596
+ if (producedA.length === 0) continue;
597
+ const isSuperset = producedA.every((tag) => producedB.includes(tag));
598
+ const isProperSuperset = isSuperset && producedB.length > producedA.length;
599
+ if (isProperSuperset) {
600
+ shadowed.push({
601
+ ruleId: ruleA.id,
602
+ shadowedBy: ruleB.id,
603
+ sharedEventTypes: shared,
604
+ reason: `Rule "${ruleB.id}" produces a superset of facts [${producedB.join(", ")}] compared to "${ruleA.id}" [${producedA.join(", ")}] for the same event types [${shared.join(", ")}]`
605
+ });
606
+ }
607
+ }
608
+ }
609
+ return shadowed;
610
+ }
611
+ function findContradictions(registry) {
612
+ const contradictions = [];
613
+ const graph = analyzeDependencyGraph(registry);
614
+ const rules = registry.getAllRules();
615
+ for (const [tag, node] of graph.facts) {
616
+ if (node.producedBy.length < 2) continue;
617
+ for (let i = 0; i < node.producedBy.length; i++) {
618
+ for (let j = i + 1; j < node.producedBy.length; j++) {
619
+ const ruleIdA = node.producedBy[i];
620
+ const ruleIdB = node.producedBy[j];
621
+ const ruleA = rules.find((r) => r.id === ruleIdA);
622
+ const ruleB = rules.find((r) => r.id === ruleIdB);
623
+ if (!ruleA || !ruleB) continue;
624
+ const typesA = normalizeEventTypes(ruleA.eventTypes);
625
+ const typesB = normalizeEventTypes(ruleB.eventTypes);
626
+ const bothCatchAll = typesA.length === 0 && typesB.length === 0;
627
+ const sharedTypes = typesA.filter((t) => typesB.includes(t));
628
+ const hasOverlap = sharedTypes.length > 0;
629
+ if (bothCatchAll || hasOverlap) {
630
+ const conflictDetail = checkContractConflict(ruleA, ruleB, tag);
631
+ if (conflictDetail || bothCatchAll || hasOverlap) {
632
+ contradictions.push({
633
+ ruleA: ruleIdA,
634
+ ruleB: ruleIdB,
635
+ conflictingTag: tag,
636
+ reason: conflictDetail ?? `Rules "${ruleIdA}" and "${ruleIdB}" both produce fact "${tag}" and respond to ${bothCatchAll ? "all events" : `event types [${sharedTypes.join(", ")}]`}`
637
+ });
638
+ }
639
+ }
640
+ }
641
+ }
642
+ }
643
+ return contradictions;
644
+ }
645
+ function findGaps(registry, expectations) {
646
+ const gaps = [];
647
+ const rules = registry.getAllRules();
648
+ const constraints = registry.getAllConstraints();
649
+ for (const exp of expectations.expectations) {
650
+ const nameLower = exp.name.toLowerCase();
651
+ const nameParts = nameLower.split(/[-_./\s]+/);
652
+ const related = rules.filter((r) => {
653
+ const idLower = r.id.toLowerCase();
654
+ const descLower = r.description.toLowerCase();
655
+ const behaviorLower = r.contract?.behavior?.toLowerCase() ?? "";
656
+ if (idLower.includes(nameLower) || nameLower.includes(idLower)) return true;
657
+ if (descLower.includes(nameLower) || behaviorLower.includes(nameLower)) return true;
658
+ const minParts = Math.min(2, nameParts.length);
659
+ const matches = nameParts.filter(
660
+ (part) => part.length > 2 && (idLower.includes(part) || descLower.includes(part))
661
+ );
662
+ return matches.length >= minParts;
663
+ });
664
+ const relatedConstraints = constraints.filter((c) => {
665
+ const idLower = c.id.toLowerCase();
666
+ const descLower = c.description.toLowerCase();
667
+ return idLower.includes(nameLower) || nameLower.includes(idLower) || descLower.includes(nameLower);
668
+ });
669
+ if (related.length === 0 && relatedConstraints.length === 0) {
670
+ gaps.push({
671
+ expectationName: exp.name,
672
+ description: `No rules or constraints found for expectation "${exp.name}"`,
673
+ partialCoverage: [],
674
+ type: "no-rule"
675
+ });
676
+ continue;
677
+ }
678
+ const uncoveredConditions = exp.conditions.filter((cond) => {
679
+ const condLower = cond.description.toLowerCase();
680
+ return !related.some(
681
+ (r) => r.contract?.examples.some(
682
+ (ex) => ex.given.toLowerCase().includes(condLower) || ex.when.toLowerCase().includes(condLower) || ex.then.toLowerCase().includes(condLower) || condLower.includes(ex.given.toLowerCase()) || condLower.includes(ex.when.toLowerCase())
683
+ ) || r.contract?.invariants.some(
684
+ (inv) => inv.toLowerCase().includes(condLower) || condLower.includes(inv.toLowerCase())
685
+ ) || r.contract?.behavior.toLowerCase().includes(condLower) || r.description.toLowerCase().includes(condLower)
686
+ );
687
+ });
688
+ if (uncoveredConditions.length > 0 && uncoveredConditions.length < exp.conditions.length) {
689
+ gaps.push({
690
+ expectationName: exp.name,
691
+ description: `Expectation "${exp.name}" is partially covered. Uncovered conditions: ${uncoveredConditions.map((c) => c.description).join("; ")}`,
692
+ partialCoverage: related.map((r) => r.id),
693
+ type: "partial-coverage"
694
+ });
695
+ } else if (uncoveredConditions.length === exp.conditions.length && exp.conditions.length > 0) {
696
+ const rulesWithoutContracts = related.filter((r) => !r.contract);
697
+ if (rulesWithoutContracts.length > 0) {
698
+ gaps.push({
699
+ expectationName: exp.name,
700
+ description: `Rules related to "${exp.name}" lack contracts: [${rulesWithoutContracts.map((r) => r.id).join(", ")}]`,
701
+ partialCoverage: related.map((r) => r.id),
702
+ type: "no-contract"
703
+ });
704
+ }
705
+ }
706
+ }
707
+ return gaps;
708
+ }
709
+ function getOrCreateFactNode(facts, tag) {
710
+ let node = facts.get(tag);
711
+ if (!node) {
712
+ node = { tag, producedBy: [], consumedBy: [] };
713
+ facts.set(tag, node);
714
+ }
715
+ return node;
716
+ }
717
+ function normalizeEventTypes(eventTypes) {
718
+ if (!eventTypes) return [];
719
+ return Array.isArray(eventTypes) ? eventTypes : [eventTypes];
720
+ }
721
+ function probeRuleExecution(rule) {
722
+ const produced = [];
723
+ const consumed = [];
724
+ const eventTypes = normalizeEventTypes(rule.eventTypes);
725
+ const syntheticEvents = eventTypes.length > 0 ? eventTypes.map((tag) => ({ tag, payload: {} })) : [{ tag: "__probe__", payload: {} }];
726
+ const syntheticState = {
727
+ context: {},
728
+ facts: [],
729
+ events: syntheticEvents,
730
+ meta: {},
731
+ protocolVersion: "1.0.0"
732
+ };
733
+ try {
734
+ const result = rule.impl(syntheticState, syntheticEvents);
735
+ if (result instanceof RuleResult) {
736
+ if (result.kind === "emit") {
737
+ for (const fact of result.facts) {
738
+ if (!produced.includes(fact.tag)) {
739
+ produced.push(fact.tag);
740
+ }
741
+ }
742
+ } else if (result.kind === "retract") {
743
+ for (const tag of result.retractTags) {
744
+ if (!consumed.includes(tag)) {
745
+ consumed.push(tag);
746
+ }
747
+ }
748
+ }
749
+ } else if (Array.isArray(result)) {
750
+ for (const fact of result) {
751
+ if (fact.tag && !produced.includes(fact.tag)) {
752
+ produced.push(fact.tag);
753
+ }
754
+ }
755
+ }
756
+ } catch {
757
+ }
758
+ return { produced, consumed };
759
+ }
760
+ function extractFactTagsFromText(text) {
761
+ const tags = [];
762
+ const patterns = [
763
+ /(?:emit|produce|retract|read|consume|fact)\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi,
764
+ /['"]([a-zA-Z][a-zA-Z0-9._-]*\.[a-zA-Z][a-zA-Z0-9._-]*)['"]?/g
765
+ ];
766
+ for (const pattern of patterns) {
767
+ let match;
768
+ while ((match = pattern.exec(text)) !== null) {
769
+ const tag = match[1];
770
+ if (tag && !tags.includes(tag)) {
771
+ tags.push(tag);
772
+ }
773
+ }
774
+ }
775
+ return tags;
776
+ }
777
+ function checkContractConflict(ruleA, ruleB, _factTag) {
778
+ if (!ruleA.contract || !ruleB.contract) return null;
779
+ for (const exA of ruleA.contract.examples) {
780
+ for (const exB of ruleB.contract.examples) {
781
+ const sameGiven = exA.given.toLowerCase() === exB.given.toLowerCase();
782
+ const sameWhen = exA.when.toLowerCase() === exB.when.toLowerCase();
783
+ const differentThen = exA.then.toLowerCase() !== exB.then.toLowerCase();
784
+ if ((sameGiven || sameWhen) && differentThen) {
785
+ return `Contract conflict: "${ruleA.id}" expects "${exA.then}" but "${ruleB.id}" expects "${exB.then}" under similar conditions`;
786
+ }
787
+ }
788
+ }
789
+ return null;
790
+ }
791
+
792
+ // src/decision-ledger/derivation.ts
793
+ function traceDerivation(factTag, _engine, registry) {
794
+ const graph = analyzeDependencyGraph(registry);
795
+ const steps = [];
796
+ const visited = /* @__PURE__ */ new Set();
797
+ function walkBackward(tag, depth) {
798
+ if (visited.has(tag) || depth > 20) return;
799
+ visited.add(tag);
800
+ const factNode = graph.facts.get(tag);
801
+ if (!factNode) {
802
+ steps.unshift({
803
+ type: "fact-produced",
804
+ id: tag,
805
+ description: `Fact "${tag}" (origin unknown \u2014 not in dependency graph)`
806
+ });
807
+ return;
808
+ }
809
+ steps.unshift({
810
+ type: "fact-produced",
811
+ id: tag,
812
+ description: `Fact "${tag}" produced`
813
+ });
814
+ for (const ruleId of factNode.producedBy) {
815
+ if (visited.has(ruleId)) continue;
816
+ visited.add(ruleId);
817
+ const rule = registry.getRule(ruleId);
818
+ steps.unshift({
819
+ type: "rule-fired",
820
+ id: ruleId,
821
+ description: `Rule "${ruleId}" fired${rule ? `: ${rule.description}` : ""}`
822
+ });
823
+ const consumed = graph.consumers.get(ruleId) ?? [];
824
+ for (const consumedTag of consumed) {
825
+ steps.unshift({
826
+ type: "fact-read",
827
+ id: consumedTag,
828
+ description: `Rule "${ruleId}" reads fact "${consumedTag}"`
829
+ });
830
+ walkBackward(consumedTag, depth + 1);
831
+ }
832
+ if (rule?.eventTypes) {
833
+ const types = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
834
+ for (const eventType of types) {
835
+ steps.unshift({
836
+ type: "event",
837
+ id: eventType,
838
+ description: `Event "${eventType}" triggers rule "${ruleId}"`
839
+ });
840
+ }
841
+ }
842
+ }
843
+ }
844
+ walkBackward(factTag, 0);
845
+ const seen = /* @__PURE__ */ new Set();
846
+ const dedupedSteps = steps.filter((step) => {
847
+ const key = `${step.type}:${step.id}`;
848
+ if (seen.has(key)) return false;
849
+ seen.add(key);
850
+ return true;
851
+ });
852
+ return {
853
+ targetFact: factTag,
854
+ steps: dedupedSteps,
855
+ depth: dedupedSteps.filter((s) => s.type === "rule-fired").length
856
+ };
857
+ }
858
+ function traceImpact(factTag, registry) {
859
+ const graph = analyzeDependencyGraph(registry);
860
+ const affectedRules = [];
861
+ const affectedFacts = [];
862
+ const visited = /* @__PURE__ */ new Set();
863
+ function walkForward(tag, depth2) {
864
+ if (visited.has(tag) || depth2 > 20) return depth2;
865
+ visited.add(tag);
866
+ const factNode = graph.facts.get(tag);
867
+ if (!factNode) return depth2;
868
+ let maxDepth = depth2;
869
+ for (const ruleId of factNode.consumedBy) {
870
+ if (!affectedRules.includes(ruleId)) {
871
+ affectedRules.push(ruleId);
872
+ }
873
+ const produced = graph.producers.get(ruleId) ?? [];
874
+ for (const producedTag of produced) {
875
+ if (producedTag !== factTag && !affectedFacts.includes(producedTag)) {
876
+ affectedFacts.push(producedTag);
877
+ const childDepth = walkForward(producedTag, depth2 + 1);
878
+ if (childDepth > maxDepth) maxDepth = childDepth;
879
+ }
880
+ }
881
+ }
882
+ return maxDepth;
883
+ }
884
+ const depth = walkForward(factTag, 0);
885
+ return {
886
+ factTag,
887
+ affectedRules,
888
+ affectedFacts,
889
+ depth
890
+ };
891
+ }
892
+
893
+ // src/decision-ledger/contract-verification.ts
894
+ function verifyContractExamples(rule, contract) {
895
+ const examples = [];
896
+ for (let i = 0; i < contract.examples.length; i++) {
897
+ const example = contract.examples[i];
898
+ try {
899
+ const state = buildStateFromDescription(example.given);
900
+ const events = buildEventsFromDescription(example.when, rule.eventTypes);
901
+ const stateWithEvents = { ...state, events };
902
+ const result = rule.impl(stateWithEvents, events);
903
+ const verification = verifyOutput(result, example.then, rule.id);
904
+ examples.push({
905
+ index: i,
906
+ given: example.given,
907
+ when: example.when,
908
+ expectedThen: example.then,
909
+ passed: verification.passed,
910
+ actualOutput: verification.actualOutput,
911
+ error: verification.error
912
+ });
913
+ } catch (error) {
914
+ examples.push({
915
+ index: i,
916
+ given: example.given,
917
+ when: example.when,
918
+ expectedThen: example.then,
919
+ passed: false,
920
+ error: `Execution error: ${error instanceof Error ? error.message : String(error)}`
921
+ });
922
+ }
923
+ }
924
+ const passCount = examples.filter((e) => e.passed).length;
925
+ const failCount = examples.filter((e) => !e.passed).length;
926
+ return {
927
+ ruleId: rule.id,
928
+ examples,
929
+ allPassed: failCount === 0,
930
+ passCount,
931
+ failCount
932
+ };
933
+ }
934
+ function verifyInvariants(registry) {
935
+ const checks = [];
936
+ const rules = registry.getAllRules();
937
+ for (const rule of rules) {
938
+ if (!rule.contract) continue;
939
+ for (const invariant of rule.contract.invariants) {
940
+ const consistent = rule.contract.examples.every((example) => {
941
+ return isConsistentWithInvariant(example, invariant);
942
+ });
943
+ checks.push({
944
+ invariant,
945
+ ruleId: rule.id,
946
+ holds: consistent,
947
+ explanation: consistent ? `Invariant "${invariant}" is consistent with all ${rule.contract.examples.length} examples` : `Invariant "${invariant}" may be violated by one or more examples`
948
+ });
949
+ }
950
+ }
951
+ return checks;
952
+ }
953
+ function findContractGaps(registry) {
954
+ const gaps = [];
955
+ const rules = registry.getAllRules();
956
+ for (const rule of rules) {
957
+ if (!rule.contract) continue;
958
+ const examples = rule.contract.examples;
959
+ const hasErrorExamples = examples.some(
960
+ (ex) => ex.then.toLowerCase().includes("error") || ex.then.toLowerCase().includes("fail") || ex.then.toLowerCase().includes("skip") || ex.then.toLowerCase().includes("noop") || ex.then.toLowerCase().includes("retract") || ex.then.toLowerCase().includes("violation")
961
+ );
962
+ if (!hasErrorExamples && examples.length > 0) {
963
+ gaps.push({
964
+ ruleId: rule.id,
965
+ description: `No error/failure path examples. All ${examples.length} examples show happy paths`,
966
+ type: "missing-error-path"
967
+ });
968
+ }
969
+ if (rule.eventTypes) {
970
+ const eventTypes = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
971
+ if (eventTypes.length > 1) {
972
+ const coveredTypes = /* @__PURE__ */ new Set();
973
+ for (const ex of examples) {
974
+ for (const et of eventTypes) {
975
+ if (ex.when.toLowerCase().includes(et.toLowerCase()) || ex.when.toLowerCase().includes(et.replace(".", " ").toLowerCase())) {
976
+ coveredTypes.add(et);
977
+ }
978
+ }
979
+ }
980
+ const uncovered = eventTypes.filter((et) => !coveredTypes.has(et));
981
+ if (uncovered.length > 0) {
982
+ gaps.push({
983
+ ruleId: rule.id,
984
+ description: `Event types [${uncovered.join(", ")}] not covered by any example`,
985
+ type: "missing-edge-case"
986
+ });
987
+ }
988
+ }
989
+ }
990
+ if (examples.length === 1) {
991
+ gaps.push({
992
+ ruleId: rule.id,
993
+ description: `Only 1 contract example \u2014 likely missing boundary conditions and edge cases`,
994
+ type: "missing-boundary"
995
+ });
996
+ }
997
+ }
998
+ return gaps;
999
+ }
1000
+ function crossReferenceContracts(registry) {
1001
+ const graph = analyzeDependencyGraph(registry);
1002
+ const references = [];
1003
+ const rules = registry.getAllRules();
1004
+ for (const rule of rules) {
1005
+ if (!rule.contract) continue;
1006
+ for (const example of rule.contract.examples) {
1007
+ const referencedTags = extractReferencedFactTags(example.given + " " + example.when);
1008
+ for (const tag of referencedTags) {
1009
+ const factNode = graph.facts.get(tag);
1010
+ const producerRuleId = factNode?.producedBy[0] ?? null;
1011
+ const valid = factNode ? factNode.producedBy.length > 0 : false;
1012
+ if (producerRuleId === rule.id) continue;
1013
+ references.push({
1014
+ sourceRuleId: rule.id,
1015
+ referencedFactTag: tag,
1016
+ producerRuleId,
1017
+ valid
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+ return references;
1023
+ }
1024
+ function buildStateFromDescription(given) {
1025
+ const context = {};
1026
+ const facts = [];
1027
+ const factPattern = /fact\s+['"]([^'"]+)['"]/gi;
1028
+ let match;
1029
+ while ((match = factPattern.exec(given)) !== null) {
1030
+ facts.push({ tag: match[1], payload: {} });
1031
+ }
1032
+ const dottedPattern = /['"]([a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9.]*)['"]/g;
1033
+ while ((match = dottedPattern.exec(given)) !== null) {
1034
+ const tag = match[1];
1035
+ if (!facts.some((f) => f.tag === tag)) {
1036
+ facts.push({ tag, payload: {} });
1037
+ }
1038
+ }
1039
+ return {
1040
+ context,
1041
+ facts,
1042
+ meta: {},
1043
+ protocolVersion: "1.0.0"
1044
+ };
1045
+ }
1046
+ function buildEventsFromDescription(when, eventTypes) {
1047
+ const events = [];
1048
+ if (eventTypes) {
1049
+ const types = Array.isArray(eventTypes) ? eventTypes : [eventTypes];
1050
+ for (const type of types) {
1051
+ if (when.toLowerCase().includes(type.toLowerCase()) || when.toLowerCase().includes(type.replace(".", " ").toLowerCase())) {
1052
+ events.push({ tag: type, payload: {} });
1053
+ }
1054
+ }
1055
+ }
1056
+ if (events.length === 0) {
1057
+ const eventPattern = /['"]([A-Z][A-Z0-9._-]+)['"]/g;
1058
+ let match;
1059
+ while ((match = eventPattern.exec(when)) !== null) {
1060
+ events.push({ tag: match[1], payload: {} });
1061
+ }
1062
+ }
1063
+ if (events.length === 0) {
1064
+ const types = eventTypes ? Array.isArray(eventTypes) ? eventTypes : [eventTypes] : ["__test__"];
1065
+ events.push({ tag: types[0], payload: {} });
1066
+ }
1067
+ return events;
1068
+ }
1069
+ function verifyOutput(result, expectedThen, ruleId) {
1070
+ const thenLower = expectedThen.toLowerCase();
1071
+ if (result instanceof RuleResult) {
1072
+ switch (result.kind) {
1073
+ case "emit": {
1074
+ const factTags = result.facts.map((f) => f.tag);
1075
+ const actualOutput = `Emitted: [${factTags.join(", ")}]`;
1076
+ const emitExpected = thenLower.includes("emit") || thenLower.includes("produce") || thenLower.includes("fact");
1077
+ if (emitExpected) {
1078
+ const expectedTags = extractReferencedFactTags(expectedThen);
1079
+ if (expectedTags.length > 0) {
1080
+ const allFound = expectedTags.every(
1081
+ (tag) => factTags.some((ft) => ft.toLowerCase() === tag.toLowerCase())
1082
+ );
1083
+ return { passed: allFound, actualOutput };
1084
+ }
1085
+ return { passed: true, actualOutput };
1086
+ }
1087
+ return { passed: true, actualOutput };
1088
+ }
1089
+ case "noop": {
1090
+ const expectNoop = thenLower.includes("noop") || thenLower.includes("nothing") || thenLower.includes("no fact") || thenLower.includes("skip");
1091
+ return {
1092
+ passed: expectNoop,
1093
+ actualOutput: `Noop: ${result.reason ?? "no reason"}`
1094
+ };
1095
+ }
1096
+ case "skip": {
1097
+ const expectSkip = thenLower.includes("skip") || thenLower.includes("noop") || thenLower.includes("not fire") || thenLower.includes("nothing");
1098
+ return {
1099
+ passed: expectSkip,
1100
+ actualOutput: `Skip: ${result.reason ?? "no reason"}`
1101
+ };
1102
+ }
1103
+ case "retract": {
1104
+ const expectRetract = thenLower.includes("retract") || thenLower.includes("remove") || thenLower.includes("clear");
1105
+ return {
1106
+ passed: expectRetract,
1107
+ actualOutput: `Retract: [${result.retractTags.join(", ")}]`
1108
+ };
1109
+ }
1110
+ }
1111
+ } else if (Array.isArray(result)) {
1112
+ const factTags = result.map((f) => f.tag);
1113
+ const actualOutput = `Facts: [${factTags.join(", ")}]`;
1114
+ return { passed: factTags.length > 0 || thenLower.includes("nothing"), actualOutput };
1115
+ }
1116
+ return { passed: false, error: `Unexpected result type from rule "${ruleId}"` };
1117
+ }
1118
+ function isConsistentWithInvariant(example, invariant) {
1119
+ const invLower = invariant.toLowerCase();
1120
+ const thenLower = example.then.toLowerCase();
1121
+ if (invLower.includes("must not") || invLower.includes("never")) {
1122
+ const keyword = invLower.replace(/must not|never|should not/g, "").trim().split(/\s+/)[0];
1123
+ if (keyword && thenLower.includes(keyword)) {
1124
+ return false;
1125
+ }
1126
+ }
1127
+ if (invLower.includes("must ") && !invLower.includes("must not")) {
1128
+ const mustPart = invLower.split("must ")[1]?.split(/[.,;]/)[0]?.trim();
1129
+ if (mustPart) {
1130
+ const negations = ["no ", "not ", "never ", "without "];
1131
+ for (const neg of negations) {
1132
+ if (thenLower.includes(neg + mustPart.split(/\s+/)[0])) {
1133
+ return false;
1134
+ }
1135
+ }
1136
+ }
1137
+ }
1138
+ return true;
1139
+ }
1140
+ function extractReferencedFactTags(text) {
1141
+ const tags = [];
1142
+ const patterns = [
1143
+ /['"]([a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9.]*)['"]/g,
1144
+ /fact\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi,
1145
+ /(?:emit|produce|retract)\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi
1146
+ ];
1147
+ for (const pattern of patterns) {
1148
+ let match;
1149
+ while ((match = pattern.exec(text)) !== null) {
1150
+ const tag = match[1];
1151
+ if (tag && !tags.includes(tag)) {
1152
+ tags.push(tag);
1153
+ }
1154
+ }
1155
+ }
1156
+ return tags;
1157
+ }
1158
+
1159
+ // src/decision-ledger/suggestions.ts
1160
+ function suggest(finding, type) {
1161
+ switch (type) {
1162
+ case "dead-rule":
1163
+ return suggestForDeadRule(finding);
1164
+ case "gap":
1165
+ return suggestForGap(finding);
1166
+ case "contradiction":
1167
+ return suggestForContradiction(finding);
1168
+ case "unreachable-state":
1169
+ return suggestForUnreachableState(finding);
1170
+ case "shadowed-rule":
1171
+ return suggestForShadowedRule(finding);
1172
+ case "contract-gap":
1173
+ return suggestForContractGap(finding);
1174
+ default:
1175
+ return {
1176
+ findingType: type,
1177
+ entityId: "unknown",
1178
+ message: "Unknown finding type",
1179
+ action: "modify",
1180
+ priority: 1
1181
+ };
1182
+ }
1183
+ }
1184
+ function suggestAll(findings) {
1185
+ const suggestions = [];
1186
+ if (findings.deadRules) {
1187
+ for (const f of findings.deadRules) {
1188
+ suggestions.push(suggestForDeadRule(f));
1189
+ }
1190
+ }
1191
+ if (findings.gaps) {
1192
+ for (const f of findings.gaps) {
1193
+ suggestions.push(suggestForGap(f));
1194
+ }
1195
+ }
1196
+ if (findings.contradictions) {
1197
+ for (const f of findings.contradictions) {
1198
+ suggestions.push(suggestForContradiction(f));
1199
+ }
1200
+ }
1201
+ if (findings.unreachableStates) {
1202
+ for (const f of findings.unreachableStates) {
1203
+ suggestions.push(suggestForUnreachableState(f));
1204
+ }
1205
+ }
1206
+ if (findings.shadowedRules) {
1207
+ for (const f of findings.shadowedRules) {
1208
+ suggestions.push(suggestForShadowedRule(f));
1209
+ }
1210
+ }
1211
+ if (findings.contractGaps) {
1212
+ for (const f of findings.contractGaps) {
1213
+ suggestions.push(suggestForContractGap(f));
1214
+ }
1215
+ }
1216
+ suggestions.sort((a, b) => b.priority - a.priority);
1217
+ return suggestions;
1218
+ }
1219
+ function suggestForDeadRule(finding) {
1220
+ const eventList = finding.requiredEventTypes.join(", ");
1221
+ return {
1222
+ findingType: "dead-rule",
1223
+ entityId: finding.ruleId,
1224
+ message: `Remove rule "${finding.ruleId}" or add event type "${finding.requiredEventTypes[0]}" to your event sources. Rule requires [${eventList}] but none are emitted by the application.`,
1225
+ action: finding.requiredEventTypes.length === 1 ? "add-event-type" : "remove",
1226
+ priority: 5,
1227
+ skeleton: `// Option 1: Remove the dead rule
1228
+ registry.removeRule('${finding.ruleId}');
1229
+
1230
+ // Option 2: Add the missing event type
1231
+ engine.step([{ tag: '${finding.requiredEventTypes[0]}', payload: {} }]);`
1232
+ };
1233
+ }
1234
+ function suggestForGap(finding) {
1235
+ const ruleId = finding.expectationName.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
1236
+ switch (finding.type) {
1237
+ case "no-rule":
1238
+ return {
1239
+ findingType: "gap",
1240
+ entityId: finding.expectationName,
1241
+ message: `Add a rule covering: "${finding.expectationName}". No rules or constraints address this expected behavior.`,
1242
+ action: "add-rule",
1243
+ priority: 8,
1244
+ skeleton: `defineRule({
1245
+ id: '${ruleId}',
1246
+ description: '${finding.description}',
1247
+ eventTypes: ['TODO_EVENT_TYPE'],
1248
+ impl: (state, events) => {
1249
+ // TODO: Implement logic for "${finding.expectationName}"
1250
+ return RuleResult.noop('Not implemented');
1251
+ },
1252
+ contract: defineContract({
1253
+ ruleId: '${ruleId}',
1254
+ behavior: '${finding.expectationName}',
1255
+ examples: [
1256
+ { given: 'TODO', when: 'TODO', then: 'TODO' },
1257
+ ],
1258
+ invariants: [],
1259
+ }),
1260
+ });`
1261
+ };
1262
+ case "partial-coverage":
1263
+ return {
1264
+ findingType: "gap",
1265
+ entityId: finding.expectationName,
1266
+ message: `Expectation "${finding.expectationName}" is partially covered by rules [${finding.partialCoverage.join(", ")}]. Add contract examples or additional rules for uncovered conditions.`,
1267
+ action: "modify",
1268
+ priority: 6
1269
+ };
1270
+ case "no-contract":
1271
+ return {
1272
+ findingType: "gap",
1273
+ entityId: finding.expectationName,
1274
+ message: `Rules related to "${finding.expectationName}" ([${finding.partialCoverage.join(", ")}]) lack contracts. Add contracts with examples covering the expected behavior.`,
1275
+ action: "add-contract",
1276
+ priority: 7
1277
+ };
1278
+ default:
1279
+ return {
1280
+ findingType: "gap",
1281
+ entityId: finding.expectationName,
1282
+ message: finding.description,
1283
+ action: "add-rule",
1284
+ priority: 5
1285
+ };
1286
+ }
1287
+ }
1288
+ function suggestForContradiction(finding) {
1289
+ return {
1290
+ findingType: "contradiction",
1291
+ entityId: `${finding.ruleA}\u2194${finding.ruleB}`,
1292
+ message: `Rules "${finding.ruleA}" and "${finding.ruleB}" both produce fact "${finding.conflictingTag}" with potentially different payloads. Add priority ordering, merge the rules, or add distinguishing conditions.`,
1293
+ action: "add-priority",
1294
+ priority: 9,
1295
+ skeleton: `// Option 1: Add priority via meta
1296
+ defineRule({
1297
+ id: '${finding.ruleA}',
1298
+ meta: { priority: 10 }, // Higher priority wins
1299
+ // ...
1300
+ });
1301
+
1302
+ // Option 2: Add distinguishing conditions
1303
+ defineRule({
1304
+ id: '${finding.ruleA}',
1305
+ impl: (state, events) => {
1306
+ // Add condition to distinguish from "${finding.ruleB}"
1307
+ if (/* specific condition */) {
1308
+ return RuleResult.emit([{ tag: '${finding.conflictingTag}', payload: { /* ... */ } }]);
1309
+ }
1310
+ return RuleResult.skip('Deferred to ${finding.ruleB}');
1311
+ },
1312
+ });`
1313
+ };
1314
+ }
1315
+ function suggestForUnreachableState(finding) {
1316
+ const tags = finding.factTags.join(", ");
1317
+ return {
1318
+ findingType: "unreachable-state",
1319
+ entityId: finding.factTags.join("+"),
1320
+ message: `No rule produces facts [${tags}] together. If this state combination is valid, add a composite rule that produces all required facts.`,
1321
+ action: "add-rule",
1322
+ priority: 4,
1323
+ skeleton: `defineRule({
1324
+ id: 'composite-${finding.factTags[0]?.replace(".", "-") ?? "unknown"}',
1325
+ description: 'Produces facts [${tags}] together',
1326
+ impl: (state, events) => {
1327
+ return RuleResult.emit([
1328
+ ${finding.factTags.map((t) => ` { tag: '${t}', payload: {} },`).join("\n")}
1329
+ ]);
1330
+ },
1331
+ });`
1332
+ };
1333
+ }
1334
+ function suggestForShadowedRule(finding) {
1335
+ return {
1336
+ findingType: "shadowed-rule",
1337
+ entityId: finding.ruleId,
1338
+ message: `Rule "${finding.ruleId}" is always superseded by "${finding.shadowedBy}" for event types [${finding.sharedEventTypes.join(", ")}]. Consider merging the rules or adding a distinguishing condition to "${finding.ruleId}".`,
1339
+ action: "merge",
1340
+ priority: 3,
1341
+ skeleton: `// Option 1: Remove the shadowed rule
1342
+ registry.removeRule('${finding.ruleId}');
1343
+
1344
+ // Option 2: Add unique behavior to the shadowed rule
1345
+ defineRule({
1346
+ id: '${finding.ruleId}',
1347
+ impl: (state, events) => {
1348
+ // Add condition that "${finding.shadowedBy}" doesn't cover
1349
+ if (/* unique condition */) {
1350
+ return RuleResult.emit([/* unique facts */]);
1351
+ }
1352
+ return RuleResult.skip('Handled by ${finding.shadowedBy}');
1353
+ },
1354
+ });`
1355
+ };
1356
+ }
1357
+ function suggestForContractGap(finding) {
1358
+ let message;
1359
+ let action = "add-contract";
1360
+ let priority;
1361
+ switch (finding.type) {
1362
+ case "missing-error-path":
1363
+ message = `Rule "${finding.ruleId}" has no error/failure examples in its contract. Add examples showing what happens when preconditions fail, inputs are invalid, or the rule needs to skip.`;
1364
+ priority = 6;
1365
+ break;
1366
+ case "missing-edge-case":
1367
+ message = `Rule "${finding.ruleId}": ${finding.description}. Add contract examples covering all declared event types.`;
1368
+ priority = 5;
1369
+ break;
1370
+ case "missing-boundary":
1371
+ message = `Rule "${finding.ruleId}" has only 1 contract example. Add boundary condition examples (empty input, maximum values, concurrent events).`;
1372
+ priority = 4;
1373
+ break;
1374
+ case "cross-reference-broken":
1375
+ message = `Rule "${finding.ruleId}": ${finding.description}. Verify the referenced fact producer exists.`;
1376
+ action = "modify";
1377
+ priority = 7;
1378
+ break;
1379
+ default:
1380
+ message = finding.description;
1381
+ priority = 3;
1382
+ }
1383
+ return {
1384
+ findingType: "contract-gap",
1385
+ entityId: finding.ruleId,
1386
+ message,
1387
+ action,
1388
+ priority
1389
+ };
1390
+ }
1391
+
1392
+ // src/decision-ledger/report.ts
1393
+ function generateLedger(registry, engine, expectations) {
1394
+ const allEventTypes = /* @__PURE__ */ new Set();
1395
+ for (const rule of registry.getAllRules()) {
1396
+ if (rule.eventTypes) {
1397
+ const types = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
1398
+ for (const t of types) allEventTypes.add(t);
1399
+ }
1400
+ }
1401
+ const deadRules = findDeadRules(registry, [...allEventTypes]);
1402
+ const unreachableStates = findUnreachableStates(registry);
1403
+ const shadowedRules = findShadowedRules(registry);
1404
+ const contradictions = findContradictions(registry);
1405
+ const contractGaps = findContractGaps(registry);
1406
+ const gaps = expectations ? findGaps(registry, expectations) : [];
1407
+ const currentFacts = engine.getFacts();
1408
+ const factDerivationChains = currentFacts.map(
1409
+ (fact) => traceDerivation(fact.tag, engine, registry)
1410
+ );
1411
+ const suggestions = suggestAll({
1412
+ deadRules,
1413
+ gaps,
1414
+ contradictions,
1415
+ unreachableStates,
1416
+ shadowedRules,
1417
+ contractGaps
1418
+ });
1419
+ const totalRules = registry.getRuleIds().length;
1420
+ const totalConstraints = registry.getConstraintIds().length;
1421
+ const totalIssues = deadRules.length + unreachableStates.length + shadowedRules.length + contradictions.length + gaps.length;
1422
+ const maxIssues = Math.max(totalRules + totalConstraints, 1);
1423
+ const healthScore = Math.max(0, Math.round(100 - totalIssues / maxIssues * 100));
1424
+ return {
1425
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1426
+ factDerivationChains,
1427
+ deadRules,
1428
+ unreachableStates,
1429
+ shadowedRules,
1430
+ contradictions,
1431
+ gaps,
1432
+ suggestions,
1433
+ summary: {
1434
+ totalRules,
1435
+ totalConstraints,
1436
+ deadRuleCount: deadRules.length,
1437
+ unreachableStateCount: unreachableStates.length,
1438
+ shadowedRuleCount: shadowedRules.length,
1439
+ contradictionCount: contradictions.length,
1440
+ gapCount: gaps.length,
1441
+ suggestionCount: suggestions.length,
1442
+ healthScore
1443
+ }
1444
+ };
1445
+ }
1446
+ function formatLedger(report) {
1447
+ const lines = [];
1448
+ const icon = report.summary.healthScore >= 90 ? "\u2705" : report.summary.healthScore >= 70 ? "\u{1F7E1}" : "\u{1F534}";
1449
+ lines.push(`# ${icon} Decision Ledger Analysis`);
1450
+ lines.push("");
1451
+ lines.push(`**Health Score:** ${report.summary.healthScore}/100`);
1452
+ lines.push(`**Timestamp:** ${report.timestamp}`);
1453
+ lines.push("");
1454
+ lines.push("## Summary");
1455
+ lines.push("");
1456
+ lines.push(`| Metric | Count |`);
1457
+ lines.push(`|--------|-------|`);
1458
+ lines.push(`| Rules | ${report.summary.totalRules} |`);
1459
+ lines.push(`| Constraints | ${report.summary.totalConstraints} |`);
1460
+ lines.push(`| Dead Rules | ${report.summary.deadRuleCount} |`);
1461
+ lines.push(`| Unreachable States | ${report.summary.unreachableStateCount} |`);
1462
+ lines.push(`| Shadowed Rules | ${report.summary.shadowedRuleCount} |`);
1463
+ lines.push(`| Contradictions | ${report.summary.contradictionCount} |`);
1464
+ lines.push(`| Gaps | ${report.summary.gapCount} |`);
1465
+ lines.push(`| Suggestions | ${report.summary.suggestionCount} |`);
1466
+ lines.push("");
1467
+ if (report.deadRules.length > 0) {
1468
+ lines.push("## \u{1F480} Dead Rules");
1469
+ lines.push("");
1470
+ for (const dr of report.deadRules) {
1471
+ lines.push(`- **${dr.ruleId}**: ${dr.reason}`);
1472
+ }
1473
+ lines.push("");
1474
+ }
1475
+ if (report.unreachableStates.length > 0) {
1476
+ lines.push("## \u{1F6AB} Unreachable States");
1477
+ lines.push("");
1478
+ for (const us of report.unreachableStates) {
1479
+ lines.push(`- **[${us.factTags.join(", ")}]**: ${us.reason}`);
1480
+ }
1481
+ lines.push("");
1482
+ }
1483
+ if (report.shadowedRules.length > 0) {
1484
+ lines.push("## \u{1F47B} Shadowed Rules");
1485
+ lines.push("");
1486
+ for (const sr of report.shadowedRules) {
1487
+ lines.push(`- **${sr.ruleId}** shadowed by **${sr.shadowedBy}**: ${sr.reason}`);
1488
+ }
1489
+ lines.push("");
1490
+ }
1491
+ if (report.contradictions.length > 0) {
1492
+ lines.push("## \u26A1 Contradictions");
1493
+ lines.push("");
1494
+ for (const c of report.contradictions) {
1495
+ lines.push(`- **${c.ruleA}** \u2194 **${c.ruleB}** on \`${c.conflictingTag}\`: ${c.reason}`);
1496
+ }
1497
+ lines.push("");
1498
+ }
1499
+ if (report.gaps.length > 0) {
1500
+ lines.push("## \u{1F573}\uFE0F Gaps");
1501
+ lines.push("");
1502
+ for (const g of report.gaps) {
1503
+ lines.push(`- **${g.expectationName}** (${g.type}): ${g.description}`);
1504
+ }
1505
+ lines.push("");
1506
+ }
1507
+ if (report.factDerivationChains.length > 0) {
1508
+ lines.push("## \u{1F517} Fact Derivation Chains");
1509
+ lines.push("");
1510
+ for (const chain of report.factDerivationChains) {
1511
+ if (chain.steps.length === 0) continue;
1512
+ lines.push(`### \`${chain.targetFact}\` (depth: ${chain.depth})`);
1513
+ for (const step of chain.steps) {
1514
+ const icon2 = step.type === "event" ? "\u26A1" : step.type === "rule-fired" ? "\u2699\uFE0F" : step.type === "fact-produced" ? "\u{1F4E6}" : "\u{1F4D6}";
1515
+ lines.push(` ${icon2} ${step.description}`);
1516
+ }
1517
+ lines.push("");
1518
+ }
1519
+ }
1520
+ if (report.suggestions.length > 0) {
1521
+ lines.push("## \u{1F4A1} Suggestions");
1522
+ lines.push("");
1523
+ for (const s of report.suggestions) {
1524
+ const priorityIcon = s.priority >= 8 ? "\u{1F534}" : s.priority >= 5 ? "\u{1F7E1}" : "\u{1F7E2}";
1525
+ lines.push(`${priorityIcon} **[${s.findingType}]** ${s.message}`);
1526
+ if (s.skeleton) {
1527
+ lines.push("```typescript");
1528
+ lines.push(s.skeleton);
1529
+ lines.push("```");
1530
+ }
1531
+ lines.push("");
1532
+ }
1533
+ }
1534
+ return lines.join("\n");
1535
+ }
1536
+ function formatBuildOutput(report) {
1537
+ const lines = [];
1538
+ lines.push(`::group::Decision Ledger Analysis (Score: ${report.summary.healthScore}/100)`);
1539
+ for (const c of report.contradictions) {
1540
+ lines.push(`::error title=Contradiction::Rules "${c.ruleA}" and "${c.ruleB}" both produce "${c.conflictingTag}"`);
1541
+ }
1542
+ for (const g of report.gaps) {
1543
+ if (g.type === "no-rule") {
1544
+ lines.push(`::error title=Missing Rule::No rule covers expectation "${g.expectationName}"`);
1545
+ }
1546
+ }
1547
+ for (const dr of report.deadRules) {
1548
+ lines.push(`::warning title=Dead Rule::Rule "${dr.ruleId}" can never fire (requires [${dr.requiredEventTypes.join(", ")}])`);
1549
+ }
1550
+ for (const sr of report.shadowedRules) {
1551
+ lines.push(`::warning title=Shadowed Rule::Rule "${sr.ruleId}" is always superseded by "${sr.shadowedBy}"`);
1552
+ }
1553
+ for (const us of report.unreachableStates) {
1554
+ lines.push(`::warning title=Unreachable State::Facts [${us.factTags.join(", ")}] cannot be produced together`);
1555
+ }
1556
+ for (const g of report.gaps) {
1557
+ if (g.type === "partial-coverage") {
1558
+ lines.push(`::warning title=Partial Coverage::Expectation "${g.expectationName}" is only partially covered`);
1559
+ } else if (g.type === "no-contract") {
1560
+ lines.push(`::warning title=Missing Contract::Rules for "${g.expectationName}" lack contracts`);
1561
+ }
1562
+ }
1563
+ lines.push("");
1564
+ lines.push(`Rules: ${report.summary.totalRules} | Constraints: ${report.summary.totalConstraints} | Health: ${report.summary.healthScore}/100`);
1565
+ lines.push(`Dead: ${report.summary.deadRuleCount} | Unreachable: ${report.summary.unreachableStateCount} | Shadowed: ${report.summary.shadowedRuleCount} | Contradictions: ${report.summary.contradictionCount} | Gaps: ${report.summary.gapCount}`);
1566
+ lines.push("::endgroup::");
1567
+ return lines.join("\n");
1568
+ }
1569
+ function diffLedgers(before, after) {
1570
+ const changes = [];
1571
+ diffItems(
1572
+ before.deadRules,
1573
+ after.deadRules,
1574
+ (dr) => dr.ruleId,
1575
+ (dr) => `Dead rule: ${dr.ruleId} \u2014 ${dr.reason}`,
1576
+ "dead-rule",
1577
+ changes
1578
+ );
1579
+ diffItems(
1580
+ before.unreachableStates,
1581
+ after.unreachableStates,
1582
+ (us) => us.factTags.join("+"),
1583
+ (us) => `Unreachable state: [${us.factTags.join(", ")}]`,
1584
+ "unreachable-state",
1585
+ changes
1586
+ );
1587
+ diffItems(
1588
+ before.shadowedRules,
1589
+ after.shadowedRules,
1590
+ (sr) => sr.ruleId,
1591
+ (sr) => `Shadowed rule: ${sr.ruleId} by ${sr.shadowedBy}`,
1592
+ "shadowed-rule",
1593
+ changes
1594
+ );
1595
+ diffItems(
1596
+ before.contradictions,
1597
+ after.contradictions,
1598
+ (c) => `${c.ruleA}\u2194${c.ruleB}`,
1599
+ (c) => `Contradiction: ${c.ruleA} \u2194 ${c.ruleB} on ${c.conflictingTag}`,
1600
+ "contradiction",
1601
+ changes
1602
+ );
1603
+ diffItems(
1604
+ before.gaps,
1605
+ after.gaps,
1606
+ (g) => g.expectationName,
1607
+ (g) => `Gap: ${g.expectationName} (${g.type})`,
1608
+ "gap",
1609
+ changes
1610
+ );
1611
+ const scoreDelta = after.summary.healthScore - before.summary.healthScore;
1612
+ const scoreDirection = scoreDelta > 0 ? "improved" : scoreDelta < 0 ? "degraded" : "unchanged";
1613
+ return {
1614
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1615
+ beforeTimestamp: before.timestamp,
1616
+ afterTimestamp: after.timestamp,
1617
+ changes,
1618
+ scoreDelta,
1619
+ summary: `Score ${scoreDirection} by ${Math.abs(scoreDelta)} points (${before.summary.healthScore} \u2192 ${after.summary.healthScore}). ${changes.length} changes: ${changes.filter((c) => c.type === "added").length} added, ${changes.filter((c) => c.type === "removed").length} removed, ${changes.filter((c) => c.type === "changed").length} changed.`
1620
+ };
1621
+ }
1622
+ function diffItems(before, after, getKey, describe, category, changes) {
1623
+ const beforeKeys = new Set(before.map(getKey));
1624
+ const afterKeys = new Set(after.map(getKey));
1625
+ for (const item of after) {
1626
+ const key = getKey(item);
1627
+ if (!beforeKeys.has(key)) {
1628
+ changes.push({
1629
+ type: "added",
1630
+ category,
1631
+ description: describe(item),
1632
+ entityId: key
1633
+ });
1634
+ }
1635
+ }
1636
+ for (const item of before) {
1637
+ const key = getKey(item);
1638
+ if (!afterKeys.has(key)) {
1639
+ changes.push({
1640
+ type: "removed",
1641
+ category,
1642
+ description: describe(item),
1643
+ entityId: key
1644
+ });
1645
+ }
1646
+ }
1647
+ }
1648
+
1649
+ export {
1650
+ defineFact,
1651
+ defineEvent,
1652
+ defineRule,
1653
+ defineConstraint,
1654
+ defineModule,
1655
+ filterEvents,
1656
+ filterFacts,
1657
+ findEvent,
1658
+ findFact,
1659
+ ContractMissing,
1660
+ ContractValidated,
1661
+ AcknowledgeContractGap,
1662
+ ValidateContracts,
1663
+ ContractGapAcknowledged,
1664
+ ContractAdded,
1665
+ ContractUpdated,
1666
+ validateContracts,
1667
+ formatValidationReport,
1668
+ formatValidationReportJSON,
1669
+ formatValidationReportSARIF,
1670
+ BehaviorLedger,
1671
+ createBehaviorLedger,
1672
+ analyzeDependencyGraph,
1673
+ findDeadRules,
1674
+ findUnreachableStates,
1675
+ findShadowedRules,
1676
+ findContradictions,
1677
+ findGaps,
1678
+ traceDerivation,
1679
+ traceImpact,
1680
+ verifyContractExamples,
1681
+ verifyInvariants,
1682
+ findContractGaps,
1683
+ crossReferenceContracts,
1684
+ suggest,
1685
+ suggestAll,
1686
+ generateLedger,
1687
+ formatLedger,
1688
+ formatBuildOutput,
1689
+ diffLedgers
1690
+ };