tywrap 0.2.0 → 0.3.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 (163) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +23 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +23 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/base.d.ts +17 -7
  33. package/dist/runtime/base.d.ts.map +1 -1
  34. package/dist/runtime/base.js +18 -1
  35. package/dist/runtime/base.js.map +1 -1
  36. package/dist/runtime/bounded-context.d.ts +252 -0
  37. package/dist/runtime/bounded-context.d.ts.map +1 -0
  38. package/dist/runtime/bounded-context.js +454 -0
  39. package/dist/runtime/bounded-context.js.map +1 -0
  40. package/dist/runtime/bridge-core.d.ts +1 -0
  41. package/dist/runtime/bridge-core.d.ts.map +1 -1
  42. package/dist/runtime/bridge-core.js +52 -17
  43. package/dist/runtime/bridge-core.js.map +1 -1
  44. package/dist/runtime/bridge-protocol.d.ts +184 -0
  45. package/dist/runtime/bridge-protocol.d.ts.map +1 -0
  46. package/dist/runtime/bridge-protocol.js +344 -0
  47. package/dist/runtime/bridge-protocol.js.map +1 -0
  48. package/dist/runtime/disposable.d.ts +40 -0
  49. package/dist/runtime/disposable.d.ts.map +1 -0
  50. package/dist/runtime/disposable.js +49 -0
  51. package/dist/runtime/disposable.js.map +1 -0
  52. package/dist/runtime/errors.d.ts +10 -0
  53. package/dist/runtime/errors.d.ts.map +1 -1
  54. package/dist/runtime/errors.js +9 -0
  55. package/dist/runtime/errors.js.map +1 -1
  56. package/dist/runtime/http-io.d.ts +91 -0
  57. package/dist/runtime/http-io.d.ts.map +1 -0
  58. package/dist/runtime/http-io.js +195 -0
  59. package/dist/runtime/http-io.js.map +1 -0
  60. package/dist/runtime/http.d.ts +48 -13
  61. package/dist/runtime/http.d.ts.map +1 -1
  62. package/dist/runtime/http.js +60 -73
  63. package/dist/runtime/http.js.map +1 -1
  64. package/dist/runtime/node.d.ts +97 -130
  65. package/dist/runtime/node.d.ts.map +1 -1
  66. package/dist/runtime/node.js +392 -521
  67. package/dist/runtime/node.js.map +1 -1
  68. package/dist/runtime/optimized-node.d.ts +1 -1
  69. package/dist/runtime/optimized-node.d.ts.map +1 -1
  70. package/dist/runtime/optimized-node.js +1 -1
  71. package/dist/runtime/optimized-node.js.map +1 -1
  72. package/dist/runtime/pooled-transport.d.ts +135 -0
  73. package/dist/runtime/pooled-transport.d.ts.map +1 -0
  74. package/dist/runtime/pooled-transport.js +177 -0
  75. package/dist/runtime/pooled-transport.js.map +1 -0
  76. package/dist/runtime/process-io.d.ts +209 -0
  77. package/dist/runtime/process-io.d.ts.map +1 -0
  78. package/dist/runtime/process-io.js +729 -0
  79. package/dist/runtime/process-io.js.map +1 -0
  80. package/dist/runtime/pyodide-io.d.ts +154 -0
  81. package/dist/runtime/pyodide-io.d.ts.map +1 -0
  82. package/dist/runtime/pyodide-io.js +459 -0
  83. package/dist/runtime/pyodide-io.js.map +1 -0
  84. package/dist/runtime/pyodide.d.ts +51 -19
  85. package/dist/runtime/pyodide.d.ts.map +1 -1
  86. package/dist/runtime/pyodide.js +57 -186
  87. package/dist/runtime/pyodide.js.map +1 -1
  88. package/dist/runtime/safe-codec.d.ts +103 -0
  89. package/dist/runtime/safe-codec.d.ts.map +1 -0
  90. package/dist/runtime/safe-codec.js +519 -0
  91. package/dist/runtime/safe-codec.js.map +1 -0
  92. package/dist/runtime/transport.d.ts +191 -0
  93. package/dist/runtime/transport.d.ts.map +1 -0
  94. package/dist/runtime/transport.js +86 -0
  95. package/dist/runtime/transport.js.map +1 -0
  96. package/dist/runtime/validators.d.ts +120 -0
  97. package/dist/runtime/validators.d.ts.map +1 -0
  98. package/dist/runtime/validators.js +229 -0
  99. package/dist/runtime/validators.js.map +1 -0
  100. package/dist/runtime/worker-pool.d.ts +208 -0
  101. package/dist/runtime/worker-pool.d.ts.map +1 -0
  102. package/dist/runtime/worker-pool.js +419 -0
  103. package/dist/runtime/worker-pool.js.map +1 -0
  104. package/dist/types/index.d.ts +51 -1
  105. package/dist/types/index.d.ts.map +1 -1
  106. package/dist/tywrap.d.ts.map +1 -1
  107. package/dist/tywrap.js +227 -328
  108. package/dist/tywrap.js.map +1 -1
  109. package/dist/utils/cache.d.ts.map +1 -1
  110. package/dist/utils/cache.js +53 -5
  111. package/dist/utils/cache.js.map +1 -1
  112. package/dist/utils/codec.d.ts.map +1 -1
  113. package/dist/utils/codec.js +114 -1
  114. package/dist/utils/codec.js.map +1 -1
  115. package/dist/utils/ir-cache.d.ts +28 -0
  116. package/dist/utils/ir-cache.d.ts.map +1 -0
  117. package/dist/utils/ir-cache.js +29 -0
  118. package/dist/utils/ir-cache.js.map +1 -0
  119. package/dist/utils/parallel-processor.d.ts +1 -1
  120. package/dist/utils/parallel-processor.d.ts.map +1 -1
  121. package/dist/utils/parallel-processor.js +111 -40
  122. package/dist/utils/parallel-processor.js.map +1 -1
  123. package/dist/utils/runtime.d.ts +5 -1
  124. package/dist/utils/runtime.d.ts.map +1 -1
  125. package/dist/utils/runtime.js +112 -18
  126. package/dist/utils/runtime.js.map +1 -1
  127. package/package.json +40 -18
  128. package/runtime/python_bridge.py +108 -15
  129. package/runtime/safe_codec.py +344 -0
  130. package/src/cli.ts +61 -2
  131. package/src/config/index.ts +63 -17
  132. package/src/core/analyzer.ts +52 -15
  133. package/src/core/annotation-parser.ts +500 -0
  134. package/src/core/discovery.ts +26 -12
  135. package/src/core/generator.ts +956 -116
  136. package/src/core/mapper.ts +158 -51
  137. package/src/index.ts +78 -6
  138. package/src/runtime/base.ts +18 -26
  139. package/src/runtime/bounded-context.ts +608 -0
  140. package/src/runtime/bridge-core.ts +58 -18
  141. package/src/runtime/bridge-protocol.ts +483 -0
  142. package/src/runtime/disposable.ts +65 -0
  143. package/src/runtime/errors.ts +14 -0
  144. package/src/runtime/http-io.ts +245 -0
  145. package/src/runtime/http.ts +76 -115
  146. package/src/runtime/node.ts +563 -676
  147. package/src/runtime/optimized-node.ts +4 -1
  148. package/src/runtime/pooled-transport.ts +263 -0
  149. package/src/runtime/process-io.ts +930 -0
  150. package/src/runtime/pyodide-io.ts +551 -0
  151. package/src/runtime/pyodide.ts +75 -215
  152. package/src/runtime/safe-codec.ts +651 -0
  153. package/src/runtime/transport.ts +278 -0
  154. package/src/runtime/validators.ts +252 -0
  155. package/src/runtime/worker-pool.ts +559 -0
  156. package/src/types/global.d.ts +11 -1
  157. package/src/types/index.ts +63 -0
  158. package/src/tywrap.ts +279 -358
  159. package/src/utils/cache.ts +59 -5
  160. package/src/utils/codec.ts +122 -1
  161. package/src/utils/ir-cache.ts +51 -0
  162. package/src/utils/parallel-processor.ts +119 -43
  163. package/src/utils/runtime.ts +129 -18
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Transport layer for BridgeProtocol.
3
+ *
4
+ * Provides an abstract I/O channel for all bridge communications across
5
+ * the JS-Python boundary. Concrete implementations handle different runtimes:
6
+ * - ProcessIO: Child process with stdio streams (Node.js)
7
+ * - HttpIO: HTTP POST requests (remote Python server)
8
+ * - PyodideIO: In-memory Pyodide calls (browser/WASM)
9
+ *
10
+ * @see https://github.com/bbopen/tywrap/issues/149
11
+ */
12
+
13
+ import type { Disposable } from './disposable.js';
14
+
15
+ // =============================================================================
16
+ // PROTOCOL CONSTANTS
17
+ // =============================================================================
18
+
19
+ /** Protocol identifier for tywrap communication */
20
+ export const PROTOCOL_ID = 'tywrap/1';
21
+
22
+ // =============================================================================
23
+ // PROTOCOL TYPES
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Protocol message format for all transports.
28
+ *
29
+ * Each method corresponds to a BridgeProtocol operation:
30
+ * - `call`: Invoke a module-level function
31
+ * - `instantiate`: Create a new class instance
32
+ * - `call_method`: Invoke a method on an existing instance
33
+ * - `dispose_instance`: Release an instance handle
34
+ * - `meta`: Get bridge metadata
35
+ */
36
+ export interface ProtocolMessage {
37
+ /** Unique message identifier for request-response correlation */
38
+ id: number;
39
+
40
+ /** Protocol identifier (must be 'tywrap/1') */
41
+ protocol: typeof PROTOCOL_ID;
42
+
43
+ /** The method to invoke */
44
+ method: 'call' | 'instantiate' | 'call_method' | 'dispose_instance' | 'meta';
45
+
46
+ /** Method parameters */
47
+ params: {
48
+ /** Python module path (for call and instantiate) */
49
+ module?: string;
50
+
51
+ /** Function name (for call) */
52
+ functionName?: string;
53
+
54
+ /** Class name (for instantiate) */
55
+ className?: string;
56
+
57
+ /** Instance handle (for call_method and dispose_instance) */
58
+ handle?: string;
59
+
60
+ /** Method name (for call_method) */
61
+ methodName?: string;
62
+
63
+ /** Positional arguments */
64
+ args?: unknown[];
65
+
66
+ /** Keyword arguments */
67
+ kwargs?: Record<string, unknown>;
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Protocol response format from the Python side.
73
+ *
74
+ * A response contains either a result or an error, never both.
75
+ * The `id` field correlates the response to its originating request.
76
+ */
77
+ export interface ProtocolResponse {
78
+ /** Message identifier matching the originating request */
79
+ id: number;
80
+
81
+ /** Protocol identifier (echoed back from request) */
82
+ protocol?: string;
83
+
84
+ /** Successful result value (undefined if error occurred) */
85
+ result?: unknown;
86
+
87
+ /** Error information (undefined if operation succeeded) */
88
+ error?: {
89
+ /** Python exception type name */
90
+ type: string;
91
+ /** Error message */
92
+ message: string;
93
+ /** Optional Python traceback for debugging */
94
+ traceback?: string;
95
+ };
96
+ }
97
+
98
+ // =============================================================================
99
+ // TRANSPORT INTERFACE
100
+ // =============================================================================
101
+
102
+ /**
103
+ * Abstract transport for sending messages across the JS-Python boundary.
104
+ *
105
+ * Transport implementations handle the low-level I/O details while
106
+ * providing a consistent interface for the higher-level BridgeProtocol.
107
+ *
108
+ * Lifecycle:
109
+ * 1. Create transport instance
110
+ * 2. Call `init()` to establish the connection
111
+ * 3. Use `send()` to exchange messages
112
+ * 4. Call `dispose()` to release resources
113
+ *
114
+ * Implementations:
115
+ * - ProcessIO: Spawns a Python child process, communicates via stdio
116
+ * - HttpIO: Sends HTTP POST requests to a Python server
117
+ * - PyodideIO: Calls Pyodide directly in-memory (WASM)
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * const transport = new ProcessIO({ pythonPath: 'python3' });
122
+ * await transport.init();
123
+ *
124
+ * const response = await transport.send(
125
+ * JSON.stringify({
126
+ * id: 1,
127
+ * protocol: 'tywrap/1',
128
+ * method: 'call',
129
+ * params: { module: 'math', functionName: 'sqrt', args: [16], kwargs: {} },
130
+ * }),
131
+ * 5000
132
+ * );
133
+ *
134
+ * await transport.dispose();
135
+ * ```
136
+ */
137
+ export interface Transport extends Disposable {
138
+ /**
139
+ * Initialize the transport.
140
+ *
141
+ * This method establishes the underlying connection (e.g., spawns a process,
142
+ * opens a connection). It must be called before `send()` can be used.
143
+ *
144
+ * This method should be idempotent - calling it multiple times after
145
+ * successful initialization should be a no-op.
146
+ *
147
+ * @throws BridgeError if initialization fails
148
+ */
149
+ init(): Promise<void>;
150
+
151
+ /**
152
+ * Send a message and wait for the response.
153
+ *
154
+ * @param message - The JSON-encoded protocol message to send
155
+ * @param timeoutMs - Timeout in milliseconds (0 = no timeout)
156
+ * @param signal - Optional AbortSignal for external cancellation
157
+ * @returns The raw response string (JSON-encoded ProtocolResponse)
158
+ *
159
+ * @throws BridgeTimeoutError if the operation times out or is aborted
160
+ * @throws BridgeDisposedError if the transport has been disposed
161
+ * @throws BridgeProtocolError if the message format is invalid
162
+ * @throws BridgeError for other transport-level failures
163
+ */
164
+ send(message: string, timeoutMs: number, signal?: AbortSignal): Promise<string>;
165
+
166
+ /**
167
+ * Whether the transport is ready to send messages.
168
+ *
169
+ * Returns `true` after successful `init()` and before `dispose()`.
170
+ * Returns `false` in all other states.
171
+ */
172
+ readonly isReady: boolean;
173
+ }
174
+
175
+ // =============================================================================
176
+ // TRANSPORT OPTIONS
177
+ // =============================================================================
178
+
179
+ /**
180
+ * Base options for creating a Transport.
181
+ *
182
+ * Concrete transport implementations may extend this with
183
+ * additional configuration options.
184
+ */
185
+ export interface TransportOptions {
186
+ /**
187
+ * Default timeout for operations in milliseconds.
188
+ *
189
+ * This timeout applies to individual `send()` calls when
190
+ * no explicit timeout is provided.
191
+ *
192
+ * @default 30000 (30 seconds)
193
+ */
194
+ defaultTimeoutMs?: number;
195
+ }
196
+
197
+ // =============================================================================
198
+ // TYPE GUARDS
199
+ // =============================================================================
200
+
201
+ /**
202
+ * Type guard to check if a value implements the Transport interface.
203
+ *
204
+ * @param value - The value to check
205
+ * @returns True if the value has all required Transport methods and properties
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * function useTransport(maybeTransport: unknown) {
210
+ * if (isTransport(maybeTransport)) {
211
+ * await maybeTransport.init();
212
+ * // TypeScript now knows maybeTransport is Transport
213
+ * }
214
+ * }
215
+ * ```
216
+ */
217
+ export function isTransport(value: unknown): value is Transport {
218
+ return (
219
+ value !== null &&
220
+ typeof value === 'object' &&
221
+ typeof (value as Transport).init === 'function' &&
222
+ typeof (value as Transport).send === 'function' &&
223
+ typeof (value as Transport).dispose === 'function' &&
224
+ 'isReady' in value
225
+ );
226
+ }
227
+
228
+ /**
229
+ * Type guard to check if a value is a valid ProtocolMessage.
230
+ *
231
+ * @param value - The value to check
232
+ * @returns True if the value conforms to the ProtocolMessage structure
233
+ */
234
+ export function isProtocolMessage(value: unknown): value is ProtocolMessage {
235
+ if (value === null || typeof value !== 'object') {
236
+ return false;
237
+ }
238
+
239
+ const msg = value as ProtocolMessage;
240
+
241
+ return (
242
+ typeof msg.id === 'number' &&
243
+ msg.protocol === PROTOCOL_ID &&
244
+ typeof msg.method === 'string' &&
245
+ ['call', 'instantiate', 'call_method', 'dispose_instance', 'meta'].includes(msg.method) &&
246
+ typeof msg.params === 'object' &&
247
+ msg.params !== null
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Type guard to check if a value is a valid ProtocolResponse.
253
+ *
254
+ * @param value - The value to check
255
+ * @returns True if the value conforms to the ProtocolResponse structure
256
+ */
257
+ export function isProtocolResponse(value: unknown): value is ProtocolResponse {
258
+ if (value === null || typeof value !== 'object') {
259
+ return false;
260
+ }
261
+
262
+ const resp = value as ProtocolResponse;
263
+
264
+ if (typeof resp.id !== 'number') {
265
+ return false;
266
+ }
267
+
268
+ // Must have either result or error (or neither for void returns)
269
+ if (resp.error !== undefined) {
270
+ if (typeof resp.error !== 'object' || resp.error === null) {
271
+ return false;
272
+ }
273
+ const err = resp.error;
274
+ return typeof err.type === 'string' && typeof err.message === 'string';
275
+ }
276
+
277
+ return true;
278
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Pure validation functions for runtime value checking.
3
+ *
4
+ * These functions are used by BoundedContext and can also be used
5
+ * independently for validation in CLI tools, config loading, etc.
6
+ */
7
+
8
+ /**
9
+ * Error thrown when validation fails.
10
+ */
11
+ export class ValidationError extends Error {
12
+ constructor(message: string) {
13
+ super(message);
14
+ this.name = 'ValidationError';
15
+ }
16
+ }
17
+
18
+ // ═══════════════════════════════════════════════════════════════════════════
19
+ // TYPE GUARDS
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+
22
+ /**
23
+ * Check if a value is a finite number (not NaN, not Infinity).
24
+ */
25
+ export function isFiniteNumber(value: unknown): value is number {
26
+ return typeof value === 'number' && Number.isFinite(value);
27
+ }
28
+
29
+ /**
30
+ * Check if a value is a positive finite number.
31
+ */
32
+ export function isPositiveNumber(value: unknown): value is number {
33
+ return isFiniteNumber(value) && value > 0;
34
+ }
35
+
36
+ /**
37
+ * Check if a value is a non-negative finite number.
38
+ */
39
+ export function isNonNegativeNumber(value: unknown): value is number {
40
+ return isFiniteNumber(value) && value >= 0;
41
+ }
42
+
43
+ /**
44
+ * Check if a value is a non-empty string.
45
+ */
46
+ export function isNonEmptyString(value: unknown): value is string {
47
+ return typeof value === 'string' && value.length > 0;
48
+ }
49
+
50
+ /**
51
+ * Check if a value is a plain object (not null, not array).
52
+ */
53
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
55
+ }
56
+
57
+ // ═══════════════════════════════════════════════════════════════════════════
58
+ // ASSERTIONS
59
+ // ═══════════════════════════════════════════════════════════════════════════
60
+
61
+ /**
62
+ * Assert that a value is a finite number.
63
+ *
64
+ * @param value - The value to check
65
+ * @param name - The name of the parameter (for error messages)
66
+ * @returns The value as a number
67
+ * @throws ValidationError if the value is not a finite number
68
+ */
69
+ export function assertFiniteNumber(value: unknown, name: string): number {
70
+ if (!isFiniteNumber(value)) {
71
+ const actual = typeof value === 'number' ? String(value) : typeof value;
72
+ throw new ValidationError(`${name} must be a finite number, got: ${actual}`);
73
+ }
74
+ return value;
75
+ }
76
+
77
+ /**
78
+ * Assert that a value is a positive number (> 0).
79
+ *
80
+ * @param value - The value to check
81
+ * @param name - The name of the parameter (for error messages)
82
+ * @returns The value as a number
83
+ * @throws ValidationError if the value is not a positive number
84
+ */
85
+ export function assertPositive(value: unknown, name: string): number {
86
+ const num = assertFiniteNumber(value, name);
87
+ if (num <= 0) {
88
+ throw new ValidationError(`${name} must be positive, got: ${num}`);
89
+ }
90
+ return num;
91
+ }
92
+
93
+ /**
94
+ * Assert that a value is a non-negative number (>= 0).
95
+ *
96
+ * @param value - The value to check
97
+ * @param name - The name of the parameter (for error messages)
98
+ * @returns The value as a number
99
+ * @throws ValidationError if the value is not a non-negative number
100
+ */
101
+ export function assertNonNegative(value: unknown, name: string): number {
102
+ const num = assertFiniteNumber(value, name);
103
+ if (num < 0) {
104
+ throw new ValidationError(`${name} must be non-negative, got: ${num}`);
105
+ }
106
+ return num;
107
+ }
108
+
109
+ /**
110
+ * Assert that a value is a string.
111
+ *
112
+ * @param value - The value to check
113
+ * @param name - The name of the parameter (for error messages)
114
+ * @returns The value as a string
115
+ * @throws ValidationError if the value is not a string
116
+ */
117
+ export function assertString(value: unknown, name: string): string {
118
+ if (typeof value !== 'string') {
119
+ throw new ValidationError(`${name} must be a string, got: ${typeof value}`);
120
+ }
121
+ return value;
122
+ }
123
+
124
+ /**
125
+ * Assert that a value is a non-empty string.
126
+ *
127
+ * @param value - The value to check
128
+ * @param name - The name of the parameter (for error messages)
129
+ * @returns The value as a string
130
+ * @throws ValidationError if the value is not a non-empty string
131
+ */
132
+ export function assertNonEmptyString(value: unknown, name: string): string {
133
+ const str = assertString(value, name);
134
+ if (str.length === 0) {
135
+ throw new ValidationError(`${name} must not be empty`);
136
+ }
137
+ return str;
138
+ }
139
+
140
+ /**
141
+ * Assert that a value is an array.
142
+ *
143
+ * @param value - The value to check
144
+ * @param name - The name of the parameter (for error messages)
145
+ * @returns The value as an array
146
+ * @throws ValidationError if the value is not an array
147
+ */
148
+ export function assertArray<T = unknown>(value: unknown, name: string): T[] {
149
+ if (!Array.isArray(value)) {
150
+ throw new ValidationError(`${name} must be an array, got: ${typeof value}`);
151
+ }
152
+ return value as T[];
153
+ }
154
+
155
+ /**
156
+ * Assert that a value is a plain object (not null, not array).
157
+ *
158
+ * @param value - The value to check
159
+ * @param name - The name of the parameter (for error messages)
160
+ * @returns The value as a Record
161
+ * @throws ValidationError if the value is not a plain object
162
+ */
163
+ export function assertObject(value: unknown, name: string): Record<string, unknown> {
164
+ if (!isPlainObject(value)) {
165
+ const actual = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
166
+ throw new ValidationError(`${name} must be an object, got: ${actual}`);
167
+ }
168
+ return value;
169
+ }
170
+
171
+ // ═══════════════════════════════════════════════════════════════════════════
172
+ // SPECIAL FLOAT DETECTION (for codec validation)
173
+ // ═══════════════════════════════════════════════════════════════════════════
174
+
175
+ /**
176
+ * Check if a value contains non-finite numbers (NaN, Infinity, -Infinity).
177
+ * Recursively checks arrays and objects.
178
+ *
179
+ * This is used to detect values that cannot be safely serialized to JSON,
180
+ * as JSON.stringify converts NaN/Infinity to null, which can cause
181
+ * silent data corruption.
182
+ *
183
+ * @param value - The value to check
184
+ * @returns True if the value contains NaN or Infinity anywhere
185
+ */
186
+ function containsSpecialFloatRecursive(value: unknown, visited: WeakSet<object>): boolean {
187
+ if (typeof value === 'number') {
188
+ return !Number.isFinite(value);
189
+ }
190
+ if (value === null || typeof value !== 'object') {
191
+ return false;
192
+ }
193
+ if (visited.has(value)) {
194
+ return false;
195
+ }
196
+ visited.add(value);
197
+ if (Array.isArray(value)) {
198
+ return value.some(item => containsSpecialFloatRecursive(item, visited));
199
+ }
200
+ if (isPlainObject(value)) {
201
+ return Object.values(value).some(item => containsSpecialFloatRecursive(item, visited));
202
+ }
203
+ return false;
204
+ }
205
+
206
+ export function containsSpecialFloat(value: unknown): boolean {
207
+ return containsSpecialFloatRecursive(value, new WeakSet<object>());
208
+ }
209
+
210
+ /**
211
+ * Assert that a value does not contain non-finite numbers.
212
+ *
213
+ * @param value - The value to check
214
+ * @param name - The name of the parameter (for error messages)
215
+ * @throws ValidationError if the value contains NaN or Infinity
216
+ */
217
+ export function assertNoSpecialFloats(value: unknown, name: string): void {
218
+ if (containsSpecialFloat(value)) {
219
+ throw new ValidationError(
220
+ `${name} contains non-finite numbers (NaN or Infinity) which cannot be serialized to JSON`
221
+ );
222
+ }
223
+ }
224
+
225
+ // ═══════════════════════════════════════════════════════════════════════════
226
+ // PATH VALIDATION (for CLI security)
227
+ // ═══════════════════════════════════════════════════════════════════════════
228
+
229
+ /**
230
+ * Sanitize a string for use in a cache key filename.
231
+ * Removes or replaces characters that could cause path traversal or invalid filenames.
232
+ *
233
+ * @param value - The string to sanitize
234
+ * @returns A safe string for use in filenames
235
+ */
236
+ export function sanitizeForFilename(value: string): string {
237
+ // Replace path separators and traversal patterns
238
+ return value
239
+ .replace(/\.\./g, '__')
240
+ .replace(/[/\\:*?"<>|]/g, '_')
241
+ .replace(/\s+/g, '_');
242
+ }
243
+
244
+ /**
245
+ * Check if a string looks like a path traversal attempt.
246
+ *
247
+ * @param value - The string to check
248
+ * @returns True if the string contains path traversal patterns
249
+ */
250
+ export function containsPathTraversal(value: string): boolean {
251
+ return value.includes('..') || value.includes('/') || value.includes('\\');
252
+ }