taist 1.1.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.
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Execution Tracer - Runtime execution tracing without explicit logging
3
+ * Captures function calls, arguments, return values, and errors
4
+ */
5
+
6
+ export class ExecutionTracer {
7
+ constructor(options = {}) {
8
+ this.enabled = options.enabled !== false;
9
+ this.depth = options.depth || 2;
10
+ this.maxEntries = options.maxEntries || 1000;
11
+
12
+ // Trace buffer (circular buffer)
13
+ this.traces = [];
14
+ this.currentIndex = 0;
15
+
16
+ // Call stack tracking
17
+ this.callStack = [];
18
+ this.currentDepth = 0;
19
+
20
+ // Performance tracking
21
+ this.startTimes = new Map();
22
+ }
23
+
24
+ /**
25
+ * Start tracing
26
+ */
27
+ start() {
28
+ this.enabled = true;
29
+ this.traces = [];
30
+ this.currentIndex = 0;
31
+ this.callStack = [];
32
+ this.currentDepth = 0;
33
+ }
34
+
35
+ /**
36
+ * Stop tracing
37
+ */
38
+ stop() {
39
+ this.enabled = false;
40
+ }
41
+
42
+ /**
43
+ * Clear all traces
44
+ */
45
+ clear() {
46
+ this.traces = [];
47
+ this.currentIndex = 0;
48
+ this.callStack = [];
49
+ this.currentDepth = 0;
50
+ this.startTimes.clear();
51
+ }
52
+
53
+ /**
54
+ * Get all traces
55
+ */
56
+ getTraces() {
57
+ return this.traces.filter(t => t !== undefined);
58
+ }
59
+
60
+ /**
61
+ * Record function entry
62
+ */
63
+ enter(name, args = []) {
64
+ if (!this.enabled) return;
65
+ if (this.currentDepth >= this.depth) return;
66
+
67
+ const traceId = this.generateId();
68
+ const entry = {
69
+ id: traceId,
70
+ name,
71
+ type: 'enter',
72
+ args: this.shouldCaptureArgs() ? this.sanitizeArgs(args) : undefined,
73
+ depth: this.currentDepth,
74
+ timestamp: Date.now()
75
+ };
76
+
77
+ this.callStack.push(traceId);
78
+ this.currentDepth++;
79
+ this.startTimes.set(traceId, performance.now());
80
+
81
+ this.addTrace(entry);
82
+
83
+ return traceId;
84
+ }
85
+
86
+ /**
87
+ * Record function exit
88
+ */
89
+ exit(name, result) {
90
+ if (!this.enabled) return;
91
+ if (this.callStack.length === 0) return;
92
+
93
+ const traceId = this.callStack.pop();
94
+ this.currentDepth = Math.max(0, this.currentDepth - 1);
95
+
96
+ const startTime = this.startTimes.get(traceId);
97
+ const duration = startTime ? performance.now() - startTime : 0;
98
+ this.startTimes.delete(traceId);
99
+
100
+ const entry = {
101
+ id: traceId,
102
+ name,
103
+ type: 'exit',
104
+ result: this.shouldCaptureResult() ? this.sanitizeValue(result) : undefined,
105
+ duration,
106
+ depth: this.currentDepth,
107
+ timestamp: Date.now()
108
+ };
109
+
110
+ this.addTrace(entry);
111
+ }
112
+
113
+ /**
114
+ * Record an error
115
+ */
116
+ error(name, error) {
117
+ if (!this.enabled) return;
118
+
119
+ const traceId = this.callStack.length > 0 ? this.callStack[this.callStack.length - 1] : this.generateId();
120
+
121
+ const entry = {
122
+ id: traceId,
123
+ name,
124
+ type: 'error',
125
+ error: this.sanitizeError(error),
126
+ depth: this.currentDepth,
127
+ timestamp: Date.now()
128
+ };
129
+
130
+ this.addTrace(entry);
131
+ }
132
+
133
+ /**
134
+ * Record a custom event
135
+ */
136
+ event(name, data = {}) {
137
+ if (!this.enabled) return;
138
+
139
+ const entry = {
140
+ id: this.generateId(),
141
+ name,
142
+ type: 'event',
143
+ data: this.sanitizeValue(data),
144
+ depth: this.currentDepth,
145
+ timestamp: Date.now()
146
+ };
147
+
148
+ this.addTrace(entry);
149
+ }
150
+
151
+ /**
152
+ * Wrap a function with tracing
153
+ */
154
+ wrap(fn, name) {
155
+ if (!this.enabled) return fn;
156
+
157
+ const tracer = this;
158
+ return new Proxy(fn, {
159
+ apply(target, thisArg, args) {
160
+ tracer.enter(name || fn.name || 'anonymous', args);
161
+
162
+ try {
163
+ const result = Reflect.apply(target, thisArg, args);
164
+
165
+ // Handle promises
166
+ if (result && typeof result.then === 'function') {
167
+ return result
168
+ .then(value => {
169
+ tracer.exit(name || fn.name || 'anonymous', value);
170
+ return value;
171
+ })
172
+ .catch(error => {
173
+ tracer.error(name || fn.name || 'anonymous', error);
174
+ throw error;
175
+ });
176
+ }
177
+
178
+ tracer.exit(name || fn.name || 'anonymous', result);
179
+ return result;
180
+ } catch (error) {
181
+ tracer.error(name || fn.name || 'anonymous', error);
182
+ throw error;
183
+ }
184
+ }
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Add trace to buffer (circular buffer)
190
+ */
191
+ addTrace(entry) {
192
+ if (this.traces.length < this.maxEntries) {
193
+ this.traces.push(entry);
194
+ } else {
195
+ this.traces[this.currentIndex] = entry;
196
+ this.currentIndex = (this.currentIndex + 1) % this.maxEntries;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Generate unique ID
202
+ */
203
+ generateId() {
204
+ return `t${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
205
+ }
206
+
207
+ /**
208
+ * Determine if should capture args based on depth level
209
+ */
210
+ shouldCaptureArgs() {
211
+ return this.depth >= 3;
212
+ }
213
+
214
+ /**
215
+ * Determine if should capture result based on depth level
216
+ */
217
+ shouldCaptureResult() {
218
+ return this.depth >= 2;
219
+ }
220
+
221
+ /**
222
+ * Sanitize arguments for storage
223
+ */
224
+ sanitizeArgs(args) {
225
+ if (!Array.isArray(args)) return [];
226
+ return args.map(arg => this.sanitizeValue(arg));
227
+ }
228
+
229
+ /**
230
+ * Sanitize a value for storage
231
+ */
232
+ sanitizeValue(value, maxDepth = 2, currentDepth = 0) {
233
+ if (currentDepth >= maxDepth) {
234
+ return '[deep]';
235
+ }
236
+
237
+ if (value === null) return null;
238
+ if (value === undefined) return undefined;
239
+
240
+ const type = typeof value;
241
+
242
+ if (type === 'string') {
243
+ return value.length > 100 ? value.slice(0, 100) + '...' : value;
244
+ }
245
+
246
+ if (type === 'number' || type === 'boolean') {
247
+ return value;
248
+ }
249
+
250
+ if (type === 'function') {
251
+ return `[Function: ${value.name || 'anonymous'}]`;
252
+ }
253
+
254
+ if (Array.isArray(value)) {
255
+ if (value.length === 0) return [];
256
+ if (value.length > 5) {
257
+ return [
258
+ ...value.slice(0, 5).map(v => this.sanitizeValue(v, maxDepth, currentDepth + 1)),
259
+ `...(${value.length - 5} more)`
260
+ ];
261
+ }
262
+ return value.map(v => this.sanitizeValue(v, maxDepth, currentDepth + 1));
263
+ }
264
+
265
+ if (type === 'object') {
266
+ if (value instanceof Error) {
267
+ return this.sanitizeError(value);
268
+ }
269
+
270
+ if (value instanceof Date) {
271
+ return value.toISOString();
272
+ }
273
+
274
+ // Regular object
275
+ const keys = Object.keys(value);
276
+ if (keys.length === 0) return {};
277
+ if (keys.length > 5) {
278
+ const result = {};
279
+ keys.slice(0, 5).forEach(key => {
280
+ result[key] = this.sanitizeValue(value[key], maxDepth, currentDepth + 1);
281
+ });
282
+ result['...'] = `(${keys.length - 5} more)`;
283
+ return result;
284
+ }
285
+
286
+ const result = {};
287
+ keys.forEach(key => {
288
+ result[key] = this.sanitizeValue(value[key], maxDepth, currentDepth + 1);
289
+ });
290
+ return result;
291
+ }
292
+
293
+ return String(value).slice(0, 50);
294
+ }
295
+
296
+ /**
297
+ * Sanitize error object
298
+ */
299
+ sanitizeError(error) {
300
+ if (typeof error === 'string') {
301
+ return { message: error };
302
+ }
303
+
304
+ return {
305
+ name: error.name || 'Error',
306
+ message: error.message || String(error),
307
+ stack: error.stack ? error.stack.split('\n').slice(0, 3) : undefined
308
+ };
309
+ }
310
+
311
+ /**
312
+ * Get summary of traces
313
+ */
314
+ getSummary() {
315
+ const traces = this.getTraces();
316
+
317
+ return {
318
+ total: traces.length,
319
+ byType: {
320
+ enter: traces.filter(t => t.type === 'enter').length,
321
+ exit: traces.filter(t => t.type === 'exit').length,
322
+ error: traces.filter(t => t.type === 'error').length,
323
+ event: traces.filter(t => t.type === 'event').length
324
+ },
325
+ errors: traces.filter(t => t.type === 'error'),
326
+ duration: traces.length > 0
327
+ ? traces[traces.length - 1].timestamp - traces[0].timestamp
328
+ : 0
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Export traces in TOON-friendly format
334
+ */
335
+ exportForToon() {
336
+ const traces = this.getTraces();
337
+
338
+ return traces
339
+ .filter(t => t.type === 'exit' || t.type === 'error')
340
+ .map(t => ({
341
+ name: t.name,
342
+ duration: t.duration,
343
+ args: t.args,
344
+ result: t.result,
345
+ error: t.error
346
+ }));
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Create and register a global tracer instance
352
+ * This allows instrumented code to access the tracer via globalThis.__taistTracer
353
+ * @param {Object} options - Tracer options
354
+ * @returns {ExecutionTracer}
355
+ */
356
+ export function createGlobalTracer(options = {}) {
357
+ const tracer = new ExecutionTracer(options);
358
+ globalThis.__taistTracer = tracer;
359
+ return tracer;
360
+ }
361
+
362
+ /**
363
+ * Get the global tracer instance
364
+ * @returns {ExecutionTracer|undefined}
365
+ */
366
+ export function getGlobalTracer() {
367
+ return globalThis.__taistTracer;
368
+ }
369
+
370
+ export default ExecutionTracer;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Taist Node.js Module Hooks - ESM Loader for code instrumentation
3
+ *
4
+ * This file implements Node.js module hooks to instrument code at load time.
5
+ * Use with: node --import ./lib/loader.js your-script.js
6
+ *
7
+ * The hooks intercept module loading and apply AST transformation to add
8
+ * tracing calls to all functions in the loaded code.
9
+ */
10
+
11
+ import { transformCode, defaultShouldInstrument } from './ast-transformer.js';
12
+ import { fileURLToPath } from 'url';
13
+ import { readFile } from 'fs/promises';
14
+
15
+ /**
16
+ * Resolve hook - determines how to resolve module specifiers
17
+ * We use default resolution, just passing through
18
+ */
19
+ export async function resolve(specifier, context, nextResolve) {
20
+ return nextResolve(specifier, context);
21
+ }
22
+
23
+ /**
24
+ * Load hook - transforms source code before it's executed
25
+ * This is where we inject instrumentation into the code
26
+ */
27
+ export async function load(url, context, nextLoad) {
28
+ // Only process file:// URLs (local files)
29
+ if (!url.startsWith('file://')) {
30
+ return nextLoad(url, context);
31
+ }
32
+
33
+ const filePath = fileURLToPath(url);
34
+
35
+ // Check if this file should be instrumented
36
+ if (!defaultShouldInstrument(filePath, { enabled: true })) {
37
+ return nextLoad(url, context);
38
+ }
39
+
40
+ // Load the original source
41
+ const result = await nextLoad(url, context);
42
+
43
+ // Only transform JavaScript/TypeScript modules
44
+ if (result.format !== 'module' && result.format !== 'commonjs') {
45
+ return result;
46
+ }
47
+
48
+ // Get the source code
49
+ let source;
50
+ if (result.source) {
51
+ source = typeof result.source === 'string'
52
+ ? result.source
53
+ : new TextDecoder().decode(result.source);
54
+ } else {
55
+ // If source isn't provided, read it from the file
56
+ source = await readFile(filePath, 'utf-8');
57
+ }
58
+
59
+ // Transform the code
60
+ const transformed = transformCode(source, filePath, { debug: process.env.TAIST_DEBUG });
61
+
62
+ if (transformed) {
63
+ return {
64
+ ...result,
65
+ source: transformed.code,
66
+ shortCircuit: true
67
+ };
68
+ }
69
+
70
+ return result;
71
+ }
package/lib/loader.js ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Taist Loader - Entry point for Node.js module instrumentation
3
+ *
4
+ * Usage:
5
+ * node --import ./lib/loader.js your-script.js
6
+ *
7
+ * Or set NODE_OPTIONS:
8
+ * NODE_OPTIONS="--import ./lib/loader.js" node your-script.js
9
+ *
10
+ * Environment variables:
11
+ * TAIST_TRACE_FILE - Path to write traces (required for trace collection)
12
+ * TAIST_DEBUG - Enable debug output
13
+ */
14
+
15
+ import { register } from 'node:module';
16
+ import { fileURLToPath, pathToFileURL } from 'node:url';
17
+ import { dirname, join } from 'node:path';
18
+ import { writeFileSync, readFileSync, existsSync } from 'node:fs';
19
+
20
+ // Get the directory of this file
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ // Register the module hooks
25
+ register('./loader-hooks.js', pathToFileURL(__dirname + '/'));
26
+
27
+ // Set up the global tracer
28
+ const traces = [];
29
+ const callStack = [];
30
+
31
+ /**
32
+ * Safely serialize a value for tracing
33
+ * Handles circular references and large objects
34
+ */
35
+ function safeSerialize(value, depth = 0) {
36
+ if (depth > 2) return '[max depth]';
37
+ if (value === undefined) return undefined;
38
+ if (value === null) return null;
39
+ if (typeof value === 'function') return `[Function: ${value.name || 'anonymous'}]`;
40
+ if (typeof value === 'symbol') return value.toString();
41
+ if (typeof value === 'bigint') return value.toString() + 'n';
42
+ if (typeof value !== 'object') return value;
43
+
44
+ // Handle arrays
45
+ if (Array.isArray(value)) {
46
+ if (value.length > 5) {
47
+ return [...value.slice(0, 5).map(v => safeSerialize(v, depth + 1)), `...+${value.length - 5}`];
48
+ }
49
+ return value.map(v => safeSerialize(v, depth + 1));
50
+ }
51
+
52
+ // Handle objects
53
+ try {
54
+ const keys = Object.keys(value);
55
+ if (keys.length > 5) {
56
+ const result = {};
57
+ keys.slice(0, 5).forEach(k => {
58
+ result[k] = safeSerialize(value[k], depth + 1);
59
+ });
60
+ result['...'] = `+${keys.length - 5} more`;
61
+ return result;
62
+ }
63
+ const result = {};
64
+ keys.forEach(k => {
65
+ result[k] = safeSerialize(value[k], depth + 1);
66
+ });
67
+ return result;
68
+ } catch {
69
+ return '[unserializable]';
70
+ }
71
+ }
72
+
73
+ // Install the global tracer
74
+ globalThis.__taistTracer = {
75
+ enter(name, args) {
76
+ const startTime = performance.now();
77
+ callStack.push({ name, startTime, args: safeSerialize(args) });
78
+ },
79
+
80
+ exit(name, result) {
81
+ const entry = callStack.pop();
82
+ if (!entry) return;
83
+
84
+ const duration = performance.now() - entry.startTime;
85
+ traces.push({
86
+ name: entry.name,
87
+ duration: Math.round(duration * 100) / 100,
88
+ args: entry.args,
89
+ result: safeSerialize(result),
90
+ type: 'exit'
91
+ });
92
+ },
93
+
94
+ error(name, error) {
95
+ const entry = callStack.pop();
96
+ if (!entry) return;
97
+
98
+ const duration = performance.now() - entry.startTime;
99
+ traces.push({
100
+ name: entry.name,
101
+ duration: Math.round(duration * 100) / 100,
102
+ args: entry.args,
103
+ error: {
104
+ name: error?.name || 'Error',
105
+ message: error?.message || String(error)
106
+ },
107
+ type: 'error'
108
+ });
109
+ },
110
+
111
+ getTraces() {
112
+ return traces;
113
+ }
114
+ };
115
+
116
+ // Write traces on process exit
117
+ function writeTraces() {
118
+ const tracePath = process.env.TAIST_TRACE_FILE;
119
+ if (!tracePath || traces.length === 0) return;
120
+
121
+ try {
122
+ // Read existing traces and append
123
+ let existing = [];
124
+ if (existsSync(tracePath)) {
125
+ try {
126
+ existing = JSON.parse(readFileSync(tracePath, 'utf-8'));
127
+ } catch {
128
+ existing = [];
129
+ }
130
+ }
131
+ writeFileSync(tracePath, JSON.stringify([...existing, ...traces]));
132
+
133
+ if (process.env.TAIST_DEBUG) {
134
+ console.error(`[taist] Wrote ${traces.length} traces to ${tracePath}`);
135
+ }
136
+ } catch (e) {
137
+ console.error('[taist] Failed to write traces:', e.message);
138
+ }
139
+ }
140
+
141
+ // Register exit handlers
142
+ process.on('exit', writeTraces);
143
+ process.on('SIGINT', () => {
144
+ writeTraces();
145
+ process.exit(130);
146
+ });
147
+ process.on('SIGTERM', () => {
148
+ writeTraces();
149
+ process.exit(143);
150
+ });
151
+
152
+ if (process.env.TAIST_DEBUG) {
153
+ console.error('[taist] Loader initialized');
154
+ }