@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.
- package/dist/application-env.d.ts +18 -0
- package/dist/application-env.d.ts.map +1 -1
- package/dist/application-env.js +36 -0
- package/dist/application-env.js.map +1 -1
- package/dist/controller-loaders/npm-loader.d.ts +41 -12
- package/dist/controller-loaders/npm-loader.d.ts.map +1 -1
- package/dist/controller-loaders/npm-loader.js +117 -51
- package/dist/controller-loaders/npm-loader.js.map +1 -1
- package/dist/controller-registry.d.ts +9 -0
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js +18 -0
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts.map +1 -1
- package/dist/controllers/module/import-controller.js +50 -28
- package/dist/controllers/module/import-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +13 -5
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +17 -1
- package/dist/kernel.js.map +1 -1
- package/dist/module-context.d.ts +56 -7
- package/dist/module-context.d.ts.map +1 -1
- package/dist/module-context.js +121 -10
- package/dist/module-context.js.map +1 -1
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +39 -3
- package/dist/schema-validator.js.map +1 -1
- package/package.json +2 -2
- package/src/application-env.ts +37 -0
- package/src/controller-loaders/npm-loader.ts +124 -58
- package/src/controller-registry.ts +18 -0
- package/src/controllers/module/import-controller.ts +58 -28
- package/src/evaluation-context.ts +15 -5
- package/src/kernel.ts +24 -1
- package/src/module-context.ts +153 -11
- package/src/schema-validator.ts +41 -3
package/src/module-context.ts
CHANGED
|
@@ -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
|
-
|
|
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`); `
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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
|
-
|
|
237
|
+
terminal: (
|
|
238
|
+
name: string,
|
|
239
|
+
) => (() => { kind: string; instance: ResourceInstance } | undefined) | undefined,
|
|
212
240
|
): void {
|
|
213
|
-
this.importedScopes.set(alias, { names: new Set(names),
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
package/src/schema-validator.ts
CHANGED
|
@@ -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(
|
|
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);
|