@telorun/analyzer 0.48.0 → 0.49.1
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/analysis-registry.d.ts +22 -11
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +36 -39
- package/dist/analyzer.d.ts +38 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +115 -83
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +72 -1
- package/dist/extends-resolution.d.ts +41 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +68 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/invocation-contract.d.ts +100 -0
- package/dist/invocation-contract.d.ts.map +1 -0
- package/dist/invocation-contract.js +208 -0
- package/dist/manifest-loader.d.ts +11 -0
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +20 -0
- package/dist/schema-compat.d.ts +12 -4
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +185 -9
- package/dist/validate-base-mapping.js +11 -1
- package/dist/validate-cel-context.d.ts +0 -6
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +51 -4
- package/dist/validate-invocation-contract.d.ts +30 -0
- package/dist/validate-invocation-contract.d.ts.map +1 -0
- package/dist/validate-invocation-contract.js +394 -0
- package/dist/validate-step-inputs.d.ts +24 -0
- package/dist/validate-step-inputs.d.ts.map +1 -0
- package/dist/validate-step-inputs.js +87 -0
- package/dist/validate-throws-coverage.d.ts +1 -1
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +9 -1
- package/package.json +3 -3
- package/src/analysis-registry.ts +44 -34
- package/src/analyzer.ts +171 -100
- package/src/builtins.ts +74 -1
- package/src/extends-resolution.ts +86 -0
- package/src/index.ts +13 -1
- package/src/invocation-contract.ts +275 -0
- package/src/manifest-loader.ts +20 -0
- package/src/schema-compat.ts +191 -8
- package/src/validate-base-mapping.ts +14 -1
- package/src/validate-cel-context.ts +49 -4
- package/src/validate-invocation-contract.ts +450 -0
- package/src/validate-step-inputs.ts +117 -0
- package/src/validate-throws-coverage.ts +12 -2
package/src/analysis-registry.ts
CHANGED
|
@@ -4,9 +4,10 @@ import { KERNEL_BUILTINS } from "./builtins.js";
|
|
|
4
4
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { computeSuggestKind, computeValidUserFacingKinds } from "./kind-suggest.js";
|
|
6
6
|
import { visitManifest as runVisitManifest, type ManifestVisitor } from "./manifest-visitor.js";
|
|
7
|
+
import type { ContractDirection, DefResolver } from "./extends-resolution.js";
|
|
8
|
+
import { resolveContract } from "./invocation-contract.js";
|
|
7
9
|
import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
8
10
|
import type { AnalysisContext } from "./types.js";
|
|
9
|
-
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
10
11
|
|
|
11
12
|
/** One reference field declared by a resource's definition, derived purely from
|
|
12
13
|
* the schema field map (independent of whether the manifest fills it). Editor
|
|
@@ -133,46 +134,44 @@ export class AnalysisRegistry {
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/** Resolves the JSON Schema for a kind's `invoke()` inputs, for editor hosts
|
|
136
|
-
* that render a typed inputs form.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
137
|
+
* that render a typed inputs form. A thin wrapper over the shared contract
|
|
138
|
+
* resolver, so the form an editor renders is the contract `telo check` and the
|
|
139
|
+
* kernel enforce — nearest declaration along `extends`, replacing rather than
|
|
140
|
+
* merging. A bare named type reference is left unresolved (returns undefined,
|
|
141
|
+
* no manifests are in scope here) so the caller can fall back to a freeform
|
|
142
|
+
* map. Undefined when the kind declares no input contract. */
|
|
142
143
|
inputTypeForKind(kind: string): Record<string, unknown> | undefined {
|
|
143
|
-
|
|
144
|
-
if (!def) return undefined;
|
|
145
|
-
const own = resolveTypeFieldToSchema(def.inputType, []);
|
|
146
|
-
if (own) return own;
|
|
147
|
-
if (def.extends) {
|
|
148
|
-
const abstractDef = this.resolveDefinition(def.extends);
|
|
149
|
-
if (abstractDef) {
|
|
150
|
-
const inherited = resolveTypeFieldToSchema(abstractDef.inputType, []);
|
|
151
|
-
if (inherited) return inherited;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return undefined;
|
|
144
|
+
return this.contractForKind(kind, "inputType");
|
|
155
145
|
}
|
|
156
146
|
|
|
157
|
-
/** Resolves the JSON Schema for a kind's `invoke()` / `
|
|
147
|
+
/** Resolves the JSON Schema for a kind's `invoke()` / `provide()` output, for
|
|
158
148
|
* editor hosts that render a typed output signature. Mirrors
|
|
159
|
-
* {@link inputTypeForKind}
|
|
160
|
-
*
|
|
161
|
-
* raw-schema forms; a bare named type reference is left unresolved. Undefined
|
|
162
|
-
* when the kind declares no output contract. */
|
|
149
|
+
* {@link inputTypeForKind}. Undefined when the kind declares no output
|
|
150
|
+
* contract. */
|
|
163
151
|
outputTypeForKind(kind: string): Record<string, unknown> | undefined {
|
|
152
|
+
return this.contractForKind(kind, "outputType");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private contractForKind(
|
|
156
|
+
kind: string,
|
|
157
|
+
direction: ContractDirection,
|
|
158
|
+
): Record<string, unknown> | undefined {
|
|
164
159
|
const def = this.resolveDefinition(kind);
|
|
165
160
|
if (!def) return undefined;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
return resolveContract(direction, undefined, def, {
|
|
162
|
+
resolveDefinition: this.scopedDefResolver(),
|
|
163
|
+
typeManifestsFor: () => [],
|
|
164
|
+
})?.schema;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A resolver that re-scopes at every hop of an `extends` chain: each kind is
|
|
168
|
+
* resolved in the module that DECLARED the definition it was read off, since
|
|
169
|
+
* `extends` aliases are lexical. `resolveParent` always passes that definition
|
|
170
|
+
* as `from`, so one resolver serves a chain of any depth crossing any number
|
|
171
|
+
* of modules. */
|
|
172
|
+
private scopedDefResolver(): DefResolver {
|
|
173
|
+
return (kind, from) =>
|
|
174
|
+
this.resolveDefinitionIn(kind, (from?.metadata as { module?: string } | undefined)?.module);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
private capabilitiesForRefs(refs: string[]): string[] {
|
|
@@ -218,6 +217,17 @@ export class AnalysisRegistry {
|
|
|
218
217
|
return ctx.definitions?.resolve(kind) ?? (resolved ? ctx.definitions?.resolve(resolved) : undefined);
|
|
219
218
|
}
|
|
220
219
|
|
|
220
|
+
/** Resolve a kind in a named module's scope. The entry point for walking an
|
|
221
|
+
* `extends` chain from outside it (the kernel's contract binding): aliases are
|
|
222
|
+
* lexical, so each hop must resolve in the module that declared the definition
|
|
223
|
+
* the kind was read off. Falls back to the global table when the module is
|
|
224
|
+
* unknown or is a root. */
|
|
225
|
+
resolveDefinitionIn(kind: string, module?: string): ResourceDefinition | undefined {
|
|
226
|
+
const scope = (module ? this.aliasesByModule.get(module) : undefined) ?? this.aliases;
|
|
227
|
+
const canonical = scope.resolveKind(kind);
|
|
228
|
+
return this.defs.resolve(kind) ?? (canonical ? this.defs.resolve(canonical) : undefined);
|
|
229
|
+
}
|
|
230
|
+
|
|
221
231
|
/** A resolver scoped to `def`'s OWN module, for resolving that definition's
|
|
222
232
|
* `extends` target.
|
|
223
233
|
*
|
package/src/analyzer.ts
CHANGED
|
@@ -2,7 +2,12 @@ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import { canonicalTypeSchemaId, OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
3
3
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
4
4
|
import { defaultRegistry, isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
AliasResolver,
|
|
7
|
+
moduleScopedDefResolver,
|
|
8
|
+
type ModuleScopes,
|
|
9
|
+
scopeResolverForModule,
|
|
10
|
+
} from "./alias-resolver.js";
|
|
6
11
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
7
12
|
import {
|
|
8
13
|
buildCelEnvironment,
|
|
@@ -11,7 +16,12 @@ import {
|
|
|
11
16
|
type CelHandlers,
|
|
12
17
|
} from "./cel-environment.js";
|
|
13
18
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
14
|
-
import { effectiveAuthorSchema } from "./extends-resolution.js";
|
|
19
|
+
import { type ContractDirection, effectiveAuthorSchema } from "./extends-resolution.js";
|
|
20
|
+
import {
|
|
21
|
+
type ContractScope,
|
|
22
|
+
PERMISSIVE_CONTRACT,
|
|
23
|
+
resolveContract,
|
|
24
|
+
} from "./invocation-contract.js";
|
|
15
25
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
16
26
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
17
27
|
import {
|
|
@@ -53,6 +63,8 @@ import { validateExtends } from "./validate-extends.js";
|
|
|
53
63
|
import { validateLogging } from "./validate-logging.js";
|
|
54
64
|
import { validateModuleArtifact } from "./validate-module-artifact.js";
|
|
55
65
|
import { validateBaseMapping } from "./validate-base-mapping.js";
|
|
66
|
+
import { validateInvocationContract } from "./validate-invocation-contract.js";
|
|
67
|
+
import { collectStepInputIssues } from "./validate-step-inputs.js";
|
|
56
68
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
57
69
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
58
70
|
import { validateReferences } from "./validate-references.js";
|
|
@@ -110,22 +122,24 @@ function resolveSelfOrAlias(
|
|
|
110
122
|
return scopeResolver.resolveKind(value);
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
/** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
|
|
126
|
+
* resolve in the module that declared the definition they were read off (so an
|
|
127
|
+
* `extends` chain crossing module boundaries re-scopes at every hop), and named
|
|
128
|
+
* `telo#Type` references resolve against the flattened manifest list. `resolveIn`
|
|
129
|
+
* is the top-level entry point, where the kind was written by the READING
|
|
130
|
+
* module and there is no declaring definition yet. */
|
|
131
|
+
export function analyzerContractScope(
|
|
120
132
|
defs: DefinitionRegistry,
|
|
121
133
|
aliases: AliasResolver,
|
|
134
|
+
scopes: ModuleScopes,
|
|
122
135
|
allManifests: Record<string, any>[],
|
|
123
|
-
):
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
): ContractScope & { resolveIn(kind: string, module?: string): ResourceDefinition | undefined } {
|
|
137
|
+
const resolve = moduleScopedDefResolver<ResourceDefinition>(defs, aliases, scopes);
|
|
138
|
+
return {
|
|
139
|
+
resolveDefinition: resolve,
|
|
140
|
+
resolveIn: resolve.in,
|
|
141
|
+
typeManifestsFor: () => allManifests,
|
|
142
|
+
};
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
const SOURCE = "telo-analyzer";
|
|
@@ -170,42 +184,37 @@ function buildSelfSchema(
|
|
|
170
184
|
}
|
|
171
185
|
|
|
172
186
|
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
173
|
-
* invocable template body
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* 3. Undefined — caller signals opaque `map<string, dyn>` upstream. */
|
|
187
|
+
* invocable template body — the shared contract resolver applied to the
|
|
188
|
+
* definition itself, so a body is typed against the exact signature callers are
|
|
189
|
+
* checked against and dispatch enforces. Walks the whole `extends` chain rather
|
|
190
|
+
* than one hop, so a definition two levels below the declaration still gets
|
|
191
|
+
* typed inputs. Undefined when nothing in the chain declares a contract —
|
|
192
|
+
* the caller signals opaque `map<string, dyn>` upstream. */
|
|
180
193
|
function lookupTemplateInputsSchema(
|
|
181
194
|
definition: Record<string, any>,
|
|
182
195
|
defs: DefinitionRegistry,
|
|
183
196
|
aliases: AliasResolver,
|
|
184
197
|
allManifests: Record<string, any>[],
|
|
198
|
+
scopes: ModuleScopes,
|
|
185
199
|
): Record<string, any> | undefined {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (abstractDef) {
|
|
193
|
-
const inherited = resolveTypeFieldToSchema(
|
|
194
|
-
(abstractDef as unknown as Record<string, unknown>).inputType,
|
|
195
|
-
allManifests,
|
|
196
|
-
);
|
|
197
|
-
if (inherited) return inherited;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return undefined;
|
|
200
|
+
return resolveContract(
|
|
201
|
+
"inputType",
|
|
202
|
+
undefined,
|
|
203
|
+
definition as unknown as ResourceDefinition,
|
|
204
|
+
analyzerContractScope(defs, aliases, scopes, allManifests),
|
|
205
|
+
)?.schema;
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
204
209
|
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
205
210
|
* have been pre-augmented:
|
|
206
211
|
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
207
|
-
* - `inputType` → resolved
|
|
208
|
-
*
|
|
212
|
+
* - `inputType` → resolved through the shared contract resolver, so
|
|
213
|
+
* `x-telo-context-from-root: inputType` substitutes the
|
|
214
|
+
* real signature. Without it the annotation would replace
|
|
215
|
+
* the node verbatim with the inline `{kind, schema}` wrapper
|
|
216
|
+
* the standard library writes everywhere, typing `inputs` as
|
|
217
|
+
* `{kind, schema}` instead of the declared properties.
|
|
209
218
|
*
|
|
210
219
|
* For non-definition manifests the original object is returned. */
|
|
211
220
|
function manifestRootForResolver(
|
|
@@ -213,9 +222,10 @@ function manifestRootForResolver(
|
|
|
213
222
|
defs: DefinitionRegistry,
|
|
214
223
|
aliases: AliasResolver,
|
|
215
224
|
allManifests: Record<string, any>[],
|
|
225
|
+
scopes: ModuleScopes,
|
|
216
226
|
): Record<string, any> {
|
|
217
227
|
if (m.kind !== "Telo.Definition") return m;
|
|
218
|
-
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests);
|
|
228
|
+
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
|
|
219
229
|
return {
|
|
220
230
|
...m,
|
|
221
231
|
schema: buildSelfSchema(m, defs, aliases),
|
|
@@ -223,9 +233,42 @@ function manifestRootForResolver(
|
|
|
223
233
|
};
|
|
224
234
|
}
|
|
225
235
|
|
|
236
|
+
/** True when an issue reports a property that is absent — its path points at a
|
|
237
|
+
* node the manifest does not contain. */
|
|
238
|
+
export const missingRequired = (issue: { message: string }): boolean =>
|
|
239
|
+
/is missing required property/.test(issue.message);
|
|
240
|
+
|
|
241
|
+
/** The path minus its last segment: the node that should have contained the
|
|
242
|
+
* missing property. Empty for a top-level miss, which anchors on the map. */
|
|
243
|
+
export function containerOf(path: string): string {
|
|
244
|
+
const dot = path.lastIndexOf(".");
|
|
245
|
+
return dot === -1 ? "" : path.slice(0, dot);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** How to name the owner of a resolved contract in a diagnostic. When the
|
|
249
|
+
* contract came from the definition's direct parent, echo the author's own
|
|
250
|
+
* spelling (`extends: Mcp.SessionProvider`) — that is the text they can find in
|
|
251
|
+
* their file. A contract inherited from further up the chain isn't written
|
|
252
|
+
* anywhere in this file, so the canonical `module.Kind` is what locates it. */
|
|
253
|
+
function contractOwnerLabel(
|
|
254
|
+
definition: Record<string, any>,
|
|
255
|
+
contract: { declaredBy?: ResourceDefinition },
|
|
256
|
+
): string {
|
|
257
|
+
const declaredBy = contract.declaredBy;
|
|
258
|
+
if (!declaredBy || declaredBy === (definition as unknown as ResourceDefinition)) {
|
|
259
|
+
return String(definition.metadata?.name ?? definition.kind);
|
|
260
|
+
}
|
|
261
|
+
const parentSpelling = definition.extends as string | undefined;
|
|
262
|
+
const canonical = `${declaredBy.metadata.module}.${declaredBy.metadata.name}`;
|
|
263
|
+
if (typeof parentSpelling === "string" && parentSpelling.endsWith(`.${declaredBy.metadata.name}`)) {
|
|
264
|
+
return parentSpelling;
|
|
265
|
+
}
|
|
266
|
+
return canonical;
|
|
267
|
+
}
|
|
268
|
+
|
|
226
269
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
227
270
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
228
|
-
function resolveLocalRef(
|
|
271
|
+
export function resolveLocalRef(
|
|
229
272
|
schema: Record<string, any> | undefined,
|
|
230
273
|
root: Record<string, any>,
|
|
231
274
|
): Record<string, any> | undefined {
|
|
@@ -241,7 +284,7 @@ function resolveLocalRef(
|
|
|
241
284
|
|
|
242
285
|
/** Gather property schemas from a (possibly variant-bearing) object schema:
|
|
243
286
|
* top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch. */
|
|
244
|
-
function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Record<string, any>]> {
|
|
287
|
+
export function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Record<string, any>]> {
|
|
245
288
|
const out: Array<[string, Record<string, any>]> = [];
|
|
246
289
|
if (schema.properties && typeof schema.properties === "object") {
|
|
247
290
|
for (const [k, v] of Object.entries(schema.properties as Record<string, any>)) {
|
|
@@ -272,7 +315,7 @@ function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Reco
|
|
|
272
315
|
* role or nesting form updates both consumers at once. No resource kind is
|
|
273
316
|
* hardcoded; recursion is driven entirely by the schema annotations.
|
|
274
317
|
*/
|
|
275
|
-
function walkStepArray(
|
|
318
|
+
export function walkStepArray(
|
|
276
319
|
steps: unknown[],
|
|
277
320
|
stepItemSchema: Record<string, any> | undefined,
|
|
278
321
|
rootSchema: Record<string, any>,
|
|
@@ -331,20 +374,18 @@ function walkStepArray(
|
|
|
331
374
|
|
|
332
375
|
/**
|
|
333
376
|
* Build a `steps` context schema from `x-telo-step-context` annotation.
|
|
334
|
-
* Walks each step in the manifest array, resolves the invoked resource's
|
|
335
|
-
* and builds `steps.<name>.result` context entries.
|
|
377
|
+
* Walks each step in the manifest array, resolves the invoked resource's output
|
|
378
|
+
* contract, and builds `steps.<name>.result` context entries.
|
|
336
379
|
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* 3. Permissive `{type: object, additionalProperties: true}` if neither
|
|
343
|
-
* yields a schema.
|
|
380
|
+
* Resolution is the shared {@link resolveContract} — the invoked resource
|
|
381
|
+
* manifest's own declaration, then the kind's, resolved to the nearest
|
|
382
|
+
* declaration along `extends`, then permissive. Sharing it with the kernel is
|
|
383
|
+
* what stops `telo check` from typing `steps.X.result` against one contract
|
|
384
|
+
* while dispatch validates against another.
|
|
344
385
|
*
|
|
345
|
-
*
|
|
346
|
-
* govern step-result chain validation — without it, the validator falls
|
|
347
|
-
* to permissive and the stream-opacity rule never fires.
|
|
386
|
+
* The kind layer is what makes `x-telo-stream` properties on definitions
|
|
387
|
+
* actually govern step-result chain validation — without it, the validator falls
|
|
388
|
+
* back to permissive and the stream-opacity rule never fires.
|
|
348
389
|
*
|
|
349
390
|
* Recursion into nested step arrays is annotation-driven via
|
|
350
391
|
* `x-telo-topology-role`. The analyzer recognises three role values:
|
|
@@ -361,10 +402,14 @@ function buildStepContextSchema(
|
|
|
361
402
|
allManifests: Record<string, any>[],
|
|
362
403
|
defs: DefinitionRegistry,
|
|
363
404
|
aliases: AliasResolver,
|
|
405
|
+
scopes: ModuleScopes,
|
|
364
406
|
): Record<string, any> | undefined {
|
|
365
407
|
const props = defSchema.properties as Record<string, any> | undefined;
|
|
366
408
|
if (!props) return undefined;
|
|
367
409
|
|
|
410
|
+
const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
|
|
411
|
+
const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
412
|
+
|
|
368
413
|
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
369
414
|
const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
|
|
370
415
|
if (!stepCtx) continue;
|
|
@@ -391,36 +436,30 @@ function buildStepContextSchema(
|
|
|
391
436
|
// not shadow real entries with a permissive `additionalProperties: true`,
|
|
392
437
|
// or unknown step references slip through chain validation.
|
|
393
438
|
if (typeof name !== "string" || !invoke || typeof invoke !== "object") return;
|
|
394
|
-
let outputSchema: Record<string, any> | undefined;
|
|
395
439
|
const invokedKind = invoke.kind as string | undefined;
|
|
396
440
|
const invokedName = invoke.name as string | undefined;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
defs,
|
|
416
|
-
aliases,
|
|
417
|
-
allManifests,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
441
|
+
// A named `!ref` carries the target's own manifest (which may narrow the
|
|
442
|
+
// contract for this one instance); an inline `{ kind, ... }` step IS the
|
|
443
|
+
// manifest. Either way the kind layer resolves through `extends`.
|
|
444
|
+
const invokedManifest = invokedName
|
|
445
|
+
? (allManifests.find(
|
|
446
|
+
(m) =>
|
|
447
|
+
(m.metadata as any)?.name === invokedName && (!invokedKind || m.kind === invokedKind),
|
|
448
|
+
) as Record<string, any> | undefined)
|
|
449
|
+
: (invoke as Record<string, any>);
|
|
450
|
+
const invokedDef = invokedKind
|
|
451
|
+
? contractScope.resolveIn(invokedKind, readingModule)
|
|
452
|
+
: undefined;
|
|
453
|
+
const outputSchema = resolveContract(
|
|
454
|
+
outputTypeField as ContractDirection,
|
|
455
|
+
invokedManifest,
|
|
456
|
+
invokedDef,
|
|
457
|
+
contractScope,
|
|
458
|
+
)?.schema;
|
|
420
459
|
stepProperties[name] = {
|
|
421
460
|
type: "object",
|
|
422
461
|
properties: {
|
|
423
|
-
result: outputSchema ??
|
|
462
|
+
result: outputSchema ?? PERMISSIVE_CONTRACT,
|
|
424
463
|
},
|
|
425
464
|
};
|
|
426
465
|
});
|
|
@@ -1549,6 +1588,13 @@ export class StaticAnalyzer {
|
|
|
1549
1588
|
// the abstract this definition `extends`. CEL fields inside the templated
|
|
1550
1589
|
// values are replaced with type-appropriate placeholders before AJV runs —
|
|
1551
1590
|
// same pattern as the per-resource schema validation above.
|
|
1591
|
+
const contractScope = analyzerContractScope(
|
|
1592
|
+
defs,
|
|
1593
|
+
aliases,
|
|
1594
|
+
{ aliasesByModule, rootModules },
|
|
1595
|
+
allManifests as Record<string, any>[],
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1552
1598
|
for (const m of allManifests) {
|
|
1553
1599
|
if (m.kind !== "Telo.Definition") continue;
|
|
1554
1600
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
@@ -1598,20 +1644,20 @@ export class StaticAnalyzer {
|
|
|
1598
1644
|
// values passed to the dispatch target's invoke(). Validate against the
|
|
1599
1645
|
// target's declared `inputType` when both sides have one.
|
|
1600
1646
|
if (dispatchKind && md.inputs && typeof md.inputs === "object") {
|
|
1601
|
-
const targetSchema =
|
|
1602
|
-
dispatchKind,
|
|
1647
|
+
const targetSchema = resolveContract(
|
|
1603
1648
|
"inputType",
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
);
|
|
1649
|
+
undefined,
|
|
1650
|
+
contractScope.resolveIn(dispatchKind, (md.metadata as any)?.module),
|
|
1651
|
+
contractScope,
|
|
1652
|
+
)?.schema;
|
|
1608
1653
|
if (targetSchema) {
|
|
1609
1654
|
emitTargetMismatch(dispatchKind, targetSchema, md.inputs, "inputs");
|
|
1610
1655
|
}
|
|
1611
1656
|
}
|
|
1612
1657
|
|
|
1613
|
-
// Top-level `result:` is a post-call mapping that must satisfy
|
|
1614
|
-
//
|
|
1658
|
+
// Top-level `result:` is a post-call mapping that must satisfy THIS
|
|
1659
|
+
// definition's output contract — its own `outputType` when it declares
|
|
1660
|
+
// one, otherwise the nearest ancestor's. It's a sibling of whichever
|
|
1615
1661
|
// dispatch entry-point declared a kind-typed target (`provide:` or
|
|
1616
1662
|
// `invoke:`). The target's outputType lives on the dispatcher's `kind`
|
|
1617
1663
|
// and is what `result` is typed against *inside* CEL — separate role.
|
|
@@ -1619,18 +1665,14 @@ export class StaticAnalyzer {
|
|
|
1619
1665
|
(provide && typeof provide === "object" && !Array.isArray(provide)) ||
|
|
1620
1666
|
(invoke && typeof invoke === "object" && !Array.isArray(invoke));
|
|
1621
1667
|
if (hasDispatchObject && md.result && typeof md.result === "object") {
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
);
|
|
1631
|
-
if (abstractSchema) {
|
|
1632
|
-
emitTargetMismatch(extendsValue, abstractSchema, md.result, "result");
|
|
1633
|
-
}
|
|
1668
|
+
const contract = resolveContract(
|
|
1669
|
+
"outputType",
|
|
1670
|
+
undefined,
|
|
1671
|
+
md as unknown as ResourceDefinition,
|
|
1672
|
+
contractScope,
|
|
1673
|
+
);
|
|
1674
|
+
if (contract) {
|
|
1675
|
+
emitTargetMismatch(contractOwnerLabel(md, contract), contract.schema, md.result, "result");
|
|
1634
1676
|
}
|
|
1635
1677
|
}
|
|
1636
1678
|
}
|
|
@@ -1675,8 +1717,33 @@ export class StaticAnalyzer {
|
|
|
1675
1717
|
allManifests as Record<string, any>[],
|
|
1676
1718
|
defs,
|
|
1677
1719
|
aliases,
|
|
1720
|
+
{ aliasesByModule, rootModules },
|
|
1678
1721
|
)
|
|
1679
1722
|
: undefined;
|
|
1723
|
+
if (e.definition?.schema) {
|
|
1724
|
+
const stepName = (m.metadata as any)?.name as string | undefined;
|
|
1725
|
+
const stepFile = (m.metadata as { source?: string } | undefined)?.source;
|
|
1726
|
+
for (const issue of collectStepInputIssues(
|
|
1727
|
+
m as Record<string, any>,
|
|
1728
|
+
e.definition.schema as Record<string, any>,
|
|
1729
|
+
allManifests as Record<string, any>[],
|
|
1730
|
+
defs,
|
|
1731
|
+
aliases,
|
|
1732
|
+
{ aliasesByModule, rootModules },
|
|
1733
|
+
)) {
|
|
1734
|
+
diagnostics.push({
|
|
1735
|
+
severity: DiagnosticSeverity.Error,
|
|
1736
|
+
code: "CONTRACT_INPUTS_MISMATCH",
|
|
1737
|
+
source: SOURCE,
|
|
1738
|
+
message: `${m.kind}/${stepName}: inputs at '${issue.path}' do not satisfy ${issue.targetLabel}'s declared inputType: ${issue.message}`,
|
|
1739
|
+
data: {
|
|
1740
|
+
resource: { kind: m.kind, name: stepName ?? "" },
|
|
1741
|
+
filePath: stepFile,
|
|
1742
|
+
path: issue.path,
|
|
1743
|
+
},
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1680
1747
|
celErrorScopes = collectErrorContextScopes(
|
|
1681
1748
|
e.definition?.schema as Record<string, any> | undefined,
|
|
1682
1749
|
);
|
|
@@ -1814,6 +1881,7 @@ export class StaticAnalyzer {
|
|
|
1814
1881
|
defs,
|
|
1815
1882
|
aliases,
|
|
1816
1883
|
allManifests as Record<string, any>[],
|
|
1884
|
+
{ aliasesByModule, rootModules },
|
|
1817
1885
|
);
|
|
1818
1886
|
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
1819
1887
|
manifestRoot: rootForResolver,
|
|
@@ -1918,6 +1986,9 @@ export class StaticAnalyzer {
|
|
|
1918
1986
|
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
1919
1987
|
|
|
1920
1988
|
diagnostics.push(...validateBaseMapping(allManifests, defs, aliases));
|
|
1989
|
+
diagnostics.push(
|
|
1990
|
+
...validateInvocationContract(allManifests, defs, aliases, aliasesByModule),
|
|
1991
|
+
);
|
|
1921
1992
|
|
|
1922
1993
|
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
1923
1994
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
package/src/builtins.ts
CHANGED
|
@@ -218,6 +218,66 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
218
218
|
additionalProperties: false,
|
|
219
219
|
},
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
// Telo.JsonSchema — the concrete data-shape kind, in the kernel rather than
|
|
223
|
+
// in an installable module for the same reason the mandatory sinks are:
|
|
224
|
+
// declaring a shape is not optional. Every kind with an invocation contract
|
|
225
|
+
// needs one, so requiring an import to write `inputType:` would put a tax on
|
|
226
|
+
// the one thing the contract wants authors to do more of — and a library
|
|
227
|
+
// declaring a contract would have to import a module purely to describe
|
|
228
|
+
// itself. `type.JsonSchema` remains as a deprecated alias of this kind.
|
|
229
|
+
kind: "Telo.Definition",
|
|
230
|
+
metadata: { name: "JsonSchema", module: "Telo" },
|
|
231
|
+
capability: "Telo.Type",
|
|
232
|
+
// Declared so the kind reads as controller-BEARING, which is what lets
|
|
233
|
+
// another definition inherit it by delegation (`extends: Telo.JsonSchema`
|
|
234
|
+
// with no controller of its own). The entry is never loaded from — the
|
|
235
|
+
// kernel registers this controller directly at boot, before any lazy
|
|
236
|
+
// resolution — it states truthfully who provides it.
|
|
237
|
+
controllers: [{ runtime: "kernel", entry: "Telo.JsonSchema" }],
|
|
238
|
+
schema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
schema: {
|
|
242
|
+
title: "Schema",
|
|
243
|
+
description: "JSON Schema definition for the declared data type.",
|
|
244
|
+
type: "object",
|
|
245
|
+
},
|
|
246
|
+
extends: {
|
|
247
|
+
title: "Extends",
|
|
248
|
+
description: "Parent type name or list of parent type names to inherit from.",
|
|
249
|
+
oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
250
|
+
},
|
|
251
|
+
rules: {
|
|
252
|
+
title: "Rules",
|
|
253
|
+
description:
|
|
254
|
+
"CEL-based business invariant rules. Each rule's condition must return true for valid data.",
|
|
255
|
+
type: "array",
|
|
256
|
+
items: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
condition: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description:
|
|
262
|
+
"CEL expression evaluated with 'this' bound to the data. Must return true for valid data.",
|
|
263
|
+
},
|
|
264
|
+
code: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Machine-readable error code surfaced on validation failure.",
|
|
267
|
+
},
|
|
268
|
+
message: {
|
|
269
|
+
type: "string",
|
|
270
|
+
description: "Optional human-readable hint for the validation failure.",
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
required: ["condition", "code"],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
required: ["schema"],
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
},
|
|
280
|
+
},
|
|
221
281
|
{
|
|
222
282
|
kind: "Telo.Definition",
|
|
223
283
|
metadata: { name: "Abstract", module: "Telo" },
|
|
@@ -491,6 +551,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
491
551
|
default: "shared",
|
|
492
552
|
},
|
|
493
553
|
targets: {
|
|
554
|
+
// Boot targets form a step list: a later target reads an earlier one's
|
|
555
|
+
// result as `steps.<name>.result`, exactly as a sequence step does, so
|
|
556
|
+
// the same annotation types that context and drives the call-site
|
|
557
|
+
// contract check.
|
|
558
|
+
"x-telo-step-context": { invoke: "invoke", outputType: "outputType" },
|
|
494
559
|
type: "array",
|
|
495
560
|
items: {
|
|
496
561
|
anyOf: [
|
|
@@ -564,7 +629,15 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
564
629
|
{ "x-telo-ref": "Telo.Runnable" },
|
|
565
630
|
],
|
|
566
631
|
},
|
|
567
|
-
inputs: {
|
|
632
|
+
inputs: {
|
|
633
|
+
// Same annotation Run.Sequence steps carry: it is what makes
|
|
634
|
+
// a boot target's inputs visible to the call-site contract
|
|
635
|
+
// check and to the wiring rule. Without it the kernel would
|
|
636
|
+
// validate these at dispatch and nothing before it.
|
|
637
|
+
"x-telo-topology-role": "inputs",
|
|
638
|
+
type: "object",
|
|
639
|
+
additionalProperties: true,
|
|
640
|
+
},
|
|
568
641
|
when: { type: "string" },
|
|
569
642
|
},
|
|
570
643
|
additionalProperties: false,
|