@telorun/kernel 0.24.2 → 0.25.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 (37) hide show
  1. package/dist/application-env.d.ts +18 -0
  2. package/dist/application-env.d.ts.map +1 -1
  3. package/dist/application-env.js +36 -0
  4. package/dist/application-env.js.map +1 -1
  5. package/dist/controller-loaders/npm-loader.d.ts +41 -12
  6. package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
  7. package/dist/controller-loaders/npm-loader.js +117 -51
  8. package/dist/controller-loaders/npm-loader.js.map +1 -1
  9. package/dist/controller-registry.d.ts +9 -0
  10. package/dist/controller-registry.d.ts.map +1 -1
  11. package/dist/controller-registry.js +18 -0
  12. package/dist/controller-registry.js.map +1 -1
  13. package/dist/controllers/module/import-controller.d.ts.map +1 -1
  14. package/dist/controllers/module/import-controller.js +50 -28
  15. package/dist/controllers/module/import-controller.js.map +1 -1
  16. package/dist/evaluation-context.d.ts.map +1 -1
  17. package/dist/evaluation-context.js +13 -5
  18. package/dist/evaluation-context.js.map +1 -1
  19. package/dist/kernel.d.ts.map +1 -1
  20. package/dist/kernel.js +17 -1
  21. package/dist/kernel.js.map +1 -1
  22. package/dist/module-context.d.ts +56 -7
  23. package/dist/module-context.d.ts.map +1 -1
  24. package/dist/module-context.js +121 -10
  25. package/dist/module-context.js.map +1 -1
  26. package/dist/schema-validator.d.ts.map +1 -1
  27. package/dist/schema-validator.js +39 -3
  28. package/dist/schema-validator.js.map +1 -1
  29. package/package.json +2 -2
  30. package/src/application-env.ts +37 -0
  31. package/src/controller-loaders/npm-loader.ts +124 -58
  32. package/src/controller-registry.ts +18 -0
  33. package/src/controllers/module/import-controller.ts +58 -28
  34. package/src/evaluation-context.ts +15 -5
  35. package/src/kernel.ts +24 -1
  36. package/src/module-context.ts +153 -11
  37. package/src/schema-validator.ts +41 -3
@@ -103,15 +103,41 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
103
103
  /** Maps import alias → its child context's exported instances. Registered by the
104
104
  * Telo.Import controller; read when resolving a cross-module `!ref Alias.name`
105
105
  * reference (Phase 5 injection and boot targets). `names` is the import's
106
- * `exports.resources` gate — only listed instances are reachable. */
106
+ * `exports.resources` gate — only listed instances are reachable. `terminal`
107
+ * returns the child's pre-flattened terminal getter for a name (a closure that
108
+ * points directly at the OWNING context's instance, however many re-export hops
109
+ * away), so resolution stays O(1) regardless of re-export depth. */
107
110
  private readonly importedScopes = new Map<
108
111
  string,
109
112
  {
110
113
  names: Set<string>;
111
- get: (name: string) => { kind: string; instance: ResourceInstance } | undefined;
114
+ terminal: (
115
+ name: string,
116
+ ) => (() => { kind: string; instance: ResourceInstance } | undefined) | undefined;
112
117
  }
113
118
  >();
114
119
 
120
+ /** This module's flattened export table: export name → terminal getter. A local
121
+ * export's getter reads this context's own `resourceInstances`; a re-export
122
+ * (`!ref Alias.name`) holds the SAME closure object as the owner's entry, copied
123
+ * by reference at build time so depth adds no resolution hops. Built once by the
124
+ * Telo.Import controller after this module's own imports have initialized. */
125
+ private readonly exportedGetters = new Map<
126
+ string,
127
+ () => { kind: string; instance: ResourceInstance } | undefined
128
+ >();
129
+
130
+ /** This module's flattened exported-KIND table: exported kind suffix → canonical
131
+ * `<owningModule>.<Kind>`. A local exported kind maps to `<thisModule>.<Kind>`; a
132
+ * re-export (`exports.kinds: [Alias.Kind]`) copies the source import's already-canonical
133
+ * string, so depth adds no resolution hops. Built by `buildExportTable`. */
134
+ private readonly exportedKinds = new Map<string, string>();
135
+
136
+ /** Maps import alias → a resolver into that import's exported-kind table. Registered by the
137
+ * Telo.Import controller; consulted by `resolveKind` so an imported library's kinds — local
138
+ * OR transitively re-exported — resolve to their true owning module in O(1). */
139
+ private readonly importedKindResolvers = new Map<string, (suffix: string) => string | undefined>();
140
+
115
141
  /**
116
142
  * Resolved controller-selection policy for this module's `Telo.Definition`s.
117
143
  * Stamped by the parent `Telo.Import` controller from the import's `runtime:`
@@ -202,24 +228,127 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
202
228
  }
203
229
 
204
230
  /** Register an import alias's exported instances for cross-module reference resolution.
205
- * `names` is the gate (the import's `exports.resources`); `get` reads the child context's
206
- * live instance + its canonical kind lazily they exist only after the import's `init()`
207
- * runs. */
231
+ * `names` is the gate (the import's `exports.resources`); `terminal` returns the child
232
+ * context's pre-flattened terminal getter for a name (existing only after the import's
233
+ * `init()` built the child's export table). */
208
234
  registerImportedScope(
209
235
  alias: string,
210
236
  names: string[],
211
- get: (name: string) => { kind: string; instance: ResourceInstance } | undefined,
237
+ terminal: (
238
+ name: string,
239
+ ) => (() => { kind: string; instance: ResourceInstance } | undefined) | undefined,
212
240
  ): void {
213
- this.importedScopes.set(alias, { names: new Set(names), get });
241
+ this.importedScopes.set(alias, { names: new Set(names), terminal });
242
+ }
243
+
244
+ /** This module's terminal getter for an exported `name`, or undefined. Read by a parent
245
+ * import building its own (re-)export table — copying this closure by reference flattens
246
+ * the chain so resolution never walks hops. */
247
+ getTerminalExport(
248
+ name: string,
249
+ ): (() => { kind: string; instance: ResourceInstance } | undefined) | undefined {
250
+ return this.exportedGetters.get(name);
251
+ }
252
+
253
+ /** O(1) resolution of an exported instance owned or re-exported by this module. */
254
+ getExported(name: string): { kind: string; instance: ResourceInstance } | undefined {
255
+ return this.exportedGetters.get(name)?.();
256
+ }
257
+
258
+ /** Register an import alias's exported-kind resolver (the child's `getExportedKind`). */
259
+ registerImportedKindScope(alias: string, resolve: (suffix: string) => string | undefined): void {
260
+ this.importedKindResolvers.set(alias, resolve);
261
+ }
262
+
263
+ /** This module's canonical kind for an exported suffix (local or re-exported), or undefined. */
264
+ getExportedKind(suffix: string): string | undefined {
265
+ return this.exportedKinds.get(suffix);
266
+ }
267
+
268
+ /** Build this module's flattened export tables from `exports.resources` / `exports.kinds`.
269
+ * A resource entry is a local export (no alias) or a re-export `Alias.Name` (alias set); a
270
+ * kind entry is a local kind (no alias) or a re-export `Alias.Kind` (alias set). A local
271
+ * export gets a fresh terminal getter / `<module>.<Kind>` canonical; a re-export copies the
272
+ * source import's terminal getter / already-canonical kind by reference, collapsing the chain
273
+ * to a single hop. Called once by the Telo.Import controller after this module's own imports
274
+ * have initialized (leaves-first), so re-export sources are already registered — an
275
+ * unresolvable re-export is therefore a permanent misconfiguration and throws here. */
276
+ buildExportTable(
277
+ entries: ReadonlyArray<{ name: string; alias?: string }>,
278
+ kindEntries: ReadonlyArray<{ name: string; alias?: string }>,
279
+ moduleName: string,
280
+ ): void {
281
+ for (const entry of entries) {
282
+ const name = entry.name;
283
+ if (entry.alias && entry.alias !== "Self") {
284
+ // A re-export resolves against this module's own imports, which have all initialized
285
+ // by now (leaves-first) — so an unresolved source is a permanent misconfiguration, not
286
+ // a transient miss. Surface it instead of silently dropping the export.
287
+ const scope = this.importedScopes.get(entry.alias);
288
+ if (!scope) {
289
+ throw new RuntimeError(
290
+ "ERR_INVALID_REEXPORT",
291
+ `Library '${moduleName}' re-exports '${entry.alias}.${name}' but declares no import ` +
292
+ `aliased '${entry.alias}'. Add it to this library's 'imports', or remove the ` +
293
+ `'exports.resources' entry.`,
294
+ );
295
+ }
296
+ const terminal = scope.terminal(name);
297
+ if (!terminal) {
298
+ throw new RuntimeError(
299
+ "ERR_INVALID_REEXPORT",
300
+ `Library '${moduleName}' re-exports '${entry.alias}.${name}', but the imported ` +
301
+ `library '${entry.alias}' exports no instance named '${name}'.`,
302
+ );
303
+ }
304
+ this.exportedGetters.set(name, terminal);
305
+ continue;
306
+ }
307
+ this.exportedGetters.set(name, () => {
308
+ const inst = this.resourceInstances.get(name);
309
+ if (!inst) return undefined;
310
+ // Canonicalize the authored kind to `<module>.<Kind>` so the cross-module ref shape
311
+ // and event naming are scope-independent (matches resolve-ref-sentinels.ts).
312
+ const rawKind = inst.resource.kind as string;
313
+ const kind = rawKind.startsWith("Self.")
314
+ ? `${moduleName}.${rawKind.slice("Self.".length)}`
315
+ : rawKind;
316
+ return { kind, instance: inst.instance };
317
+ });
318
+ }
319
+ for (const k of kindEntries) {
320
+ if (k.alias && k.alias !== "Self") {
321
+ const resolver = this.importedKindResolvers.get(k.alias);
322
+ if (!resolver) {
323
+ throw new RuntimeError(
324
+ "ERR_INVALID_REEXPORT",
325
+ `Library '${moduleName}' re-exports kind '${k.alias}.${k.name}' but declares no ` +
326
+ `import aliased '${k.alias}'. Add it to this library's 'imports', or remove the ` +
327
+ `'exports.kinds' entry.`,
328
+ );
329
+ }
330
+ const canonical = resolver(k.name);
331
+ if (!canonical) {
332
+ throw new RuntimeError(
333
+ "ERR_INVALID_REEXPORT",
334
+ `Library '${moduleName}' re-exports kind '${k.alias}.${k.name}', but the imported ` +
335
+ `library '${k.alias}' exports no kind named '${k.name}'.`,
336
+ );
337
+ }
338
+ this.exportedKinds.set(k.name, canonical);
339
+ continue;
340
+ }
341
+ this.exportedKinds.set(k.name, `${moduleName}.${k.name}`);
342
+ }
214
343
  }
215
344
 
216
345
  /** Resolve `Alias.name` to a live exported instance, gated by `exports.resources`.
217
346
  * Returns undefined when the alias is unknown, the name isn't exported, or the import
218
- * hasn't initialized its instances yet (the injection path defers and retries). */
347
+ * hasn't built its export table yet (the injection path defers and retries). O(1). */
219
348
  override resolveImportedInstance(alias: string, name: string): ResourceInstance | undefined {
220
349
  const scope = this.importedScopes.get(alias);
221
350
  if (!scope || !scope.names.has(name)) return undefined;
222
- return scope.get(name)?.instance;
351
+ return scope.terminal(name)?.()?.instance;
223
352
  }
224
353
 
225
354
  /** Like `resolveImportedInstance`, but returns the `{kind, name}` ref (canonical kind)
@@ -228,7 +357,7 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
228
357
  resolveImportedRef(alias: string, name: string): { kind: string; name: string } | undefined {
229
358
  const scope = this.importedScopes.get(alias);
230
359
  if (!scope || !scope.names.has(name)) return undefined;
231
- const entry = scope.get(name);
360
+ const entry = scope.terminal(name)?.();
232
361
  return entry ? { kind: entry.kind, name } : undefined;
233
362
  }
234
363
 
@@ -292,6 +421,14 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
292
421
  `Exported kinds: ${[...allowed].join(", ")}`,
293
422
  );
294
423
  }
424
+ // Re-export override: if this import's exported-kind table maps the suffix to a DIFFERENT
425
+ // owning module, the kind is transitively re-exported (`exports.kinds: [Alias.Kind]`) —
426
+ // resolve to its true owner. A local kind maps to `${realModule}.${suffix}` (no override),
427
+ // and a module without `exports.kinds` has an empty table (unrestricted, unchanged). Built
428
+ // deferred, so before the import inits this returns the un-overridden kind, whose controller
429
+ // miss makes the init loop retry until the table is ready.
430
+ const reExported = this.importedKindResolvers.get(prefix)?.(suffix);
431
+ if (reExported && reExported !== `${realModule}.${suffix}`) return reExported;
295
432
  return `${realModule}.${suffix}`;
296
433
  }
297
434
 
@@ -363,7 +500,12 @@ export class ModuleContext extends EvaluationContext implements IModuleContext {
363
500
  await this.run(target, ctx);
364
501
  continue;
365
502
  }
366
- if ("invoke" in target && target.invoke !== undefined) {
503
+ // A bare `!ref Alias.name` boot target is Phase-5-injected into the live
504
+ // instance, which for a Run.Sequence exposes both `run()` and `invoke()`.
505
+ // Guard against treating such an instance as an authored inline-invoke
506
+ // step — only a structural `{ invoke: <ref>, inputs }` spec (no `run`)
507
+ // belongs here; the live instance falls through to the runnable branch.
508
+ if (!isRunnableInstance(target) && "invoke" in target && target.invoke !== undefined) {
367
509
  const step: InvokeStep = {
368
510
  name: target.name ?? `Target${i}`,
369
511
  when: target.when,
@@ -1,5 +1,5 @@
1
1
  import { evaluate } from "@marcbachmann/cel-js";
2
- import { DataValidator, RuntimeError, TypeRule } from "@telorun/sdk";
2
+ import { DataValidator, isCompiledValue, RuntimeError, TypeRule } from "@telorun/sdk";
3
3
  import AjvModule, { type ValidateFunction } from "ajv";
4
4
  import standaloneCodeMod from "ajv/dist/standalone/index.js";
5
5
  import addFormats from "ajv-formats";
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
8
8
  import { createRequire } from "node:module";
9
9
  import * as path from "node:path";
10
10
  import { formatAjvErrors } from "./manifest-schemas.js";
11
- import { ManifestRootSchema, normalizeRefSlots } from "@telorun/templating";
11
+ import { isTaggedSentinel, ManifestRootSchema, normalizeRefSlots } from "@telorun/templating";
12
12
 
13
13
  const Ajv = AjvModule.default ?? AjvModule;
14
14
  // AJV's standalone subpath is CJS — the default export shows up as either
@@ -78,6 +78,39 @@ function verifyAndExtractBody(text: string): string | null {
78
78
  return actual === match[1] ? body : null;
79
79
  }
80
80
 
81
+ /** Deep-clone `value` collapsing every CEL/template sentinel to its original
82
+ * `source` string, for use as the validator cache *key* only.
83
+ *
84
+ * The same `Telo.Definition` schema reaches `compile()` in two forms: the
85
+ * build-time validator warm feeds the raw analysis graph (parse-time
86
+ * `{__tagged, engine, source}` sentinels, or plain `${{ }}` strings for inline
87
+ * templates), while the runtime feeds the compile-loader output (`{__compiled,
88
+ * source, parts}`). A `!cel` / `!sql` tag embedded anywhere in a schema — most
89
+ * commonly inside `examples` / `description`, but the loader rewrites them at
90
+ * any position — therefore serialises differently between the two, so without
91
+ * normalisation the same kind hashes differently and the runtime misses the
92
+ * warmed cache (and fails to persist it read-only). Both sentinel shapes carry
93
+ * the same original `source`, so collapsing each to that string converges them.
94
+ *
95
+ * This rewrites only the hash key — AJV still compiles the full `injected`
96
+ * schema. Unlike dropping keys by name, it never removes a structural node, so
97
+ * a property literally named `description` / `examples` keeps its schema in the
98
+ * key and two genuinely different shapes never collide. */
99
+ function normalizeSentinelsForHash(value: unknown): unknown {
100
+ if (isCompiledValue(value) || isTaggedSentinel(value)) {
101
+ return typeof value.source === "string" ? value.source : { __teloSentinel: true };
102
+ }
103
+ if (Array.isArray(value)) return value.map(normalizeSentinelsForHash);
104
+ if (value && typeof value === "object") {
105
+ const out: Record<string, unknown> = {};
106
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
107
+ out[k] = normalizeSentinelsForHash(v);
108
+ }
109
+ return out;
110
+ }
111
+ return value;
112
+ }
113
+
81
114
  export class SchemaValidator {
82
115
  private ajv: InstanceType<typeof Ajv>;
83
116
  private typeRules = new Map<string, TypeRule[]>();
@@ -194,7 +227,12 @@ export class SchemaValidator {
194
227
  const injected = normalizeRefSlots(withImplicit) as typeof withImplicit;
195
228
 
196
229
  const hash = createHash("sha256")
197
- .update(JSON.stringify({ runtime: VALIDATOR_RUNTIME_TAG, schema: injected }))
230
+ .update(
231
+ JSON.stringify({
232
+ runtime: VALIDATOR_RUNTIME_TAG,
233
+ schema: normalizeSentinelsForHash(injected),
234
+ }),
235
+ )
198
236
  .digest("hex")
199
237
  .slice(0, 32);
200
238
  const cachedByHash = this.hashCache.get(hash);