@pulse-compute/wasm-compiler 1.0.0-beta.4 → 1.0.0-beta.6

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.
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const ts = require('typescript');
6
+ const { attachTerminalPackageNative } = require('./spine/terminal-package-native.js');
6
7
  const { buildSchemaJsonCompile } = require('@pulse-compute/wasm-schema-json/compiler/schema-json-compile');
7
8
  const { buildCanonicalSchemaBundle } = require('@pulse-compute/wasm-schema-json/compiler/canonical-schema-codecs');
8
9
  const {
@@ -509,6 +510,23 @@ function sourceDiagnosticForPackageEffect(effect, message, detail = {}) {
509
510
  });
510
511
  }
511
512
 
513
+ // Native must lower every reachable package site. JavaScript executes helpers
514
+ // from the original source graph; only sites owned by normalized Router handlers
515
+ // require a generated counterpart. Keep the full recognition bundle for package
516
+ // inspection, validation and provider requirements on both targets.
517
+ function packageSitesRequiringRouterLink(sites, records, options) {
518
+ if (options.target !== 'javascript') return sites;
519
+ return sites.filter((site) => records.some((record) => {
520
+ const ir = record && record.operationIr;
521
+ if (!ir || !ir.sourceFile || !ir.handler) return false;
522
+ const start = Number(site && site.range && site.range.start);
523
+ const end = Number(site && site.range && site.range.end);
524
+ return sameModulePath(site && site.loc && site.loc.file, ir.sourceFile.fileName || ir.file, options.rootDir)
525
+ && Number.isSafeInteger(start) && Number.isSafeInteger(end)
526
+ && start >= ir.handler.getStart(ir.sourceFile) && end <= ir.handler.getEnd();
527
+ }));
528
+ }
529
+
512
530
  function routerGeneratedPackageEffectLookup(router, recognition, options = {}) {
513
531
  const effects = [...(recognition && recognition.operations || [])].map((operation) => operation.canonicalEffect);
514
532
  if (!router || effects.length === 0) return undefined;
@@ -588,12 +606,15 @@ function routerGeneratedPackageEffectLookup(router, recognition, options = {}) {
588
606
  }
589
607
  }
590
608
 
591
- if (generatedByStart.size !== effects.length) {
592
- const linked = new Set([...generatedByStart.values()]);
593
- const missing = effects.filter((effect) => !linked.has(effect));
594
- const first = missing[0];
609
+ // A handler can be registered more than once. Each entry must link all of its
610
+ // generated calls above, while coverage here is over authored source effects.
611
+ // Counting generated sites would reject valid one-to-many source mappings.
612
+ const linked = new Set(generatedByStart.values());
613
+ const required = packageSitesRequiringRouterLink(effects, records, options);
614
+ const missing = required.filter((effect) => !linked.has(effect));
615
+ if (missing.length > 0) {
595
616
  throw new CanonicalProjectCompileError(
596
- `Generated Router source linked ${generatedByStart.size} of ${effects.length} reachable package effect(s).`,
617
+ `Generated Router source linked ${linked.size} of ${required.length} required package effect(s).`,
597
618
  missing.map((effect) => sourceDiagnosticForPackageEffect(effect, `Reachable package effect ${effect.kind} was not linked into generated Router source.`, {
598
619
  contractId: effect.contractId
599
620
  })),
@@ -728,11 +749,13 @@ function routerGeneratedPackageIntrinsicLookup(router, recognition, options = {}
728
749
  }
729
750
  }
730
751
 
731
- if (generatedByStart.size !== intrinsics.length) {
732
- const linked = new Set([...generatedByStart.values()]);
733
- const missing = intrinsics.filter((intrinsic) => !linked.has(intrinsic));
752
+ // Intrinsic calls share the same source-to-registration multiplicity as effects.
753
+ const linked = new Set(generatedByStart.values());
754
+ const required = packageSitesRequiringRouterLink(intrinsics, records, options);
755
+ const missing = required.filter((intrinsic) => !linked.has(intrinsic));
756
+ if (missing.length > 0) {
734
757
  throw new CanonicalProjectCompileError(
735
- `Generated Router source linked ${generatedByStart.size} of ${intrinsics.length} reachable package intrinsic(s).`,
758
+ `Generated Router source linked ${linked.size} of ${required.length} required package intrinsic(s).`,
736
759
  missing.map((intrinsic) => sourceDiagnosticForPackageEffect(
737
760
  intrinsic,
738
761
  `Reachable package intrinsic ${intrinsic.kind} was not linked into generated Router source.`,
@@ -1045,7 +1068,20 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
1045
1068
  const terminalPackageApplication = terminalPackageIntrinsicApplication(root, packageOperationRecognition);
1046
1069
 
1047
1070
  let linkedProjectModules;
1048
- if (multiModule) {
1071
+ let localHelperCall = false;
1072
+ if (root?.kind === 'router') {
1073
+ const visit = node => {
1074
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1075
+ if (options.target !== 'javascript') localHelperCall = true;
1076
+ else for (let parent = node.parent; parent && !ts.isFunctionLike(parent); parent = parent.parent) {
1077
+ if (ts.isForStatement(parent)) localHelperCall = true;
1078
+ }
1079
+ }
1080
+ ts.forEachChild(node, visit);
1081
+ };
1082
+ visit(root.module.sourceFile);
1083
+ }
1084
+ if (multiModule || localHelperCall) {
1049
1085
  try {
1050
1086
  linkedProjectModules = root.kind === 'router'
1051
1087
  ? linkProjectRouterModules(graphBuild, { rootDir, target: options.target })
@@ -1095,13 +1131,13 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
1095
1131
  ? terminalPackageApplication.sourceText
1096
1132
  : authoringSourceText;
1097
1133
  const generatedPackageEffectForCall = router
1098
- ? routerGeneratedPackageEffectLookup(router, packageOperationRecognition, { rootDir })
1134
+ ? routerGeneratedPackageEffectLookup(router, packageOperationRecognition, { rootDir, target: options.target })
1099
1135
  : packageEffectLookup(packageOperationRecognition);
1100
1136
  const generatedPackageResultAdapterForCall = router
1101
1137
  ? routerGeneratedPackageResultAdapterLookup(router, packageOperationRecognition, { rootDir })
1102
1138
  : packageResultAdapterLookup(packageOperationRecognition);
1103
1139
  const generatedPackageIntrinsicForCall = router
1104
- ? routerGeneratedPackageIntrinsicLookup(router, packageOperationRecognition, { rootDir })
1140
+ ? routerGeneratedPackageIntrinsicLookup(router, packageOperationRecognition, { rootDir, target: options.target })
1105
1141
  : packageIntrinsicLookup(packageOperationRecognition);
1106
1142
  const applicationEnvelope = createApplicationEnvelope(router, options.applicationProjectMetadata);
1107
1143
  const compileOptions = {
@@ -1198,6 +1234,10 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
1198
1234
  ].map((file) => path.resolve(file)))])
1199
1235
  });
1200
1236
  attachPackageOperationRecognition(project, packageOperationRecognition);
1237
+ if (terminalPackageApplication) attachTerminalPackageNative(project, {
1238
+ application: terminalPackageApplication, recognition: packageOperationRecognition,
1239
+ managedHandlers, schemaBundle: schema.bundle, cwd: rootDir, workspaceRoot: options.workspaceRoot
1240
+ });
1201
1241
  return project;
1202
1242
  }
1203
1243
 
@@ -301,6 +301,7 @@ function buildExecutionPlan(routerTreeArtifact, dispatchTable, options = {}) {
301
301
  pattern: mountPattern,
302
302
  scopeId: scopeBefore,
303
303
  childRouter: op.router,
304
+ ...(op.eligibility ? { eligibility: op.eligibility } : {}),
304
305
  childStartIndex: null,
305
306
  parentContinueIndex: null,
306
307
  loc: op.loc
package/src/extractor.js CHANGED
@@ -421,6 +421,29 @@ function parseTimeoutLiteral(sourceFile, node, diagnostics, phase = 'extract') {
421
421
  return config;
422
422
  }
423
423
 
424
+ function parseMountEligibility(sourceFile, node, diagnostics) {
425
+ const result = {};
426
+ let valid = ts.isObjectLiteralExpression(node) && node.properties.length === 2;
427
+ if (valid) {
428
+ for (const property of node.properties) {
429
+ if (!ts.isPropertyAssignment(property) ||
430
+ !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) ||
431
+ !['state', 'equals'].includes(property.name.text) ||
432
+ Object.hasOwn(result, property.name.text) || !ts.isStringLiteral(property.initializer)) {
433
+ valid = false;
434
+ break;
435
+ }
436
+ result[property.name.text] = property.initializer.text;
437
+ }
438
+ }
439
+ if (valid && Object.hasOwn(result, 'state') && Object.hasOwn(result, 'equals')) return result;
440
+ diagnostics.push(diagnostic(sourceFile, node, DIAGNOSTIC_CODES.UNSUPPORTED_MOUNT_ELIGIBILITY,
441
+ 'Mount eligibility must be an inline { state: "key", equals: "value" } descriptor with exactly two string literals.',
442
+ 'Select a family in earlier middleware. Eligibility cannot call functions, run effects, use getters, spreads, computed keys or dynamic values.',
443
+ { phase: 'extract' }));
444
+ return undefined;
445
+ }
446
+
424
447
  function parsePathLiteral(sourceFile, node, diagnostics, usage, phase = 'extract') {
425
448
  if (!isStringLiteralLike(node)) {
426
449
  diagnostics.push(
@@ -732,6 +755,7 @@ function parseRouterOperation(sourceFile, step, diagnostics, options = {}) {
732
755
  return {
733
756
  ...base(definition.opKind),
734
757
  path: stripScopedWildcard(parsePathLiteral(sourceFile, args[signature.pathArg], diagnostics, 'mount')),
758
+ ...(signature.eligibilityArg !== undefined ? { eligibility: parseMountEligibility(sourceFile, args[signature.eligibilityArg], diagnostics) } : {}),
735
759
  router: ts.isIdentifier(unwrappedRouter.expression) ? unwrappedRouter.expression.text : nodeText(sourceFile, routerArg),
736
760
  ...(unwrappedRouter.wrappers.length > 0 ? { routerWrappers: unwrappedRouter.wrappers } : {})
737
761
  };
@@ -1625,6 +1649,18 @@ function extractFromSourceFile(sourceFile, options = {}) {
1625
1649
  }
1626
1650
 
1627
1651
  const ir = buildRouterIR(sourceFile, options);
1652
+ // Historical table/harness emitters have no request-state eligibility ABI.
1653
+ // Keep this new authoring form on the canonical Router pipeline only.
1654
+ const selectedMount = ir.routerIR.flatMap(router => router.ops || []).find(op => op.eligibility);
1655
+ if (selectedMount) {
1656
+ throw new ExtractionError([{
1657
+ code: DIAGNOSTIC_CODES.UNSUPPORTED_MOUNT_ELIGIBILITY,
1658
+ severity: 'error', phase: 'extract', loc: selectedMount.loc,
1659
+ message: 'Selected mounts require the canonical Router compiler; historical table/harness emitters do not support eligibility.',
1660
+ hint: 'Use the canonical Pulse project compile/build workflow.'
1661
+ }], { passes, source });
1662
+ }
1663
+
1628
1664
  const indexDiagnostics = ir.diagnostics.filter((diag) => (diag.phase || diag.pass) === 'index');
1629
1665
  const extractDiagnostics = ir.diagnostics.filter((diag) => (diag.phase || diag.pass) !== 'index');
1630
1666
  passes.push(makePass('index', indexDiagnostics.length === 0 ? 'ok' : 'error', {
@@ -38,7 +38,10 @@ function packageStatus(module, graphBuild) {
38
38
  .find((entry) => entry.packageName === module.packageName);
39
39
  if (product) {
40
40
  const target = product.javascriptTarget;
41
- if (target && target.status === 'supported') {
41
+ // Source loadability is separate from selected-provider eligibility. The
42
+ // target-support owner checks provider-dependent requirements before build.
43
+ if (target && ['supported', 'provider-dependent'].includes(target.status)
44
+ && target.realization === 'javascript-package-runtime' && target.entry) {
42
45
  return Object.freeze({ status: 'package-runtime', runtimeEntry: target.entry, reasonCode: null });
43
46
  }
44
47
  return Object.freeze({
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ // Private planning facts about the existing, normalized S3GetTextResult contract.
4
+ // They never change an effect envelope or validate an arbitrary object by shape.
5
+ function bind(local, effect) {
6
+ return local.declaration === 'const' && effect.kind === 's3.getText'
7
+ && effect.declaredResult === 's3-get-text-result'
8
+ ? { ...local, textResult: { invalidated: false }, textFound: false } : local;
9
+ }
10
+
11
+ function refine(test, truth, scope) {
12
+ if (test.kind === 'unary' && test.operator === '!') return refine(test.value, !truth, scope);
13
+ if (test.kind !== 'binary') return;
14
+ if (test.operator === (truth ? '&&' : '||')) {
15
+ refine(test.left, truth, scope);
16
+ refine(test.right, truth, scope);
17
+ return;
18
+ }
19
+ if (!['===', '!=='].includes(test.operator) || truth !== (test.operator === '===')) return;
20
+ const read = test.left.kind === 'literal' && test.left.value === 'found' ? test.right
21
+ : test.right.kind === 'literal' && test.right.value === 'found' ? test.left : undefined;
22
+ if (read?.kind !== 'property' || read.property !== 'status' || read.object.kind !== 'local') return;
23
+ const local = scope.get(read.object.name);
24
+ if (local?.id === read.object.id && local.textResult && !local.textResult.invalidated) {
25
+ scope.set(local.name, { ...local, textFound: true });
26
+ }
27
+ }
28
+
29
+ function propertyKind(object, property, scope) {
30
+ if (object.kind !== 'local' || property !== 'text') return;
31
+ const local = scope.get(object.name);
32
+ if (local?.id === object.id && local.textFound && !local.textResult.invalidated) return 'string';
33
+ }
34
+
35
+ function join(scope, thenScope, elseScope, thenReturns, elseReturns) {
36
+ for (const [name, local] of scope) {
37
+ if (!local.textResult || local.textResult.invalidated) continue;
38
+ const yes = thenScope.get(name)?.textFound;
39
+ const no = elseScope.get(name)?.textFound;
40
+ if (thenReturns && no || elseReturns && yes || yes && no) scope.set(name, { ...local, textFound: true });
41
+ }
42
+ }
43
+
44
+ // Property writes may use aliases. Conservatively retire all live result facts,
45
+ // including sibling scopes sharing the record, instead of assuming no aliasing.
46
+ function invalidate(scope) {
47
+ for (const local of scope.values()) if (local.textResult) local.textResult.invalidated = true;
48
+ }
49
+
50
+ module.exports = { bind, refine, propertyKind, join, invalidate };
@@ -561,6 +561,63 @@ function linkProjectRouterModules(graphBuild, options = {}) {
561
561
  }
562
562
  }
563
563
 
564
+ // Only HTTP handler loop calls enter JavaScript's bounded inspection. Never
565
+ // walk an ordinary dependency function into a new purity requirement.
566
+ const httpFunctions = new Set();
567
+ if (options.target === 'javascript') for (const ref of handlerReferences) {
568
+ if (!['route', 'middleware'].includes(ref.role)) continue;
569
+ const module = context.projectModules.get(ref.module);
570
+ if (!module) continue;
571
+ if (!ref.localName.startsWith('<inline:')) {
572
+ httpFunctions.add(functionForNamedHandler(module.sourceFile, ref.localName));
573
+ } else {
574
+ const visit = node => {
575
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
576
+ const pos = module.sourceFile.getLineAndCharacterOfPosition(node.getStart(module.sourceFile));
577
+ if (pos.line + 1 === ref.source.line && pos.character + 1 === ref.source.column) httpFunctions.add(node);
578
+ }
579
+ ts.forEachChild(node, visit);
580
+ };
581
+ visit(module.sourceFile);
582
+ }
583
+ }
584
+
585
+ const sourceHelpers = [];
586
+ const helperCalls = new WeakMap();
587
+ {
588
+ const byIdentity = new Map();
589
+ for (const module of context.projectModules.values()) {
590
+ function visit(node) {
591
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
592
+ const call = node;
593
+ const awaited = ts.isAwaitExpression(node.parent);
594
+ const target = resolveLocal(context, indexes, module, call.expression.text, [], [], undefined, consumedImports);
595
+ const fn = target?.module && functionForNamedHandler(target.module.sourceFile, target.localName);
596
+ // JavaScript keeps ordinary calls in its original graph. Only bounded
597
+ // loop calls enter the shared pure-helper inspection contract.
598
+ let inLoop = false;
599
+ let owner = call.parent;
600
+ for (; owner && !ts.isFunctionLike(owner); owner = owner.parent) {
601
+ if (ts.isForStatement(owner)) inLoop = true;
602
+ }
603
+ const inspectPureLoopCall = inLoop && httpFunctions.has(owner) && fn && !fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword);
604
+ if (fn && (options.target !== 'javascript' || inspectPureLoopCall) && (awaited && fn.parameters.length > 1 || !awaited)) {
605
+ const identity = `${target.module.path}#${target.localName}`;
606
+ let helper = byIdentity.get(identity);
607
+ if (!helper) {
608
+ helper = { pure: !fn.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword), id: `helper:${identity}`, name: `__pulse_helper_${sourceHelpers.length}`, functionNode: fn,
609
+ resolveType: node => require('../spine/pure-helper-source').resolvePureType(context, target.module, node),
610
+ sourceFile: target.module.sourceFile, source: { file: target.module.path, name: target.localName } };
611
+ sourceHelpers.push(helper); byIdentity.set(identity, helper);
612
+ }
613
+ helperCalls.set(call, helper);
614
+ }
615
+ }
616
+ ts.forEachChild(node, visit);
617
+ }
618
+ visit(module.sourceFile);
619
+ }
620
+ }
564
621
  const rootGlobalName = routerSymbols.get(`${root.module.path}:${root.localName}`);
565
622
  const ordered = orderLinkedRouters(routers, rootGlobalName);
566
623
  if (options.target !== 'javascript') validateRuntimeProjectImports(context, consumedImports, diagnostics);
@@ -616,7 +673,14 @@ function linkProjectRouterModules(graphBuild, options = {}) {
616
673
  entrySourceFile: entryModule.sourceFile,
617
674
  entrySourceText: entryModule.sourceText,
618
675
  retainedDeclarations: retained,
676
+ sourceHelpers,
677
+ helperForCall: call => helperCalls.get(ts.getOriginalNode(call)),
619
678
  functionNodeForHandler,
679
+ pureTypeForNode(node) {
680
+ const original = ts.getOriginalNode(node);
681
+ const module = [...context.projectModules.values()].find(m => m.sourceFile === original.getSourceFile());
682
+ return module && require('../spine/pure-helper-source').resolvePureType(context, module, original);
683
+ },
620
684
  sourceFileForPath(file) {
621
685
  const module = context.projectModules.get(file);
622
686
  return module && module.sourceFile;
@@ -92,7 +92,8 @@ function classifyPackage(reachable, planned, product, context, options = {}) {
92
92
  }
93
93
 
94
94
  const targetStatus = product && product.javascriptTarget && product.javascriptTarget.status;
95
- if (targetStatus === 'not-realized' || targetStatus === 'provider-dependent') {
95
+ if (targetStatus === 'not-realized'
96
+ || (targetStatus === 'provider-dependent' && (!planned || planned.status !== 'package-runtime'))) {
96
97
  return Object.freeze({ ...common, status: 'pending', reasonId: 'package-javascript-realization-pending', owner: pendingPackageOwner(product) });
97
98
  }
98
99
  if (targetStatus === 'unsupported') {
@@ -335,7 +336,14 @@ function buildJavascriptTargetSupportEvidence(project, options = {}, policy = {}
335
336
  const capabilities = Object.freeze(compilerCapabilities
336
337
  .map((id) => policy.classifyCapability(id, context))
337
338
  .sort((a, b) => a.id.localeCompare(b.id)));
338
- const providerRequirements = Object.freeze(requirementsFromMetadata(compiled.metadata)
339
+ const requiredContracts = new Set(packages.filter((entry) => entry.required).map((entry) => entry.contractId));
340
+ const packageRequirements = (compiled.packageProduct && compiled.packageProduct.contracts || [])
341
+ .filter((entry) => requiredContracts.has(entry.contractId) && entry.javascriptTarget
342
+ && entry.javascriptTarget.status === 'provider-dependent')
343
+ .flatMap((entry) => entry.javascriptTarget.providerRequirements || []);
344
+ const providerRequirements = Object.freeze([...new Set([
345
+ ...requirementsFromMetadata(compiled.metadata), ...packageRequirements
346
+ ])]
339
347
  .map((id) => policy.classifyProviderRequirement(id, compilerCapabilities, context))
340
348
  .sort((a, b) => a.id.localeCompare(b.id)));
341
349
  const status = expectedProjectStatus(application, capabilities, providerRequirements, packages);
@@ -139,7 +139,7 @@ function projectRequire(projectRoot) {
139
139
  function resolveToolchainEntry(selector, packageName, options = {}) {
140
140
  const request = `${packageName}/toolchain`;
141
141
  const builtin = BUILTIN_PROVIDER_IDS.includes(selector);
142
- const resolver = builtin ? require : projectRequire(options.projectRoot);
142
+ const resolver = projectRequire(builtin ? options.builtinRoot || options.projectRoot : options.projectRoot);
143
143
  try {
144
144
  return resolver.resolve(request);
145
145
  } catch (cause) {
@@ -263,13 +263,13 @@ function providerTargetAvailability(provider, options = {}) {
263
263
 
264
264
  function providerIds(options = {}) {
265
265
  return Object.freeze(BUILTIN_PROVIDER_IDS.filter((id) => (
266
- options.executable !== true || getProviderDriver(id).executable === true
266
+ options.executable !== true || getProviderDriver(id, options).executable === true
267
267
  )));
268
268
  }
269
269
 
270
- function providerConfigReferences() {
270
+ function providerConfigReferences(options = {}) {
271
271
  return Object.freeze(BUILTIN_PROVIDER_IDS
272
- .map((id) => getProviderDriver(id))
272
+ .map((id) => getProviderDriver(id, options))
273
273
  .filter((driver) => driver.configReference)
274
274
  .map((driver) => driver.configReference));
275
275
  }
@@ -283,7 +283,7 @@ for (const id of BUILTIN_PROVIDER_IDS) {
283
283
  }
284
284
  Object.freeze(drivers);
285
285
 
286
- module.exports = Object.freeze({
286
+ const api = Object.freeze({
287
287
  PROVIDER_PACKAGE_SCOPE,
288
288
  PROVIDER_PACKAGE_PREFIX,
289
289
  BUILTIN_PROVIDER_IDS,
@@ -297,3 +297,27 @@ module.exports = Object.freeze({
297
297
  providerIds,
298
298
  providerConfigReferences
299
299
  });
300
+
301
+ // Bind a composition-owned resolution root without process-global registration.
302
+ // The CLI supplies its own package root for bundled IDs; scoped providers still
303
+ // resolve exactly from the project. A compiler consumer supplies only its project.
304
+ function createProviderResolver(defaults = {}) {
305
+ const options = Object.freeze({ ...defaults });
306
+ const resolver = { ...api };
307
+ for (const name of ['resolveProviderToolchain', 'getProviderDriver', 'providerTargetAvailability']) {
308
+ resolver[name] = (value, input = {}) => api[name](value, { ...input, ...options });
309
+ }
310
+ for (const name of ['getProviderTargetDescriptor', 'getProviderTargetSupportDeclaration']) {
311
+ resolver[name] = (value, target, input = {}) => api[name](value, target, { ...input, ...options });
312
+ }
313
+ resolver.providerIds = (input = {}) => providerIds({ ...input, ...options });
314
+ resolver.providerConfigReferences = () => providerConfigReferences(options);
315
+ const boundDrivers = {};
316
+ for (const id of BUILTIN_PROVIDER_IDS) Object.defineProperty(boundDrivers, id, {
317
+ enumerable: true, get() { return resolver.getProviderDriver(id); }
318
+ });
319
+ resolver.drivers = Object.freeze(boundDrivers);
320
+ return Object.freeze(resolver);
321
+ }
322
+
323
+ module.exports = Object.freeze({ ...api, createProviderResolver });
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+ const { CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION } = require('@pulse-compute/wasm-contracts/handler/canonical-native-plan');
3
+ // These descriptors prove reads of existing handles, never runtime conversions.
4
+ const SCALAR_TYPES = Object.freeze(['string', 'number', 'boolean']);
5
+ const BORROW_LIMITS = Object.freeze({ recordDepth: 2, recordFields: 32 });
6
+ const FORBIDDEN_FIELDS = Object.freeze(['__proto__', 'constructor', 'prototype']);
7
+ const scalar = type => SCALAR_TYPES.includes(type);
8
+ const kind = type => typeof type === 'string' ? type : type?.kind === 'record' ? 'object' : type?.kind === 'number-array' ? 'array' : 'unknown';
9
+ function validType(type, depth = 0) {
10
+ if (scalar(type)) return true;
11
+ if (!type || typeof type !== 'object' || depth > BORROW_LIMITS.recordDepth) return false;
12
+ if (type.kind === 'number-array') return Object.keys(type).length === 1;
13
+ return type.kind === 'record' && depth < BORROW_LIMITS.recordDepth && Object.keys(type).sort().join() === 'fields,kind'
14
+ && Array.isArray(type.fields) && type.fields.length > 0 && type.fields.length <= BORROW_LIMITS.recordFields
15
+ && new Set(type.fields.map(f => f?.name)).size === type.fields.length
16
+ && type.fields.every(f => f && Object.keys(f).sort().join() === 'name,type' && typeof f.name === 'string'
17
+ && /^[A-Za-z_$][\w$]*$/.test(f.name) && !FORBIDDEN_FIELDS.includes(f.name) && validType(f.type, depth + 1));
18
+ }
19
+ function same(a, b) {
20
+ if (a === undefined && b === undefined) return true;
21
+ if (typeof a === 'string' || typeof b === 'string') return a === b;
22
+ if (!a || !b || a.kind !== b.kind) return false;
23
+ return a.kind === 'number-array' || Array.isArray(a.fields) && Array.isArray(b.fields) && a.fields.length === b.fields.length && a.fields.every(f => same(f.type, b.fields.find(g => g.name === f.name)?.type));
24
+ }
25
+ function member(type, property) {
26
+ if ((type === 'string' || type?.kind === 'number-array') && property === 'length') return 'number';
27
+ return type?.kind === 'record' && Array.isArray(type.fields) ? type.fields.find(f => f.name === property)?.type : undefined;
28
+ }
29
+ // Result facts are not admission. These two existing inference boundaries have
30
+ // different evidence: plan kinds describe dynamic values; readType reconstructs
31
+ // proven structural types. Preserve their differences until policy is changed
32
+ // deliberately. Independent helper validation still owns operand/admission proof.
33
+ const BINARY_RULES = Object.freeze({
34
+ '===': 'comparison', '!==': 'comparison', '==': 'comparison', '!=': 'comparison',
35
+ '<': 'comparison', '<=': 'comparison', '>': 'comparison', '>=': 'comparison',
36
+ '&&': 'logical', '||': 'logical', '??': 'logical',
37
+ '+': 'numeric', '-': 'numeric', '*': 'numeric', '/': 'numeric', '%': 'numeric',
38
+ '**': 'numeric', '&': 'numeric', '|': 'numeric', '^': 'numeric',
39
+ '<<': 'numeric', '>>': 'numeric', '>>>': 'numeric'
40
+ });
41
+ const UNARY_TYPES = Object.freeze({ '!': 'boolean', typeof: 'string', '+': 'number', '-': 'number', '~': 'number' });
42
+ function binaryRule(operator) {
43
+ return Object.hasOwn(BINARY_RULES, operator) ? BINARY_RULES[operator] : undefined;
44
+ }
45
+ function binaryResult(operator, a, b, planKind, optionalStringDefault = true) {
46
+ const rule = binaryRule(operator);
47
+ const unknown = planKind ? 'unknown' : undefined;
48
+ if (rule === 'logical') {
49
+ if (planKind ? a === b : same(a, b)) return a;
50
+ if (optionalStringDefault && ['||', '??'].includes(operator) && (planKind
51
+ ? a === 'string-or-undefined' && b === 'string'
52
+ || operator === '??' && a === 'string' && b === 'string-or-undefined'
53
+ : a === 'string-or-undefined' && b === 'string')) return 'string';
54
+ return unknown;
55
+ }
56
+ if (rule === 'comparison' || planKind && operator === 'in') return 'boolean';
57
+ if (!planKind && (!scalar(a) || !scalar(b))) return undefined;
58
+ if (operator === '+' && (a === 'string' || b === 'string')) return 'string';
59
+ // The proven reader historically reports numeric result facts for scalar
60
+ // operands, even before operator admission. It must not become a validator.
61
+ return !planKind || rule === 'numeric' ? 'number' : unknown;
62
+ }
63
+ function binaryKind(operator, leftKind, rightKind, optionalStringDefault = true) {
64
+ return binaryResult(operator, leftKind, rightKind, true, optionalStringDefault);
65
+ }
66
+ function unaryType(operator) {
67
+ return Object.hasOwn(UNARY_TYPES, operator) ? UNARY_TYPES[operator] : undefined;
68
+ }
69
+ function unaryKind(operator) { return unaryType(operator) || 'number'; }
70
+ function element(type, indexType, literalIndex, optionalStringIndex = false) {
71
+ if (indexType === 'number') {
72
+ if (type === 'string') return optionalStringIndex ? 'string-or-undefined' : 'string';
73
+ if (type?.kind === 'number-array') return 'number';
74
+ }
75
+ if (typeof literalIndex === 'string') return member(type, literalIndex);
76
+ }
77
+ // Projection types are derived from declarations, literals and schema boundaries.
78
+ // They describe existing dynamic handles; they do not validate external data.
79
+ function schemaType(node) {
80
+ if (['string','boolean'].includes(node?.kind)) return node.kind;
81
+ if (['i32','u32','f64'].includes(node?.kind)) return 'number';
82
+ if (node?.kind === 'array' && schemaType(node.element) === 'number') return {kind:'number-array'};
83
+ if (node?.kind === 'object') return {kind:'record', ...(node.open || node.additionalProperties ? {open:true} : {}), fields: node.fields.map(f => ({name:f.name,
84
+ type: f.required ? schemaType(f.value) : schemaType(f.value) === 'string' ? 'string-or-undefined' : undefined}))};
85
+ }
86
+ function schemaRead(id, registry) {
87
+ return id?.kind === 'literal' && typeof id.value === 'string' ? schemaType(registry?.schemas?.find(s => s.id === id.value)?.root) : undefined;
88
+ }
89
+ function effectType(effect, registry) {
90
+ if (effect.kind === 'kv.getVersioned' && effect.borrowedValue?.version === CANONICAL_NATIVE_TYPED_KV_BORROW_VERSION
91
+ && validType(effect.borrowedValue.type)) return {kind:'record', fields:[
92
+ {name:'status',type:'string'}, {name:'value',type:effect.borrowedValue.type}]};
93
+ if (effect.result?.decoder?.kind === 'json') return schemaRead(effect.result.decoder.arguments?.[0], registry);
94
+ }
95
+ function readType(e, types, optionalStringIndex = false) {
96
+ if (!e) return;
97
+ const read = value => readType(value, types, optionalStringIndex);
98
+ if (e.kind === 'local') return types?.get(e.id);
99
+ if (e.kind === 'literal' && scalar(typeof e.value)) return typeof e.value;
100
+ if (e.kind === 'property') return member(read(e.object), e.property);
101
+ if (e.kind === 'element') {
102
+ const type = element(read(e.object), read(e.index), e.index?.kind === 'literal' ? e.index.value : undefined, optionalStringIndex);
103
+ if (type) return type;
104
+ }
105
+ if (e.kind === 'intrinsic' && e.name === 'request.text' && Array.isArray(e.arguments) && e.arguments.length === 0) return 'string';
106
+ if (e.kind === 'intrinsic' && e.name === 'state.get' && Array.isArray(e.arguments)
107
+ && e.arguments.length === 1 && read(e.arguments[0]) === 'string') return 'string-or-undefined';
108
+ if (e.kind === 'intrinsic' && ['request.json','schema.decode.text'].includes(e.name)) return schemaRead(e.arguments?.[e.name === 'request.json' ? 0 : 1], types?.schemas);
109
+ if (e.kind === 'array' && e.items.every(item => read(item) === 'number')) return {kind:'number-array'};
110
+ if (e.kind === 'object') {
111
+ if (!e.entries.some(entry => entry.kind === 'spread')) {
112
+ if (e.entries.every(f => f.kind === 'property' && f.key?.kind === 'literal' && typeof f.key.value === 'string')
113
+ && new Set(e.entries.map(f => f.key.value)).size === e.entries.length)
114
+ return {kind:'record',fields:e.entries.map(f=>({name:f.key.value,type:read(f.value)}))};
115
+ return;
116
+ }
117
+ const fields = new Map(), explicit = new Set();
118
+ for (const entry of e.entries) {
119
+ if (entry.kind === 'spread') {
120
+ const spread = read(entry.value);
121
+ if (spread?.kind !== 'record' || !validType(spread)) return;
122
+ for (const field of spread.fields) fields.set(field.name, field.type);
123
+ } else if (entry.kind === 'property' && entry.key?.kind === 'literal' && typeof entry.key.value === 'string'
124
+ && !explicit.has(entry.key.value)) {
125
+ explicit.add(entry.key.value);
126
+ fields.set(entry.key.value, read(entry.value));
127
+ } else return;
128
+ }
129
+ const result = {kind:'record', fields:[...fields].map(([name,type]) => ({name,type}))};
130
+ if (validType(result)) return result;
131
+ }
132
+ if (e.kind === 'conditional') { const a=read(e.whenTrue),b=read(e.whenFalse); if(same(a,b))return a; }
133
+ if (e.kind === 'binary') return binaryResult(e.operator, read(e.left), read(e.right), false);
134
+ if (e.kind === 'unary') return unaryType(e.operator);
135
+ if (e.kind === 'template') return 'string';
136
+ }
137
+ module.exports = { SCALAR_TYPES, BORROW_LIMITS, FORBIDDEN_FIELDS, scalar, kind, validType, same, member,
138
+ binaryRule, binaryKind, unaryKind, element, readType, schemaType, schemaRead, effectType };