@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.
- package/README.md +53 -1
- package/bin/provider-proof-composition.js +34 -0
- package/bin/pulsewasm-extract.js +20 -0
- package/package.json +60 -5
- package/src/artifacts-dir.js +13 -0
- package/src/ast-json.js +52 -0
- package/src/build-manifest.js +354 -0
- package/src/canonical-api-compiler.js +269 -0
- package/src/canonical-native-compiler.js +411 -0
- package/src/canonical-native-plan.js +1478 -0
- package/src/canonical-project-compiler.js +1224 -0
- package/src/canonical-router-compiler.js +25 -0
- package/src/cli-intents.js +1235 -0
- package/src/cli.js +1927 -0
- package/src/codegen/assemblyscript-compile.js +3 -0
- package/src/codegen/assemblyscript-core.js +3 -0
- package/src/codegen/assemblyscript-shape.js +3 -0
- package/src/codegen/assemblyscript-wasm-smoke.js +3 -0
- package/src/codegen/backend-capabilities.js +3 -0
- package/src/codegen/channel-broadcaster.js +3 -0
- package/src/codegen/compiled-handlers.js +3 -0
- package/src/codegen/compiled-wasm-runtime.js +3 -0
- package/src/codegen/config-references.js +248 -0
- package/src/codegen/dispatch-ts.js +145 -0
- package/src/codegen/effect-composition.js +331 -0
- package/src/codegen/effect-runtime.js +418 -0
- package/src/codegen/execution-harness-ts.js +467 -0
- package/src/codegen/handler-bindings-ts.js +298 -0
- package/src/codegen/handler-library-contracts.js +3 -0
- package/src/codegen/host-capabilities.js +3 -0
- package/src/codegen/host-runtime-kernel.js +3 -0
- package/src/codegen/integrated-compiled-app.js +3 -0
- package/src/codegen/json-body.js +3 -0
- package/src/codegen/library-sidecars.js +3 -0
- package/src/codegen/local-harness-ts.js +453 -0
- package/src/codegen/pulse-wrapper.js +217 -0
- package/src/codegen/request-result-headers.js +3 -0
- package/src/codegen/schema-json-compile.js +3 -0
- package/src/codegen/schema-json-sidecar-v2.js +3 -0
- package/src/codegen/schema-json-sidecar.js +3 -0
- package/src/codegen/streaming-passthrough.js +3 -0
- package/src/codegen/wasm-host-abi.js +3 -0
- package/src/codegen/wasm-host-bridge.js +3 -0
- package/src/compiled-wasm-host-runtime-kv.js +3 -0
- package/src/config-resolver.js +813 -0
- package/src/crypto-requirement-planner.js +89 -0
- package/src/definitions/config-schema.js +14 -0
- package/src/definitions/handler-roles.js +14 -0
- package/src/definitions/path-grammar.js +14 -0
- package/src/definitions/router-api.js +14 -0
- package/src/diagnostics/codes.js +14 -0
- package/src/diagnostics/reporter.js +14 -0
- package/src/diagnostics.js +14 -0
- package/src/dispatch-table.js +400 -0
- package/src/events/event-emit.js +265 -0
- package/src/events/event-topology.js +127 -0
- package/src/execution-plan.js +463 -0
- package/src/extractor.js +2816 -0
- package/src/handler-eval.js +1154 -0
- package/src/handler-table.js +326 -0
- package/src/index.js +19 -0
- package/src/javascript-application-plan.js +181 -0
- package/src/kv-provider.js +3 -0
- package/src/path-table.js +60 -0
- package/src/path.js +14 -0
- package/src/patterns/config-define.js +30 -0
- package/src/patterns/dependency-call.js +18 -0
- package/src/patterns/env-lookup.js +13 -0
- package/src/patterns/handler-reference.js +29 -0
- package/src/patterns/path-literal.js +35 -0
- package/src/patterns/result.js +15 -0
- package/src/patterns/router-chain-call.js +50 -0
- package/src/patterns/router-construction.js +19 -0
- package/src/project/package-reachability.js +782 -0
- package/src/project/reachable-graph-builder.js +992 -0
- package/src/project/reachable-graph-contract.js +54 -0
- package/src/project/reachable-graph-implementation.js +36 -0
- package/src/project/router-module-linker.js +710 -0
- package/src/project-config-compiler.js +222 -0
- package/src/project-target-support.js +500 -0
- package/src/provider-toolchain.js +299 -0
- package/src/spine/async-surface-normalizer.js +328 -0
- package/src/spine/canonical-handler-ir.js +336 -0
- package/src/spine/canonical-native-module.js +87 -0
- package/src/spine/canonical-native-plan.js +89 -0
- package/src/spine/canonical-project.js +76 -0
- package/src/spine/canonical-router.js +155 -0
- package/src/spine/canonical-source.js +165 -0
- package/src/spine/diagnostic-authority.js +290 -0
- package/src/spine/equivalence.js +262 -0
- package/src/spine/guest-unit-stage.js +87 -0
- package/src/spine/handler-ir-emitter.js +455 -0
- package/src/spine/handler-ir-managed.js +1598 -0
- package/src/spine/handler-ir.js +797 -0
- package/src/spine/handler-surface-authority.js +588 -0
- package/src/spine/package-operation-seam.js +1015 -0
- package/src/spine/pipeline.js +202 -0
- package/src/spine/plain-handler-frontend.js +545 -0
- package/src/spine/provider-requirement-authority.js +208 -0
- package/src/spine/router-control-contract.js +17 -0
- package/src/spine/router-handler-frontend.js +514 -0
- package/src/spine/router-handler-ir.js +372 -0
- package/src/spine/router-topology-frontend.js +638 -0
- 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 };
|