@telorun/kernel 0.4.1 → 0.5.0
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/dist/controller-loader.d.ts +19 -20
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +67 -247
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-loaders/napi-loader.d.ts +27 -0
- package/dist/controller-loaders/napi-loader.d.ts.map +1 -0
- package/dist/controller-loaders/napi-loader.js +158 -0
- package/dist/controller-loaders/napi-loader.js.map +1 -0
- package/dist/controller-loaders/npm-loader.d.ts +20 -0
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -0
- package/dist/controller-loaders/npm-loader.js +256 -0
- package/dist/controller-loaders/npm-loader.js.map +1 -0
- package/dist/controller-registry.d.ts +30 -20
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js +50 -99
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +11 -0
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +28 -1
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/controllers/resource-definition/abstract-controller.d.ts +35 -0
- package/dist/controllers/resource-definition/abstract-controller.d.ts.map +1 -0
- package/dist/controllers/resource-definition/abstract-controller.js +34 -0
- package/dist/controllers/resource-definition/abstract-controller.js.map +1 -0
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +35 -14
- package/dist/kernel.js.map +1 -1
- package/dist/manifest-schemas.d.ts +50 -0
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +31 -0
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context.d.ts +11 -1
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +6 -0
- package/dist/module-context.js.map +1 -1
- package/dist/resource-context.d.ts +2 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +6 -1
- package/dist/resource-context.js.map +1 -1
- package/dist/runtime-registry.d.ts +50 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +140 -0
- package/dist/runtime-registry.js.map +1 -0
- package/package.json +3 -3
- package/src/controller-loader.ts +77 -273
- package/src/controller-loaders/napi-loader.ts +191 -0
- package/src/controller-loaders/npm-loader.ts +285 -0
- package/src/controller-registry.ts +66 -129
- package/src/controllers/module/import-controller.ts +30 -1
- package/src/controllers/resource-definition/abstract-controller.ts +56 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
- package/src/kernel.ts +43 -13
- package/src/manifest-schemas.ts +33 -0
- package/src/module-context.ts +22 -1
- package/src/resource-context.ts +8 -1
- package/src/runtime-registry.ts +170 -0
package/src/kernel.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AnalysisRegistry, DEFAULT_MANIFEST_FILENAME, isModuleKind, Loader, StaticAnalyzer } from "@telorun/analyzer";
|
|
2
2
|
import {
|
|
3
3
|
ControllerContext,
|
|
4
|
+
ControllerPolicy,
|
|
4
5
|
Kernel as IKernel,
|
|
5
6
|
type LoadOptions,
|
|
6
7
|
ResourceContext,
|
|
@@ -23,8 +24,22 @@ import { EventStream } from "./event-stream.js";
|
|
|
23
24
|
import { EventBus } from "./events.js";
|
|
24
25
|
import { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
|
|
25
26
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
27
|
+
import { policyFingerprint } from "./runtime-registry.js";
|
|
26
28
|
import { SchemaValidator } from "./schema-validator.js";
|
|
27
29
|
|
|
30
|
+
/** Walks up the EvaluationContext parent chain to the nearest enclosing
|
|
31
|
+
* ModuleContext and returns its controller policy (or undefined). Used to
|
|
32
|
+
* pick the right cache entry when a kind has been loaded under multiple
|
|
33
|
+
* runtime selections. */
|
|
34
|
+
function findEnclosingPolicy(ctx: IEvaluationContext): ControllerPolicy | undefined {
|
|
35
|
+
let cur: IEvaluationContext | undefined = ctx;
|
|
36
|
+
while (cur) {
|
|
37
|
+
if (cur instanceof ModuleContext) return cur.getControllerPolicy();
|
|
38
|
+
cur = cur.parent;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
export interface KernelOptions {
|
|
29
44
|
stdin?: NodeJS.ReadableStream;
|
|
30
45
|
stdout?: NodeJS.WritableStream;
|
|
@@ -83,8 +98,13 @@ export class Kernel implements IKernel {
|
|
|
83
98
|
moduleName: string,
|
|
84
99
|
kindName: string,
|
|
85
100
|
controllerInstance: any,
|
|
101
|
+
fingerprint?: string,
|
|
86
102
|
): Promise<void> {
|
|
87
|
-
this.controllers.registerController(
|
|
103
|
+
this.controllers.registerController(
|
|
104
|
+
`${moduleName}.${kindName}`,
|
|
105
|
+
controllerInstance,
|
|
106
|
+
fingerprint,
|
|
107
|
+
);
|
|
88
108
|
await controllerInstance.register?.(this.createControllerContext(`${moduleName}.${kindName}`));
|
|
89
109
|
}
|
|
90
110
|
|
|
@@ -148,6 +168,10 @@ export class Kernel implements IKernel {
|
|
|
148
168
|
"Telo.Definition",
|
|
149
169
|
await import("./controllers/resource-definition/resource-definition-controller.js"),
|
|
150
170
|
);
|
|
171
|
+
this.controllers.registerController(
|
|
172
|
+
"Telo.Abstract",
|
|
173
|
+
await import("./controllers/resource-definition/abstract-controller.js"),
|
|
174
|
+
);
|
|
151
175
|
const moduleController = await import("./controllers/module/module-controller.js");
|
|
152
176
|
this.controllers.registerController("Telo.Application", moduleController);
|
|
153
177
|
this.controllers.registerController("Telo.Library", moduleController);
|
|
@@ -260,10 +284,12 @@ export class Kernel implements IKernel {
|
|
|
260
284
|
* Phase 2: Start - Initialize resources
|
|
261
285
|
*/
|
|
262
286
|
async start(): Promise<void> {
|
|
263
|
-
// Call
|
|
264
|
-
|
|
287
|
+
// Call register hooks for controllers actually loaded at this point (built-ins).
|
|
288
|
+
// User-module kinds load their controllers during Phase 3 (Telo.Definition.init),
|
|
289
|
+
// and registerController() fires their register hook there.
|
|
290
|
+
for (const kind of this.controllers.getControllerKinds()) {
|
|
265
291
|
const controller = this.controllers.getController(kind);
|
|
266
|
-
if (controller
|
|
292
|
+
if (controller.register) {
|
|
267
293
|
await controller.register(this.createControllerContext(`controller:${kind}`));
|
|
268
294
|
}
|
|
269
295
|
}
|
|
@@ -475,12 +501,13 @@ export class Kernel implements IKernel {
|
|
|
475
501
|
// resolveKind() throws with a clear message if the alias or kind is not found.
|
|
476
502
|
const resolvedKind = this.rootContext.resolveKind(kind);
|
|
477
503
|
|
|
478
|
-
const
|
|
504
|
+
const fingerprint = policyFingerprint(findEnclosingPolicy(evalContext));
|
|
505
|
+
const controller = this.controllers.getControllerOrUndefined(resolvedKind, fingerprint);
|
|
479
506
|
if (!controller) {
|
|
480
507
|
const kindInfo =
|
|
481
508
|
resolvedKind !== kind ? `'${kind}' (resolved to '${resolvedKind}')` : `'${kind}'`;
|
|
482
509
|
throw new Error(
|
|
483
|
-
`No controller registered for kind ${kindInfo}, known controllers are: ${this.controllers.getKinds().join(", ")}`,
|
|
510
|
+
`No controller registered for kind ${kindInfo} (runtime fingerprint "${fingerprint}"), known controllers are: ${this.controllers.getKinds().join(", ")}`,
|
|
484
511
|
);
|
|
485
512
|
}
|
|
486
513
|
|
|
@@ -539,14 +566,17 @@ export class Kernel implements IKernel {
|
|
|
539
566
|
|
|
540
567
|
if (!runtime.length) return { instance, ctx };
|
|
541
568
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
569
|
+
// Override invoke in-place so all lifecycle methods (init/invoke/teardown/snapshot)
|
|
570
|
+
// share the same `this`. A wrapper object would split identity: state mutated by
|
|
571
|
+
// init() on the wrapper would be invisible to the original invoke(), which still
|
|
572
|
+
// runs with `this === instance`. Mutating in place also preserves the prototype
|
|
573
|
+
// chain — class-declared methods remain reachable.
|
|
574
|
+
const originalInvoke = instance.invoke!.bind(instance);
|
|
575
|
+
instance.invoke = async (inputs: any) => {
|
|
576
|
+
const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
|
|
577
|
+
return originalInvoke(expanded);
|
|
548
578
|
};
|
|
549
|
-
return { instance
|
|
579
|
+
return { instance, ctx };
|
|
550
580
|
}
|
|
551
581
|
|
|
552
582
|
/**
|
package/src/manifest-schemas.ts
CHANGED
|
@@ -49,6 +49,16 @@ const throwsSchema = {
|
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/** Alias-form pattern for `extends` values: "<Alias>.<AbstractName>".
|
|
53
|
+
* Resolved against the declaring file's `Telo.Import` aliases — identical to how
|
|
54
|
+
* kind prefixes work (e.g. `kind: Http.Api` resolves `Http` via the importer's
|
|
55
|
+
* alias registration). Identity form (`std/mod#Name`) is intentionally not
|
|
56
|
+
* accepted: aliases carry the module version via their `Telo.Import` source,
|
|
57
|
+
* which canonical module names can't.
|
|
58
|
+
* - Alias: PascalCase (first letter uppercase)
|
|
59
|
+
* - Name: PascalCase */
|
|
60
|
+
const EXTENDS_ALIAS_PATTERN = "^[A-Z][A-Za-z0-9_]*\\.[A-Z][A-Za-z0-9_]*$";
|
|
61
|
+
|
|
52
62
|
const baseDefinition = {
|
|
53
63
|
type: "object",
|
|
54
64
|
required: ["kind", "metadata"],
|
|
@@ -56,6 +66,7 @@ const baseDefinition = {
|
|
|
56
66
|
kind: { const: "Telo.Definition" },
|
|
57
67
|
metadata: metadataSchema,
|
|
58
68
|
capability: { type: "string" },
|
|
69
|
+
extends: { type: "string", pattern: EXTENDS_ALIAS_PATTERN },
|
|
59
70
|
schema: { type: "object", additionalProperties: true },
|
|
60
71
|
controllers: { type: "array", items: { type: "string" } },
|
|
61
72
|
throws: throwsSchema,
|
|
@@ -114,11 +125,33 @@ export const ResourceDefinitionSchema = {
|
|
|
114
125
|
],
|
|
115
126
|
};
|
|
116
127
|
|
|
128
|
+
/** Schema for `kind: Telo.Abstract`. Library-declared abstracts are type blueprints —
|
|
129
|
+
* they may carry an optional `capability` (lifecycle inherited by implementations)
|
|
130
|
+
* and an optional `schema` (shared base for implementations). `controllers` and `throws`
|
|
131
|
+
* are forbidden (no runtime implementation; throws lives on concrete definitions).
|
|
132
|
+
* Other fields are permitted for forward compatibility with typed-abstracts work
|
|
133
|
+
* (inputType, outputType, …) — Telo.Abstract is an extension point by design. */
|
|
134
|
+
export const ResourceAbstractSchema = {
|
|
135
|
+
type: "object",
|
|
136
|
+
required: ["kind", "metadata"],
|
|
137
|
+
properties: {
|
|
138
|
+
kind: { const: "Telo.Abstract" },
|
|
139
|
+
metadata: metadataSchema,
|
|
140
|
+
capability: { type: "string" },
|
|
141
|
+
schema: { type: "object", additionalProperties: true },
|
|
142
|
+
},
|
|
143
|
+
not: {
|
|
144
|
+
anyOf: [{ required: ["controllers"] }, { required: ["throws"] }],
|
|
145
|
+
},
|
|
146
|
+
additionalProperties: true,
|
|
147
|
+
};
|
|
148
|
+
|
|
117
149
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
118
150
|
addFormats.default(ajv);
|
|
119
151
|
|
|
120
152
|
export const validateRuntimeResource = ajv.compile(RuntimeResourceSchema);
|
|
121
153
|
export const validateResourceDefinition = ajv.compile(ResourceDefinitionSchema);
|
|
154
|
+
export const validateResourceAbstract = ajv.compile(ResourceAbstractSchema);
|
|
122
155
|
|
|
123
156
|
export function formatAjvErrors(errors: any[] | null | undefined): string {
|
|
124
157
|
if (!errors || errors.length === 0) return "Unknown schema error";
|
package/src/module-context.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ControllerPolicy,
|
|
3
|
+
Invocable,
|
|
4
|
+
ModuleContext as IModuleContext,
|
|
5
|
+
} from "@telorun/sdk";
|
|
2
6
|
import type { EmitEvent, InstanceFactory } from "@telorun/sdk";
|
|
3
7
|
import { EvaluationContext } from "./evaluation-context.js";
|
|
4
8
|
|
|
@@ -55,6 +59,15 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
55
59
|
/** Maps import alias → allowed kind names. Absent entry = unrestricted (e.g. Kernel). */
|
|
56
60
|
private readonly importedKinds = new Map<string, Set<string>>();
|
|
57
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Resolved controller-selection policy for this module's `Telo.Definition`s.
|
|
64
|
+
* Stamped by the parent `Telo.Import` controller from the import's `runtime:`
|
|
65
|
+
* field; read by `Telo.Definition.init` (via `ResourceContext.getControllerPolicy`)
|
|
66
|
+
* when invoking `ControllerLoader.load`. `undefined` means "no policy set" —
|
|
67
|
+
* loader treats it as `auto`.
|
|
68
|
+
*/
|
|
69
|
+
private _controllerPolicy: ControllerPolicy | undefined;
|
|
70
|
+
|
|
58
71
|
constructor(
|
|
59
72
|
source: string,
|
|
60
73
|
variables: Record<string, unknown> = {},
|
|
@@ -103,6 +116,14 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
103
116
|
this._rebuildContext();
|
|
104
117
|
}
|
|
105
118
|
|
|
119
|
+
setControllerPolicy(policy: ControllerPolicy | undefined): void {
|
|
120
|
+
this._controllerPolicy = policy;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getControllerPolicy(): ControllerPolicy | undefined {
|
|
124
|
+
return this._controllerPolicy;
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
protected override onResourceSnapshotted(name: string, snap: Record<string, unknown>): void {
|
|
107
128
|
this.setResource(name, snap);
|
|
108
129
|
}
|
package/src/resource-context.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
RuntimeError,
|
|
7
7
|
RuntimeResource,
|
|
8
8
|
isCompiledValue,
|
|
9
|
+
type ControllerPolicy,
|
|
9
10
|
type EvaluationContext as IEvaluationContext,
|
|
10
11
|
type LoadOptions,
|
|
11
12
|
type ModuleContext,
|
|
@@ -17,6 +18,7 @@ import AjvModule from "ajv";
|
|
|
17
18
|
import addFormats from "ajv-formats";
|
|
18
19
|
import { Kernel } from "./kernel.js";
|
|
19
20
|
import { formatAjvErrors } from "./manifest-schemas.js";
|
|
21
|
+
import { policyFingerprint } from "./runtime-registry.js";
|
|
20
22
|
import { SchemaValidator } from "./schema-validator.js";
|
|
21
23
|
|
|
22
24
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
@@ -261,13 +263,18 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
261
263
|
kindName: string,
|
|
262
264
|
controllerInstance: any,
|
|
263
265
|
): Promise<void> {
|
|
264
|
-
|
|
266
|
+
const fingerprint = policyFingerprint(this.moduleContext.getControllerPolicy());
|
|
267
|
+
await this.kernel.registerController(moduleName, kindName, controllerInstance, fingerprint);
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
registerDefinition(def: any) {
|
|
268
271
|
this.kernel.registerResourceDefinition(def);
|
|
269
272
|
}
|
|
270
273
|
|
|
274
|
+
getControllerPolicy(): ControllerPolicy | undefined {
|
|
275
|
+
return this.moduleContext.getControllerPolicy();
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
on(event: string, handler: (payload?: any) => void | Promise<void>): void {
|
|
272
279
|
this.kernel.on(event, handler);
|
|
273
280
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { ControllerPolicy } from "@telorun/sdk";
|
|
2
|
+
import { RuntimeError } from "@telorun/sdk";
|
|
3
|
+
|
|
4
|
+
export type { ControllerPolicy } from "@telorun/sdk";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The PURL-type prefix the kernel itself runs (i.e. "no FFI" controllers
|
|
8
|
+
* for this kernel). For the Node.js kernel, that's `pkg:npm`. A future
|
|
9
|
+
* Rust kernel reports `pkg:cargo` here.
|
|
10
|
+
*/
|
|
11
|
+
export const KERNEL_NATIVE_PURL_TYPE = "pkg:npm";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wildcard sentinel inside a resolved `ControllerPolicy.load`. Means
|
|
15
|
+
* "all remaining controllers in declaration order, minus PURL types
|
|
16
|
+
* already listed earlier in the same policy." May appear at most once.
|
|
17
|
+
*/
|
|
18
|
+
export const POLICY_WILDCARD = "*";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maps user-facing runtime labels to PURL-type prefixes. The user-facing
|
|
22
|
+
* label is the implementation directory name a contributor sees at
|
|
23
|
+
* `modules/<name>/<label>/`.
|
|
24
|
+
*/
|
|
25
|
+
const LABEL_TO_PURL_TYPE: Readonly<Record<string, string>> = {
|
|
26
|
+
nodejs: "pkg:npm",
|
|
27
|
+
rust: "pkg:cargo",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Labels that only make sense as a single value (`runtime: auto`,
|
|
32
|
+
* `runtime: native`) — they describe a whole policy, not one slot in a
|
|
33
|
+
* list. `any` is also reserved but is allowed as the final list entry,
|
|
34
|
+
* so it is handled separately in `normalizeRuntime`.
|
|
35
|
+
*/
|
|
36
|
+
const SINGLE_ONLY_LABELS = new Set(["auto", "native"]);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Default policy for missing `runtime:` field — equivalent to `runtime: auto`.
|
|
40
|
+
* Tries kernel-native first, then any other declared controller in declaration order.
|
|
41
|
+
*/
|
|
42
|
+
export const DEFAULT_POLICY: ControllerPolicy = {
|
|
43
|
+
load: [KERNEL_NATIVE_PURL_TYPE, POLICY_WILDCARD],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a `runtime:` field value (string, array, or undefined) into a
|
|
48
|
+
* canonical `ControllerPolicy`. Throws `ERR_RUNTIME_INVALID` on:
|
|
49
|
+
* - empty array
|
|
50
|
+
* - unknown label
|
|
51
|
+
* - `any` anywhere but the final list entry
|
|
52
|
+
* - duplicate label
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeRuntime(value: string | ReadonlyArray<string> | undefined): ControllerPolicy {
|
|
55
|
+
if (value === undefined) {
|
|
56
|
+
return DEFAULT_POLICY;
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return resolveSingle(value);
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(value)) {
|
|
62
|
+
throw new RuntimeError(
|
|
63
|
+
"ERR_RUNTIME_INVALID",
|
|
64
|
+
`runtime must be a string or array of strings, got ${typeof value}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (value.length === 0) {
|
|
68
|
+
throw new RuntimeError(
|
|
69
|
+
"ERR_RUNTIME_INVALID",
|
|
70
|
+
"runtime: [] has no useful meaning. Omit the field for `auto`, or list at least one runtime label.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const load: string[] = [];
|
|
74
|
+
for (let i = 0; i < value.length; i++) {
|
|
75
|
+
const entry = value[i];
|
|
76
|
+
if (typeof entry !== "string") {
|
|
77
|
+
throw new RuntimeError(
|
|
78
|
+
"ERR_RUNTIME_INVALID",
|
|
79
|
+
`runtime list entries must be strings, got ${typeof entry}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (entry === "any") {
|
|
83
|
+
if (i !== value.length - 1) {
|
|
84
|
+
throw new RuntimeError(
|
|
85
|
+
"ERR_RUNTIME_INVALID",
|
|
86
|
+
"runtime: 'any' may only appear as the last entry in the list",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
load.push(POLICY_WILDCARD);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const purlType = labelToPurlType(entry);
|
|
93
|
+
if (load.includes(purlType)) {
|
|
94
|
+
throw new RuntimeError(
|
|
95
|
+
"ERR_RUNTIME_INVALID",
|
|
96
|
+
`runtime: '${entry}' listed twice (resolves to ${purlType})`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
load.push(purlType);
|
|
100
|
+
}
|
|
101
|
+
return { load };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveSingle(label: string): ControllerPolicy {
|
|
105
|
+
if (label === "auto") {
|
|
106
|
+
return DEFAULT_POLICY;
|
|
107
|
+
}
|
|
108
|
+
if (label === "native") {
|
|
109
|
+
return { load: [KERNEL_NATIVE_PURL_TYPE] };
|
|
110
|
+
}
|
|
111
|
+
if (label === "any") {
|
|
112
|
+
return { load: [POLICY_WILDCARD] };
|
|
113
|
+
}
|
|
114
|
+
return { load: [labelToPurlType(label)] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function labelToPurlType(label: string): string {
|
|
118
|
+
if (SINGLE_ONLY_LABELS.has(label)) {
|
|
119
|
+
throw new RuntimeError(
|
|
120
|
+
"ERR_RUNTIME_INVALID",
|
|
121
|
+
`runtime label '${label}' describes a whole policy and is only valid as a single value, not inside a list`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const purlType = LABEL_TO_PURL_TYPE[label];
|
|
125
|
+
if (!purlType) {
|
|
126
|
+
const known = Object.keys(LABEL_TO_PURL_TYPE).concat(["auto", "native", "any"]).sort().join(", ");
|
|
127
|
+
throw new RuntimeError(
|
|
128
|
+
"ERR_RUNTIME_INVALID",
|
|
129
|
+
`Unknown runtime label '${label}'. Known: ${known}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return purlType;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Stable short hash of a resolved policy, for use as a registry cache key
|
|
137
|
+
* suffix. Two imports with the same resolved policy share a cached
|
|
138
|
+
* controller; divergent policies get separate entries.
|
|
139
|
+
*
|
|
140
|
+
* Both `undefined` (no policy stamped) and any policy structurally equal to
|
|
141
|
+
* `DEFAULT_POLICY` (`runtime: auto`, missing `runtime:`, or any list that
|
|
142
|
+
* normalizes to the auto shape) collapse to the `"default"` fingerprint —
|
|
143
|
+
* the plan's contract is that "missing" is sugar for "auto", so they must
|
|
144
|
+
* share a cache entry.
|
|
145
|
+
*/
|
|
146
|
+
export function policyFingerprint(policy: ControllerPolicy | undefined): string {
|
|
147
|
+
if (!policy || isDefaultPolicy(policy)) {
|
|
148
|
+
return "default";
|
|
149
|
+
}
|
|
150
|
+
return policy.load.join(",");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Structural equality check against `DEFAULT_POLICY`. Used at policy-stamp
|
|
155
|
+
* time (import-controller) to skip stamping when the resolved policy is the
|
|
156
|
+
* canonical default — `runtime: auto`, `runtime: [nodejs, any]`, etc. all
|
|
157
|
+
* normalize to the same shape and should be observationally identical to a
|
|
158
|
+
* plain omitted `runtime:` field, both at the fingerprint level (handled by
|
|
159
|
+
* `policyFingerprint`) and at the policy-presence level.
|
|
160
|
+
*/
|
|
161
|
+
export function isDefaultPolicy(policy: ControllerPolicy): boolean {
|
|
162
|
+
if (policy === DEFAULT_POLICY) return true;
|
|
163
|
+
const a = policy.load;
|
|
164
|
+
const b = DEFAULT_POLICY.load;
|
|
165
|
+
if (a.length !== b.length) return false;
|
|
166
|
+
for (let i = 0; i < a.length; i++) {
|
|
167
|
+
if (a[i] !== b[i]) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|