@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 +3 -3
- package/package.json +9 -9
- package/src/canonical-native-compiler.js +8 -1
- package/src/canonical-project-compiler.js +8 -1
- package/src/javascript-application-plan.js +1 -0
- package/src/project/npm-source-resolution.js +91 -0
- package/src/project/package-reachability.js +2 -1
- package/src/project/reachable-graph-builder.js +131 -19
- package/src/report-handler-topology.js +104 -0
- package/src/report-reference-provenance.js +5 -3
- package/src/report-router-control.js +42 -0
- package/src/report-source-inputs.js +55 -0
- package/src/spine/binding-ownership.js +13 -0
- package/src/spine/handler-surface-authority.js +71 -0
- package/src/spine/package-operation-seam.js +21 -7
- package/src/spine/router-handler-frontend.js +6 -13
- package/src/spine/terminal-package-native.js +4 -2
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
13
|
-
"@pulse-compute/wasm-contracts": "1.0.0-beta.
|
|
14
|
-
"@pulse-compute/wasm-guest-link": "1.0.0-beta.
|
|
15
|
-
"@pulse-compute/wasm-host-runtime": "1.0.0-beta.
|
|
16
|
-
"@pulse-compute/wasm-library-kit": "1.0.0-beta.
|
|
17
|
-
"@pulse-compute/wasm-runtime-core-as": "1.0.0-beta.
|
|
18
|
-
"@pulse-compute/wasm-schema-json": "1.0.0-beta.
|
|
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.
|
|
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,
|
|
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 = '
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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))
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
if (
|
|
135
|
-
if (fact.surfaceId.startsWith('ctx.
|
|
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
|
|
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);
|