@plures/praxis 1.4.0 → 2.0.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 (72) hide show
  1. package/dist/browser/{chunk-N63K4KWS.js → chunk-4IRUGWR3.js} +1 -1
  2. package/dist/browser/chunk-6MVRT7CK.js +363 -0
  3. package/dist/browser/chunk-6SJ44Q64.js +473 -0
  4. package/dist/browser/chunk-BQOYZBWA.js +282 -0
  5. package/dist/browser/chunk-IG5BJ2MT.js +91 -0
  6. package/dist/browser/{chunk-MJK3IYTJ.js → chunk-JZDJU2DO.js} +4 -84
  7. package/dist/browser/chunk-ZEW4LJAJ.js +353 -0
  8. package/dist/browser/{engine-YIEGSX7U.js → engine-3B5WJPGT.js} +2 -1
  9. package/dist/browser/expectations/index.d.ts +180 -0
  10. package/dist/browser/expectations/index.js +14 -0
  11. package/dist/browser/factory/index.d.ts +150 -0
  12. package/dist/browser/factory/index.js +15 -0
  13. package/dist/browser/index.d.ts +277 -3
  14. package/dist/browser/index.js +425 -60
  15. package/dist/browser/integrations/svelte.d.ts +4 -2
  16. package/dist/browser/integrations/svelte.js +3 -2
  17. package/dist/browser/project/index.d.ts +177 -0
  18. package/dist/browser/project/index.js +19 -0
  19. package/dist/browser/reactive-engine.svelte-BwWadvAW.d.ts +224 -0
  20. package/dist/browser/rule-result-DcXWe9tn.d.ts +206 -0
  21. package/dist/browser/rules-BaWMqxuG.d.ts +277 -0
  22. package/dist/browser/unified/index.d.ts +239 -0
  23. package/dist/browser/unified/index.js +20 -0
  24. package/dist/node/chunk-6MVRT7CK.js +363 -0
  25. package/dist/node/chunk-AZLNISFI.js +1690 -0
  26. package/dist/node/chunk-IG5BJ2MT.js +91 -0
  27. package/dist/node/{chunk-KMJWAFZV.js → chunk-JZDJU2DO.js} +4 -89
  28. package/dist/node/{chunk-7M3HV4XR.js → chunk-WFRHXZBP.js} +3 -3
  29. package/dist/node/cli/index.cjs +48 -0
  30. package/dist/node/cli/index.js +2 -2
  31. package/dist/node/{engine-FEN5IYZ5.js → engine-VFHCIEM4.js} +2 -1
  32. package/dist/node/index.cjs +2114 -0
  33. package/dist/node/index.d.cts +964 -280
  34. package/dist/node/index.d.ts +964 -280
  35. package/dist/node/index.js +575 -10
  36. package/dist/node/integrations/svelte.d.cts +3 -2
  37. package/dist/node/integrations/svelte.d.ts +3 -2
  38. package/dist/node/integrations/svelte.js +3 -2
  39. package/dist/node/{reactive-engine.svelte-DekxqFu0.d.ts → reactive-engine.svelte-BBZLMzus.d.ts} +3 -79
  40. package/dist/node/{reactive-engine.svelte-Cg0Yc2Hs.d.cts → reactive-engine.svelte-Cbq_V20o.d.cts} +3 -79
  41. package/dist/node/rule-result-B9GMivAn.d.cts +80 -0
  42. package/dist/node/rule-result-Bo3sFMmN.d.ts +80 -0
  43. package/dist/node/{server-SYZPDULV.js → server-FKLVY57V.js} +4 -2
  44. package/dist/node/unified/index.cjs +484 -0
  45. package/dist/node/unified/index.d.cts +240 -0
  46. package/dist/node/unified/index.d.ts +240 -0
  47. package/dist/node/unified/index.js +21 -0
  48. package/dist/node/{validate-TQGVIG7G.js → validate-BY7JNY7H.js} +2 -1
  49. package/package.json +38 -11
  50. package/src/__tests__/chronos-project.test.ts +799 -0
  51. package/src/__tests__/decision-ledger.test.ts +857 -402
  52. package/src/chronos/diff.ts +336 -0
  53. package/src/chronos/hooks.ts +227 -0
  54. package/src/chronos/index.ts +83 -0
  55. package/src/chronos/project-chronicle.ts +198 -0
  56. package/src/chronos/timeline.ts +152 -0
  57. package/src/decision-ledger/analyzer-types.ts +280 -0
  58. package/src/decision-ledger/analyzer.ts +518 -0
  59. package/src/decision-ledger/contract-verification.ts +456 -0
  60. package/src/decision-ledger/derivation.ts +158 -0
  61. package/src/decision-ledger/index.ts +59 -0
  62. package/src/decision-ledger/report.ts +378 -0
  63. package/src/decision-ledger/suggestions.ts +287 -0
  64. package/src/index.browser.ts +103 -0
  65. package/src/index.ts +98 -0
  66. package/src/unified/__tests__/unified.test.ts +396 -0
  67. package/src/unified/core.ts +517 -0
  68. package/src/unified/index.ts +32 -0
  69. package/src/unified/rules.ts +66 -0
  70. package/src/unified/types.ts +148 -0
  71. package/dist/browser/reactive-engine.svelte-DjynI82A.d.ts +0 -688
  72. package/dist/node/chunk-FWOXU4MM.js +0 -487
@@ -0,0 +1,456 @@
1
+ /**
2
+ * Decision Ledger — Contract Verification
3
+ *
4
+ * Goes beyond simple contract existence checking to actually run rules
5
+ * against their contract examples, verify invariants, and cross-reference
6
+ * fact dependencies between contracts.
7
+ */
8
+
9
+ import type { PraxisRegistry, RuleDescriptor } from '../core/rules.js';
10
+ import type { PraxisState, PraxisEvent, PraxisFact } from '../core/protocol.js';
11
+ import { RuleResult } from '../core/rule-result.js';
12
+ import type { Contract } from './types.js';
13
+ import type {
14
+ ContractVerificationResult,
15
+ ExampleVerification,
16
+ InvariantCheck,
17
+ ContractCoverageGap,
18
+ CrossReference,
19
+ } from './analyzer-types.js';
20
+ import { analyzeDependencyGraph } from './analyzer.js';
21
+
22
+ /**
23
+ * Actually run a rule's implementation against each contract example's
24
+ * `given` state and verify the output matches `then`.
25
+ *
26
+ * This is deeper than contract existence checking — it executes the rule.
27
+ */
28
+ export function verifyContractExamples<TContext = unknown>(
29
+ rule: RuleDescriptor<TContext>,
30
+ contract: Contract,
31
+ ): ContractVerificationResult {
32
+ const examples: ExampleVerification[] = [];
33
+
34
+ for (let i = 0; i < contract.examples.length; i++) {
35
+ const example = contract.examples[i];
36
+
37
+ try {
38
+ // Build synthetic state from `given`
39
+ const state = buildStateFromDescription<TContext>(example.given);
40
+
41
+ // Build events from `when`
42
+ const events = buildEventsFromDescription(example.when, rule.eventTypes);
43
+
44
+ // Add events to state so rules can access state.events
45
+ const stateWithEvents = { ...state, events };
46
+
47
+ // Execute the rule
48
+ const result = rule.impl(stateWithEvents, events);
49
+
50
+ // Check if output matches `then`
51
+ const verification = verifyOutput(result, example.then, rule.id);
52
+
53
+ examples.push({
54
+ index: i,
55
+ given: example.given,
56
+ when: example.when,
57
+ expectedThen: example.then,
58
+ passed: verification.passed,
59
+ actualOutput: verification.actualOutput,
60
+ error: verification.error,
61
+ });
62
+ } catch (error) {
63
+ examples.push({
64
+ index: i,
65
+ given: example.given,
66
+ when: example.when,
67
+ expectedThen: example.then,
68
+ passed: false,
69
+ error: `Execution error: ${error instanceof Error ? error.message : String(error)}`,
70
+ });
71
+ }
72
+ }
73
+
74
+ const passCount = examples.filter(e => e.passed).length;
75
+ const failCount = examples.filter(e => !e.passed).length;
76
+
77
+ return {
78
+ ruleId: rule.id,
79
+ examples,
80
+ allPassed: failCount === 0,
81
+ passCount,
82
+ failCount,
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Check that stated invariants hold across all rules.
88
+ *
89
+ * For each rule with a contract, check if the invariants are consistent
90
+ * with the rule's behavior description and examples.
91
+ */
92
+ export function verifyInvariants<TContext = unknown>(
93
+ registry: PraxisRegistry<TContext>,
94
+ ): InvariantCheck[] {
95
+ const checks: InvariantCheck[] = [];
96
+ const rules = registry.getAllRules();
97
+
98
+ for (const rule of rules) {
99
+ if (!rule.contract) continue;
100
+
101
+ for (const invariant of rule.contract.invariants) {
102
+ // Check if the invariant is consistent with examples
103
+ const consistent = rule.contract.examples.every(example => {
104
+ return isConsistentWithInvariant(example, invariant);
105
+ });
106
+
107
+ checks.push({
108
+ invariant,
109
+ ruleId: rule.id,
110
+ holds: consistent,
111
+ explanation: consistent
112
+ ? `Invariant "${invariant}" is consistent with all ${rule.contract.examples.length} examples`
113
+ : `Invariant "${invariant}" may be violated by one or more examples`,
114
+ });
115
+ }
116
+ }
117
+
118
+ return checks;
119
+ }
120
+
121
+ /**
122
+ * Find rules with contracts that don't cover all code paths.
123
+ *
124
+ * Analyzes contract examples to find:
125
+ * - Rules with only happy-path examples (no error cases)
126
+ * - Rules with no boundary condition examples
127
+ * - Rules that handle multiple event types but only have examples for some
128
+ */
129
+ export function findContractGaps<TContext = unknown>(
130
+ registry: PraxisRegistry<TContext>,
131
+ ): ContractCoverageGap[] {
132
+ const gaps: ContractCoverageGap[] = [];
133
+ const rules = registry.getAllRules();
134
+
135
+ for (const rule of rules) {
136
+ if (!rule.contract) continue;
137
+
138
+ const examples = rule.contract.examples;
139
+
140
+ // Check: no error/failure examples
141
+ const hasErrorExamples = examples.some(
142
+ ex =>
143
+ ex.then.toLowerCase().includes('error') ||
144
+ ex.then.toLowerCase().includes('fail') ||
145
+ ex.then.toLowerCase().includes('skip') ||
146
+ ex.then.toLowerCase().includes('noop') ||
147
+ ex.then.toLowerCase().includes('retract') ||
148
+ ex.then.toLowerCase().includes('violation'),
149
+ );
150
+
151
+ if (!hasErrorExamples && examples.length > 0) {
152
+ gaps.push({
153
+ ruleId: rule.id,
154
+ description: `No error/failure path examples. All ${examples.length} examples show happy paths`,
155
+ type: 'missing-error-path',
156
+ });
157
+ }
158
+
159
+ // Check: multiple event types but not all covered in examples
160
+ if (rule.eventTypes) {
161
+ const eventTypes = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
162
+ if (eventTypes.length > 1) {
163
+ const coveredTypes = new Set<string>();
164
+ for (const ex of examples) {
165
+ for (const et of eventTypes) {
166
+ if (
167
+ ex.when.toLowerCase().includes(et.toLowerCase()) ||
168
+ ex.when.toLowerCase().includes(et.replace('.', ' ').toLowerCase())
169
+ ) {
170
+ coveredTypes.add(et);
171
+ }
172
+ }
173
+ }
174
+
175
+ const uncovered = eventTypes.filter(et => !coveredTypes.has(et));
176
+ if (uncovered.length > 0) {
177
+ gaps.push({
178
+ ruleId: rule.id,
179
+ description: `Event types [${uncovered.join(', ')}] not covered by any example`,
180
+ type: 'missing-edge-case',
181
+ });
182
+ }
183
+ }
184
+ }
185
+
186
+ // Check: only one example (likely insufficient)
187
+ if (examples.length === 1) {
188
+ gaps.push({
189
+ ruleId: rule.id,
190
+ description: `Only 1 contract example — likely missing boundary conditions and edge cases`,
191
+ type: 'missing-boundary',
192
+ });
193
+ }
194
+ }
195
+
196
+ return gaps;
197
+ }
198
+
199
+ /**
200
+ * Find contracts that reference facts from other rules and verify
201
+ * those producing rules actually exist.
202
+ */
203
+ export function crossReferenceContracts<TContext = unknown>(
204
+ registry: PraxisRegistry<TContext>,
205
+ ): CrossReference[] {
206
+ const graph = analyzeDependencyGraph(registry);
207
+ const references: CrossReference[] = [];
208
+ const rules = registry.getAllRules();
209
+
210
+ for (const rule of rules) {
211
+ if (!rule.contract) continue;
212
+
213
+ // Scan contract examples for references to fact tags
214
+ for (const example of rule.contract.examples) {
215
+ const referencedTags = extractReferencedFactTags(example.given + ' ' + example.when);
216
+
217
+ for (const tag of referencedTags) {
218
+ const factNode = graph.facts.get(tag);
219
+ const producerRuleId = factNode?.producedBy[0] ?? null;
220
+ const valid = factNode ? factNode.producedBy.length > 0 : false;
221
+
222
+ // Don't self-reference
223
+ if (producerRuleId === rule.id) continue;
224
+
225
+ references.push({
226
+ sourceRuleId: rule.id,
227
+ referencedFactTag: tag,
228
+ producerRuleId,
229
+ valid,
230
+ });
231
+ }
232
+ }
233
+ }
234
+
235
+ return references;
236
+ }
237
+
238
+ // ─── Helpers ────────────────────────────────────────────────────────────────
239
+
240
+ /**
241
+ * Build a minimal synthetic state from a "given" description.
242
+ */
243
+ function buildStateFromDescription<TContext>(given: string): PraxisState & { context: TContext } {
244
+ // Parse simple key-value patterns from given text
245
+ const context = {} as TContext;
246
+ const facts: PraxisFact[] = [];
247
+
248
+ // Extract fact references from given (e.g., "fact 'user.loggedIn' exists")
249
+ const factPattern = /fact\s+['"]([^'"]+)['"]/gi;
250
+ let match;
251
+ while ((match = factPattern.exec(given)) !== null) {
252
+ facts.push({ tag: match[1], payload: {} });
253
+ }
254
+
255
+ // Also check for dotted identifiers that look like facts
256
+ const dottedPattern = /['"]([a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9.]*)['"]/g;
257
+ while ((match = dottedPattern.exec(given)) !== null) {
258
+ const tag = match[1];
259
+ if (!facts.some(f => f.tag === tag)) {
260
+ facts.push({ tag, payload: {} });
261
+ }
262
+ }
263
+
264
+ return {
265
+ context,
266
+ facts,
267
+ meta: {},
268
+ protocolVersion: '1.0.0',
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Build synthetic events from a "when" description.
274
+ */
275
+ function buildEventsFromDescription(
276
+ when: string,
277
+ eventTypes?: string | string[],
278
+ ): PraxisEvent[] {
279
+ const events: PraxisEvent[] = [];
280
+
281
+ // Use rule's declared event types first
282
+ if (eventTypes) {
283
+ const types = Array.isArray(eventTypes) ? eventTypes : [eventTypes];
284
+ for (const type of types) {
285
+ if (when.toLowerCase().includes(type.toLowerCase()) || when.toLowerCase().includes(type.replace('.', ' ').toLowerCase())) {
286
+ events.push({ tag: type, payload: {} });
287
+ }
288
+ }
289
+ }
290
+
291
+ // If no events built from types, create one from the when description
292
+ if (events.length === 0) {
293
+ // Look for event tag patterns in the when text
294
+ const eventPattern = /['"]([A-Z][A-Z0-9._-]+)['"]/g;
295
+ let match;
296
+ while ((match = eventPattern.exec(when)) !== null) {
297
+ events.push({ tag: match[1], payload: {} });
298
+ }
299
+ }
300
+
301
+ // Fallback: create a generic event
302
+ if (events.length === 0) {
303
+ const types = eventTypes
304
+ ? (Array.isArray(eventTypes) ? eventTypes : [eventTypes])
305
+ : ['__test__'];
306
+ events.push({ tag: types[0], payload: {} });
307
+ }
308
+
309
+ return events;
310
+ }
311
+
312
+ /**
313
+ * Verify rule output against expected "then" description.
314
+ */
315
+ function verifyOutput(
316
+ result: unknown,
317
+ expectedThen: string,
318
+ ruleId: string,
319
+ ): { passed: boolean; actualOutput?: string; error?: string } {
320
+ const thenLower = expectedThen.toLowerCase();
321
+
322
+ if (result instanceof RuleResult) {
323
+ switch (result.kind) {
324
+ case 'emit': {
325
+ // Check if the emitted facts match the expected output
326
+ const factTags = result.facts.map(f => f.tag);
327
+ const actualOutput = `Emitted: [${factTags.join(', ')}]`;
328
+
329
+ // Check for specific fact tag mentions
330
+ const emitExpected =
331
+ thenLower.includes('emit') ||
332
+ thenLower.includes('produce') ||
333
+ thenLower.includes('fact');
334
+
335
+ if (emitExpected) {
336
+ // Check if any mentioned fact tags are in the output
337
+ const expectedTags = extractReferencedFactTags(expectedThen);
338
+ if (expectedTags.length > 0) {
339
+ const allFound = expectedTags.every(tag =>
340
+ factTags.some(ft => ft.toLowerCase() === tag.toLowerCase()),
341
+ );
342
+ return { passed: allFound, actualOutput };
343
+ }
344
+ return { passed: true, actualOutput };
345
+ }
346
+
347
+ return { passed: true, actualOutput };
348
+ }
349
+ case 'noop': {
350
+ const expectNoop =
351
+ thenLower.includes('noop') ||
352
+ thenLower.includes('nothing') ||
353
+ thenLower.includes('no fact') ||
354
+ thenLower.includes('skip');
355
+ return {
356
+ passed: expectNoop,
357
+ actualOutput: `Noop: ${result.reason ?? 'no reason'}`,
358
+ };
359
+ }
360
+ case 'skip': {
361
+ const expectSkip =
362
+ thenLower.includes('skip') ||
363
+ thenLower.includes('noop') ||
364
+ thenLower.includes('not fire') ||
365
+ thenLower.includes('nothing');
366
+ return {
367
+ passed: expectSkip,
368
+ actualOutput: `Skip: ${result.reason ?? 'no reason'}`,
369
+ };
370
+ }
371
+ case 'retract': {
372
+ const expectRetract =
373
+ thenLower.includes('retract') ||
374
+ thenLower.includes('remove') ||
375
+ thenLower.includes('clear');
376
+ return {
377
+ passed: expectRetract,
378
+ actualOutput: `Retract: [${result.retractTags.join(', ')}]`,
379
+ };
380
+ }
381
+ }
382
+ } else if (Array.isArray(result)) {
383
+ // Legacy PraxisFact[] return
384
+ const factTags = (result as PraxisFact[]).map(f => f.tag);
385
+ const actualOutput = `Facts: [${factTags.join(', ')}]`;
386
+ return { passed: factTags.length > 0 || thenLower.includes('nothing'), actualOutput };
387
+ }
388
+
389
+ return { passed: false, error: `Unexpected result type from rule "${ruleId}"` };
390
+ }
391
+
392
+ /**
393
+ * Check if a contract example is consistent with an invariant statement.
394
+ */
395
+ function isConsistentWithInvariant(
396
+ example: { given: string; when: string; then: string },
397
+ invariant: string,
398
+ ): boolean {
399
+ const invLower = invariant.toLowerCase();
400
+ const thenLower = example.then.toLowerCase();
401
+
402
+ // Check for explicit contradictions
403
+ // "must not" invariant vs "does" in then
404
+ if (invLower.includes('must not') || invLower.includes('never')) {
405
+ const keyword = invLower
406
+ .replace(/must not|never|should not/g, '')
407
+ .trim()
408
+ .split(/\s+/)[0];
409
+ if (keyword && thenLower.includes(keyword)) {
410
+ return false; // Potential violation
411
+ }
412
+ }
413
+
414
+ // "must" invariant — check the then doesn't contradict
415
+ if (invLower.includes('must ') && !invLower.includes('must not')) {
416
+ // Extract what must happen
417
+ const mustPart = invLower.split('must ')[1]?.split(/[.,;]/)[0]?.trim();
418
+ if (mustPart) {
419
+ // If then explicitly contradicts
420
+ const negations = ['no ', 'not ', 'never ', 'without '];
421
+ for (const neg of negations) {
422
+ if (thenLower.includes(neg + mustPart.split(/\s+/)[0])) {
423
+ return false;
424
+ }
425
+ }
426
+ }
427
+ }
428
+
429
+ return true; // No contradiction detected
430
+ }
431
+
432
+ /**
433
+ * Extract fact tags referenced in text.
434
+ */
435
+ function extractReferencedFactTags(text: string): string[] {
436
+ const tags: string[] = [];
437
+
438
+ // Match dotted identifiers that look like fact tags
439
+ const patterns = [
440
+ /['"]([a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9.]*)['"]/g,
441
+ /fact\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi,
442
+ /(?:emit|produce|retract)\s+['"]?([a-zA-Z][a-zA-Z0-9._-]+)['"]?/gi,
443
+ ];
444
+
445
+ for (const pattern of patterns) {
446
+ let match;
447
+ while ((match = pattern.exec(text)) !== null) {
448
+ const tag = match[1];
449
+ if (tag && !tags.includes(tag)) {
450
+ tags.push(tag);
451
+ }
452
+ }
453
+ }
454
+
455
+ return tags;
456
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Decision Ledger — Fact Derivation Tracing
3
+ *
4
+ * Trace how facts are derived through chains of rule firings,
5
+ * and analyze the impact of removing a fact from the system.
6
+ */
7
+
8
+ import type { PraxisRegistry } from '../core/rules.js';
9
+ import type { LogicEngine } from '../core/engine.js';
10
+ import type { DerivationChain, DerivationStep, ImpactReport } from './analyzer-types.js';
11
+ import { analyzeDependencyGraph } from './analyzer.js';
12
+
13
+ /**
14
+ * Trace how a fact was derived through the rule chain.
15
+ *
16
+ * Starting from the fact tag, walks backward through the dependency graph
17
+ * to find the full derivation chain: event → rule A → fact X → rule B → fact Y
18
+ *
19
+ * Uses the engine's current state to identify which rules actually fired.
20
+ */
21
+ export function traceDerivation<TContext = unknown>(
22
+ factTag: string,
23
+ _engine: LogicEngine<TContext>,
24
+ registry: PraxisRegistry<TContext>,
25
+ ): DerivationChain {
26
+ const graph = analyzeDependencyGraph(registry);
27
+ const steps: DerivationStep[] = [];
28
+ const visited = new Set<string>();
29
+
30
+ function walkBackward(tag: string, depth: number): void {
31
+ if (visited.has(tag) || depth > 20) return; // prevent cycles
32
+ visited.add(tag);
33
+
34
+ const factNode = graph.facts.get(tag);
35
+ if (!factNode) {
36
+ steps.unshift({
37
+ type: 'fact-produced',
38
+ id: tag,
39
+ description: `Fact "${tag}" (origin unknown — not in dependency graph)`,
40
+ });
41
+ return;
42
+ }
43
+
44
+ // This fact was produced
45
+ steps.unshift({
46
+ type: 'fact-produced',
47
+ id: tag,
48
+ description: `Fact "${tag}" produced`,
49
+ });
50
+
51
+ // Walk to producers
52
+ for (const ruleId of factNode.producedBy) {
53
+ if (visited.has(ruleId)) continue;
54
+ visited.add(ruleId);
55
+
56
+ const rule = registry.getRule(ruleId);
57
+ steps.unshift({
58
+ type: 'rule-fired',
59
+ id: ruleId,
60
+ description: `Rule "${ruleId}" fired${rule ? `: ${rule.description}` : ''}`,
61
+ });
62
+
63
+ // What does this rule consume?
64
+ const consumed = graph.consumers.get(ruleId) ?? [];
65
+ for (const consumedTag of consumed) {
66
+ steps.unshift({
67
+ type: 'fact-read',
68
+ id: consumedTag,
69
+ description: `Rule "${ruleId}" reads fact "${consumedTag}"`,
70
+ });
71
+ walkBackward(consumedTag, depth + 1);
72
+ }
73
+
74
+ // What events trigger this rule?
75
+ if (rule?.eventTypes) {
76
+ const types = Array.isArray(rule.eventTypes) ? rule.eventTypes : [rule.eventTypes];
77
+ for (const eventType of types) {
78
+ steps.unshift({
79
+ type: 'event',
80
+ id: eventType,
81
+ description: `Event "${eventType}" triggers rule "${ruleId}"`,
82
+ });
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ walkBackward(factTag, 0);
89
+
90
+ // Deduplicate steps while preserving order
91
+ const seen = new Set<string>();
92
+ const dedupedSteps = steps.filter(step => {
93
+ const key = `${step.type}:${step.id}`;
94
+ if (seen.has(key)) return false;
95
+ seen.add(key);
96
+ return true;
97
+ });
98
+
99
+ return {
100
+ targetFact: factTag,
101
+ steps: dedupedSteps,
102
+ depth: dedupedSteps.filter(s => s.type === 'rule-fired').length,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Trace the impact of removing a fact from the system.
108
+ *
109
+ * Returns which rules would stop firing and which downstream facts
110
+ * would disappear if the given fact were removed.
111
+ */
112
+ export function traceImpact<TContext = unknown>(
113
+ factTag: string,
114
+ registry: PraxisRegistry<TContext>,
115
+ ): ImpactReport {
116
+ const graph = analyzeDependencyGraph(registry);
117
+ const affectedRules: string[] = [];
118
+ const affectedFacts: string[] = [];
119
+ const visited = new Set<string>();
120
+
121
+ function walkForward(tag: string, depth: number): number {
122
+ if (visited.has(tag) || depth > 20) return depth;
123
+ visited.add(tag);
124
+
125
+ const factNode = graph.facts.get(tag);
126
+ if (!factNode) return depth;
127
+
128
+ let maxDepth = depth;
129
+
130
+ // Find rules that consume this fact
131
+ for (const ruleId of factNode.consumedBy) {
132
+ if (!affectedRules.includes(ruleId)) {
133
+ affectedRules.push(ruleId);
134
+ }
135
+
136
+ // What facts do those rules produce? Those would also disappear.
137
+ const produced = graph.producers.get(ruleId) ?? [];
138
+ for (const producedTag of produced) {
139
+ if (producedTag !== factTag && !affectedFacts.includes(producedTag)) {
140
+ affectedFacts.push(producedTag);
141
+ const childDepth = walkForward(producedTag, depth + 1);
142
+ if (childDepth > maxDepth) maxDepth = childDepth;
143
+ }
144
+ }
145
+ }
146
+
147
+ return maxDepth;
148
+ }
149
+
150
+ const depth = walkForward(factTag, 0);
151
+
152
+ return {
153
+ factTag,
154
+ affectedRules,
155
+ affectedFacts,
156
+ depth,
157
+ };
158
+ }
@@ -68,3 +68,62 @@ export {
68
68
  type GenerationResult,
69
69
  generateContractFromRule,
70
70
  } from './reverse-generator.js';
71
+
72
+ // ─── Analyzer Engine ────────────────────────────────────────────────────────
73
+
74
+ export {
75
+ type FactNode,
76
+ type DependencyEdge,
77
+ type DependencyGraph,
78
+ type DerivationStep,
79
+ type DerivationChain,
80
+ type DeadRule,
81
+ type UnreachableState,
82
+ type ShadowedRule,
83
+ type Contradiction,
84
+ type Gap,
85
+ type ImpactReport,
86
+ type ExampleVerification,
87
+ type ContractVerificationResult,
88
+ type InvariantCheck,
89
+ type ContractCoverageGap,
90
+ type CrossReference,
91
+ type FindingType,
92
+ type Suggestion,
93
+ type AnalysisReport,
94
+ type LedgerDiffEntry,
95
+ type LedgerDiff,
96
+ } from './analyzer-types.js';
97
+
98
+ export {
99
+ analyzeDependencyGraph,
100
+ findDeadRules,
101
+ findUnreachableStates,
102
+ findShadowedRules,
103
+ findContradictions,
104
+ findGaps,
105
+ } from './analyzer.js';
106
+
107
+ export {
108
+ traceDerivation,
109
+ traceImpact,
110
+ } from './derivation.js';
111
+
112
+ export {
113
+ verifyContractExamples,
114
+ verifyInvariants,
115
+ findContractGaps,
116
+ crossReferenceContracts,
117
+ } from './contract-verification.js';
118
+
119
+ export {
120
+ suggest,
121
+ suggestAll,
122
+ } from './suggestions.js';
123
+
124
+ export {
125
+ generateLedger,
126
+ formatLedger,
127
+ formatBuildOutput,
128
+ diffLedgers,
129
+ } from './report.js';