@pulse-compute/wasm-compiler 1.0.0-beta.8 → 1.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
  > **Stability:** Internal compiler interface; no application-author compatibility guarantee.<br>
9
9
  > **npm:** [`@pulse-compute/wasm-compiler`](https://www.npmjs.com/package/@pulse-compute/wasm-compiler)<br>
10
10
  > **Canonical replacement:** `@pulse-compute/cli`<br>
11
- > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.8/packages/implementation-packages/)
11
+ > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.9/packages/implementation-packages/)
12
12
  >
13
- > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.8` package policy.
13
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.9` package policy.
14
14
  <!-- pulse-package-status:end -->
15
15
 
16
16
  Whole-project compiler implementation used by the `pulse` workflow. Application projects should use `@pulse-compute/cli`, not import this package directly.
@@ -63,4 +63,4 @@ The package exports its root implementation plus focused compiler, CLI, extracto
63
63
 
64
64
  Compiler changes should be covered by focused static/lowering tests, canonical project inspection, provider-plan parity, packed-package discovery where relevant, and the release documentation gates.
65
65
 
66
- See the installed [implementation package guide](https://pulsecompute.io/v1.0.0-beta.8/packages/implementation-packages/) and [compilation concepts](https://pulsecompute.io/v1.0.0-beta.8/concepts/compilation-and-lowering/).
66
+ See the installed [implementation package guide](https://pulsecompute.io/v1.0.0-beta.9/packages/implementation-packages/) and [compilation concepts](https://pulsecompute.io/v1.0.0-beta.9/concepts/compilation-and-lowering/).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-compute/wasm-compiler",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
4
4
  "license": "Apache-2.0",
5
5
  "description": "PulseWasm compiler package inside the Pulse monorepo.",
6
6
  "type": "commonjs",
@@ -9,13 +9,13 @@
9
9
  "pulsewasm": "./bin/pulsewasm-extract.js"
10
10
  },
11
11
  "dependencies": {
12
- "@pulse-compute/wasm-build-support": "1.0.0-beta.8",
13
- "@pulse-compute/wasm-contracts": "1.0.0-beta.8",
14
- "@pulse-compute/wasm-guest-link": "1.0.0-beta.8",
15
- "@pulse-compute/wasm-host-runtime": "1.0.0-beta.8",
16
- "@pulse-compute/wasm-library-kit": "1.0.0-beta.8",
17
- "@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.8",
18
- "@pulse-compute/wasm-schema-json": "1.0.0-beta.8",
12
+ "@pulse-compute/wasm-build-support": "1.0.0-beta.9",
13
+ "@pulse-compute/wasm-contracts": "1.0.0-beta.9",
14
+ "@pulse-compute/wasm-guest-link": "1.0.0-beta.9",
15
+ "@pulse-compute/wasm-host-runtime": "1.0.0-beta.9",
16
+ "@pulse-compute/wasm-library-kit": "1.0.0-beta.9",
17
+ "@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.9",
18
+ "@pulse-compute/wasm-schema-json": "1.0.0-beta.9",
19
19
  "assemblyscript": "0.28.18",
20
20
  "json-as": "1.5.0",
21
21
  "typescript": "5.9.3"
@@ -56,7 +56,7 @@
56
56
  "url": "git+https://github.com/pulse-compute/pulse.git",
57
57
  "directory": "wasm/packages/compiler"
58
58
  },
59
- "homepage": "https://pulsecompute.io/v1.0.0-beta.8/packages/implementation-packages/",
59
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.9/packages/implementation-packages/",
60
60
  "bugs": {
61
61
  "url": "https://github.com/pulse-compute/pulse/issues"
62
62
  },
@@ -142,7 +142,14 @@ function realizeCanonicalNativePlan(plan, options = {}, providerRequirements) {
142
142
  source, sourceHash: sha256(source), manifest: Object.freeze({
143
143
  ...application.manifest, planVersion: plan.version, planHash: plan.planHash, sourceHash: sha256(source)
144
144
  })
145
- }) : runtimeCore.generateCanonicalNativeAssemblyScript(plan, options);
145
+ }) : runtimeCore.generateCanonicalNativeAssemblyScript(plan, {
146
+ ...options,
147
+ // This compiler emits the pulse_host artifact consumed by Node Native.
148
+ // Provider writers own their final emission policies. Linked guests retain
149
+ // their existing path until separately qualified; budgeted plans are gated
150
+ // by the canonical numeric proof itself.
151
+ numericRegions: guestUnits.length === 0 ? 'host-numeric-regions-v1' : undefined
152
+ });
146
153
  const schemaCodecsActive = generated.manifest.schemaCodecs && generated.manifest.schemaCodecs.active === true;
147
154
  let jsonAs;
148
155
  if (schemaCodecsActive) {
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { retainSourceInputs, sourceInputsForCompiled } = require('./report-source-inputs');
4
+
3
5
  const fs = require('node:fs');
4
6
  const path = require('node:path');
5
7
  const ts = require('typescript');
@@ -395,6 +397,7 @@ function prepareReachableProjectPackageOperations(graphBuild, options = {}) {
395
397
  // identity checkout-independent. Absolute paths remain private graph
396
398
  // facts and must not feed project or native-plan hashes.
397
399
  ...(preserveSingleFilePublicEnvelope ? { publicSourceName: module.path } : {}),
400
+ packageDirs: context.packageContractCatalog.contracts.map(entry => entry.packageDir),
398
401
  selectedContracts: [...selectedContracts].sort()
399
402
  }));
400
403
  }
@@ -995,6 +998,7 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
995
998
  try {
996
999
  graphBuild = buildReachableProjectGraph(absoluteEntry, {
997
1000
  rootDir,
1001
+ target: options.target,
998
1002
  workspaceRoot: options.workspaceRoot,
999
1003
  tsconfigFile: options.tsconfigFile,
1000
1004
  resolver: options.moduleResolver,
@@ -1230,10 +1234,12 @@ function compileCanonicalProjectLegacy(entryFile, options = {}) {
1230
1234
  ...(applicationEnvelope ? { application: applicationEnvelope } : {}),
1231
1235
  watchFiles: Object.freeze([...new Set([
1232
1236
  ...graphBuild.projectFiles,
1237
+ ...graphBuild.packageSourceManifests,
1233
1238
  ...schema.dependencies,
1234
1239
  ...(packageOperationRecognition.dependencies || [])
1235
1240
  ].map((file) => path.resolve(file)))])
1236
1241
  });
1242
+ retainSourceInputs(project, projectGraphContext(graphBuild));
1237
1243
  attachPackageOperationRecognition(project, packageOperationRecognition);
1238
1244
  if (terminalPackageApplication) attachTerminalPackageNative(project, {
1239
1245
  application: terminalPackageApplication, recognition: packageOperationRecognition,
@@ -1261,5 +1267,6 @@ module.exports = Object.freeze({
1261
1267
  compileProjectSchemas,
1262
1268
  compileProjectPackageExtensions,
1263
1269
  compileCanonicalProject,
1264
- packageRealizationArtifactsForCompiled
1270
+ packageRealizationArtifactsForCompiled,
1271
+ sourceInputsForCompiled
1265
1272
  });
@@ -112,6 +112,7 @@ function buildJavascriptApplicationPlan(entryFile, options = {}) {
112
112
  let graphBuild;
113
113
  try {
114
114
  graphBuild = buildReachableProjectGraph(absoluteEntry, {
115
+ target: 'javascript',
115
116
  rootDir: options.rootDir,
116
117
  workspaceRoot: options.workspaceRoot,
117
118
  configFile: options.configFile,
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const ts = require('typescript');
6
+ const { sourceResolution } = require('../report-source-inputs');
7
+ const reportOwners = new WeakMap();
8
+
9
+ // This resolver reads source; it never imports a package or its compiler metadata.
10
+ const CONDITIONS = new Set(['pulse', 'import', 'default']);
11
+
12
+ function fail(message) { throw new Error(message); }
13
+
14
+ function exportTarget(exports, subpath) {
15
+ let selected = exports;
16
+ if (exports && typeof exports === 'object' && !Array.isArray(exports)) {
17
+ const keys = Object.keys(exports);
18
+ if (keys.some(key => key.startsWith('.'))) {
19
+ if (keys.some(key => !key.startsWith('.'))) fail('Mixed export subpaths and conditions are unsupported.');
20
+ if (!Object.hasOwn(exports, subpath)) fail(`No exact declared export for ${subpath}; wildcard exports are unsupported.`);
21
+ selected = exports[subpath];
22
+ } else if (subpath !== '.') fail(`No exact declared export for ${subpath}.`);
23
+ } else if (subpath !== '.') fail(`No exact declared export for ${subpath}.`);
24
+ while (selected && typeof selected === 'object' && !Array.isArray(selected)) {
25
+ const key = Object.keys(selected).find(key => CONDITIONS.has(key));
26
+ if (!key) fail('No pulse, import or default export condition.');
27
+ selected = selected[key];
28
+ }
29
+ if (typeof selected !== 'string') fail('A source export must select one string target; blocked, missing and array targets are unsupported.');
30
+ if (!selected.startsWith('./') || /[\\%?#*\0]/.test(selected)
31
+ || selected.slice(2).split('/').some(part => !part || part === '.' || part === '..' || part.toLowerCase() === 'node_modules')) {
32
+ fail('A source export must name a contained relative file without traversal or node_modules.');
33
+ }
34
+ return selected;
35
+ }
36
+
37
+ function sourceFile(root, candidate, declaration = false, reportOwner, prefix) {
38
+ const relative = path.relative(root, candidate);
39
+ if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
40
+ || relative.split(path.sep).some(part => part.toLowerCase() === 'node_modules')) fail('Source file escapes its package root.');
41
+ const supported = file => /\.(?:ts|mts|js|mjs)$/.test(file) && (declaration || !/\.d\.(?:ts|mts)$/.test(file));
42
+ if (!supported(candidate)) fail('Only .ts, .mts, .js and .mjs ESM source files are supported.');
43
+ if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) fail('Declared source file does not exist.');
44
+ const real = fs.realpathSync(candidate);
45
+ if (!supported(real)) fail('Source symlink must resolve to a supported ESM source file.');
46
+ const realRelative = path.relative(root, real);
47
+ if (!realRelative || realRelative === '..' || realRelative.startsWith(`..${path.sep}`) || path.isAbsolute(realRelative)
48
+ || realRelative.split(path.sep).some(part => part.toLowerCase() === 'node_modules')) fail('Source symlink escapes its package root.');
49
+ sourceResolution(reportOwner, candidate, real, `${prefix}/${realRelative.split(path.sep).join('/')}`);
50
+ return real;
51
+ }
52
+
53
+ function packageSource(manifestPath, manifest, packageName, subpath, manifestHash, reportOwner) {
54
+ if (manifest.name !== packageName || typeof manifest.version !== 'string'
55
+ || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(manifest.version)) fail('Source packages require a matching name and concrete version.');
56
+ const root = fs.realpathSync(path.dirname(manifestPath));
57
+ const target = exportTarget(manifest.exports, subpath);
58
+ const prefix = `npm/${packageName}@${manifest.version}/${manifestHash}`;
59
+ const absolute = sourceFile(root, path.resolve(root, target), false, reportOwner, prefix);
60
+ if (manifest.type === 'commonjs' && /\.(?:js|ts)$/.test(absolute)) fail('Explicit CommonJS package sources are unsupported; declare an ESM source export.');
61
+ const owner = Object.freeze({ root, absolute, packageName, packageVersion: manifest.version, packageManifestHash: manifestHash, packageType: manifest.type,
62
+ prefix });
63
+ if (reportOwner) reportOwners.set(owner, reportOwner);
64
+ return owner;
65
+ }
66
+
67
+ function relativeSource(owner, importer, specifier, declaration = false) {
68
+ if (!specifier.startsWith('./') && !specifier.startsWith('../')) fail('Package source cannot use application tsconfig aliases.');
69
+ if (/[\\%?#\0]/.test(specifier)) fail('Unsupported package source specifier.');
70
+ // Exact ESM file links only: no extension substitution, directory or main fallback.
71
+ const absolute = sourceFile(owner.root, path.resolve(path.dirname(importer), specifier), declaration, reportOwners.get(owner), owner.prefix);
72
+ if (!declaration && owner.packageType === 'commonjs' && /\.(?:js|ts)$/.test(absolute)) fail('Explicit CommonJS package sources are unsupported.');
73
+ return absolute;
74
+ }
75
+
76
+ function unsupportedStatement(sourceFile) {
77
+ const functionValue = node => {
78
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))) node = node.expression;
79
+ return node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
80
+ };
81
+ return sourceFile.statements.find(statement => {
82
+ if (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement) || ts.isFunctionDeclaration(statement)
83
+ || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isEmptyStatement(statement)) return false;
84
+ if (ts.isExportAssignment(statement)) return statement.isExportEquals || !(ts.isIdentifier(statement.expression) || functionValue(statement.expression));
85
+ if (ts.isVariableStatement(statement)) return !(statement.declarationList.flags & ts.NodeFlags.Const)
86
+ || statement.declarationList.declarations.some(declaration => !ts.isIdentifier(declaration.name) || !functionValue(declaration.initializer));
87
+ return true;
88
+ });
89
+ }
90
+
91
+ module.exports = Object.freeze({ packageSource, relativeSource, unsupportedStatement });
@@ -337,7 +337,7 @@ function packageRelationsForModule(context, targetKey) {
337
337
  for (const module of context.projectModules.values()) {
338
338
  for (const relation of [...module.imports, ...module.reExports]) {
339
339
  const resolution = module.resolutions.find((entry) => entry.relation === relation);
340
- if (!resolution || resolution.targetKey !== targetKey) continue;
340
+ if (!resolution || (resolution.packageKey || resolution.targetKey) !== targetKey) continue;
341
341
  records.push(Object.freeze({ module, relation }));
342
342
  }
343
343
  }
@@ -505,6 +505,7 @@ function derivePackageReachabilityProjection(context, manifest, states) {
505
505
  const reExportSources = relations.filter((entry) => entry.relation.kind === 're-export' && entry.relation.typeOnly !== true).map((entry) => entry.relation.source);
506
506
  let nativeStatus = 'unsupported';
507
507
  if (module.coreAuthoring) nativeStatus = 'core-authoring';
508
+ else if (module.sourceModule) nativeStatus = 'static-source';
508
509
  else if (module.packageContract && module.contract && module.contract.targetSupport.native === true) nativeStatus = 'trusted-lowerable';
509
510
  if (module.runtime && nativeStatus === 'trusted-lowerable') selectedContracts.add(module.packageContract);
510
511
  packages.push({
@@ -2,8 +2,10 @@
2
2
 
3
3
  const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
+ const { readSourceInput, missingManifest } = require('../report-source-inputs');
5
6
  const path = require('node:path');
6
7
  const ts = require('typescript');
8
+ const npmSource = require('./npm-source-resolution.js');
7
9
  const { discoverPackageContractCatalog } = require('../spine/package-operation-seam.js');
8
10
  const {
9
11
  PackageReachabilityError,
@@ -34,7 +36,8 @@ class ReachableProjectGraphError extends Error {
34
36
  constructor(message, diagnostics = [], detail = {}) {
35
37
  super(message);
36
38
  this.name = 'ReachableProjectGraphError';
37
- this.code = 'PULSE_PROJECT_GRAPH_FAILED';
39
+ this.code = diagnostics.length > 0 && diagnostics.every(entry => entry.code === 'PULSE_NATIVE_IMPORT_UNSUPPORTED')
40
+ ? 'PULSE_NATIVE_IMPORT_UNSUPPORTED' : 'PULSE_PROJECT_GRAPH_FAILED';
38
41
  this.diagnostics = Object.freeze([...diagnostics]);
39
42
  this.detail = Object.freeze({ ...detail });
40
43
  }
@@ -158,8 +161,8 @@ function propertyNameText(node) {
158
161
  return undefined;
159
162
  }
160
163
 
161
- function parseModule(rootDir, absolutePath, logicalPathInput) {
162
- const sourceText = fs.readFileSync(absolutePath, 'utf8');
164
+ function parseModule(rootDir, absolutePath, logicalPathInput, reportOwner) {
165
+ const sourceText = reportOwner ? readSourceInput(reportOwner, absolutePath, 'source', logicalPathInput) : fs.readFileSync(absolutePath, 'utf8');
163
166
  const logicalPath = logicalPathInput ? graph.normalizePortablePath(logicalPathInput, 'project module path') : portableFilePath(rootDir, absolutePath);
164
167
  const sourceFile = ts.createSourceFile(logicalPath, sourceText, ts.ScriptTarget.ES2022, true, scriptKindForFile(absolutePath));
165
168
  const imports = [];
@@ -419,12 +422,13 @@ function workspacePackageIndex(workspaceRoot) {
419
422
  return out;
420
423
  }
421
424
 
422
- function searchNodeModulesPackage(importerFile, packageName, stopDir) {
425
+ function searchNodeModulesPackage(importerFile, packageName, stopDir, reportOwner) {
423
426
  let current = path.dirname(importerFile);
424
427
  const stop = path.resolve(stopDir);
425
428
  while (true) {
426
429
  const candidate = path.join(current, 'node_modules', ...packageName.split('/'), 'package.json');
427
430
  if (fs.existsSync(candidate)) return candidate;
431
+ missingManifest(reportOwner, candidate, packageName);
428
432
  if (current === stop || current === path.dirname(current)) break;
429
433
  current = path.dirname(current);
430
434
  }
@@ -437,27 +441,73 @@ function packageOwner(packageName) {
437
441
  return 'third-party';
438
442
  }
439
443
 
444
+ function discoverResolvedPackageContract(context, manifestPath, packageName) {
445
+ if (!manifestPath || context.fixedPackageContractCatalog) return;
446
+ const packageDir = fs.realpathSync(path.dirname(manifestPath));
447
+ if (context.packageContractDirs.has(packageDir)) return;
448
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
449
+ if (!manifest.pulse?.contract && !manifest.pulsewasm?.manifest && !manifest.pulsewasm?.lowerableManifest) return;
450
+ if (manifest.name !== packageName) throw new TypeError(`Resolved lowerer package identity must match ${packageName}.`);
451
+ context.packageContractDirs.add(packageDir);
452
+ const catalog = discoverPackageContractCatalog({ cwd: context.rootDir, workspaceRoot: context.workspaceRoot,
453
+ packageDirs: [...context.packageContractDirs] });
454
+ context.packageContractCatalog = catalog;
455
+ for (const entry of catalog.contracts) {
456
+ context.packageContractsBySubpath.set(entry.lowerableSubpath, entry);
457
+ for (const subpath of entry.compatibilitySubpaths) context.packageContractsBySubpath.set(subpath, entry);
458
+ context.packageContractsByPackage.set(entry.packageName, catalog.contracts.filter(value => value.packageName === entry.packageName));
459
+ }
460
+ for (const entry of catalog.productCatalog.contracts) {
461
+ context.packageProductContractsByPackage.set(entry.npmPackage, entry);
462
+ context.packageProductContractsBySpecifier.set(entry.authoring.import, { contract: entry, role: 'authoring' });
463
+ for (const specifier of entry.ownership.helperImports) context.packageProductContractsBySpecifier.set(specifier, { contract: entry, role: 'helper' });
464
+ for (const specifier of entry.ownership.reExportImports) context.packageProductContractsBySpecifier.set(specifier, { contract: entry, role: 're-export' });
465
+ }
466
+ }
467
+
440
468
  function packageModuleForSpecifier(context, importer, classification) {
441
469
  const packageName = classification.packageName;
442
470
  const specifier = classification.specifier;
471
+ // Resolve the installed copy at this import before granting compiler authority.
472
+ const resolvedManifest = searchNodeModulesPackage(importer.absolutePath, packageName, context.workspaceRoot)
473
+ || context.workspacePackages.get(packageName);
474
+ discoverResolvedPackageContract(context, resolvedManifest, packageName);
443
475
  const contract = context.packageContractsBySubpath.get(specifier) || null;
476
+ const declaredOwners = context.packageContractsByPackage.get(packageName) || [];
477
+ if (resolvedManifest && declaredOwners.some(entry => entry.packageDir && fs.realpathSync(entry.packageDir) !== fs.realpathSync(path.dirname(resolvedManifest)))) {
478
+ throw new TypeError(`Resolved package ${packageName} does not match its selected lowerer owner.`);
479
+ }
444
480
  const productBinding = context.packageProductContractsBySpecifier.get(specifier) || null;
445
481
  const productContract = productBinding && productBinding.contract || context.packageProductContractsByPackage.get(packageName) || null;
446
482
  const productRole = productBinding && productBinding.role || null;
447
- let manifestPath = context.workspacePackages.get(packageName);
448
- if (!manifestPath) manifestPath = searchNodeModulesPackage(importer.absolutePath, packageName, context.workspaceRoot);
483
+ const trusted = CORE_AUTHORING_PACKAGES.has(packageName) || productContract || context.packageContractsByPackage.has(packageName);
484
+ let manifestPath = (trusted || !context.nativePackageSources) && resolvedManifest;
485
+ if (!manifestPath && importer.packageSource && importer.packageSource.packageName === packageName) manifestPath = path.join(importer.packageSource.root, 'package.json');
486
+ if (!manifestPath) manifestPath = searchNodeModulesPackage(importer.absolutePath, packageName, context.workspaceRoot, context.nativePackageSources && !trusted ? context : undefined);
449
487
  let manifest;
450
488
  let manifestHash;
451
489
  if (manifestPath) {
452
- const source = fs.readFileSync(manifestPath, 'utf8');
453
- manifest = JSON.parse(source);
454
- manifestHash = sha256(source);
490
+ try {
491
+ const source = context.nativePackageSources && !trusted
492
+ ? readSourceInput(context, manifestPath, 'manifest', packageName) : fs.readFileSync(manifestPath, 'utf8');
493
+ manifest = JSON.parse(source);
494
+ if (!isPlainObject(manifest)) throw new TypeError('Expected a package manifest object.');
495
+ manifestHash = sha256(source);
496
+ } catch (_) {
497
+ throw new ReachableProjectGraphError(`Invalid package manifest for ${packageName}.`, [diagnostic(context.rootDir, importer.sourceFile, null,
498
+ 'PULSE_NATIVE_IMPORT_UNSUPPORTED', `Cannot read a valid package manifest for ${packageName}.`, { packageName })]);
499
+ }
455
500
  } else {
456
501
  manifest = { name: packageName, version: 'unresolved' };
457
502
  manifestHash = sha256(JSON.stringify(manifest));
458
503
  }
459
504
  const packageSubpath = classification.packageSubpath || '.';
460
505
  const key = `package:${packageName}:${packageSubpath}:${manifestHash}`;
506
+ const existing = context.packageModules.get(key);
507
+ if (context.nativePackageSources && existing && existing.sourceCandidate && existing.manifestPath && manifestPath && existing.manifestPath !== fs.realpathSync(manifestPath)) {
508
+ throw new ReachableProjectGraphError(`Ambiguous installed copies of ${packageName}@${manifest.version}.`, [diagnostic(context.rootDir, importer.sourceFile, null,
509
+ 'PULSE_NATIVE_IMPORT_UNSUPPORTED', 'Distinct installed package roots share one package identity; deduplicate the dependency.', { packageName, packageSubpath })]);
510
+ }
461
511
  if (!context.packageModules.has(key)) {
462
512
  context.packageModules.set(key, {
463
513
  key,
@@ -478,6 +528,9 @@ function packageModuleForSpecifier(context, importer, classification) {
478
528
  source: null,
479
529
  specifier,
480
530
  classification,
531
+ manifestPath: manifestPath && fs.realpathSync(manifestPath),
532
+ manifest,
533
+ sourceCandidate: !trusted && Boolean(manifestPath),
481
534
  contract,
482
535
  productContract,
483
536
  productRole,
@@ -487,6 +540,35 @@ function packageModuleForSpecifier(context, importer, classification) {
487
540
  return context.packageModules.get(key);
488
541
  }
489
542
 
543
+ function loadPackageSource(context, packageModule, importer, relation) {
544
+ if (packageModule.sourceModule) return packageModule.sourceModule;
545
+ let owner;
546
+ try {
547
+ owner = npmSource.packageSource(packageModule.manifestPath, packageModule.manifest, packageModule.packageName,
548
+ packageModule.packageSubpath, packageModule.packageManifestHash, context);
549
+ const identity = `${owner.packageName}:${owner.packageManifestHash}`;
550
+ const priorRoot = context.packageSourceRoots.get(identity);
551
+ if (priorRoot && priorRoot !== owner.root) throw new Error('Distinct installed roots share this package identity; deduplicate the dependency.');
552
+ context.packageSourceRoots.set(identity, owner.root);
553
+ } catch (error) {
554
+ context.diagnostics.push(diagnostic(context.rootDir, importer.sourceFile, relation.statement, 'PULSE_NATIVE_IMPORT_UNSUPPORTED',
555
+ `Cannot resolve static source export ${relation.specifier}: ${error.message}`, { packageName: packageModule.packageName,
556
+ packageVersion: packageModule.packageVersion, packageSubpath: packageModule.packageSubpath, automaticFallback: false }));
557
+ return undefined;
558
+ }
559
+ const sourcePath = `${owner.prefix}/${portableFilePath(owner.root, owner.absolute)}`;
560
+ // Register before following imports so cycles are reported by the graph, not recursion.
561
+ packageModule.sourceModule = sourcePath;
562
+ packageModule.source = { file: sourcePath, line: 1, column: 1 };
563
+ const module = loadProjectModule(context, owner.absolute, sourcePath, owner);
564
+ packageModule.contentHash = sha256(`${packageModule.contentHash}:${module.contentHash}`);
565
+ packageModule.exports = exportedNamesForModule(module);
566
+ context.edges.push({ kind: 're-export', from: packageModule.key, to: sourcePath,
567
+ specifier: `./${portableFilePath(owner.root, owner.absolute)}`, resolutionKind: 'project-relative', runtime: true,
568
+ importedNames: [], exportedNames: packageModule.exports, packageContract: null, source: packageModule.source });
569
+ return sourcePath;
570
+ }
571
+
490
572
  function generatedConfigModule(context) {
491
573
  if (!context.configFile) return undefined;
492
574
  const key = 'generated:pulse-project-config';
@@ -562,9 +644,19 @@ function scanUnsupportedBoundaries(context, module) {
562
644
  function resolveModuleEdges(context, module) {
563
645
  const relations = [...module.imports, ...module.reExports];
564
646
  for (const relation of relations) {
565
- const classification = graph.classifyModuleSpecifier(relation.specifier, context.resolver);
647
+ const classification = graph.classifyModuleSpecifier(relation.specifier, module.packageSource ? graph.normalizeModuleResolverInput({ baseUrl: '.' }) : context.resolver);
566
648
  if (classification.kind === 'project-relative' || classification.kind === 'tsconfig-path') {
567
- const resolved = resolveProjectSpecifier(context, module, relation.specifier);
649
+ let resolved;
650
+ if (module.packageSource) {
651
+ try {
652
+ const absolute = npmSource.relativeSource(module.packageSource, module.absolutePath, relation.specifier, relation.kind === 'type-import' || relation.typeOnly === true);
653
+ resolved = { status: 'resolved', absolute, portable: `${module.packageSource.prefix}/${portableFilePath(module.packageSource.root, absolute)}` };
654
+ } catch (error) {
655
+ context.diagnostics.push(diagnostic(context.rootDir, module.sourceFile, relation.statement, 'PULSE_NATIVE_IMPORT_UNSUPPORTED',
656
+ `Cannot resolve package source ${relation.specifier}: ${error.message}`, { automaticFallback: false }));
657
+ continue;
658
+ }
659
+ } else resolved = resolveProjectSpecifier(context, module, relation.specifier);
568
660
  if (resolved.status === 'missing') {
569
661
  context.diagnostics.push(diagnostic(context.rootDir, module.sourceFile, relation.statement, 'PULSE_PROJECT_MODULE_NOT_FOUND', `Unable to resolve project module ${JSON.stringify(relation.specifier)} from ${module.path}.`, { specifier: relation.specifier, candidates: resolved.candidates }));
570
662
  continue;
@@ -590,7 +682,7 @@ function resolveModuleEdges(context, module) {
590
682
  });
591
683
  continue;
592
684
  }
593
- const target = loadProjectModule(context, resolved.absolute, resolved.portable);
685
+ const target = loadProjectModule(context, resolved.absolute, resolved.portable, module.packageSource);
594
686
  module.resolutions.push(Object.freeze({ relation, classification, targetKey: target.path }));
595
687
  context.edges.push({
596
688
  kind: relation.kind,
@@ -638,7 +730,9 @@ function resolveModuleEdges(context, module) {
638
730
  }
639
731
 
640
732
  const packageModule = packageModuleForSpecifier(context, module, classification);
641
- module.resolutions.push(Object.freeze({ relation, classification, targetKey: packageModule.key }));
733
+ const sourceKey = context.nativePackageSources && relation.kind !== 'type-import' && relation.typeOnly !== true && packageModule.sourceCandidate
734
+ ? loadPackageSource(context, packageModule, module, relation) : undefined;
735
+ module.resolutions.push(Object.freeze({ relation, classification, targetKey: sourceKey || packageModule.key, packageKey: packageModule.key }));
642
736
  const packageContractEdge = relation.kind !== 'type-import' && packageModule.packageContract != null;
643
737
  context.edges.push({
644
738
  kind: packageContractEdge ? 'package-contract' : relation.kind,
@@ -655,13 +749,17 @@ function resolveModuleEdges(context, module) {
655
749
  }
656
750
  }
657
751
 
658
- function loadProjectModule(context, absolutePath, logicalPath) {
659
- const contained = realContainedPath(context.rootDir, absolutePath);
752
+ function loadProjectModule(context, absolutePath, logicalPath, packageSource) {
753
+ const contained = realContainedPath(packageSource ? packageSource.root : context.rootDir, absolutePath);
660
754
  if (!contained) throw new ReachableProjectGraphError(`Project module ${absolutePath} is outside the workspace or missing.`, [], { rootDir: context.rootDir, file: absolutePath });
661
755
  const key = logicalPath
662
756
  ? graph.normalizePortablePath(logicalPath, 'project module path')
663
757
  : portablePath(context.rootDir, path.resolve(absolutePath));
664
- if (context.projectModules.has(key)) return context.projectModules.get(key);
758
+ if (context.projectModules.has(key)) {
759
+ const existing = context.projectModules.get(key);
760
+ if (existing.absolutePath !== contained) throw new ReachableProjectGraphError(`Source identity collision at ${key}.`);
761
+ return existing;
762
+ }
665
763
  const folded = key.toLocaleLowerCase('en-US');
666
764
  const collision = [...context.projectModules.keys()].find((existing) => existing.toLocaleLowerCase('en-US') === folded && existing !== key);
667
765
  if (collision) {
@@ -675,9 +773,16 @@ function loadProjectModule(context, absolutePath, logicalPath) {
675
773
  detail: Object.freeze({ existing: collision, candidate: key })
676
774
  })], { rootDir: context.rootDir, existing: collision, candidate: key });
677
775
  }
678
- const module = parseModule(context.rootDir, contained, key);
776
+ const module = parseModule(context.rootDir, contained, key, packageSource ? context : undefined);
777
+ module.packageSource = packageSource;
679
778
  context.projectModules.set(key, module);
680
779
  context.diagnostics.push(...module.parseDiagnostics);
780
+ if (packageSource) {
781
+ const unsupported = npmSource.unsupportedStatement(module.sourceFile);
782
+ if (unsupported) context.diagnostics.push(diagnostic(context.rootDir, module.sourceFile, unsupported, 'PULSE_NATIVE_IMPORT_UNSUPPORTED',
783
+ 'Package source permits static imports/re-exports, types and function declarations only; module initialization and captured values are unsupported.',
784
+ { packageName: packageSource.packageName, packageVersion: packageSource.packageVersion, automaticFallback: false }));
785
+ }
681
786
  scanUnsupportedBoundaries(context, module);
682
787
  resolveModuleEdges(context, module);
683
788
  return module;
@@ -721,7 +826,9 @@ function moduleInputs(context) {
721
826
  modules.push({
722
827
  key: module.path,
723
828
  kind: 'project',
724
- owner: 'application',
829
+ owner: module.packageSource ? packageOwner(module.packageSource.packageName) : 'application',
830
+ ...(module.packageSource ? { packageName: module.packageSource.packageName, packageVersion: module.packageSource.packageVersion,
831
+ packageManifestHash: module.packageSource.packageManifestHash, packageSubpath: `./${portableFilePath(module.packageSource.root, module.absolutePath)}` } : {}),
725
832
  format: module.format,
726
833
  runtime: module.runtime === true,
727
834
  path: module.path,
@@ -764,7 +871,7 @@ function createContext(entryFile, options = {}) {
764
871
  const packageContractCatalog = options.packageContractCatalog || discoverPackageContractCatalog({
765
872
  cwd: rootDir,
766
873
  workspaceRoot,
767
- scanNodeModules: true
874
+ packageDirs: []
768
875
  });
769
876
  const packageContractsBySubpath = new Map();
770
877
  for (const entry of packageContractCatalog.contracts) {
@@ -797,14 +904,18 @@ function createContext(entryFile, options = {}) {
797
904
  ? Object.freeze({ ...options.projectFragments })
798
905
  : Object.freeze({}),
799
906
  resolver: resolverInputFromProject(rootDir, options),
907
+ nativePackageSources: options.target !== 'javascript',
800
908
  workspacePackages: workspacePackageIndex(workspaceRoot),
801
909
  packageContractCatalog,
910
+ fixedPackageContractCatalog: Boolean(options.packageContractCatalog),
911
+ packageContractDirs: new Set(),
802
912
  packageContractsBySubpath,
803
913
  packageContractsByPackage,
804
914
  packageProductContractsByPackage,
805
915
  packageProductContractsBySpecifier,
806
916
  projectModules: new Map(),
807
917
  packageModules: new Map(),
918
+ packageSourceRoots: new Map(),
808
919
  generatedModules: new Map(),
809
920
  externalModules: new Map(),
810
921
  edges: [],
@@ -915,6 +1026,7 @@ function buildReachableProjectGraph(entryFile, options = {}) {
915
1026
  entryKey: entry.path,
916
1027
  graph: preliminary,
917
1028
  projectFiles: Object.freeze([...context.projectModules.values()].map((module) => module.absolutePath).sort()),
1029
+ packageSourceManifests: Object.freeze([...context.packageModules.values()].filter(module => module.sourceModule).map(module => module.manifestPath).sort()),
918
1030
  runtimeProjectFiles: Object.freeze([...context.projectModules.values()].filter((module) => module.runtime).map((module) => module.absolutePath).sort()),
919
1031
  projectModulePaths: Object.freeze([...context.projectModules.values()].map((module) => module.path).sort()),
920
1032
  runtimeProjectModulePaths: Object.freeze([...context.projectModules.values()].filter((module) => module.runtime).map((module) => module.path).sort()),
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ const { createHash } = require('node:crypto');
4
+ const { topologyForCanonicalRouterOutput, surfaceFactsForCanonicalRouterOutput, handlerIrsForCanonicalRouterOutput } = require('./spine/canonical-router');
5
+ const { sourceModelForRouterRecognition, capabilityForHandlerSurface, staticString } = require('./spine/handler-surface-authority');
6
+ const { projectRouterControl } = require('./report-router-control');
7
+ const { bindingOwnership } = require('./spine/binding-ownership');
8
+ const unavailable = () => ({ state: 'unavailable', value: null, reason: 'not-recorded' });
9
+ const available = value => ({ state: 'available', value, reason: null });
10
+ const freeze = value => { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); } return value; };
11
+
12
+ // Post-emission observation only. Read retained compiler objects, never reparse
13
+ // emitted source or read files. Nothing here enters executable metadata/hashes.
14
+ function collectHandlerTopology(compiled) {
15
+ const topology = topologyForCanonicalRouterOutput(compiled.router);
16
+ if (!topology) return undefined;
17
+ // A newer/other topology must not turn successful compilation into a report
18
+ // failure, nor acquire a fabricated role. Its analytics remain unavailable.
19
+ if (topology.entries.some(({ entry }) => !['route', 'use', 'mount', 'error', 'event', 'fallback'].includes(entry.kind))) return undefined;
20
+ const linked = topology.options.linkedProjectModules;
21
+ const files = new Map();
22
+ const sourceFile = file => linked?.sourceFileForPath(file)
23
+ || (topology.sourceFile.fileName === file ? topology.sourceFile : undefined);
24
+ function span(file, start, end) {
25
+ const source = sourceFile(file);
26
+ if (!source || !Number.isSafeInteger(start) || !Number.isSafeInteger(end)
27
+ || start < 0 || end <= start || end > source.text.length) return unavailable();
28
+ if (!files.has(file)) files.set(file, createHash('sha256').update(source.text).digest('hex'));
29
+ const point = offset => { const p = source.getLineAndCharacterOfPosition(offset);
30
+ return { offset, line: p.line + 1, column: p.character + 1 }; };
31
+ return available({ file, sha256: files.get(file), start: point(start), end: point(end) });
32
+ }
33
+ const location = loc => span(loc?.file, loc?.start?.offset, loc?.end?.offset);
34
+ const modules = new Map((compiled.reachableGraph?.modules || []).filter(m => m.path).map(m => [m.path, m]));
35
+ function packageFor(file) {
36
+ const module = modules.get(file);
37
+ if (!module) return unavailable();
38
+ return module.packageName && module.packageVersion
39
+ ? available({ name: module.packageName, version: module.packageVersion, sourceIdentity: module.path })
40
+ : { state: 'not-applicable', value: null, reason: 'not-applicable' };
41
+ }
42
+ const handlers = topology.handlerTable.handlers.map(handler => ({
43
+ id: handler.id, kind: 'handler', name: handler.localName || handler.exportName || handler.inlineName || null,
44
+ source: location(handler.loc), package: packageFor(handler.file),
45
+ evidence: [{ basis: 'handler-table', producer: 'canonical-router-handler-table' }]
46
+ }));
47
+ for (const helper of linked?.sourceHelpers || []) {
48
+ const file = helper.source.file, node = helper.functionNode, source = sourceFile(file);
49
+ handlers.push({ id: helper.id, kind: 'helper', name: helper.source.name || null,
50
+ source: source && node ? span(file, node.getStart(source), node.end) : unavailable(), package: packageFor(file),
51
+ evidence: [{ basis: 'handler-table', producer: 'canonical-router-linked-helper' }] });
52
+ }
53
+ const http = new Map(topology.entries.filter(d => d.entry.kind !== 'event').map(d => [d.entry.index, d.entry.stableId]));
54
+ const controls = new Map((handlerIrsForCanonicalRouterOutput(compiled.router) || []).map(row => [row.entryStableId, row.operationIr]));
55
+ const mounts = [];
56
+ const registrations = topology.entries.map(({ entry, rawEntry }) => {
57
+ // The compiler's child/parent-return interval identifies this particular
58
+ // mount expansion, even when router definition paths are identical.
59
+ while (mounts.length && entry.index >= mounts.at(-1).end) mounts.pop();
60
+ const registration = {
61
+ id: entry.stableId, handlerId: entry.handlerId || null,
62
+ kind: entry.kind === 'use' ? 'middleware' : entry.kind,
63
+ authored: Boolean(rawEntry.loc) && entry.kind !== 'fallback',
64
+ source: location(rawEntry.loc), routerId: entry.router, routerPath: [...entry.routerPath],
65
+ mountPath: entry.kind === 'event' ? [] : mounts.map(m => m.id), order: entry.index,
66
+ match: available({ method: entry.method || null, path: entry.path ?? null, eventType: entry.eventType || null }),
67
+ control: projectRouterControl(entry, rawEntry, controls.get(entry.stableId)),
68
+ flow: { next: http.get(entry.nextIndex) || null, child: http.get(entry.childStartIndex) || null,
69
+ parentContinue: http.get(entry.parentContinueIndex) || null },
70
+ evidence: [{ basis: 'router-topology', producer: 'canonical-router-execution-topology' }]
71
+ };
72
+ if (entry.kind === 'mount' && entry.childStartIndex > entry.index && entry.parentContinueIndex >= entry.childStartIndex) {
73
+ mounts.push({ id: entry.stableId, end: entry.parentContinueIndex });
74
+ }
75
+ return registration;
76
+ });
77
+ const model = sourceModelForRouterRecognition(surfaceFactsForCanonicalRouterOutput(compiled.router)?.recognition);
78
+ const nodeSource = row => span(row.sourceFile.fileName, row.node.getStart(row.sourceFile), row.node.end);
79
+ const useSites = model?.authoredSites.map(row => {
80
+ const recognized = row.recognized, capability = capabilityForHandlerSurface(recognized);
81
+ const provider = recognized.detail?.provider, effect = recognized.detail?.packageEffect;
82
+ const name = provider ? staticString(provider.resource, { unwrap: true }) : effect?.resource?.kind === 'literal' ? effect.resource.value : undefined;
83
+ const binding = bindingOwnership(provider?.providerKind || effect?.providerKind, name,
84
+ Boolean(provider?.resource) && name === undefined || effect?.resource?.kind === 'dynamic');
85
+ return { ownerId: row.ownerId, surfaceId: recognized.surfaceId, operation: recognized.canonicalOperation,
86
+ capability: capability ? available(capability) : unavailable(),
87
+ binding: binding || { state: 'not-applicable', kind: null, canonicalId: null },
88
+ source: nodeSource(row), evidence: [{ basis: 'authored-recognition', producer: 'canonical-handler-surface-authority' }] };
89
+ });
90
+ const calls = new Map();
91
+ let helperCallsComplete = true;
92
+ for (const row of model?.validatedCalls || []) {
93
+ const source = nodeSource(row);
94
+ // No lowered offsets, call order or guessed identities stand in for source.
95
+ if (source.state !== 'available') { helperCallsComplete = false; continue; }
96
+ const key = JSON.stringify([row.callerId, row.calleeId, source.value.file, source.value.start.offset, source.value.end.offset]);
97
+ if (!calls.has(key)) calls.set(key, { canonicalId: null, callerId: row.callerId, calleeId: row.calleeId,
98
+ registrationIds: [], source, evidence: [{ basis: 'validated-helper-call', producer: 'canonical-router-handler-validation' }] });
99
+ calls.get(key).registrationIds.push(row.registrationId);
100
+ }
101
+ return freeze({ version: 'pulse.compiler-report-topology.v1', handlers, registrations,
102
+ ...(model ? { useSites, helperCalls: [...calls.values()], helperCallsComplete } : {}) });
103
+ }
104
+ module.exports = { collectHandlerTopology };
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const { bindingOwnership } = require('./spine/binding-ownership');
3
4
  const { packageOperationRecognitionForCompiled } = require('./spine/package-operation-seam');
4
5
 
5
6
  // Build-time projection of compiler facts. This record never enters the Native
@@ -65,9 +66,8 @@ function collectCanonicalReportReferences(compiled, plan) {
65
66
  : input(effect, kind === 'kv' ? 'store' : 'name');
66
67
  const resolved = literal(expression);
67
68
  const name = resolved ?? (effect.resource?.kind === 'literal' && typeof effect.resource.value === 'string' ? effect.resource.value : null);
68
- add('binding', name, kind, consumers, effect.id, {
69
- dynamic: effect.resource?.kind === 'dynamic' || Boolean(expression && expression.kind !== 'literal')
70
- });
69
+ const binding = bindingOwnership(kind, name, effect.resource?.kind === 'dynamic' || Boolean(expression && expression.kind !== 'literal'));
70
+ add('binding', binding.canonicalId, binding.kind, consumers, effect.id, { dynamic: binding.state === 'dynamic' });
71
71
  }
72
72
  if (effect.kind === 'event.emit') {
73
73
  const schema = literal(field(input(effect, 'emission'), 'schema'));
@@ -110,7 +110,9 @@ function collectCanonicalReportReferences(compiled, plan) {
110
110
  // Only compact allowlisted data crosses into Report. No provider metadata,
111
111
  // effect payloads, binding values, resource bodies or expression text survives.
112
112
  const routeBehaviors = require('./report-route-behavior').routeBehaviorForMetadata(compiled.metadata.router);
113
+ const topology = require('./report-handler-topology').collectHandlerTopology(compiled);
113
114
  return Object.freeze({ version: 'pulse.compiler-report-references.v1',
115
+ ...(topology ? { topology } : {}),
114
116
  ...(routeBehaviors ? { routeBehaviors } : {}),
115
117
  responsePayloads: require('./report-response-payloads').collectResponsePayloads(plan, entries, helpers),
116
118
  references: Object.freeze([...rows.values()].map(row => Object.freeze({ ...row, entryIds: Object.freeze(unique(row.entryIds)),
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ // Project only accepted canonical operations. No AST text, predicates, provider
4
+ // values or generated positions enter the report, and no runtime IR is changed.
5
+ function projectRouterControl(entry, rawEntry, ir) {
6
+ const unavailable = { state: 'unavailable', value: null, reason: 'not-recorded' };
7
+ const value = { pattern: null, eligibility: Boolean(rawEntry.eligibility),
8
+ normal: false, error: false, terminal: false, basis: 'canonical-router-control' };
9
+ if (entry.kind === 'route' || entry.kind === 'mount' || entry.kind === 'use' && entry.scoped) {
10
+ if (!rawEntry.pattern?.normalized) return unavailable;
11
+ value.pattern = rawEntry.pattern.normalized;
12
+ }
13
+ if (entry.kind === 'mount' || !entry.handlerId) value.normal = true;
14
+ else {
15
+ if (!ir?.body) return unavailable;
16
+ let known = true;
17
+ function visit(operation) {
18
+ if (!operation) return;
19
+ switch (operation.kind) {
20
+ case 'block': operation.statements.forEach(visit); break;
21
+ case 'if': visit(operation.thenOperation); visit(operation.elseOperation); break;
22
+ case 'router-guard': visit(operation.body); break;
23
+ case 'router-transfer':
24
+ if (operation.clearNormalMode) value.normal = true;
25
+ else if (operation.errorExpression) value.error = true;
26
+ else known = false;
27
+ break;
28
+ case 'source-statement':
29
+ if (operation.role === 'router-return') value.terminal = true;
30
+ else if (operation.role !== 'router-source') known = false;
31
+ break;
32
+ default: known = false;
33
+ }
34
+ }
35
+ visit(ir.body);
36
+ if (entry.kind === 'event') value.terminal = true; // Accepted void fallthrough.
37
+ if (!known || !value.normal && !value.error && !value.terminal) return unavailable;
38
+ }
39
+ return { state: 'available', value, reason: null };
40
+ }
41
+
42
+ module.exports = { projectRouterControl };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { createHash } = require('node:crypto');
6
+ const reads = new WeakMap(), outputs = new WeakMap();
7
+ const stamp = s => (s.isDirectory() ? [s.dev, s.ino, s.mode] : [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs]).map(String).join(':');
8
+ // In-memory read receipts only: never enter the graph, plan or executable hashes.
9
+ function state(file) {
10
+ const chain = []; let directory = path.parse(file).root;
11
+ for (const part of path.relative(directory, file).split(path.sep)) {
12
+ directory = path.join(directory, part); chain.push(stamp(fs.lstatSync(directory, { bigint: true })));
13
+ }
14
+ return { real: fs.realpathSync(file), token: JSON.stringify(chain) };
15
+ }
16
+ function receiptFor(owner) {
17
+ if (!reads.has(owner)) reads.set(owner, { rows: [], error: null, bytes: 0 });
18
+ return reads.get(owner);
19
+ }
20
+ function readSourceInput(owner, file, kind, identity) {
21
+ const receipt = receiptFor(owner);
22
+ let before; try { before = state(file); } catch { receipt.error = 'REPORT_INPUT_UNBOUND'; }
23
+ const source = fs.readFileSync(file, 'utf8'); // Preserve the compiler's ordinary read/error behavior.
24
+ try {
25
+ const after = state(file), bytes = Buffer.byteLength(source);
26
+ if (!before || before.real !== after.real || before.token !== after.token) receipt.error = 'REPORT_CONCURRENT_CHANGE';
27
+ receipt.bytes += bytes;
28
+ if (receipt.rows.length >= 20000 || receipt.bytes > 256 * 1024 * 1024) receipt.error = 'REPORT_LIMIT';
29
+ if (!receipt.error) receipt.rows.push(Object.freeze({ file, ...after, kind, identity, bytes,
30
+ sha256: createHash('sha256').update(source).digest('hex') }));
31
+ } catch { receipt.error = 'REPORT_INPUT_UNBOUND'; }
32
+ return source;
33
+ }
34
+ function missingManifest(owner, file, identity) {
35
+ if (!owner) return;
36
+ const receipt = receiptFor(owner);
37
+ if (receipt.rows.length >= 20000) receipt.error = 'REPORT_LIMIT';
38
+ if (!receipt.error) receipt.rows.push(Object.freeze({ file, real: null, token: 'absent', kind: 'missing-manifest', identity, bytes: 0, sha256: null }));
39
+ }
40
+ function sourceResolution(owner, file, real, identity) {
41
+ if (!owner || file === real) return;
42
+ const receipt = receiptFor(owner);
43
+ try {
44
+ const observed = state(file);
45
+ if (observed.real !== real) receipt.error = 'REPORT_CONCURRENT_CHANGE';
46
+ if (receipt.rows.length >= 20000) receipt.error = 'REPORT_LIMIT';
47
+ if (!receipt.error) receipt.rows.push(Object.freeze({ file, ...observed, kind: 'source-link', identity, bytes: 0, sha256: null }));
48
+ } catch { receipt.error = 'REPORT_INPUT_UNBOUND'; }
49
+ }
50
+ function retainSourceInputs(compiled, context) {
51
+ const receipt = reads.get(context);
52
+ outputs.set(compiled, Object.freeze({ rows: Object.freeze([...(receipt?.rows || [])]), error: receipt?.error || null }));
53
+ }
54
+ function sourceInputsForCompiled(compiled) { return outputs.get(compiled); }
55
+ module.exports = { readSourceInput, missingManifest, sourceResolution, retainSourceInputs, sourceInputsForCompiled };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ // A named binding is compiler evidence, independent of a host's configured
4
+ // value. Both authored recognition and lowered-reference projection use this
5
+ // state policy; absence of a literal never invents a resolved binding.
6
+ function bindingOwnership(kind, name, dynamic = false) {
7
+ if (!['config', 'secret', 'kv', 's3'].includes(kind)) return undefined;
8
+ const canonicalId = typeof name === 'string' ? name : null;
9
+ return Object.freeze({ kind, canonicalId,
10
+ state: canonicalId !== null ? 'resolved' : dynamic ? 'dynamic' : 'unknown' });
11
+ }
12
+
13
+ module.exports = { bindingOwnership };
@@ -469,6 +469,25 @@ function routerHandlerNames(entry) {
469
469
  });
470
470
  }
471
471
 
472
+ // Shared compiler projection. Recognition details own provider/package capability
473
+ // names; synchronous surfaces use the same mapping as router analysis.
474
+ function capabilityForHandlerSurface(fact) {
475
+ if (fact.detail?.provider) return fact.detail.provider.capability;
476
+ if (fact.detail?.packageEffect) return String(fact.detail.packageEffect.capability || fact.detail.packageEffect.kind);
477
+ const id = fact.surfaceId;
478
+ if (id === 'ctx.req.body') return 'request.body.forward';
479
+ if (id.startsWith('ctx.fetch.')) return 'fetch';
480
+ if (id.startsWith('ctx.req.')) return id.slice(4);
481
+ if (id.startsWith('ctx.config.')) return 'config.get';
482
+ if (id.startsWith('ctx.secret.')) return 'secret.get';
483
+ if (id.startsWith('ctx.kv.') || id.startsWith('ctx.state.')) return id.slice(4);
484
+ if (id === 'ctx.emit') return 'event.emit';
485
+ if (id.startsWith('ctx.log.')) return 'logging';
486
+ if (['ctx.json', 'ctx.text', 'ctx.response'].includes(id)) return `response.${id.slice(4)}`;
487
+ if (id === 'ctx.param') return 'route.param';
488
+ return undefined;
489
+ }
490
+
472
491
  function inspectCanonicalRouterSurfaces(topology, options = {}) {
473
492
  if (!topology || topology.frontend !== 'canonical-router' || !topology.sourceFile || !Array.isArray(topology.entries)) {
474
493
  throw new TypeError('inspectCanonicalRouterSurfaces requires a prepared canonical Router topology.');
@@ -476,6 +495,47 @@ function inspectCanonicalRouterSurfaces(topology, options = {}) {
476
495
  const sourceFile = topology.sourceFile;
477
496
  const facts = [];
478
497
  const surfacesByEntry = new Map();
498
+ // Private observations retain original nodes before normalization. They never
499
+ // enter recognition facts, summaries, executable metadata or identity hashes.
500
+ const authoredSites = [], validatedCalls = [], seenOwners = new Set();
501
+ function retainAuthored(ownerId, fn, file, names) {
502
+ if (seenOwners.has(ownerId)) return;
503
+ seenOwners.add(ownerId);
504
+ const seenNodes = new WeakSet();
505
+ function visit(node, aliases) {
506
+ if (seenNodes.has(node)) return;
507
+ seenNodes.add(node);
508
+ if (ts.isTypeNode(node) || ts.isFunctionLike(node)) return;
509
+ if (ts.isBlock(node)) {
510
+ const local = new Map(aliases);
511
+ for (const statement of node.statements) {
512
+ const namespace = extractKvNamespaceDeclaration(statement, names.ctxName, { unwrap: true });
513
+ if (namespace) local.set(namespace.variableName, namespace.store);
514
+ visit(statement, local);
515
+ }
516
+ return;
517
+ }
518
+ // Await/parentheses are syntax wrappers, not additional operation sites.
519
+ const current = expressionFor(node, { unwrap: true });
520
+ if (current !== node) { visit(current, aliases); return; }
521
+ if (ts.isAwaitExpression(node)) { visit(node.expression, aliases); return; }
522
+ const recognized = recognizeHandlerSurface(node, { ...names, unwrap: true,
523
+ kvAliases: aliases, packageEffectForCall: options.packageEffectForCall });
524
+ if (recognized && recognized.class !== 'handler-wrapper') {
525
+ authoredSites.push(Object.freeze({ ownerId, sourceFile: file, node, recognized }));
526
+ if (ts.isCallExpression(node)) {
527
+ for (const argument of node.arguments) visit(argument, aliases);
528
+ // A fetch projection owns its fetch receiver; provider receiver inputs
529
+ // can themselves contain synchronous context operations.
530
+ if (recognized.detail?.provider?.resource) visit(recognized.detail.provider.resource, aliases);
531
+ for (const argument of recognized.detail?.fetch?.fetchCall?.arguments || []) visit(argument, aliases);
532
+ return;
533
+ }
534
+ }
535
+ ts.forEachChild(node, child => visit(child, aliases));
536
+ }
537
+ if (fn.body) visit(fn.body, new Map());
538
+ }
479
539
 
480
540
  function record(entry, node, names) {
481
541
  const recognized = recognizeHandlerSurface(node, {
@@ -514,6 +574,7 @@ function inspectCanonicalRouterSurfaces(topology, options = {}) {
514
574
  for (const entry of topology.entries) {
515
575
  if (!entry.functionNode) continue;
516
576
  const names = routerHandlerNames(entry);
577
+ retainAuthored(entry.entry.handlerId, entry.functionNode, entry.sourceFile || sourceFile, names);
517
578
  record(entry, entry.functionNode, names);
518
579
  function visit(node) {
519
580
  if (typeof ts.isTypeNode === 'function' && ts.isTypeNode(node)) return;
@@ -528,6 +589,9 @@ function inspectCanonicalRouterSurfaces(topology, options = {}) {
528
589
  if (entry.functionNode.body) visit(entry.functionNode.body);
529
590
  }
530
591
 
592
+ for (const helper of options.linkedProjectModules?.sourceHelpers || []) {
593
+ if (!helper.pure) retainAuthored(helper.id, helper.functionNode, helper.sourceFile, { ctxName: 'ctx' });
594
+ }
531
595
  const recognition = Object.freeze({
532
596
  version: HANDLER_SURFACE_AUTHORITY_VERSION,
533
597
  contractVersion: contract.HANDLER_SURFACE_CONTRACT_VERSION,
@@ -543,6 +607,12 @@ function inspectCanonicalRouterSurfaces(topology, options = {}) {
543
607
  });
544
608
  canonicalRouterModels.set(recognition, Object.freeze({
545
609
  topology,
610
+ authoredSites: Object.freeze(authoredSites),
611
+ get validatedCalls() { return Object.freeze([...validatedCalls]); },
612
+ retainValidatedCall(entry, helper, node, sourceFile) {
613
+ validatedCalls.push(Object.freeze({ callerId: entry.handlerId, calleeId: helper.id,
614
+ registrationId: entry.stableId, node: ts.getOriginalNode(node), sourceFile }));
615
+ },
546
616
  namesForEntry(entryStableId) {
547
617
  const entry = topology.entries.find((candidate) => candidate.entry.stableId === entryStableId);
548
618
  return entry ? routerHandlerNames(entry) : undefined;
@@ -587,6 +657,7 @@ module.exports = Object.freeze({
587
657
  staticString,
588
658
  recognizeManagedHandlerWrapper,
589
659
  recognizeHandlerSurface,
660
+ capabilityForHandlerSurface,
590
661
  classifyRecognizedHandlerSurface,
591
662
  extractFetchChain,
592
663
  extractEventEmitCall,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const crypto = require('node:crypto');
4
+ const { isTrustedLowerer } = require('@pulse-compute/wasm-contracts/library/manifest');
4
5
  const fs = require('node:fs');
5
6
  const { createRequire } = require('node:module');
6
7
  const path = require('node:path');
@@ -82,7 +83,7 @@ function catalogDescriptor(record, productRecord) {
82
83
  const nativeSupported = Boolean(
83
84
  product
84
85
  && ['supported', 'provider-dependent'].includes(product.targets.native.status)
85
- && compiler.trust === 'first-party'
86
+ && isTrustedLowerer(compiler.trust)
86
87
  && validation.status === 'ok'
87
88
  && wasm.mode !== 'lowering-plan-only'
88
89
  && wasm.sidecar
@@ -130,11 +131,13 @@ function discoverPackageContractCatalog(options = {}) {
130
131
  const records = discoverLowerableLibraryManifests({
131
132
  cwd,
132
133
  workspaceRoot: options.workspaceRoot,
134
+ packageDirs: options.packageDirs,
133
135
  scanNodeModules: options.scanNodeModules !== false
134
136
  });
135
137
  const productCatalog = discoverPulsePackageContracts({
136
138
  cwd,
137
139
  workspaceRoot: options.workspaceRoot,
140
+ packageDirs: options.packageDirs,
138
141
  scanNodeModules: options.scanNodeModules !== false
139
142
  });
140
143
  const productsByContract = new Map(productCatalog.contracts.map((entry) => [entry.contractId, { contract: entry, metadataHash: entry.metadataHash }]));
@@ -168,7 +171,7 @@ function discoverPackageContractCatalog(options = {}) {
168
171
  }),
169
172
  contracts: Object.freeze(contracts),
170
173
  policy: Object.freeze({
171
- trustedFirstPartyOnly: true,
174
+ trustedBuildDependencies: true,
172
175
  sourceSubstringSelection: false,
173
176
  graphReachabilitySelection: true,
174
177
  publicRegistration: false,
@@ -333,6 +336,10 @@ function canonicalRealizationArtifact(artifact, record) {
333
336
  { id, version, kind, mediaType, contractId, package: packageName }
334
337
  );
335
338
  }
339
+ if (kind === 'package-runtime-wasm') {
340
+ try { return require('@pulse-compute/wasm-contracts/package/runtime-binding').validateRuntimeBinding(value).artifact; }
341
+ catch { throw new PackageOperationSeamError('PULSE_PACKAGE_REALIZATION_ARTIFACT_INVALID', 'Package runtime Wasm must match the supported bounded ABI.', { id }); }
342
+ }
336
343
  return deepFreeze(value);
337
344
  }
338
345
 
@@ -615,6 +622,7 @@ function recognizeProjectPackageOperations(entryFile, options = {}) {
615
622
  const manifestRecords = discoverLowerableLibraryManifests({
616
623
  cwd: rootDir,
617
624
  workspaceRoot: options.workspaceRoot,
625
+ packageDirs: options.packageDirs,
618
626
  scanNodeModules: true
619
627
  });
620
628
  const selected = selectPackageRecords(manifestRecords, sourceText, options.selectedContracts);
@@ -638,8 +646,10 @@ function recognizeProjectPackageOperations(entryFile, options = {}) {
638
646
  // This source-aware step is package recognition and validation. The later
639
647
  // package-lowering phase receives only the canonical operations below.
640
648
  const result = buildPackageOwnedLoweringPlan({
649
+ typescript: ts,
641
650
  cwd: rootDir,
642
651
  workspaceRoot: options.workspaceRoot,
652
+ packageDirs: options.packageDirs,
643
653
  manifestRecords,
644
654
  contractId: record.manifest.contractId,
645
655
  sourcePath: sourceName,
@@ -666,8 +676,12 @@ function recognizeProjectPackageOperations(entryFile, options = {}) {
666
676
  .map(canonicalPackageSchemaReference)
667
677
  .map((reference) => schemaReferenceWithSourceFile(reference, sourceName));
668
678
  schemaReferences.push(...recognizedSchemaReferences);
669
- realizationArtifacts.push(...(contributions.realizationArtifacts || [])
670
- .map((artifact) => canonicalRealizationArtifact(artifact, record)));
679
+ const runtimeArtifacts = (contributions.realizationArtifacts || []).map(artifact => canonicalRealizationArtifact(artifact, record));
680
+ for (const effect of canonicalEffects.filter(effect => effect.kind === 'package.invoke')) {
681
+ try { require('@pulse-compute/wasm-contracts/package/runtime-binding').validateRuntimeInvocation(effect, runtimeArtifacts); }
682
+ catch { throw new PackageOperationSeamError('PULSE_PACKAGE_REALIZATION_ARTIFACT_INVALID', 'Package invocation requires an exact declared static runtime binding.', { contractId: record.manifest.contractId }); }
683
+ }
684
+ realizationArtifacts.push(...runtimeArtifacts);
671
685
  inspectionArtifacts.push(...(contributions.inspectionArtifacts || [])
672
686
  .map((artifact) => canonicalInspectionArtifact(artifact, record)));
673
687
  managedHandlers.push(...(contributions.managedHandlers || [])
@@ -736,7 +750,7 @@ function recognizeProjectPackageOperations(entryFile, options = {}) {
736
750
  lateLoweringReceivesSourceAst: false,
737
751
  lateLoweringReceivesSourceText: false,
738
752
  lateLoweringInput: CANONICAL_PACKAGE_OPERATION_VERSION,
739
- firstPartyBuildersOnly: true,
753
+ trustedBuildDependencies: true,
740
754
  externalRegistration: false
741
755
  })
742
756
  });
@@ -880,7 +894,7 @@ function combinePackageOperationRecognitions(recognitions = []) {
880
894
  lateLoweringReceivesSourceAst: false,
881
895
  lateLoweringReceivesSourceText: false,
882
896
  lateLoweringInput: CANONICAL_PACKAGE_OPERATION_VERSION,
883
- firstPartyBuildersOnly: true,
897
+ trustedBuildDependencies: true,
884
898
  externalRegistration: false
885
899
  })
886
900
  });
@@ -944,7 +958,7 @@ function packageOperationRecognitionFromEffects(effects = []) {
944
958
  lateLoweringReceivesSourceAst: false,
945
959
  lateLoweringReceivesSourceText: false,
946
960
  lateLoweringInput: CANONICAL_PACKAGE_OPERATION_VERSION,
947
- firstPartyBuildersOnly: true,
961
+ trustedBuildDependencies: true,
948
962
  externalRegistration: false
949
963
  })
950
964
  });
@@ -7,6 +7,7 @@ const {
7
7
  staticString,
8
8
  recognizeManagedHandlerWrapper,
9
9
  recognizeHandlerSurface,
10
+ capabilityForHandlerSurface,
10
11
  sourceModelForRouterRecognition
11
12
  } = require('./handler-surface-authority.js');
12
13
  const { normalizeManagedHandler } = require('./async-surface-normalizer.js');
@@ -130,17 +131,9 @@ function analysisForSurfaceFacts(surfaceFacts) {
130
131
  const capabilities = new Set();
131
132
  let fetchCount = 0;
132
133
  for (const fact of facts) {
133
- if (fact.surfaceId === 'ctx.req.body') { capabilities.add('request.body.forward'); continue; }
134
- if (fact.surfaceId.startsWith('ctx.fetch.')) { capabilities.add('fetch'); fetchCount += 1; continue; }
135
- if (fact.surfaceId.startsWith('ctx.req.')) capabilities.add(fact.surfaceId.slice('ctx.'.length));
136
- else if (fact.surfaceId.startsWith('ctx.config.')) capabilities.add('config.get');
137
- else if (fact.surfaceId.startsWith('ctx.secret.')) capabilities.add('secret.get');
138
- else if (fact.surfaceId.startsWith('ctx.kv.')) capabilities.add(fact.surfaceId.slice('ctx.'.length));
139
- else if (fact.surfaceId.startsWith('ctx.state.')) capabilities.add(fact.surfaceId.slice('ctx.'.length));
140
- else if (fact.surfaceId === 'ctx.emit') capabilities.add('event.emit');
141
- else if (fact.surfaceId.startsWith('ctx.log.')) capabilities.add('logging');
142
- else if (['ctx.json', 'ctx.text', 'ctx.response'].includes(fact.surfaceId)) capabilities.add(`response.${fact.surfaceId.slice('ctx.'.length)}`);
143
- else if (fact.surfaceId === 'ctx.param') capabilities.add('route.param');
134
+ const capability = capabilityForHandlerSurface(fact);
135
+ if (capability) capabilities.add(capability);
136
+ if (fact.surfaceId.startsWith('ctx.fetch.')) fetchCount += 1;
144
137
  }
145
138
  return Object.freeze({
146
139
  capabilities: Object.freeze([...capabilities].sort()),
@@ -283,7 +276,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
283
276
  || node.arguments.length !== helper.functionNode.parameters.length || !['route', 'middleware'].includes(role)) {
284
277
  diagnostics.push(diagnostic(sourceFile, original, 'PULSE_NATIVE_PURE_HELPER_CALL_UNSUPPORTED',
285
278
  'Pure helpers require a static unshadowed synchronous pure call in an HTTP handler.'));
286
- }
279
+ } else sourceModelForRouterRecognition(recognition)?.retainValidatedCall(entry, helper, original, sourceFile);
287
280
  return ts.factory.updateCallExpression(node, ts.factory.createIdentifier(helper.name), undefined,
288
281
  node.arguments.map(arg => ts.visitNode(arg, visit)));
289
282
  }
@@ -292,7 +285,7 @@ function normalizeRouterHandler(topology, descriptor, recognition, classificatio
292
285
  || !ts.isIdentifier(node.arguments[0]) || node.arguments[0].text !== ctxName
293
286
  || !['route', 'middleware'].includes(role)) {
294
287
  diagnostics.push(diagnostic(sourceFile, original, 'PULSE_NATIVE_HELPER_CALL_UNSUPPORTED', 'A source helper requires an unshadowed, directly awaited local binding and the current request context in an HTTP handler.'));
295
- }
288
+ } else sourceModelForRouterRecognition(recognition)?.retainValidatedCall(entry, helper, original, sourceFile);
296
289
  return ts.factory.updateCallExpression(node, ts.factory.createIdentifier(helper.name), undefined, node.arguments.slice(1));
297
290
  }
298
291
  if (errorName && ts.isShorthandPropertyAssignment(node) && node.name.text === errorName) {
@@ -3,6 +3,8 @@
3
3
  const { findLowerableManifestRecord } = require('@pulse-compute/wasm-library-kit/compiler/package-lowering');
4
4
  const { loadLowerableCompilerBuilder } = require('@pulse-compute/wasm-library-kit/compiler/handler-library-contracts');
5
5
  const { compileManagedHandlerNativeBundle } = require('./handler-ir-managed.js');
6
+ const { isTrustedLowerer } = require('@pulse-compute/wasm-contracts/library/manifest');
7
+ const path = require('node:path');
6
8
  const applications = new WeakMap();
7
9
 
8
10
  function attachTerminalPackageNative(compiled, inputs) {
@@ -14,8 +16,8 @@ function terminalPackageNativeForCompiled(compiled) {
14
16
  if (!inputs) return null;
15
17
  const { application, recognition, managedHandlers, schemaBundle, cwd, workspaceRoot } = inputs;
16
18
  const selected = recognition.packages.find(entry => entry.contractId === application.contractId);
17
- if (!selected || selected.builder.trust !== 'first-party') throw new TypeError('Native application requires its selected trusted lowerer.');
18
- const { record } = findLowerableManifestRecord({ cwd, workspaceRoot, contractId: selected.contractId, npmPackage: selected.package });
19
+ if (!selected || !isTrustedLowerer(selected.builder.trust)) throw new TypeError('Native application requires its selected trusted lowerer.');
20
+ const { record } = findLowerableManifestRecord({ cwd, workspaceRoot, packageDirs: recognition.dependencies.map(file => path.dirname(file)), contractId: selected.contractId, npmPackage: selected.package });
19
21
  const exportName = record?.manifest?.compiler?.nativeApplicationExport;
20
22
  if (!exportName) return null;
21
23
  const loaded = loadLowerableCompilerBuilder(record);