@criterionx/devtools 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @criterionx/devtools
2
+
3
+ Development tools for debugging and visualizing Criterion decision traces.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @criterionx/devtools
9
+ # or
10
+ pnpm add @criterionx/devtools
11
+ ```
12
+
13
+ ## Features
14
+
15
+ - **Trace Collector** - Capture and analyze decision evaluations
16
+ - **HTML Report** - Visual trace viewer with rule evaluation details
17
+ - **Export** - JSON, HTML, and Markdown export formats
18
+ - **Console Formatting** - Pretty-print traces for debugging
19
+ - **Statistics** - Summary analytics for collected traces
20
+
21
+ ## Usage
22
+
23
+ ### Collecting Traces
24
+
25
+ ```typescript
26
+ import { createCollector } from "@criterionx/devtools";
27
+ import { riskDecision } from "./decisions";
28
+
29
+ const collector = createCollector({
30
+ maxTraces: 1000, // Maximum traces to keep
31
+ autoLog: true, // Log to console automatically
32
+ });
33
+
34
+ // Use collector.run() instead of engine.run()
35
+ const result = collector.run(
36
+ riskDecision,
37
+ { amount: 50000, country: "US" },
38
+ { profile: defaultProfile }
39
+ );
40
+
41
+ // Get the last trace
42
+ const trace = collector.getLastTrace();
43
+ console.log(trace?.result.status); // "OK"
44
+ console.log(trace?.durationMs); // 0.42
45
+ ```
46
+
47
+ ### Generating HTML Reports
48
+
49
+ ```typescript
50
+ import { createCollector, generateHtmlReport } from "@criterionx/devtools";
51
+ import fs from "fs";
52
+
53
+ const collector = createCollector();
54
+
55
+ // Run some decisions
56
+ collector.run(riskDecision, input1, { profile });
57
+ collector.run(riskDecision, input2, { profile });
58
+ collector.run(riskDecision, input3, { profile });
59
+
60
+ // Generate HTML report
61
+ const html = generateHtmlReport(
62
+ collector.getTraces(),
63
+ collector.getSummary(),
64
+ {
65
+ title: "Risk Decision Traces",
66
+ darkMode: true,
67
+ }
68
+ );
69
+
70
+ fs.writeFileSync("traces.html", html);
71
+ ```
72
+
73
+ ### Console Output
74
+
75
+ ```typescript
76
+ import { createCollector, formatTraceForConsole } from "@criterionx/devtools";
77
+
78
+ const collector = createCollector();
79
+ collector.run(riskDecision, { amount: 100, country: "US" }, { profile });
80
+
81
+ const trace = collector.getLastTrace()!;
82
+ console.log(formatTraceForConsole(trace));
83
+
84
+ // Output:
85
+ // ──────────────────────────────────────────────────
86
+ // Decision: transaction-risk v1.0.0
87
+ // Status: OK
88
+ // Duration: 0.42ms
89
+ // Time: 2024-01-15T10:30:00.000Z
90
+ //
91
+ // Rule Evaluation:
92
+ // ✗ blocked-country
93
+ // ✗ high-amount
94
+ // ✓ default — Default low risk
95
+ //
96
+ // Result: Default low risk
97
+ // ──────────────────────────────────────────────────
98
+ ```
99
+
100
+ ### Exporting Traces
101
+
102
+ ```typescript
103
+ import { createCollector, exportToJson, exportTrace } from "@criterionx/devtools";
104
+
105
+ const collector = createCollector();
106
+ // ... run decisions ...
107
+
108
+ // Export all traces as JSON
109
+ const json = exportToJson(collector.getTraces(), {
110
+ pretty: true,
111
+ includeData: true, // Include input/output data
112
+ });
113
+
114
+ // Export single trace as markdown
115
+ const trace = collector.getLastTrace()!;
116
+ const markdown = exportTrace(trace, "markdown");
117
+ ```
118
+
119
+ ### Statistics
120
+
121
+ ```typescript
122
+ const collector = createCollector();
123
+ // ... run multiple decisions ...
124
+
125
+ const summary = collector.getSummary();
126
+
127
+ console.log(summary.totalTraces); // 150
128
+ console.log(summary.avgDurationMs); // 0.38
129
+ console.log(summary.byStatus); // { OK: 145, NO_MATCH: 5 }
130
+ console.log(summary.byRule); // { "high-risk": 50, "default": 95, ... }
131
+ console.log(summary.byDecision); // { "transaction-risk": 100, ... }
132
+ ```
133
+
134
+ ## API Reference
135
+
136
+ ### `createCollector(options?)`
137
+
138
+ Create a new trace collector.
139
+
140
+ **Options:**
141
+ - `maxTraces` - Maximum traces to keep (default: 1000)
142
+ - `autoLog` - Log traces to console (default: false)
143
+
144
+ **Methods:**
145
+ - `run(decision, input, options, registry?)` - Run decision and collect trace
146
+ - `getTraces()` - Get all collected traces
147
+ - `getTracesForDecision(id)` - Filter traces by decision ID
148
+ - `getLastTrace()` - Get most recent trace
149
+ - `getSummary()` - Get statistics summary
150
+ - `clear()` - Clear all traces
151
+ - `count` - Number of traces
152
+
153
+ ### `generateHtmlReport(traces, summary, options?)`
154
+
155
+ Generate an HTML report.
156
+
157
+ **Options:**
158
+ - `title` - Report title (default: "Criterion Trace Viewer")
159
+ - `darkMode` - Use dark theme (default: false)
160
+
161
+ ### `formatTraceForConsole(trace)`
162
+
163
+ Format a trace for console output.
164
+
165
+ ### `exportToJson(traces, options?)`
166
+
167
+ Export traces as JSON.
168
+
169
+ **Options:**
170
+ - `pretty` - Pretty print (default: true)
171
+ - `includeData` - Include input/output (default: true)
172
+
173
+ ### `exportToHtml(traces, summary, options?)`
174
+
175
+ Export traces as HTML (same as `generateHtmlReport`).
176
+
177
+ ### `exportTrace(trace, format)`
178
+
179
+ Export single trace as JSON or Markdown.
180
+
181
+ ### `createShareableUrl(traces, summary, options?)`
182
+
183
+ Create a data URI for sharing (base64 encoded HTML).
184
+
185
+ ## License
186
+
187
+ MIT
@@ -0,0 +1,141 @@
1
+ import { Result, Decision, RunOptions, ProfileRegistry } from '@criterionx/core';
2
+
3
+ /**
4
+ * A recorded trace of a decision evaluation
5
+ */
6
+ interface Trace<TInput = unknown, TOutput = unknown, TProfile = unknown> {
7
+ id: string;
8
+ timestamp: string;
9
+ decisionId: string;
10
+ decisionVersion: string;
11
+ input: TInput;
12
+ profile: TProfile;
13
+ profileId?: string;
14
+ result: Result<TOutput>;
15
+ durationMs: number;
16
+ }
17
+ /**
18
+ * Options for the trace collector
19
+ */
20
+ interface CollectorOptions {
21
+ /** Maximum number of traces to keep in memory */
22
+ maxTraces?: number;
23
+ /** Auto-flush traces to console */
24
+ autoLog?: boolean;
25
+ }
26
+ /**
27
+ * Options for exporting traces
28
+ */
29
+ interface ExportOptions {
30
+ /** Format to export */
31
+ format: "json" | "html";
32
+ /** Include input/output data */
33
+ includeData?: boolean;
34
+ /** Pretty print JSON */
35
+ pretty?: boolean;
36
+ }
37
+ /**
38
+ * Summary statistics for collected traces
39
+ */
40
+ interface TraceSummary {
41
+ totalTraces: number;
42
+ byDecision: Record<string, number>;
43
+ byStatus: Record<string, number>;
44
+ byRule: Record<string, number>;
45
+ avgDurationMs: number;
46
+ }
47
+ /**
48
+ * Options for the HTML viewer
49
+ */
50
+ interface ViewerOptions {
51
+ /** Title for the HTML report */
52
+ title?: string;
53
+ /** Include inline styles */
54
+ inlineStyles?: boolean;
55
+ /** Dark mode */
56
+ darkMode?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Trace Collector
61
+ *
62
+ * Wraps the Criterion engine to collect and analyze decision traces.
63
+ */
64
+ declare class TraceCollector {
65
+ private traces;
66
+ private readonly maxTraces;
67
+ private readonly autoLog;
68
+ private readonly engine;
69
+ constructor(options?: CollectorOptions);
70
+ /**
71
+ * Run a decision and collect the trace
72
+ */
73
+ run<TInput, TOutput, TProfile>(decision: Decision<TInput, TOutput, TProfile>, input: TInput, options: RunOptions<TProfile>, registry?: ProfileRegistry<TProfile>): Result<TOutput>;
74
+ /**
75
+ * Add a trace to the collection
76
+ */
77
+ private addTrace;
78
+ /**
79
+ * Log a trace to console
80
+ */
81
+ private logTrace;
82
+ /**
83
+ * Get all collected traces
84
+ */
85
+ getTraces(): Trace[];
86
+ /**
87
+ * Get traces for a specific decision
88
+ */
89
+ getTracesForDecision(decisionId: string): Trace[];
90
+ /**
91
+ * Get the most recent trace
92
+ */
93
+ getLastTrace(): Trace | undefined;
94
+ /**
95
+ * Get summary statistics
96
+ */
97
+ getSummary(): TraceSummary;
98
+ /**
99
+ * Clear all traces
100
+ */
101
+ clear(): void;
102
+ /**
103
+ * Get trace count
104
+ */
105
+ get count(): number;
106
+ }
107
+ /**
108
+ * Create a new trace collector
109
+ */
110
+ declare function createCollector(options?: CollectorOptions): TraceCollector;
111
+
112
+ /**
113
+ * Generate a complete HTML report for traces
114
+ */
115
+ declare function generateHtmlReport(traces: Trace[], summary: TraceSummary, options?: ViewerOptions): string;
116
+ /**
117
+ * Format a trace for console output
118
+ */
119
+ declare function formatTraceForConsole(trace: Trace): string;
120
+
121
+ /**
122
+ * Export traces to JSON format
123
+ */
124
+ declare function exportToJson(traces: Trace[], options?: {
125
+ pretty?: boolean;
126
+ includeData?: boolean;
127
+ }): string;
128
+ /**
129
+ * Export traces to HTML format
130
+ */
131
+ declare function exportToHtml(traces: Trace[], summary: TraceSummary, options?: ViewerOptions): string;
132
+ /**
133
+ * Export a single trace for debugging
134
+ */
135
+ declare function exportTrace(trace: Trace, format?: "json" | "markdown"): string;
136
+ /**
137
+ * Create a shareable report URL (data URI)
138
+ */
139
+ declare function createShareableUrl(traces: Trace[], summary: TraceSummary, options?: ViewerOptions): string;
140
+
141
+ export { type CollectorOptions, type ExportOptions, type Trace, TraceCollector, type TraceSummary, type ViewerOptions, createCollector, createShareableUrl, exportToHtml, exportToJson, exportTrace, formatTraceForConsole, generateHtmlReport };
package/dist/index.js ADDED
@@ -0,0 +1,415 @@
1
+ // src/collector.ts
2
+ import {
3
+ Engine
4
+ } from "@criterionx/core";
5
+ function generateId() {
6
+ return `trace_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
7
+ }
8
+ var TraceCollector = class {
9
+ traces = [];
10
+ maxTraces;
11
+ autoLog;
12
+ engine;
13
+ constructor(options = {}) {
14
+ this.maxTraces = options.maxTraces ?? 1e3;
15
+ this.autoLog = options.autoLog ?? false;
16
+ this.engine = new Engine();
17
+ }
18
+ /**
19
+ * Run a decision and collect the trace
20
+ */
21
+ run(decision, input, options, registry) {
22
+ const startTime = performance.now();
23
+ const result = this.engine.run(decision, input, options, registry);
24
+ const endTime = performance.now();
25
+ const durationMs = endTime - startTime;
26
+ const trace = {
27
+ id: generateId(),
28
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
29
+ decisionId: decision.id,
30
+ decisionVersion: decision.version,
31
+ input,
32
+ profile: typeof options.profile === "string" ? registry?.get(options.profile) : options.profile,
33
+ profileId: typeof options.profile === "string" ? options.profile : void 0,
34
+ result,
35
+ durationMs
36
+ };
37
+ this.addTrace(trace);
38
+ if (this.autoLog) {
39
+ this.logTrace(trace);
40
+ }
41
+ return result;
42
+ }
43
+ /**
44
+ * Add a trace to the collection
45
+ */
46
+ addTrace(trace) {
47
+ this.traces.push(trace);
48
+ if (this.traces.length > this.maxTraces) {
49
+ this.traces = this.traces.slice(-this.maxTraces);
50
+ }
51
+ }
52
+ /**
53
+ * Log a trace to console
54
+ */
55
+ logTrace(trace) {
56
+ const status = trace.result.status;
57
+ const rule = trace.result.meta.matchedRule ?? "none";
58
+ const duration = trace.durationMs.toFixed(2);
59
+ console.log(
60
+ `[Criterion] ${trace.decisionId} | ${status} | rule: ${rule} | ${duration}ms`
61
+ );
62
+ }
63
+ /**
64
+ * Get all collected traces
65
+ */
66
+ getTraces() {
67
+ return [...this.traces];
68
+ }
69
+ /**
70
+ * Get traces for a specific decision
71
+ */
72
+ getTracesForDecision(decisionId) {
73
+ return this.traces.filter((t) => t.decisionId === decisionId);
74
+ }
75
+ /**
76
+ * Get the most recent trace
77
+ */
78
+ getLastTrace() {
79
+ return this.traces[this.traces.length - 1];
80
+ }
81
+ /**
82
+ * Get summary statistics
83
+ */
84
+ getSummary() {
85
+ const byDecision = {};
86
+ const byStatus = {};
87
+ const byRule = {};
88
+ let totalDuration = 0;
89
+ for (const trace of this.traces) {
90
+ byDecision[trace.decisionId] = (byDecision[trace.decisionId] ?? 0) + 1;
91
+ byStatus[trace.result.status] = (byStatus[trace.result.status] ?? 0) + 1;
92
+ const rule = trace.result.meta.matchedRule ?? "NO_MATCH";
93
+ byRule[rule] = (byRule[rule] ?? 0) + 1;
94
+ totalDuration += trace.durationMs;
95
+ }
96
+ return {
97
+ totalTraces: this.traces.length,
98
+ byDecision,
99
+ byStatus,
100
+ byRule,
101
+ avgDurationMs: this.traces.length > 0 ? totalDuration / this.traces.length : 0
102
+ };
103
+ }
104
+ /**
105
+ * Clear all traces
106
+ */
107
+ clear() {
108
+ this.traces = [];
109
+ }
110
+ /**
111
+ * Get trace count
112
+ */
113
+ get count() {
114
+ return this.traces.length;
115
+ }
116
+ };
117
+ function createCollector(options) {
118
+ return new TraceCollector(options);
119
+ }
120
+
121
+ // src/viewer.ts
122
+ function generateStyles(darkMode) {
123
+ const bg = darkMode ? "#1a1a2e" : "#ffffff";
124
+ const bgCard = darkMode ? "#16213e" : "#f8f9fa";
125
+ const text = darkMode ? "#eaeaea" : "#212529";
126
+ const textMuted = darkMode ? "#a0a0a0" : "#6c757d";
127
+ const border = darkMode ? "#0f3460" : "#dee2e6";
128
+ const success = "#28a745";
129
+ const danger = "#dc3545";
130
+ const warning = "#ffc107";
131
+ return `
132
+ * { box-sizing: border-box; margin: 0; padding: 0; }
133
+ body {
134
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
135
+ background: ${bg};
136
+ color: ${text};
137
+ line-height: 1.6;
138
+ padding: 2rem;
139
+ }
140
+ .container { max-width: 1200px; margin: 0 auto; }
141
+ h1 { margin-bottom: 1.5rem; font-size: 1.75rem; }
142
+ h2 { margin: 1.5rem 0 1rem; font-size: 1.25rem; color: ${textMuted}; }
143
+ .summary {
144
+ display: grid;
145
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
146
+ gap: 1rem;
147
+ margin-bottom: 2rem;
148
+ }
149
+ .stat {
150
+ background: ${bgCard};
151
+ border: 1px solid ${border};
152
+ border-radius: 8px;
153
+ padding: 1rem;
154
+ }
155
+ .stat-value { font-size: 2rem; font-weight: bold; }
156
+ .stat-label { color: ${textMuted}; font-size: 0.875rem; }
157
+ .trace {
158
+ background: ${bgCard};
159
+ border: 1px solid ${border};
160
+ border-radius: 8px;
161
+ margin-bottom: 1rem;
162
+ overflow: hidden;
163
+ }
164
+ .trace-header {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ align-items: center;
168
+ padding: 1rem;
169
+ border-bottom: 1px solid ${border};
170
+ cursor: pointer;
171
+ }
172
+ .trace-header:hover { background: ${darkMode ? "#1f2b4a" : "#f1f3f4"}; }
173
+ .trace-title { font-weight: 600; }
174
+ .trace-meta { display: flex; gap: 1rem; font-size: 0.875rem; color: ${textMuted}; }
175
+ .badge {
176
+ display: inline-block;
177
+ padding: 0.25rem 0.5rem;
178
+ border-radius: 4px;
179
+ font-size: 0.75rem;
180
+ font-weight: 600;
181
+ }
182
+ .badge-ok { background: ${success}; color: white; }
183
+ .badge-no-match { background: ${warning}; color: black; }
184
+ .badge-invalid { background: ${danger}; color: white; }
185
+ .trace-body { padding: 1rem; display: none; }
186
+ .trace.open .trace-body { display: block; }
187
+ .trace-section { margin-bottom: 1rem; }
188
+ .trace-section-title { font-weight: 600; margin-bottom: 0.5rem; font-size: 0.875rem; }
189
+ .rules-list { list-style: none; }
190
+ .rule {
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 0.5rem;
194
+ padding: 0.5rem;
195
+ border-radius: 4px;
196
+ margin-bottom: 0.25rem;
197
+ }
198
+ .rule-matched { background: ${darkMode ? "#1a3a2a" : "#d4edda"}; }
199
+ .rule-icon { font-size: 1rem; }
200
+ .rule-id { font-weight: 500; }
201
+ .rule-explain { color: ${textMuted}; font-size: 0.875rem; }
202
+ pre {
203
+ background: ${darkMode ? "#0d1117" : "#f6f8fa"};
204
+ border: 1px solid ${border};
205
+ border-radius: 4px;
206
+ padding: 1rem;
207
+ overflow-x: auto;
208
+ font-size: 0.875rem;
209
+ }
210
+ code { font-family: 'SF Mono', Monaco, Consolas, monospace; }
211
+ `;
212
+ }
213
+ function renderTrace(trace, index) {
214
+ const statusClass = trace.result.status === "OK" ? "badge-ok" : trace.result.status === "NO_MATCH" ? "badge-no-match" : "badge-invalid";
215
+ const rules = trace.result.meta.evaluatedRules.map(
216
+ (rule) => `
217
+ <li class="rule ${rule.matched ? "rule-matched" : ""}">
218
+ <span class="rule-icon">${rule.matched ? "\u2713" : "\u2717"}</span>
219
+ <span class="rule-id">${rule.ruleId}</span>
220
+ ${rule.explanation ? `<span class="rule-explain">\u2014 ${rule.explanation}</span>` : ""}
221
+ </li>
222
+ `
223
+ ).join("");
224
+ return `
225
+ <div class="trace" id="trace-${index}">
226
+ <div class="trace-header" onclick="toggleTrace(${index})">
227
+ <div>
228
+ <span class="trace-title">${trace.decisionId}</span>
229
+ <span class="badge ${statusClass}">${trace.result.status}</span>
230
+ </div>
231
+ <div class="trace-meta">
232
+ <span>${trace.result.meta.matchedRule ?? "No match"}</span>
233
+ <span>${trace.durationMs.toFixed(2)}ms</span>
234
+ <span>${new Date(trace.timestamp).toLocaleTimeString()}</span>
235
+ </div>
236
+ </div>
237
+ <div class="trace-body">
238
+ <div class="trace-section">
239
+ <div class="trace-section-title">Rule Evaluation</div>
240
+ <ul class="rules-list">${rules}</ul>
241
+ </div>
242
+ <div class="trace-section">
243
+ <div class="trace-section-title">Input</div>
244
+ <pre><code>${JSON.stringify(trace.input, null, 2)}</code></pre>
245
+ </div>
246
+ ${trace.result.data ? `
247
+ <div class="trace-section">
248
+ <div class="trace-section-title">Output</div>
249
+ <pre><code>${JSON.stringify(trace.result.data, null, 2)}</code></pre>
250
+ </div>
251
+ ` : ""}
252
+ <div class="trace-section">
253
+ <div class="trace-section-title">Explanation</div>
254
+ <p>${trace.result.meta.explanation}</p>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ `;
259
+ }
260
+ function renderSummary(summary) {
261
+ return `
262
+ <div class="summary">
263
+ <div class="stat">
264
+ <div class="stat-value">${summary.totalTraces}</div>
265
+ <div class="stat-label">Total Traces</div>
266
+ </div>
267
+ <div class="stat">
268
+ <div class="stat-value">${summary.avgDurationMs.toFixed(2)}ms</div>
269
+ <div class="stat-label">Avg Duration</div>
270
+ </div>
271
+ <div class="stat">
272
+ <div class="stat-value">${summary.byStatus["OK"] ?? 0}</div>
273
+ <div class="stat-label">Successful</div>
274
+ </div>
275
+ <div class="stat">
276
+ <div class="stat-value">${(summary.byStatus["NO_MATCH"] ?? 0) + (summary.byStatus["INVALID_INPUT"] ?? 0) + (summary.byStatus["INVALID_OUTPUT"] ?? 0)}</div>
277
+ <div class="stat-label">Failed/No Match</div>
278
+ </div>
279
+ </div>
280
+ `;
281
+ }
282
+ function generateHtmlReport(traces, summary, options = {}) {
283
+ const title = options.title ?? "Criterion Trace Viewer";
284
+ const darkMode = options.darkMode ?? false;
285
+ const tracesHtml = traces.slice().reverse().map((t, i) => renderTrace(t, i)).join("");
286
+ return `<!DOCTYPE html>
287
+ <html lang="en">
288
+ <head>
289
+ <meta charset="UTF-8">
290
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
291
+ <title>${title}</title>
292
+ <style>${generateStyles(darkMode)}</style>
293
+ </head>
294
+ <body>
295
+ <div class="container">
296
+ <h1>${title}</h1>
297
+
298
+ <h2>Summary</h2>
299
+ ${renderSummary(summary)}
300
+
301
+ <h2>Traces (${traces.length})</h2>
302
+ ${tracesHtml || "<p>No traces recorded yet.</p>"}
303
+ </div>
304
+
305
+ <script>
306
+ function toggleTrace(index) {
307
+ const trace = document.getElementById('trace-' + index);
308
+ trace.classList.toggle('open');
309
+ }
310
+ </script>
311
+ </body>
312
+ </html>`;
313
+ }
314
+ function formatTraceForConsole(trace) {
315
+ const lines = [];
316
+ const hr = "\u2500".repeat(50);
317
+ lines.push(hr);
318
+ lines.push(`Decision: ${trace.decisionId} v${trace.decisionVersion}`);
319
+ lines.push(`Status: ${trace.result.status}`);
320
+ lines.push(`Duration: ${trace.durationMs.toFixed(2)}ms`);
321
+ lines.push(`Time: ${trace.timestamp}`);
322
+ lines.push("");
323
+ lines.push("Rule Evaluation:");
324
+ for (const rule of trace.result.meta.evaluatedRules) {
325
+ const icon = rule.matched ? "\u2713" : "\u2717";
326
+ const explain = rule.explanation ? ` \u2014 ${rule.explanation}` : "";
327
+ lines.push(` ${icon} ${rule.ruleId}${explain}`);
328
+ }
329
+ lines.push("");
330
+ lines.push(`Result: ${trace.result.meta.explanation}`);
331
+ lines.push(hr);
332
+ return lines.join("\n");
333
+ }
334
+
335
+ // src/export.ts
336
+ function exportToJson(traces, options = {}) {
337
+ const { pretty = true, includeData = true } = options;
338
+ const exportData = traces.map((trace) => {
339
+ if (!includeData) {
340
+ return {
341
+ id: trace.id,
342
+ timestamp: trace.timestamp,
343
+ decisionId: trace.decisionId,
344
+ decisionVersion: trace.decisionVersion,
345
+ status: trace.result.status,
346
+ matchedRule: trace.result.meta.matchedRule,
347
+ durationMs: trace.durationMs
348
+ };
349
+ }
350
+ return trace;
351
+ });
352
+ return pretty ? JSON.stringify(exportData, null, 2) : JSON.stringify(exportData);
353
+ }
354
+ function exportToHtml(traces, summary, options = {}) {
355
+ return generateHtmlReport(traces, summary, options);
356
+ }
357
+ function exportTrace(trace, format = "json") {
358
+ if (format === "markdown") {
359
+ return formatTraceAsMarkdown(trace);
360
+ }
361
+ return JSON.stringify(trace, null, 2);
362
+ }
363
+ function formatTraceAsMarkdown(trace) {
364
+ const lines = [];
365
+ lines.push(`# Trace: ${trace.decisionId}`);
366
+ lines.push("");
367
+ lines.push(`- **Version:** ${trace.decisionVersion}`);
368
+ lines.push(`- **Status:** ${trace.result.status}`);
369
+ lines.push(`- **Duration:** ${trace.durationMs.toFixed(2)}ms`);
370
+ lines.push(`- **Timestamp:** ${trace.timestamp}`);
371
+ lines.push("");
372
+ lines.push("## Rule Evaluation");
373
+ lines.push("");
374
+ lines.push("| Rule | Matched | Explanation |");
375
+ lines.push("|------|---------|-------------|");
376
+ for (const rule of trace.result.meta.evaluatedRules) {
377
+ const matched = rule.matched ? "Yes" : "No";
378
+ const explain = rule.explanation ?? "-";
379
+ lines.push(`| ${rule.ruleId} | ${matched} | ${explain} |`);
380
+ }
381
+ lines.push("");
382
+ lines.push("## Input");
383
+ lines.push("");
384
+ lines.push("```json");
385
+ lines.push(JSON.stringify(trace.input, null, 2));
386
+ lines.push("```");
387
+ lines.push("");
388
+ if (trace.result.data) {
389
+ lines.push("## Output");
390
+ lines.push("");
391
+ lines.push("```json");
392
+ lines.push(JSON.stringify(trace.result.data, null, 2));
393
+ lines.push("```");
394
+ lines.push("");
395
+ }
396
+ lines.push("## Explanation");
397
+ lines.push("");
398
+ lines.push(trace.result.meta.explanation);
399
+ return lines.join("\n");
400
+ }
401
+ function createShareableUrl(traces, summary, options = {}) {
402
+ const html = generateHtmlReport(traces, summary, options);
403
+ const encoded = Buffer.from(html).toString("base64");
404
+ return `data:text/html;base64,${encoded}`;
405
+ }
406
+ export {
407
+ TraceCollector,
408
+ createCollector,
409
+ createShareableUrl,
410
+ exportToHtml,
411
+ exportToJson,
412
+ exportTrace,
413
+ formatTraceForConsole,
414
+ generateHtmlReport
415
+ };
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@criterionx/devtools",
3
+ "version": "0.3.1",
4
+ "description": "Development tools for debugging Criterion decisions",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --dts --clean",
15
+ "dev": "tsup src/index.ts --format esm --dts --watch",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "test:coverage": "vitest run --coverage",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "keywords": [
22
+ "criterion",
23
+ "devtools",
24
+ "debugging",
25
+ "decision-engine",
26
+ "trace-viewer"
27
+ ],
28
+ "author": {
29
+ "name": "Tomas Maritano",
30
+ "url": "https://github.com/tomymaritano"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/tomymaritano/criterionx.git",
36
+ "directory": "packages/devtools"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/tomymaritano/criterionx/issues"
40
+ },
41
+ "homepage": "https://github.com/tomymaritano/criterionx#readme",
42
+ "dependencies": {
43
+ "@criterionx/core": "workspace:*"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.0.0",
47
+ "tsup": "^8.0.0",
48
+ "typescript": "^5.3.0",
49
+ "vitest": "^4.0.0",
50
+ "zod": "^3.23.0"
51
+ },
52
+ "peerDependencies": {
53
+ "zod": "^3.0.0"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ }
58
+ }