@telorun/kernel 0.2.5 → 0.2.6

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 (137) hide show
  1. package/dist/base-definition.d.ts +14 -0
  2. package/dist/base-definition.d.ts.map +1 -0
  3. package/dist/base-definition.js +17 -0
  4. package/dist/base-definition.js.map +1 -0
  5. package/dist/capabilities/component.d.ts +3 -0
  6. package/dist/capabilities/component.d.ts.map +1 -0
  7. package/dist/capabilities/component.js +4 -0
  8. package/dist/capabilities/component.js.map +1 -0
  9. package/dist/capabilities/executable.d.ts +3 -0
  10. package/dist/capabilities/executable.d.ts.map +1 -0
  11. package/dist/capabilities/executable.js +5 -0
  12. package/dist/capabilities/executable.js.map +1 -0
  13. package/dist/capabilities/handler.d.ts +3 -0
  14. package/dist/capabilities/handler.d.ts.map +1 -0
  15. package/dist/capabilities/handler.js +4 -0
  16. package/dist/capabilities/handler.js.map +1 -0
  17. package/dist/capabilities/invokable.d.ts +3 -0
  18. package/dist/capabilities/invokable.d.ts.map +1 -0
  19. package/dist/capabilities/invokable.js +5 -0
  20. package/dist/capabilities/invokable.js.map +1 -0
  21. package/dist/capabilities/listener.d.ts +3 -0
  22. package/dist/capabilities/listener.d.ts.map +1 -0
  23. package/dist/capabilities/listener.js +5 -0
  24. package/dist/capabilities/listener.js.map +1 -0
  25. package/dist/capabilities/mount.d.ts +3 -0
  26. package/dist/capabilities/mount.d.ts.map +1 -0
  27. package/dist/capabilities/mount.js +5 -0
  28. package/dist/capabilities/mount.js.map +1 -0
  29. package/dist/capabilities/provider.d.ts +3 -0
  30. package/dist/capabilities/provider.d.ts.map +1 -0
  31. package/dist/capabilities/provider.js +8 -0
  32. package/dist/capabilities/provider.js.map +1 -0
  33. package/dist/capabilities/runnable.d.ts +3 -0
  34. package/dist/capabilities/runnable.d.ts.map +1 -0
  35. package/dist/capabilities/runnable.js +5 -0
  36. package/dist/capabilities/runnable.js.map +1 -0
  37. package/dist/capabilities/service.d.ts +3 -0
  38. package/dist/capabilities/service.d.ts.map +1 -0
  39. package/dist/capabilities/service.js +5 -0
  40. package/dist/capabilities/service.js.map +1 -0
  41. package/dist/capabilities/template.d.ts +3 -0
  42. package/dist/capabilities/template.d.ts.map +1 -0
  43. package/dist/capabilities/template.js +5 -0
  44. package/dist/capabilities/template.js.map +1 -0
  45. package/dist/capabilities/type.d.ts +3 -0
  46. package/dist/capabilities/type.d.ts.map +1 -0
  47. package/dist/capabilities/type.js +5 -0
  48. package/dist/capabilities/type.js.map +1 -0
  49. package/dist/controller-loader.d.ts.map +1 -1
  50. package/dist/controller-loader.js +2 -1
  51. package/dist/controller-loader.js.map +1 -1
  52. package/dist/controller-registry.d.ts +0 -4
  53. package/dist/controller-registry.d.ts.map +1 -1
  54. package/dist/controller-registry.js +11 -22
  55. package/dist/controller-registry.js.map +1 -1
  56. package/dist/controllers/capability/capability-controller.d.ts +0 -5
  57. package/dist/controllers/capability/capability-controller.d.ts.map +1 -1
  58. package/dist/controllers/capability/capability-controller.js +1 -5
  59. package/dist/controllers/capability/capability-controller.js.map +1 -1
  60. package/dist/controllers/module/import-controller.d.ts +0 -3
  61. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  62. package/dist/controllers/module/import-controller.js +21 -27
  63. package/dist/controllers/module/import-controller.js.map +1 -1
  64. package/dist/controllers/module/module-controller.d.ts +0 -59
  65. package/dist/controllers/module/module-controller.d.ts.map +1 -1
  66. package/dist/controllers/module/module-controller.js +0 -45
  67. package/dist/controllers/module/module-controller.js.map +1 -1
  68. package/dist/controllers/resource-definition/resource-definition-controller.d.ts +3 -4
  69. package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
  70. package/dist/controllers/resource-definition/resource-definition-controller.js +6 -8
  71. package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
  72. package/dist/controllers/resource-definition/resource-template-controller.d.ts +12 -0
  73. package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -0
  74. package/dist/controllers/resource-definition/resource-template-controller.js +112 -0
  75. package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -0
  76. package/dist/index.d.ts +1 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +0 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/kernel.d.ts +21 -24
  81. package/dist/kernel.d.ts.map +1 -1
  82. package/dist/kernel.js +254 -189
  83. package/dist/kernel.js.map +1 -1
  84. package/dist/loader.d.ts +9 -8
  85. package/dist/loader.d.ts.map +1 -1
  86. package/dist/loader.js +50 -132
  87. package/dist/loader.js.map +1 -1
  88. package/dist/manifest-adapters/http-adapter.d.ts.map +1 -1
  89. package/dist/manifest-adapters/http-adapter.js +3 -1
  90. package/dist/manifest-adapters/http-adapter.js.map +1 -1
  91. package/dist/manifest-adapters/local-file-adapter.d.ts +7 -5
  92. package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
  93. package/dist/manifest-adapters/local-file-adapter.js +21 -26
  94. package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
  95. package/dist/manifest-adapters/manifest-adapter.d.ts +2 -0
  96. package/dist/manifest-adapters/manifest-adapter.d.ts.map +1 -1
  97. package/dist/manifest-adapters/registry-adapter.d.ts.map +1 -1
  98. package/dist/manifest-adapters/registry-adapter.js +3 -1
  99. package/dist/manifest-adapters/registry-adapter.js.map +1 -1
  100. package/dist/manifest-schemas.d.ts +61 -25
  101. package/dist/manifest-schemas.d.ts.map +1 -1
  102. package/dist/manifest-schemas.js +56 -16
  103. package/dist/manifest-schemas.js.map +1 -1
  104. package/dist/resource-context.d.ts +2 -5
  105. package/dist/resource-context.d.ts.map +1 -1
  106. package/dist/resource-context.js +31 -21
  107. package/dist/resource-context.js.map +1 -1
  108. package/dist/schema-valiator.d.ts.map +1 -1
  109. package/dist/schema-valiator.js +13 -2
  110. package/dist/schema-valiator.js.map +1 -1
  111. package/package.json +6 -7
  112. package/src/controller-loader.ts +2 -1
  113. package/src/controller-registry.ts +11 -26
  114. package/src/controllers/module/import-controller.ts +30 -30
  115. package/src/controllers/module/module-controller.ts +0 -51
  116. package/src/controllers/resource-definition/resource-definition-controller.ts +14 -15
  117. package/src/controllers/resource-definition/resource-template-controller.ts +138 -0
  118. package/src/index.ts +1 -1
  119. package/src/kernel.ts +313 -224
  120. package/src/loader.ts +54 -165
  121. package/src/manifest-adapters/local-file-adapter.ts +24 -31
  122. package/src/manifest-adapters/manifest-adapter.ts +2 -0
  123. package/src/manifest-schemas.ts +60 -24
  124. package/src/resource-context.ts +31 -30
  125. package/src/schema-valiator.ts +14 -3
  126. package/src/capabilities/component.yaml +0 -4
  127. package/src/capabilities/executable.yaml +0 -8
  128. package/src/capabilities/handler.yaml +0 -4
  129. package/src/capabilities/listener.yaml +0 -4
  130. package/src/capabilities/provider.yaml +0 -4
  131. package/src/capabilities/template.yaml +0 -4
  132. package/src/capabilities/type.yaml +0 -4
  133. package/src/controllers/capability/capability-controller.ts +0 -41
  134. package/src/controllers/module/module.json +0 -48
  135. package/src/controllers/module/module.yaml +0 -32
  136. package/src/manifest-adapters/http-adapter.ts +0 -35
  137. package/src/manifest-adapters/registry-adapter.ts +0 -56
package/src/kernel.ts CHANGED
@@ -1,14 +1,15 @@
1
+ import { AnalysisRegistry, StaticAnalyzer } from "@telorun/analyzer";
1
2
  import {
2
- ControllerContext,
3
- EvaluationContext,
4
- Kernel as IKernel,
5
- ModuleContext,
6
- ResourceContext,
7
- ResourceDefinition,
8
- ResourceInstance,
9
- ResourceManifest,
10
- RuntimeError,
11
- RuntimeEvent,
3
+ ControllerContext,
4
+ Kernel as IKernel,
5
+ ModuleContext,
6
+ ResourceContext,
7
+ ResourceDefinition,
8
+ ResourceInstance,
9
+ ResourceManifest,
10
+ RuntimeError,
11
+ RuntimeEvent,
12
+ isCompiledValue,
12
13
  } from "@telorun/sdk";
13
14
  import * as path from "path";
14
15
  import { ControllerRegistry } from "./controller-registry.js";
@@ -23,7 +24,9 @@ import { SchemaValidator } from "./schema-valiator.js";
23
24
  * Handles resource loading, initialization, and execution through controllers
24
25
  */
25
26
  export class Kernel implements IKernel {
26
- private loader: Loader = new Loader();
27
+ private readonly loader = new Loader();
28
+ private readonly analyzer = new StaticAnalyzer();
29
+ private readonly registry = new AnalysisRegistry();
27
30
  // private manifests: ManifestRegistry = new ManifestRegistry();
28
31
  private controllers: ControllerRegistry = new ControllerRegistry();
29
32
  private eventBus: EventBus = new EventBus();
@@ -34,33 +37,19 @@ export class Kernel implements IKernel {
34
37
  // string,
35
38
  // { resource: ResourceManifest; instance: ResourceInstance }
36
39
  // > = new Map();
37
- private resourceEventBuses: Map<string, EventBus> = new Map();
40
+
38
41
  private holdCount = 0;
39
42
  private idleResolvers: Array<() => void> = [];
40
43
  private _exitCode = 0;
41
44
  // private bootContextRegistry = new BootContextRegistry();
42
45
  private readonly sharedSchemaValidator = new SchemaValidator();
43
46
  private rootContext!: ModuleContext;
47
+ private staticManifests: ResourceManifest[] = [];
44
48
 
45
49
  constructor() {
46
50
  this.setupEventStreaming();
47
51
  }
48
52
 
49
- // async teardownResource(module: string, kind: string, name: string): Promise<void> {
50
- // const key = this.getResourceKey(module, kind, name);
51
- // const entry = this.resourceInstances.get(key);
52
- // if (!entry) return;
53
- // const { resource, instance } = entry;
54
- // if (instance.teardown) {
55
- // await instance.teardown();
56
- // }
57
- // await this.eventBus.emit(`${resource.kind}.${resource.metadata.name}.Teardown`, {
58
- // resource: { kind: resource.kind, name: resource.metadata.name },
59
- // });
60
- // // this.resourceInstances.delete(key);
61
- // this.resourceEventBuses.delete(key);
62
- // }
63
-
64
53
  async registerController(
65
54
  moduleName: string,
66
55
  kindName: string,
@@ -73,16 +62,9 @@ export class Kernel implements IKernel {
73
62
  /**
74
63
  * Register a resource definition with the controller registry
75
64
  */
76
- registerResourceDefinition(
77
- definition: ResourceDefinition,
78
- // basePath?: string,
79
- // namespace?: string | null,
80
- ): void {
65
+ registerResourceDefinition(definition: ResourceDefinition): void {
81
66
  this.controllers.registerDefinition(definition);
82
- }
83
-
84
- registerCapability(name: string, schema?: Record<string, any>): void {
85
- this.controllers.registerCapability(name, schema);
67
+ this.registry.registerDefinition(definition);
86
68
  }
87
69
 
88
70
  getModuleContext(_moduleName: string): ModuleContext {
@@ -100,14 +82,14 @@ export class Kernel implements IKernel {
100
82
  kinds: string[],
101
83
  ): void {
102
84
  this.rootContext.registerImport(alias, targetModule, kinds);
85
+ this.registry.registerImport(alias, targetModule, kinds);
103
86
  }
104
87
 
105
- isCapabilityRegistered(name: string): boolean {
106
- return this.controllers.isCapabilityRegistered(name);
107
- }
108
-
109
- getCapabilitySchema(name: string): Record<string, any> | null | undefined {
110
- return this.controllers.getCapabilitySchema(name);
88
+ /** Returns the live analysis registry backed by this kernel's known definitions and aliases.
89
+ * Pass to StaticAnalyzer.analyze() for incremental validation of new manifests against
90
+ * already-registered types (e.g. front-end editor validating a manifest before submitting). */
91
+ getAnalysisRegistry(): AnalysisRegistry {
92
+ return this.registry;
111
93
  }
112
94
 
113
95
  /**
@@ -120,81 +102,95 @@ export class Kernel implements IKernel {
120
102
  // Declare built-in module namespaces upfront so getContext() can distinguish
121
103
  // "not yet populated" from a completely unknown module name.
122
104
  this.rootContext.registerImport("Kernel", "Kernel", []); // built-ins, unrestricted
123
- // this.moduleContextRegistry.declareModule("default"); // user resources with no module field
124
105
 
125
- this.controllers.registerDefinition({
126
- kind: "Kernel.Definition",
127
- metadata: { name: "Definition", module: "Kernel" },
128
- capabilities: ["template"],
129
- schema: { type: "object" },
130
- });
106
+ // Register built-in definitions with the controller registry.
107
+ // AnalysisRegistry's underlying DefinitionRegistry already seeds KERNEL_BUILTINS on construction.
108
+ for (const def of this.registry.builtinDefinitions()) this.controllers.registerDefinition(def);
109
+
131
110
  this.controllers.registerController(
132
111
  "Kernel.Definition",
133
112
  await import("./controllers/resource-definition/resource-definition-controller.js"),
134
113
  );
135
- this.controllers.registerDefinition({
136
- kind: "Kernel.Definition",
137
- metadata: { name: "Module", module: "Kernel" },
138
- capabilities: ["template"],
139
- schema: { type: "object" },
140
- });
141
114
  this.controllers.registerController(
142
115
  "Kernel.Module",
143
116
  await import("./controllers/module/module-controller.js"),
144
117
  );
145
- this.controllers.registerDefinition({
146
- kind: "Kernel.Definition",
147
- metadata: { name: "Import", module: "Kernel" },
148
- capabilities: ["template"],
149
- schema: { type: "object" },
150
- });
151
118
  this.controllers.registerController(
152
119
  "Kernel.Import",
153
120
  await import("./controllers/module/import-controller.js"),
154
121
  );
155
- this.controllers.registerDefinition({
156
- kind: "Kernel.Definition",
157
- metadata: { name: "Capability", module: "Kernel" },
158
- capabilities: ["template"],
159
- schema: { type: "object" },
160
- });
161
- this.controllers.registerController(
162
- "Kernel.Capability",
163
- await import("./controllers/capability/capability-controller.js"),
164
- );
165
122
  }
166
123
 
167
124
  /**
168
125
  * Load from runtime configuration file
169
126
  */
170
127
  async loadFromConfig(runtimeYamlPath: string): Promise<void> {
128
+ // Resolve directory paths to their module.yaml so that relative imports
129
+ // (e.g. ../../modules/foo) use the correct base directory.
130
+ const sourceUrl = await this.loader.resolveEntryPoint(
131
+ new URL(runtimeYamlPath, `file://${process.cwd()}/`).href,
132
+ );
171
133
  this.rootContext = new ModuleContext(
172
- this.loader.resolvePath(`file://${process.cwd()}/`, runtimeYamlPath),
134
+ sourceUrl,
173
135
  {},
174
136
  {},
175
137
  {},
176
138
  [],
177
139
  this._createInstance.bind(this),
178
140
  (event, payload) => this.eventBus.emit(event, payload),
141
+ process.env,
179
142
  );
180
143
  // Initialize built-in Runtime definitions first
181
144
  await this.loadBuiltinDefinitions();
182
145
 
183
- // Load built-in capability manifests before user configuration so that
184
- // capability resources are available in the first initialization pass
185
- const { fileURLToPath } = await import("url");
186
- const capabilitiesDir = fileURLToPath(new URL("./capabilities/", import.meta.url));
187
- const capabilityManifests = await this.loader.loadDirectory(capabilitiesDir);
146
+ // Phase 5: attach injection hook fires between create() and init() for every resource
147
+ this.rootContext.preInitHook = (resource, getInstance) =>
148
+ this._injectDependencies(resource, getInstance);
149
+
150
+ // Static analysis pre-flight: validates schemas and invocation context compatibility.
151
+ // All errors are fatal — kernel does not start if analysis fails.
152
+ const staticManifests = await this.loader.loadManifests(sourceUrl);
153
+ this.staticManifests = staticManifests;
154
+
155
+ // Register module identities for x-telo-ref resolution (Phase 3 prerequisite).
156
+ // Kernel built-ins ("kernel" → "Kernel") are auto-registered when Kernel.Abstract
157
+ // definitions are registered in loadBuiltinDefinitions() above.
158
+ for (const m of staticManifests) {
159
+ if (m.kind === "Kernel.Module" && m.metadata?.name && m.metadata?.namespace) {
160
+ this.registry.registerModuleIdentity(
161
+ m.metadata.namespace as string,
162
+ m.metadata.name as string,
163
+ );
164
+ }
165
+ }
166
+
167
+ const errors = this.analyzer.analyzeErrors(staticManifests, {}, this.registry);
168
+ if (errors.length > 0) {
169
+ throw new RuntimeError(
170
+ "ERR_MANIFEST_VALIDATION_FAILED",
171
+ "Manifest validation failed",
172
+ errors.map((d) => ({
173
+ severity: "error" as const,
174
+ message: d.message,
175
+ code: d.code !== undefined ? String(d.code) : undefined,
176
+ resource: (d.data as any)?.resource
177
+ ? `${(d.data as any).resource.kind}.${(d.data as any).resource.name}`
178
+ : undefined,
179
+ })),
180
+ );
181
+ }
188
182
 
189
183
  // Load runtime configuration — root module gets access to host env
190
- const userManifests = await this.loader.loadManifest(
191
- runtimeYamlPath,
192
- `file://${process.cwd()}/`,
193
- { env: process.env },
194
- );
195
- const allManifests = [...capabilityManifests, ...userManifests];
184
+ const allManifests = await this.loader.loadModule(sourceUrl, { compile: true });
196
185
 
197
- for (const manifest of allManifests) {
186
+ // Phase 2: normalize inline resources — extract inline values from x-telo-ref slots
187
+ // into first-class named manifests and replace them in-place with {kind, name} references.
188
+ // Update staticManifests so Phase 3 (validateReferences) and Phase 4 (DAG) see
189
+ // the same normalized structure.
190
+ const normalizedManifests = this.analyzer.normalize(allManifests, this.registry);
191
+ this.staticManifests = normalizedManifests;
192
+
193
+ for (const manifest of normalizedManifests) {
198
194
  if (manifest.kind === "Kernel.Module") {
199
195
  this.rootContext.setSecrets(manifest.secrets ?? {});
200
196
  this.rootContext.setVariables(manifest.variables ?? {});
@@ -226,6 +222,36 @@ export class Kernel implements IKernel {
226
222
  }
227
223
  }
228
224
 
225
+ // Phase 3+4: reference validation, cycle detection, and topo sort
226
+ const {
227
+ diagnostics: refErrors,
228
+ order,
229
+ cycleError,
230
+ } = this.analyzer.prepare(this.staticManifests, this.registry);
231
+ if (refErrors.length > 0) {
232
+ throw new RuntimeError(
233
+ "ERR_MANIFEST_VALIDATION_FAILED",
234
+ "Manifest validation failed",
235
+ refErrors.map((d) => ({
236
+ severity: "error" as const,
237
+ message: d.message,
238
+ code: d.code !== undefined ? String(d.code) : undefined,
239
+ resource: (d.data as any)?.resource
240
+ ? `${(d.data as any).resource.kind}.${(d.data as any).resource.name}`
241
+ : undefined,
242
+ })),
243
+ );
244
+ }
245
+ if (cycleError) {
246
+ throw new RuntimeError("ERR_CIRCULAR_DEPENDENCY", cycleError);
247
+ }
248
+
249
+ // Phase 5: sort pending resources into topo order so injection always finds
250
+ // initialized dependencies, then run the init loop.
251
+ if (order) {
252
+ this.rootContext.setInitOrder(order);
253
+ }
254
+
229
255
  // Initialize resources
230
256
  try {
231
257
  await this.rootContext.initializeResources();
@@ -241,14 +267,6 @@ export class Kernel implements IKernel {
241
267
  }
242
268
  }
243
269
 
244
- // async runInstances(): Promise<void> {
245
- // for (const { instance } of this.resourceInstances.values()) {
246
- // if (instance.run) {
247
- // await instance.run();
248
- // }
249
- // }
250
- // }
251
-
252
270
  async emitRuntimeEvent(event: string, payload?: any): Promise<void> {
253
271
  await this.eventBus.emit(event, payload);
254
272
  }
@@ -327,7 +345,7 @@ export class Kernel implements IKernel {
327
345
  },
328
346
  acquireHold: (reason?: string) => this.acquireHold(reason),
329
347
  expandValue: (value: any, context: Record<string, any>) =>
330
- this.rootContext.merge(context).expand(value),
348
+ this.rootContext.expandWith(value, context),
331
349
  requestExit: (code: number) => this.requestExit(code),
332
350
  };
333
351
  }
@@ -345,15 +363,15 @@ export class Kernel implements IKernel {
345
363
  }
346
364
 
347
365
  /**
348
- * Create a resource instance: resolves the controller, validates the schema,
349
- * calls create/init, registers context providers, stores the snapshot, and
350
- * mirrors the instance into the Kernel-level and module-level registries.
351
- * Returns null when the controller is not yet registered (retry signal).
366
+ * Create phase only: resolves the controller, validates the schema, and calls
367
+ * controller.create(). Returns { instance, ctx } so initializeResources can
368
+ * run init() separately in its second phase. Returns null when the controller
369
+ * is not yet registered (retry signal).
352
370
  */
353
371
  private async _createInstance(
354
372
  evalContext: ModuleContext,
355
373
  resource: ResourceManifest,
356
- ): Promise<ResourceInstance | null> {
374
+ ): Promise<{ instance: ResourceInstance; ctx: ResourceContext } | null> {
357
375
  const kind = resource.kind;
358
376
 
359
377
  // Resolve the alias-prefixed kind to its real fully-qualified kind.
@@ -379,8 +397,27 @@ export class Kernel implements IKernel {
379
397
  throw new Error(`No schema defined for ${kind} controller`);
380
398
  }
381
399
 
400
+ // Resolve eval paths from x-telo-eval annotations in the parent and own schema
401
+ const definition = this.controllers.getDefinition(resolvedKind);
402
+ const parentDef = definition?.capability
403
+ ? this.controllers.getDefinition(definition.capability)
404
+ : undefined;
405
+ const parentEval = parentDef?.schema
406
+ ? buildEvalPaths(parentDef.schema)
407
+ : { compile: [], runtime: [] };
408
+ const ownEval = definition?.schema
409
+ ? buildEvalPaths(definition.schema)
410
+ : { compile: [], runtime: [] };
411
+ const compile = [...parentEval.compile, ...ownEval.compile];
412
+ const runtime = [...parentEval.runtime, ...ownEval.runtime];
413
+
414
+ // Schema validation runs before CEL evaluation so it sees the original manifest
415
+ // shape. CompiledValue wrappers (from load-time precompilation) are stripped,
416
+ // restoring the pre-CEL string view that the schema expects.
382
417
  try {
383
- this.sharedSchemaValidator.compile(controller.schema).validate(resource);
418
+ this.sharedSchemaValidator
419
+ .compile(controller.schema)
420
+ .validate(stripCompiledValues(resource, controller.schema as Record<string, unknown>));
384
421
  } catch (error) {
385
422
  throw new RuntimeError(
386
423
  "ERR_RESOURCE_SCHEMA_VALIDATION_FAILED",
@@ -388,138 +425,55 @@ export class Kernel implements IKernel {
388
425
  );
389
426
  }
390
427
 
391
- const ctx = this.createResourceContext(evalContext, resource);
392
- const instance = await controller.create(resource, ctx);
428
+ // Expand compile-time CEL fields before passing to the controller.
429
+ const processedResource = compile.length
430
+ ? (evalContext.expandPaths(
431
+ resource as Record<string, unknown>,
432
+ compile,
433
+ runtime,
434
+ ) as ResourceManifest)
435
+ : resource;
436
+
437
+ const ctx = this.createResourceContext(evalContext, processedResource);
438
+ const instance = await controller.create(processedResource, ctx);
393
439
  if (!instance) return null;
394
440
 
395
- if (instance.init) await instance.init(ctx);
396
-
397
- // if (isContextProvider(instance)) {
398
- // this.bootContextRegistry.register(
399
- // resource.kind,
400
- // resource.metadata.name,
401
- // resource.metadata.module,
402
- // resource.grants as string[] | undefined,
403
- // instance.provideContext(),
404
- // );
405
- // }
406
-
407
- if (instance.snapshot) {
408
- const snap = await Promise.resolve(instance.snapshot()).catch(() => ({}));
409
- if (evalContext instanceof ModuleContext) {
410
- evalContext.setResource(resource.metadata.name, (snap as Record<string, unknown>) ?? {});
411
- }
412
- }
441
+ if (!runtime.length) return { instance, ctx };
413
442
 
414
- return instance;
415
- }
416
-
417
- /**
418
- * Tear down all resource instances owned by a dynamically-spawned context,
419
- * cascading depth-first through its children. Removes entries from both
420
- * ctx.resourceInstances and the Kernel-level resourceInstances map, and
421
- * emits Teardown events (matching the behaviour of teardownResource()).
422
- */
423
- async teardownContext(ctx: EvaluationContext): Promise<void> {
424
- for (const child of [...ctx.children].reverse()) {
425
- await this.teardownContext(child);
426
- }
427
- const entries = [...ctx.resourceInstances.entries()].reverse();
428
- for (const [key, { resource, instance }] of entries) {
429
- if (instance.teardown) await instance.teardown();
430
- await this.eventBus.emit(`${resource.kind}.${resource.metadata.name}.Teardown`, {
431
- resource: { kind: resource.kind, name: resource.metadata.name },
432
- });
433
- // this.resourceInstances.delete(key);
434
- ctx.resourceInstances.delete(key);
435
- this.resourceEventBuses.delete(key);
436
- }
443
+ const wrapped: ResourceInstance = {
444
+ ...instance,
445
+ invoke: async (inputs: any) => {
446
+ const expanded = evalContext.expandPaths(inputs as Record<string, unknown>, runtime);
447
+ return instance.invoke!(expanded);
448
+ },
449
+ };
450
+ return { instance: wrapped, ctx };
437
451
  }
438
452
 
439
- // getResourcesByKind(kind: string): RuntimeResource[] {
440
- // const resources: RuntimeResource[] = [];
441
- // for (const entry of this.resourceInstances.values()) {
442
- // if (entry.resource.kind === kind) {
443
- // resources.push(entry.instance as any);
444
- // }
445
- // }
446
- // return resources;
447
- // }
448
-
449
- // getResourceByName(declaringModule: string, alias: string, name: string): RuntimeResource | null {
450
- // const realModule = this.moduleContextRegistry.resolveAlias(declaringModule, alias) ?? alias;
451
- // for (const { resource, instance } of this.resourceInstances.values()) {
452
- // if (resource.metadata.module === realModule && resource.metadata.name === name) {
453
- // return instance as RuntimeResource;
454
- // }
455
- // }
456
- // return null;
457
- // }
458
-
459
453
  /**
460
- * Returns the unique set of local file paths from which resources were loaded.
461
- * HTTP/HTTPS sources are excluded — they cannot be watched on disk.
454
+ * Phase 5 Inject live instances into reference fields of a resource config.
455
+ *
456
+ * Called between create() and init() for every resource. Walks the definition's
457
+ * field map and replaces each {kind, name} reference value (outside scope visibility
458
+ * paths) with the live ResourceInstance returned by getInstance(name). Fields within
459
+ * scope paths are left as {kind, name} — the controller resolves them at runtime.
462
460
  */
463
- // getSourceFiles(): string[] {
464
- // const seen = new Set<string>();
465
- // for (const { resource } of this.resourceInstances.values()) {
466
- // const src = resource.metadata.source;
467
- // if (src && !src.startsWith("http://") && !src.startsWith("https://")) {
468
- // seen.add(src);
469
- // }
470
- // }
471
- // return Array.from(seen);
472
- // }
473
-
474
- /**
475
- * Reload all resources that were loaded from the given source file.
476
- * Safe order: parse first → if parse succeeds, tear down old → init new → run new only.
477
- */
478
- // async reloadSource(sourcePath: string): Promise<void> {
479
- // // Parse first — bail before touching running resources if the file is invalid
480
- // const newManifests = await this.loader.loadManifest(sourcePath, `file://${process.cwd()}/`, {
481
- // env: process.env,
482
- // });
483
-
484
- // // Collect keys of resources loaded from this source (in insertion order)
485
- // const keysFromSource: string[] = [];
486
- // for (const [key, { resource }] of this.resourceInstances.entries()) {
487
- // if (resource.metadata.source === sourcePath) {
488
- // keysFromSource.push(key);
489
- // }
490
- // }
491
- // // Tear down in reverse order (children first via cascade)
492
- // for (const key of [...keysFromSource].reverse()) {
493
- // const entry = this.resourceInstances.get(key);
494
- // if (!entry) continue; // already removed by a cascade
495
- // await this.teardownResource(
496
- // entry.resource.metadata.module,
497
- // entry.resource.kind,
498
- // entry.resource.metadata.name,
499
- // );
500
- // }
501
-
502
- // for (const manifest of newManifests) {
503
- // this.rootContext.registerManifest(manifest);
504
- // }
505
- // const keysBefore = new Set(this.resourceInstances.keys());
506
- // await this.initializeResources();
507
-
508
- // // Run only newly created instances (not all instances — avoids double-run)
509
- // const newKeys = Array.from(this.resourceInstances.keys()).filter((k) => !keysBefore.has(k));
510
- // for (const key of newKeys) {
511
- // const entry = this.resourceInstances.get(key);
512
- // if (entry?.instance.run) {
513
- // await entry.instance.run();
514
- // }
515
- // }
516
- // }
517
-
518
- private getResourceKey(module: string, kind: string, name: string): string {
519
- if (!kind.includes(".")) {
520
- throw new Error(`Resource kind must include module prefix: ${kind}`);
521
- }
522
- return `${module}.${kind}.${name}`;
461
+ private _injectDependencies(
462
+ resource: ResourceManifest,
463
+ getInstance: (name: string) => ResourceInstance | undefined,
464
+ ): void {
465
+ this.registry.iterateFieldEntries(
466
+ resource,
467
+ (fieldPath) => injectAtPath(resource, fieldPath, getInstance),
468
+ (fieldPath) => {
469
+ const val = (resource as Record<string, unknown>)[fieldPath];
470
+ if (Array.isArray(val)) {
471
+ (resource as Record<string, unknown>)[fieldPath] = this.rootContext.createScopeHandle(
472
+ val as ResourceManifest[],
473
+ );
474
+ }
475
+ },
476
+ );
523
477
  }
524
478
 
525
479
  /**
@@ -556,3 +510,138 @@ export class Kernel implements IKernel {
556
510
  };
557
511
  }
558
512
  }
513
+
514
+ /** Returns a schema-appropriate placeholder value for a CompiledValue field. */
515
+ function placeholderForSchema(schema: Record<string, unknown>): unknown {
516
+ if (schema.default !== undefined) return schema.default;
517
+ switch (schema.type) {
518
+ case "integer":
519
+ case "number":
520
+ return (schema.minimum as number | undefined) ?? 0;
521
+ case "boolean":
522
+ return false;
523
+ case "array":
524
+ return [];
525
+ case "object":
526
+ return {};
527
+ default:
528
+ return "";
529
+ }
530
+ }
531
+
532
+ /** Replaces CompiledValue wrappers with schema-appropriate placeholders for schema validation.
533
+ * Template strings were compiled from YAML at load time; this restores a shape
534
+ * that AJV can validate without evaluating expressions. */
535
+ function stripCompiledValues(v: unknown, schema: Record<string, unknown> = {}): unknown {
536
+ if (isCompiledValue(v)) return placeholderForSchema(schema);
537
+ if (Array.isArray(v)) {
538
+ const itemSchema = (schema.items ?? {}) as Record<string, unknown>;
539
+ return v.map((item) => stripCompiledValues(item, itemSchema));
540
+ }
541
+ if (v !== null && typeof v === "object") {
542
+ const props = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
543
+ const out: Record<string, unknown> = {};
544
+ for (const [k, val] of Object.entries(v as Record<string, unknown>)) {
545
+ out[k] = stripCompiledValues(val, props[k] ?? {});
546
+ }
547
+ return out;
548
+ }
549
+ return v;
550
+ }
551
+
552
+ /**
553
+ * Walks `resource` following `fieldPath` (dot notation, `[]` = array traversal).
554
+ * For each leaf value that looks like a {kind, name} reference, calls getInstance(name)
555
+ * and replaces the value in-place with the returned live ResourceInstance.
556
+ * Values where getInstance returns undefined are left unchanged.
557
+ */
558
+ /**
559
+ * Traverses a definition schema and collects all paths annotated with `x-telo-eval`.
560
+ * Root-level `x-telo-eval` produces the `"**"` wildcard (expand all fields).
561
+ * Property-level annotations produce the dot-notation path to that property.
562
+ */
563
+ function buildEvalPaths(schema: Record<string, any>): { compile: string[]; runtime: string[] } {
564
+ const compile: string[] = [];
565
+ const runtime: string[] = [];
566
+
567
+ if (schema["x-telo-eval"] === "compile") compile.push("**");
568
+ else if (schema["x-telo-eval"] === "runtime") runtime.push("**");
569
+
570
+ if (schema.properties) {
571
+ for (const [key, propSchema] of Object.entries(schema.properties as Record<string, any>)) {
572
+ collectEvalPathsNode(propSchema, key, compile, runtime);
573
+ }
574
+ }
575
+
576
+ return { compile, runtime };
577
+ }
578
+
579
+ function collectEvalPathsNode(
580
+ node: Record<string, any>,
581
+ path: string,
582
+ compile: string[],
583
+ runtime: string[],
584
+ ): void {
585
+ if (node["x-telo-eval"] === "compile") {
586
+ compile.push(path);
587
+ return;
588
+ }
589
+ if (node["x-telo-eval"] === "runtime") {
590
+ runtime.push(path);
591
+ return;
592
+ }
593
+ if (node.properties) {
594
+ for (const [key, propSchema] of Object.entries(node.properties as Record<string, any>)) {
595
+ collectEvalPathsNode(propSchema, `${path}.${key}`, compile, runtime);
596
+ }
597
+ }
598
+ }
599
+
600
+ function injectAtPath(
601
+ resource: ResourceManifest,
602
+ fieldPath: string,
603
+ getInstance: (name: string) => ResourceInstance | undefined,
604
+ ): void {
605
+ const parts = fieldPath.split(".");
606
+
607
+ function traverse(obj: unknown, partsLeft: string[]): void {
608
+ if (!obj || typeof obj !== "object" || partsLeft.length === 0) return;
609
+ const [head, ...rest] = partsLeft;
610
+ const isArr = head.endsWith("[]");
611
+ const key = isArr ? head.slice(0, -2) : head;
612
+ const container = obj as Record<string, unknown>;
613
+ const val = container[key];
614
+ if (val == null) return;
615
+
616
+ if (isArr) {
617
+ if (!Array.isArray(val)) return;
618
+ for (let i = 0; i < val.length; i++) {
619
+ const elem = val[i];
620
+ if (!elem || typeof elem !== "object") continue;
621
+ if (rest.length === 0) {
622
+ const ref = elem as Record<string, unknown>;
623
+ if (typeof ref.kind === "string" && typeof ref.name === "string") {
624
+ const instance = getInstance(ref.name);
625
+ if (instance) val[i] = instance;
626
+ }
627
+ } else {
628
+ traverse(elem, rest);
629
+ }
630
+ }
631
+ } else {
632
+ if (rest.length === 0) {
633
+ if (val && typeof val === "object" && !Array.isArray(val)) {
634
+ const ref = val as Record<string, unknown>;
635
+ if (typeof ref.kind === "string" && typeof ref.name === "string") {
636
+ const instance = getInstance(ref.name);
637
+ if (instance) container[key] = instance;
638
+ }
639
+ }
640
+ } else {
641
+ traverse(val, rest);
642
+ }
643
+ }
644
+ }
645
+
646
+ traverse(resource, parts);
647
+ }