@pulse-compute/wasm-compiler 1.0.0-beta.5 → 1.0.0-beta.7

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.
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const ts = require('typescript');
4
+ const values = require('../pure-helper-values');
5
+
6
+ // Admission only; lowering remains in Handler IR and the canonical plan.
7
+ function validatePureHelperSource(helper) {
8
+ const fn = helper.functionNode, source = helper.sourceFile, diagnostics = [];
9
+ const scalar = node => node && values.scalar(ts.tokenToString(node.kind));
10
+ helper.parameterTypes = fn.parameters.map(p => helper.resolveType?.(p.type) || (scalar(p.type) ? p.type.getText(source) : undefined));
11
+ function fail(node, reason, message) {
12
+ const pos = source.getLineAndCharacterOfPosition(node.getStart(source));
13
+ diagnostics.push({ code: `PULSE_NATIVE_PURE_HELPER_${reason}_UNSUPPORTED`, severity: 'error', message,
14
+ file: helper.source.file, position: { line: pos.line + 1, column: pos.character + 1 } });
15
+ }
16
+ if (!ts.isFunctionDeclaration(fn) || fn.asteriskToken || fn.typeParameters?.length || !fn.body || !ts.isBlock(fn.body)
17
+ || fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) || !scalar(fn.type)
18
+ || fn.parameters.some(p => !ts.isIdentifier(p.name) || p.name.text === 'ctx' || !helper.parameterTypes[fn.parameters.indexOf(p)] || p.initializer || p.dotDotDotToken || p.questionToken)
19
+ || new Set(fn.parameters.map(p => p.name.getText(source))).size !== fn.parameters.length) {
20
+ fail(fn, 'SIGNATURE', 'Pure helpers require synchronous function declarations with explicit bounded scalar/record/number-array parameters and a scalar result.');
21
+ return diagnostics;
22
+ }
23
+ // Function declarations are mutable bindings in JS; reject writes anywhere in
24
+ // the defining module. Caller shadowing is checked at the resolved call site.
25
+ function assignments(node) {
26
+ const target = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
27
+ ? node.left : (ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) ? node.operand : undefined;
28
+ if (target && ts.isIdentifier(target) && target.text === fn.name.text) fail(node, 'IDENTITY', 'Pure helper identities cannot be reassigned.');
29
+ ts.forEachChild(node, assignments);
30
+ }
31
+ assignments(source);
32
+ const params = new Set(fn.parameters.map(p => p.name.text));
33
+ const names = new Set(params);
34
+ function collect(node) {
35
+ if (node !== fn && ts.isFunctionLike(node)) { fail(node, 'CAPTURE', 'Nested functions and closures are outside pure helpers.'); return; }
36
+ if (ts.isVariableDeclaration(node)) {
37
+ if (!ts.isIdentifier(node.name) || names.has(node.name.text)) fail(node, 'LOCAL', 'Pure helper locals must have unique identifier bindings.');
38
+ else names.add(node.name.text);
39
+ }
40
+ ts.forEachChild(node, collect);
41
+ }
42
+ collect(fn.body);
43
+ function inspect(node) {
44
+ if (ts.isCallExpression(node)) { fail(node, 'CALL', 'Pure helpers cannot call helpers, callbacks, methods, effects or Router operations.'); return; }
45
+ if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isThrowStatement(node) || ts.isTryStatement(node)) {
46
+ fail(node, 'CONTROL', 'Async operations and exception syntax are outside pure helpers.'); return;
47
+ }
48
+ if (ts.isPropertyAccessExpression(node)) {
49
+ if (node.questionDotToken) fail(node, 'SHAPE', 'Optional access is outside pure helpers.');
50
+ inspect(node.expression); return;
51
+ }
52
+ if (ts.isElementAccessExpression(node)) {
53
+ if (node.questionDotToken) fail(node, 'SHAPE', 'Optional access is outside pure helpers.');
54
+ inspect(node.expression); inspect(node.argumentExpression); return;
55
+ }
56
+ if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)
57
+ || ts.isNewExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node)) {
58
+ fail(node, 'VALUE', 'Pure helpers cannot allocate structured values or use type assertions.'); return;
59
+ }
60
+ if (ts.isIdentifier(node) && !names.has(node.text)) fail(node, 'CAPTURE', `Pure helper cannot capture ${node.text}.`);
61
+ const target = ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
62
+ ? node.left : ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) ? node.operand : undefined;
63
+ if (target && (!ts.isIdentifier(target) || params.has(target.text))) fail(node, 'MUTATION', 'Pure helper parameters are read-only.');
64
+ if (ts.isVariableDeclaration(node)) {
65
+ if (!node.initializer || node.type && !scalar(node.type) && !helper.resolveType?.(node.type)) fail(node, 'LOCAL', 'Pure helper locals require supported initializers.');
66
+ if (node.initializer) inspect(node.initializer);
67
+ return;
68
+ }
69
+ ts.forEachChild(node, inspect);
70
+ }
71
+ inspect(fn.body);
72
+ return diagnostics;
73
+ }
74
+ // Resolve only structural declarations in the already contained project graph.
75
+ // No TypeScript checker widening, ambient types, package lookup or runtime import.
76
+ function resolvePureType(context, module, node, seen = new Set(), depth = 0) {
77
+ if (!node || depth > values.BORROW_LIMITS.recordDepth) return;
78
+ if (values.scalar(ts.tokenToString(node.kind))) return node.getText(module.sourceFile);
79
+ if (ts.isArrayTypeNode(node) && node.elementType.kind === ts.SyntaxKind.NumberKeyword) return { kind: 'number-array' };
80
+ if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && !node.typeArguments?.length) {
81
+ const name = node.typeName.text, key = module.path + '#' + name;
82
+ if (seen.has(key)) return;
83
+ const next = new Set([...seen, key]);
84
+ const declarations = module.sourceFile.statements.filter(s => (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) && s.name.text === name);
85
+ if (declarations.length === 1) {
86
+ const decl = declarations[0];
87
+ if (decl.typeParameters?.length || decl.heritageClauses?.length) return;
88
+ return resolvePureType(context, module, ts.isTypeAliasDeclaration(decl) ? decl.type : decl, next, depth);
89
+ }
90
+ for (const relation of module.imports) {
91
+ const binding = relation.bindings.find(b => b.localName === name);
92
+ if (!binding || binding.importedName === '*') continue;
93
+ const resolution = module.resolutions.find(r => r.relation === relation);
94
+ const target = resolution && context.projectModules.get(resolution.targetKey);
95
+ if (!target) return;
96
+ const exported = target.sourceFile.statements.find(s => (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) && s.name.text === binding.importedName && s.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword));
97
+ const targetName = target.localExports.get(binding.importedName) || exported?.name.text;
98
+ if (!targetName) return;
99
+ const decl = target.sourceFile.statements.find(s => (ts.isInterfaceDeclaration(s) || ts.isTypeAliasDeclaration(s)) && s.name.text === targetName);
100
+ if (!decl || decl.typeParameters?.length || decl.heritageClauses?.length) return;
101
+ return resolvePureType(context, target, ts.isTypeAliasDeclaration(decl) ? decl.type : decl, next, depth);
102
+ }
103
+ return;
104
+ }
105
+ if ((ts.isInterfaceDeclaration(node) || ts.isTypeLiteralNode(node)) && depth < values.BORROW_LIMITS.recordDepth) {
106
+ const fields = [];
107
+ for (const member of node.members) {
108
+ if (!ts.isPropertySignature(member) || !ts.isIdentifier(member.name) || member.questionToken || member.initializer) return;
109
+ const name = member.name.text;
110
+ if (values.FORBIDDEN_FIELDS.includes(name) || fields.some(f => f.name === name)) return;
111
+ const type = resolvePureType(context, module, member.type, seen, depth + 1);
112
+ if (!type) return;
113
+ fields.push({ name, type });
114
+ }
115
+ if (fields.length && fields.length <= values.BORROW_LIMITS.recordFields) return { kind: 'record', fields };
116
+ }
117
+ }
118
+ function typeNode(type) {
119
+ if (typeof type === 'string') return ts.factory.createKeywordTypeNode({string:ts.SyntaxKind.StringKeyword,number:ts.SyntaxKind.NumberKeyword,boolean:ts.SyntaxKind.BooleanKeyword}[type]);
120
+ if (type.kind === 'number-array') return ts.factory.createArrayTypeNode(typeNode('number'));
121
+ return ts.factory.createTypeLiteralNode(type.fields.map(f => ts.factory.createPropertySignature(undefined, f.name, undefined, typeNode(f.type))));
122
+ }
123
+ module.exports = { validatePureHelperSource, resolvePureType, typeNode };
@@ -130,6 +130,7 @@ function analysisForSurfaceFacts(surfaceFacts) {
130
130
  const capabilities = new Set();
131
131
  let fetchCount = 0;
132
132
  for (const fact of facts) {
133
+ if (fact.surfaceId === 'ctx.req.body') { capabilities.add('request.body.forward'); continue; }
133
134
  if (fact.surfaceId.startsWith('ctx.fetch.')) { capabilities.add('fetch'); fetchCount += 1; continue; }
134
135
  if (fact.surfaceId.startsWith('ctx.req.')) capabilities.add(fact.surfaceId.slice('ctx.'.length));
135
136
  else if (fact.surfaceId.startsWith('ctx.config.')) capabilities.add('config.get');
@@ -197,6 +198,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
197
198
  handlerAuthoring: topology.options && topology.options.handlerAuthoring,
198
199
  requireAsync: topology.options && topology.options.requireAsync === true,
199
200
  requireEffectAwait: topology.options && topology.options.requireEffectAwait === true,
201
+ helperForCall: topology.options?.linkedProjectModules?.helperForCall,
200
202
  packageEffectForCall: topology.options && topology.options.packageEffectForCall
201
203
  || ((call) => packageEffectsByStart.get(call.getStart(sourceFile)))
202
204
  });
@@ -230,7 +232,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
230
232
  }
231
233
  const contextSurface = recognizeHandlerSurface(node, { ctxName, unwrap: true });
232
234
  if (contextSurface) {
233
- const httpOnly = contextSurface.surfaceId === 'ctx.param'
235
+ const httpOnly = contextSurface.surfaceId.startsWith('ctx.output.') || contextSurface.surfaceId === 'ctx.param'
234
236
  || contextSurface.surfaceId.startsWith('ctx.req.')
235
237
  || ['ctx.json', 'ctx.text', 'ctx.response'].includes(contextSurface.surfaceId);
236
238
  const eventOnly = contextSurface.surfaceId.startsWith('ctx.event.');
@@ -258,6 +260,41 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
258
260
 
259
261
  const rewriteTransformer = (context) => {
260
262
  const visit = (node) => {
263
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)
264
+ && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === ctxName
265
+ && node.expression.name.text === 'kv' && node.typeArguments?.length === 1) {
266
+ const type = topology.options?.linkedProjectModules?.pureTypeForNode?.(node.typeArguments[0]);
267
+ if (type) return ts.factory.updateCallExpression(node, node.expression,
268
+ [require('./pure-helper-source').typeNode(type)], node.arguments.map(arg => ts.visitNode(arg, visit)));
269
+ }
270
+ const helper = ts.isCallExpression(node) && topology.options?.linkedProjectModules?.helperForCall?.(node);
271
+ if (helper) {
272
+ const original = ts.getOriginalNode(node);
273
+ const awaited = original.parent && ts.isAwaitExpression(original.parent);
274
+ const binding = awaited && original.parent.parent;
275
+ let shadowed = false;
276
+ const inspect = item => {
277
+ if ((ts.isVariableDeclaration(item) || ts.isParameter(item) || ts.isFunctionDeclaration(item)) && ts.isIdentifier(item.name) && item.name.text === original.expression.text) shadowed = true;
278
+ ts.forEachChild(item, inspect);
279
+ };
280
+ inspect(authoredFunctionNode);
281
+ if (helper.pure) {
282
+ if (awaited || shadowed || node.typeArguments?.length || node.questionDotToken
283
+ || node.arguments.length !== helper.functionNode.parameters.length || !['route', 'middleware'].includes(role)) {
284
+ diagnostics.push(diagnostic(sourceFile, original, 'PULSE_NATIVE_PURE_HELPER_CALL_UNSUPPORTED',
285
+ 'Pure helpers require a static unshadowed synchronous pure call in an HTTP handler.'));
286
+ }
287
+ return ts.factory.updateCallExpression(node, ts.factory.createIdentifier(helper.name), undefined,
288
+ node.arguments.map(arg => ts.visitNode(arg, visit)));
289
+ }
290
+ if (!binding || !ts.isVariableDeclaration(binding) || !ts.isIdentifier(binding.name)
291
+ || shadowed || node.arguments.length !== helper.functionNode.parameters.length
292
+ || !ts.isIdentifier(node.arguments[0]) || node.arguments[0].text !== ctxName
293
+ || !['route', 'middleware'].includes(role)) {
294
+ diagnostics.push(diagnostic(sourceFile, original, 'PULSE_NATIVE_HELPER_CALL_UNSUPPORTED', 'A source helper requires an unshadowed, directly awaited local binding and the current request context in an HTTP handler.'));
295
+ }
296
+ return ts.factory.updateCallExpression(node, ts.factory.createIdentifier(helper.name), undefined, node.arguments.slice(1));
297
+ }
261
298
  if (errorName && ts.isShorthandPropertyAssignment(node) && node.name.text === errorName) {
262
299
  return ts.factory.createPropertyAssignment(ts.factory.createIdentifier(errorName), ts.factory.createIdentifier(ERROR));
263
300
  }
@@ -1,4 +1,7 @@
1
1
  'use strict';
2
+ const ts = require('typescript');
3
+ const { normalizeManagedHandler } = require('./async-surface-normalizer');
4
+ const { CanonicalRouterCompileError } = require('./router-topology-frontend');
2
5
 
3
6
  const {
4
7
  buildRouterHandlerIr
@@ -142,15 +145,73 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
142
145
  const topology = prepared.topology;
143
146
  const operationByEntry = new Map();
144
147
  const emittedByEntry = new Map();
148
+ const bodyByEntry = new Map();
149
+ const authoredHandlers = new Map(topology.handlerTable.handlers.map(handler => [handler.id, handler]));
145
150
  for (const handler of prepared.handlers) {
146
151
  const operationIr = buildRouterHandlerIr(handler);
147
152
  operationByEntry.set(handler.entry.stableId, operationIr);
148
153
  emittedByEntry.set(handler.entry.stableId, emitCanonicalRouterHandlerBody(operationIr));
154
+ // The first private body family is a terminal HTTP route. Transfer-capable
155
+ // handlers retain the existing cursor/mode lowering until their own pass.
156
+ if (handler.entry.kind === 'route' && !operationIr.summary.operationKinds['router-transfer']) {
157
+ const authored = authoredHandlers.get(handler.entry.handlerId);
158
+ bodyByEntry.set(handler.entry.stableId, Object.freeze({
159
+ version: 'pulse.router-native-body.v1',
160
+ family: 'terminal-route',
161
+ name: `__pulse_body_${handler.entry.stableId}`,
162
+ source: authored && authored.loc
163
+ }));
164
+ }
149
165
  }
150
166
 
151
167
  const header = topology.retainedDeclarations;
152
168
  let synthetic = header ? `${header}\n\n` : '';
153
169
  synthetic += 'export default function __pulse_router_entry(ctx) {\n';
170
+ const helpers = [];
171
+ const helperRecords = [];
172
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
173
+ for (const helper of topology.options?.linkedProjectModules?.sourceHelpers || []) {
174
+ const fn = helper.functionNode;
175
+ if (helper.pure) {
176
+ const diagnostics = require('./pure-helper-source').validatePureHelperSource(helper);
177
+ if (diagnostics.length) throw new CanonicalRouterCompileError('Unsupported pure source helper.', diagnostics);
178
+ const start = synthetic.length;
179
+ synthetic += `function ${helper.name}(${fn.parameters.map(p => printer.printNode(ts.EmitHint.Unspecified, p, helper.sourceFile)).join(',')}) ${printer.printNode(ts.EmitHint.Unspecified, fn.body, helper.sourceFile)}\n`;
180
+ const generatedRange = { start, end: synthetic.length };
181
+ helpers.push({ id: helper.id, name: helper.name, pure: true, source: helper.source,
182
+ resultKind: fn.type.getText(helper.sourceFile),
183
+ parameters: fn.parameters.map((p, i) => { const type = helper.parameterTypes[i]; return { name: p.name.text, valueKind: typeof type === 'string' ? type : type.kind === 'record' ? 'object' : 'array', ...(typeof type === 'object' ? { borrow: { version: require('@pulse-compute/wasm-contracts/handler/canonical-native-plan').CANONICAL_NATIVE_PURE_BORROW_VERSION, type } } : {}) }; }), generatedRange });
184
+ helperRecords.push({ entryStableId: helper.id, operationIr: { sourceFile: helper.sourceFile, handler: fn },
185
+ canonicalIr: { router: { entry: { generatedRange } } } });
186
+ continue;
187
+ }
188
+ const declaration = fn.parent;
189
+ const immutable = !ts.isVariableDeclaration(declaration) || Boolean(declaration.parent.flags & ts.NodeFlags.Const);
190
+ let reassigned = false;
191
+ const scanAssignments = node => {
192
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
193
+ && ts.isIdentifier(node.left) && node.left.text === helper.source.name) reassigned = true;
194
+ ts.forEachChild(node, scanAssignments);
195
+ };
196
+ scanAssignments(helper.sourceFile);
197
+ const valid = immutable && !reassigned && ts.isBlock(fn.body) && fn.parameters[0]?.name?.text === 'ctx'
198
+ && fn.parameters.every((p, i) => ts.isIdentifier(p.name) && !p.initializer && !p.dotDotDotToken && !p.questionToken
199
+ && (i === 0 || [ts.SyntaxKind.StringKeyword, ts.SyntaxKind.NumberKeyword, ts.SyntaxKind.BooleanKeyword].includes(p.type?.kind)))
200
+ && new Set(fn.parameters.map(p => p.name.text)).size === fn.parameters.length
201
+ && fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
202
+ if (!valid) throw new CanonicalRouterCompileError('Unsupported source helper signature.', [{ code: 'PULSE_NATIVE_HELPER_SIGNATURE_UNSUPPORTED', severity: 'error', message: 'Helpers require async block bodies, ctx first and explicit scalar inputs.', file: helper.source.file }]);
203
+ const normalized = normalizeManagedHandler(fn, { sourceFile: helper.sourceFile, ctxName: 'ctx', frontend: 'canonical-router',
204
+ target: 'native', requireAsync: true, packageEffectForCall: topology.options.packageEffectForCall });
205
+ if (normalized.diagnostics.length) throw new CanonicalRouterCompileError('Unsupported source helper body.', normalized.diagnostics);
206
+ const start = synthetic.length;
207
+ const parameters = fn.parameters.slice(1).map(p => printer.printNode(ts.EmitHint.Unspecified, p, helper.sourceFile)).join(',');
208
+ synthetic += `function ${helper.name}(${parameters}) ${printer.printNode(ts.EmitHint.Unspecified, normalized.functionNode.body, helper.sourceFile)}\n`;
209
+ const generatedRange = { start, end: synthetic.length };
210
+ helpers.push({ id: helper.id, name: helper.name, source: helper.source,
211
+ parameters: fn.parameters.slice(1).map(p => ({ name: p.name.text, valueKind: p.type.getText(helper.sourceFile) })), generatedRange });
212
+ helperRecords.push({ entryStableId: helper.id, operationIr: { sourceFile: helper.sourceFile, handler: fn },
213
+ canonicalIr: { router: { entry: { generatedRange } } } });
214
+ }
154
215
 
155
216
  const eventReachable = Boolean(topology.eventTopology && topology.eventCatalog && topology.eventCatalog.events.length > 0);
156
217
  const eventTopology = realizedEventTopology(topology, prepared);
@@ -198,7 +259,9 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
198
259
 
199
260
  if (entry.kind === 'mount') {
200
261
  const pattern = rawEntry.pattern && rawEntry.pattern.normalized || `${rawEntry.path || '/'}/*`;
201
- synthetic += ` if (${MODE} === 0 && __pulse_router_match(ctx.req.path, ${JSON.stringify(pattern)})) {\n`;
262
+ const eligibility = rawEntry.eligibility;
263
+ const condition = eligibility ? ` && ctx.state.get(${JSON.stringify(eligibility.state)}) === ${JSON.stringify(eligibility.equals)}` : '';
264
+ synthetic += ` if (${MODE} === 0 && __pulse_router_match(ctx.req.path, ${JSON.stringify(pattern)})${condition}) {\n`;
202
265
  synthetic += ` ${CURSOR} = ${rawEntry.childStartIndex};\n`;
203
266
  synthetic += ' } else {\n';
204
267
  synthetic += ` ${CURSOR} = ${rawEntry.parentContinueIndex};\n`;
@@ -213,10 +276,13 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
213
276
  }
214
277
  if (entry.kind === 'route') matchCondition += ` && ctx.req.method === ${JSON.stringify(entry.method)} && __pulse_router_match(ctx.req.path, ${JSON.stringify(entry.path)})`;
215
278
  synthetic += ` if (${matchCondition}) {\n`;
279
+ const nativeBody = bodyByEntry.get(entry.stableId);
280
+ if (nativeBody) synthetic += ` function ${nativeBody.name}() {\n`;
216
281
  const start = synthetic.length;
217
- synthetic += `${indent(emitted.sourceText, 6)}\n`;
282
+ synthetic += `${indent(emitted.sourceText, nativeBody ? 8 : 6)}\n`;
218
283
  const end = synthetic.length;
219
284
  generatedRange = Object.freeze({ start, end });
285
+ if (nativeBody) synthetic += ` }\n return ${nativeBody.name}();\n`;
220
286
  synthetic += ' } else {\n';
221
287
  synthetic += ` ${CURSOR} = ${entry.nextIndex};\n`;
222
288
  synthetic += ' }\n';
@@ -227,6 +293,7 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
227
293
 
228
294
  const record = Object.freeze({
229
295
  ...entry,
296
+ ...(bodyByEntry.has(entry.stableId) ? { nativeBody: bodyByEntry.get(entry.stableId) } : {}),
230
297
  ...(eventReachable ? { plane: 'http' } : {}),
231
298
  generatedRange,
232
299
  generatedBlockRange: Object.freeze({ start: openingStart, end: synthetic.length })
@@ -354,6 +421,7 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
354
421
  }),
355
422
  routes: Object.freeze(routeRecords),
356
423
  entries: Object.freeze(entryRecords),
424
+ ...(helpers.length ? { helpers: Object.freeze(helpers) } : {}),
357
425
  ...(eventReachable ? { applicationEntries: Object.freeze(entryRecords) } : {})
358
426
  }),
359
427
  diagnostics: Object.freeze([])
@@ -362,7 +430,7 @@ function emitCanonicalRouterFromHandlerIrs(prepared) {
362
430
  return Object.freeze({
363
431
  version: ROUTER_HANDLER_IR_BUNDLE_VERSION,
364
432
  output,
365
- handlers: Object.freeze(handlerRecords)
433
+ handlers: Object.freeze([...helperRecords, ...handlerRecords])
366
434
  });
367
435
  }
368
436
 
@@ -188,7 +188,8 @@ function stableEntryId(entry) {
188
188
  method: entry.method,
189
189
  path: entry.path,
190
190
  handlerId: entry.handler && (entry.handler.id || entry.handler.handlerId),
191
- childRouter: entry.childRouter
191
+ childRouter: entry.childRouter,
192
+ ...(entry.eligibility ? { eligibility: entry.eligibility } : {})
192
193
  });
193
194
  return `router_entry_${crypto.createHash('sha256').update(input).digest('hex').slice(0, 24)}`;
194
195
  }
@@ -233,6 +234,7 @@ function cleanExecutionEntry(entry) {
233
234
  routeRuntimeId: Number.isInteger(entry.runtimeId) ? entry.runtimeId : undefined,
234
235
  handlerId: entry.handler && (entry.handler.id || entry.handler.handlerId) || undefined,
235
236
  childRouter: entry.childRouter || undefined,
237
+ ...(entry.eligibility ? { eligibility: Object.freeze({ ...entry.eligibility }) } : {}),
236
238
  childStartIndex: Number.isInteger(entry.childStartIndex) ? entry.childStartIndex : undefined,
237
239
  parentContinueIndex: Number.isInteger(entry.parentContinueIndex) ? entry.parentContinueIndex : undefined
238
240
  });
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { findLowerableManifestRecord } = require('@pulse-compute/wasm-library-kit/compiler/package-lowering');
4
+ const { loadLowerableCompilerBuilder } = require('@pulse-compute/wasm-library-kit/compiler/handler-library-contracts');
5
+ const { compileManagedHandlerNativeBundle } = require('./handler-ir-managed.js');
6
+ const applications = new WeakMap();
7
+
8
+ function attachTerminalPackageNative(compiled, inputs) {
9
+ applications.set(compiled, inputs);
10
+ }
11
+
12
+ function terminalPackageNativeForCompiled(compiled) {
13
+ const inputs = applications.get(compiled);
14
+ if (!inputs) return null;
15
+ const { application, recognition, managedHandlers, schemaBundle, cwd, workspaceRoot } = inputs;
16
+ const selected = recognition.packages.find(entry => entry.contractId === application.contractId);
17
+ if (!selected || selected.builder.trust !== 'first-party') throw new TypeError('Native application requires its selected trusted lowerer.');
18
+ const { record } = findLowerableManifestRecord({ cwd, workspaceRoot, contractId: selected.contractId, npmPackage: selected.package });
19
+ const exportName = record?.manifest?.compiler?.nativeApplicationExport;
20
+ if (!exportName) return null;
21
+ const loaded = loadLowerableCompilerBuilder(record);
22
+ const builder = Object.hasOwn(loaded.module, exportName) ? loaded.module[exportName] : null;
23
+ if (typeof builder !== 'function') throw new TypeError('Trusted Native application builder is missing.');
24
+ const intrinsic = recognition.intrinsics.find(entry => entry.intrinsic === application.intrinsic);
25
+ const result = builder({
26
+ plan: intrinsic.staticArguments[0],
27
+ managedHandlerNativeBundle: compileManagedHandlerNativeBundle(managedHandlers),
28
+ schemaBundle
29
+ });
30
+ if (result.version !== 'pulse.package-native-application.v1' || result.contractId !== selected.contractId
31
+ || result.package !== selected.package || result.intrinsic !== application.intrinsic || result.automaticFallback !== false) {
32
+ throw new TypeError('Trusted Native application result does not match its selected package.');
33
+ }
34
+ return result;
35
+ }
36
+
37
+ module.exports = { attachTerminalPackageNative, terminalPackageNativeForCompiled };