1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
/**
* receipt.ts
* Execution receipt builder and types
* Tracks execution metadata, timing, and outputs for audit trail and debugging
*/
/**
* Builder for constructing execution receipts
* Accumulates timing, outputs, and metadata during pipeline execution
* Provides a fluent interface for receipt construction
*/
export class ReceiptBuilder {
constructor(config) {
this.startTimestamp = null;
this.stepTimings = new Map();
this.outputs = new Map();
this.executedSteps = [];
this.config = config;
this.sourceFormat = config.source.format;
}
/**
* Records the start time of execution
* Must be called once at the beginning of pipeline execution
*/
start() {
this.startTimestamp = new Date();
return this;
}
/**
* Records timing for a single pipeline step
* Accumulates timing information for the final receipt
* Steps should be recorded in execution order
*
* @param stepId - Unique identifier of the step
* @param durationMs - Duration of step execution in milliseconds
*/
recordStep(stepId, durationMs) {
this.stepTimings.set(stepId, durationMs);
if (!this.executedSteps.includes(stepId)) {
this.executedSteps.push(stepId);
}
return this;
}
/**
* Sets the final outputs from pipeline execution
* Typically called after all steps complete
* Outputs are keyed by step ID or result name
*
* @param outputs - Map of step outputs keyed by step ID
*/
setOutputs(outputs) {
this.outputs = new Map(Object.entries(outputs));
return this;
}
/**
* Records the size of input data in bytes
* Used for performance analysis
*
* @param sizeBytes - Size of input data in bytes
*/
setInputDataSize(sizeBytes) {
this.inputDataSize = sizeBytes;
return this;
}
/**
* Records the size of output data in bytes
* Used for performance analysis
*
* @param sizeBytes - Size of output data in bytes
*/
setOutputDataSize(sizeBytes) {
this.outputDataSize = sizeBytes;
return this;
}
/**
* Constructs the final execution receipt
* Must be called after start() and ideally after recordStep() calls and setOutputs()
*
* @returns Complete ExecutionReceipt
* @throws Error if start() was not called
*/
build() {
if (!this.startTimestamp) {
throw new Error('ReceiptBuilder.start() must be called before build()');
}
const finishTimestamp = new Date();
const totalMs = finishTimestamp.getTime() - this.startTimestamp.getTime();
return {
runId: generateRunId(),
engineVersion: '0.5.4',
configHash: hashConfig(this.config),
profile: this.config.execution.profile,
pipeline: this.executedSteps,
timing: {
total_ms: totalMs,
steps: Object.fromEntries(this.stepTimings),
},
outputs: Object.fromEntries(this.outputs),
receipt: {
startedAt: this.startTimestamp.toISOString(),
finishedAt: finishTimestamp.toISOString(),
inputDataSize: this.inputDataSize,
outputDataSize: this.outputDataSize,
sourceFormat: this.sourceFormat,
},
};
}
}
/**
* Generates a unique run identifier
* Format: "run_<ISO_timestamp>_<random4>"
* Example: "run_2026-04-04T17:30:45.123Z_a7b2"
*
* @returns Unique run ID string
*/
export function generateRunId() {
const timestamp = new Date().toISOString();
const random = Math.random().toString(16).substring(2, 6);
return `run_${timestamp}_${random}`;
}
/**
* Computes a deterministic hash of a configuration object
* Uses sorted JSON stringification for consistency
* Hash is used for caching and deduplication
*
* @param config - Configuration to hash
* @returns Hex string hash (32 chars, like MD5)
*/
export function hashConfig(config) {
// Create a deterministic representation by sorting keys
const sortedConfig = sortObjectKeys(config);
const jsonStr = JSON.stringify(sortedConfig);
// Simple hash function (simulating MD5-like output)
// In production, would use crypto.subtle.digest('SHA-256', ...)
return simpleHash(jsonStr);
}
/**
* Recursively sorts all keys in an object to ensure deterministic JSON representation
* Handles nested objects and arrays
*/
function sortObjectKeys(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => sortObjectKeys(item));
}
const sorted = {};
const keys = Object.keys(obj).sort();
for (const key of keys) {
sorted[key] = sortObjectKeys(obj[key]);
}
return sorted;
}
/**
* Simple hash function for deterministic string hashing
* Produces a 32-character hex string similar to MD5/SHA output
* NOT cryptographically secure - for content addressing only
*
* @param str - String to hash
* @returns 32-character hex string
*/
function simpleHash(str) {
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) + hash + str.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
// Convert to hex and pad to 32 chars
let hex = (hash >>> 0).toString(16);
while (hex.length < 32) {
hex = '0' + hex;
}
return hex.substring(0, 32);
}
/**
* Formats an ExecutionReceipt for console logging
* Provides human-readable summary of execution with timing and metadata
*
* @param receipt - Receipt to format
* @returns Formatted string representation suitable for console output
*/
export function formatReceipt(receipt) {
const lines = [];
lines.push('=== Execution Receipt ===');
lines.push(`Run ID: ${receipt.runId}`);
lines.push(`Engine: v${receipt.engineVersion}`);
lines.push(`Config Hash: ${receipt.configHash}`);
lines.push(`Profile: ${receipt.profile}`);
lines.push('');
lines.push('Pipeline Execution:');
for (const step of receipt.pipeline) {
const timing = receipt.timing.steps[step];
const timingStr = timing !== undefined ? `${timing}ms` : 'N/A';
lines.push(` - ${step}: ${timingStr}`);
}
lines.push('');
lines.push('Timing:');
lines.push(` Total: ${receipt.timing.total_ms}ms`);
if (receipt.pipeline.length > 0) {
const avgStepTime = receipt.timing.total_ms / receipt.pipeline.length;
lines.push(` Average per step: ${Math.round(avgStepTime)}ms`);
}
lines.push('');
lines.push('Metadata:');
if (receipt.receipt.sourceFormat) {
lines.push(` Source Format: ${receipt.receipt.sourceFormat}`);
}
if (receipt.receipt.inputDataSize !== undefined) {
lines.push(` Input Size: ${formatBytes(receipt.receipt.inputDataSize)}`);
}
if (receipt.receipt.outputDataSize !== undefined) {
lines.push(` Output Size: ${formatBytes(receipt.receipt.outputDataSize)}`);
}
lines.push('');
lines.push('Timestamps:');
lines.push(` Started: ${receipt.receipt.startedAt}`);
lines.push(` Finished: ${receipt.receipt.finishedAt}`);
return lines.join('\n');
}
/**
* Formats a byte count as a human-readable string
*/
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i];
}
/**
* Creates a compressed representation of receipt for storage
* Useful for logging to files or databases
*
* @param receipt - Receipt to compress
* @returns Compressed JSON string
*/
export function compressReceipt(receipt) {
// Omit outputs if they're too large (>1MB when stringified)
const outputsStr = JSON.stringify(receipt.outputs);
if (outputsStr.length > 1000000) {
const compressed = {
...receipt,
outputs: {
_note: 'Outputs omitted (too large)',
size_bytes: outputsStr.length,
},
};
return JSON.stringify(compressed);
}
return JSON.stringify(receipt);
}
/**
* Parses a compressed receipt from JSON string
* Restores type information
*
* @param json - JSON string representation of receipt
* @returns Parsed ExecutionReceipt
*/
export function parseReceipt(json) {
return JSON.parse(json);
}
//# sourceMappingURL=receipt.js.map