@telorun/kernel 0.4.1 → 0.6.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 +30 -3
- 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/evaluation-context.js +1 -1
- package/dist/evaluation-context.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +14 -16
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +58 -49
- 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/{manifest-adapters/local-file-adapter.d.ts → manifest-sources/local-file-source.d.ts} +3 -3
- package/dist/manifest-sources/local-file-source.d.ts.map +1 -0
- package/dist/{manifest-adapters/local-file-adapter.js → manifest-sources/local-file-source.js} +2 -2
- package/dist/manifest-sources/local-file-source.js.map +1 -0
- package/dist/manifest-sources/memory-source.d.ts +23 -0
- package/dist/manifest-sources/memory-source.d.ts.map +1 -0
- package/dist/manifest-sources/memory-source.js +83 -0
- package/dist/manifest-sources/memory-source.js.map +1 -0
- package/dist/module-context.d.ts +17 -3
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +19 -1
- package/dist/module-context.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +0 -1
- package/dist/registry.js.map +1 -1
- package/dist/resource-context.d.ts +2 -7
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +8 -28
- 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 +16 -5
- 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 +32 -3
- package/src/controllers/resource-definition/abstract-controller.ts +56 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
- package/src/evaluation-context.ts +1 -1
- package/src/index.ts +2 -1
- package/src/kernel.ts +86 -67
- package/src/manifest-schemas.ts +33 -0
- package/src/{manifest-adapters/local-file-adapter.ts → manifest-sources/local-file-source.ts} +2 -2
- package/src/manifest-sources/memory-source.ts +104 -0
- package/src/module-context.ts +36 -3
- package/src/registry.ts +0 -1
- package/src/resource-context.ts +11 -36
- package/src/runtime-registry.ts +170 -0
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +0 -1
- package/dist/manifest-adapters/local-file-adapter.js.map +0 -1
- package/dist/manifest-adapters/manifest-adapter.d.ts +0 -35
- package/dist/manifest-adapters/manifest-adapter.d.ts.map +0 -1
- package/dist/manifest-adapters/manifest-adapter.js +0 -2
- package/dist/manifest-adapters/manifest-adapter.js.map +0 -1
- package/src/manifest-adapters/manifest-adapter.ts +0 -35
package/src/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { ControllerLoader } from "./controller-loader.js";
|
|
2
2
|
export { ControllerRegistry } from "./controller-registry.js";
|
|
3
3
|
export { EvaluationContext } from "./evaluation-context.js";
|
|
4
|
-
export {
|
|
4
|
+
export { LocalFileSource } from "./manifest-sources/local-file-source.js";
|
|
5
|
+
export { MemorySource } from "./manifest-sources/memory-source.js";
|
|
5
6
|
export { EventStream } from "./event-stream.js";
|
|
6
7
|
export { ExecutionContext } from "./execution-context.js";
|
|
7
8
|
export { Kernel, type KernelOptions } from "./kernel.js";
|
package/src/kernel.ts
CHANGED
|
@@ -1,37 +1,65 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AnalysisRegistry,
|
|
3
|
+
isModuleKind,
|
|
4
|
+
Loader,
|
|
5
|
+
StaticAnalyzer,
|
|
6
|
+
type ManifestSource,
|
|
7
|
+
} from "@telorun/analyzer";
|
|
2
8
|
import {
|
|
3
9
|
ControllerContext,
|
|
10
|
+
ControllerPolicy,
|
|
4
11
|
Kernel as IKernel,
|
|
5
|
-
|
|
12
|
+
isCompiledValue,
|
|
6
13
|
ResourceContext,
|
|
7
14
|
ResourceDefinition,
|
|
8
15
|
ResourceInstance,
|
|
9
16
|
ResourceManifest,
|
|
10
17
|
RuntimeError,
|
|
11
18
|
RuntimeEvent,
|
|
12
|
-
isCompiledValue,
|
|
13
19
|
type EvaluationContext as IEvaluationContext,
|
|
14
20
|
type ModuleContext as IModuleContext,
|
|
21
|
+
type LoadOptions,
|
|
15
22
|
type ParsedArgs,
|
|
16
23
|
} from "@telorun/sdk";
|
|
17
24
|
import { createHash } from "node:crypto";
|
|
18
|
-
import { ModuleContext } from "./module-context.js";
|
|
19
|
-
import * as path from "path";
|
|
20
25
|
import { parseArgs } from "util";
|
|
21
26
|
import { ControllerRegistry } from "./controller-registry.js";
|
|
22
27
|
import { EventStream } from "./event-stream.js";
|
|
23
28
|
import { EventBus } from "./events.js";
|
|
24
|
-
import {
|
|
29
|
+
import { ModuleContext } from "./module-context.js";
|
|
25
30
|
import { ResourceContextImpl } from "./resource-context.js";
|
|
31
|
+
import { policyFingerprint } from "./runtime-registry.js";
|
|
26
32
|
import { SchemaValidator } from "./schema-validator.js";
|
|
27
33
|
|
|
34
|
+
/** Walks up the EvaluationContext parent chain to the nearest enclosing
|
|
35
|
+
* ModuleContext and returns its controller policy (or undefined). Used to
|
|
36
|
+
* pick the right cache entry when a kind has been loaded under multiple
|
|
37
|
+
* runtime selections. */
|
|
38
|
+
function findEnclosingModule(ctx: IEvaluationContext): ModuleContext | undefined {
|
|
39
|
+
let cur: IEvaluationContext | undefined = ctx;
|
|
40
|
+
while (cur) {
|
|
41
|
+
if (cur instanceof ModuleContext) return cur;
|
|
42
|
+
cur = cur.parent;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findEnclosingPolicy(ctx: IEvaluationContext): ControllerPolicy | undefined {
|
|
48
|
+
return findEnclosingModule(ctx)?.getControllerPolicy();
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
export interface KernelOptions {
|
|
29
52
|
stdin?: NodeJS.ReadableStream;
|
|
30
53
|
stdout?: NodeJS.WritableStream;
|
|
31
54
|
stderr?: NodeJS.WritableStream;
|
|
32
55
|
env?: Record<string, string | undefined>;
|
|
33
56
|
argv?: string[];
|
|
34
|
-
/**
|
|
57
|
+
/** Manifest sources the kernel uses to resolve URLs passed to `load()`.
|
|
58
|
+
* Required: pass an explicit list (`[]` is allowed but means every URL
|
|
59
|
+
* fails to dispatch). Order matters — later entries take priority over
|
|
60
|
+
* earlier ones (sources are unshifted onto the dispatch chain). */
|
|
61
|
+
sources: ManifestSource[];
|
|
62
|
+
/** Base URL for the registry source. When unset, the `RegistrySource`
|
|
35
63
|
* default applies. Callers (e.g. the CLI) are responsible for resolving
|
|
36
64
|
* `TELO_REGISTRY_URL` or any other env-based fallback before passing. */
|
|
37
65
|
registryUrl?: string;
|
|
@@ -67,7 +95,7 @@ export class Kernel implements IKernel {
|
|
|
67
95
|
readonly argv: string[];
|
|
68
96
|
readonly registryUrl: string | undefined;
|
|
69
97
|
|
|
70
|
-
constructor(options: KernelOptions
|
|
98
|
+
constructor(options: KernelOptions) {
|
|
71
99
|
this.stdin = options.stdin ?? process.stdin;
|
|
72
100
|
this.stdout = options.stdout ?? process.stdout;
|
|
73
101
|
this.stderr = options.stderr ?? process.stderr;
|
|
@@ -75,7 +103,9 @@ export class Kernel implements IKernel {
|
|
|
75
103
|
this.argv = options.argv ?? [];
|
|
76
104
|
this.registryUrl = options.registryUrl;
|
|
77
105
|
this.loader = new Loader({ registryUrl: this.registryUrl, celHandlers });
|
|
78
|
-
|
|
106
|
+
for (const source of options.sources) {
|
|
107
|
+
this.loader.register(source);
|
|
108
|
+
}
|
|
79
109
|
this.setupEventStreaming();
|
|
80
110
|
}
|
|
81
111
|
|
|
@@ -83,8 +113,13 @@ export class Kernel implements IKernel {
|
|
|
83
113
|
moduleName: string,
|
|
84
114
|
kindName: string,
|
|
85
115
|
controllerInstance: any,
|
|
116
|
+
fingerprint?: string,
|
|
86
117
|
): Promise<void> {
|
|
87
|
-
this.controllers.registerController(
|
|
118
|
+
this.controllers.registerController(
|
|
119
|
+
`${moduleName}.${kindName}`,
|
|
120
|
+
controllerInstance,
|
|
121
|
+
fingerprint,
|
|
122
|
+
);
|
|
88
123
|
await controllerInstance.register?.(this.createControllerContext(`${moduleName}.${kindName}`));
|
|
89
124
|
}
|
|
90
125
|
|
|
@@ -96,24 +131,6 @@ export class Kernel implements IKernel {
|
|
|
96
131
|
this.registry.registerDefinition(definition);
|
|
97
132
|
}
|
|
98
133
|
|
|
99
|
-
getModuleContext(_moduleName: string): ModuleContext {
|
|
100
|
-
return this.rootContext;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
resolveModuleAlias(_declaringModule: string, alias: string): string | undefined {
|
|
104
|
-
return this.rootContext.importAliases.get(alias);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
registerModuleImport(
|
|
108
|
-
_declaringModule: string,
|
|
109
|
-
alias: string,
|
|
110
|
-
targetModule: string,
|
|
111
|
-
kinds: string[],
|
|
112
|
-
): void {
|
|
113
|
-
this.rootContext.registerImport(alias, targetModule, kinds);
|
|
114
|
-
this.registry.registerImport(alias, targetModule, kinds);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
134
|
loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]> {
|
|
118
135
|
return this.loader.loadModule(url, options);
|
|
119
136
|
}
|
|
@@ -148,6 +165,10 @@ export class Kernel implements IKernel {
|
|
|
148
165
|
"Telo.Definition",
|
|
149
166
|
await import("./controllers/resource-definition/resource-definition-controller.js"),
|
|
150
167
|
);
|
|
168
|
+
this.controllers.registerController(
|
|
169
|
+
"Telo.Abstract",
|
|
170
|
+
await import("./controllers/resource-definition/abstract-controller.js"),
|
|
171
|
+
);
|
|
151
172
|
const moduleController = await import("./controllers/module/module-controller.js");
|
|
152
173
|
this.controllers.registerController("Telo.Application", moduleController);
|
|
153
174
|
this.controllers.registerController("Telo.Library", moduleController);
|
|
@@ -158,11 +179,12 @@ export class Kernel implements IKernel {
|
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
/**
|
|
161
|
-
* Load
|
|
182
|
+
* Load a manifest by URL. The URL is dispatched through the registered
|
|
183
|
+
* `ManifestSource` chain (file://, http://, pkg:, memory://, …); URL-shape
|
|
184
|
+
* normalization is each source's responsibility.
|
|
162
185
|
*/
|
|
163
|
-
async
|
|
164
|
-
const
|
|
165
|
-
const sourceUrl = await this.loader.resolveEntryPoint(resolvedUrl);
|
|
186
|
+
async load(url: string): Promise<void> {
|
|
187
|
+
const sourceUrl = await this.loader.resolveEntryPoint(url);
|
|
166
188
|
this.rootContext = new ModuleContext(
|
|
167
189
|
sourceUrl,
|
|
168
190
|
{},
|
|
@@ -246,24 +268,16 @@ export class Kernel implements IKernel {
|
|
|
246
268
|
}
|
|
247
269
|
}
|
|
248
270
|
|
|
249
|
-
/**
|
|
250
|
-
* Phase 1: Load - Ingest files from directory and load runtime config
|
|
251
|
-
* @deprecated Use loadFromConfig instead
|
|
252
|
-
*/
|
|
253
|
-
async loadDirectory(dirPath: string): Promise<void> {
|
|
254
|
-
const configYamlPath = path.join(dirPath, DEFAULT_MANIFEST_FILENAME);
|
|
255
|
-
|
|
256
|
-
await this.loadFromConfig(configYamlPath);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
271
|
/**
|
|
260
272
|
* Phase 2: Start - Initialize resources
|
|
261
273
|
*/
|
|
262
274
|
async start(): Promise<void> {
|
|
263
|
-
// Call
|
|
264
|
-
|
|
275
|
+
// Call register hooks for controllers actually loaded at this point (built-ins).
|
|
276
|
+
// User-module kinds load their controllers during Phase 3 (Telo.Definition.init),
|
|
277
|
+
// and registerController() fires their register hook there.
|
|
278
|
+
for (const kind of this.controllers.getControllerKinds()) {
|
|
265
279
|
const controller = this.controllers.getController(kind);
|
|
266
|
-
if (controller
|
|
280
|
+
if (controller.register) {
|
|
267
281
|
await controller.register(this.createControllerContext(`controller:${kind}`));
|
|
268
282
|
}
|
|
269
283
|
}
|
|
@@ -381,10 +395,6 @@ export class Kernel implements IKernel {
|
|
|
381
395
|
return {
|
|
382
396
|
on: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
383
397
|
this.eventBus.on(event, handler),
|
|
384
|
-
once: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
385
|
-
this.eventBus.once(event, handler),
|
|
386
|
-
off: (event: string, handler: (event: RuntimeEvent) => void | Promise<void>) =>
|
|
387
|
-
this.eventBus.off(event, handler),
|
|
388
398
|
emit: (event: string, payload?: any) => {
|
|
389
399
|
const namespaced = event.includes(".") ? event : `${kind}.${event}`;
|
|
390
400
|
void this.eventBus.emit(namespaced, payload);
|
|
@@ -471,16 +481,19 @@ export class Kernel implements IKernel {
|
|
|
471
481
|
): Promise<{ instance: ResourceInstance; ctx: ResourceContext } | null> {
|
|
472
482
|
const kind = resource.kind;
|
|
473
483
|
|
|
474
|
-
// Resolve the alias-prefixed kind to its real fully-qualified kind
|
|
475
|
-
//
|
|
476
|
-
|
|
484
|
+
// Resolve the alias-prefixed kind to its real fully-qualified kind against the
|
|
485
|
+
// declaring module's own scope. resolveKind() walks up the parent chain so root
|
|
486
|
+
// built-ins (like `Telo`) remain visible from inside imported libraries; sibling
|
|
487
|
+
// modules stay isolated because they're not in the chain.
|
|
488
|
+
const resolvedKind = (findEnclosingModule(evalContext) ?? this.rootContext).resolveKind(kind);
|
|
477
489
|
|
|
478
|
-
const
|
|
490
|
+
const fingerprint = policyFingerprint(findEnclosingPolicy(evalContext));
|
|
491
|
+
const controller = this.controllers.getControllerOrUndefined(resolvedKind, fingerprint);
|
|
479
492
|
if (!controller) {
|
|
480
493
|
const kindInfo =
|
|
481
494
|
resolvedKind !== kind ? `'${kind}' (resolved to '${resolvedKind}')` : `'${kind}'`;
|
|
482
495
|
throw new Error(
|
|
483
|
-
`No controller registered for kind ${kindInfo}, known controllers are: ${this.controllers.getKinds().join(", ")}`,
|
|
496
|
+
`No controller registered for kind ${kindInfo} (runtime fingerprint "${fingerprint}"), known controllers are: ${this.controllers.getKinds().join(", ")}`,
|
|
484
497
|
);
|
|
485
498
|
}
|
|
486
499
|
|
|
@@ -539,14 +552,17 @@ export class Kernel implements IKernel {
|
|
|
539
552
|
|
|
540
553
|
if (!runtime.length) return { instance, ctx };
|
|
541
554
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
555
|
+
// Override invoke in-place so all lifecycle methods (init/invoke/teardown/snapshot)
|
|
556
|
+
// share the same `this`. A wrapper object would split identity: state mutated by
|
|
557
|
+
// init() on the wrapper would be invisible to the original invoke(), which still
|
|
558
|
+
// runs with `this === instance`. Mutating in place also preserves the prototype
|
|
559
|
+
// chain — class-declared methods remain reachable.
|
|
560
|
+
const originalInvoke = instance.invoke!.bind(instance);
|
|
561
|
+
instance.invoke = async (inputs: any) => {
|
|
562
|
+
const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
|
|
563
|
+
return originalInvoke(expanded);
|
|
548
564
|
};
|
|
549
|
-
return { instance
|
|
565
|
+
return { instance, ctx };
|
|
550
566
|
}
|
|
551
567
|
|
|
552
568
|
/**
|
|
@@ -633,7 +649,11 @@ function resolveSchemaRef(
|
|
|
633
649
|
schema: Record<string, unknown>,
|
|
634
650
|
root: Record<string, unknown>,
|
|
635
651
|
): Record<string, unknown> {
|
|
636
|
-
if (
|
|
652
|
+
if (
|
|
653
|
+
schema.$ref &&
|
|
654
|
+
typeof schema.$ref === "string" &&
|
|
655
|
+
(schema.$ref as string).startsWith("#/$defs/")
|
|
656
|
+
) {
|
|
637
657
|
const defName = (schema.$ref as string).slice("#/$defs/".length);
|
|
638
658
|
const defs = root.$defs as Record<string, Record<string, unknown>> | undefined;
|
|
639
659
|
const resolved = defs?.[defName];
|
|
@@ -651,7 +671,9 @@ function collectSchemaProperties(
|
|
|
651
671
|
};
|
|
652
672
|
for (const sub of (schema.oneOf ?? schema.anyOf ?? []) as Record<string, unknown>[]) {
|
|
653
673
|
if (sub && typeof sub === "object" && sub.properties) {
|
|
654
|
-
for (const [k, v] of Object.entries(
|
|
674
|
+
for (const [k, v] of Object.entries(
|
|
675
|
+
sub.properties as Record<string, Record<string, unknown>>,
|
|
676
|
+
)) {
|
|
655
677
|
if (!(k in props)) props[k] = v;
|
|
656
678
|
}
|
|
657
679
|
}
|
|
@@ -672,10 +694,7 @@ function stripCompiledValues(
|
|
|
672
694
|
|
|
673
695
|
if (isCompiledValue(v)) return placeholderForSchema(resolved);
|
|
674
696
|
if (Array.isArray(v)) {
|
|
675
|
-
const itemSchema = resolveSchemaRef(
|
|
676
|
-
(resolved.items ?? {}) as Record<string, unknown>,
|
|
677
|
-
root,
|
|
678
|
-
);
|
|
697
|
+
const itemSchema = resolveSchemaRef((resolved.items ?? {}) as Record<string, unknown>, root);
|
|
679
698
|
return v.map((item) => stripCompiledValues(item, itemSchema, root));
|
|
680
699
|
}
|
|
681
700
|
if (v !== null && typeof v === "object") {
|
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/{manifest-adapters/local-file-adapter.ts → manifest-sources/local-file-source.ts}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_MANIFEST_FILENAME, type
|
|
1
|
+
import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "@telorun/analyzer";
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
@@ -12,7 +12,7 @@ function toFileUrl(filePath: string): string {
|
|
|
12
12
|
return pathToFileURL(filePath).href;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export class
|
|
15
|
+
export class LocalFileSource implements ManifestSource {
|
|
16
16
|
supports(pathOrUrl: string): boolean {
|
|
17
17
|
return (
|
|
18
18
|
pathOrUrl.startsWith("file://") ||
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "@telorun/analyzer";
|
|
2
|
+
import { posix } from "node:path";
|
|
3
|
+
import { stringify as yamlStringify } from "yaml";
|
|
4
|
+
|
|
5
|
+
const SCHEME = "memory://";
|
|
6
|
+
|
|
7
|
+
function stripScheme(url: string): string {
|
|
8
|
+
return url.startsWith(SCHEME) ? url.slice(SCHEME.length) : url;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isAbsoluteUrl(s: string): boolean {
|
|
12
|
+
return /^[a-zA-Z][a-zA-Z0-9+\-.]*:/.test(s);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** In-memory `ManifestSource` for embedders and tests. Register manifest text
|
|
16
|
+
* (or parsed-manifest object arrays) under bare module names; the source
|
|
17
|
+
* canonicalizes module entry points as `<name>/telo.yaml`, mirroring disk's
|
|
18
|
+
* "module is a directory containing telo.yaml" convention so relative imports
|
|
19
|
+
* (`./sub`, `../sibling`) work transparently with POSIX path resolution. */
|
|
20
|
+
export class MemorySource implements ManifestSource {
|
|
21
|
+
private readonly entries = new Map<string, string>();
|
|
22
|
+
|
|
23
|
+
/** Register a manifest source. `name` is a bare module name (`"app"`,
|
|
24
|
+
* `"lib"`, or hierarchical `"auth/login"`) or a partial-file path with a
|
|
25
|
+
* `.yaml`/`.yml` extension. Bare names are stored under `<name>/telo.yaml`;
|
|
26
|
+
* extension-bearing names are stored literally. Object-array `content` is
|
|
27
|
+
* serialized via `yaml.stringify` so the loader downstream is identical to
|
|
28
|
+
* the YAML-text path. */
|
|
29
|
+
set(name: string, content: string | unknown[]): void {
|
|
30
|
+
if (!name) {
|
|
31
|
+
throw new Error("MemorySource.set: name must be non-empty");
|
|
32
|
+
}
|
|
33
|
+
if (name.startsWith("/")) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`MemorySource.set: name '${name}' must not start with '/' — memory:// has no absolute root`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (isAbsoluteUrl(name)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`MemorySource.set: name '${name}' must be a bare key, not a URL with a scheme`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const normalized = posix.normalize(name);
|
|
44
|
+
if (normalized.startsWith("..") || normalized === "." || normalized === "..") {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`MemorySource.set: name '${name}' contains '..' segments that escape the namespace`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const text = typeof content === "string"
|
|
51
|
+
? content
|
|
52
|
+
: content
|
|
53
|
+
.filter((doc) => doc !== null && doc !== undefined)
|
|
54
|
+
.map((doc) => yamlStringify(doc))
|
|
55
|
+
.join("---\n");
|
|
56
|
+
|
|
57
|
+
const hasYamlExt = normalized.endsWith(".yaml") || normalized.endsWith(".yml");
|
|
58
|
+
const key = hasYamlExt ? normalized : `${normalized}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
59
|
+
this.entries.set(key, text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
supports(url: string): boolean {
|
|
63
|
+
return url.startsWith(SCHEME);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async read(url: string): Promise<{ text: string; source: string }> {
|
|
67
|
+
const key = stripScheme(url);
|
|
68
|
+
// Direct hit (literal-extension files, or already-canonicalized telo.yaml URLs).
|
|
69
|
+
const direct = this.entries.get(key);
|
|
70
|
+
if (direct !== undefined) {
|
|
71
|
+
return { text: direct, source: `${SCHEME}${key}` };
|
|
72
|
+
}
|
|
73
|
+
// Directory-style fall-through: bare module name → <name>/telo.yaml.
|
|
74
|
+
const fallback = `${key}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
75
|
+
const fallbackText = this.entries.get(fallback);
|
|
76
|
+
if (fallbackText !== undefined) {
|
|
77
|
+
return { text: fallbackText, source: `${SCHEME}${fallback}` };
|
|
78
|
+
}
|
|
79
|
+
throw new Error(
|
|
80
|
+
`MemorySource: no entry for '${url}'. Tried keys '${key}' and '${fallback}'.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resolveRelative(base: string, relative: string): string {
|
|
85
|
+
if (isAbsoluteUrl(relative)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`MemorySource.resolveRelative: relative '${relative}' is an absolute URL — pass it directly, not through resolveRelative`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (relative.startsWith("/")) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
`MemorySource.resolveRelative: 'memory://' has no absolute root; use a full 'memory://<name>' URL instead of '${relative}'`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const baseKey = stripScheme(base);
|
|
96
|
+
const joined = posix.normalize(posix.join(posix.dirname(baseKey), relative));
|
|
97
|
+
if (joined === ".." || joined.startsWith("../")) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`MemorySource.resolveRelative: relative '${relative}' escapes the memory:// namespace from base '${base}'`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return `${SCHEME}${joined}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
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
|
|
|
@@ -50,11 +54,20 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
50
54
|
private _resources: Record<string, unknown>;
|
|
51
55
|
|
|
52
56
|
/** Maps import alias → real module name for kind resolution. */
|
|
53
|
-
readonly importAliases = new Map<string, string>();
|
|
57
|
+
private readonly importAliases = new Map<string, string>();
|
|
54
58
|
|
|
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
|
}
|
|
@@ -118,6 +139,10 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
118
139
|
}
|
|
119
140
|
}
|
|
120
141
|
|
|
142
|
+
hasImport(alias: string): boolean {
|
|
143
|
+
return this.importAliases.has(alias);
|
|
144
|
+
}
|
|
145
|
+
|
|
121
146
|
getInstance(name: string): unknown {
|
|
122
147
|
const entry = this.resourceInstances.get(name);
|
|
123
148
|
if (!entry) {
|
|
@@ -142,7 +167,10 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
142
167
|
/**
|
|
143
168
|
* Resolve a fully-qualified kind like "Http.Server" to its real kind "http-server.Server".
|
|
144
169
|
* Splits on the first dot, looks up the prefix in importAliases, validates against
|
|
145
|
-
* importedKinds (if set), and reconstructs the resolved kind.
|
|
170
|
+
* importedKinds (if set), and reconstructs the resolved kind. When the alias is not
|
|
171
|
+
* present locally, walks up the lifecycle parent chain so children inherit ancestors'
|
|
172
|
+
* imports (notably the root's `Telo` built-in). Sibling modules — being absent from the
|
|
173
|
+
* chain — remain isolated.
|
|
146
174
|
* Throws with a clear message if the alias is unknown or the kind is not exported.
|
|
147
175
|
*/
|
|
148
176
|
resolveKind(kind: string): string {
|
|
@@ -154,6 +182,11 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
|
|
|
154
182
|
const suffix = kind.slice(dot + 1);
|
|
155
183
|
const realModule = this.importAliases.get(prefix);
|
|
156
184
|
if (!realModule) {
|
|
185
|
+
let cur = this.parent;
|
|
186
|
+
while (cur) {
|
|
187
|
+
if (cur instanceof ModuleContext) return cur.resolveKind(kind);
|
|
188
|
+
cur = cur.parent;
|
|
189
|
+
}
|
|
157
190
|
const known = [...this.importAliases.keys()].join(", ") || "(none)";
|
|
158
191
|
throw new Error(
|
|
159
192
|
`Kind '${kind}': no module imported with alias '${prefix}'. Known aliases: ${known}`,
|
package/src/registry.ts
CHANGED
|
@@ -15,7 +15,6 @@ export class ManifestRegistry {
|
|
|
15
15
|
register(resource: RuntimeResource): void {
|
|
16
16
|
const { kind, metadata } = resource;
|
|
17
17
|
const { name } = metadata;
|
|
18
|
-
console.log("Registering resource:", kind, name);
|
|
19
18
|
if (!this.resources.has(kind)) {
|
|
20
19
|
this.resources.set(kind, new Map());
|
|
21
20
|
}
|
package/src/resource-context.ts
CHANGED
|
@@ -6,17 +6,19 @@ 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,
|
|
12
13
|
type ParsedArgs,
|
|
13
14
|
type TypeRule,
|
|
14
15
|
} from "@telorun/sdk";
|
|
15
|
-
import { EvaluationContext } from "./evaluation-context.js";
|
|
16
16
|
import AjvModule from "ajv";
|
|
17
17
|
import addFormats from "ajv-formats";
|
|
18
|
+
import { EvaluationContext } from "./evaluation-context.js";
|
|
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;
|
|
@@ -217,8 +219,7 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
217
219
|
const hasInlineProperties = Object.keys(resource).some(
|
|
218
220
|
(k) => k !== "kind" && k !== "name" && k !== "metadata",
|
|
219
221
|
);
|
|
220
|
-
const hasExplicitName =
|
|
221
|
-
resource.name !== undefined || resource.metadata?.name !== undefined;
|
|
222
|
+
const hasExplicitName = resource.name !== undefined || resource.metadata?.name !== undefined;
|
|
222
223
|
const shouldRegister =
|
|
223
224
|
(hasInlineProperties || (!hasExplicitName && resourceName !== undefined)) &&
|
|
224
225
|
!this.moduleContext.hasManifest(name);
|
|
@@ -237,20 +238,6 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
237
238
|
return { kind, name };
|
|
238
239
|
}
|
|
239
240
|
|
|
240
|
-
teardownResource(kind: string, name: string): Promise<void> {
|
|
241
|
-
throw new Error("Method teardownResource not implemented.");
|
|
242
|
-
// const parts = kind.split(".");
|
|
243
|
-
// if (parts.length > 2) {
|
|
244
|
-
// return this.kernel.teardownResource(parts[0], parts.slice(1).join("."), name);
|
|
245
|
-
// }
|
|
246
|
-
// return this.kernel.teardownResource(this.metadata.module, kind, name);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
getResources(kind: string): RuntimeResource[] {
|
|
250
|
-
throw new Error("Method teardownResource not implemented.");
|
|
251
|
-
// return this.kernel.getResourcesByKind(kind);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
241
|
getResourcesByName(_kind: string, name: string): RuntimeResource | null {
|
|
255
242
|
const entry = this.moduleContext.resourceInstances.get(name);
|
|
256
243
|
return (entry?.resource ?? null) as RuntimeResource | null;
|
|
@@ -261,23 +248,20 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
261
248
|
kindName: string,
|
|
262
249
|
controllerInstance: any,
|
|
263
250
|
): Promise<void> {
|
|
264
|
-
|
|
251
|
+
const fingerprint = policyFingerprint(this.moduleContext.getControllerPolicy());
|
|
252
|
+
await this.kernel.registerController(moduleName, kindName, controllerInstance, fingerprint);
|
|
265
253
|
}
|
|
266
254
|
|
|
267
255
|
registerDefinition(def: any) {
|
|
268
256
|
this.kernel.registerResourceDefinition(def);
|
|
269
257
|
}
|
|
270
258
|
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
once(event: string, handler: (payload?: any) => void | Promise<void>): void {
|
|
276
|
-
throw new Error("Method once not implemented.");
|
|
259
|
+
getControllerPolicy(): ControllerPolicy | undefined {
|
|
260
|
+
return this.moduleContext.getControllerPolicy();
|
|
277
261
|
}
|
|
278
262
|
|
|
279
|
-
|
|
280
|
-
|
|
263
|
+
on(event: string, handler: (payload?: any) => void | Promise<void>): void {
|
|
264
|
+
this.kernel.on(event, handler);
|
|
281
265
|
}
|
|
282
266
|
|
|
283
267
|
async emit(event: string, payload?: any) {
|
|
@@ -301,16 +285,7 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
301
285
|
}
|
|
302
286
|
|
|
303
287
|
registerModuleImport(alias: string, targetModule: string, kinds: string[]): void {
|
|
304
|
-
|
|
305
|
-
this.kernel.registerModuleImport(declaringModule ?? "", alias, targetModule, kinds);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
resolveModuleAlias(declaringModule: string, alias: string): string | undefined {
|
|
309
|
-
return this.kernel.resolveModuleAlias(declaringModule, alias);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
getModuleContext(moduleName: string): ModuleContext {
|
|
313
|
-
return this.kernel.getModuleContext(moduleName);
|
|
288
|
+
this.moduleContext.registerImport(alias, targetModule, kinds);
|
|
314
289
|
}
|
|
315
290
|
|
|
316
291
|
/**
|