tywrap 0.8.0 → 0.10.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 (159) hide show
  1. package/README.md +42 -22
  2. package/SECURITY.md +38 -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 +254 -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 +3 -3
  31. package/dist/runtime/bridge-codec.d.ts.map +1 -1
  32. package/dist/runtime/bridge-codec.js +129 -62
  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 +50 -0
  76. package/dist/runtime/validators.d.ts.map +1 -1
  77. package/dist/runtime/validators.js +154 -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 +17 -0
  88. package/dist/utils/codec.d.ts.map +1 -1
  89. package/dist/utils/codec.js +500 -85
  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.d.ts.map +1 -1
  98. package/dist/version.js +1 -1
  99. package/dist/version.js.map +1 -1
  100. package/package.json +7 -6
  101. package/runtime/frame_codec.py +7 -1
  102. package/runtime/python_bridge.py +85 -125
  103. package/runtime/tywrap_bridge_core.py +515 -187
  104. package/src/config/index.ts +11 -0
  105. package/src/core/annotation-parser.ts +7 -4
  106. package/src/core/generator.ts +330 -205
  107. package/src/core/mapper.ts +8 -8
  108. package/src/index.ts +7 -4
  109. package/src/runtime/base-bridge.ts +5 -30
  110. package/src/runtime/bounded-context.ts +12 -67
  111. package/src/runtime/bridge-codec.ts +158 -74
  112. package/src/runtime/errors.ts +21 -0
  113. package/src/runtime/http-transport.ts +6 -1
  114. package/src/runtime/index.ts +6 -0
  115. package/src/runtime/node.ts +9 -120
  116. package/src/runtime/pooled-transport.ts +424 -90
  117. package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
  118. package/src/runtime/pyodide-transport.ts +7 -3
  119. package/src/runtime/rpc-client.ts +37 -139
  120. package/src/runtime/subprocess-transport.ts +210 -285
  121. package/src/runtime/timed-out-request-tracker.ts +1 -1
  122. package/src/runtime/transport.ts +15 -23
  123. package/src/runtime/validators.ts +212 -0
  124. package/src/types/index.ts +21 -60
  125. package/src/tywrap.ts +157 -22
  126. package/src/utils/cache.ts +3 -3
  127. package/src/utils/codec.ts +749 -112
  128. package/src/utils/ir-cache.ts +1 -1
  129. package/src/utils/runtime.ts +17 -128
  130. package/src/version.ts +1 -1
  131. package/dist/core/discovery.d.ts +0 -103
  132. package/dist/core/discovery.d.ts.map +0 -1
  133. package/dist/core/discovery.js +0 -380
  134. package/dist/core/discovery.js.map +0 -1
  135. package/dist/core/validation.d.ts +0 -102
  136. package/dist/core/validation.d.ts.map +0 -1
  137. package/dist/core/validation.js +0 -490
  138. package/dist/core/validation.js.map +0 -1
  139. package/dist/runtime/base.d.ts +0 -22
  140. package/dist/runtime/base.d.ts.map +0 -1
  141. package/dist/runtime/base.js +0 -23
  142. package/dist/runtime/base.js.map +0 -1
  143. package/dist/runtime/transport-pool.d.ts +0 -196
  144. package/dist/runtime/transport-pool.d.ts.map +0 -1
  145. package/dist/runtime/transport-pool.js +0 -418
  146. package/dist/runtime/transport-pool.js.map +0 -1
  147. package/runtime/__pycache__/_tywrap_conformance_chunking_fixtures.cpython-311.pyc +0 -0
  148. package/runtime/__pycache__/_tywrap_member_fixtures.cpython-311.pyc +0 -0
  149. package/runtime/__pycache__/_tywrap_w4_chunking_fixture.cpython-311.pyc +0 -0
  150. package/runtime/__pycache__/_tywrap_w5_request_chunking_fixture.cpython-311.pyc +0 -0
  151. package/runtime/__pycache__/_tywrap_w6_pool_chunking_fixture.cpython-311.pyc +0 -0
  152. package/runtime/__pycache__/frame_codec.cpython-311.pyc +0 -0
  153. package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
  154. package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
  155. package/runtime/safe_codec.py +0 -352
  156. package/src/core/discovery.ts +0 -477
  157. package/src/core/validation.ts +0 -729
  158. package/src/runtime/base.ts +0 -24
  159. 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