@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,1478 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const ts = require('typescript');
7
+ const { executeCanonicalNativePlanSpine } = require('./spine/canonical-native-plan.js');
8
+
9
+ function loadNativePlanContract() {
10
+ try { return require('@pulse-compute/wasm-contracts/handler/canonical-native-plan'); }
11
+ catch (error) {
12
+ if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
13
+ return require('../../contracts/src/handler/canonical-native-plan.js');
14
+ }
15
+ throw error;
16
+ }
17
+ }
18
+
19
+ function loadLoggingContract() {
20
+ try { return require('@pulse-compute/wasm-contracts/logging'); }
21
+ catch (error) {
22
+ if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
23
+ return require('../../contracts/src/logging.js');
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ function loadCryptoContract() {
30
+ try { return require('@pulse-compute/wasm-contracts/crypto/contracts'); }
31
+ catch (error) {
32
+ if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
33
+ return require('../../contracts/src/crypto/contracts.js');
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+
39
+ function loadEventContract() {
40
+ try { return require('@pulse-compute/wasm-contracts/events'); }
41
+ catch (error) {
42
+ if (error && ['MODULE_NOT_FOUND', 'ERR_PACKAGE_PATH_NOT_EXPORTED'].includes(error.code)) {
43
+ return require('../../contracts/src/events/contracts.js');
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ const contract = loadNativePlanContract();
50
+ const loggingContract = loadLoggingContract();
51
+ const cryptoContract = loadCryptoContract();
52
+ const eventContract = loadEventContract();
53
+ const CANONICAL_NATIVE_PLAN_COMPILER_VERSION = 'pulse.canonical-native-plan-compiler.v2';
54
+ const GENERATED_HANDLER_NAME = '__pulse_handler';
55
+ const PULSE_RUNTIME_PARAMETER = '__pulse';
56
+
57
+ const ASSIGNMENT_OPERATORS = new Set([
58
+ '=', '+=', '-=', '*=', '/=', '%=', '**=', '&&=', '||=', '??=', '&=', '|=', '^=', '<<=', '>>=', '>>>='
59
+ ]);
60
+ const PURE_BINARY_OPERATORS = new Set([
61
+ '===', '!==', '==', '!=', '<', '<=', '>', '>=', '+', '-', '*', '/', '%', '**',
62
+ '&&', '||', '??', '&', '|', '^', '<<', '>>', '>>>', 'in'
63
+ ]);
64
+ const PREFIX_OPERATORS = new Set(['!', '+', '-', '~', 'typeof', 'void']);
65
+ const UPDATE_OPERATORS = new Set(['++', '--']);
66
+ const FETCH_RESPONSE_METHODS = new Set(['json', 'text', 'header']);
67
+ const COMPILER_OWNED_INTRINSICS = Object.freeze({
68
+ __pulse_event_runtime_id: Object.freeze(['event.runtime-id', 'number']),
69
+ __pulse_router_match: Object.freeze(['router.match', 'boolean']),
70
+ __pulse_router_param: Object.freeze(['router.param', 'string-or-undefined'])
71
+ });
72
+ const EFFECT_STATIC_FIELDS = new Set([
73
+ 'id', 'kind', 'source', 'package', 'contractId', 'providerKind', 'operation', 'capability', 'result'
74
+ ]);
75
+
76
+ class CanonicalNativePlanError extends Error {
77
+ constructor(message, diagnostics = []) {
78
+ super(message);
79
+ this.name = 'CanonicalNativePlanError';
80
+ this.code = 'PULSE_CANONICAL_NATIVE_PLAN_FAILED';
81
+ this.diagnostics = Object.freeze([...diagnostics]);
82
+ }
83
+ }
84
+
85
+ function stableHash(value) {
86
+ return crypto.createHash(contract.CANONICAL_NATIVE_PLAN_HASH_ALGORITHM).update(String(value)).digest('hex');
87
+ }
88
+
89
+ function stableObject(value) {
90
+ if (Array.isArray(value)) return value.map(stableObject);
91
+ if (!value || typeof value !== 'object') return value;
92
+ const out = {};
93
+ for (const key of Object.keys(value).sort()) {
94
+ if (value[key] !== undefined) out[key] = stableObject(value[key]);
95
+ }
96
+ return out;
97
+ }
98
+
99
+ function stableStringify(value, space = 0) {
100
+ return JSON.stringify(stableObject(value), null, space);
101
+ }
102
+
103
+ function cloneJson(value) {
104
+ if (value === undefined) return undefined;
105
+ return JSON.parse(JSON.stringify(value));
106
+ }
107
+
108
+ function deepFreeze(value) {
109
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
110
+ for (const item of Object.values(value)) deepFreeze(item);
111
+ return Object.freeze(value);
112
+ }
113
+
114
+ function sourcePosition(sourceFile, node) {
115
+ const offset = node && typeof node.getStart === 'function' ? node.getStart(sourceFile) : 0;
116
+ const position = sourceFile.getLineAndCharacterOfPosition(offset);
117
+ return Object.freeze({ line: position.line + 1, column: position.character + 1, offset });
118
+ }
119
+
120
+ function diagnostic(sourceFile, node, code, message, detail = {}) {
121
+ return Object.freeze({
122
+ code,
123
+ kind: 'CanonicalNativePlanDiagnostic',
124
+ severity: 'error',
125
+ message,
126
+ file: sourceFile ? sourceFile.fileName : undefined,
127
+ position: sourceFile ? sourcePosition(sourceFile, node || sourceFile) : undefined,
128
+ detail: Object.freeze({ ...detail })
129
+ });
130
+ }
131
+
132
+ function unwrap(node) {
133
+ let current = node;
134
+ while (current && (
135
+ ts.isParenthesizedExpression(current)
136
+ || ts.isAsExpression(current)
137
+ || ts.isNonNullExpression(current)
138
+ || ts.isTypeAssertionExpression(current)
139
+ || ts.isPartiallyEmittedExpression(current)
140
+ )) current = current.expression;
141
+ return current;
142
+ }
143
+
144
+ function literalString(node) {
145
+ const current = unwrap(node);
146
+ return current && (ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current)) ? current.text : undefined;
147
+ }
148
+
149
+ function propertyName(sourceFile, name) {
150
+ if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return String(name.text);
151
+ if (ts.isComputedPropertyName(name)) return undefined;
152
+ return name ? name.getText(sourceFile) : undefined;
153
+ }
154
+
155
+ function declarationKind(statement) {
156
+ const flags = statement.declarationList.flags;
157
+ if ((flags & ts.NodeFlags.Const) !== 0) return 'const';
158
+ if ((flags & ts.NodeFlags.Let) !== 0) return 'let';
159
+ return 'var';
160
+ }
161
+
162
+ function formatStatementPath(parts) {
163
+ let out = String(parts[0] || 'entry');
164
+ for (const part of parts.slice(1)) {
165
+ if (typeof part === 'number') out += `[${part}]`;
166
+ else out += `.${part}`;
167
+ }
168
+ return out;
169
+ }
170
+
171
+ function generatedHandler(sourceFile) {
172
+ return sourceFile.statements.filter((statement) => (
173
+ ts.isFunctionDeclaration(statement)
174
+ && statement.name
175
+ && statement.name.text === GENERATED_HANDLER_NAME
176
+ ));
177
+ }
178
+
179
+ function contextPath(node, ctxName) {
180
+ const parts = [];
181
+ let current = unwrap(node);
182
+ while (current && ts.isPropertyAccessExpression(current)) {
183
+ parts.unshift(current.name.text);
184
+ current = unwrap(current.expression);
185
+ }
186
+ if (current && ts.isIdentifier(current) && current.text === ctxName) return parts;
187
+ return undefined;
188
+ }
189
+
190
+ function contextValueKind(parts) {
191
+ const key = parts.join('.');
192
+ if (['req.method', 'req.url', 'req.path'].includes(key)) return 'string';
193
+ if (key === 'req.headers') return 'headers';
194
+ if (key === 'event.payload') return 'json';
195
+ return 'unknown';
196
+ }
197
+
198
+ function intrinsicForContextCall(parts) {
199
+ const key = parts.join('.');
200
+ const entries = {
201
+ 'req.header': ['request.header', 'string-or-undefined'],
202
+ 'req.text': ['request.text', 'string'],
203
+ 'req.json': ['request.json', 'json'],
204
+ json: ['response.json', 'pulse-result'],
205
+ text: ['response.text', 'pulse-result'],
206
+ response: ['response.custom', 'pulse-result'],
207
+ kv: ['kv.namespace', 'kv-namespace'],
208
+ 'state.get': ['state.get', 'string-or-undefined'],
209
+ 'state.set': ['state.set', 'undefined']
210
+ };
211
+ return entries[key];
212
+ }
213
+
214
+ function resultKindForEffect(site, decoder, continuation, resultMode) {
215
+ if (decoder && decoder.kind === 'json') return 'json';
216
+ if (decoder && decoder.kind === 'text') return 'string';
217
+ if (resultMode === 'return' && continuation && continuation.kind === 'opaque-fetch-return') return 'opaque-response';
218
+ if (site.result === 'opaque-response') return 'opaque-response';
219
+ if (site.result === 'structured-response') return 'structured-response';
220
+ if (site.result === 'ack') return 'ack';
221
+ if (site.kind === 'fetch') return 'fetch-response';
222
+ if (site.kind === 'config.get' || site.kind === 'secret.get') return 'string-or-undefined';
223
+ if (site.kind === 'kv.get') return 'json-or-undefined';
224
+ if (site.kind === 'kv.put') return 'ack';
225
+ if (['kv.getVersioned', 'kv.insertIfAbsent', 'kv.compareAndSwap'].includes(site.kind)) return 'json';
226
+ if (site.kind === 'event.emit') return 'ack';
227
+ return 'unknown';
228
+ }
229
+
230
+ function inferBinaryValueKind(operator, left, right) {
231
+ if (['===', '!==', '==', '!=', '<', '<=', '>', '>=', 'in'].includes(operator)) return 'boolean';
232
+ if (['&&', '||', '??'].includes(operator)) {
233
+ if (left.valueKind === right.valueKind) return left.valueKind;
234
+ if (['||', '??'].includes(operator)) {
235
+ const kinds = new Set([left.valueKind, right.valueKind]);
236
+ if (kinds.has('string') && kinds.has('string-or-undefined')) return 'string';
237
+ }
238
+ return 'unknown';
239
+ }
240
+ if (operator === '+' && (left.valueKind === 'string' || right.valueKind === 'string')) return 'string';
241
+ if (['+', '-', '*', '/', '%', '**', '&', '|', '^', '<<', '>>', '>>>'].includes(operator)) return 'number';
242
+ return 'unknown';
243
+ }
244
+
245
+ class NativePlanBuilder {
246
+ constructor(compiled, options = {}) {
247
+ this.compiled = compiled;
248
+ this.options = options;
249
+ this.diagnostics = [];
250
+ this.effects = [];
251
+ this.locals = [];
252
+ this.effectOccurrences = new Map();
253
+ this.continuationOccurrences = new Map();
254
+ this.localIndex = 0;
255
+ this.effectOrder = 0;
256
+ this.reporting = loggingContract.reportingDescriptor(options.reporting);
257
+ this.enabledLogCount = 0;
258
+ this.prunedLogCount = 0;
259
+ this.summary = {
260
+ statementCount: 0,
261
+ expressionCount: 0,
262
+ localCount: 0,
263
+ branchCount: 0,
264
+ returnCount: 0,
265
+ effectCount: 0,
266
+ effectGroupCount: 0,
267
+ continuationCount: 0,
268
+ maxStatementDepth: 0
269
+ };
270
+
271
+ if (!compiled || compiled.ok !== true || !compiled.metadata || typeof compiled.generatedSource !== 'string') {
272
+ const code = contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.COMPILED_PROGRAM_REQUIRED;
273
+ throw new CanonicalNativePlanError('A successful canonical compilation is required to build a native plan.', [
274
+ diagnostic(undefined, undefined, code, 'Expected compileCanonicalSource/compileCanonicalProject output with metadata and generatedSource.')
275
+ ]);
276
+ }
277
+
278
+ if (compiled.nativeEligibility && compiled.nativeEligibility.eligible !== true) {
279
+ const diagnostics = (compiled.nativeEligibility.blockers || []).map((blocker) => Object.freeze({
280
+ code: blocker.code,
281
+ kind: 'CanonicalNativePlanDiagnostic',
282
+ severity: 'error',
283
+ message: blocker.message,
284
+ file: blocker.source && blocker.source.file,
285
+ position: blocker.source ? Object.freeze({ line: blocker.source.line, column: blocker.source.column }) : undefined,
286
+ detail: Object.freeze({
287
+ blockerId: blocker.id,
288
+ blockerKind: blocker.kind,
289
+ moduleId: blocker.moduleId,
290
+ handlerIds: blocker.handlerIds,
291
+ packageName: blocker.packageName,
292
+ packageSubpath: blocker.packageSubpath,
293
+ contractId: blocker.contractId,
294
+ specifier: blocker.specifier,
295
+ automaticFallback: false
296
+ })
297
+ }));
298
+ throw new CanonicalNativePlanError(
299
+ `The canonical project is not eligible for native-plan lowering (${diagnostics.length} blocker${diagnostics.length === 1 ? '' : 's'}).`,
300
+ diagnostics
301
+ );
302
+ }
303
+
304
+ const eventCatalog = compiled.eventCatalog || compiled.metadata && compiled.metadata.events && compiled.metadata.events.catalog;
305
+ const nativeIneligibleEvents = eventCatalog && Array.isArray(eventCatalog.events)
306
+ ? eventCatalog.events.filter((entry) => entry && entry.eligibility && entry.eligibility.native !== true)
307
+ : [];
308
+ if (nativeIneligibleEvents.length > 0) {
309
+ throw new CanonicalNativePlanError(
310
+ `The canonical event catalog contains ${nativeIneligibleEvents.length} event handler${nativeIneligibleEvents.length === 1 ? '' : 's'} not eligible for Native lowering.`,
311
+ nativeIneligibleEvents.map((entry) => diagnostic(
312
+ undefined,
313
+ undefined,
314
+ contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EVENT_HANDLER_INELIGIBLE,
315
+ `Event ${JSON.stringify(entry.type)} is not eligible for Native lowering in this implementation pass.`,
316
+ {
317
+ eventStableId: entry.stableId,
318
+ eventRuntimeId: entry.runtimeId,
319
+ handlerStableId: entry.handlerStableId,
320
+ capabilities: entry.capabilities,
321
+ automaticFallback: false
322
+ }
323
+ ))
324
+ );
325
+ }
326
+
327
+ this.metadata = compiled.metadata;
328
+ this.generatedFile = `${this.metadata.file || 'app.ts'}.canonical.generated.js`;
329
+ this.sourceFile = ts.createSourceFile(
330
+ this.generatedFile,
331
+ compiled.generatedSource,
332
+ ts.ScriptTarget.ES2022,
333
+ true,
334
+ ts.ScriptKind.JS
335
+ );
336
+ const handlers = generatedHandler(this.sourceFile);
337
+ if (handlers.length === 0) {
338
+ this.fail(this.sourceFile, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.GENERATED_HANDLER_MISSING, `Generated canonical program does not contain ${GENERATED_HANDLER_NAME}.`);
339
+ this.handler = undefined;
340
+ } else {
341
+ if (handlers.length > 1) this.fail(handlers[1], contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.GENERATED_HANDLER_DUPLICATE, `Generated canonical program contains more than one ${GENERATED_HANDLER_NAME}.`);
342
+ this.handler = handlers[0];
343
+ }
344
+ this.ctxName = this.handler && this.handler.parameters[0] && ts.isIdentifier(this.handler.parameters[0].name)
345
+ ? this.handler.parameters[0].name.text
346
+ : String(this.metadata.ctxParameter || 'ctx');
347
+ this.effectSites = new Map((this.metadata.effectSites || []).map((site) => [String(site.id), site]));
348
+ this.continuationSites = new Map((this.metadata.continuationSites || []).map((site) => [String(site.id), site]));
349
+ this.compilerOwnedCalls = new Set((this.metadata.compilerOwnedCalls || []).map(String));
350
+ this.compilerOwnedIntrinsics = new Map(Object.entries(COMPILER_OWNED_INTRINSICS));
351
+ for (const entry of this.metadata.compilerOwnedIntrinsics || []) {
352
+ if (!entry || !entry.compilerName || !entry.intrinsic) continue;
353
+ this.compilerOwnedIntrinsics.set(String(entry.compilerName), Object.freeze([
354
+ String(entry.intrinsic),
355
+ String(entry.valueKind || 'unknown')
356
+ ]));
357
+ }
358
+ }
359
+
360
+ fail(node, code, message, detail = {}) {
361
+ this.diagnostics.push(diagnostic(this.sourceFile, node, code, message, detail));
362
+ }
363
+
364
+ expression(node, scope) {
365
+ const current = unwrap(node);
366
+ this.summary.expressionCount += 1;
367
+ if (!current) return Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
368
+
369
+ if (ts.isStringLiteral(current) || ts.isNoSubstitutionTemplateLiteral(current)) {
370
+ return Object.freeze({ kind: 'literal', value: current.text, valueKind: 'string' });
371
+ }
372
+ if (ts.isNumericLiteral(current)) {
373
+ return Object.freeze({ kind: 'literal', value: Number(current.text), valueKind: 'number' });
374
+ }
375
+ if (current.kind === ts.SyntaxKind.TrueKeyword || current.kind === ts.SyntaxKind.FalseKeyword) {
376
+ return Object.freeze({ kind: 'literal', value: current.kind === ts.SyntaxKind.TrueKeyword, valueKind: 'boolean' });
377
+ }
378
+ if (current.kind === ts.SyntaxKind.NullKeyword) return Object.freeze({ kind: 'literal', value: null, valueKind: 'null' });
379
+
380
+ if (ts.isIdentifier(current)) {
381
+ if (current.text === 'undefined') return Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
382
+ if (current.text === this.ctxName) return Object.freeze({ kind: 'context-read', path: Object.freeze([]), valueKind: 'unknown' });
383
+ const local = scope.get(current.text);
384
+ if (!local) {
385
+ this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.LOCAL_UNRESOLVED, `Generated canonical expression references unresolved local ${current.text}.`, { local: current.text });
386
+ return Object.freeze({ kind: 'local', id: `unresolved:${current.text}`, name: current.text, valueKind: 'unknown' });
387
+ }
388
+ return Object.freeze({ kind: 'local', id: local.id, name: local.name, valueKind: local.valueKind });
389
+ }
390
+
391
+ if (ts.isArrayLiteralExpression(current)) {
392
+ const items = current.elements.map((item) => {
393
+ if (ts.isSpreadElement(item)) return Object.freeze({ kind: 'spread', value: this.expression(item.expression, scope), valueKind: 'unknown' });
394
+ if (ts.isOmittedExpression(item)) return Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
395
+ return this.expression(item, scope);
396
+ });
397
+ return Object.freeze({ kind: 'array', items: Object.freeze(items), valueKind: 'array' });
398
+ }
399
+
400
+ if (ts.isObjectLiteralExpression(current)) {
401
+ const entries = [];
402
+ for (const property of current.properties) {
403
+ if (ts.isSpreadAssignment(property)) {
404
+ entries.push(Object.freeze({ kind: 'spread', value: this.expression(property.expression, scope) }));
405
+ continue;
406
+ }
407
+ if (ts.isShorthandPropertyAssignment(property)) {
408
+ const local = scope.get(property.name.text);
409
+ if (!local) {
410
+ this.fail(property, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.LOCAL_UNRESOLVED, `Object shorthand references unresolved local ${property.name.text}.`, { local: property.name.text });
411
+ continue;
412
+ }
413
+ entries.push(Object.freeze({
414
+ kind: 'property',
415
+ key: Object.freeze({ kind: 'literal', value: property.name.text }),
416
+ value: Object.freeze({ kind: 'local', id: local.id, name: local.name, valueKind: local.valueKind })
417
+ }));
418
+ continue;
419
+ }
420
+ if (ts.isPropertyAssignment(property)) {
421
+ const computed = ts.isComputedPropertyName(property.name);
422
+ const key = computed
423
+ ? Object.freeze({ kind: 'computed', value: this.expression(property.name.expression, scope) })
424
+ : Object.freeze({ kind: 'literal', value: propertyName(this.sourceFile, property.name) });
425
+ entries.push(Object.freeze({ kind: 'property', key, value: this.expression(property.initializer, scope) }));
426
+ continue;
427
+ }
428
+ this.fail(property, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Object methods and accessors are outside the canonical native value model.', { syntax: ts.SyntaxKind[property.kind] });
429
+ }
430
+ return Object.freeze({ kind: 'object', entries: Object.freeze(entries), valueKind: 'object' });
431
+ }
432
+
433
+ if (ts.isTemplateExpression(current)) {
434
+ const parts = [Object.freeze({ kind: 'text', value: current.head.text })];
435
+ for (const span of current.templateSpans) {
436
+ parts.push(Object.freeze({ kind: 'value', value: this.expression(span.expression, scope) }));
437
+ parts.push(Object.freeze({ kind: 'text', value: span.literal.text }));
438
+ }
439
+ return Object.freeze({ kind: 'template', parts: Object.freeze(parts), valueKind: 'string' });
440
+ }
441
+
442
+ if (ts.isBinaryExpression(current)) {
443
+ const operator = current.operatorToken.getText(this.sourceFile);
444
+ if (ASSIGNMENT_OPERATORS.has(operator)) {
445
+ const target = this.expression(current.left, scope);
446
+ const value = this.expression(current.right, scope);
447
+ return Object.freeze({ kind: 'assignment', operator, target, value, valueKind: value.valueKind || 'unknown' });
448
+ }
449
+ if (!PURE_BINARY_OPERATORS.has(operator)) {
450
+ this.fail(current.operatorToken, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Binary operator ${operator} is outside the canonical native value model.`, { operator });
451
+ }
452
+ const left = this.expression(current.left, scope);
453
+ const right = this.expression(current.right, scope);
454
+ return Object.freeze({ kind: 'binary', operator, left, right, valueKind: inferBinaryValueKind(operator, left, right) });
455
+ }
456
+
457
+ if (ts.isPrefixUnaryExpression(current)) {
458
+ const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(0, 1);
459
+ if (UPDATE_OPERATORS.has(operator)) {
460
+ return Object.freeze({ kind: 'update', operator, prefix: true, target: this.expression(current.operand, scope), valueKind: 'number' });
461
+ }
462
+ if (!PREFIX_OPERATORS.has(operator)) {
463
+ this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Prefix operator ${operator} is outside the canonical native value model.`, { operator });
464
+ }
465
+ const value = this.expression(current.operand, scope);
466
+ return Object.freeze({ kind: 'unary', operator, value, valueKind: operator === '!' ? 'boolean' : (operator === 'typeof' ? 'string' : value.valueKind || 'unknown') });
467
+ }
468
+
469
+ if (ts.isPostfixUnaryExpression(current)) {
470
+ const operator = ts.tokenToString(current.operator) || current.getText(this.sourceFile).slice(-2);
471
+ if (!UPDATE_OPERATORS.has(operator)) this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Postfix operator ${operator} is outside the canonical native value model.`, { operator });
472
+ return Object.freeze({ kind: 'update', operator, prefix: false, target: this.expression(current.operand, scope), valueKind: 'number' });
473
+ }
474
+
475
+ if (ts.isConditionalExpression(current)) {
476
+ const whenTrue = this.expression(current.whenTrue, scope);
477
+ const whenFalse = this.expression(current.whenFalse, scope);
478
+ return Object.freeze({
479
+ kind: 'conditional',
480
+ test: this.expression(current.condition, scope),
481
+ whenTrue,
482
+ whenFalse,
483
+ valueKind: whenTrue.valueKind === whenFalse.valueKind ? whenTrue.valueKind : 'unknown'
484
+ });
485
+ }
486
+
487
+ if (ts.isPropertyAccessExpression(current)) {
488
+ if (current.questionDotToken) {
489
+ this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Optional property access is outside the canonical native value model.');
490
+ }
491
+ const ctxPath = contextPath(current, this.ctxName);
492
+ const eventPayloadMember = ctxPath
493
+ && ctxPath.length > 2
494
+ && ctxPath[0] === 'event'
495
+ && ctxPath[1] === 'payload';
496
+ if (ctxPath && !eventPayloadMember) {
497
+ const key = ctxPath.join('.');
498
+ if (!contract.CANONICAL_NATIVE_CONTEXT_READS.includes(key)) {
499
+ this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Context read ctx.${key} is not part of the canonical native context contract.`, { path: ctxPath });
500
+ }
501
+ return Object.freeze({ kind: 'context-read', path: Object.freeze(ctxPath), valueKind: contextValueKind(ctxPath) });
502
+ }
503
+ const object = this.expression(current.expression, scope);
504
+ let valueKind = 'unknown';
505
+ if (object.valueKind === 'fetch-response') {
506
+ if (current.name.text === 'status') valueKind = 'number';
507
+ else if (current.name.text === 'ok') valueKind = 'boolean';
508
+ else if (current.name.text === 'headers') valueKind = 'headers';
509
+ else this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Fetch response property .${current.name.text} is outside the canonical native response contract.`, { property: current.name.text });
510
+ }
511
+ return Object.freeze({ kind: 'property', object, property: current.name.text, valueKind });
512
+ }
513
+
514
+ if (ts.isElementAccessExpression(current)) {
515
+ if (current.questionDotToken) this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Optional element access is outside the canonical native value model.');
516
+ return Object.freeze({
517
+ kind: 'element',
518
+ object: this.expression(current.expression, scope),
519
+ index: this.expression(current.argumentExpression, scope),
520
+ valueKind: 'unknown'
521
+ });
522
+ }
523
+
524
+ if (ts.isTypeOfExpression(current)) {
525
+ return Object.freeze({ kind: 'unary', operator: 'typeof', value: this.expression(current.expression, scope), valueKind: 'string' });
526
+ }
527
+
528
+ if (ts.isVoidExpression(current)) {
529
+ return Object.freeze({ kind: 'unary', operator: 'void', value: this.expression(current.expression, scope), valueKind: 'undefined' });
530
+ }
531
+
532
+ if (ts.isCallExpression(current)) return this.callExpression(current, scope);
533
+
534
+ if (ts.isSpreadElement(current)) return Object.freeze({ kind: 'spread', value: this.expression(current.expression, scope), valueKind: 'unknown' });
535
+
536
+ if (ts.isYieldExpression(current)) {
537
+ this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'A generated yield must be consumed as an explicit native effect statement.');
538
+ return Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
539
+ }
540
+
541
+ this.fail(current, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Generated expression ${ts.SyntaxKind[current.kind]} is outside the canonical native value model.`, { syntax: ts.SyntaxKind[current.kind] });
542
+ return Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
543
+ }
544
+
545
+ callExpression(call, scope) {
546
+ if (call.questionDotToken) this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Optional calls are outside the canonical native value model.');
547
+ const target = unwrap(call.expression);
548
+ const ctxPath = contextPath(target, this.ctxName);
549
+ if (ctxPath) {
550
+ const intrinsic = intrinsicForContextCall(ctxPath);
551
+ if (!intrinsic) {
552
+ this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Context call ctx.${ctxPath.join('.')} is not part of the canonical native intrinsic set.`, { path: ctxPath });
553
+ return Object.freeze({ kind: 'intrinsic', name: `unsupported:${ctxPath.join('.')}`, arguments: Object.freeze([]), valueKind: 'unknown' });
554
+ }
555
+ const argumentsArray = call.arguments.map((argument) => this.expression(argument, scope));
556
+ if (['state.get', 'state.set'].includes(intrinsic[0])) {
557
+ const expected = intrinsic[0] === 'state.get' ? 1 : 2;
558
+ if (call.arguments.length !== expected) {
559
+ this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `${intrinsic[0]} requires exactly ${expected} argument(s).`, { intrinsic: intrinsic[0], argumentCount: call.arguments.length });
560
+ }
561
+ if (argumentsArray[0] && argumentsArray[0].valueKind !== 'string') {
562
+ this.fail(call.arguments[0] || call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `${intrinsic[0]} requires a string key.`, { intrinsic: intrinsic[0], argument: 'key', valueKind: argumentsArray[0].valueKind });
563
+ }
564
+ if (intrinsic[0] === 'state.set' && argumentsArray[1] && argumentsArray[1].valueKind !== 'string') {
565
+ this.fail(call.arguments[1] || call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'state.set requires a string value.', { intrinsic: intrinsic[0], argument: 'value', valueKind: argumentsArray[1].valueKind });
566
+ }
567
+ }
568
+ return Object.freeze({
569
+ kind: 'intrinsic',
570
+ name: intrinsic[0],
571
+ arguments: Object.freeze(argumentsArray),
572
+ valueKind: intrinsic[1]
573
+ });
574
+ }
575
+
576
+ if (ts.isIdentifier(target) && this.compilerOwnedCalls.has(target.text) && this.compilerOwnedIntrinsics.has(target.text)) {
577
+ const intrinsic = this.compilerOwnedIntrinsics.get(target.text);
578
+ return Object.freeze({
579
+ kind: 'intrinsic',
580
+ name: intrinsic[0],
581
+ arguments: Object.freeze(call.arguments.map((argument) => this.expression(argument, scope))),
582
+ valueKind: intrinsic[1]
583
+ });
584
+ }
585
+
586
+ if (ts.isPropertyAccessExpression(target)) {
587
+ const receiver = this.expression(target.expression, scope);
588
+ const method = target.name.text;
589
+ if (!FETCH_RESPONSE_METHODS.has(method) || receiver.valueKind !== 'fetch-response') {
590
+ this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, `Method call .${method}() is outside the canonical native fetch-response contract.`, { method, receiverKind: receiver.valueKind });
591
+ }
592
+ let valueKind = 'unknown';
593
+ if (method === 'json') valueKind = 'json';
594
+ else if (method === 'text') valueKind = 'string';
595
+ else if (method === 'header') valueKind = 'string-or-undefined';
596
+ return Object.freeze({
597
+ kind: 'method-call',
598
+ receiver,
599
+ method,
600
+ arguments: Object.freeze(call.arguments.map((argument) => this.expression(argument, scope))),
601
+ valueKind
602
+ });
603
+ }
604
+
605
+ this.fail(call, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EXPRESSION_UNSUPPORTED, 'Direct function calls are outside the canonical native value model.', { callee: target.getText(this.sourceFile) });
606
+ return Object.freeze({ kind: 'intrinsic', name: 'unsupported:call', arguments: Object.freeze([]), valueKind: 'unknown' });
607
+ }
608
+
609
+ allocateLocal(name, valueKind, statementPath, declaration) {
610
+ this.localIndex += 1;
611
+ const local = Object.freeze({
612
+ id: `local-${this.localIndex}`,
613
+ name: String(name),
614
+ valueKind: contract.CANONICAL_NATIVE_VALUE_KINDS.includes(valueKind) ? valueKind : 'unknown',
615
+ declaration,
616
+ statementPath
617
+ });
618
+ this.locals.push(local);
619
+ this.summary.localCount += 1;
620
+ return local;
621
+ }
622
+
623
+ pulseYield(expression) {
624
+ let current = unwrap(expression);
625
+ let decoder;
626
+ if (current && ts.isCallExpression(current) && ts.isPropertyAccessExpression(unwrap(current.expression))) {
627
+ const target = unwrap(current.expression);
628
+ const receiver = unwrap(target.expression);
629
+ if (receiver && ts.isYieldExpression(receiver) && ['json', 'text'].includes(target.name.text)) {
630
+ decoder = Object.freeze({
631
+ kind: target.name.text,
632
+ arguments: Object.freeze(current.arguments.map((argument) => argument))
633
+ });
634
+ current = receiver;
635
+ }
636
+ }
637
+ if (!current || !ts.isYieldExpression(current) || !current.expression) return undefined;
638
+ const call = unwrap(current.expression);
639
+ if (!call || !ts.isCallExpression(call) || !ts.isPropertyAccessExpression(unwrap(call.expression))) return undefined;
640
+ const target = unwrap(call.expression);
641
+ if (!ts.isIdentifier(unwrap(target.expression)) || unwrap(target.expression).text !== PULSE_RUNTIME_PARAMETER) return undefined;
642
+ if (!['effect', 'group'].includes(target.name.text)) return undefined;
643
+ return Object.freeze({ mode: target.name.text, call, decoder });
644
+ }
645
+
646
+ markerFields(marker) {
647
+ if (!marker || !ts.isObjectLiteralExpression(unwrap(marker))) {
648
+ this.fail(marker || this.handler, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Canonical native effects require an object-literal marker.');
649
+ return new Map();
650
+ }
651
+ const fields = new Map();
652
+ for (const property of unwrap(marker).properties) {
653
+ if (!ts.isPropertyAssignment(property)) {
654
+ this.fail(property, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Canonical native effect markers may contain only static property assignments.');
655
+ continue;
656
+ }
657
+ const name = propertyName(this.sourceFile, property.name);
658
+ if (!name || fields.has(name)) {
659
+ this.fail(property, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Canonical native effect marker fields must have unique static names.', { name });
660
+ continue;
661
+ }
662
+ fields.set(name, property.initializer);
663
+ }
664
+ return fields;
665
+ }
666
+
667
+ prepareEffect(marker, continuationId, scope, statementPath, result, groupIndex) {
668
+ const fields = this.markerFields(marker);
669
+ const effectId = literalString(fields.get('id'));
670
+ const kind = literalString(fields.get('kind'));
671
+ const site = effectId ? this.effectSites.get(effectId) : undefined;
672
+ const continuation = this.continuationSites.get(continuationId);
673
+ if (!effectId || !kind || !site) {
674
+ this.fail(marker, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Generated effect marker must identify a known canonical effect site.', { effectId, kind, continuationId });
675
+ } else if (String(site.kind) !== kind) {
676
+ this.fail(marker, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_MISMATCH, `Generated effect ${effectId} kind ${kind} does not match canonical metadata ${site.kind}.`, { effectId, generatedKind: kind, metadataKind: site.kind });
677
+ }
678
+ if (!continuation || (effectId && !continuation.effectIds.includes(effectId))) {
679
+ this.fail(marker, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.CONTINUATION_MISMATCH, `Effect ${effectId || '<unknown>'} does not belong to continuation ${continuationId}.`, { effectId, continuationId });
680
+ }
681
+
682
+ const inputs = [];
683
+ for (const [name, value] of fields) {
684
+ if (EFFECT_STATIC_FIELDS.has(name)) continue;
685
+ inputs.push(Object.freeze({ name, value: this.expression(value, scope) }));
686
+ }
687
+ this.effectOrder += 1;
688
+ const record = Object.freeze({
689
+ order: this.effectOrder,
690
+ id: effectId || `invalid-effect-${this.effectOrder}`,
691
+ kind: kind || 'invalid',
692
+ providerKind: site && site.providerKind ? String(site.providerKind) : undefined,
693
+ operation: site && site.operation ? String(site.operation) : undefined,
694
+ capability: site && site.capability ? String(site.capability) : undefined,
695
+ grouped: Boolean(site && site.grouped),
696
+ groupIndex: Number.isInteger(groupIndex) ? groupIndex : undefined,
697
+ resource: cloneJson(site && site.resource),
698
+ package: site && site.package ? String(site.package) : undefined,
699
+ contractId: site && site.contractId ? String(site.contractId) : undefined,
700
+ declaredResult: site && site.result ? String(site.result) : undefined,
701
+ decoder: site && site.decoder ? String(site.decoder) : null,
702
+ continuationId,
703
+ statementPath,
704
+ source: Object.freeze({ file: this.metadata.file, ...(cloneJson(site && site.position) || {}) }),
705
+ routeStableId: site && site.routeStableId ? String(site.routeStableId) : undefined,
706
+ routeRuntimeId: site && Number.isInteger(site.routeRuntimeId) ? site.routeRuntimeId : undefined,
707
+ routeMethod: site && site.routeMethod ? String(site.routeMethod) : undefined,
708
+ routePath: site && site.routePath ? String(site.routePath) : undefined,
709
+ routerEntryStableId: site && site.routerEntryStableId ? String(site.routerEntryStableId) : undefined,
710
+ routerEntryKind: site && site.routerEntryKind ? String(site.routerEntryKind) : undefined,
711
+ routerEntryIndex: site && Number.isInteger(site.routerEntryIndex) ? site.routerEntryIndex : undefined,
712
+ routerEntryPath: site && site.routerEntryPath ? String(site.routerEntryPath) : undefined,
713
+ applicationEntryStableId: site && site.applicationEntryStableId ? String(site.applicationEntryStableId) : undefined,
714
+ applicationEntryKind: site && site.applicationEntryKind ? String(site.applicationEntryKind) : undefined,
715
+ applicationEntryPlane: site && site.applicationEntryPlane ? String(site.applicationEntryPlane) : undefined,
716
+ applicationEntryIndex: site && Number.isInteger(site.applicationEntryIndex) ? site.applicationEntryIndex : undefined,
717
+ eventStableId: site && site.eventStableId ? String(site.eventStableId) : undefined,
718
+ eventRuntimeId: site && Number.isInteger(site.eventRuntimeId) ? site.eventRuntimeId : undefined,
719
+ eventType: site && site.eventType ? String(site.eventType) : undefined,
720
+ eventSchemaId: site && site.eventSchemaId !== undefined ? site.eventSchemaId : undefined,
721
+ inputs: Object.freeze(inputs),
722
+ result
723
+ });
724
+ this.effects.push(record);
725
+ this.summary.effectCount += 1;
726
+ this.effectOccurrences.set(record.id, (this.effectOccurrences.get(record.id) || 0) + 1);
727
+ this.continuationOccurrences.set(continuationId, statementPath);
728
+ return record;
729
+ }
730
+
731
+ decoderPlan(decoder, scope) {
732
+ if (!decoder) return undefined;
733
+ return Object.freeze({
734
+ kind: decoder.kind,
735
+ arguments: Object.freeze(decoder.arguments.map((argument) => this.expression(argument, scope)))
736
+ });
737
+ }
738
+
739
+ lowerVariableStatement(statement, scope, pathParts, depth) {
740
+ const out = [];
741
+ const declaration = declarationKind(statement);
742
+ for (let index = 0; index < statement.declarationList.declarations.length; index += 1) {
743
+ const item = statement.declarationList.declarations[index];
744
+ const itemPath = formatStatementPath(statement.declarationList.declarations.length === 1 ? pathParts : [...pathParts, 'declaration', index]);
745
+ const yielded = item.initializer ? this.pulseYield(item.initializer) : undefined;
746
+
747
+ if (yielded && yielded.mode === 'group') {
748
+ if (!ts.isArrayBindingPattern(item.name) || statement.declarationList.declarations.length !== 1) {
749
+ this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.BINDING_UNSUPPORTED, 'Grouped canonical effects require one array binding declaration.');
750
+ continue;
751
+ }
752
+ const markersNode = yielded.call.arguments[0];
753
+ const continuationId = literalString(yielded.call.arguments[1]);
754
+ if (!markersNode || !ts.isArrayLiteralExpression(unwrap(markersNode)) || !continuationId) {
755
+ this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Grouped canonical effects require an effect-marker array and literal continuation ID.');
756
+ continue;
757
+ }
758
+ const markers = unwrap(markersNode).elements;
759
+ const bindings = item.name.elements;
760
+ if (markers.length !== bindings.length) {
761
+ this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_MISMATCH, 'Grouped effect marker and binding counts differ.', { markers: markers.length, bindings: bindings.length });
762
+ }
763
+ const results = [];
764
+ const prepared = [];
765
+ for (let groupIndex = 0; groupIndex < markers.length; groupIndex += 1) {
766
+ const binding = bindings[groupIndex];
767
+ if (!binding || ts.isOmittedExpression(binding) || !ts.isIdentifier(binding.name)) {
768
+ this.fail(binding || item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.BINDING_UNSUPPORTED, 'Grouped effect results require simple local bindings.', { groupIndex });
769
+ continue;
770
+ }
771
+ const markerFields = this.markerFields(markers[groupIndex]);
772
+ const effectId = literalString(markerFields.get('id'));
773
+ const site = effectId ? this.effectSites.get(effectId) : undefined;
774
+ const local = this.allocateLocal(binding.name.text, resultKindForEffect(site || {}, undefined), itemPath, declaration);
775
+ scope.set(local.name, local);
776
+ const result = Object.freeze({ mode: 'bind', localId: local.id, localName: local.name, valueKind: local.valueKind });
777
+ prepared.push(this.prepareEffect(markers[groupIndex], continuationId, scope, itemPath, result, groupIndex));
778
+ results.push(Object.freeze({ effectId: effectId || `invalid-effect-${groupIndex + 1}`, localId: local.id, localName: local.name, valueKind: local.valueKind }));
779
+ }
780
+ this.summary.effectGroupCount += 1;
781
+ out.push(Object.freeze({
782
+ kind: 'effect-group',
783
+ continuationId,
784
+ effectIds: Object.freeze(prepared.map((effect) => effect.id)),
785
+ results: Object.freeze(results),
786
+ statementPath: itemPath
787
+ }));
788
+ continue;
789
+ }
790
+
791
+ if (yielded && yielded.mode === 'effect') {
792
+ if (!ts.isIdentifier(item.name)) {
793
+ this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.BINDING_UNSUPPORTED, 'Canonical effect results require a simple local binding.');
794
+ continue;
795
+ }
796
+ const continuationId = literalString(yielded.call.arguments[1]);
797
+ const marker = yielded.call.arguments[0];
798
+ if (!continuationId || !marker) {
799
+ this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Canonical effect requires a marker and literal continuation ID.');
800
+ continue;
801
+ }
802
+ const fields = this.markerFields(marker);
803
+ const effectId = literalString(fields.get('id'));
804
+ const site = effectId ? this.effectSites.get(effectId) : undefined;
805
+ const decoder = this.decoderPlan(yielded.decoder, scope);
806
+ const local = this.allocateLocal(item.name.text, resultKindForEffect(site || {}, decoder), itemPath, declaration);
807
+ const result = Object.freeze({ mode: 'bind', localId: local.id, localName: local.name, valueKind: local.valueKind, decoder });
808
+ const effect = this.prepareEffect(marker, continuationId, scope, itemPath, result);
809
+ scope.set(local.name, local);
810
+ out.push(Object.freeze({ kind: 'effect', effectId: effect.id, continuationId, result, statementPath: itemPath }));
811
+ continue;
812
+ }
813
+
814
+ if (!ts.isIdentifier(item.name)) {
815
+ this.fail(item, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.BINDING_UNSUPPORTED, 'Canonical native locals require simple identifier bindings outside compiler-owned effect groups.', { binding: item.name.getText(this.sourceFile) });
816
+ continue;
817
+ }
818
+ const value = item.initializer ? this.expression(item.initializer, scope) : Object.freeze({ kind: 'undefined', valueKind: 'undefined' });
819
+ const local = this.allocateLocal(item.name.text, value.valueKind || 'unknown', itemPath, declaration);
820
+ scope.set(local.name, local);
821
+ out.push(Object.freeze({ kind: 'local', localId: local.id, name: local.name, declaration, valueKind: local.valueKind, value, statementPath: itemPath }));
822
+ }
823
+ return out;
824
+ }
825
+
826
+ lowerEffectStatement(expression, scope, statementPath, resultMode) {
827
+ const yielded = this.pulseYield(expression);
828
+ if (!yielded || yielded.mode !== 'effect') return undefined;
829
+ const continuationId = literalString(yielded.call.arguments[1]);
830
+ const marker = yielded.call.arguments[0];
831
+ if (!continuationId || !marker) {
832
+ this.fail(expression, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_INVALID, 'Canonical effect requires a marker and literal continuation ID.');
833
+ return undefined;
834
+ }
835
+ const fields = this.markerFields(marker);
836
+ const effectId = literalString(fields.get('id'));
837
+ const site = effectId ? this.effectSites.get(effectId) : undefined;
838
+ const continuation = this.continuationSites.get(continuationId);
839
+ const result = Object.freeze({
840
+ mode: resultMode,
841
+ valueKind: resultKindForEffect(site || {}, undefined, continuation, resultMode)
842
+ });
843
+ const effect = this.prepareEffect(marker, continuationId, scope, statementPath, result);
844
+ return Object.freeze({ kind: 'effect', effectId: effect.id, continuationId, result, statementPath });
845
+ }
846
+
847
+ lowerLogStatement(expression, scope, statementPath) {
848
+ const current = unwrap(expression);
849
+ if (!current || !ts.isCallExpression(current)) return undefined;
850
+ const parts = contextPath(current.expression, this.ctxName);
851
+ if (!parts || parts.length !== 2 || parts[0] !== 'log') return undefined;
852
+ const name = parts[1];
853
+ const level = loggingContract.LOG_METHOD_LEVELS[name];
854
+ if (!level) return undefined;
855
+ if (current.arguments.length !== 1) {
856
+ this.fail(
857
+ current,
858
+ contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.LOG_INVALID,
859
+ `ctx.log.${name} requires exactly one string message.`,
860
+ { level: name, arguments: current.arguments.length }
861
+ );
862
+ return [];
863
+ }
864
+ if (!loggingContract.logStatementEnabled(level, this.reporting.level)) {
865
+ this.prunedLogCount += 1;
866
+ return [];
867
+ }
868
+ const message = this.expression(current.arguments[0], scope);
869
+ if (message.valueKind !== 'string') {
870
+ this.fail(
871
+ current.arguments[0],
872
+ contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.LOG_INVALID,
873
+ `ctx.log.${name} requires a string message on the Native target.`,
874
+ { level: name, valueKind: message.valueKind }
875
+ );
876
+ return [];
877
+ }
878
+ this.enabledLogCount += 1;
879
+ return [Object.freeze({
880
+ kind: 'expression',
881
+ expression: Object.freeze({
882
+ kind: 'intrinsic',
883
+ name: 'logging.emit',
884
+ arguments: Object.freeze([
885
+ Object.freeze({ kind: 'literal', value: level, valueKind: 'number' }),
886
+ message
887
+ ]),
888
+ valueKind: 'undefined'
889
+ }),
890
+ statementPath
891
+ })];
892
+ }
893
+
894
+ statement(statement, scope, pathParts, depth) {
895
+ const statementPath = formatStatementPath(pathParts);
896
+ this.summary.maxStatementDepth = Math.max(this.summary.maxStatementDepth, depth);
897
+
898
+ if (ts.isBlock(statement)) return this.statementList(statement.statements, new Map(scope), [...pathParts, 'body'], depth + 1);
899
+
900
+ if (ts.isVariableStatement(statement)) return this.lowerVariableStatement(statement, scope, pathParts, depth);
901
+
902
+ if (ts.isIfStatement(statement)) {
903
+ this.summary.branchCount += 1;
904
+ const thenScope = new Map(scope);
905
+ const elseScope = new Map(scope);
906
+ const thenBody = ts.isBlock(statement.thenStatement)
907
+ ? this.statementList(statement.thenStatement.statements, thenScope, [...pathParts, 'then'], depth + 1)
908
+ : this.statement(statement.thenStatement, thenScope, [...pathParts, 'then', 0], depth + 1);
909
+ const elseBody = !statement.elseStatement
910
+ ? []
911
+ : ts.isBlock(statement.elseStatement)
912
+ ? this.statementList(statement.elseStatement.statements, elseScope, [...pathParts, 'else'], depth + 1)
913
+ : this.statement(statement.elseStatement, elseScope, [...pathParts, 'else', 0], depth + 1);
914
+ return [Object.freeze({
915
+ kind: 'if',
916
+ test: this.expression(statement.expression, scope),
917
+ then: Object.freeze(thenBody),
918
+ else: Object.freeze(elseBody),
919
+ statementPath
920
+ })];
921
+ }
922
+
923
+ if (ts.isReturnStatement(statement)) {
924
+ this.summary.returnCount += 1;
925
+ if (!statement.expression) return [Object.freeze({ kind: 'return', value: Object.freeze({ kind: 'undefined', valueKind: 'undefined' }), statementPath })];
926
+ const effect = this.lowerEffectStatement(statement.expression, scope, statementPath, 'return');
927
+ if (effect) return [effect];
928
+ return [Object.freeze({ kind: 'return', value: this.expression(statement.expression, scope), statementPath })];
929
+ }
930
+
931
+ if (ts.isExpressionStatement(statement)) {
932
+ const log = this.lowerLogStatement(statement.expression, scope, statementPath);
933
+ if (log) return log;
934
+ const effect = this.lowerEffectStatement(statement.expression, scope, statementPath, 'discard');
935
+ if (effect) return [effect];
936
+ return [Object.freeze({ kind: 'expression', expression: this.expression(statement.expression, scope), statementPath })];
937
+ }
938
+
939
+ if (ts.isEmptyStatement(statement)) return [];
940
+
941
+ this.fail(statement, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.STATEMENT_UNSUPPORTED, `Generated statement ${ts.SyntaxKind[statement.kind]} is outside the canonical native control-flow model.`, { syntax: ts.SyntaxKind[statement.kind] });
942
+ return [];
943
+ }
944
+
945
+ statementList(statements, scope, pathParts, depth) {
946
+ const out = [];
947
+ for (let index = 0; index < statements.length; index += 1) {
948
+ const lowered = this.statement(statements[index], scope, [...pathParts, index], depth);
949
+ for (const item of lowered) {
950
+ out.push(item);
951
+ this.summary.statementCount += 1;
952
+ }
953
+ }
954
+ return out;
955
+ }
956
+
957
+ reconcile() {
958
+ const expectedEffectIds = (this.metadata.effectSites || []).map((site) => String(site.id));
959
+ const actualEffectIds = this.effects.map((site) => site.id);
960
+ if (JSON.stringify(actualEffectIds) !== JSON.stringify(expectedEffectIds)) {
961
+ this.fail(this.handler || this.sourceFile, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_MISMATCH, 'Native-plan effect order does not match canonical compiler metadata.', { expectedEffectIds, actualEffectIds });
962
+ }
963
+ for (const effectId of expectedEffectIds) {
964
+ if (this.effectOccurrences.get(effectId) !== 1) this.fail(this.handler || this.sourceFile, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.EFFECT_MISMATCH, `Canonical effect ${effectId} must appear exactly once in the native plan.`, { effectId, occurrences: this.effectOccurrences.get(effectId) || 0 });
965
+ }
966
+ for (const site of this.metadata.continuationSites || []) {
967
+ if (!this.continuationOccurrences.has(String(site.id))) this.fail(this.handler || this.sourceFile, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.CONTINUATION_MISMATCH, `Canonical continuation ${site.id} is not represented in the native plan.`, { continuationId: site.id });
968
+ }
969
+ }
970
+
971
+ build() {
972
+ if (!this.handler || !this.handler.body) {
973
+ throw new CanonicalNativePlanError(`Native-plan lowering failed for ${this.metadata.file}.`, this.diagnostics);
974
+ }
975
+ const body = this.statementList(this.handler.body.statements, new Map(), ['entry', 'body'], 0);
976
+ this.reconcile();
977
+ if (this.diagnostics.length > 0) throw new CanonicalNativePlanError(`Native-plan lowering failed for ${this.metadata.file}.`, this.diagnostics);
978
+
979
+ const continuations = (this.metadata.continuationSites || []).map((site, index) => Object.freeze({
980
+ id: String(site.id),
981
+ kind: String(site.kind),
982
+ effectIds: Object.freeze(site.effectIds.map(String)),
983
+ stateIndex: index + 1,
984
+ statementPath: this.continuationOccurrences.get(String(site.id)),
985
+ source: Object.freeze({ file: this.metadata.file, ...(cloneJson(site.position) || {}) }),
986
+ routeStableId: site.routeStableId ? String(site.routeStableId) : undefined,
987
+ routeRuntimeId: Number.isInteger(site.routeRuntimeId) ? site.routeRuntimeId : undefined,
988
+ routeMethod: site.routeMethod ? String(site.routeMethod) : undefined,
989
+ routePath: site.routePath ? String(site.routePath) : undefined,
990
+ routerEntryStableId: site.routerEntryStableId ? String(site.routerEntryStableId) : undefined,
991
+ routerEntryKind: site.routerEntryKind ? String(site.routerEntryKind) : undefined,
992
+ routerEntryIndex: Number.isInteger(site.routerEntryIndex) ? site.routerEntryIndex : undefined,
993
+ routerEntryPath: site.routerEntryPath ? String(site.routerEntryPath) : undefined,
994
+ applicationEntryStableId: site.applicationEntryStableId ? String(site.applicationEntryStableId) : undefined,
995
+ applicationEntryKind: site.applicationEntryKind ? String(site.applicationEntryKind) : undefined,
996
+ applicationEntryPlane: site.applicationEntryPlane ? String(site.applicationEntryPlane) : undefined,
997
+ applicationEntryIndex: Number.isInteger(site.applicationEntryIndex) ? site.applicationEntryIndex : undefined,
998
+ eventStableId: site.eventStableId ? String(site.eventStableId) : undefined,
999
+ eventRuntimeId: Number.isInteger(site.eventRuntimeId) ? site.eventRuntimeId : undefined,
1000
+ eventType: site.eventType ? String(site.eventType) : undefined,
1001
+ eventSchemaId: site.eventSchemaId !== undefined ? site.eventSchemaId : undefined
1002
+ }));
1003
+ const states = [Object.freeze({ id: 'entry', kind: 'entry', stateIndex: 0 })]
1004
+ .concat(continuations.map((site) => Object.freeze({ id: site.id, kind: 'continuation', continuationKind: site.kind, effectIds: site.effectIds, stateIndex: site.stateIndex })));
1005
+ this.summary = summarizeNativePlan(body, this.locals, this.effects, continuations);
1006
+ const hasInboundEvents = Number(this.metadata.events && this.metadata.events.count || 0) > 0;
1007
+
1008
+ const unsigned = {
1009
+ version: contract.CANONICAL_NATIVE_PLAN_VERSION,
1010
+ compilerVersion: CANONICAL_NATIVE_PLAN_COMPILER_VERSION,
1011
+ hashAlgorithm: contract.CANONICAL_NATIVE_PLAN_HASH_ALGORITHM,
1012
+ canonical: Object.freeze({
1013
+ programVersion: this.metadata.version,
1014
+ compilerVersion: this.metadata.compilerVersion,
1015
+ runtimeProtocolVersion: this.metadata.runtimeProtocolVersion
1016
+ }),
1017
+ source: Object.freeze({
1018
+ file: this.metadata.file,
1019
+ sourceHash: this.metadata.sourceHash,
1020
+ projectSourceHash: this.metadata.projectSourceHash
1021
+ }),
1022
+ ownership: Object.freeze({
1023
+ version: contract.CANONICAL_NATIVE_PLAN_OWNERSHIP_VERSION,
1024
+ providerNeutral: true,
1025
+ provider: null,
1026
+ providerSpecificUserland: false,
1027
+ providerSdkUserland: false,
1028
+ javascriptRuntime: false,
1029
+ promiseSemantics: false,
1030
+ asyncify: false
1031
+ }),
1032
+ logging: Object.freeze({
1033
+ contractVersion: loggingContract.LOGGING_CONTRACT_VERSION,
1034
+ reporting: this.reporting,
1035
+ enabledStatements: this.enabledLogCount,
1036
+ prunedStatements: this.prunedLogCount,
1037
+ abi: loggingContract.PULSE_LOG_ABI
1038
+ }),
1039
+ routing: this.metadata.router ? deepFreeze(cloneJson(this.metadata.router)) : undefined,
1040
+ ...(this.metadata.application ? { application: deepFreeze(cloneJson(this.metadata.application)) } : {}),
1041
+ ...(this.metadata.applicationEntries ? { applicationEntries: deepFreeze(cloneJson(this.metadata.applicationEntries)) } : {}),
1042
+ ...(hasInboundEvents ? { events: deepFreeze(cloneJson(this.metadata.events)) } : {}),
1043
+ ...(this.metadata.json ? { json: deepFreeze(cloneJson({
1044
+ ...this.metadata.json,
1045
+ parser: this.metadata.json.genericParserRequired ? 'host-generic-json' : 'schema-specialized',
1046
+ parserOwnership: this.metadata.json.genericParserRequired ? 'pulse-host-capability' : 'compiled-schema-codec',
1047
+ inclusion: this.metadata.json.genericParserRequired ? 'reachable-schema-less-call' : 'schema-bound-only',
1048
+ costClass: this.metadata.json.genericParserRequired ? 'dynamic-host' : 'specialized',
1049
+ limits: {
1050
+ maxBytes: Number(this.metadata.json.maxBytes || 65536),
1051
+ maxDepth: null,
1052
+ depthBounded: false
1053
+ }
1054
+ })) } : {}),
1055
+ ...((this.metadata.capabilities || []).some((capability) => String(capability).startsWith('state.')) ? {
1056
+ requestState: hasInboundEvents
1057
+ ? Object.freeze({ enabled: true, representation: 'guest-string-map', reset: 'invocation-start', persistence: 'invocation-only' })
1058
+ : Object.freeze({ enabled: true, representation: 'guest-string-map', reset: 'pulse-start', persistence: 'request-only' })
1059
+ } : {}),
1060
+ entry: Object.freeze({
1061
+ kind: hasInboundEvents ? 'application' : (this.metadata.router ? 'router' : 'handler'),
1062
+ name: String(this.metadata.handler || 'default'),
1063
+ contextParameter: this.ctxName,
1064
+ body: Object.freeze(body)
1065
+ }),
1066
+ locals: Object.freeze(this.locals),
1067
+ effects: Object.freeze(this.effects),
1068
+ continuations: Object.freeze(continuations),
1069
+ states: Object.freeze(states),
1070
+ capabilities: Object.freeze([...(this.metadata.capabilities || [])].map(String).sort()),
1071
+ schemas: Object.freeze({
1072
+ sourceHash: this.metadata.schemaSourceHash,
1073
+ ids: Object.freeze([...(this.metadata.schemaIds || [])].map(String)),
1074
+ responseCaseIds: Object.freeze([...(this.metadata.responseCaseIds || [])].map(String)),
1075
+ registryHash: this.metadata.schemaRegistryHash,
1076
+ codecTableHash: this.metadata.schemaCodecTableHash,
1077
+ fullCodecRealization: this.metadata.schemaFullCodecRealization === true,
1078
+ registry: deepFreeze(cloneJson(this.metadata.schemaRegistry || {})),
1079
+ references: deepFreeze(cloneJson(this.metadata.schemaReferences || []))
1080
+ }),
1081
+ ...(this.compiled.cryptoRealizationPlan && this.compiled.cryptoRealizationPlan.target === 'native' ? {
1082
+ crypto: deepFreeze(cloneJson(this.compiled.cryptoRealizationPlan))
1083
+ } : {}),
1084
+ packages: Object.freeze({ effects: deepFreeze(cloneJson(this.metadata.packageEffects || [])) }),
1085
+ summary: Object.freeze({ ...this.summary })
1086
+ };
1087
+ const planHash = stableHash(stableStringify(unsigned));
1088
+ const plan = deepFreeze({ ...unsigned, planHash });
1089
+ assertCanonicalNativePlan(plan);
1090
+ return plan;
1091
+ }
1092
+ }
1093
+
1094
+ function walkExpression(expression, visit) {
1095
+ if (!expression || typeof expression !== 'object') return;
1096
+ visit(expression);
1097
+ switch (expression.kind) {
1098
+ case 'array':
1099
+ for (const item of expression.items || []) walkExpression(item, visit);
1100
+ break;
1101
+ case 'object':
1102
+ for (const entry of expression.entries || []) {
1103
+ if (entry.kind === 'spread') walkExpression(entry.value, visit);
1104
+ else {
1105
+ if (entry.key && entry.key.kind === 'computed') walkExpression(entry.key.value, visit);
1106
+ walkExpression(entry.value, visit);
1107
+ }
1108
+ }
1109
+ break;
1110
+ case 'template':
1111
+ for (const part of expression.parts || []) if (part.kind === 'value') walkExpression(part.value, visit);
1112
+ break;
1113
+ case 'binary':
1114
+ walkExpression(expression.left, visit);
1115
+ walkExpression(expression.right, visit);
1116
+ break;
1117
+ case 'unary':
1118
+ walkExpression(expression.value, visit);
1119
+ break;
1120
+ case 'conditional':
1121
+ walkExpression(expression.test, visit);
1122
+ walkExpression(expression.whenTrue, visit);
1123
+ walkExpression(expression.whenFalse, visit);
1124
+ break;
1125
+ case 'property':
1126
+ walkExpression(expression.object, visit);
1127
+ break;
1128
+ case 'element':
1129
+ walkExpression(expression.object, visit);
1130
+ walkExpression(expression.index, visit);
1131
+ break;
1132
+ case 'intrinsic':
1133
+ for (const argument of expression.arguments || []) walkExpression(argument, visit);
1134
+ break;
1135
+ case 'method-call':
1136
+ walkExpression(expression.receiver, visit);
1137
+ for (const argument of expression.arguments || []) walkExpression(argument, visit);
1138
+ break;
1139
+ case 'assignment':
1140
+ walkExpression(expression.target, visit);
1141
+ walkExpression(expression.value, visit);
1142
+ break;
1143
+ case 'update':
1144
+ walkExpression(expression.target, visit);
1145
+ break;
1146
+ case 'spread':
1147
+ walkExpression(expression.value, visit);
1148
+ break;
1149
+ default:
1150
+ break;
1151
+ }
1152
+ }
1153
+
1154
+ function walkStatements(statements, visitor) {
1155
+ for (const statement of statements || []) {
1156
+ visitor(statement);
1157
+ if (statement.kind === 'local') walkExpression(statement.value, visitor.expression);
1158
+ else if (statement.kind === 'if') {
1159
+ walkExpression(statement.test, visitor.expression);
1160
+ walkStatements(statement.then, visitor);
1161
+ walkStatements(statement.else, visitor);
1162
+ } else if (statement.kind === 'return') walkExpression(statement.value, visitor.expression);
1163
+ else if (statement.kind === 'expression') walkExpression(statement.expression, visitor.expression);
1164
+ }
1165
+ }
1166
+
1167
+ function countExpression(expression) {
1168
+ let count = 0;
1169
+ walkExpression(expression, () => { count += 1; });
1170
+ return count;
1171
+ }
1172
+
1173
+ function summarizeNativePlan(body, locals, effects, continuations) {
1174
+ const summary = {
1175
+ statementCount: 0,
1176
+ expressionCount: 0,
1177
+ localCount: locals.length,
1178
+ branchCount: 0,
1179
+ returnCount: 0,
1180
+ effectCount: effects.length,
1181
+ effectGroupCount: 0,
1182
+ continuationCount: continuations.length,
1183
+ maxStatementDepth: 0
1184
+ };
1185
+
1186
+ function visitStatements(statements, depth) {
1187
+ for (const statement of statements || []) {
1188
+ summary.statementCount += 1;
1189
+ summary.maxStatementDepth = Math.max(summary.maxStatementDepth, depth);
1190
+ if (statement.kind === 'local') summary.expressionCount += countExpression(statement.value);
1191
+ else if (statement.kind === 'if') {
1192
+ summary.branchCount += 1;
1193
+ summary.expressionCount += countExpression(statement.test);
1194
+ visitStatements(statement.then, depth + 1);
1195
+ visitStatements(statement.else, depth + 1);
1196
+ } else if (statement.kind === 'return') {
1197
+ summary.returnCount += 1;
1198
+ summary.expressionCount += countExpression(statement.value);
1199
+ } else if (statement.kind === 'expression') {
1200
+ summary.expressionCount += countExpression(statement.expression);
1201
+ } else if (statement.kind === 'effect') {
1202
+ if (statement.result && statement.result.mode === 'return') summary.returnCount += 1;
1203
+ } else if (statement.kind === 'effect-group') {
1204
+ summary.effectGroupCount += 1;
1205
+ }
1206
+ }
1207
+ }
1208
+
1209
+ visitStatements(body, 0);
1210
+ for (const effect of effects) {
1211
+ for (const input of effect.inputs || []) summary.expressionCount += countExpression(input.value);
1212
+ const decoder = effect.result && effect.result.decoder;
1213
+ for (const argument of (decoder && decoder.arguments) || []) summary.expressionCount += countExpression(argument);
1214
+ }
1215
+ return summary;
1216
+ }
1217
+
1218
+ // Normalize the callable shape expected by walkStatements without exposing mutable visitor state.
1219
+ function validateExpression(expression, fail, localIds, detail = {}) {
1220
+ walkExpression(expression, (node) => {
1221
+ if (!contract.CANONICAL_NATIVE_EXPRESSION_KINDS.includes(node.kind)) fail('expression kind is unknown', { ...detail, kind: node.kind });
1222
+ if (node.kind === 'local' && !localIds.has(node.id)) fail('expression references unknown local', { ...detail, localId: node.id });
1223
+ if (node.kind === 'intrinsic' && !contract.CANONICAL_NATIVE_INTRINSICS.includes(node.name)) fail('intrinsic is unknown', { ...detail, intrinsic: node.name });
1224
+ });
1225
+ }
1226
+
1227
+ function validateResult(result, fail, localIds, detail = {}) {
1228
+ if (!result || !contract.CANONICAL_NATIVE_RESULT_MODES.includes(result.mode)) {
1229
+ fail('effect result mode is unknown', { ...detail, mode: result && result.mode });
1230
+ return;
1231
+ }
1232
+ if (result.localId && !localIds.has(result.localId)) fail('effect result references unknown local', { ...detail, localId: result.localId });
1233
+ if (result.decoder) {
1234
+ if (!['json', 'text'].includes(result.decoder.kind)) fail('effect result decoder is unknown', { ...detail, decoder: result.decoder.kind });
1235
+ for (const argument of result.decoder.arguments || []) validateExpression(argument, fail, localIds, detail);
1236
+ }
1237
+ }
1238
+
1239
+ function validatePlanTree(plan, fail, localIds, effectIds, continuationIds) {
1240
+ const visitor = (statement) => {
1241
+ if (!contract.CANONICAL_NATIVE_STATEMENT_KINDS.includes(statement.kind)) fail('statement kind is unknown', { kind: statement.kind });
1242
+ if (statement.kind === 'local' && !localIds.has(statement.localId)) fail('local statement references unknown local', { localId: statement.localId });
1243
+ if (statement.kind === 'effect') {
1244
+ if (!effectIds.has(statement.effectId)) fail('effect statement references unknown effect', { effectId: statement.effectId });
1245
+ if (!continuationIds.has(statement.continuationId)) fail('effect statement references unknown continuation', { continuationId: statement.continuationId });
1246
+ validateResult(statement.result, fail, localIds, { effectId: statement.effectId });
1247
+ }
1248
+ if (statement.kind === 'effect-group') {
1249
+ if (!continuationIds.has(statement.continuationId)) fail('effect group references unknown continuation', { continuationId: statement.continuationId });
1250
+ for (const effectId of statement.effectIds || []) if (!effectIds.has(effectId)) fail('effect group references unknown effect', { effectId });
1251
+ for (const result of statement.results || []) if (!localIds.has(result.localId)) fail('effect group result references unknown local', { localId: result.localId });
1252
+ }
1253
+ };
1254
+ visitor.expression = (expression) => validateExpression(expression, fail, localIds);
1255
+ walkStatements(plan.entry && plan.entry.body, visitor);
1256
+ }
1257
+
1258
+ function assertCanonicalNativePlan(plan) {
1259
+ const errors = [];
1260
+ const fail = (message, detail = {}) => errors.push(Object.freeze({ message, detail: Object.freeze({ ...detail }) }));
1261
+ if (!plan || typeof plan !== 'object' || Array.isArray(plan)) fail('plan must be an object');
1262
+ else {
1263
+ if (plan.version !== contract.CANONICAL_NATIVE_PLAN_VERSION) fail('plan version mismatch', { actual: plan.version });
1264
+ if (plan.compilerVersion !== CANONICAL_NATIVE_PLAN_COMPILER_VERSION) fail('plan compiler version mismatch', { actual: plan.compilerVersion });
1265
+ if (plan.hashAlgorithm !== contract.CANONICAL_NATIVE_PLAN_HASH_ALGORITHM) fail('plan hash algorithm mismatch', { actual: plan.hashAlgorithm });
1266
+ if (!plan.ownership || plan.ownership.providerNeutral !== true || plan.ownership.provider !== null) fail('plan must remain provider-neutral');
1267
+ if (plan.ownership && (plan.ownership.javascriptRuntime || plan.ownership.promiseSemantics || plan.ownership.asyncify)) fail('plan must not claim JavaScript runtime, Promise, or Asyncify semantics');
1268
+ if (!plan.entry || !['handler', 'router', 'application'].includes(plan.entry.kind) || !Array.isArray(plan.entry.body)) fail('plan must contain a handler, router, or application entry body');
1269
+ if (plan.events !== undefined) {
1270
+ if (
1271
+ !plan.events
1272
+ || !plan.events.abi
1273
+ || plan.events.abi.version !== eventContract.EVENT_NATIVE_ABI_EXTENSION_VERSION
1274
+ || plan.events.abi.abiVersion !== eventContract.EVENT_NATIVE_ABI_EXTENSION.abiVersion
1275
+ || !plan.events.catalog
1276
+ ) {
1277
+ fail('plan event ABI metadata is invalid', { events: plan.events });
1278
+ } else {
1279
+ try {
1280
+ const normalizedCatalog = eventContract.normalizeEventCatalog({
1281
+ version: plan.events.catalog.version,
1282
+ contractId: plan.events.catalog.contractId,
1283
+ events: plan.events.catalog.events
1284
+ });
1285
+ if (stableStringify(normalizedCatalog) !== stableStringify(plan.events.catalog)) {
1286
+ fail('plan event catalog is not canonical', { catalogHash: plan.events.catalog.catalogHash });
1287
+ }
1288
+ } catch (error) {
1289
+ fail('plan event catalog is invalid', { code: error && error.code, message: error && error.message });
1290
+ }
1291
+ }
1292
+ if (plan.entry && plan.entry.kind !== 'application') fail('event-reachable plans require an application entry body');
1293
+ if (!Array.isArray(plan.applicationEntries) || !plan.applicationEntries.some((entry) => entry && entry.plane === 'event')) {
1294
+ fail('event-reachable plans require plane-neutral application entry metadata');
1295
+ }
1296
+ }
1297
+ if (!Array.isArray(plan.locals)) fail('plan locals must be an array');
1298
+ if (!Array.isArray(plan.effects)) fail('plan effects must be an array');
1299
+ if (!Array.isArray(plan.continuations)) fail('plan continuations must be an array');
1300
+ if (!Array.isArray(plan.states)) fail('plan states must be an array');
1301
+ if (plan.crypto !== undefined) {
1302
+ if (
1303
+ !plan.crypto
1304
+ || typeof plan.crypto !== 'object'
1305
+ || plan.crypto.version !== cryptoContract.CRYPTO_REALIZATION_PLAN_VERSION
1306
+ || plan.crypto.target !== 'native'
1307
+ || plan.crypto.automaticFallback !== false
1308
+ || !Array.isArray(plan.crypto.algorithms)
1309
+ ) {
1310
+ fail('plan crypto realization evidence is invalid', { crypto: plan.crypto });
1311
+ } else {
1312
+ const knownRealizations = new Map(
1313
+ cryptoContract.CRYPTO_REALIZATIONS.map((entry) => [`${entry.id}:${entry.algorithm}`, entry])
1314
+ );
1315
+ for (const entry of plan.crypto.algorithms) {
1316
+ const known = entry && knownRealizations.get(`${entry.realization}:${entry.algorithm}`);
1317
+ if (
1318
+ !known
1319
+ || known.algorithm !== entry.algorithm
1320
+ || known.kind !== entry.kind
1321
+ || known.implementation !== entry.implementation
1322
+ || !known.targets.includes('native')
1323
+ || entry.automaticFallback !== false
1324
+ ) {
1325
+ fail('plan crypto algorithm realization is invalid', {
1326
+ algorithm: entry && entry.algorithm,
1327
+ realization: entry && entry.realization,
1328
+ kind: entry && entry.kind,
1329
+ implementation: entry && entry.implementation
1330
+ });
1331
+ }
1332
+ }
1333
+ const unsignedCrypto = { ...plan.crypto };
1334
+ delete unsignedCrypto.planHash;
1335
+ const expectedCryptoHash = stableHash(stableStringify(unsignedCrypto));
1336
+ if (plan.crypto.planHash !== expectedCryptoHash) {
1337
+ fail('plan crypto realization hash mismatch', {
1338
+ expectedHash: expectedCryptoHash,
1339
+ actualHash: plan.crypto.planHash
1340
+ });
1341
+ }
1342
+ }
1343
+ }
1344
+ if (
1345
+ !plan.logging
1346
+ || plan.logging.contractVersion !== loggingContract.LOGGING_CONTRACT_VERSION
1347
+ || !plan.logging.reporting
1348
+ || plan.logging.reporting.version !== loggingContract.LOGGING_CONTRACT_VERSION
1349
+ || !Number.isInteger(plan.logging.enabledStatements)
1350
+ || !Number.isInteger(plan.logging.prunedStatements)
1351
+ || !plan.logging.abi
1352
+ || plan.logging.abi.signature !== loggingContract.PULSE_LOG_ABI.signature
1353
+ ) {
1354
+ fail('plan logging evidence is invalid', { logging: plan.logging });
1355
+ }
1356
+
1357
+ const unsigned = { ...plan };
1358
+ delete unsigned.planHash;
1359
+ const expectedHash = stableHash(stableStringify(unsigned));
1360
+ if (plan.planHash !== expectedHash) fail('plan hash mismatch', { expectedHash, actual: plan.planHash });
1361
+
1362
+ const locals = Array.isArray(plan.locals) ? plan.locals : [];
1363
+ const effects = Array.isArray(plan.effects) ? plan.effects : [];
1364
+ const continuations = Array.isArray(plan.continuations) ? plan.continuations : [];
1365
+ const localIds = new Set();
1366
+ for (const local of locals) {
1367
+ if (!local || typeof local !== 'object' || localIds.has(local.id)) fail('local IDs must be unique', { localId: local && local.id });
1368
+ else localIds.add(local.id);
1369
+ if (local && !contract.CANONICAL_NATIVE_VALUE_KINDS.includes(local.valueKind)) fail('local value kind is unknown', { localId: local.id, valueKind: local.valueKind });
1370
+ }
1371
+
1372
+ const effectIds = new Set();
1373
+ let previousOrder = 0;
1374
+ for (const effect of effects) {
1375
+ if (!effect || typeof effect !== 'object' || effectIds.has(effect.id)) fail('effect IDs must be unique', { effectId: effect && effect.id });
1376
+ else effectIds.add(effect.id);
1377
+ if (effect && effect.order !== previousOrder + 1) fail('effect order must be contiguous and deterministic', { effectId: effect.id, expected: previousOrder + 1, actual: effect.order });
1378
+ if (effect && Number.isInteger(effect.order)) previousOrder = effect.order;
1379
+ const inputNames = new Set();
1380
+ for (const input of (effect && effect.inputs) || []) {
1381
+ if (!input || typeof input.name !== 'string' || inputNames.has(input.name)) fail('effect input names must be unique strings', { effectId: effect && effect.id, input: input && input.name });
1382
+ else inputNames.add(input.name);
1383
+ validateExpression(input && input.value, fail, localIds, { effectId: effect && effect.id, input: input && input.name });
1384
+ }
1385
+ if (effect) validateResult(effect.result, fail, localIds, { effectId: effect.id });
1386
+ }
1387
+
1388
+ const continuationIds = new Set();
1389
+ for (const continuation of continuations) {
1390
+ if (!continuation || typeof continuation !== 'object' || continuationIds.has(continuation.id)) fail('continuation IDs must be unique', { continuationId: continuation && continuation.id });
1391
+ else continuationIds.add(continuation.id);
1392
+ for (const effectId of (continuation && continuation.effectIds) || []) if (!effectIds.has(effectId)) fail('continuation references unknown effect', { continuationId: continuation && continuation.id, effectId });
1393
+ }
1394
+ for (const effect of effects) {
1395
+ if (!continuationIds.has(effect.continuationId)) fail('effect references unknown continuation', { effectId: effect.id, continuationId: effect.continuationId });
1396
+ }
1397
+
1398
+ if (plan.entry && Array.isArray(plan.entry.body)) validatePlanTree(plan, fail, localIds, effectIds, continuationIds);
1399
+
1400
+ const states = Array.isArray(plan.states) ? plan.states : [];
1401
+ if (states.length !== continuations.length + 1) fail('state table must contain entry plus one state per continuation', { expected: continuations.length + 1, actual: states.length });
1402
+ if (states[0] && (states[0].id !== 'entry' || states[0].kind !== 'entry' || states[0].stateIndex !== 0)) fail('state zero must be the entry state');
1403
+ for (let index = 0; index < continuations.length; index += 1) {
1404
+ const continuation = continuations[index];
1405
+ const state = states[index + 1];
1406
+ if (!state || state.id !== continuation.id || state.kind !== 'continuation' || state.stateIndex !== continuation.stateIndex || stableStringify(state.effectIds) !== stableStringify(continuation.effectIds)) {
1407
+ fail('continuation state table mismatch', { continuationId: continuation.id, stateIndex: index + 1 });
1408
+ }
1409
+ }
1410
+
1411
+ const effectById = new Map(effects.map((entry) => [entry.id, entry]));
1412
+ const statementVisitor = (statement) => {
1413
+ if (statement.kind === 'effect') {
1414
+ const record = effectById.get(statement.effectId);
1415
+ if (record && stableStringify(statement.result) !== stableStringify(record.result)) fail('effect statement result differs from effect record', { effectId: statement.effectId });
1416
+ if (record && statement.continuationId !== record.continuationId) fail('effect statement continuation differs from effect record', { effectId: statement.effectId });
1417
+ }
1418
+ if (statement.kind === 'effect-group') {
1419
+ for (const item of statement.results || []) {
1420
+ const record = effectById.get(item.effectId);
1421
+ if (!record || !record.result || record.result.localId !== item.localId) fail('effect-group result differs from effect record', { effectId: item.effectId, localId: item.localId });
1422
+ }
1423
+ }
1424
+ };
1425
+ statementVisitor.expression = () => {};
1426
+ if (plan.entry && Array.isArray(plan.entry.body)) walkStatements(plan.entry.body, statementVisitor);
1427
+
1428
+ if (!plan.summary || typeof plan.summary !== 'object') fail('plan summary is required');
1429
+ else {
1430
+ const expectedSummary = summarizeNativePlan(plan.entry && plan.entry.body, locals, effects, continuations);
1431
+ for (const [key, expected] of Object.entries(expectedSummary)) {
1432
+ if (plan.summary[key] !== expected) fail(`summary ${key} mismatch`, { expected, actual: plan.summary[key] });
1433
+ }
1434
+ }
1435
+ }
1436
+ if (errors.length > 0) {
1437
+ const diagnostics = errors.map((entry) => diagnostic(undefined, undefined, contract.CANONICAL_NATIVE_PLAN_DIAGNOSTIC_CODES.PLAN_INVALID, entry.message, entry.detail));
1438
+ throw new CanonicalNativePlanError('Canonical native plan validation failed.', diagnostics);
1439
+ }
1440
+ return Object.freeze({
1441
+ ok: true,
1442
+ version: plan.version,
1443
+ planHash: plan.planHash,
1444
+ locals: plan.locals.length,
1445
+ effects: plan.effects.length,
1446
+ continuations: plan.continuations.length,
1447
+ statements: plan.summary.statementCount,
1448
+ expressions: plan.summary.expressionCount
1449
+ });
1450
+ }
1451
+
1452
+ function buildCanonicalNativePlanLegacy(compiled, options = {}) {
1453
+ return new NativePlanBuilder(compiled, options).build();
1454
+ }
1455
+
1456
+ function buildCanonicalNativePlan(compiled, options = {}) {
1457
+ return executeCanonicalNativePlanSpine(compiled, options, buildCanonicalNativePlanLegacy);
1458
+ }
1459
+
1460
+ function writeCanonicalNativePlan(plan, targetFile) {
1461
+ const report = assertCanonicalNativePlan(plan);
1462
+ const file = path.resolve(targetFile);
1463
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1464
+ fs.writeFileSync(file, `${stableStringify(plan, 2)}\n`);
1465
+ return Object.freeze({ file, bytes: fs.statSync(file).size, planHash: report.planHash });
1466
+ }
1467
+
1468
+ module.exports = Object.freeze({
1469
+ CANONICAL_NATIVE_PLAN_VERSION: contract.CANONICAL_NATIVE_PLAN_VERSION,
1470
+ CANONICAL_NATIVE_PLAN_COMPILER_VERSION,
1471
+ CANONICAL_NATIVE_PLAN_HASH_ALGORITHM: contract.CANONICAL_NATIVE_PLAN_HASH_ALGORITHM,
1472
+ CANONICAL_NATIVE_PLAN_OWNERSHIP_VERSION: contract.CANONICAL_NATIVE_PLAN_OWNERSHIP_VERSION,
1473
+ CanonicalNativePlanError,
1474
+ buildCanonicalNativePlan,
1475
+ validateCanonicalNativePlan: assertCanonicalNativePlan,
1476
+ writeCanonicalNativePlan,
1477
+ stableStringify
1478
+ });