@pulse-compute/wasm-compiler 0.0.0 → 1.0.0-beta.2

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 (104) hide show
  1. package/README.md +53 -1
  2. package/bin/provider-proof-composition.js +34 -0
  3. package/bin/pulsewasm-extract.js +20 -0
  4. package/package.json +60 -5
  5. package/src/artifacts-dir.js +13 -0
  6. package/src/ast-json.js +52 -0
  7. package/src/build-manifest.js +354 -0
  8. package/src/canonical-api-compiler.js +269 -0
  9. package/src/canonical-native-compiler.js +411 -0
  10. package/src/canonical-native-plan.js +1478 -0
  11. package/src/canonical-project-compiler.js +1224 -0
  12. package/src/canonical-router-compiler.js +25 -0
  13. package/src/cli-intents.js +1235 -0
  14. package/src/cli.js +1927 -0
  15. package/src/codegen/assemblyscript-compile.js +3 -0
  16. package/src/codegen/assemblyscript-core.js +3 -0
  17. package/src/codegen/assemblyscript-shape.js +3 -0
  18. package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
  19. package/src/codegen/backend-capabilities.js +3 -0
  20. package/src/codegen/channel-broadcaster.js +3 -0
  21. package/src/codegen/compiled-handlers.js +3 -0
  22. package/src/codegen/compiled-wasm-runtime.js +3 -0
  23. package/src/codegen/config-references.js +248 -0
  24. package/src/codegen/dispatch-ts.js +145 -0
  25. package/src/codegen/effect-composition.js +331 -0
  26. package/src/codegen/effect-runtime.js +418 -0
  27. package/src/codegen/execution-harness-ts.js +467 -0
  28. package/src/codegen/handler-bindings-ts.js +298 -0
  29. package/src/codegen/handler-library-contracts.js +3 -0
  30. package/src/codegen/host-capabilities.js +3 -0
  31. package/src/codegen/host-runtime-kernel.js +3 -0
  32. package/src/codegen/integrated-compiled-app.js +3 -0
  33. package/src/codegen/json-body.js +3 -0
  34. package/src/codegen/library-sidecars.js +3 -0
  35. package/src/codegen/local-harness-ts.js +453 -0
  36. package/src/codegen/pulse-wrapper.js +217 -0
  37. package/src/codegen/request-result-headers.js +3 -0
  38. package/src/codegen/schema-json-compile.js +3 -0
  39. package/src/codegen/schema-json-sidecar-v2.js +3 -0
  40. package/src/codegen/schema-json-sidecar.js +3 -0
  41. package/src/codegen/streaming-passthrough.js +3 -0
  42. package/src/codegen/wasm-host-abi.js +3 -0
  43. package/src/codegen/wasm-host-bridge.js +3 -0
  44. package/src/compiled-wasm-host-runtime-kv.js +3 -0
  45. package/src/config-resolver.js +813 -0
  46. package/src/crypto-requirement-planner.js +89 -0
  47. package/src/definitions/config-schema.js +14 -0
  48. package/src/definitions/handler-roles.js +14 -0
  49. package/src/definitions/path-grammar.js +14 -0
  50. package/src/definitions/router-api.js +14 -0
  51. package/src/diagnostics/codes.js +14 -0
  52. package/src/diagnostics/reporter.js +14 -0
  53. package/src/diagnostics.js +14 -0
  54. package/src/dispatch-table.js +400 -0
  55. package/src/events/event-emit.js +265 -0
  56. package/src/events/event-topology.js +127 -0
  57. package/src/execution-plan.js +463 -0
  58. package/src/extractor.js +2816 -0
  59. package/src/handler-eval.js +1154 -0
  60. package/src/handler-table.js +326 -0
  61. package/src/index.js +19 -0
  62. package/src/javascript-application-plan.js +181 -0
  63. package/src/kv-provider.js +3 -0
  64. package/src/path-table.js +60 -0
  65. package/src/path.js +14 -0
  66. package/src/patterns/config-define.js +30 -0
  67. package/src/patterns/dependency-call.js +18 -0
  68. package/src/patterns/env-lookup.js +13 -0
  69. package/src/patterns/handler-reference.js +29 -0
  70. package/src/patterns/path-literal.js +35 -0
  71. package/src/patterns/result.js +15 -0
  72. package/src/patterns/router-chain-call.js +50 -0
  73. package/src/patterns/router-construction.js +19 -0
  74. package/src/project/package-reachability.js +782 -0
  75. package/src/project/reachable-graph-builder.js +992 -0
  76. package/src/project/reachable-graph-contract.js +54 -0
  77. package/src/project/reachable-graph-implementation.js +36 -0
  78. package/src/project/router-module-linker.js +710 -0
  79. package/src/project-config-compiler.js +222 -0
  80. package/src/project-target-support.js +500 -0
  81. package/src/provider-toolchain.js +299 -0
  82. package/src/spine/async-surface-normalizer.js +328 -0
  83. package/src/spine/canonical-handler-ir.js +336 -0
  84. package/src/spine/canonical-native-module.js +87 -0
  85. package/src/spine/canonical-native-plan.js +89 -0
  86. package/src/spine/canonical-project.js +76 -0
  87. package/src/spine/canonical-router.js +155 -0
  88. package/src/spine/canonical-source.js +165 -0
  89. package/src/spine/diagnostic-authority.js +290 -0
  90. package/src/spine/equivalence.js +262 -0
  91. package/src/spine/guest-unit-stage.js +87 -0
  92. package/src/spine/handler-ir-emitter.js +455 -0
  93. package/src/spine/handler-ir-managed.js +1598 -0
  94. package/src/spine/handler-ir.js +797 -0
  95. package/src/spine/handler-surface-authority.js +588 -0
  96. package/src/spine/package-operation-seam.js +1015 -0
  97. package/src/spine/pipeline.js +202 -0
  98. package/src/spine/plain-handler-frontend.js +545 -0
  99. package/src/spine/provider-requirement-authority.js +208 -0
  100. package/src/spine/router-control-contract.js +17 -0
  101. package/src/spine/router-handler-frontend.js +514 -0
  102. package/src/spine/router-handler-ir.js +372 -0
  103. package/src/spine/router-topology-frontend.js +638 -0
  104. package/src/stable-id.js +14 -0
@@ -0,0 +1,453 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const { PACKAGE_VERSION, normalizeArtifact } = require('../diagnostics.js');
5
+ const { collectExpectedSlots } = require('./execution-harness-ts.js');
6
+
7
+ const LOCAL_HARNESS_VERSION = 'pulsewasm.local-harness.v1';
8
+
9
+ function sha256(text) {
10
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
11
+ }
12
+
13
+ function tsJson(value) {
14
+ return JSON.stringify(value, null, 2)
15
+ .replace(/</g, '\\u003c')
16
+ .replace(/>/g, '\\u003e')
17
+ .replace(/&/g, '\\u0026');
18
+ }
19
+
20
+ function header(file = 'local harness') {
21
+ return [
22
+ '/*',
23
+ ' * Generated by PulseWasm Phase 9D.',
24
+ ' * Do not edit by hand.',
25
+ ' *',
26
+ ` * This ${file} is generated from execution-plan.json + handler-bindings.json.`,
27
+ ' * It provides a local validation surface around the generated routing core.',
28
+ ' */',
29
+ ''
30
+ ].join('\n');
31
+ }
32
+
33
+ function makeFile(file, text) {
34
+ return {
35
+ file,
36
+ bytes: Buffer.byteLength(text, 'utf8'),
37
+ sha256: sha256(text),
38
+ text
39
+ };
40
+ }
41
+
42
+ function buildLocalHarnessSource(expectedSlots) {
43
+ return `${header()}import { match } from './dispatch'
44
+ import { EXECUTION_ENTRIES, EXECUTION_SCOPES } from './execution-plan'
45
+ import {
46
+ createExecutionContext,
47
+ execute,
48
+ executeLifecycle,
49
+ type ExecuteOptions,
50
+ type ExecutionContext,
51
+ type ExecutionResult,
52
+ type ExecutionTraceEvent
53
+ } from './execution-harness'
54
+ import { getHandlerBySlot, assertHandlerBindingCoverage, type PulseHandler } from './handler-slots'
55
+
56
+ export const RESULT_NONE = 0 as const
57
+ export const RESULT_TEXT = 1 as const
58
+ export const RESULT_JSON = 2 as const
59
+ export const RESULT_BINARY = 3 as const
60
+ export const RESULT_EMPTY = 4 as const
61
+
62
+ export const DEFAULT_STATUS_NOT_FOUND = 404 as const
63
+ export const DEFAULT_STATUS_INTERNAL_ERROR = 500 as const
64
+ export const DEFAULT_STATUS_NO_CONTENT = 204 as const
65
+
66
+ export const DEFAULT_ERROR_NO_RESULT = 'PULSEWASM_NO_RESULT' as const
67
+ export const DEFAULT_ERROR_NOT_FOUND = 'PULSEWASM_NOT_FOUND' as const
68
+ export const DEFAULT_ERROR_UNHANDLED = 'PULSEWASM_UNHANDLED_ERROR' as const
69
+ export const DEFAULT_ERROR_BROADCASTER_MISSING = 'PULSEWASM_BROADCASTER_MISSING' as const
70
+
71
+ export type PulseResultKind =
72
+ | typeof RESULT_NONE
73
+ | typeof RESULT_TEXT
74
+ | typeof RESULT_JSON
75
+ | typeof RESULT_BINARY
76
+ | typeof RESULT_EMPTY
77
+
78
+ export type PulseResultRef = {
79
+ readonly statusCode: number
80
+ readonly bodyRef: unknown
81
+ readonly headersRef: Map<string, string>
82
+ readonly kind: PulseResultKind
83
+ readonly code?: string
84
+ readonly message?: string
85
+ }
86
+
87
+ export type PulseLocalContext = ExecutionContext & {
88
+ state: Map<string, unknown>
89
+ result?: PulseResultRef
90
+ channels: string[]
91
+ broadcasts: LocalBroadcastRecord[]
92
+ }
93
+
94
+ export type LocalBroadcastRecord = {
95
+ readonly channels: readonly string[]
96
+ readonly routeIndex?: number
97
+ readonly routeName?: string
98
+ readonly value?: unknown
99
+ }
100
+
101
+ export type Broadcaster = (
102
+ channels: readonly string[],
103
+ ctx: PulseLocalContext,
104
+ detail: { readonly routeEntry?: unknown; readonly execution: ExecutionResult }
105
+ ) => unknown
106
+
107
+ export type LocalHarnessOptions = Omit<ExecuteOptions, 'ctx'> & {
108
+ readonly ctx?: PulseLocalContext | ExecutionContext
109
+ readonly broadcaster?: Broadcaster
110
+ }
111
+
112
+ export type LocalLifecycleOptions = Omit<ExecuteOptions, 'ctx'> & {
113
+ readonly ctx?: PulseLocalContext | ExecutionContext
114
+ }
115
+
116
+ export type LocalHarnessRun = {
117
+ readonly status: 'ok' | 'not-found' | 'error' | 'contract-violation'
118
+ readonly result: PulseResultRef
119
+ readonly ctx: PulseLocalContext
120
+ readonly execution?: ExecutionResult
121
+ readonly trace: readonly ExecutionTraceEvent[]
122
+ readonly executedSlots: readonly number[]
123
+ readonly channels: readonly string[]
124
+ readonly broadcasts: readonly LocalBroadcastRecord[]
125
+ }
126
+
127
+ export const EXPECTED_LOCAL_HARNESS_SLOTS = ${tsJson(expectedSlots)} as const
128
+
129
+ function ensureSync(value: unknown, label: string): void {
130
+ if (value && typeof (value as { then?: unknown }).then === 'function') {
131
+ throw new Error('PulseWasm Phase 9D local harness received a Promise from ' + label)
132
+ }
133
+ }
134
+
135
+ function emptyHeaders(): Map<string, string> {
136
+ return new Map<string, string>()
137
+ }
138
+
139
+ export function makeEmptyResult(statusCode: number = DEFAULT_STATUS_NO_CONTENT): PulseResultRef {
140
+ return { statusCode, bodyRef: undefined, headersRef: emptyHeaders(), kind: RESULT_EMPTY }
141
+ }
142
+
143
+ export function makeTextResult(body: string, statusCode: number = 200): PulseResultRef {
144
+ return { statusCode, bodyRef: String(body), headersRef: emptyHeaders(), kind: RESULT_TEXT }
145
+ }
146
+
147
+ export function makeJsonResult(body: unknown, statusCode: number = 200): PulseResultRef {
148
+ return { statusCode, bodyRef: body, headersRef: emptyHeaders(), kind: RESULT_JSON }
149
+ }
150
+
151
+ export function makeErrorResult(code: string, message: string, statusCode: number = DEFAULT_STATUS_INTERNAL_ERROR): PulseResultRef {
152
+ return { statusCode, bodyRef: message, headersRef: emptyHeaders(), kind: RESULT_TEXT, code, message }
153
+ }
154
+
155
+ function isPulseResultRef(value: unknown): value is PulseResultRef {
156
+ return Boolean(
157
+ value &&
158
+ typeof value === 'object' &&
159
+ typeof (value as PulseResultRef).statusCode === 'number' &&
160
+ typeof (value as PulseResultRef).kind === 'number'
161
+ )
162
+ }
163
+
164
+ export function normalizeResult(value: unknown, defaultStatus: number = 200): PulseResultRef {
165
+ if (isPulseResultRef(value)) return value
166
+ if (typeof value === 'string') return makeTextResult(value, defaultStatus)
167
+ if (value instanceof Uint8Array) return { statusCode: defaultStatus, bodyRef: value, headersRef: emptyHeaders(), kind: RESULT_BINARY }
168
+ if (value == null) return makeEmptyResult(defaultStatus)
169
+ return makeJsonResult(value, defaultStatus)
170
+ }
171
+
172
+ function errorMessage(error: unknown): string {
173
+ if (error && typeof error === 'object' && typeof (error as { message?: unknown }).message === 'string') return (error as { message: string }).message
174
+ return String(error ?? 'Unhandled PulseWasm error')
175
+ }
176
+
177
+ function errorCode(error: unknown): string {
178
+ if (error && typeof error === 'object' && typeof (error as { code?: unknown }).code === 'string') return (error as { code: string }).code
179
+ return DEFAULT_ERROR_UNHANDLED
180
+ }
181
+
182
+ function errorStatus(error: unknown): number {
183
+ if (error && typeof error === 'object' && typeof (error as { statusCode?: unknown }).statusCode === 'number') return (error as { statusCode: number }).statusCode
184
+ return DEFAULT_STATUS_INTERNAL_ERROR
185
+ }
186
+
187
+ export function normalizeErrorResult(error: unknown): PulseResultRef {
188
+ return makeErrorResult(errorCode(error), errorMessage(error), errorStatus(error))
189
+ }
190
+
191
+ export function createLocalContext(method: string, requestPath: string, extra: Record<string, unknown> = {}): PulseLocalContext {
192
+ const base = createExecutionContext(method, requestPath, extra) as PulseLocalContext
193
+ const providedState = (extra as { state?: unknown }).state
194
+ base.state = providedState instanceof Map ? providedState as Map<string, unknown> : new Map<string, unknown>()
195
+ base.channels = Array.isArray((extra as { channels?: unknown }).channels) ? Array.from((extra as { channels: string[] }).channels) : []
196
+ base.broadcasts = Array.isArray((extra as { broadcasts?: unknown }).broadcasts) ? Array.from((extra as { broadcasts: LocalBroadcastRecord[] }).broadcasts) : []
197
+ return base
198
+ }
199
+
200
+ function asLocalContext(method: string, requestPath: string, ctx?: PulseLocalContext | ExecutionContext): PulseLocalContext {
201
+ if (ctx) {
202
+ const local = ctx as PulseLocalContext
203
+ local.method = String(method || '').toUpperCase()
204
+ local.path = requestPath
205
+ if (!(local.state instanceof Map)) local.state = new Map<string, unknown>()
206
+ if (!Array.isArray(local.channels)) local.channels = []
207
+ if (!Array.isArray(local.broadcasts)) local.broadcasts = []
208
+ return local
209
+ }
210
+ return createLocalContext(method, requestPath)
211
+ }
212
+
213
+ function entryAt(index: number): any | undefined {
214
+ return (EXECUTION_ENTRIES as readonly any[])[index]
215
+ }
216
+
217
+ function scopeById(scopeId: number | null | undefined): any | undefined {
218
+ if (!Number.isInteger(scopeId)) return undefined
219
+ return (EXECUTION_SCOPES as readonly any[]).find((scope) => scope.id === scopeId)
220
+ }
221
+
222
+ function lastRouteEntryFromTrace(trace: readonly ExecutionTraceEvent[]): any | undefined {
223
+ for (let i = trace.length - 1; i >= 0; i -= 1) {
224
+ const event = trace[i]
225
+ if (event.kind !== 'enter' || event.phase !== 'normal') continue
226
+ const entry = entryAt(event.index)
227
+ if (entry && entry.kind === 'route') return entry
228
+ }
229
+ return undefined
230
+ }
231
+
232
+ function normalizeChannels(value: unknown): string[] {
233
+ if (typeof value === 'string') return value.length > 0 ? [value] : []
234
+ if (Array.isArray(value)) return value.filter((item): item is string => typeof item === 'string' && item.length > 0)
235
+ return []
236
+ }
237
+
238
+ function resolveChannelsFromExecution(execution: ExecutionResult, ctx: PulseLocalContext, resolver: (slot: number) => PulseHandler | undefined): { channels: string[]; routeEntry?: unknown; error?: PulseResultRef } {
239
+ const routeEntry = lastRouteEntryFromTrace(execution.trace)
240
+ if (!routeEntry) return { channels: [] }
241
+ const scope = scopeById(routeEntry.scopeId)
242
+ const channel = scope?.channel || routeEntry.routePlan?.channel
243
+ if (!channel) return { channels: [], routeEntry }
244
+ if (channel.kind === 'static') return { channels: normalizeChannels(channel.value), routeEntry }
245
+ if (!Number.isInteger(channel.slot)) {
246
+ return { channels: [], routeEntry, error: makeErrorResult('PULSEWASM_CHANNEL_HANDLER_MISSING', 'Channel routing has no concrete handler slot.', DEFAULT_STATUS_INTERNAL_ERROR) }
247
+ }
248
+ const handler = resolver(channel.slot)
249
+ if (!handler) {
250
+ return { channels: [], routeEntry, error: makeErrorResult('PULSEWASM_CHANNEL_HANDLER_MISSING', 'Channel routing referenced a missing handler binding.', DEFAULT_STATUS_INTERNAL_ERROR) }
251
+ }
252
+ const value = handler(ctx)
253
+ ensureSync(value, 'channel handler ' + String(channel.name || channel.id || channel.slot))
254
+ return { channels: normalizeChannels(value), routeEntry }
255
+ }
256
+
257
+ function finalizeRun(status: LocalHarnessRun['status'], result: PulseResultRef, ctx: PulseLocalContext, execution?: ExecutionResult, channels: string[] = []): LocalHarnessRun {
258
+ ctx.result = result
259
+ ctx.channels = channels
260
+ return {
261
+ status,
262
+ result,
263
+ ctx,
264
+ execution,
265
+ trace: execution?.trace || ctx.trace || [],
266
+ executedSlots: execution?.executedSlots || [],
267
+ channels,
268
+ broadcasts: ctx.broadcasts
269
+ }
270
+ }
271
+
272
+ export function assertLocalHarnessBindingCoverage(expectedSlots: readonly number[] = EXPECTED_LOCAL_HARNESS_SLOTS): void {
273
+ assertHandlerBindingCoverage(expectedSlots)
274
+ }
275
+
276
+ export function executeRequest(method: string, requestPath: string, options: LocalHarnessOptions = {}): LocalHarnessRun {
277
+ const normalizedMethod = String(method || '').toUpperCase()
278
+ const ctx = asLocalContext(normalizedMethod, requestPath, options.ctx)
279
+ const resolver = options.getHandlerBySlot || getHandlerBySlot
280
+ let execution: ExecutionResult
281
+
282
+ try {
283
+ execution = execute(normalizedMethod, requestPath, { ...options, ctx, getHandlerBySlot: resolver })
284
+ } catch (error) {
285
+ return finalizeRun('error', normalizeErrorResult(error), ctx)
286
+ }
287
+
288
+ if (execution.status === 'not-found') {
289
+ return finalizeRun('not-found', makeErrorResult(DEFAULT_ERROR_NOT_FOUND, 'No route handled the request.', DEFAULT_STATUS_NOT_FOUND), ctx, execution)
290
+ }
291
+
292
+ if (execution.status === 'error') {
293
+ return finalizeRun('error', normalizeErrorResult(execution.error), ctx, execution)
294
+ }
295
+
296
+ if (execution.status === 'halt') {
297
+ return finalizeRun('contract-violation', makeErrorResult(DEFAULT_ERROR_NO_RESULT, 'Handler halted without result or continuation.', DEFAULT_STATUS_INTERNAL_ERROR), ctx, execution)
298
+ }
299
+
300
+ if (execution.status !== 'response') {
301
+ return finalizeRun('error', makeErrorResult(DEFAULT_ERROR_NO_RESULT, 'Dispatch completed without a response result.', DEFAULT_STATUS_INTERNAL_ERROR), ctx, execution)
302
+ }
303
+
304
+ const channelResolution = resolveChannelsFromExecution(execution, ctx, resolver)
305
+ if (channelResolution.error) return finalizeRun('error', channelResolution.error, ctx, execution)
306
+ const channels = channelResolution.channels
307
+ if (channels.length > 0) {
308
+ if (!options.broadcaster) {
309
+ return finalizeRun('error', makeErrorResult(DEFAULT_ERROR_BROADCASTER_MISSING, 'Channel routing resolved channels, but no broadcaster adaptor was provided.', DEFAULT_STATUS_INTERNAL_ERROR), ctx, execution, channels)
310
+ }
311
+ try {
312
+ const value = options.broadcaster(channels, ctx, { routeEntry: channelResolution.routeEntry, execution })
313
+ ensureSync(value, 'broadcaster adaptor')
314
+ ctx.broadcasts.push({ channels, routeIndex: (channelResolution.routeEntry as { index?: number } | undefined)?.index, routeName: (channelResolution.routeEntry as { handler?: { name?: string } } | undefined)?.handler?.name, value })
315
+ } catch (error) {
316
+ return finalizeRun('error', normalizeErrorResult(error), ctx, execution, channels)
317
+ }
318
+ }
319
+
320
+ return finalizeRun('ok', normalizeResult(execution.value, 200), ctx, execution, channels)
321
+ }
322
+
323
+ function executeLifecycleEvent(method: string, requestPath: string, event: 'connect' | 'disconnect', options: LocalLifecycleOptions = {}): LocalHarnessRun {
324
+ const normalizedMethod = String(method || '').toUpperCase()
325
+ const ctx = asLocalContext(normalizedMethod, requestPath, options.ctx)
326
+ const matched = match(normalizedMethod, requestPath)
327
+ if (!matched) {
328
+ return finalizeRun('not-found', makeErrorResult(DEFAULT_ERROR_NOT_FOUND, 'No route matched lifecycle request.', DEFAULT_STATUS_NOT_FOUND), ctx)
329
+ }
330
+ try {
331
+ const execution = executeLifecycle(matched, event, { ...options, ctx, getHandlerBySlot: options.getHandlerBySlot || getHandlerBySlot })
332
+ return finalizeRun('ok', makeEmptyResult(DEFAULT_STATUS_NO_CONTENT), ctx, execution)
333
+ } catch (error) {
334
+ return finalizeRun('error', normalizeErrorResult(error), ctx)
335
+ }
336
+ }
337
+
338
+ export function executeConnect(method: string, requestPath: string, options: LocalLifecycleOptions = {}): LocalHarnessRun {
339
+ return executeLifecycleEvent(method, requestPath, 'connect', options)
340
+ }
341
+
342
+ export function executeDisconnect(method: string, requestPath: string, options: LocalLifecycleOptions = {}): LocalHarnessRun {
343
+ return executeLifecycleEvent(method, requestPath, 'disconnect', options)
344
+ }
345
+ `;
346
+ }
347
+
348
+ function buildTypeScriptLocalHarness(dispatchTable, handlerBindings, executionHarness, options = {}) {
349
+ const cwd = options.cwd || process.cwd();
350
+ const diagnostics = [];
351
+ const executionPlan = options.executionPlan;
352
+
353
+ if (!handlerBindings) {
354
+ diagnostics.push({
355
+ pass: 'local-harness',
356
+ code: 'PULSEWASM_LOCAL_HARNESS_REQUIRES_HANDLER_BINDINGS',
357
+ severity: 'error',
358
+ message: 'The Phase 9D local harness requires generated handler slot bindings.',
359
+ hint: 'Enable --emit-handler-bindings or use --emit-local-harness, which implies handler bindings.',
360
+ loc: { file: '<local-harness>' }
361
+ });
362
+ }
363
+
364
+ if (!executionHarness) {
365
+ diagnostics.push({
366
+ pass: 'local-harness',
367
+ code: 'PULSEWASM_LOCAL_HARNESS_REQUIRES_EXECUTION_HARNESS',
368
+ severity: 'error',
369
+ message: 'The Phase 9D local harness requires the generated execution harness.',
370
+ hint: 'Use --emit-local-harness, which implies --emit-execution-harness.',
371
+ loc: { file: '<local-harness>' }
372
+ });
373
+ }
374
+
375
+ if (!executionPlan) {
376
+ diagnostics.push({
377
+ pass: 'local-harness',
378
+ code: 'PULSEWASM_LOCAL_HARNESS_REQUIRES_EXECUTION_PLAN',
379
+ severity: 'error',
380
+ message: 'The Phase 9D local harness requires execution-plan.json.',
381
+ hint: 'Run the execution-plan pass before generated local harness emission.',
382
+ loc: { file: '<local-harness>' }
383
+ });
384
+ }
385
+
386
+ const expectedSlots = collectExpectedSlots(dispatchTable, executionPlan);
387
+ const files = [makeFile('generated/local-harness.ts', buildLocalHarnessSource(expectedSlots))];
388
+
389
+ const artifact = normalizeArtifact({
390
+ version: LOCAL_HARNESS_VERSION,
391
+ generatedBy: options.generatedBy || PACKAGE_VERSION,
392
+ source: dispatchTable?.source,
393
+ dispatchTableVersion: dispatchTable?.version,
394
+ executionPlanVersion: executionPlan?.version,
395
+ handlerBindingsVersion: handlerBindings?.artifact?.version,
396
+ executionHarnessVersion: executionHarness?.artifact?.version,
397
+ policy: {
398
+ phase: '9D',
399
+ sourceOfTruth: 'execution-plan.json + generated execution harness + generated handler slot bindings',
400
+ usesOriginalRouter: false,
401
+ runsWasm: false,
402
+ emitsWasm: false,
403
+ resultAbi: 'PulseResultRef v1',
404
+ stateBag: 'Map',
405
+ broadcasterRequiredWhenChannelsResolve: true,
406
+ lifecycleEntrypoints: ['executeConnect', 'executeDisconnect'],
407
+ requestEntrypoint: 'executeRequest',
408
+ hostBindings: false,
409
+ highLevelPulseWrapper: false
410
+ },
411
+ resultAbi: {
412
+ version: 'pulsewasm.result-abi.v1',
413
+ fields: ['statusCode', 'bodyRef', 'headersRef', 'kind'],
414
+ kinds: {
415
+ RESULT_NONE: 0,
416
+ RESULT_TEXT: 1,
417
+ RESULT_JSON: 2,
418
+ RESULT_BINARY: 3,
419
+ RESULT_EMPTY: 4
420
+ }
421
+ },
422
+ fallthrough: {
423
+ normal: { statusCode: 404, code: 'PULSEWASM_NOT_FOUND' },
424
+ error: { statusCode: 500, code: 'PULSEWASM_UNHANDLED_ERROR' },
425
+ contractViolation: { statusCode: 500, code: 'PULSEWASM_NO_RESULT' },
426
+ broadcasterMissing: { statusCode: 500, code: 'PULSEWASM_BROADCASTER_MISSING' }
427
+ },
428
+ output: {
429
+ language: 'typescript',
430
+ module: 'esm',
431
+ files: files.map(({ file, bytes, sha256: hash }) => ({ file, bytes, sha256: hash }))
432
+ },
433
+ summary: {
434
+ entries: executionPlan?.summary?.entries || 0,
435
+ routes: executionPlan?.summary?.routes || 0,
436
+ errors: executionPlan?.summary?.errors || 0,
437
+ scopes: executionPlan?.summary?.scopes || 0,
438
+ expectedHandlerSlots: expectedSlots.length,
439
+ resultAbi: true,
440
+ mapState: true,
441
+ broadcasterAdaptor: true,
442
+ lifecycleEntrypoints: 2,
443
+ diagnostics: diagnostics.length
444
+ }
445
+ }, cwd);
446
+
447
+ return { version: LOCAL_HARNESS_VERSION, artifact, files, diagnostics };
448
+ }
449
+
450
+ module.exports = {
451
+ LOCAL_HARNESS_VERSION,
452
+ buildTypeScriptLocalHarness
453
+ };
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const { PACKAGE_VERSION, normalizeArtifact, normalizeDiagnostic } = require('../diagnostics.js');
8
+
9
+ const WRAPPER_INTEGRATION_VERSION = 'pulsewasm.wrapper-integration.v1';
10
+ const PULSE_BUILD_OUTPUT_VERSION = 'pulsewasm.pulse-build-output.v1';
11
+ const PULSE_DEV_RUNTIME_VERSION = 'pulsewasm.pulse-dev-runtime.v1';
12
+ const PHASE = '13A';
13
+
14
+ function sha256Text(text) { return crypto.createHash('sha256').update(String(text || ''), 'utf8').digest('hex'); }
15
+ function makeFile(file, text) { return { file, text, bytes: Buffer.byteLength(text, 'utf8'), sha256: sha256Text(text) }; }
16
+ function diagnostic(code, message, hint, details) {
17
+ return normalizeDiagnostic({ phase: 'pulse-wrapper', severity: 'error', code, message, hint, details, loc: { file: '<pulse-wrapper>' } });
18
+ }
19
+ function readOutputBuffer(output) { return output && output.file && fs.existsSync(output.file) ? fs.readFileSync(output.file) : undefined; }
20
+ function writeTempFile(root, file, textOrBuffer) {
21
+ const abs = path.join(root, file);
22
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
23
+ fs.writeFileSync(abs, textOrBuffer);
24
+ return abs;
25
+ }
26
+ function jsonText(value) { return `${JSON.stringify(value, null, 2)}\n`; }
27
+ function normalizeHeaders(headers) { return (headers || []).map((pair) => [String(pair[0]), String(pair[1])]); }
28
+
29
+ function buildWrapperSource() {
30
+ return `/* Generated by PulseWasm Phase 13A.\n * High-level Pulse wrapper integration over the compiled-Wasm local runtime.\n */\n'use strict';\nconst fs = require('node:fs');\nconst path = require('node:path');\n\nfunction defineConfig(configOrFactory) { return configOrFactory; }\nfunction readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')); }\nfunction defaultArtifactDir() { return path.resolve(__dirname, '../..'); }\nfunction artifactDirFrom(options) { return path.resolve(options && options.artifactDir ? options.artifactDir : defaultArtifactDir()); }\nfunction artifactPath(artifactDir, file) { return path.join(artifactDir, file); }\nfunction safeReadJson(artifactDir, file) { const target = artifactPath(artifactDir, file); return fs.existsSync(target) ? readJson(target) : undefined; }\n\nfunction createPulseBuildOutput(options = {}) {\n const artifactDir = artifactDirFrom(options);\n const resolvedConfig = safeReadJson(artifactDir, 'resolved-config.json');\n const deploymentPosture = safeReadJson(artifactDir, 'deployment-posture.json');\n const buildManifest = safeReadJson(artifactDir, 'build-manifest.json');\n const artifacts = {\n resolvedConfig: 'resolved-config.json',\n deploymentPosture: 'deployment-posture.json',\n compiledWasmRuntime: 'compiled-wasm-runtime.json',\n compiledWasmHostRuntime: 'compiled-wasm-host-runtime.json',\n compiledWasmNodeAdapter: 'compiled-wasm-node-adapter.json',\n compiledWasmRuntimeSmoke: 'compiled-wasm-runtime-smoke.json',\n wasm: 'generated/compiled-wasm-runtime/pulsewasm-compiled-runtime.wasm',\n hostRuntime: 'generated/host/compiled-wasm-host-runtime.cjs',\n nodeAdapter: 'generated/node/compiled-wasm-node-adapter.cjs'\n };\n return {\n version: 'pulsewasm.pulse-build-output.runtime.v1',\n artifactDir,\n profile: resolvedConfig && resolvedConfig.profile,\n runtime: resolvedConfig && resolvedConfig.runtime,\n engine: deploymentPosture && deploymentPosture.engine,\n handlerExecutionMode: resolvedConfig && resolvedConfig.runtime && resolvedConfig.runtime.handlerExecutionMode,\n deploymentPosture,\n buildManifest,\n artifacts\n };\n}\n\nfunction createPulseDevRuntime(options = {}) {\n const artifactDir = artifactDirFrom(options);\n const buildOutput = createPulseBuildOutput({ artifactDir });\n const posture = options.deploymentPosture || buildOutput.deploymentPosture || { engine: 'wasm' };\n if ((posture.engine || 'wasm') === 'js' && !options.allowJsEngine) {\n const err = new Error('Pulse dev runtime refuses JS engine posture for the Wasm wrapper path.');\n err.code = 'PULSEWASM_WRAPPER_ENGINE_MISMATCH';\n throw err;\n }\n const hostRuntimePath = options.hostRuntimePath || artifactPath(artifactDir, buildOutput.artifacts.hostRuntime);\n const nodeAdapterPath = options.nodeAdapterPath || artifactPath(artifactDir, buildOutput.artifacts.nodeAdapter);\n const wasmPath = options.wasmPath || artifactPath(artifactDir, buildOutput.artifacts.wasm);\n if (!fs.existsSync(hostRuntimePath)) { const err = new Error('Missing compiled-wasm host runtime artifact.'); err.code = 'PULSEWASM_WRAPPER_HOST_RUNTIME_MISSING'; throw err; }\n if (!fs.existsSync(nodeAdapterPath)) { const err = new Error('Missing compiled-wasm Node adapter artifact.'); err.code = 'PULSEWASM_WRAPPER_NODE_ADAPTER_MISSING'; throw err; }\n if (!fs.existsSync(wasmPath)) { const err = new Error('Missing compiled-wasm runtime .wasm artifact.'); err.code = 'PULSEWASM_WRAPPER_WASM_MISSING'; throw err; }\n const { createPulseWasmCompiledHostRuntime } = require(hostRuntimePath);\n const { createPulseWasmCompiledNodeAdapter, createNodeHandler, createNodeServer } = require(nodeAdapterPath);\n const runtime = createPulseWasmCompiledHostRuntime({\n wasmPath,\n deploymentPosture: posture,\n hostResponses: options.hostResponses\n });\n const nodeAdapter = createPulseWasmCompiledNodeAdapter({ runtime });\n return {\n version: 'pulsewasm.pulse-dev-runtime.runtime.v1',\n engine: posture.engine || 'wasm',\n handlerExecutionMode: 'compiled-wasm',\n buildOutput,\n runtime,\n nodeAdapter,\n executeRequest(input) { return runtime.executeRequest(input); },\n executeConnect(input) { return runtime.executeConnect(input); },\n executeDisconnect(input) { return runtime.executeDisconnect(input); },\n createNodeHandler(options = {}) { return createNodeHandler(runtime, options); },\n createNodeServer(options = {}) { return createNodeServer(runtime, options); },\n close() { if (runtime.close) return runtime.close(); }\n };\n}\n\nfunction createPulseNodeHandler(options = {}) {\n return createPulseDevRuntime(options).createNodeHandler(options);\n}\n\nmodule.exports = {\n defineConfig,\n createPulseBuildOutput,\n createPulseDevRuntime,\n createPulseNodeHandler\n};\n`;
31
+ }
32
+
33
+ function buildMarkdown(artifact) {
34
+ const lines = [];
35
+ lines.push('# PulseWasm Phase 13A — High-Level Wrapper Integration');
36
+ lines.push('');
37
+ lines.push('The wrapper is a thin paved path over the existing compiled-Wasm local runtime. It consumes artifacts; it does not redefine extraction, handler lowering, sidecar semantics, effects, or platform behavior.');
38
+ lines.push('');
39
+ lines.push('## Locked config shape');
40
+ lines.push('');
41
+ lines.push('- Root config defines source topology only: `entry`, `rootRouter`, and `profiles`.');
42
+ lines.push('- `PULSE_PROFILE` / CLI profile selects the active profile.');
43
+ lines.push('- Profile `runtime` defines engine, handler execution mode, timeouts, payload, and capabilities.');
44
+ lines.push('- Profiles may not change route topology.');
45
+ lines.push('');
46
+ lines.push('## Wrapper exports');
47
+ for (const name of artifact.wrapper.exports) lines.push(`- \`${name}\``);
48
+ lines.push('');
49
+ lines.push('## Smoke');
50
+ lines.push(`- Checks: \`${artifact.summary.smokeChecks}\``);
51
+ lines.push(`- Failed checks: \`${artifact.summary.failedSmokeChecks}\``);
52
+ return `${lines.join('\n')}\n`;
53
+ }
54
+
55
+ function runWrapperSmoke({ wrapperSource, compiledWasmRuntime, resolvedConfig, deploymentPosture }) {
56
+ const checks = [];
57
+ const failedChecks = [];
58
+ function check(name, actual, expected) {
59
+ const passed = actual === expected;
60
+ const record = { name, actual, expected, passed };
61
+ checks.push(record);
62
+ if (!passed) failedChecks.push(record);
63
+ }
64
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pulsewasm-13a-wrapper-'));
65
+ try {
66
+ writeTempFile(tmp, 'generated/pulse/pulse-wrapper.cjs', wrapperSource);
67
+ for (const file of compiledWasmRuntime.files || []) writeTempFile(tmp, file.file, file.text);
68
+ for (const output of compiledWasmRuntime.outputFiles || []) {
69
+ const buffer = readOutputBuffer(output);
70
+ if (buffer) writeTempFile(tmp, output.file.replace(/^.*?generated\//, 'generated/'), buffer);
71
+ }
72
+ writeTempFile(tmp, 'resolved-config.json', jsonText(resolvedConfig));
73
+ writeTempFile(tmp, 'deployment-posture.json', jsonText(deploymentPosture));
74
+ writeTempFile(tmp, 'build-manifest.json', jsonText({ version: 'pulsewasm.wrapper-smoke-manifest.v1' }));
75
+ const wrapper = require(path.join(tmp, 'generated/pulse/pulse-wrapper.cjs'));
76
+ check('defineConfig_function', typeof wrapper.defineConfig, 'function');
77
+ check('createPulseBuildOutput_function', typeof wrapper.createPulseBuildOutput, 'function');
78
+ check('createPulseDevRuntime_function', typeof wrapper.createPulseDevRuntime, 'function');
79
+ const output = wrapper.createPulseBuildOutput({ artifactDir: tmp });
80
+ check('build_output_profile', output.profile, resolvedConfig.profile);
81
+ check('build_output_engine', output.engine, 'wasm');
82
+ check('build_output_handler_mode', output.handlerExecutionMode, 'compiled-wasm');
83
+ const dev = wrapper.createPulseDevRuntime({ artifactDir: tmp });
84
+ check('dev_runtime_engine', dev.engine, 'wasm');
85
+ check('dev_runtime_handler_mode', dev.handlerExecutionMode, 'compiled-wasm');
86
+ const schemaOk = dev.executeRequest({ method: 'POST', path: '/users', bodyText: '{"name":"Ada","age":37,"active":true}' });
87
+ check('schema_success_status', schemaOk.status, 201);
88
+ check('schema_success_kind', schemaOk.kind, 'json');
89
+ const schemaBad = dev.executeRequest({ method: 'POST', path: '/users', bodyText: '{"name":"Ada","age":"bad","active":true}' });
90
+ check('schema_error_status', schemaBad.status, 400);
91
+ const extension = dev.executeRequest({ method: 'GET', path: '/extension' });
92
+ check('extension_status', extension.status, 220);
93
+ const stream = dev.nodeAdapter.executeRequest({ method: 'GET', path: '/stream' });
94
+ check('stream_kind', stream.kind, 'stream');
95
+ check('stream_response_ref', stream.responseRef, 77);
96
+ check('no_js_handlers_required', dev.runtime.handlerExecutionMode, 'compiled-wasm');
97
+ try {
98
+ wrapper.createPulseDevRuntime({ artifactDir: tmp, deploymentPosture: { engine: 'js' } });
99
+ check('js_posture_rejected', false, true);
100
+ } catch (error) {
101
+ check('js_posture_rejected', error.code === 'PULSEWASM_WRAPPER_ENGINE_MISMATCH', true);
102
+ }
103
+ } catch (error) {
104
+ failedChecks.push({ name: 'wrapper_smoke_exception', actual: error && error.message ? error.message : String(error), expected: 'no exception', passed: false });
105
+ }
106
+ return { checks, failedChecks, tmp };
107
+ }
108
+
109
+ function buildPulseWrapperIntegration(inputs = {}, options = {}) {
110
+ const generatedBy = options.generatedBy || PACKAGE_VERSION;
111
+ const resolvedConfig = options.resolvedConfig || inputs.resolvedConfig || {};
112
+ const compiledWasmRuntime = inputs.compiledWasmRuntime;
113
+ const deploymentPosture = inputs.deploymentPosture?.artifact || inputs.deploymentPosture || { engine: 'wasm', wasmOptimized: true };
114
+ const diagnostics = [];
115
+ if (!compiledWasmRuntime || !compiledWasmRuntime.artifact) {
116
+ diagnostics.push(diagnostic('PULSEWASM_WRAPPER_COMPILED_RUNTIME_REQUIRED', 'Phase 13A requires compiled-wasm-runtime.json.', 'Enable --emit-compiled-wasm-runtime before --emit-pulse-wrapper.'));
117
+ }
118
+ if ((resolvedConfig.version || '') !== 'pulsewasm.resolved-config.v2') {
119
+ diagnostics.push(diagnostic('PULSEWASM_WRAPPER_CONFIG_V2_REQUIRED', 'Phase 13A requires resolved-config.v2.', 'Use config with root entry/rootRouter/profiles and runtime settings under the selected profile.'));
120
+ }
121
+ const wrapperSource = buildWrapperSource();
122
+ let smoke = { checks: [], failedChecks: [] };
123
+ if (diagnostics.length === 0) {
124
+ smoke = runWrapperSmoke({ wrapperSource, compiledWasmRuntime, resolvedConfig, deploymentPosture });
125
+ if (smoke.failedChecks.length > 0) {
126
+ diagnostics.push(diagnostic('PULSEWASM_WRAPPER_SMOKE_FAILED', 'Pulse wrapper integration smoke checks failed.', 'Inspect wrapper-integration.json smoke checks.', { failedChecks: smoke.failedChecks }));
127
+ }
128
+ }
129
+
130
+ const buildOutput = normalizeArtifact({
131
+ version: PULSE_BUILD_OUTPUT_VERSION,
132
+ generatedBy,
133
+ phase: PHASE,
134
+ profile: resolvedConfig.profile,
135
+ entry: resolvedConfig.entry,
136
+ rootRouter: resolvedConfig.rootRouter,
137
+ runtime: resolvedConfig.runtime,
138
+ engine: deploymentPosture.engine || 'wasm',
139
+ handlerExecutionMode: resolvedConfig.runtime?.handlerExecutionMode || 'compiled-wasm',
140
+ artifacts: {
141
+ resolvedConfig: 'resolved-config.json',
142
+ deploymentPosture: 'deployment-posture.json',
143
+ compiledWasmRuntime: 'compiled-wasm-runtime.json',
144
+ wasm: 'generated/compiled-wasm-runtime/pulsewasm-compiled-runtime.wasm',
145
+ wrapper: 'generated/pulse/pulse-wrapper.cjs'
146
+ }
147
+ });
148
+ const devRuntime = normalizeArtifact({
149
+ version: PULSE_DEV_RUNTIME_VERSION,
150
+ generatedBy,
151
+ phase: PHASE,
152
+ engine: deploymentPosture.engine || 'wasm',
153
+ handlerExecutionMode: 'compiled-wasm',
154
+ nodeAdapter: true,
155
+ compiledWasmRuntime: true,
156
+ jsUserHandlerImports: false,
157
+ noAutomaticFallback: true,
158
+ summary: {
159
+ requestPathProven: diagnostics.length === 0,
160
+ schemaRouteProven: smoke.checks.some((check) => check.name === 'schema_success_status' && check.passed),
161
+ extensionRouteProven: smoke.checks.some((check) => check.name === 'extension_status' && check.passed),
162
+ streamRouteProven: smoke.checks.some((check) => check.name === 'stream_kind' && check.passed)
163
+ }
164
+ });
165
+ const artifact = normalizeArtifact({
166
+ version: WRAPPER_INTEGRATION_VERSION,
167
+ generatedBy,
168
+ phase: PHASE,
169
+ status: diagnostics.length === 0 ? 'ok' : 'error',
170
+ policy: {
171
+ wrapperIsThin: true,
172
+ consumesArtifacts: true,
173
+ redefinesSemantics: false,
174
+ platformAdapter: false,
175
+ automaticJsFallback: false,
176
+ configContract: 'root topology + profile runtime settings'
177
+ },
178
+ config: {
179
+ version: resolvedConfig.version,
180
+ entry: resolvedConfig.entry,
181
+ rootRouter: resolvedConfig.rootRouter,
182
+ profile: resolvedConfig.profile,
183
+ runtime: resolvedConfig.runtime,
184
+ selectedProfileFieldPresent: Object.prototype.hasOwnProperty.call(resolvedConfig, 'selectedProfile'),
185
+ profileSourceFieldPresent: Object.prototype.hasOwnProperty.call(resolvedConfig, 'profileSource')
186
+ },
187
+ wrapper: {
188
+ file: 'generated/pulse/pulse-wrapper.cjs',
189
+ exports: ['defineConfig', 'createPulseBuildOutput', 'createPulseDevRuntime', 'createPulseNodeHandler']
190
+ },
191
+ smoke,
192
+ summary: {
193
+ smokeChecks: smoke.checks.length,
194
+ failedSmokeChecks: smoke.failedChecks.length,
195
+ profile: resolvedConfig.profile,
196
+ engine: deploymentPosture.engine || 'wasm',
197
+ handlerExecutionMode: resolvedConfig.runtime?.handlerExecutionMode || 'compiled-wasm',
198
+ compiledWasmRuntime: Boolean(compiledWasmRuntime?.artifact),
199
+ nodeAdapterPath: true,
200
+ noJsHandlerImports: true,
201
+ automaticFallback: false
202
+ },
203
+ diagnostics
204
+ });
205
+ return {
206
+ artifact,
207
+ buildOutput,
208
+ devRuntime,
209
+ files: [
210
+ makeFile('generated/pulse/pulse-wrapper.cjs', wrapperSource),
211
+ makeFile('generated/host/pulse-wrapper-integration.md', buildMarkdown(artifact))
212
+ ],
213
+ diagnostics
214
+ };
215
+ }
216
+
217
+ module.exports = { buildPulseWrapperIntegration };
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('@pulse-compute/wasm-host-runtime/compiler/request-result-headers');