tywrap 0.8.0 → 0.9.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 (157) hide show
  1. package/README.md +22 -7
  2. package/SECURITY.md +39 -0
  3. package/dist/config/index.d.ts.map +1 -1
  4. package/dist/config/index.js +8 -0
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/core/annotation-parser.d.ts +2 -1
  7. package/dist/core/annotation-parser.d.ts.map +1 -1
  8. package/dist/core/annotation-parser.js +6 -3
  9. package/dist/core/annotation-parser.js.map +1 -1
  10. package/dist/core/generator.d.ts +23 -4
  11. package/dist/core/generator.d.ts.map +1 -1
  12. package/dist/core/generator.js +243 -170
  13. package/dist/core/generator.js.map +1 -1
  14. package/dist/core/mapper.d.ts +3 -2
  15. package/dist/core/mapper.d.ts.map +1 -1
  16. package/dist/core/mapper.js +5 -5
  17. package/dist/core/mapper.js.map +1 -1
  18. package/dist/index.d.ts +7 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +6 -4
  21. package/dist/index.js.map +1 -1
  22. package/dist/runtime/base-bridge.d.ts +3 -7
  23. package/dist/runtime/base-bridge.d.ts.map +1 -1
  24. package/dist/runtime/base-bridge.js +4 -17
  25. package/dist/runtime/base-bridge.js.map +1 -1
  26. package/dist/runtime/bounded-context.d.ts +3 -22
  27. package/dist/runtime/bounded-context.d.ts.map +1 -1
  28. package/dist/runtime/bounded-context.js +13 -53
  29. package/dist/runtime/bounded-context.js.map +1 -1
  30. package/dist/runtime/bridge-codec.d.ts +1 -1
  31. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  32. package/dist/runtime/bridge-codec.js +78 -55
  33. package/dist/runtime/bridge-codec.js.map +1 -1
  34. package/dist/runtime/errors.d.ts +16 -0
  35. package/dist/runtime/errors.d.ts.map +1 -1
  36. package/dist/runtime/errors.js +17 -0
  37. package/dist/runtime/errors.js.map +1 -1
  38. package/dist/runtime/http-transport.d.ts +1 -1
  39. package/dist/runtime/http-transport.d.ts.map +1 -1
  40. package/dist/runtime/http-transport.js +1 -1
  41. package/dist/runtime/http-transport.js.map +1 -1
  42. package/dist/runtime/index.d.ts +1 -0
  43. package/dist/runtime/index.d.ts.map +1 -1
  44. package/dist/runtime/index.js +1 -0
  45. package/dist/runtime/index.js.map +1 -1
  46. package/dist/runtime/node.d.ts +7 -36
  47. package/dist/runtime/node.d.ts.map +1 -1
  48. package/dist/runtime/node.js +2 -80
  49. package/dist/runtime/node.js.map +1 -1
  50. package/dist/runtime/pooled-transport.d.ts +120 -59
  51. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  52. package/dist/runtime/pooled-transport.js +345 -78
  53. package/dist/runtime/pooled-transport.js.map +1 -1
  54. package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
  55. package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
  56. package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
  57. package/dist/runtime/pyodide-transport.d.ts +1 -1
  58. package/dist/runtime/pyodide-transport.d.ts.map +1 -1
  59. package/dist/runtime/pyodide-transport.js +2 -3
  60. package/dist/runtime/pyodide-transport.js.map +1 -1
  61. package/dist/runtime/rpc-client.d.ts +7 -36
  62. package/dist/runtime/rpc-client.d.ts.map +1 -1
  63. package/dist/runtime/rpc-client.js +20 -102
  64. package/dist/runtime/rpc-client.js.map +1 -1
  65. package/dist/runtime/subprocess-transport.d.ts +30 -69
  66. package/dist/runtime/subprocess-transport.d.ts.map +1 -1
  67. package/dist/runtime/subprocess-transport.js +179 -236
  68. package/dist/runtime/subprocess-transport.js.map +1 -1
  69. package/dist/runtime/timed-out-request-tracker.d.ts +2 -1
  70. package/dist/runtime/timed-out-request-tracker.d.ts.map +1 -1
  71. package/dist/runtime/transport.d.ts +9 -19
  72. package/dist/runtime/transport.d.ts.map +1 -1
  73. package/dist/runtime/transport.js +1 -1
  74. package/dist/runtime/transport.js.map +1 -1
  75. package/dist/runtime/validators.d.ts +49 -0
  76. package/dist/runtime/validators.d.ts.map +1 -1
  77. package/dist/runtime/validators.js +152 -0
  78. package/dist/runtime/validators.js.map +1 -1
  79. package/dist/types/index.d.ts +14 -39
  80. package/dist/types/index.d.ts.map +1 -1
  81. package/dist/tywrap.d.ts +3 -2
  82. package/dist/tywrap.d.ts.map +1 -1
  83. package/dist/tywrap.js +140 -13
  84. package/dist/tywrap.js.map +1 -1
  85. package/dist/utils/cache.d.ts +3 -16
  86. package/dist/utils/cache.d.ts.map +1 -1
  87. package/dist/utils/codec.d.ts +1 -0
  88. package/dist/utils/codec.d.ts.map +1 -1
  89. package/dist/utils/codec.js +13 -13
  90. package/dist/utils/codec.js.map +1 -1
  91. package/dist/utils/ir-cache.d.ts +2 -1
  92. package/dist/utils/ir-cache.d.ts.map +1 -1
  93. package/dist/utils/runtime.d.ts +0 -29
  94. package/dist/utils/runtime.d.ts.map +1 -1
  95. package/dist/utils/runtime.js +16 -107
  96. package/dist/utils/runtime.js.map +1 -1
  97. package/dist/version.js +1 -1
  98. package/package.json +7 -6
  99. package/runtime/frame_codec.py +7 -1
  100. package/runtime/python_bridge.py +85 -125
  101. package/runtime/tywrap_bridge_core.py +133 -150
  102. package/src/config/index.ts +11 -0
  103. package/src/core/annotation-parser.ts +7 -4
  104. package/src/core/generator.ts +315 -205
  105. package/src/core/mapper.ts +8 -8
  106. package/src/index.ts +7 -4
  107. package/src/runtime/base-bridge.ts +5 -30
  108. package/src/runtime/bounded-context.ts +12 -67
  109. package/src/runtime/bridge-codec.ts +94 -65
  110. package/src/runtime/errors.ts +21 -0
  111. package/src/runtime/http-transport.ts +6 -1
  112. package/src/runtime/index.ts +6 -0
  113. package/src/runtime/node.ts +9 -120
  114. package/src/runtime/pooled-transport.ts +424 -90
  115. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  116. package/src/runtime/pyodide-transport.ts +7 -3
  117. package/src/runtime/rpc-client.ts +37 -139
  118. package/src/runtime/subprocess-transport.ts +210 -285
  119. package/src/runtime/timed-out-request-tracker.ts +1 -1
  120. package/src/runtime/transport.ts +15 -23
  121. package/src/runtime/validators.ts +204 -0
  122. package/src/types/index.ts +21 -60
  123. package/src/tywrap.ts +157 -22
  124. package/src/utils/cache.ts +3 -3
  125. package/src/utils/codec.ts +21 -13
  126. package/src/utils/ir-cache.ts +1 -1
  127. package/src/utils/runtime.ts +17 -128
  128. package/src/version.ts +1 -1
  129. package/dist/core/discovery.d.ts +0 -103
  130. package/dist/core/discovery.d.ts.map +0 -1
  131. package/dist/core/discovery.js +0 -380
  132. package/dist/core/discovery.js.map +0 -1
  133. package/dist/core/validation.d.ts +0 -102
  134. package/dist/core/validation.d.ts.map +0 -1
  135. package/dist/core/validation.js +0 -490
  136. package/dist/core/validation.js.map +0 -1
  137. package/dist/runtime/base.d.ts +0 -22
  138. package/dist/runtime/base.d.ts.map +0 -1
  139. package/dist/runtime/base.js +0 -23
  140. package/dist/runtime/base.js.map +0 -1
  141. package/dist/runtime/transport-pool.d.ts +0 -196
  142. package/dist/runtime/transport-pool.d.ts.map +0 -1
  143. package/dist/runtime/transport-pool.js +0 -418
  144. package/dist/runtime/transport-pool.js.map +0 -1
  145. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  146. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  147. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  148. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  149. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  150. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  151. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  152. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  153. package/runtime/safe_codec.py +0 -352
  154. package/src/core/discovery.ts +0 -477
  155. package/src/core/validation.ts +0 -729
  156. package/src/runtime/base.ts +0 -24
  157. package/src/runtime/transport-pool.ts +0 -538
package/src/tywrap.ts CHANGED
@@ -16,13 +16,15 @@ import type {
16
16
  Parameter,
17
17
  PythonType,
18
18
  PythonModuleConfig,
19
+ IrContract,
19
20
  } from './types/index.js';
20
21
  import { fsUtils, pathUtils, processUtils, isWindows } from './utils/runtime.js';
21
22
  import { globalCache } from './utils/cache.js';
22
23
  import { resolvePythonExecutable } from './utils/python.js';
23
24
  import { computeIrCacheFilename } from './utils/ir-cache.js';
25
+ import { logger } from './utils/logger.js';
24
26
 
25
- const TYWRAP_IR_VERSION = '0.3.0';
27
+ const TYWRAP_IR_VERSION = '0.4.0';
26
28
 
27
29
  // Collect unknown typing constructs encountered during annotation parsing (per-generate run)
28
30
  let unknownTypeNamesCollector: Map<string, number> = new Map();
@@ -34,7 +36,7 @@ function recordUnknown(name: string): void {
34
36
  /**
35
37
  * Main tywrap function
36
38
  */
37
- export interface TywrapInstance {
39
+ interface TywrapInstance {
38
40
  mapper: TypeMapper;
39
41
  generator: CodeGenerator;
40
42
  options: Partial<TywrapOptions>;
@@ -42,7 +44,9 @@ export interface TywrapInstance {
42
44
 
43
45
  export async function tywrap(options: Partial<TywrapOptions> = {}): Promise<TywrapInstance> {
44
46
  const mapper = new TypeMapper({ presets: options.types?.presets });
45
- const generator = new CodeGenerator(mapper);
47
+ const generator = new CodeGenerator(mapper, {
48
+ onTypeDegrade: typeName => recordUnknown(`unresolvable generated type ${typeName}`),
49
+ });
46
50
 
47
51
  globalCache.setDebug(options.debug ?? false);
48
52
 
@@ -63,7 +67,7 @@ export interface GenerateRunOptions {
63
67
 
64
68
  export interface GenerateFailure {
65
69
  module: string;
66
- code: 'ir-unavailable';
70
+ code: 'ir-unavailable' | 'ir-version-mismatch' | 'contract-invalid';
67
71
  message: string;
68
72
  }
69
73
 
@@ -83,6 +87,68 @@ function normalizeForComparison(text: string): string {
83
87
  return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
84
88
  }
85
89
 
90
+ function stableJson(value: unknown): unknown {
91
+ if (Array.isArray(value)) {
92
+ return value.map(stableJson);
93
+ }
94
+ if (value !== null && typeof value === 'object') {
95
+ return Object.fromEntries(
96
+ Object.entries(value as Record<string, unknown>)
97
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
98
+ .map(([key, child]) => [key, stableJson(child)])
99
+ );
100
+ }
101
+ return value;
102
+ }
103
+
104
+ function serializeContract(ir: IrContract): string {
105
+ const contract = { ...ir };
106
+ delete contract.metadata;
107
+ return `${JSON.stringify(stableJson(contract), null, 2)}\n`;
108
+ }
109
+
110
+ function validateIrVersion(
111
+ ir: unknown,
112
+ source: string
113
+ ): { ir: IrContract | null; error?: string; code?: GenerateFailure['code'] } {
114
+ if (ir === null || typeof ir !== 'object' || Array.isArray(ir)) {
115
+ return { ir: null, code: 'contract-invalid', error: `${source} is not a JSON IR object.` };
116
+ }
117
+ const contract = ir as IrContract;
118
+ if (typeof contract.ir_version !== 'string') {
119
+ return {
120
+ ir: null,
121
+ code: 'contract-invalid',
122
+ error: `${source} is missing its string ir_version.`,
123
+ };
124
+ }
125
+ if (contract.ir_version !== TYWRAP_IR_VERSION) {
126
+ return {
127
+ ir: null,
128
+ code: 'ir-version-mismatch',
129
+ error: `IR version mismatch: TypeScript expects ${TYWRAP_IR_VERSION}, but ${source} declares ${contract.ir_version}. Regenerate the contract with a matching tywrap_ir.`,
130
+ };
131
+ }
132
+ if (typeof contract.module !== 'string' || contract.module.length === 0) {
133
+ return { ir: null, code: 'contract-invalid', error: `${source} is missing its module name.` };
134
+ }
135
+ const requiredArrays: ReadonlyArray<readonly [string, unknown]> = [
136
+ ['functions', contract.functions],
137
+ ['classes', contract.classes],
138
+ ['type_aliases', contract.type_aliases],
139
+ ];
140
+ for (const [field, value] of requiredArrays) {
141
+ if (!Array.isArray(value)) {
142
+ return {
143
+ ir: null,
144
+ code: 'contract-invalid',
145
+ error: `${source} is missing required array field ${field}.`,
146
+ };
147
+ }
148
+ }
149
+ return { ir: contract };
150
+ }
151
+
86
152
  async function safeReadFile(path: string): Promise<string | null> {
87
153
  try {
88
154
  return await fsUtils.readFile(path);
@@ -106,13 +172,17 @@ async function fetchAndCacheIr(
106
172
  cacheKey: string,
107
173
  caching: boolean,
108
174
  checkMode: boolean
109
- ): Promise<{ ir: unknown | null; error?: string }> {
175
+ ): Promise<{ ir: IrContract | null; error?: string; code?: GenerateFailure['code'] }> {
110
176
  let ir: unknown | null = null;
111
177
  let irError: string | undefined;
112
178
  if (caching && fsUtils.isAvailable() && !checkMode) {
113
179
  try {
114
180
  const cached = await fsUtils.readFile(pathUtils.join(CACHE_DIR, cacheKey));
115
- ir = JSON.parse(cached);
181
+ const cachedValidation = validateIrVersion(
182
+ JSON.parse(cached),
183
+ `Cached Python IR for ${moduleKey}`
184
+ );
185
+ ir = cachedValidation.ir;
116
186
  } catch {
117
187
  ir = null;
118
188
  }
@@ -124,13 +194,60 @@ async function fetchAndCacheIr(
124
194
  });
125
195
  ir = fetchResult.ir;
126
196
  irError = fetchResult.error;
127
- if (ir && caching && fsUtils.isAvailable() && !checkMode) {
128
- try {
129
- await fsUtils.writeFile(pathUtils.join(CACHE_DIR, cacheKey), JSON.stringify(ir));
130
- } catch {}
197
+ }
198
+ if (!ir) {
199
+ return { ir: null, error: irError, code: 'ir-unavailable' };
200
+ }
201
+ const validated = validateIrVersion(ir, `Python IR for ${moduleKey}`);
202
+ if (!validated.ir) {
203
+ return {
204
+ ir: null,
205
+ error: irError ?? validated.error,
206
+ code: validated.code ?? 'ir-unavailable',
207
+ };
208
+ }
209
+ if (caching && fsUtils.isAvailable() && !checkMode) {
210
+ try {
211
+ await fsUtils.writeFile(pathUtils.join(CACHE_DIR, cacheKey), JSON.stringify(validated.ir));
212
+ } catch {}
213
+ }
214
+ return { ir: validated.ir };
215
+ }
216
+
217
+ async function readContractInput(
218
+ moduleKey: string,
219
+ contractInput: TywrapOptions['contractInput']
220
+ ): Promise<{ ir: IrContract | null; error?: string; code?: GenerateFailure['code'] }> {
221
+ const inputPath = typeof contractInput === 'string' ? contractInput : contractInput?.[moduleKey];
222
+ if (!inputPath) {
223
+ return {
224
+ ir: null,
225
+ code: 'contract-invalid',
226
+ error: `No contractInput path configured for module ${moduleKey}.`,
227
+ };
228
+ }
229
+ try {
230
+ const parsed = JSON.parse(await fsUtils.readFile(inputPath)) as unknown;
231
+ const validated = validateIrVersion(parsed, `Contract ${inputPath}`);
232
+ if (!validated.ir) {
233
+ return { ir: null, error: validated.error, code: validated.code };
131
234
  }
235
+ if (validated.ir.module !== moduleKey) {
236
+ return {
237
+ ir: null,
238
+ code: 'contract-invalid',
239
+ error: `Contract ${inputPath} is for module ${validated.ir.module}, not ${moduleKey}.`,
240
+ };
241
+ }
242
+ return { ir: validated.ir };
243
+ } catch (error: unknown) {
244
+ const message = error instanceof Error ? error.message : String(error);
245
+ return {
246
+ ir: null,
247
+ code: 'contract-invalid',
248
+ error: `Failed to read contract ${inputPath}: ${message}`,
249
+ };
132
250
  }
133
- return { ir, error: irError };
134
251
  }
135
252
 
136
253
  /**
@@ -320,25 +437,39 @@ export async function generate(
320
437
  unknownTypeNamesCollector = new Map();
321
438
  // Resolve module IR via the Python extractor (with optional cache)
322
439
  const cacheKey = await computeCacheKey(moduleKey, resolvedOptions);
323
- const { ir, error: irError } = await fetchAndCacheIr(
324
- moduleKey,
325
- resolvedOptions,
326
- pythonPath,
327
- cacheKey,
328
- caching,
329
- checkMode
330
- );
440
+ const irResult = resolvedOptions.contractInput
441
+ ? await readContractInput(moduleKey, resolvedOptions.contractInput)
442
+ : await fetchAndCacheIr(moduleKey, resolvedOptions, pythonPath, cacheKey, caching, checkMode);
443
+ const { ir, error: irError } = irResult;
331
444
  if (!ir) {
332
445
  const message = `No IR produced for module ${moduleKey}${irError ? `: ${irError}` : ''}`;
333
446
  warnings.push(message);
334
447
  failures.push({
335
448
  module: moduleKey,
336
- code: 'ir-unavailable',
449
+ code: irResult.code ?? 'ir-unavailable',
337
450
  message,
338
451
  });
339
452
  continue;
340
453
  }
341
454
 
455
+ const irWarnings = Array.isArray((ir as Record<string, unknown>).warnings)
456
+ ? ((ir as Record<string, unknown>).warnings as unknown[])
457
+ : [];
458
+ for (const warning of irWarnings) {
459
+ if (typeof warning !== 'string') {
460
+ continue;
461
+ }
462
+ // Only type-honesty degrades feed --fail-on-warn. Environment capability
463
+ // notices (e.g. typing.get_overloads missing on Python < 3.11) describe
464
+ // the analyzing interpreter, not the generated types, and must not fail
465
+ // an otherwise clean build.
466
+ if (warning.startsWith('Return annotation for ')) {
467
+ recordUnknown(`Python IR warning: ${warning}`);
468
+ } else {
469
+ logger.info(`Python IR notice: ${warning}`, { component: 'Generate' });
470
+ }
471
+ }
472
+
342
473
  const moduleModel = transformIrToTsModel(ir);
343
474
 
344
475
  // Apply module-level export filtering (functions/classes + excludes).
@@ -351,6 +482,10 @@ export async function generate(
351
482
  const baseName = moduleModel.name || 'module';
352
483
  const filesToEmit: Array<{ path: string; content: string }> = [
353
484
  { path: pathUtils.join(outputDir, `${baseName}.generated.ts`), content: gen.typescript },
485
+ {
486
+ path: pathUtils.join(outputDir, `${baseName}.contract.json`),
487
+ content: serializeContract(ir),
488
+ },
354
489
  ];
355
490
 
356
491
  // Optional .d.ts emission (header-only declarations mirroring exports)
@@ -441,7 +576,7 @@ async function fetchPythonIr(
441
576
  try {
442
577
  const primary = await execAndParseIr(
443
578
  pythonPath,
444
- ['-m', 'tywrap_ir', '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
579
+ ['-m', 'tywrap_ir', '--module', moduleName, '--no-pretty'],
445
580
  execOptions,
446
581
  'tywrap_ir output'
447
582
  );
@@ -476,7 +611,7 @@ async function fetchPythonIr(
476
611
 
477
612
  const fallback = await execAndParseIr(
478
613
  pythonPath,
479
- [localMain, '--module', moduleName, '--ir-version', TYWRAP_IR_VERSION, '--no-pretty'],
614
+ [localMain, '--module', moduleName, '--no-pretty'],
480
615
  execOptions,
481
616
  'tywrap_ir fallback output'
482
617
  );
@@ -12,7 +12,7 @@ import { getComponentLogger } from './logger.js';
12
12
 
13
13
  const log = getComponentLogger('Cache');
14
14
 
15
- export interface CacheEntry<T = unknown> {
15
+ interface CacheEntry<T = unknown> {
16
16
  key: string;
17
17
  data: T;
18
18
  timestamp: number;
@@ -27,7 +27,7 @@ export interface CacheEntry<T = unknown> {
27
27
  };
28
28
  }
29
29
 
30
- export interface CacheStats {
30
+ interface CacheStats {
31
31
  totalEntries: number;
32
32
  totalSize: number;
33
33
  hitRate: number;
@@ -36,7 +36,7 @@ export interface CacheStats {
36
36
  memoryUsage: number;
37
37
  }
38
38
 
39
- export interface CacheConfig {
39
+ interface CacheConfig {
40
40
  baseDir: string;
41
41
  maxSize: number; // Maximum cache size in bytes
42
42
  maxAge: number; // Maximum age in milliseconds
@@ -9,6 +9,8 @@
9
9
  * Sklearn estimators: { "__tywrap__": "sklearn.estimator", "encoding": "json", ... }
10
10
  */
11
11
 
12
+ import { tagDecodedShape } from '../runtime/validators.js';
13
+
12
14
  // Avoid hard dependency on apache-arrow types at compile time to keep install optional.
13
15
  export type ArrowTable = { readonly numCols?: number; readonly numRows?: number } & Record<
14
16
  string,
@@ -68,6 +70,7 @@ export type ValueEnvelope =
68
70
  readonly b64?: string; // when encoding=arrow
69
71
  readonly data?: unknown; // when encoding=json
70
72
  readonly shape?: readonly number[];
73
+ readonly dtype?: string | null;
71
74
  }
72
75
  | {
73
76
  readonly __tywrap__: 'scipy.sparse';
@@ -442,13 +445,16 @@ function decodeArrowOrJsonEnvelope<T>(
442
445
  throw new Error(`Invalid ${typeTag} envelope: missing b64`);
443
446
  }
444
447
  const bytes = fromBase64(b64);
445
- return decodeArrow(bytes);
448
+ const decoded = decodeArrow(bytes);
449
+ return isPromiseLike(decoded)
450
+ ? decoded.then(item => tagDecodedShape(item, { marker: typeTag as 'dataframe' | 'series' }))
451
+ : tagDecodedShape(decoded, { marker: typeTag as 'dataframe' | 'series' });
446
452
  }
447
453
  if (encoding === 'json') {
448
454
  if (!('data' in value)) {
449
455
  throw new Error(`Invalid ${typeTag} envelope: missing data`);
450
456
  }
451
- return value.data;
457
+ return tagDecodedShape(value.data, { marker: typeTag as 'dataframe' | 'series' });
452
458
  }
453
459
  throw new Error(`Invalid ${typeTag} envelope: unsupported encoding ${String(encoding)}`);
454
460
  }
@@ -463,6 +469,8 @@ const decodeNdarrayEnvelope: EnvelopeHandler = (value, decodeArrow) => {
463
469
  const encoding = value.encoding;
464
470
  const shapeValue = value.shape;
465
471
  const shape = isNumberArray(shapeValue) ? shapeValue : undefined;
472
+ const dtype = typeof value.dtype === 'string' ? value.dtype : undefined;
473
+ const metadata = { marker: 'ndarray' as const, dims: shape?.length, dtype };
466
474
 
467
475
  if (encoding === 'arrow') {
468
476
  const b64 = value.b64;
@@ -480,30 +488,30 @@ const decodeNdarrayEnvelope: EnvelopeHandler = (value, decodeArrow) => {
480
488
  return decoded.then(data => {
481
489
  const values = extractArrowValues(data);
482
490
  if (!values) {
483
- return data; // Fallback: return raw data if extraction fails
491
+ return tagDecodedShape(data, metadata); // Fallback: keep provenance on raw data.
484
492
  }
485
493
  // Reshape scalars and multi-dimensional arrays, but not 1D
486
- if (shape && shape.length !== 1) {
487
- return reshapeArray(values, shape);
488
- }
489
- return values;
494
+ return tagDecodedShape(
495
+ shape && shape.length !== 1 ? reshapeArray(values, shape) : values,
496
+ metadata
497
+ );
490
498
  });
491
499
  }
492
500
  const values = extractArrowValues(decoded);
493
501
  if (!values) {
494
- return decoded; // Fallback: return raw data if extraction fails
502
+ return tagDecodedShape(decoded, metadata); // Fallback: keep provenance on raw data.
495
503
  }
496
504
  // Reshape scalars and multi-dimensional arrays, but not 1D
497
- if (shape && shape.length !== 1) {
498
- return reshapeArray(values, shape);
499
- }
500
- return values;
505
+ return tagDecodedShape(
506
+ shape && shape.length !== 1 ? reshapeArray(values, shape) : values,
507
+ metadata
508
+ );
501
509
  }
502
510
  if (encoding === 'json') {
503
511
  if (!('data' in value)) {
504
512
  throw new Error('Invalid ndarray envelope: missing data');
505
513
  }
506
- return value.data;
514
+ return tagDecodedShape(value.data, metadata);
507
515
  }
508
516
  throw new Error(`Invalid ndarray envelope: unsupported encoding ${String(encoding)}`);
509
517
  };
@@ -1,6 +1,6 @@
1
1
  import { hashUtils } from './runtime.js';
2
2
 
3
- export interface IrCacheKeyObject {
3
+ interface IrCacheKeyObject {
4
4
  module: string;
5
5
  moduleVersion: string | null;
6
6
  irVersion: string;
@@ -23,13 +23,16 @@ interface RuntimeCapabilities {
23
23
 
24
24
  // Cache for runtime detection to avoid repeated environment checks
25
25
  let runtimeCache: RuntimeInfo | null = null;
26
-
27
- /**
28
- * Clear runtime cache (for testing purposes only)
29
- * @internal
30
- */
31
- export function clearRuntimeCache(): void {
32
- runtimeCache = null;
26
+ let runtimeCacheSignature: string | undefined;
27
+
28
+ function currentRuntimeSignature(): string {
29
+ return [
30
+ typeof Deno !== 'undefined' && Deno !== null,
31
+ typeof Bun !== 'undefined' && Bun !== null,
32
+ typeof process !== 'undefined' && process.versions?.node,
33
+ typeof window !== 'undefined' ? window.isSecureContext : undefined,
34
+ typeof self !== 'undefined' ? self.isSecureContext : undefined,
35
+ ].join('|');
33
36
  }
34
37
 
35
38
  /**
@@ -37,7 +40,8 @@ export function clearRuntimeCache(): void {
37
40
  * Results are cached and frozen to prevent external mutation
38
41
  */
39
42
  export function detectRuntime(): RuntimeInfo {
40
- if (runtimeCache) {
43
+ const signature = currentRuntimeSignature();
44
+ if (runtimeCache && runtimeCacheSignature === signature) {
41
45
  return runtimeCache;
42
46
  }
43
47
  // Deno detection (must come before Node.js check)
@@ -56,6 +60,7 @@ export function detectRuntime(): RuntimeInfo {
56
60
  capabilities: Object.freeze(capabilities),
57
61
  };
58
62
  runtimeCache = Object.freeze(result) as RuntimeInfo;
63
+ runtimeCacheSignature = signature;
59
64
  return runtimeCache;
60
65
  }
61
66
 
@@ -75,6 +80,7 @@ export function detectRuntime(): RuntimeInfo {
75
80
  capabilities: Object.freeze(capabilities),
76
81
  };
77
82
  runtimeCache = Object.freeze(result) as RuntimeInfo;
83
+ runtimeCacheSignature = signature;
78
84
  return runtimeCache;
79
85
  }
80
86
 
@@ -94,6 +100,7 @@ export function detectRuntime(): RuntimeInfo {
94
100
  capabilities: Object.freeze(capabilities),
95
101
  };
96
102
  runtimeCache = Object.freeze(result) as RuntimeInfo;
103
+ runtimeCacheSignature = signature;
97
104
  return runtimeCache;
98
105
  }
99
106
 
@@ -119,6 +126,7 @@ export function detectRuntime(): RuntimeInfo {
119
126
  capabilities: Object.freeze(capabilities),
120
127
  };
121
128
  runtimeCache = Object.freeze(result) as RuntimeInfo;
129
+ runtimeCacheSignature = signature;
122
130
  return runtimeCache;
123
131
  }
124
132
 
@@ -137,6 +145,7 @@ export function detectRuntime(): RuntimeInfo {
137
145
 
138
146
  // Cache and freeze the result to prevent external mutation
139
147
  runtimeCache = Object.freeze(result) as RuntimeInfo;
148
+ runtimeCacheSignature = signature;
140
149
  return runtimeCache;
141
150
  }
142
151
 
@@ -183,52 +192,6 @@ export function hasCapability(capability: keyof RuntimeCapabilities): boolean {
183
192
  return getRuntimeCapabilities()[capability];
184
193
  }
185
194
 
186
- /**
187
- * Get the best runtime strategy for Python execution
188
- */
189
- export function getBestPythonRuntime(): 'node' | 'pyodide' | 'http' {
190
- const runtime = detectRuntime();
191
-
192
- if (runtime.name === 'browser') {
193
- return 'pyodide';
194
- }
195
-
196
- if (runtime.capabilities.subprocess) {
197
- return 'node'; // Works for Node.js, Deno, and Bun
198
- }
199
-
200
- // Fallback to HTTP bridge
201
- return 'http';
202
- }
203
-
204
- type PathModule = typeof import('node:path');
205
-
206
- // Cache for lazy-loaded path module
207
- let pathModule: PathModule | null = null;
208
-
209
- /**
210
- * Lazy load Node.js path module on demand
211
- */
212
- async function loadPathModule(): Promise<PathModule | null> {
213
- if (pathModule) {
214
- return pathModule;
215
- }
216
-
217
- const runtime = detectRuntime();
218
- if (runtime.name === 'node') {
219
- try {
220
- pathModule = await import('node:path');
221
- return pathModule;
222
- } catch {
223
- // Fallback for older Node.js versions
224
- pathModule = await import('path');
225
- return pathModule;
226
- }
227
- }
228
-
229
- return null;
230
- }
231
-
232
195
  /**
233
196
  * Apply a single path segment to the accumulated normalized segments,
234
197
  * resolving '..' and dropping '.'/empty segments in place.
@@ -283,30 +246,6 @@ export const pathUtils = {
283
246
  return normalizePath(joined);
284
247
  },
285
248
 
286
- /**
287
- * Join paths asynchronously with enhanced Node.js support
288
- */
289
- async joinAsync(...segments: string[]): Promise<string> {
290
- const runtime = detectRuntime();
291
-
292
- // For Node.js, use the real path module when available
293
- if (runtime.name === 'node') {
294
- const pathMod = await loadPathModule();
295
- if (pathMod?.posix) {
296
- return pathMod.posix.join(...segments);
297
- }
298
- }
299
-
300
- // Fallback implementation with normalization
301
- const joined = segments
302
- .filter(Boolean)
303
- .join('/')
304
- .replace(/\/+/g, '/') // Replace multiple slashes with single slash
305
- .replace(/\\/g, '/'); // Normalize backslashes to forward slashes
306
-
307
- return normalizePath(joined);
308
- },
309
-
310
249
  /**
311
250
  * Resolve absolute path in a cross-runtime way (synchronous)
312
251
  */
@@ -329,27 +268,6 @@ export const pathUtils = {
329
268
  // Fallback: normalize and return as-is for relative paths
330
269
  return normalizePath(path);
331
270
  },
332
-
333
- /**
334
- * Resolve absolute path in a cross-runtime way (asynchronous)
335
- */
336
- async resolveAsync(path: string): Promise<string> {
337
- const runtime = detectRuntime();
338
-
339
- if (runtime.name === 'node') {
340
- const pathMod = await loadPathModule();
341
- if (pathMod) {
342
- return pathMod.resolve(path);
343
- }
344
- }
345
-
346
- if (runtime.name === 'browser') {
347
- return new URL(path, location.href).href;
348
- }
349
-
350
- // Fallback: normalize and return as-is
351
- return normalizePath(path);
352
- },
353
271
  };
354
272
 
355
273
  /**
@@ -642,28 +560,6 @@ export function isWindows(): boolean {
642
560
  return deno?.build?.os === 'windows';
643
561
  }
644
562
 
645
- /**
646
- * Check if running on macOS
647
- */
648
- export function isMacOS(): boolean {
649
- if (typeof process !== 'undefined' && process.platform) {
650
- return process.platform === 'darwin';
651
- }
652
- const deno = (globalThis as unknown as { Deno?: { build?: { os?: string } } }).Deno;
653
- return deno?.build?.os === 'darwin';
654
- }
655
-
656
- /**
657
- * Check if running on Linux
658
- */
659
- export function isLinux(): boolean {
660
- if (typeof process !== 'undefined' && process.platform) {
661
- return process.platform === 'linux';
662
- }
663
- const deno = (globalThis as unknown as { Deno?: { build?: { os?: string } } }).Deno;
664
- return deno?.build?.os === 'linux';
665
- }
666
-
667
563
  /**
668
564
  * Check if a path is absolute
669
565
  */
@@ -676,13 +572,6 @@ export function isAbsolutePath(path: string): boolean {
676
572
  return /^[A-Za-z]:[\\/]/.test(path);
677
573
  }
678
574
 
679
- /**
680
- * Get the path separator for the current platform
681
- */
682
- export function getPathSeparator(): string {
683
- return isWindows() ? '\\' : '/';
684
- }
685
-
686
575
  /**
687
576
  * Get the default Python executable name for the current platform
688
577
  */
package/src/version.ts CHANGED
@@ -9,4 +9,4 @@
9
9
  * Regenerate with: node scripts/generate-version.mjs
10
10
  */
11
11
 
12
- export const VERSION: string = "0.8.0";
12
+ export const VERSION: string = "0.9.0";