trickle-observe 0.2.2 → 0.2.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.
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+ /**
3
+ * Variable-level tracing — captures the runtime type and sample value
4
+ * of variable assignments within function bodies.
5
+ *
6
+ * This is injected by the Module._compile source transform. After each
7
+ * `const/let/var x = expr;` statement, the transform inserts:
8
+ *
9
+ * __trickle_tv(x, 'x', 42, 'my-module', '/path/to/file.ts');
10
+ *
11
+ * The traceVar function:
12
+ * 1. Infers the TypeNode from the runtime value
13
+ * 2. Captures a sanitized sample value
14
+ * 3. Appends to .trickle/variables.jsonl
15
+ * 4. Caches by (file:line:varName + typeHash) to avoid duplicates
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ exports.initVarTracer = initVarTracer;
52
+ exports.traceVar = traceVar;
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ const type_inference_1 = require("./type-inference");
56
+ const type_hash_1 = require("./type-hash");
57
+ /** Where to write variable observations */
58
+ let varsFilePath = '';
59
+ let debugMode = false;
60
+ /** Cache: "file:line:varName:typeHash" → already written */
61
+ const varCache = new Set();
62
+ /** Batch buffer for writing — avoids one fs.appendFileSync per variable */
63
+ let varBuffer = [];
64
+ let flushTimer = null;
65
+ const FLUSH_INTERVAL_MS = 1000;
66
+ const MAX_BUFFER_SIZE = 100;
67
+ /**
68
+ * Initialize the variable tracer.
69
+ * Called once during observe-register setup.
70
+ */
71
+ function initVarTracer(opts = {}) {
72
+ debugMode = opts.debug === true;
73
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
74
+ try {
75
+ fs.mkdirSync(dir, { recursive: true });
76
+ }
77
+ catch { }
78
+ varsFilePath = path.join(dir, 'variables.jsonl');
79
+ if (debugMode) {
80
+ console.log(`[trickle/vars] Variable tracing enabled → ${varsFilePath}`);
81
+ }
82
+ }
83
+ /**
84
+ * Trace a variable's runtime value.
85
+ * Called by injected code after each variable declaration.
86
+ *
87
+ * @param value - The variable's current value (already computed)
88
+ * @param varName - The variable name in source
89
+ * @param line - Line number in source file
90
+ * @param moduleName - Module name (derived from filename)
91
+ * @param filePath - Absolute path to source file
92
+ */
93
+ function traceVar(value, varName, line, moduleName, filePath) {
94
+ // Auto-initialize if not yet done (needed for Vite/Vitest worker processes)
95
+ if (!varsFilePath) {
96
+ initVarTracer();
97
+ if (!varsFilePath)
98
+ return;
99
+ }
100
+ try {
101
+ const type = (0, type_inference_1.inferType)(value, 3);
102
+ // Create a stable hash for dedup
103
+ const dummyArgs = { kind: 'tuple', elements: [] };
104
+ const typeHash = (0, type_hash_1.hashType)(dummyArgs, type);
105
+ const cacheKey = `${filePath}:${line}:${varName}:${typeHash}`;
106
+ if (varCache.has(cacheKey))
107
+ return;
108
+ varCache.add(cacheKey);
109
+ const sample = sanitizeVarSample(value);
110
+ const observation = {
111
+ kind: 'variable',
112
+ varName,
113
+ line,
114
+ module: moduleName,
115
+ file: filePath,
116
+ type,
117
+ typeHash,
118
+ sample,
119
+ };
120
+ // Buffer the write
121
+ varBuffer.push(JSON.stringify(observation));
122
+ if (varBuffer.length >= MAX_BUFFER_SIZE) {
123
+ flushVarBuffer();
124
+ }
125
+ else if (!flushTimer) {
126
+ flushTimer = setTimeout(() => {
127
+ flushVarBuffer();
128
+ }, FLUSH_INTERVAL_MS);
129
+ if (flushTimer && typeof flushTimer === 'object' && 'unref' in flushTimer) {
130
+ flushTimer.unref();
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ // Never crash user's app
136
+ }
137
+ }
138
+ /**
139
+ * Flush buffered variable observations to disk.
140
+ */
141
+ function flushVarBuffer() {
142
+ if (flushTimer) {
143
+ clearTimeout(flushTimer);
144
+ flushTimer = null;
145
+ }
146
+ if (varBuffer.length === 0)
147
+ return;
148
+ const lines = varBuffer.join('\n') + '\n';
149
+ varBuffer = [];
150
+ try {
151
+ fs.appendFileSync(varsFilePath, lines);
152
+ }
153
+ catch {
154
+ // Never crash user's app
155
+ }
156
+ }
157
+ /**
158
+ * Sanitize a variable value for safe serialization.
159
+ * More aggressive truncation than function samples since there are many more variables.
160
+ */
161
+ function sanitizeVarSample(value, depth = 2) {
162
+ if (depth <= 0)
163
+ return '[truncated]';
164
+ if (value === null || value === undefined)
165
+ return value;
166
+ const t = typeof value;
167
+ if (t === 'string') {
168
+ const s = value;
169
+ return s.length > 100 ? s.substring(0, 100) + '...' : s;
170
+ }
171
+ if (t === 'number' || t === 'boolean')
172
+ return value;
173
+ if (t === 'bigint')
174
+ return String(value);
175
+ if (t === 'symbol')
176
+ return String(value);
177
+ if (t === 'function')
178
+ return `[Function: ${value.name || 'anonymous'}]`;
179
+ if (Array.isArray(value)) {
180
+ return value.slice(0, 3).map(item => sanitizeVarSample(item, depth - 1));
181
+ }
182
+ if (t === 'object') {
183
+ if (value instanceof Date)
184
+ return value.toISOString();
185
+ if (value instanceof RegExp)
186
+ return String(value);
187
+ if (value instanceof Error)
188
+ return { error: value.message };
189
+ if (value instanceof Map)
190
+ return `[Map: ${value.size} entries]`;
191
+ if (value instanceof Set)
192
+ return `[Set: ${value.size} items]`;
193
+ if (value instanceof Promise)
194
+ return '[Promise]';
195
+ const obj = value;
196
+ const result = {};
197
+ const keys = Object.keys(obj).slice(0, 10);
198
+ for (const key of keys) {
199
+ try {
200
+ result[key] = sanitizeVarSample(obj[key], depth - 1);
201
+ }
202
+ catch {
203
+ result[key] = '[unreadable]';
204
+ }
205
+ }
206
+ return result;
207
+ }
208
+ return String(value);
209
+ }
210
+ // Flush on process exit — use 'exit' event (synchronous, fires even on process.exit())
211
+ // because Vitest workers and forked processes may exit without 'beforeExit'.
212
+ // flushVarBuffer uses fs.appendFileSync so it's safe in the 'exit' handler.
213
+ if (typeof process !== 'undefined' && process.on) {
214
+ const exitFlush = () => { flushVarBuffer(); };
215
+ process.on('exit', exitFlush);
216
+ process.on('beforeExit', exitFlush);
217
+ process.on('SIGTERM', exitFlush);
218
+ process.on('SIGINT', exitFlush);
219
+ }
@@ -167,10 +167,18 @@ function inferArray(arr, depth, seen) {
167
167
  }
168
168
  return { kind: 'array', element: unifyTypes(elementTypes) };
169
169
  }
170
+ const MAX_PROPERTIES = 20;
170
171
  function inferPlainObject(obj, depth, seen) {
171
172
  const properties = {};
172
173
  const keys = Object.keys(obj);
173
- for (const key of keys) {
174
+ // Skip internal/private properties (common in Node.js built-in objects like
175
+ // http.Server, streams, etc.) — they make generated types unusably verbose.
176
+ const publicKeys = keys.filter(k => !k.startsWith('_'));
177
+ // If filtering removed everything, use all keys (it's a plain data object with _ keys)
178
+ const effectiveKeys = publicKeys.length > 0 ? publicKeys : keys;
179
+ // Cap the number of properties to keep types manageable
180
+ const cappedKeys = effectiveKeys.slice(0, MAX_PROPERTIES);
181
+ for (const key of cappedKeys) {
174
182
  try {
175
183
  properties[key] = infer(obj[key], depth - 1, seen);
176
184
  }
@@ -2,8 +2,9 @@
2
2
  * Vite plugin for trickle observation.
3
3
  *
4
4
  * Integrates into Vite's (and Vitest's) transform pipeline to wrap
5
- * user functions with trickle observation the same thing observe-register.ts
6
- * does for Node's Module._compile, but for Vite/Vitest.
5
+ * user functions with trickle observation AND trace variable assignments
6
+ * the same thing observe-register.ts does for Node's Module._compile,
7
+ * but for Vite/Vitest.
7
8
  *
8
9
  * Usage in vitest.config.ts:
9
10
  *
@@ -25,6 +26,8 @@ export interface TricklePluginOptions {
25
26
  backendUrl?: string;
26
27
  /** Enable debug logging */
27
28
  debug?: boolean;
29
+ /** Disable variable tracing (default: enabled) */
30
+ traceVars?: boolean;
28
31
  }
29
32
  export declare function tricklePlugin(options?: TricklePluginOptions): {
30
33
  name: string;