trickle-observe 0.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.
Files changed (57) hide show
  1. package/auto-env.js +13 -0
  2. package/auto-esm.mjs +128 -0
  3. package/auto.js +3 -0
  4. package/dist/auto-codegen.d.ts +29 -0
  5. package/dist/auto-codegen.js +999 -0
  6. package/dist/auto-register.d.ts +16 -0
  7. package/dist/auto-register.js +99 -0
  8. package/dist/cache.d.ts +27 -0
  9. package/dist/cache.js +52 -0
  10. package/dist/env-detect.d.ts +5 -0
  11. package/dist/env-detect.js +35 -0
  12. package/dist/express.d.ts +44 -0
  13. package/dist/express.js +342 -0
  14. package/dist/fetch-observer.d.ts +24 -0
  15. package/dist/fetch-observer.js +217 -0
  16. package/dist/index.d.ts +64 -0
  17. package/dist/index.js +172 -0
  18. package/dist/observe-register.d.ts +29 -0
  19. package/dist/observe-register.js +455 -0
  20. package/dist/observe.d.ts +44 -0
  21. package/dist/observe.js +109 -0
  22. package/dist/proxy-tracker.d.ts +15 -0
  23. package/dist/proxy-tracker.js +172 -0
  24. package/dist/register.d.ts +21 -0
  25. package/dist/register.js +105 -0
  26. package/dist/transport.d.ts +22 -0
  27. package/dist/transport.js +228 -0
  28. package/dist/type-hash.d.ts +5 -0
  29. package/dist/type-hash.js +60 -0
  30. package/dist/type-inference.d.ts +14 -0
  31. package/dist/type-inference.js +259 -0
  32. package/dist/types.d.ts +78 -0
  33. package/dist/types.js +2 -0
  34. package/dist/wrap.d.ts +10 -0
  35. package/dist/wrap.js +247 -0
  36. package/observe-esm-hooks.mjs +367 -0
  37. package/observe-esm.mjs +40 -0
  38. package/observe.js +2 -0
  39. package/package.json +26 -0
  40. package/register.js +2 -0
  41. package/src/auto-codegen.ts +1058 -0
  42. package/src/auto-register.ts +102 -0
  43. package/src/cache.ts +53 -0
  44. package/src/env-detect.ts +22 -0
  45. package/src/express.ts +386 -0
  46. package/src/fetch-observer.ts +226 -0
  47. package/src/index.ts +199 -0
  48. package/src/observe-register.ts +453 -0
  49. package/src/observe.ts +127 -0
  50. package/src/proxy-tracker.ts +208 -0
  51. package/src/register.ts +110 -0
  52. package/src/transport.ts +207 -0
  53. package/src/type-hash.ts +71 -0
  54. package/src/type-inference.ts +285 -0
  55. package/src/types.ts +61 -0
  56. package/src/wrap.ts +289 -0
  57. package/tsconfig.json +8 -0
@@ -0,0 +1,999 @@
1
+ "use strict";
2
+ /**
3
+ * Lightweight inline codegen for `trickle/auto`.
4
+ *
5
+ * Reads .trickle/observations.jsonl and generates .d.ts sidecar files
6
+ * next to source files. Runs entirely inside the user's process —
7
+ * no CLI or backend required.
8
+ */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.injectTypes = injectTypes;
44
+ exports.generateTypes = generateTypes;
45
+ exports.generateCoverageReport = generateCoverageReport;
46
+ exports.generateTypeSummary = generateTypeSummary;
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ // ── Type merging (same logic as CLI local-codegen) ──
50
+ function typeNodeKey(node) {
51
+ switch (node.kind) {
52
+ case 'primitive': return `p:${node.name}`;
53
+ case 'unknown': return 'unknown';
54
+ case 'array': return `a:${typeNodeKey(node.element)}`;
55
+ case 'tuple': return `t:[${(node.elements || []).map(typeNodeKey).join(',')}]`;
56
+ case 'object': {
57
+ const props = node.properties || {};
58
+ const entries = Object.keys(props).sort().map(k => `${k}:${typeNodeKey(props[k])}`);
59
+ return `o:{${entries.join(',')}}`;
60
+ }
61
+ case 'union': return `u:(${(node.members || []).map(typeNodeKey).sort().join('|')})`;
62
+ default: return JSON.stringify(node);
63
+ }
64
+ }
65
+ function mergeTypeNodes(a, b) {
66
+ if (typeNodeKey(a) === typeNodeKey(b))
67
+ return a;
68
+ if (a.kind === 'object' && b.kind === 'object') {
69
+ const aP = a.properties || {}, bP = b.properties || {};
70
+ const allKeys = new Set([...Object.keys(aP), ...Object.keys(bP)]);
71
+ const merged = {};
72
+ for (const k of allKeys) {
73
+ const inA = k in aP, inB = k in bP;
74
+ if (inA && inB)
75
+ merged[k] = mergeTypeNodes(aP[k], bP[k]);
76
+ else if (inA)
77
+ merged[k] = makeOptional(aP[k]);
78
+ else
79
+ merged[k] = makeOptional(bP[k]);
80
+ }
81
+ return { kind: 'object', properties: merged };
82
+ }
83
+ if (a.kind === 'array' && b.kind === 'array' && a.element && b.element) {
84
+ return { kind: 'array', element: mergeTypeNodes(a.element, b.element) };
85
+ }
86
+ if (a.kind === 'tuple' && b.kind === 'tuple') {
87
+ const aE = a.elements || [], bE = b.elements || [];
88
+ if (aE.length === bE.length) {
89
+ return { kind: 'tuple', elements: aE.map((el, i) => mergeTypeNodes(el, bE[i])) };
90
+ }
91
+ // Different lengths: merge common prefix, make extra elements optional
92
+ const shorter = aE.length < bE.length ? aE : bE;
93
+ const longer = aE.length < bE.length ? bE : aE;
94
+ const merged = [];
95
+ for (let i = 0; i < longer.length; i++) {
96
+ if (i < shorter.length) {
97
+ merged.push(mergeTypeNodes(shorter[i], longer[i]));
98
+ }
99
+ else {
100
+ merged.push(makeOptional(longer[i]));
101
+ }
102
+ }
103
+ return { kind: 'tuple', elements: merged };
104
+ }
105
+ return deduplicateUnion([
106
+ ...(a.kind === 'union' ? (a.members || []) : [a]),
107
+ ...(b.kind === 'union' ? (b.members || []) : [b]),
108
+ ]);
109
+ }
110
+ function makeOptional(node) {
111
+ if (node.kind === 'primitive' && node.name === 'undefined')
112
+ return node;
113
+ if (node.kind === 'union') {
114
+ const members = node.members || [];
115
+ if (members.some(m => m.kind === 'primitive' && m.name === 'undefined'))
116
+ return node;
117
+ return { kind: 'union', members: [...members, { kind: 'primitive', name: 'undefined' }] };
118
+ }
119
+ return { kind: 'union', members: [node, { kind: 'primitive', name: 'undefined' }] };
120
+ }
121
+ function deduplicateUnion(members) {
122
+ const seen = new Set();
123
+ const unique = [];
124
+ for (const m of members) {
125
+ if (m.kind === 'union') {
126
+ for (const inner of m.members || []) {
127
+ const k = typeNodeKey(inner);
128
+ if (!seen.has(k)) {
129
+ seen.add(k);
130
+ unique.push(inner);
131
+ }
132
+ }
133
+ }
134
+ else {
135
+ const k = typeNodeKey(m);
136
+ if (!seen.has(k)) {
137
+ seen.add(k);
138
+ unique.push(m);
139
+ }
140
+ }
141
+ }
142
+ return unique.length === 1 ? unique[0] : { kind: 'union', members: unique };
143
+ }
144
+ // ── Read + merge observations ──
145
+ function readAndMerge(jsonlPath) {
146
+ if (!fs.existsSync(jsonlPath))
147
+ return [];
148
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
149
+ const lines = content.trim().split('\n').filter(Boolean);
150
+ const byFunc = new Map();
151
+ for (const line of lines) {
152
+ try {
153
+ const obs = JSON.parse(line);
154
+ if (obs.functionName && obs.argsType && obs.returnType) {
155
+ if (!byFunc.has(obs.functionName))
156
+ byFunc.set(obs.functionName, []);
157
+ byFunc.get(obs.functionName).push(obs);
158
+ }
159
+ }
160
+ catch { /* skip */ }
161
+ }
162
+ const results = [];
163
+ for (const [name, observations] of byFunc) {
164
+ let args = observations[0].argsType;
165
+ let ret = observations[0].returnType;
166
+ for (let i = 1; i < observations.length; i++) {
167
+ if (observations[i].typeHash !== observations[0].typeHash) {
168
+ args = mergeTypeNodes(args, observations[i].argsType);
169
+ ret = mergeTypeNodes(ret, observations[i].returnType);
170
+ }
171
+ }
172
+ // Use paramNames from the latest observation that has them
173
+ const paramNames = observations.reduce((acc, obs) => obs.paramNames && obs.paramNames.length > 0 ? obs.paramNames : acc, undefined);
174
+ // Use sample data from the first observation that has it
175
+ const sampleObs = observations.find(obs => obs.sampleInput != null || obs.sampleOutput != null);
176
+ // isAsync if any observation marked it as async
177
+ const isAsync = observations.some(obs => obs.isAsync === true);
178
+ // Collect unique type variants (by typeHash) for overload generation
179
+ const seenHashes = new Set();
180
+ const variants = [];
181
+ for (const obs of observations) {
182
+ if (!seenHashes.has(obs.typeHash)) {
183
+ seenHashes.add(obs.typeHash);
184
+ variants.push({
185
+ argsType: obs.argsType,
186
+ returnType: obs.returnType,
187
+ paramNames: obs.paramNames,
188
+ isAsync: obs.isAsync || undefined,
189
+ });
190
+ }
191
+ }
192
+ results.push({
193
+ name, argsType: args, returnType: ret,
194
+ module: observations[observations.length - 1].module,
195
+ isAsync: isAsync || undefined,
196
+ paramNames,
197
+ sampleInput: sampleObs?.sampleInput,
198
+ sampleOutput: sampleObs?.sampleOutput,
199
+ // Only include variants if there are 2-5 distinct patterns (overload-worthy)
200
+ variants: variants.length >= 2 && variants.length <= 5 ? variants : undefined,
201
+ });
202
+ }
203
+ return results;
204
+ }
205
+ // ── TypeScript generation ──
206
+ function toPascalCase(name) {
207
+ return name
208
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
209
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
210
+ .trim().split(/\s+/)
211
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
212
+ .join('');
213
+ }
214
+ function extractOptional(node) {
215
+ if (node.kind !== 'union')
216
+ return { isOptional: false, innerType: node };
217
+ const members = node.members || [];
218
+ const hasUndef = members.some(m => m.kind === 'primitive' && m.name === 'undefined');
219
+ if (!hasUndef)
220
+ return { isOptional: false, innerType: node };
221
+ const without = members.filter(m => !(m.kind === 'primitive' && m.name === 'undefined'));
222
+ if (without.length === 0)
223
+ return { isOptional: true, innerType: { kind: 'primitive', name: 'undefined' } };
224
+ if (without.length === 1)
225
+ return { isOptional: true, innerType: without[0] };
226
+ return { isOptional: true, innerType: { kind: 'union', members: without } };
227
+ }
228
+ function typeToTS(node, ext, parent, prop, indent) {
229
+ switch (node.kind) {
230
+ case 'primitive': return node.name === 'integer' ? 'number' : (node.name || 'unknown');
231
+ case 'unknown': return 'unknown';
232
+ case 'array': {
233
+ const inner = typeToTS(node.element, ext, parent, prop, indent);
234
+ return node.element.kind === 'union' ? `Array<${inner}>` : `${inner}[]`;
235
+ }
236
+ case 'tuple':
237
+ return `[${(node.elements || []).map((e, i) => typeToTS(e, ext, parent, `${prop || 'el'}${i}`, indent)).join(', ')}]`;
238
+ case 'union':
239
+ return (node.members || []).map(m => typeToTS(m, ext, parent, prop, indent)).join(' | ');
240
+ case 'map': return `Map<${typeToTS(node.key, ext, parent, 'key', indent)}, ${typeToTS(node.value, ext, parent, 'value', indent)}>`;
241
+ case 'set': return `Set<${typeToTS(node.element, ext, parent, prop, indent)}>`;
242
+ case 'promise': return `Promise<${typeToTS(node.resolved, ext, parent, prop, indent)}>`;
243
+ case 'function': {
244
+ const params = (node.params || []).map((p, i) => `arg${i}: ${typeToTS(p, ext, parent, `p${i}`, indent)}`);
245
+ return `(${params.join(', ')}) => ${typeToTS(node.returnType, ext, parent, 'ret', indent)}`;
246
+ }
247
+ case 'object': {
248
+ const keys = Object.keys(node.properties || {});
249
+ if (keys.length === 0)
250
+ return 'Record<string, never>';
251
+ if (keys.length > 2 && prop) {
252
+ const iName = toPascalCase(parent) + toPascalCase(prop);
253
+ if (!ext.some(e => e.name === iName))
254
+ ext.push({ name: iName, node });
255
+ return iName;
256
+ }
257
+ const pad = ' '.repeat(indent + 1), close = ' '.repeat(indent);
258
+ const entries = keys.map(k => {
259
+ const { isOptional, innerType } = extractOptional(node.properties[k]);
260
+ const val = typeToTS(innerType, ext, parent, k, indent + 1);
261
+ return isOptional ? `${pad}${k}?: ${val};` : `${pad}${k}: ${val};`;
262
+ });
263
+ return `{\n${entries.join('\n')}\n${close}}`;
264
+ }
265
+ default: return 'unknown';
266
+ }
267
+ }
268
+ function renderInterface(name, node, ext) {
269
+ const keys = Object.keys(node.properties || {});
270
+ const lines = [`export interface ${name} {`];
271
+ for (const k of keys) {
272
+ const { isOptional, innerType } = extractOptional(node.properties[k]);
273
+ const val = typeToTS(innerType, ext, name, k, 1);
274
+ lines.push(isOptional ? ` ${k}?: ${val};` : ` ${k}: ${val};`);
275
+ }
276
+ lines.push('}');
277
+ return lines.join('\n');
278
+ }
279
+ function formatSampleValue(val, depth = 0) {
280
+ if (val === null || val === undefined)
281
+ return String(val);
282
+ if (typeof val === 'string')
283
+ return depth === 0 && val.length > 60 ? `"${val.slice(0, 57)}..."` : `"${val}"`;
284
+ if (typeof val === 'number' || typeof val === 'boolean')
285
+ return String(val);
286
+ if (Array.isArray(val)) {
287
+ if (val.length === 0)
288
+ return '[]';
289
+ if (depth > 1)
290
+ return '[...]';
291
+ const items = val.slice(0, 5).map(v => formatSampleValue(v, depth + 1));
292
+ return val.length > 5 ? `[${items.join(', ')}, ...]` : `[${items.join(', ')}]`;
293
+ }
294
+ if (typeof val === 'object') {
295
+ const entries = Object.entries(val);
296
+ if (entries.length === 0)
297
+ return '{}';
298
+ if (depth > 1)
299
+ return '{...}';
300
+ const items = entries.slice(0, 6).map(([k, v]) => `${k}: ${formatSampleValue(v, depth + 1)}`);
301
+ return entries.length > 6 ? `{ ${items.join(', ')}, ... }` : `{ ${items.join(', ')} }`;
302
+ }
303
+ return String(val);
304
+ }
305
+ function buildExampleComment(fn) {
306
+ if (fn.sampleInput == null && fn.sampleOutput == null)
307
+ return [];
308
+ const paramNames = fn.paramNames || [];
309
+ const lines = [];
310
+ // Format args
311
+ let argsStr = '';
312
+ if (Array.isArray(fn.sampleInput)) {
313
+ argsStr = fn.sampleInput.map(v => formatSampleValue(v)).join(', ');
314
+ }
315
+ else if (fn.sampleInput != null) {
316
+ argsStr = formatSampleValue(fn.sampleInput);
317
+ }
318
+ // Format return value
319
+ const retStr = fn.sampleOutput != null ? formatSampleValue(fn.sampleOutput) : undefined;
320
+ lines.push('/**');
321
+ lines.push(` * @example`);
322
+ if (retStr) {
323
+ lines.push(` * ${fn.name}(${argsStr})`);
324
+ lines.push(` * // => ${retStr}`);
325
+ }
326
+ else {
327
+ lines.push(` * ${fn.name}(${argsStr})`);
328
+ }
329
+ lines.push(' */');
330
+ return lines;
331
+ }
332
+ function generateClassDts(className, methods) {
333
+ const sections = [];
334
+ const ext = [];
335
+ const classLines = [`export declare class ${className} {`];
336
+ for (const fn of methods) {
337
+ const methodName = fn.name.split('.')[1] || fn.name;
338
+ const base = toPascalCase(className) + toPascalCase(methodName);
339
+ // Generate overloads if we have multiple distinct type patterns
340
+ if (fn.variants && fn.variants.length >= 2) {
341
+ for (const variant of fn.variants) {
342
+ const vNames = variant.paramNames || fn.paramNames || [];
343
+ let vArgEntries = [];
344
+ if (variant.argsType.kind === 'tuple') {
345
+ vArgEntries = (variant.argsType.elements || []).map((el, i) => ({
346
+ paramName: vNames[i] || `arg${i}`,
347
+ typeNode: el,
348
+ })).filter(e => e.paramName !== 'this' && e.paramName !== 'self');
349
+ }
350
+ let vRet = typeToTS(variant.returnType, ext, base, undefined, 1);
351
+ if (variant.isAsync)
352
+ vRet = `Promise<${vRet}>`;
353
+ const vParams = vArgEntries.map(e => `${e.paramName}: ${typeToTS(e.typeNode, ext, base, e.paramName, 1)}`);
354
+ classLines.push(` ${methodName}(${vParams.join(', ')}): ${vRet};`);
355
+ }
356
+ }
357
+ else {
358
+ // Build params (skip 'this' if present)
359
+ let argEntries = [];
360
+ if (fn.argsType.kind === 'tuple') {
361
+ const names = fn.paramNames || [];
362
+ argEntries = (fn.argsType.elements || []).map((el, i) => ({
363
+ paramName: names[i] || `arg${i}`,
364
+ typeNode: el,
365
+ })).filter(e => e.paramName !== 'this');
366
+ }
367
+ // Return type
368
+ let retType = typeToTS(fn.returnType, ext, base, undefined, 1);
369
+ if (fn.isAsync)
370
+ retType = `Promise<${retType}>`;
371
+ // Build params string (handle optional params)
372
+ const params = argEntries.map(e => {
373
+ const { isOptional, innerType } = extractOptional(e.typeNode);
374
+ if (isOptional) {
375
+ return `${e.paramName}?: ${typeToTS(innerType, ext, base, e.paramName, 1)}`;
376
+ }
377
+ return `${e.paramName}: ${typeToTS(e.typeNode, ext, base, e.paramName, 1)}`;
378
+ });
379
+ classLines.push(` ${methodName}(${params.join(', ')}): ${retType};`);
380
+ }
381
+ }
382
+ classLines.push('}');
383
+ // Emit extracted interfaces before the class
384
+ const emitted = new Set();
385
+ let cursor = 0;
386
+ while (cursor < ext.length) {
387
+ const iface = ext[cursor++];
388
+ if (emitted.has(iface.name))
389
+ continue;
390
+ emitted.add(iface.name);
391
+ sections.push(renderInterface(iface.name, iface.node, ext));
392
+ sections.push('');
393
+ }
394
+ sections.push(...classLines);
395
+ sections.push('');
396
+ return sections;
397
+ }
398
+ function generateDts(functions) {
399
+ const sections = [
400
+ '// Auto-generated by trickle/auto from runtime observations',
401
+ `// Generated at ${new Date().toISOString()}`,
402
+ '// Do not edit — types update automatically as your code runs',
403
+ '',
404
+ ];
405
+ // Separate class methods from standalone functions
406
+ const classMethods = new Map();
407
+ const standaloneFunctions = [];
408
+ for (const fn of functions) {
409
+ if (fn.name.includes('.')) {
410
+ const className = fn.name.split('.')[0];
411
+ if (!classMethods.has(className))
412
+ classMethods.set(className, []);
413
+ classMethods.get(className).push(fn);
414
+ }
415
+ else {
416
+ standaloneFunctions.push(fn);
417
+ }
418
+ }
419
+ // Generate class declarations
420
+ for (const [className, methods] of classMethods) {
421
+ sections.push(...generateClassDts(className, methods));
422
+ }
423
+ for (const fn of standaloneFunctions) {
424
+ const base = toPascalCase(fn.name);
425
+ const ext = [];
426
+ const lines = [];
427
+ // Args
428
+ let argEntries = [];
429
+ if (fn.argsType.kind === 'tuple') {
430
+ const names = fn.paramNames || [];
431
+ argEntries = (fn.argsType.elements || []).map((el, i) => ({
432
+ paramName: names[i] || `arg${i}`,
433
+ typeNode: el,
434
+ }));
435
+ }
436
+ else if (fn.argsType.kind === 'object') {
437
+ argEntries = Object.keys(fn.argsType.properties || {}).map(k => ({ paramName: k, typeNode: fn.argsType.properties[k] }));
438
+ }
439
+ else {
440
+ argEntries = [{ paramName: 'input', typeNode: fn.argsType }];
441
+ }
442
+ const singleObj = argEntries.length === 1 && argEntries[0].typeNode.kind === 'object';
443
+ if (singleObj) {
444
+ lines.push(renderInterface(`${base}Input`, argEntries[0].typeNode, ext));
445
+ lines.push('');
446
+ }
447
+ // Return
448
+ const outName = `${base}Output`;
449
+ if (fn.returnType.kind === 'object' && Object.keys(fn.returnType.properties || {}).length > 0) {
450
+ lines.push(renderInterface(outName, fn.returnType, ext));
451
+ lines.push('');
452
+ }
453
+ else {
454
+ lines.push(`export type ${outName} = ${typeToTS(fn.returnType, ext, base, undefined, 0)};`);
455
+ lines.push('');
456
+ }
457
+ // Extracted interfaces
458
+ const emitted = new Set();
459
+ const extLines = [];
460
+ let cursor = 0;
461
+ while (cursor < ext.length) {
462
+ const iface = ext[cursor++];
463
+ if (emitted.has(iface.name))
464
+ continue;
465
+ emitted.add(iface.name);
466
+ extLines.push(renderInterface(iface.name, iface.node, ext));
467
+ extLines.push('');
468
+ }
469
+ // Function declaration
470
+ const ident = base.charAt(0).toLowerCase() + base.slice(1);
471
+ if (extLines.length > 0)
472
+ sections.push(...extLines);
473
+ sections.push(...lines);
474
+ // Add @example JSDoc if sample data is available
475
+ const exampleLines = buildExampleComment(fn);
476
+ if (exampleLines.length > 0)
477
+ sections.push(...exampleLines);
478
+ // Generate overloads if we have multiple distinct type patterns
479
+ if (fn.variants && fn.variants.length >= 2) {
480
+ for (const variant of fn.variants) {
481
+ const vExt = [];
482
+ const vRet = typeToTS(variant.returnType, vExt, base, undefined, 0);
483
+ const vRetDecl = variant.isAsync ? `Promise<${vRet}>` : vRet;
484
+ const vNames = variant.paramNames || fn.paramNames || [];
485
+ let vArgEntries = [];
486
+ if (variant.argsType.kind === 'tuple') {
487
+ vArgEntries = (variant.argsType.elements || []).map((el, i) => ({
488
+ paramName: vNames[i] || `arg${i}`,
489
+ typeNode: el,
490
+ }));
491
+ }
492
+ const vParams = vArgEntries.map(e => `${e.paramName}: ${typeToTS(e.typeNode, vExt, base, e.paramName, 0)}`);
493
+ sections.push(`export declare function ${ident}(${vParams.join(', ')}): ${vRetDecl};`);
494
+ }
495
+ }
496
+ else {
497
+ const retDecl = fn.isAsync ? `Promise<${outName}>` : outName;
498
+ let decl;
499
+ if (singleObj) {
500
+ decl = `export declare function ${ident}(input: ${base}Input): ${retDecl};`;
501
+ }
502
+ else {
503
+ const params = argEntries.map(e => {
504
+ if (e.typeNode.kind === 'object' && Object.keys(e.typeNode.properties || {}).length > 0)
505
+ return `${e.paramName}: ${base}${toPascalCase(e.paramName)}`;
506
+ // Check if parameter is optional (union with undefined)
507
+ const { isOptional, innerType } = extractOptional(e.typeNode);
508
+ if (isOptional) {
509
+ return `${e.paramName}?: ${typeToTS(innerType, ext, base, e.paramName, 0)}`;
510
+ }
511
+ return `${e.paramName}: ${typeToTS(e.typeNode, ext, base, e.paramName, 0)}`;
512
+ });
513
+ decl = `export declare function ${ident}(${params.join(', ')}): ${retDecl};`;
514
+ }
515
+ sections.push(decl);
516
+ }
517
+ sections.push('');
518
+ }
519
+ return sections.join('\n').trimEnd() + '\n';
520
+ }
521
+ // ── JSDoc type formatting ──
522
+ function typeToJSDoc(node) {
523
+ switch (node.kind) {
524
+ case 'primitive': return node.name === 'integer' ? 'number' : (node.name || '*');
525
+ case 'unknown': return '*';
526
+ case 'array': {
527
+ const inner = typeToJSDoc(node.element);
528
+ return `${inner}[]`;
529
+ }
530
+ case 'tuple':
531
+ return `[${(node.elements || []).map(typeToJSDoc).join(', ')}]`;
532
+ case 'union':
533
+ return (node.members || []).map(typeToJSDoc).join(' | ');
534
+ case 'map': return `Map<${typeToJSDoc(node.key)}, ${typeToJSDoc(node.value)}>`;
535
+ case 'set': return `Set<${typeToJSDoc(node.element)}>`;
536
+ case 'promise': return `Promise<${typeToJSDoc(node.resolved)}>`;
537
+ case 'function': {
538
+ const params = (node.params || []).map((p, i) => `arg${i}: ${typeToJSDoc(p)}`);
539
+ return `function(${params.join(', ')}): ${typeToJSDoc(node.returnType)}`;
540
+ }
541
+ case 'object': {
542
+ const props = node.properties || {};
543
+ const keys = Object.keys(props);
544
+ if (keys.length === 0)
545
+ return 'Object';
546
+ const entries = keys.map(k => {
547
+ const { isOptional, innerType } = extractOptional(props[k]);
548
+ return isOptional ? `${k}?: ${typeToJSDoc(innerType)}` : `${k}: ${typeToJSDoc(innerType)}`;
549
+ });
550
+ return `{ ${entries.join(', ')} }`;
551
+ }
552
+ default: return '*';
553
+ }
554
+ }
555
+ // ── JSDoc injection into source files ──
556
+ const funcDeclRe = /^(\s*(?:export\s+)?(?:async\s+)?function\s+)(\w+)\s*(?:<[^>]*>)?\s*\(/;
557
+ const arrowRe = /^(\s*(?:export\s+)?(?:const|let|var)\s+)(\w+)\s*=\s*(?:async\s+)?\(/;
558
+ const methodRe = /^(\s+)(\w+)\s*\(([^)]*)\)\s*\{/;
559
+ function injectJSDocIntoFile(filePath, functions) {
560
+ const source = fs.readFileSync(filePath, 'utf-8');
561
+ const lines = source.split('\n');
562
+ const fnMap = new Map(functions.map(f => [f.name, f]));
563
+ const result = [];
564
+ let changed = false;
565
+ for (let i = 0; i < lines.length; i++) {
566
+ const line = lines[i];
567
+ const trimmed = line.trimStart();
568
+ // Try to match a function declaration
569
+ let fnName = null;
570
+ let m = trimmed.match(funcDeclRe);
571
+ if (m)
572
+ fnName = m[2];
573
+ if (!fnName) {
574
+ m = trimmed.match(arrowRe);
575
+ if (m)
576
+ fnName = m[2];
577
+ }
578
+ if (!fnName) {
579
+ m = trimmed.match(methodRe);
580
+ if (m)
581
+ fnName = m[2];
582
+ }
583
+ if (!fnName || !fnMap.has(fnName)) {
584
+ result.push(line);
585
+ continue;
586
+ }
587
+ // Check if there's already a JSDoc comment above
588
+ const prevIdx = result.length - 1;
589
+ if (prevIdx >= 0) {
590
+ const prev = result[prevIdx].trim();
591
+ if (prev === '*/' || prev.endsWith('*/')) {
592
+ let j = prevIdx;
593
+ while (j >= 0 && !result[j].trim().startsWith('/**'))
594
+ j--;
595
+ if (j >= 0) {
596
+ const block = result.slice(j, prevIdx + 1).join('\n');
597
+ if (block.includes('@param') || block.includes('@returns') || block.includes('@trickle')) {
598
+ result.push(line);
599
+ continue;
600
+ }
601
+ }
602
+ }
603
+ }
604
+ const fn = fnMap.get(fnName);
605
+ const indent = line.match(/^(\s*)/)?.[1] || '';
606
+ // Build JSDoc
607
+ const jsdocLines = [`${indent}/** @trickle — auto-generated from runtime observations`];
608
+ // Params
609
+ const argElements = fn.argsType.kind === 'tuple' ? (fn.argsType.elements || []) : [];
610
+ const paramNames = fn.paramNames || [];
611
+ for (let pi = 0; pi < argElements.length; pi++) {
612
+ const pName = paramNames[pi] || `arg${pi}`;
613
+ const pType = typeToJSDoc(argElements[pi]);
614
+ jsdocLines.push(`${indent} * @param {${pType}} ${pName}`);
615
+ }
616
+ // Return type
617
+ const retType = typeToJSDoc(fn.returnType);
618
+ if (retType !== 'undefined' && retType !== 'void') {
619
+ jsdocLines.push(`${indent} * @returns {${retType}}`);
620
+ }
621
+ jsdocLines.push(`${indent} */`);
622
+ if (jsdocLines.length > 2) {
623
+ result.push(...jsdocLines);
624
+ changed = true;
625
+ }
626
+ result.push(line);
627
+ }
628
+ if (changed) {
629
+ fs.writeFileSync(filePath, result.join('\n'), 'utf-8');
630
+ }
631
+ return changed;
632
+ }
633
+ /**
634
+ * Inject JSDoc comments into JS source files based on observations.
635
+ * Only runs when TRICKLE_INJECT=1.
636
+ */
637
+ function injectTypes() {
638
+ if (process.env.TRICKLE_INJECT !== '1')
639
+ return 0;
640
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
641
+ const jsonlPath = path.join(trickleDir, 'observations.jsonl');
642
+ if (!fs.existsSync(jsonlPath))
643
+ return 0;
644
+ const functions = readAndMerge(jsonlPath);
645
+ if (functions.length === 0)
646
+ return 0;
647
+ const byModule = new Map();
648
+ for (const fn of functions) {
649
+ const mod = fn.module || '_default';
650
+ if (!byModule.has(mod))
651
+ byModule.set(mod, []);
652
+ byModule.get(mod).push(fn);
653
+ }
654
+ let injected = 0;
655
+ for (const [mod, fns] of byModule) {
656
+ if (mod.includes('.') && !mod.includes('/') && !mod.includes('\\'))
657
+ continue;
658
+ const sourceFile = findSourceFile(mod);
659
+ if (!sourceFile)
660
+ continue;
661
+ const ext = path.extname(sourceFile);
662
+ // Only inject into JS files (not .ts — those already have types)
663
+ if (!['.js', '.jsx', '.mjs', '.cjs'].includes(ext))
664
+ continue;
665
+ try {
666
+ if (injectJSDocIntoFile(sourceFile, fns)) {
667
+ injected += fns.length;
668
+ }
669
+ }
670
+ catch { /* don't crash user's app */ }
671
+ }
672
+ return injected;
673
+ }
674
+ // ── Public API ──
675
+ let lastSize = 0;
676
+ let lastContent = '';
677
+ /**
678
+ * Read observations and generate .d.ts files next to source files.
679
+ * Returns the number of functions typed.
680
+ */
681
+ function generateTypes() {
682
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
683
+ const jsonlPath = path.join(trickleDir, 'observations.jsonl');
684
+ if (!fs.existsSync(jsonlPath))
685
+ return 0;
686
+ // Skip if file hasn't changed
687
+ try {
688
+ const stat = fs.statSync(jsonlPath);
689
+ if (stat.size === lastSize)
690
+ return -1; // -1 = no change
691
+ lastSize = stat.size;
692
+ }
693
+ catch {
694
+ return 0;
695
+ }
696
+ const functions = readAndMerge(jsonlPath);
697
+ if (functions.length === 0)
698
+ return 0;
699
+ // Group by module and generate .d.ts next to source files
700
+ const byModule = new Map();
701
+ for (const fn of functions) {
702
+ const mod = fn.module || '_default';
703
+ if (!byModule.has(mod))
704
+ byModule.set(mod, []);
705
+ byModule.get(mod).push(fn);
706
+ }
707
+ let totalFunctions = 0;
708
+ for (const [mod, fns] of byModule) {
709
+ // Skip HTTP route observations (module is hostname like "localhost")
710
+ if (mod.includes('.') && !mod.includes('/') && !mod.includes('\\'))
711
+ continue;
712
+ const dts = generateDts(fns);
713
+ if (dts === lastContent)
714
+ continue;
715
+ // Find source file for this module
716
+ const sourceFile = findSourceFile(mod);
717
+ if (!sourceFile)
718
+ continue;
719
+ const ext = path.extname(sourceFile);
720
+ const dir = path.dirname(sourceFile);
721
+ const baseName = path.basename(sourceFile, ext);
722
+ // For .ts/.tsx files, use .trickle.d.ts to avoid conflicts (TS ignores .d.ts next to .ts)
723
+ const isTs = ext === '.ts' || ext === '.tsx';
724
+ const dtsPath = path.join(dir, `${baseName}${isTs ? '.trickle' : ''}.d.ts`);
725
+ try {
726
+ fs.writeFileSync(dtsPath, dts, 'utf-8');
727
+ lastContent = dts;
728
+ totalFunctions += fns.length;
729
+ }
730
+ catch { /* don't crash user's app */ }
731
+ }
732
+ return totalFunctions;
733
+ }
734
+ // ── Type coverage report ──
735
+ const jsFuncDeclRe = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(/;
736
+ const jsArrowRe = /^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(/;
737
+ const jsMethodRe = /^\s+(\w+)\s*\([^)]*\)\s*\{/;
738
+ function extractFunctionNames(source, ext) {
739
+ const names = [];
740
+ const lines = source.split('\n');
741
+ for (const line of lines) {
742
+ const trimmed = line.trimStart();
743
+ // Skip comments
744
+ if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
745
+ continue;
746
+ let m = trimmed.match(jsFuncDeclRe);
747
+ if (m && m[1] !== 'constructor') {
748
+ names.push(m[1]);
749
+ continue;
750
+ }
751
+ m = trimmed.match(jsArrowRe);
752
+ if (m) {
753
+ names.push(m[1]);
754
+ continue;
755
+ }
756
+ // Methods only for class-based files
757
+ m = trimmed.match(jsMethodRe);
758
+ if (m && m[1] !== 'constructor' && m[1] !== 'if' && m[1] !== 'for' && m[1] !== 'while' && m[1] !== 'switch' && m[1] !== 'catch') {
759
+ names.push(m[1]);
760
+ }
761
+ }
762
+ return [...new Set(names)];
763
+ }
764
+ /**
765
+ * Generate a type coverage report comparing observed types against
766
+ * all function declarations found in source files.
767
+ * Only runs when TRICKLE_COVERAGE=1.
768
+ */
769
+ function generateCoverageReport() {
770
+ if (process.env.TRICKLE_COVERAGE !== '1')
771
+ return null;
772
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
773
+ const jsonlPath = path.join(trickleDir, 'observations.jsonl');
774
+ if (!fs.existsSync(jsonlPath))
775
+ return null;
776
+ const functions = readAndMerge(jsonlPath);
777
+ if (functions.length === 0)
778
+ return null;
779
+ // Group observed functions by module
780
+ const observedByModule = new Map();
781
+ for (const fn of functions) {
782
+ const mod = fn.module || '_default';
783
+ if (!observedByModule.has(mod))
784
+ observedByModule.set(mod, new Set());
785
+ observedByModule.get(mod).add(fn.name);
786
+ }
787
+ const entries = [];
788
+ let totalAll = 0;
789
+ let typedAll = 0;
790
+ for (const [mod, observedNames] of observedByModule) {
791
+ // Skip HTTP route observations
792
+ if (mod.includes('.') && !mod.includes('/') && !mod.includes('\\'))
793
+ continue;
794
+ const sourceFile = findSourceFile(mod);
795
+ if (!sourceFile)
796
+ continue;
797
+ let source;
798
+ try {
799
+ source = fs.readFileSync(sourceFile, 'utf-8');
800
+ }
801
+ catch {
802
+ continue;
803
+ }
804
+ const ext = path.extname(sourceFile);
805
+ const allFunctions = extractFunctionNames(source, ext);
806
+ if (allFunctions.length === 0)
807
+ continue;
808
+ const typed = allFunctions.filter(n => observedNames.has(n));
809
+ const untyped = allFunctions.filter(n => !observedNames.has(n));
810
+ totalAll += allFunctions.length;
811
+ typedAll += typed.length;
812
+ const relPath = path.relative(process.cwd(), sourceFile);
813
+ entries.push({
814
+ file: relPath,
815
+ total: allFunctions.length,
816
+ typed: typed.length,
817
+ untyped,
818
+ });
819
+ }
820
+ if (entries.length === 0)
821
+ return null;
822
+ // Sort: incomplete files first, then by coverage
823
+ entries.sort((a, b) => {
824
+ const aRatio = a.typed / a.total;
825
+ const bRatio = b.typed / b.total;
826
+ return aRatio - bRatio;
827
+ });
828
+ const lines = ['[trickle/auto] Type coverage:'];
829
+ for (const entry of entries) {
830
+ const pct = Math.round((entry.typed / entry.total) * 100);
831
+ const marker = pct === 100 ? ' ✓' : '';
832
+ lines.push(` ${entry.file}: ${entry.typed}/${entry.total} (${pct}%)${marker}`);
833
+ if (entry.untyped.length > 0) {
834
+ lines.push(` Untyped: ${entry.untyped.join(', ')}`);
835
+ }
836
+ }
837
+ const totalPct = totalAll > 0 ? Math.round((typedAll / totalAll) * 100) : 0;
838
+ lines.push(` Total: ${typedAll}/${totalAll} functions (${totalPct}%)`);
839
+ return lines.join('\n');
840
+ }
841
+ // ── Type summary with change detection ──
842
+ const SNAPSHOT_FILE = '.trickle/type-snapshot.json';
843
+ function typeToCompact(node, depth = 0) {
844
+ if (depth > 2)
845
+ return '...';
846
+ switch (node.kind) {
847
+ case 'primitive': return node.name === 'integer' ? 'number' : (node.name || 'unknown');
848
+ case 'unknown': return 'unknown';
849
+ case 'array': return `${typeToCompact(node.element, depth + 1)}[]`;
850
+ case 'tuple': return `[${(node.elements || []).map(e => typeToCompact(e, depth + 1)).join(', ')}]`;
851
+ case 'union': return (node.members || []).map(m => typeToCompact(m, depth)).join(' | ');
852
+ case 'promise': return `Promise<${typeToCompact(node.resolved, depth + 1)}>`;
853
+ case 'map': return `Map<${typeToCompact(node.key, depth + 1)}, ${typeToCompact(node.value, depth + 1)}>`;
854
+ case 'set': return `Set<${typeToCompact(node.element, depth + 1)}>`;
855
+ case 'function': return `(...) => ${typeToCompact(node.returnType, depth + 1)}`;
856
+ case 'object': {
857
+ const props = node.properties || {};
858
+ const keys = Object.keys(props);
859
+ if (keys.length === 0)
860
+ return '{}';
861
+ if (keys.length > 4) {
862
+ const shown = keys.slice(0, 3).map(k => `${k}: ${typeToCompact(props[k], depth + 1)}`);
863
+ return `{ ${shown.join(', ')}, ... }`;
864
+ }
865
+ return `{ ${keys.map(k => `${k}: ${typeToCompact(props[k], depth + 1)}`).join(', ')} }`;
866
+ }
867
+ default: return 'unknown';
868
+ }
869
+ }
870
+ function buildSignature(fn) {
871
+ const paramNames = fn.paramNames || [];
872
+ let params = '';
873
+ if (fn.argsType.kind === 'tuple') {
874
+ const elements = fn.argsType.elements || [];
875
+ params = elements.map((el, i) => {
876
+ const name = paramNames[i] || `arg${i}`;
877
+ return `${name}: ${typeToCompact(el)}`;
878
+ }).join(', ');
879
+ }
880
+ const ret = typeToCompact(fn.returnType);
881
+ const retDisplay = fn.isAsync ? `Promise<${ret}>` : ret;
882
+ const prefix = fn.isAsync ? 'async ' : '';
883
+ return `${prefix}${fn.name}(${params}) → ${retDisplay}`;
884
+ }
885
+ function loadSnapshot() {
886
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
887
+ const snapshotPath = path.join(trickleDir, SNAPSHOT_FILE.split('/').pop());
888
+ try {
889
+ const content = fs.readFileSync(snapshotPath, 'utf-8');
890
+ return JSON.parse(content);
891
+ }
892
+ catch {
893
+ return null;
894
+ }
895
+ }
896
+ function saveSnapshot(snapshot) {
897
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
898
+ const snapshotPath = path.join(trickleDir, SNAPSHOT_FILE.split('/').pop());
899
+ try {
900
+ fs.mkdirSync(trickleDir, { recursive: true });
901
+ fs.writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2), 'utf-8');
902
+ }
903
+ catch { /* don't crash */ }
904
+ }
905
+ /**
906
+ * Generate a type summary showing discovered function signatures.
907
+ * When a previous snapshot exists, highlights new and changed functions.
908
+ * Only runs when TRICKLE_SUMMARY=1.
909
+ */
910
+ function generateTypeSummary() {
911
+ if (process.env.TRICKLE_SUMMARY !== '1')
912
+ return null;
913
+ const trickleDir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
914
+ const jsonlPath = path.join(trickleDir, 'observations.jsonl');
915
+ if (!fs.existsSync(jsonlPath))
916
+ return null;
917
+ const functions = readAndMerge(jsonlPath);
918
+ if (functions.length === 0)
919
+ return null;
920
+ // Filter out HTTP route observations
921
+ const userFunctions = functions.filter(fn => {
922
+ const mod = fn.module || '';
923
+ return !(mod.includes('.') && !mod.includes('/') && !mod.includes('\\'));
924
+ });
925
+ if (userFunctions.length === 0)
926
+ return null;
927
+ // Build current signatures
928
+ const currentSigs = {};
929
+ for (const fn of userFunctions) {
930
+ currentSigs[fn.name] = buildSignature(fn);
931
+ }
932
+ // Load previous snapshot for diff
933
+ const prevSnapshot = loadSnapshot();
934
+ const prevSigs = prevSnapshot?.functions || {};
935
+ // Compute diff
936
+ let newCount = 0;
937
+ let changedCount = 0;
938
+ const entries = [];
939
+ // Sort functions by module then name for clean output
940
+ const sorted = [...userFunctions].sort((a, b) => {
941
+ if (a.module !== b.module)
942
+ return a.module.localeCompare(b.module);
943
+ return a.name.localeCompare(b.name);
944
+ });
945
+ for (const fn of sorted) {
946
+ const sig = currentSigs[fn.name];
947
+ if (!(fn.name in prevSigs)) {
948
+ entries.push({ sig, status: 'new' });
949
+ newCount++;
950
+ }
951
+ else if (prevSigs[fn.name] !== sig) {
952
+ entries.push({ sig, status: 'changed' });
953
+ changedCount++;
954
+ }
955
+ else {
956
+ entries.push({ sig, status: 'same' });
957
+ }
958
+ }
959
+ // Save new snapshot
960
+ saveSnapshot({ functions: currentSigs });
961
+ // Build output
962
+ const header = newCount > 0 || changedCount > 0
963
+ ? `[trickle/auto] Discovered types (${newCount} new, ${changedCount} changed):`
964
+ : `[trickle/auto] Discovered types:`;
965
+ const lines = [header];
966
+ for (const entry of entries) {
967
+ const prefix = entry.status === 'new' ? ' + ' : entry.status === 'changed' ? ' ~ ' : ' ';
968
+ const suffix = entry.status === 'new' ? ' NEW' : entry.status === 'changed' ? ' CHANGED' : '';
969
+ lines.push(`${prefix}${entry.sig}${suffix}`);
970
+ }
971
+ return lines.join('\n');
972
+ }
973
+ /**
974
+ * Try to find the source file for a given module name.
975
+ */
976
+ function findSourceFile(moduleName) {
977
+ const cwd = process.cwd();
978
+ const exts = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
979
+ // Try direct match in cwd
980
+ for (const ext of exts) {
981
+ const candidate = path.join(cwd, moduleName + ext);
982
+ if (fs.existsSync(candidate))
983
+ return candidate;
984
+ }
985
+ // Try with dashes/underscores replaced
986
+ const normalized = moduleName.replace(/[-_]/g, '');
987
+ for (const ext of exts) {
988
+ const candidate = path.join(cwd, normalized + ext);
989
+ if (fs.existsSync(candidate))
990
+ return candidate;
991
+ }
992
+ // Try as subdirectory/index
993
+ for (const ext of exts) {
994
+ const candidate = path.join(cwd, moduleName, `index${ext}`);
995
+ if (fs.existsSync(candidate))
996
+ return candidate;
997
+ }
998
+ return null;
999
+ }