tywrap 0.5.0 → 0.6.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 (169) hide show
  1. package/dist/cli.js +8 -6
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +236 -147
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts.map +1 -1
  7. package/dist/core/annotation-parser.js +93 -83
  8. package/dist/core/annotation-parser.js.map +1 -1
  9. package/dist/core/discovery.d.ts.map +1 -1
  10. package/dist/core/discovery.js +26 -0
  11. package/dist/core/discovery.js.map +1 -1
  12. package/dist/core/emit-call.d.ts +58 -0
  13. package/dist/core/emit-call.d.ts.map +1 -0
  14. package/dist/core/emit-call.js +103 -0
  15. package/dist/core/emit-call.js.map +1 -0
  16. package/dist/core/generator.d.ts +2 -0
  17. package/dist/core/generator.d.ts.map +1 -1
  18. package/dist/core/generator.js +54 -211
  19. package/dist/core/generator.js.map +1 -1
  20. package/dist/core/mapper.d.ts.map +1 -1
  21. package/dist/core/mapper.js +118 -104
  22. package/dist/core/mapper.js.map +1 -1
  23. package/dist/index.d.ts +6 -25
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -31
  26. package/dist/index.js.map +1 -1
  27. package/dist/runtime/bounded-context.d.ts +2 -2
  28. package/dist/runtime/bounded-context.js +2 -2
  29. package/dist/runtime/{safe-codec.d.ts → bridge-codec.d.ts} +11 -11
  30. package/dist/runtime/bridge-codec.d.ts.map +1 -0
  31. package/dist/runtime/{safe-codec.js → bridge-codec.js} +26 -26
  32. package/dist/runtime/bridge-codec.js.map +1 -0
  33. package/dist/runtime/{http-io.d.ts → http-transport.d.ts} +6 -6
  34. package/dist/runtime/http-transport.d.ts.map +1 -0
  35. package/dist/runtime/{http-io.js → http-transport.js} +3 -3
  36. package/dist/runtime/http-transport.js.map +1 -0
  37. package/dist/runtime/http.d.ts +3 -3
  38. package/dist/runtime/http.d.ts.map +1 -1
  39. package/dist/runtime/http.js +4 -4
  40. package/dist/runtime/http.js.map +1 -1
  41. package/dist/runtime/index.d.ts +9 -1
  42. package/dist/runtime/index.d.ts.map +1 -1
  43. package/dist/runtime/index.js +9 -1
  44. package/dist/runtime/index.js.map +1 -1
  45. package/dist/runtime/node.d.ts +6 -6
  46. package/dist/runtime/node.d.ts.map +1 -1
  47. package/dist/runtime/node.js +7 -7
  48. package/dist/runtime/node.js.map +1 -1
  49. package/dist/runtime/pooled-transport.d.ts +11 -11
  50. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  51. package/dist/runtime/pooled-transport.js +10 -10
  52. package/dist/runtime/pooled-transport.js.map +1 -1
  53. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  54. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  55. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  56. package/dist/runtime/{pyodide-io.d.ts → pyodide-transport.d.ts} +8 -8
  57. package/dist/runtime/pyodide-transport.d.ts.map +1 -0
  58. package/dist/runtime/{pyodide-io.js → pyodide-transport.js} +5 -5
  59. package/dist/runtime/pyodide-transport.js.map +1 -0
  60. package/dist/runtime/pyodide.d.ts +3 -3
  61. package/dist/runtime/pyodide.d.ts.map +1 -1
  62. package/dist/runtime/pyodide.js +5 -5
  63. package/dist/runtime/pyodide.js.map +1 -1
  64. package/dist/runtime/rpc-client.d.ts +5 -5
  65. package/dist/runtime/rpc-client.d.ts.map +1 -1
  66. package/dist/runtime/rpc-client.js +6 -7
  67. package/dist/runtime/rpc-client.js.map +1 -1
  68. package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +9 -9
  69. package/dist/runtime/subprocess-transport.d.ts.map +1 -0
  70. package/dist/runtime/{process-io.js → subprocess-transport.js} +6 -6
  71. package/dist/runtime/subprocess-transport.js.map +1 -0
  72. package/dist/runtime/{worker-pool.d.ts → transport-pool.d.ts} +15 -15
  73. package/dist/runtime/transport-pool.d.ts.map +1 -0
  74. package/dist/runtime/{worker-pool.js → transport-pool.js} +6 -6
  75. package/dist/runtime/transport-pool.js.map +1 -0
  76. package/dist/runtime/transport.d.ts +14 -8
  77. package/dist/runtime/transport.d.ts.map +1 -1
  78. package/dist/runtime/transport.js +10 -4
  79. package/dist/runtime/transport.js.map +1 -1
  80. package/dist/types/index.d.ts +10 -1
  81. package/dist/types/index.d.ts.map +1 -1
  82. package/dist/tywrap.d.ts.map +1 -1
  83. package/dist/tywrap.js +1 -3
  84. package/dist/tywrap.js.map +1 -1
  85. package/dist/utils/cache.d.ts +2 -2
  86. package/dist/utils/cache.d.ts.map +1 -1
  87. package/dist/utils/cache.js +2 -2
  88. package/dist/utils/cache.js.map +1 -1
  89. package/dist/utils/codec.d.ts +1 -1
  90. package/dist/utils/codec.d.ts.map +1 -1
  91. package/dist/utils/codec.js +181 -182
  92. package/dist/utils/codec.js.map +1 -1
  93. package/dist/version.d.ts +12 -0
  94. package/dist/version.d.ts.map +1 -0
  95. package/dist/version.js +12 -0
  96. package/dist/version.js.map +1 -0
  97. package/package.json +3 -5
  98. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  99. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  100. package/runtime/python_bridge.py +54 -6
  101. package/runtime/safe_codec.py +8 -8
  102. package/runtime/tywrap_bridge_core.py +159 -23
  103. package/src/cli.ts +8 -12
  104. package/src/config/index.ts +271 -165
  105. package/src/core/annotation-parser.ts +112 -105
  106. package/src/core/discovery.ts +32 -0
  107. package/src/core/emit-call.ts +171 -0
  108. package/src/core/generator.ts +67 -263
  109. package/src/core/mapper.ts +125 -110
  110. package/src/index.ts +11 -68
  111. package/src/runtime/bounded-context.ts +2 -2
  112. package/src/runtime/{safe-codec.ts → bridge-codec.ts} +27 -27
  113. package/src/runtime/{http-io.ts → http-transport.ts} +5 -5
  114. package/src/runtime/http.ts +5 -5
  115. package/src/runtime/index.ts +23 -1
  116. package/src/runtime/node.ts +15 -15
  117. package/src/runtime/pooled-transport.ts +15 -15
  118. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  119. package/src/runtime/{pyodide-io.ts → pyodide-transport.ts} +7 -7
  120. package/src/runtime/pyodide.ts +6 -6
  121. package/src/runtime/rpc-client.ts +12 -8
  122. package/src/runtime/{process-io.ts → subprocess-transport.ts} +8 -8
  123. package/src/runtime/{worker-pool.ts → transport-pool.ts} +26 -26
  124. package/src/runtime/transport.ts +15 -8
  125. package/src/types/index.ts +10 -1
  126. package/src/tywrap.ts +1 -3
  127. package/src/utils/cache.ts +2 -2
  128. package/src/utils/codec.ts +217 -196
  129. package/src/version.ts +12 -0
  130. package/dist/core/analyzer.d.ts +0 -64
  131. package/dist/core/analyzer.d.ts.map +0 -1
  132. package/dist/core/analyzer.js +0 -710
  133. package/dist/core/analyzer.js.map +0 -1
  134. package/dist/runtime/http-io.d.ts.map +0 -1
  135. package/dist/runtime/http-io.js.map +0 -1
  136. package/dist/runtime/optimized-node.d.ts +0 -23
  137. package/dist/runtime/optimized-node.d.ts.map +0 -1
  138. package/dist/runtime/optimized-node.js +0 -23
  139. package/dist/runtime/optimized-node.js.map +0 -1
  140. package/dist/runtime/process-io.d.ts.map +0 -1
  141. package/dist/runtime/process-io.js.map +0 -1
  142. package/dist/runtime/protocol.d.ts +0 -3
  143. package/dist/runtime/protocol.d.ts.map +0 -1
  144. package/dist/runtime/protocol.js +0 -3
  145. package/dist/runtime/protocol.js.map +0 -1
  146. package/dist/runtime/pyodide-io.d.ts.map +0 -1
  147. package/dist/runtime/pyodide-io.js.map +0 -1
  148. package/dist/runtime/safe-codec.d.ts.map +0 -1
  149. package/dist/runtime/safe-codec.js.map +0 -1
  150. package/dist/runtime/worker-pool.d.ts.map +0 -1
  151. package/dist/runtime/worker-pool.js.map +0 -1
  152. package/dist/utils/bundle-optimizer.d.ts +0 -182
  153. package/dist/utils/bundle-optimizer.d.ts.map +0 -1
  154. package/dist/utils/bundle-optimizer.js +0 -680
  155. package/dist/utils/bundle-optimizer.js.map +0 -1
  156. package/dist/utils/memory-profiler.d.ts +0 -123
  157. package/dist/utils/memory-profiler.d.ts.map +0 -1
  158. package/dist/utils/memory-profiler.js +0 -426
  159. package/dist/utils/memory-profiler.js.map +0 -1
  160. package/dist/utils/parallel-processor.d.ts +0 -146
  161. package/dist/utils/parallel-processor.d.ts.map +0 -1
  162. package/dist/utils/parallel-processor.js +0 -707
  163. package/dist/utils/parallel-processor.js.map +0 -1
  164. package/src/core/analyzer.ts +0 -824
  165. package/src/runtime/optimized-node.ts +0 -25
  166. package/src/runtime/protocol.ts +0 -2
  167. package/src/utils/bundle-optimizer.ts +0 -876
  168. package/src/utils/memory-profiler.ts +0 -582
  169. package/src/utils/parallel-processor.ts +0 -955
@@ -310,7 +310,14 @@ export interface TywrapOptions {
310
310
 
311
311
  export interface PythonModuleConfig {
312
312
  version?: string;
313
- runtime: RuntimeStrategy;
313
+ /**
314
+ * @deprecated Dead input. The per-module runtime is never read during code
315
+ * generation — the active runtime is resolved from the top-level
316
+ * {@link RuntimeConfig} (`runtime.node`, etc.). Kept optional so existing
317
+ * configs that still set it continue to validate; it has no effect and will
318
+ * be removed in a future major release.
319
+ */
320
+ runtime?: RuntimeStrategy;
314
321
  functions?: string[];
315
322
  classes?: string[];
316
323
  /** Exclude specific exports by exact name. */
@@ -343,11 +350,13 @@ export interface PyodideConfig {
343
350
  export interface NodeConfig {
344
351
  pythonPath?: string;
345
352
  virtualEnv?: string;
353
+ /** Per-call timeout in milliseconds for the subprocess bridge. */
346
354
  timeout?: number;
347
355
  }
348
356
 
349
357
  export interface HttpConfig {
350
358
  baseURL: string;
359
+ /** Per-request timeout in milliseconds for the HTTP bridge. */
351
360
  timeout?: number;
352
361
  headers?: Record<string, string>;
353
362
  }
package/src/tywrap.ts CHANGED
@@ -18,7 +18,6 @@ import type {
18
18
  } from './types/index.js';
19
19
  import { fsUtils, pathUtils, processUtils, isWindows } from './utils/runtime.js';
20
20
  import { globalCache } from './utils/cache.js';
21
- import { globalParallelProcessor } from './utils/parallel-processor.js';
22
21
  import { resolvePythonExecutable } from './utils/python.js';
23
22
  import { computeIrCacheFilename } from './utils/ir-cache.js';
24
23
 
@@ -45,7 +44,6 @@ export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<Tywr
45
44
  const generator = new CodeGenerator(mapper);
46
45
 
47
46
  globalCache.setDebug(options.debug ?? false);
48
- globalParallelProcessor.setDebug(options.debug ?? false);
49
47
 
50
48
  return {
51
49
  mapper,
@@ -155,7 +153,7 @@ export async function generate(
155
153
  const moduleConfig = entry[1];
156
154
  // reset collector for each module
157
155
  unknownTypeNamesCollector = new Map();
158
- // Prefer Python IR extractor over TS analyzer with optional cache
156
+ // Resolve module IR via the Python extractor (with optional cache)
159
157
  const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
160
158
  let ir: unknown | null = null;
161
159
  let irError: string | undefined;
@@ -47,7 +47,7 @@ export interface CacheConfig {
47
47
  debug?: boolean;
48
48
  }
49
49
 
50
- export class IntelligentCache {
50
+ export class ArtifactCache {
51
51
  private memoryCache = new Map<string, CacheEntry>();
52
52
  private stats = {
53
53
  hits: 0,
@@ -685,4 +685,4 @@ export class IntelligentCache {
685
685
  }
686
686
 
687
687
  // Export singleton instance
688
- export const globalCache = new IntelligentCache();
688
+ export const globalCache = new ArtifactCache();
@@ -40,7 +40,7 @@ export interface SklearnEstimator {
40
40
  params: Record<string, unknown>;
41
41
  }
42
42
 
43
- export type CodecEnvelope =
43
+ export type ValueEnvelope =
44
44
  | {
45
45
  readonly __tywrap__: 'dataframe';
46
46
  readonly codecVersion?: number;
@@ -337,7 +337,7 @@ function reshapeArray(flat: unknown[], shape: readonly number[]): unknown {
337
337
  // Why: decoding needs to reject incompatible envelopes before we attempt to interpret payloads.
338
338
  const CODEC_VERSION = 1;
339
339
 
340
- function assertCodecVersion(envelope: { codecVersion?: unknown }, marker: string): void {
340
+ function assertCodecVersion(envelope: { codecVersion?: unknown }, typeTag: string): void {
341
341
  if (!('codecVersion' in envelope)) {
342
342
  return;
343
343
  }
@@ -346,235 +346,256 @@ function assertCodecVersion(envelope: { codecVersion?: unknown }, marker: string
346
346
  return;
347
347
  }
348
348
  if (typeof version !== 'number' || !Number.isFinite(version)) {
349
- throw new Error(`Invalid ${marker} envelope: codecVersion must be a number`);
349
+ throw new Error(`Invalid ${typeTag} envelope: codecVersion must be a number`);
350
350
  }
351
351
  if (version !== CODEC_VERSION) {
352
- throw new Error(`Unsupported ${marker} envelope codecVersion: ${version}`);
352
+ throw new Error(`Unsupported ${typeTag} envelope codecVersion: ${version}`);
353
353
  }
354
354
  }
355
355
 
356
- function decodeEnvelopeCore<T>(
357
- value: unknown,
356
+ /**
357
+ * Per-typeTag decode handler.
358
+ *
359
+ * Why: each scientific value envelope type (dataframe, series, ndarray, scipy.sparse,
360
+ * torch.tensor, sklearn.estimator) has its own validation and decode logic. Splitting
361
+ * the dispatch into one handler per type keeps each branch focused and testable while
362
+ * preserving byte-identical decoded output.
363
+ */
364
+ type EnvelopeHandler = <T>(
365
+ value: { [k: string]: unknown },
358
366
  decodeArrow: (bytes: Uint8Array) => MaybePromise<T>,
359
367
  recurse: (value: unknown) => MaybePromise<T | unknown>
360
- ): MaybePromise<T | unknown> {
361
- if (!isObject(value)) {
362
- return value;
363
- }
364
- const marker = (value as { __tywrap__?: unknown }).__tywrap__;
365
- if (typeof marker !== 'string') {
366
- return value as unknown;
367
- }
368
+ ) => MaybePromise<T | unknown>;
368
369
 
369
- if (
370
- marker === 'dataframe' ||
371
- marker === 'series' ||
372
- marker === 'ndarray' ||
373
- marker === 'scipy.sparse' ||
374
- marker === 'torch.tensor' ||
375
- marker === 'sklearn.estimator'
376
- ) {
377
- assertCodecVersion(value as { codecVersion?: unknown }, marker);
378
- }
379
-
380
- if (marker === 'dataframe') {
381
- const encoding = (value as { encoding?: unknown }).encoding;
382
- if (encoding === 'arrow') {
383
- const b64 = (value as { b64?: unknown }).b64;
384
- if (typeof b64 !== 'string') {
385
- throw new Error('Invalid dataframe envelope: missing b64');
386
- }
387
- const bytes = fromBase64(b64);
388
- return decodeArrow(bytes);
389
- }
390
- if (encoding === 'json') {
391
- if (!('data' in (value as object))) {
392
- throw new Error('Invalid dataframe envelope: missing data');
393
- }
394
- return (value as { data: unknown }).data;
370
+ /**
371
+ * Decode an Arrow-or-JSON envelope (dataframe / series).
372
+ *
373
+ * Why: dataframe and series share identical arrow/json handling; the only difference
374
+ * is the typeTag name used in error messages.
375
+ */
376
+ function decodeArrowOrJsonEnvelope<T>(
377
+ value: { [k: string]: unknown },
378
+ decodeArrow: (bytes: Uint8Array) => MaybePromise<T>,
379
+ typeTag: string
380
+ ): MaybePromise<T | unknown> {
381
+ const encoding = value.encoding;
382
+ if (encoding === 'arrow') {
383
+ const b64 = value.b64;
384
+ if (typeof b64 !== 'string') {
385
+ throw new Error(`Invalid ${typeTag} envelope: missing b64`);
395
386
  }
396
- throw new Error(`Invalid dataframe envelope: unsupported encoding ${String(encoding)}`);
387
+ const bytes = fromBase64(b64);
388
+ return decodeArrow(bytes);
397
389
  }
398
-
399
- if (marker === 'series') {
400
- const encoding = (value as { encoding?: unknown }).encoding;
401
- if (encoding === 'arrow') {
402
- const b64 = (value as { b64?: unknown }).b64;
403
- if (typeof b64 !== 'string') {
404
- throw new Error('Invalid series envelope: missing b64');
405
- }
406
- const bytes = fromBase64(b64);
407
- return decodeArrow(bytes);
390
+ if (encoding === 'json') {
391
+ if (!('data' in value)) {
392
+ throw new Error(`Invalid ${typeTag} envelope: missing data`);
408
393
  }
409
- if (encoding === 'json') {
410
- if (!('data' in (value as object))) {
411
- throw new Error('Invalid series envelope: missing data');
412
- }
413
- return (value as { data: unknown }).data;
414
- }
415
- throw new Error(`Invalid series envelope: unsupported encoding ${String(encoding)}`);
394
+ return value.data;
416
395
  }
396
+ throw new Error(`Invalid ${typeTag} envelope: unsupported encoding ${String(encoding)}`);
397
+ }
417
398
 
418
- if (marker === 'ndarray') {
419
- const encoding = (value as { encoding?: unknown }).encoding;
420
- const shapeValue = (value as { shape?: unknown }).shape;
421
- const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
399
+ const decodeDataframeEnvelope: EnvelopeHandler = (value, decodeArrow) =>
400
+ decodeArrowOrJsonEnvelope(value, decodeArrow, 'dataframe');
422
401
 
423
- if (encoding === 'arrow') {
424
- const b64 = (value as { b64?: unknown }).b64;
425
- if (typeof b64 !== 'string') {
426
- throw new Error('Invalid ndarray envelope: missing b64');
427
- }
428
- const bytes = fromBase64(b64);
429
- const decoded = decodeArrow(bytes);
430
-
431
- // Extract values from Arrow table and reshape if needed
432
- // Arrow only handles 1D arrays, so we flatten on encode and reshape here
433
- // Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
434
- // Skip reshape for: 1D arrays (shape.length === 1) - return as-is
435
- if (isPromiseLike(decoded)) {
436
- return decoded.then(data => {
437
- const values = extractArrowValues(data);
438
- if (!values) {
439
- return data; // Fallback: return raw data if extraction fails
440
- }
441
- // Reshape scalars and multi-dimensional arrays, but not 1D
442
- if (shape && shape.length !== 1) {
443
- return reshapeArray(values, shape);
444
- }
445
- return values;
446
- });
447
- }
448
- const values = extractArrowValues(decoded);
449
- if (!values) {
450
- return decoded; // Fallback: return raw data if extraction fails
451
- }
452
- // Reshape scalars and multi-dimensional arrays, but not 1D
453
- if (shape && shape.length !== 1) {
454
- return reshapeArray(values, shape);
455
- }
456
- return values;
457
- }
458
- if (encoding === 'json') {
459
- if (!('data' in (value as object))) {
460
- throw new Error('Invalid ndarray envelope: missing data');
461
- }
462
- return (value as { data: unknown }).data;
463
- }
464
- throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
465
- }
402
+ const decodeSeriesEnvelope: EnvelopeHandler = (value, decodeArrow) =>
403
+ decodeArrowOrJsonEnvelope(value, decodeArrow, 'series');
466
404
 
467
- if (marker === 'scipy.sparse') {
468
- const encoding = (value as { encoding?: unknown }).encoding;
469
- if (encoding !== 'json') {
470
- throw new Error(`Invalid scipy.sparse envelope: unsupported encoding ${String(encoding)}`);
405
+ const decodeNdarrayEnvelope: EnvelopeHandler = (value, decodeArrow) => {
406
+ const encoding = value.encoding;
407
+ const shapeValue = value.shape;
408
+ const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
409
+
410
+ if (encoding === 'arrow') {
411
+ const b64 = value.b64;
412
+ if (typeof b64 !== 'string') {
413
+ throw new Error('Invalid ndarray envelope: missing b64');
471
414
  }
472
- const format = (value as { format?: unknown }).format;
473
- if (format !== 'csr' && format !== 'csc' && format !== 'coo') {
474
- throw new Error(`Invalid scipy.sparse envelope: unsupported format ${String(format)}`);
415
+ const bytes = fromBase64(b64);
416
+ const decoded = decodeArrow(bytes);
417
+
418
+ // Extract values from Arrow table and reshape if needed
419
+ // Arrow only handles 1D arrays, so we flatten on encode and reshape here
420
+ // Reshape for: scalars (shape.length === 0) and multi-dim (shape.length > 1)
421
+ // Skip reshape for: 1D arrays (shape.length === 1) - return as-is
422
+ if (isPromiseLike(decoded)) {
423
+ return decoded.then(data => {
424
+ const values = extractArrowValues(data);
425
+ if (!values) {
426
+ return data; // Fallback: return raw data if extraction fails
427
+ }
428
+ // Reshape scalars and multi-dimensional arrays, but not 1D
429
+ if (shape && shape.length !== 1) {
430
+ return reshapeArray(values, shape);
431
+ }
432
+ return values;
433
+ });
475
434
  }
476
- const shape = (value as { shape?: unknown }).shape;
477
- if (
478
- !Array.isArray(shape) ||
479
- shape.length !== 2 ||
480
- typeof shape[0] !== 'number' ||
481
- typeof shape[1] !== 'number'
482
- ) {
483
- throw new Error('Invalid scipy.sparse envelope: shape must be a 2-item number[]');
435
+ const values = extractArrowValues(decoded);
436
+ if (!values) {
437
+ return decoded; // Fallback: return raw data if extraction fails
484
438
  }
485
- const data = (value as { data?: unknown }).data;
486
- if (!Array.isArray(data)) {
487
- throw new Error('Invalid scipy.sparse envelope: data must be an array');
439
+ // Reshape scalars and multi-dimensional arrays, but not 1D
440
+ if (shape && shape.length !== 1) {
441
+ return reshapeArray(values, shape);
488
442
  }
489
- const dtypeValue = (value as { dtype?: unknown }).dtype;
490
- const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
491
-
492
- if (format === 'coo') {
493
- const row = (value as { row?: unknown }).row;
494
- const col = (value as { col?: unknown }).col;
495
- if (!Array.isArray(row) || !Array.isArray(col)) {
496
- throw new Error('Invalid scipy.sparse envelope: coo requires row and col arrays');
497
- }
498
- return {
499
- format,
500
- shape,
501
- data,
502
- row,
503
- col,
504
- dtype,
505
- } satisfies SparseMatrix;
443
+ return values;
444
+ }
445
+ if (encoding === 'json') {
446
+ if (!('data' in value)) {
447
+ throw new Error('Invalid ndarray envelope: missing data');
506
448
  }
449
+ return value.data;
450
+ }
451
+ throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
452
+ };
507
453
 
508
- const indices = (value as { indices?: unknown }).indices;
509
- const indptr = (value as { indptr?: unknown }).indptr;
510
- if (!Array.isArray(indices) || !Array.isArray(indptr)) {
511
- throw new Error('Invalid scipy.sparse envelope: csr/csc requires indices and indptr arrays');
454
+ const decodeScipySparseEnvelope: EnvelopeHandler = value => {
455
+ const encoding = value.encoding;
456
+ if (encoding !== 'json') {
457
+ throw new Error(`Invalid scipy.sparse envelope: unsupported encoding ${String(encoding)}`);
458
+ }
459
+ const format = value.format;
460
+ if (format !== 'csr' && format !== 'csc' && format !== 'coo') {
461
+ throw new Error(`Invalid scipy.sparse envelope: unsupported format ${String(format)}`);
462
+ }
463
+ const shape = value.shape;
464
+ if (
465
+ !Array.isArray(shape) ||
466
+ shape.length !== 2 ||
467
+ typeof shape[0] !== 'number' ||
468
+ typeof shape[1] !== 'number'
469
+ ) {
470
+ throw new Error('Invalid scipy.sparse envelope: shape must be a 2-item number[]');
471
+ }
472
+ const data = value.data;
473
+ if (!Array.isArray(data)) {
474
+ throw new Error('Invalid scipy.sparse envelope: data must be an array');
475
+ }
476
+ const dtypeValue = value.dtype;
477
+ const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
478
+
479
+ if (format === 'coo') {
480
+ const row = value.row;
481
+ const col = value.col;
482
+ if (!Array.isArray(row) || !Array.isArray(col)) {
483
+ throw new Error('Invalid scipy.sparse envelope: coo requires row and col arrays');
512
484
  }
513
485
  return {
514
486
  format,
515
487
  shape,
516
488
  data,
517
- indices,
518
- indptr,
489
+ row,
490
+ col,
519
491
  dtype,
520
492
  } satisfies SparseMatrix;
521
493
  }
522
494
 
523
- if (marker === 'torch.tensor') {
524
- const encoding = (value as { encoding?: unknown }).encoding;
525
- if (encoding !== 'ndarray') {
526
- throw new Error(`Invalid torch.tensor envelope: unsupported encoding ${String(encoding)}`);
527
- }
528
- if (!('value' in (value as object))) {
529
- throw new Error('Invalid torch.tensor envelope: missing value');
530
- }
531
- const nested = (value as { value: unknown }).value;
532
- if (!isObject(nested) || (nested as { __tywrap__?: unknown }).__tywrap__ !== 'ndarray') {
533
- throw new Error('Invalid torch.tensor envelope: value must be an ndarray envelope');
534
- }
535
- const decoded = recurse(nested);
536
- const shapeValue = (value as { shape?: unknown }).shape;
537
- const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
538
- const dtypeValue = (value as { dtype?: unknown }).dtype;
539
- const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
540
- const deviceValue = (value as { device?: unknown }).device;
541
- const device = typeof deviceValue === 'string' ? deviceValue : undefined;
495
+ const indices = value.indices;
496
+ const indptr = value.indptr;
497
+ if (!Array.isArray(indices) || !Array.isArray(indptr)) {
498
+ throw new Error('Invalid scipy.sparse envelope: csr/csc requires indices and indptr arrays');
499
+ }
500
+ return {
501
+ format,
502
+ shape,
503
+ data,
504
+ indices,
505
+ indptr,
506
+ dtype,
507
+ } satisfies SparseMatrix;
508
+ };
509
+
510
+ const decodeTorchTensorEnvelope: EnvelopeHandler = <T>(
511
+ value: { [k: string]: unknown },
512
+ _decodeArrow: (bytes: Uint8Array) => MaybePromise<T>,
513
+ recurse: (value: unknown) => MaybePromise<T | unknown>
514
+ ): MaybePromise<T | unknown> => {
515
+ const encoding = value.encoding;
516
+ if (encoding !== 'ndarray') {
517
+ throw new Error(`Invalid torch.tensor envelope: unsupported encoding ${String(encoding)}`);
518
+ }
519
+ if (!('value' in value)) {
520
+ throw new Error('Invalid torch.tensor envelope: missing value');
521
+ }
522
+ const nested = value.value;
523
+ if (!isObject(nested) || (nested as { __tywrap__?: unknown }).__tywrap__ !== 'ndarray') {
524
+ throw new Error('Invalid torch.tensor envelope: value must be an ndarray envelope');
525
+ }
526
+ const decoded = recurse(nested);
527
+ const shapeValue = value.shape;
528
+ const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
529
+ const dtypeValue = value.dtype;
530
+ const dtype = typeof dtypeValue === 'string' ? dtypeValue : undefined;
531
+ const deviceValue = value.device;
532
+ const device = typeof deviceValue === 'string' ? deviceValue : undefined;
542
533
 
543
- if (isPromiseLike(decoded)) {
544
- return decoded.then(data => ({ data, shape, dtype, device })) as Promise<T | unknown>;
545
- }
546
- return { data: decoded, shape, dtype, device } satisfies TorchTensor;
534
+ if (isPromiseLike(decoded)) {
535
+ return decoded.then(data => ({ data, shape, dtype, device })) as Promise<T | unknown>;
547
536
  }
537
+ return { data: decoded, shape, dtype, device } satisfies TorchTensor;
538
+ };
548
539
 
549
- if (marker === 'sklearn.estimator') {
550
- const encoding = (value as { encoding?: unknown }).encoding;
551
- if (encoding !== 'json') {
552
- throw new Error(
553
- `Invalid sklearn.estimator envelope: unsupported encoding ${String(encoding)}`
554
- );
555
- }
556
- const className = (value as { className?: unknown }).className;
557
- const module = (value as { module?: unknown }).module;
558
- const params = (value as { params?: unknown }).params;
559
- if (typeof className !== 'string' || typeof module !== 'string' || !isObject(params)) {
560
- throw new Error(
561
- 'Invalid sklearn.estimator envelope: expected className/module strings + params object'
562
- );
563
- }
564
- const versionValue = (value as { version?: unknown }).version;
565
- if (versionValue !== undefined && typeof versionValue !== 'string') {
566
- throw new Error('Invalid sklearn.estimator envelope: version must be a string when provided');
567
- }
568
- const version = typeof versionValue === 'string' ? versionValue : undefined;
569
- return {
570
- className,
571
- module,
572
- version,
573
- params,
574
- } satisfies SklearnEstimator;
540
+ const decodeSklearnEstimatorEnvelope: EnvelopeHandler = value => {
541
+ const encoding = value.encoding;
542
+ if (encoding !== 'json') {
543
+ throw new Error(`Invalid sklearn.estimator envelope: unsupported encoding ${String(encoding)}`);
544
+ }
545
+ const className = value.className;
546
+ const module = value.module;
547
+ const params = value.params;
548
+ if (typeof className !== 'string' || typeof module !== 'string' || !isObject(params)) {
549
+ throw new Error(
550
+ 'Invalid sklearn.estimator envelope: expected className/module strings + params object'
551
+ );
552
+ }
553
+ const versionValue = value.version;
554
+ if (versionValue !== undefined && typeof versionValue !== 'string') {
555
+ throw new Error('Invalid sklearn.estimator envelope: version must be a string when provided');
556
+ }
557
+ const version = typeof versionValue === 'string' ? versionValue : undefined;
558
+ return {
559
+ className,
560
+ module,
561
+ version,
562
+ params,
563
+ } satisfies SklearnEstimator;
564
+ };
565
+
566
+ // Why: dispatch over the __tywrap__ typeTag instead of a long if-chain so each type's
567
+ // decode logic lives in one focused handler. The typeTag strings are the on-the-wire keys
568
+ // emitted by the Python bridge and MUST stay byte-identical. A Map keyed by the typeTag
569
+ // avoids prototype-chain lookups when dispatching on attacker-controlled typeTag strings.
570
+ const ENVELOPE_HANDLERS: ReadonlyMap<string, EnvelopeHandler> = new Map([
571
+ ['dataframe', decodeDataframeEnvelope],
572
+ ['series', decodeSeriesEnvelope],
573
+ ['ndarray', decodeNdarrayEnvelope],
574
+ ['scipy.sparse', decodeScipySparseEnvelope],
575
+ ['torch.tensor', decodeTorchTensorEnvelope],
576
+ ['sklearn.estimator', decodeSklearnEstimatorEnvelope],
577
+ ]);
578
+
579
+ function decodeEnvelopeCore<T>(
580
+ value: unknown,
581
+ decodeArrow: (bytes: Uint8Array) => MaybePromise<T>,
582
+ recurse: (value: unknown) => MaybePromise<T | unknown>
583
+ ): MaybePromise<T | unknown> {
584
+ if (!isObject(value)) {
585
+ return value;
586
+ }
587
+ const typeTag = (value as { __tywrap__?: unknown }).__tywrap__;
588
+ if (typeof typeTag !== 'string') {
589
+ return value as unknown;
590
+ }
591
+
592
+ const handler = ENVELOPE_HANDLERS.get(typeTag);
593
+ if (!handler) {
594
+ return value as unknown;
575
595
  }
576
596
 
577
- return value as unknown;
597
+ assertCodecVersion(value as { codecVersion?: unknown }, typeTag);
598
+ return handler(value, decodeArrow, recurse);
578
599
  }
579
600
 
580
601
  function decodeEnvelope<T>(value: unknown, decodeArrow: (bytes: Uint8Array) => T): T | unknown {
package/src/version.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * AUTO-GENERATED — DO NOT EDIT.
3
+ *
4
+ * Source: package.json (version field)
5
+ * Generator: scripts/generate-version.mjs (runs in `npm run build`)
6
+ *
7
+ * Single-sources the package version so the public API can re-export it without
8
+ * importing package.json from outside tsconfig `rootDir`.
9
+ * Regenerate with: node scripts/generate-version.mjs
10
+ */
11
+
12
+ export const VERSION: string = "0.6.0";
@@ -1,64 +0,0 @@
1
- /**
2
- * PyAnalyzer - Python AST analysis and type extraction
3
- */
4
- import type { SyntaxNode } from 'tree-sitter';
5
- import type { AnalysisResult, PythonFunction, PythonClass, PythonType } from '../types/index.js';
6
- export declare class PyAnalyzer {
7
- private parser;
8
- private initialized;
9
- constructor();
10
- /**
11
- * Initialize the tree-sitter parser with Python grammar
12
- */
13
- initialize(): Promise<void>;
14
- /**
15
- * Analyze Python module source code and extract structure with caching
16
- */
17
- analyzePythonModule(source: string, modulePath?: string): Promise<AnalysisResult>;
18
- /**
19
- * Extract function definitions from AST
20
- */
21
- extractFunctions(node: SyntaxNode): Promise<PythonFunction[]>;
22
- /**
23
- * Extract class definitions from AST
24
- */
25
- extractClasses(node: SyntaxNode): Promise<PythonClass[]>;
26
- /**
27
- * Extract single function from function_definition node
28
- */
29
- private extractFunction;
30
- /**
31
- * Extract single class from class_definition node
32
- */
33
- private extractClass;
34
- /**
35
- * Extract function parameters from parameters node
36
- */
37
- private extractParameters;
38
- /**
39
- * Parse type annotation node into PythonType
40
- */
41
- parseTypeAnnotation(node: SyntaxNode): PythonType;
42
- private parseGenericType;
43
- private parseUnionType;
44
- private parseGenericArguments;
45
- /**
46
- * Extract import statements
47
- */
48
- private extractImports;
49
- /**
50
- * Utility methods
51
- */
52
- private findNodesByType;
53
- private findDirectDefinitions;
54
- private extractModuleName;
55
- private extractDecorators;
56
- private extractDocstring;
57
- private containsYield;
58
- private extractBases;
59
- private extractProperty;
60
- private extractExports;
61
- private collectSyntaxErrors;
62
- private generateStatistics;
63
- }
64
- //# sourceMappingURL=analyzer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/core/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,OAAO,KAAK,EACV,cAAc,EAKd,cAAc,EACd,WAAW,EAEX,UAAU,EAIX,MAAM,mBAAmB,CAAC;AAS3B,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,WAAW,CAAS;;IAM5B;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAajC;;OAEG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAiFvF;;OAEG;IACG,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAiBnE;;OAEG;IACG,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAiB9D;;OAEG;YACW,eAAe;IAyC7B;;OAEG;YACW,YAAY;IAuD1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkHzB;;OAEG;IACH,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU;IA0FjD,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,qBAAqB;IAiC7B;;OAEG;IACH,OAAO,CAAC,cAAc;IAwFtB;;OAEG;IACH,OAAO,CAAC,eAAe;IAqBvB,OAAO,CAAC,qBAAqB;IAoB7B,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,gBAAgB;IAyBxB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,kBAAkB;CA0B3B"}