@telorun/kernel 0.35.0 → 0.37.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/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +6 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.d.ts +6 -2
- package/dist/controllers/resource-definition/resource-template-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-template-controller.js +170 -127
- package/dist/controllers/resource-definition/resource-template-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +8 -0
- package/dist/evaluation-context.d.ts.map +1 -1
- package/dist/evaluation-context.js +12 -0
- package/dist/evaluation-context.js.map +1 -1
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +29 -3
- package/dist/kernel.js.map +1 -1
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +5 -3
- package/dist/resource-context.js.map +1 -1
- package/package.json +3 -3
- package/src/controllers/resource-definition/resource-definition-controller.ts +6 -1
- package/src/controllers/resource-definition/resource-template-controller.ts +190 -162
- package/src/evaluation-context.ts +19 -0
- package/src/kernel.ts +32 -2
- package/src/resource-context.ts +5 -9
|
@@ -1,18 +1,49 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CompiledValue,
|
|
3
|
+
ControllerInstance,
|
|
4
|
+
EvaluationContext,
|
|
5
|
+
ResourceContext,
|
|
6
|
+
ResourceInstance,
|
|
7
|
+
} from "@telorun/sdk";
|
|
2
8
|
import { isCompiledValue } from "@telorun/sdk";
|
|
9
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
10
|
+
|
|
11
|
+
/** CEL variables that are only bound at call time (request handling, step
|
|
12
|
+
* chaining, error branches) — never at a template's init(). A persistent
|
|
13
|
+
* child's body is expanded against `self` only, so a `${{ }}` node referencing
|
|
14
|
+
* any of these must survive untouched for the consuming controller (e.g. an
|
|
15
|
+
* Http.Api evaluating route CEL per request) to evaluate later. A node mixing
|
|
16
|
+
* `self` with a deferred variable in one expression is unsupported by design —
|
|
17
|
+
* keep `self`-derived literals and request-derived values in separate fields
|
|
18
|
+
* (e.g. a `self`-built SQL string vs. request-built `bindings`). */
|
|
19
|
+
const DEFERRED_VARS = new Set(["request", "result", "steps", "error"]);
|
|
20
|
+
const DEFERRED_RE = new RegExp(`(?<![.\\w])(?:${[...DEFERRED_VARS].join("|")})(?![\\w])`);
|
|
21
|
+
|
|
22
|
+
/** True when an expression reads a call-time-only variable. Prefers the AST-
|
|
23
|
+
* derived root identifiers stamped on the CompiledValue at compile time (exact —
|
|
24
|
+
* ignores string literals and substrings); falls back to a source-text scan for
|
|
25
|
+
* values produced by engines that surface no AST. */
|
|
26
|
+
function referencesDeferred(value: CompiledValue): boolean {
|
|
27
|
+
if (value.refs) return value.refs.some((r) => DEFERRED_VARS.has(r));
|
|
28
|
+
return typeof value.source === "string" && DEFERRED_RE.test(value.source);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Matches a CEL source that is exactly a `self.<path>` member access (capturing
|
|
32
|
+
* the `.<path>` tail) — the form resolved by direct navigation rather than CEL. */
|
|
33
|
+
const SELF_PATH = /^self((?:\.[A-Za-z_$][\w$]*)+)$/;
|
|
3
34
|
|
|
4
35
|
/** Reports the resources: entries available to dispatch against, by expanded
|
|
5
36
|
* name and kind. Used in error messages to guide the developer back to the
|
|
6
37
|
* template's `resources:` array when a dispatch target doesn't match. */
|
|
7
38
|
function describeAvailableTargets(
|
|
8
|
-
ctx:
|
|
39
|
+
ctx: EvaluationContext,
|
|
9
40
|
resources: any[] | undefined,
|
|
10
41
|
self: Record<string, unknown>,
|
|
11
42
|
): string {
|
|
12
43
|
if (!resources || resources.length === 0) return "<none>";
|
|
13
44
|
return resources
|
|
14
45
|
.map((r) => {
|
|
15
|
-
const expanded = ctx.
|
|
46
|
+
const expanded = ctx.expandWith(r?.metadata?.name ?? "", { self }) as string;
|
|
16
47
|
const kind = typeof r?.kind === "string" ? r.kind : "<unknown-kind>";
|
|
17
48
|
return `'${expanded || "<unnamed>"}' (${kind})`;
|
|
18
49
|
})
|
|
@@ -25,201 +56,198 @@ export function createTemplateController(definition: {
|
|
|
25
56
|
invoke?: string | { kind?: string; name: string };
|
|
26
57
|
inputs?: Record<string, any>;
|
|
27
58
|
run?: string;
|
|
59
|
+
mount?: string | { kind?: string; name: string };
|
|
28
60
|
provide?: { kind: string; name: string };
|
|
29
61
|
result?: Record<string, any>;
|
|
30
|
-
}): ControllerInstance {
|
|
62
|
+
}, definingContext: EvaluationContext): ControllerInstance {
|
|
31
63
|
return {
|
|
32
64
|
schema: definition.schema ?? { type: "object", additionalProperties: true },
|
|
33
65
|
|
|
34
66
|
create: async (resource: any, ctx: ResourceContext): Promise<ResourceInstance> => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// `
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
67
|
+
void ctx; // child scope is rooted on definingContext, not the instance's ctx
|
|
68
|
+
// `self` is read lazily: Phase 5 injection mutates `resource`'s ref slots
|
|
69
|
+
// (e.g. `connection: !ref Db` → the live instance) AFTER create() but before
|
|
70
|
+
// init(), so capturing self here would freeze the pre-injection refs. Every
|
|
71
|
+
// expansion reads the current resource state instead.
|
|
72
|
+
const getSelf = () => ({ ...resource, name: resource.metadata.name });
|
|
73
|
+
|
|
74
|
+
// A dispatch field names which `resources:` entry receives the call. It is
|
|
75
|
+
// a string name template (legacy shorthand) or an object `{ kind?, name }`
|
|
76
|
+
// for explicit kind-typed dispatch. Per-call data lives on the top-level
|
|
77
|
+
// `inputs:` sibling (same factoring as Run.Sequence steps), never in the
|
|
78
|
+
// target's resource body — the body is `self`-only so every child can be
|
|
79
|
+
// created once at init and reused across calls.
|
|
80
|
+
const targetName = (field: string | { kind?: string; name: string } | undefined): string | null => {
|
|
81
|
+
if (field == null) return null;
|
|
82
|
+
const nameTemplate =
|
|
83
|
+
typeof field === "object" && !isCompiledValue(field) ? field.name : field;
|
|
84
|
+
return nameTemplate
|
|
85
|
+
? (definingContext.expandWith(nameTemplate, { self: getSelf() }) as string)
|
|
47
86
|
: null;
|
|
48
|
-
|
|
49
|
-
const invokeTarget = invokeNameTemplate
|
|
50
|
-
? (ctx.moduleContext.expandWith(invokeNameTemplate, { self }) as string)
|
|
51
|
-
: null;
|
|
52
|
-
const runTarget = definition.run
|
|
53
|
-
? (ctx.moduleContext.expandWith(definition.run, { self }) as string)
|
|
54
|
-
: null;
|
|
55
|
-
const provideTarget = definition.provide?.name
|
|
56
|
-
? (ctx.moduleContext.expandWith(definition.provide.name, { self }) as string)
|
|
57
|
-
: null;
|
|
58
|
-
|
|
59
|
-
const persistentManifests: any[] = [];
|
|
60
|
-
let ephemeralTemplate: any = null;
|
|
87
|
+
};
|
|
61
88
|
|
|
89
|
+
const invokeTarget = targetName(definition.invoke);
|
|
90
|
+
const runTarget = targetName(definition.run);
|
|
91
|
+
const mountTarget = targetName(definition.mount);
|
|
92
|
+
const provideTarget = targetName(definition.provide);
|
|
93
|
+
|
|
94
|
+
const childContext = definingContext.spawnChildContext();
|
|
95
|
+
|
|
96
|
+
// Resolves the live instance of a dispatch target from the child context.
|
|
97
|
+
// Every `resources:` entry is a persistent child created once at init(),
|
|
98
|
+
// so the target is looked up — never re-created — per call.
|
|
99
|
+
const dispatchEntry = (target: string, role: string) => {
|
|
100
|
+
const entry = childContext.resourceInstances.get(target);
|
|
101
|
+
if (!entry) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Template '${resource.metadata.name}': '${role}:' targets '${target}' ` +
|
|
104
|
+
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(definingContext, definition.resources, getSelf())}.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return entry;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const capabilityError = (entry: any, target: string, role: string, expected: string): Error => {
|
|
111
|
+
const targetKind = (entry?.resource?.kind ?? "<unknown-kind>") as string;
|
|
112
|
+
const targetDef = definingContext.getDefinition?.(targetKind);
|
|
113
|
+
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
114
|
+
return new Error(
|
|
115
|
+
`Template '${resource.metadata.name}': '${role}:' target '${targetKind}/${target}' ` +
|
|
116
|
+
`has capability '${actualCap}', not ${expected}. Update '${role}:' to a ${expected} kind, ` +
|
|
117
|
+
`or change the target's kind in 'resources:'.`,
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const expand = (value: any, extra: Record<string, unknown>) =>
|
|
122
|
+
definingContext.expandWith(definingContext.expandWith(value, extra), extra);
|
|
123
|
+
|
|
124
|
+
// A local `!ref` inside a template body names a sibling `resources:` entry.
|
|
125
|
+
// The entry carries the kind, so resolve each sibling's expanded name to its
|
|
126
|
+
// kind — used to stamp the ref's `{kind, name}` injection shape (an empty
|
|
127
|
+
// kind is rejected downstream as a malformed inline resource).
|
|
128
|
+
const siblingKinds = new Map<string, string>();
|
|
62
129
|
for (const template of definition.resources ?? []) {
|
|
63
|
-
const expandedName =
|
|
64
|
-
self,
|
|
130
|
+
const expandedName = definingContext.expandWith(template?.metadata?.name ?? "", {
|
|
131
|
+
self: getSelf(),
|
|
65
132
|
}) as string;
|
|
66
|
-
|
|
67
|
-
expandedName
|
|
68
|
-
expandedName === runTarget ||
|
|
69
|
-
expandedName === provideTarget;
|
|
70
|
-
if (isTarget) {
|
|
71
|
-
ephemeralTemplate = template;
|
|
72
|
-
} else {
|
|
73
|
-
persistentManifests.push(ctx.moduleContext.expandWith(template, { self }));
|
|
133
|
+
if (expandedName && typeof template?.kind === "string") {
|
|
134
|
+
siblingKinds.set(expandedName, template.kind);
|
|
74
135
|
}
|
|
75
136
|
}
|
|
76
137
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
138
|
+
// Expand a persistent child's body against `self`. Self-only CEL resolves
|
|
139
|
+
// to literals now; CEL bound only at call time (DEFERRED_VARS) passes
|
|
140
|
+
// through compiled for the child's own controller. `!ref` sentinels are
|
|
141
|
+
// rewritten to the `{kind, name, alias?}` injection shape here — Phase 2.5
|
|
142
|
+
// (`resolveRefSentinels`) does not descend into template bodies, so the
|
|
143
|
+
// child context's Phase 5 injection would otherwise see an unrecognized
|
|
144
|
+
// sentinel and leave the slot unresolved. Kind is left empty: injection
|
|
145
|
+
// dispatches by name and recovers the kind from the resolved instance.
|
|
146
|
+
const expandSelf = (value: any): any => {
|
|
147
|
+
if (isCompiledValue(value)) {
|
|
148
|
+
if (referencesDeferred(value)) return value;
|
|
149
|
+
// A pure `self.<path>` access (e.g. a `connection: !ref` passed down) is
|
|
150
|
+
// resolved by navigating the resource directly. Going through CEL would
|
|
151
|
+
// re-emit the value through CEL's output type-checker, which rejects live
|
|
152
|
+
// resource instances (unrecognized class constructors) — so the connection
|
|
153
|
+
// a consumer wired in could never reach a child's slot. Complex self
|
|
154
|
+
// expressions (string building) still evaluate via CEL, where they yield
|
|
155
|
+
// CEL-safe scalars.
|
|
156
|
+
const path = typeof value.source === "string" ? value.source.trim().match(SELF_PATH) : null;
|
|
157
|
+
if (path) {
|
|
158
|
+
let cur: any = getSelf();
|
|
159
|
+
for (const key of path[1].split(".").slice(1)) cur = cur?.[key];
|
|
160
|
+
return cur;
|
|
161
|
+
}
|
|
162
|
+
return definingContext.expandWith(value, { self: getSelf() });
|
|
100
163
|
}
|
|
164
|
+
if (isRefSentinel(value)) {
|
|
165
|
+
const source = value.source;
|
|
166
|
+
const dot = source.indexOf(".");
|
|
167
|
+
const alias = dot > 0 ? source.slice(0, dot) : undefined;
|
|
168
|
+
if (alias && alias !== "Self") {
|
|
169
|
+
const name = source.slice(dot + 1);
|
|
170
|
+
return { kind: siblingKinds.get(name) ?? "", name, alias };
|
|
171
|
+
}
|
|
172
|
+
const name = alias === "Self" ? source.slice(dot + 1) : source;
|
|
173
|
+
return { kind: siblingKinds.get(name) ?? "", name };
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) return value.map(expandSelf);
|
|
176
|
+
if (value !== null && typeof value === "object") {
|
|
177
|
+
const out: Record<string, unknown> = {};
|
|
178
|
+
for (const [k, v] of Object.entries(value)) out[k] = expandSelf(v);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
101
182
|
};
|
|
102
183
|
|
|
184
|
+
// init() may run more than once: when a child's local ref names a sibling
|
|
185
|
+
// not yet initialized, child init defers with ERR_LOCAL_REF_PENDING and the
|
|
186
|
+
// outer multi-pass loop retries this resource. Registration must happen
|
|
187
|
+
// once; each retry only resumes the child init loop (already-initialized
|
|
188
|
+
// children are skipped, still-pending ones advance).
|
|
189
|
+
let registered = false;
|
|
190
|
+
|
|
103
191
|
return {
|
|
104
192
|
init: async () => {
|
|
105
|
-
|
|
193
|
+
if (!registered) {
|
|
194
|
+
for (const template of definition.resources ?? []) {
|
|
195
|
+
childContext.registerManifest(expandSelf(template));
|
|
196
|
+
}
|
|
197
|
+
registered = true;
|
|
198
|
+
}
|
|
106
199
|
await childContext.initializeResources();
|
|
107
200
|
},
|
|
108
201
|
|
|
109
202
|
...(invokeTarget && {
|
|
110
203
|
invoke: async (inputs: any) => {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
115
|
-
);
|
|
204
|
+
const entry = dispatchEntry(invokeTarget, "invoke");
|
|
205
|
+
if (!entry.instance?.invoke) {
|
|
206
|
+
throw capabilityError(entry, invokeTarget, "invoke", "Telo.Invocable");
|
|
116
207
|
}
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
return withEphemeral(expanded, async (name) => {
|
|
123
|
-
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
124
|
-
if (!entry?.instance?.invoke) {
|
|
125
|
-
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
126
|
-
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
127
|
-
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
128
|
-
throw new Error(
|
|
129
|
-
`Template '${resource.metadata.name}': 'invoke:' target '${targetKind}/${invokeTarget}' ` +
|
|
130
|
-
`has capability '${actualCap}', not Telo.Invocable. Update 'invoke:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
// Top-level `inputs:` (sibling of `invoke:`) carries the values passed
|
|
134
|
-
// to the dispatch target's invoke(). When absent, fall back to the
|
|
135
|
-
// expanded resource entry's own `inputs` field (legacy string-form
|
|
136
|
-
// shape where the inputs live on the resource declaration), then
|
|
137
|
-
// finally to the caller's `inputs` arg.
|
|
138
|
-
const invokeInputs = definition.inputs != null
|
|
139
|
-
? ctx.moduleContext.expandWith(
|
|
140
|
-
ctx.moduleContext.expandWith(definition.inputs, extraContext),
|
|
141
|
-
extraContext,
|
|
142
|
-
)
|
|
143
|
-
: expanded.inputs ?? inputs;
|
|
144
|
-
const raw = await entry.instance.invoke(invokeInputs);
|
|
145
|
-
if (definition.result == null) return raw;
|
|
146
|
-
const resultContext = { self, result: raw };
|
|
147
|
-
return ctx.moduleContext.expandWith(
|
|
148
|
-
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
149
|
-
resultContext,
|
|
150
|
-
);
|
|
151
|
-
});
|
|
208
|
+
const invokeInputs =
|
|
209
|
+
definition.inputs != null ? expand(definition.inputs, { self: getSelf(), inputs }) : inputs;
|
|
210
|
+
const raw = await entry.instance.invoke(invokeInputs);
|
|
211
|
+
if (definition.result == null) return raw;
|
|
212
|
+
return expand(definition.result, { self: getSelf(), result: raw });
|
|
152
213
|
},
|
|
153
214
|
}),
|
|
154
215
|
|
|
155
216
|
...(runTarget && {
|
|
156
217
|
run: async () => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
`but no entry in 'resources:' has that metadata.name. Available: ${describeAvailableTargets(ctx, definition.resources, self)}.`,
|
|
161
|
-
);
|
|
218
|
+
const entry = dispatchEntry(runTarget, "run");
|
|
219
|
+
if (!entry.instance?.run) {
|
|
220
|
+
throw capabilityError(entry, runTarget, "run", "Telo.Runnable");
|
|
162
221
|
}
|
|
163
|
-
|
|
164
|
-
const expanded = ctx.moduleContext.expandWith(
|
|
165
|
-
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
166
|
-
extraContext,
|
|
167
|
-
) as any;
|
|
168
|
-
return withEphemeral(expanded, async (name) => {
|
|
169
|
-
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
170
|
-
if (!entry?.instance?.run) {
|
|
171
|
-
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
172
|
-
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
173
|
-
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Template '${resource.metadata.name}': 'run:' target '${targetKind}/${runTarget}' ` +
|
|
176
|
-
`has capability '${actualCap}', not Telo.Runnable. Update 'run:' to a Telo.Runnable kind, or change the target's kind in 'resources:'.`,
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
return entry.instance.run();
|
|
180
|
-
});
|
|
222
|
+
return entry.instance.run();
|
|
181
223
|
},
|
|
182
224
|
}),
|
|
183
225
|
|
|
184
226
|
...(provideTarget && {
|
|
185
227
|
provide: async () => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
228
|
+
const entry = dispatchEntry(provideTarget, "provide");
|
|
229
|
+
if (!entry.instance?.invoke) {
|
|
230
|
+
throw capabilityError(entry, provideTarget, "provide", "Telo.Invocable");
|
|
231
|
+
}
|
|
232
|
+
const provideInputs: any =
|
|
233
|
+
definition.inputs != null ? expand(definition.inputs, { self: getSelf() }) : {};
|
|
234
|
+
const raw = await entry.instance.invoke(provideInputs);
|
|
235
|
+
if (definition.result == null) return raw;
|
|
236
|
+
return expand(definition.result, { self: getSelf(), result: raw });
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
|
|
240
|
+
...(mountTarget && {
|
|
241
|
+
// `register(app, prefix)` is the Telo.Mount contract a consuming
|
|
242
|
+
// Http.Server calls. It is not on the base ResourceInstance type, so
|
|
243
|
+
// the persistent mount child is accessed structurally.
|
|
244
|
+
register: (app: any, prefix?: string) => {
|
|
245
|
+
const entry = dispatchEntry(mountTarget, "mount");
|
|
246
|
+
const mountable = entry.instance as { register?: (app: any, prefix?: string) => unknown };
|
|
247
|
+
if (typeof mountable.register !== "function") {
|
|
248
|
+
throw capabilityError(entry, mountTarget, "mount", "Telo.Mount");
|
|
191
249
|
}
|
|
192
|
-
|
|
193
|
-
const expanded = ctx.moduleContext.expandWith(
|
|
194
|
-
ctx.moduleContext.expandWith(ephemeralTemplate, extraContext),
|
|
195
|
-
extraContext,
|
|
196
|
-
) as any;
|
|
197
|
-
return withEphemeral(expanded, async (name) => {
|
|
198
|
-
const entry = ctx.moduleContext.resourceInstances.get(name);
|
|
199
|
-
if (!entry?.instance?.invoke) {
|
|
200
|
-
const targetKind = (entry?.resource?.kind ?? expanded?.kind ?? "<unknown-kind>") as string;
|
|
201
|
-
const targetDef = ctx.moduleContext.getDefinition?.(targetKind);
|
|
202
|
-
const actualCap = typeof targetDef?.capability === "string" ? targetDef.capability : "<unknown>";
|
|
203
|
-
throw new Error(
|
|
204
|
-
`Template '${resource.metadata.name}': 'provide:' target '${targetKind}/${provideTarget}' ` +
|
|
205
|
-
`has capability '${actualCap}', not Telo.Invocable. Update 'provide:' to a Telo.Invocable kind, or change the target's kind in 'resources:'.`,
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
const provideInputs: any =
|
|
209
|
-
definition.inputs != null
|
|
210
|
-
? ctx.moduleContext.expandWith(
|
|
211
|
-
ctx.moduleContext.expandWith(definition.inputs, extraContext),
|
|
212
|
-
extraContext,
|
|
213
|
-
)
|
|
214
|
-
: {};
|
|
215
|
-
const raw = await entry.instance.invoke(provideInputs);
|
|
216
|
-
if (definition.result == null) return raw;
|
|
217
|
-
const resultContext = { self, result: raw };
|
|
218
|
-
return ctx.moduleContext.expandWith(
|
|
219
|
-
ctx.moduleContext.expandWith(definition.result, resultContext),
|
|
220
|
-
resultContext,
|
|
221
|
-
);
|
|
222
|
-
});
|
|
250
|
+
return mountable.register(app, prefix);
|
|
223
251
|
},
|
|
224
252
|
}),
|
|
225
253
|
|
|
@@ -274,6 +274,25 @@ export class EvaluationContext implements IEvaluationContext {
|
|
|
274
274
|
return child;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/** Spawn a fresh child context attached to this node — the isolated scope a
|
|
278
|
+
* templated definition registers its `resources:` into. Rooting it on the
|
|
279
|
+
* context that DEFINED the template (not the consumer that instantiated the
|
|
280
|
+
* kind) is what makes the template's internal kind aliases and `!ref`s
|
|
281
|
+
* resolve against the defining library's imports. Local sibling refs resolve
|
|
282
|
+
* in the child; a cross-module `!ref Alias.name` delegates to this context's
|
|
283
|
+
* imports (a plain child resolves none of its own). */
|
|
284
|
+
spawnChildContext(): EvaluationContext {
|
|
285
|
+
const child = new EvaluationContext(
|
|
286
|
+
this.source,
|
|
287
|
+
this.context,
|
|
288
|
+
this._createInstance,
|
|
289
|
+
this._secretValues,
|
|
290
|
+
this.emit,
|
|
291
|
+
);
|
|
292
|
+
child.resolveImportedInstance = (alias, name) => this.resolveImportedInstance(alias, name);
|
|
293
|
+
return this.spawnChild(child);
|
|
294
|
+
}
|
|
295
|
+
|
|
277
296
|
/**
|
|
278
297
|
* Resolve a cross-module exported instance (`!ref Alias.name`) to its live instance.
|
|
279
298
|
* Overridable hook: the base context has no imports, so it returns undefined; ModuleContext
|
package/src/kernel.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AnalysisRegistry,
|
|
3
|
+
defaultSources,
|
|
3
4
|
flattenForAnalyzer,
|
|
4
5
|
flattenLoadedModule,
|
|
5
6
|
isModuleKind,
|
|
@@ -150,7 +151,7 @@ export class Kernel implements IKernel {
|
|
|
150
151
|
this.env = options.env ?? process.env;
|
|
151
152
|
this.argv = options.argv ?? [];
|
|
152
153
|
this.registryUrl = options.registryUrl;
|
|
153
|
-
this.loader = new Loader(
|
|
154
|
+
this.loader = new Loader(defaultSources(this.registryUrl), { celHandlers: nodeCelHandlers });
|
|
154
155
|
for (const source of options.sources) {
|
|
155
156
|
this.loader.register(source);
|
|
156
157
|
}
|
|
@@ -239,7 +240,21 @@ export class Kernel implements IKernel {
|
|
|
239
240
|
* custom `ManifestSource`s — `isImportValidatedAtLoad` etc. only hit
|
|
240
241
|
* when both sides agree on the canonical URL. */
|
|
241
242
|
resolveImportUrl(fromSource: string, importSource: string): string {
|
|
242
|
-
|
|
243
|
+
const resolved = this.loader.resolveImportUrl(fromSource, importSource);
|
|
244
|
+
// Apply version-reconciliation overrides captured during `load()`: when the
|
|
245
|
+
// entry graph hoisted this module identity to a higher version, redirect the
|
|
246
|
+
// import-controller's independent re-resolution onto the winning source so a
|
|
247
|
+
// sub-library importing a lower version loads the same controller/definition
|
|
248
|
+
// the analyzer registered — never a second, colliding copy. Keyed by
|
|
249
|
+
// canonical URL; `canonicalize` maps a registry ref (returned verbatim by
|
|
250
|
+
// the loader) to the URL the graph walk already resolved it to.
|
|
251
|
+
const overrides = this._loadedGraph?.overrides;
|
|
252
|
+
if (overrides && overrides.size > 0) {
|
|
253
|
+
const canonical = this.loader.canonicalize(resolved) ?? resolved;
|
|
254
|
+
const winner = overrides.get(canonical);
|
|
255
|
+
if (winner) return winner;
|
|
256
|
+
}
|
|
257
|
+
return resolved;
|
|
243
258
|
}
|
|
244
259
|
|
|
245
260
|
/**
|
|
@@ -348,6 +363,21 @@ export class Kernel implements IKernel {
|
|
|
348
363
|
throw analysisGraph.errors[0].error;
|
|
349
364
|
}
|
|
350
365
|
this._loadedGraph = analysisGraph;
|
|
366
|
+
// Version reconciliation: an incompatible major mismatch is fatal (the
|
|
367
|
+
// hoist override would silently run the wrong major); a same-major hoist is
|
|
368
|
+
// advisory — the override already redirects every importer to the winner.
|
|
369
|
+
const versionConflicts = analysisGraph.versionDiagnostics.filter(
|
|
370
|
+
(d) => d.code === "MODULE_VERSION_CONFLICT",
|
|
371
|
+
);
|
|
372
|
+
if (versionConflicts.length > 0) {
|
|
373
|
+
throw new RuntimeError(
|
|
374
|
+
"ERR_MANIFEST_VALIDATION_FAILED",
|
|
375
|
+
versionConflicts.map((d) => d.message).join("\n"),
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
for (const d of analysisGraph.versionDiagnostics) {
|
|
379
|
+
if (d.code === "MODULE_VERSION_HOISTED") console.warn(`warning: ${d.message}`);
|
|
380
|
+
}
|
|
351
381
|
const staticManifests = flattenForAnalyzer(analysisGraph);
|
|
352
382
|
this.staticManifests = staticManifests;
|
|
353
383
|
|
package/src/resource-context.ts
CHANGED
|
@@ -21,7 +21,6 @@ import { isRefSentinel } from "@telorun/templating";
|
|
|
21
21
|
import { stripCompiledValues } from "./schema-compiled-values.js";
|
|
22
22
|
import AjvModule from "ajv";
|
|
23
23
|
import addFormats from "ajv-formats";
|
|
24
|
-
import { EvaluationContext } from "./evaluation-context.js";
|
|
25
24
|
import { Kernel } from "./kernel.js";
|
|
26
25
|
import { formatAjvErrors } from "./manifest-schemas.js";
|
|
27
26
|
import { policyFingerprint } from "./runtime-registry.js";
|
|
@@ -454,14 +453,11 @@ export class ResourceContextImpl implements ResourceContext {
|
|
|
454
453
|
* call initializeResources() to initialize them in isolation.
|
|
455
454
|
*/
|
|
456
455
|
spawnChildContext(): IEvaluationContext {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
this.moduleContext.emit,
|
|
463
|
-
);
|
|
464
|
-
return this.moduleContext.spawnChild(child);
|
|
456
|
+
// One construction path: defer to the module context's own
|
|
457
|
+
// spawnChildContext(). Rooted on this resource's module context (the
|
|
458
|
+
// consumer scope); a templated definition that needs library-scoped
|
|
459
|
+
// resolution calls the defining library's context directly instead.
|
|
460
|
+
return this.moduleContext.spawnChildContext();
|
|
465
461
|
}
|
|
466
462
|
|
|
467
463
|
transientChild(context: Record<string, any>): IEvaluationContext {
|