@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.
Files changed (89) hide show
  1. package/dist/controller-loader.d.ts +19 -20
  2. package/dist/controller-loader.d.ts.map +1 -1
  3. package/dist/controller-loader.js +67 -247
  4. package/dist/controller-loader.js.map +1 -1
  5. package/dist/controller-loaders/napi-loader.d.ts +27 -0
  6. package/dist/controller-loaders/napi-loader.d.ts.map +1 -0
  7. package/dist/controller-loaders/napi-loader.js +158 -0
  8. package/dist/controller-loaders/napi-loader.js.map +1 -0
  9. package/dist/controller-loaders/npm-loader.d.ts +20 -0
  10. package/dist/controller-loaders/npm-loader.d.ts.map +1 -0
  11. package/dist/controller-loaders/npm-loader.js +256 -0
  12. package/dist/controller-loaders/npm-loader.js.map +1 -0
  13. package/dist/controller-registry.d.ts +30 -20
  14. package/dist/controller-registry.d.ts.map +1 -1
  15. package/dist/controller-registry.js +50 -99
  16. package/dist/controller-registry.js.map +1 -1
  17. package/dist/controllers/module/import-controller.d.ts +11 -0
  18. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  19. package/dist/controllers/module/import-controller.js +30 -3
  20. package/dist/controllers/module/import-controller.js.map +1 -1
  21. package/dist/controllers/resource-definition/abstract-controller.d.ts +35 -0
  22. package/dist/controllers/resource-definition/abstract-controller.d.ts.map +1 -0
  23. package/dist/controllers/resource-definition/abstract-controller.js +34 -0
  24. package/dist/controllers/resource-definition/abstract-controller.js.map +1 -0
  25. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  26. package/dist/controllers/resource-definition/resource-definition-controller.js +1 -1
  27. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  28. package/dist/evaluation-context.js +1 -1
  29. package/dist/evaluation-context.js.map +1 -1
  30. package/dist/index.d.ts +2 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/kernel.d.ts +14 -16
  35. package/dist/kernel.d.ts.map +1 -1
  36. package/dist/kernel.js +58 -49
  37. package/dist/kernel.js.map +1 -1
  38. package/dist/manifest-schemas.d.ts +50 -0
  39. package/dist/manifest-schemas.d.ts.map +1 -1
  40. package/dist/manifest-schemas.js +31 -0
  41. package/dist/manifest-schemas.js.map +1 -1
  42. package/dist/{manifest-adapters/local-file-adapter.d.ts → manifest-sources/local-file-source.d.ts} +3 -3
  43. package/dist/manifest-sources/local-file-source.d.ts.map +1 -0
  44. package/dist/{manifest-adapters/local-file-adapter.js → manifest-sources/local-file-source.js} +2 -2
  45. package/dist/manifest-sources/local-file-source.js.map +1 -0
  46. package/dist/manifest-sources/memory-source.d.ts +23 -0
  47. package/dist/manifest-sources/memory-source.d.ts.map +1 -0
  48. package/dist/manifest-sources/memory-source.js +83 -0
  49. package/dist/manifest-sources/memory-source.js.map +1 -0
  50. package/dist/module-context.d.ts +17 -3
  51. package/dist/module-context.d.ts.map +1 -1
  52. package/dist/module-context.js +19 -1
  53. package/dist/module-context.js.map +1 -1
  54. package/dist/registry.d.ts.map +1 -1
  55. package/dist/registry.js +0 -1
  56. package/dist/registry.js.map +1 -1
  57. package/dist/resource-context.d.ts +2 -7
  58. package/dist/resource-context.d.ts.map +1 -1
  59. package/dist/resource-context.js +8 -28
  60. package/dist/resource-context.js.map +1 -1
  61. package/dist/runtime-registry.d.ts +50 -0
  62. package/dist/runtime-registry.d.ts.map +1 -0
  63. package/dist/runtime-registry.js +140 -0
  64. package/dist/runtime-registry.js.map +1 -0
  65. package/package.json +16 -5
  66. package/src/controller-loader.ts +77 -273
  67. package/src/controller-loaders/napi-loader.ts +191 -0
  68. package/src/controller-loaders/npm-loader.ts +285 -0
  69. package/src/controller-registry.ts +66 -129
  70. package/src/controllers/module/import-controller.ts +32 -3
  71. package/src/controllers/resource-definition/abstract-controller.ts +56 -0
  72. package/src/controllers/resource-definition/resource-definition-controller.ts +1 -0
  73. package/src/evaluation-context.ts +1 -1
  74. package/src/index.ts +2 -1
  75. package/src/kernel.ts +86 -67
  76. package/src/manifest-schemas.ts +33 -0
  77. package/src/{manifest-adapters/local-file-adapter.ts → manifest-sources/local-file-source.ts} +2 -2
  78. package/src/manifest-sources/memory-source.ts +104 -0
  79. package/src/module-context.ts +36 -3
  80. package/src/registry.ts +0 -1
  81. package/src/resource-context.ts +11 -36
  82. package/src/runtime-registry.ts +170 -0
  83. package/dist/manifest-adapters/local-file-adapter.d.ts.map +0 -1
  84. package/dist/manifest-adapters/local-file-adapter.js.map +0 -1
  85. package/dist/manifest-adapters/manifest-adapter.d.ts +0 -35
  86. package/dist/manifest-adapters/manifest-adapter.d.ts.map +0 -1
  87. package/dist/manifest-adapters/manifest-adapter.js +0 -2
  88. package/dist/manifest-adapters/manifest-adapter.js.map +0 -1
  89. 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 { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
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 { AnalysisRegistry, DEFAULT_MANIFEST_FILENAME, isModuleKind, Loader, StaticAnalyzer } from "@telorun/analyzer";
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
- type LoadOptions,
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 { LocalFileAdapter } from "./manifest-adapters/local-file-adapter.js";
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
- /** Base URL for the registry adapter. When unset, the RegistryAdapter
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
- this.loader.register(new LocalFileAdapter());
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(`${moduleName}.${kindName}`, controllerInstance);
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 from runtime configuration file
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 loadFromConfig(runtimeYamlPath: string): Promise<void> {
164
- const resolvedUrl = new URL(runtimeYamlPath, `file://${process.cwd()}/`).href;
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 controller register hooks first (before any initialization)
264
- for (const kind of this.controllers.getKinds()) {
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?.register) {
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
- // resolveKind() throws with a clear message if the alias or kind is not found.
476
- const resolvedKind = this.rootContext.resolveKind(kind);
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 controller = this.controllers.getControllerOrUndefined(resolvedKind);
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
- const wrapped: ResourceInstance = {
543
- ...instance,
544
- invoke: async (inputs: any) => {
545
- const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
546
- return instance.invoke!(expanded);
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: wrapped, ctx };
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 (schema.$ref && typeof schema.$ref === "string" && (schema.$ref as string).startsWith("#/$defs/")) {
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(sub.properties as Record<string, Record<string, unknown>>)) {
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") {
@@ -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";
@@ -1,4 +1,4 @@
1
- import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "@telorun/analyzer";
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 LocalFileAdapter implements ManifestAdapter {
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
+ }
@@ -1,4 +1,8 @@
1
- import type { Invocable, ModuleContext as IModuleContext } from "@telorun/sdk";
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
  }
@@ -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
- await this.kernel.registerController(moduleName, kindName, controllerInstance);
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
- on(event: string, handler: (payload?: any) => void | Promise<void>): void {
272
- this.kernel.on(event, handler);
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
- off(event: string, handler: (payload?: any) => void | Promise<void>): void {
280
- throw new Error("Method off not implemented.");
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
- const declaringModule = (this.metadata as any).module as string | undefined;
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
  /**