@telorun/analyzer 0.47.0 → 0.49.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/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 +121 -83
- package/dist/artifact-layer-index.d.ts +55 -0
- package/dist/artifact-layer-index.d.ts.map +1 -0
- package/dist/artifact-layer-index.js +116 -0
- package/dist/artifact-selector.d.ts +81 -0
- package/dist/artifact-selector.d.ts.map +1 -0
- package/dist/artifact-selector.js +122 -0
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +130 -20
- 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 +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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/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-module-artifact.d.ts +27 -0
- package/dist/validate-module-artifact.d.ts.map +1 -0
- package/dist/validate-module-artifact.js +131 -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 +2 -2
- package/src/analysis-registry.ts +44 -34
- package/src/analyzer.ts +177 -100
- package/src/artifact-layer-index.ts +162 -0
- package/src/artifact-selector.ts +171 -0
- package/src/builtins.ts +135 -20
- package/src/extends-resolution.ts +86 -0
- package/src/index.ts +38 -1
- package/src/invocation-contract.ts +275 -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-module-artifact.ts +141 -0
- package/src/validate-step-inputs.ts +117 -0
- package/src/validate-throws-coverage.ts +12 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
|
-
import type
|
|
2
|
+
import { type ResourceManifest } from "@telorun/sdk";
|
|
3
3
|
import { type AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { type AnalysisDiagnostic } from "./types.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"validate-throws-coverage.d.ts","sourceRoot":"","sources":["../src/validate-throws-coverage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAOnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA0ezE,oDAAoD;AACpD,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,GAAG,EAAE,WAAW,EAChB,eAAe,GAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAa,EACvD,WAAW,GAAE,GAAG,CAAC,MAAM,CAAa,GACnC,kBAAkB,EAAE,CAkDtB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
2
|
+
import { AMBIENT_CONTRACT_ERROR_CODES, isAmbientContractErrorCode, } from "@telorun/sdk";
|
|
2
3
|
import { scopeResolverForModule } from "./alias-resolver.js";
|
|
3
4
|
import { createResolveCtx, resolveThrowsUnion, } from "./resolve-throws-union.js";
|
|
4
5
|
import { DiagnosticSeverity } from "./types.js";
|
|
@@ -186,12 +187,19 @@ function checkCatchesCoverage(entries, union, resource, filePath, arrayPath, env
|
|
|
186
187
|
const { proven, codes } = extractCoveredCodes(e.when, env);
|
|
187
188
|
if (proven) {
|
|
188
189
|
for (const c of codes) {
|
|
190
|
+
// An ambient kernel code (contract violations) is raised by the kernel,
|
|
191
|
+
// not declared by the kind, so naming it is legal and still typo-checked
|
|
192
|
+
// — but it is NOT part of the declared union, so it never counts toward
|
|
193
|
+
// coverage. Folding these into every union would make every bounded
|
|
194
|
+
// catches: block in the standard library incomplete overnight.
|
|
195
|
+
if (isAmbientContractErrorCode(c))
|
|
196
|
+
continue;
|
|
189
197
|
if (!declaredCodes.has(c)) {
|
|
190
198
|
diagnostics.push({
|
|
191
199
|
severity: DiagnosticSeverity.Error,
|
|
192
200
|
code: "UNDECLARED_THROW_CODE",
|
|
193
201
|
source: SOURCE,
|
|
194
|
-
message: `catches[${i}] references code '${c}' which is not in the handler's declared throw union {${[...declaredCodes].sort().join(", ") || "∅"}}${union.unbounded ? "
|
|
202
|
+
message: `catches[${i}] references code '${c}' which is not in the handler's declared throw union {${[...declaredCodes].sort().join(", ") || "∅"}} (ambient kernel codes ${AMBIENT_CONTRACT_ERROR_CODES.join(", ")} may also be named)${union.unbounded ? "; the union is unbounded, so a catch-all is required" : ""}.`,
|
|
195
203
|
data: { resource, filePath, path: `${arrayPath}[${i}].when` },
|
|
196
204
|
});
|
|
197
205
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.61.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
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 {
|
|
@@ -51,7 +61,10 @@ import {
|
|
|
51
61
|
import { buildEvalPaths, evalPathsCover } from "./eval-paths.js";
|
|
52
62
|
import { validateExtends } from "./validate-extends.js";
|
|
53
63
|
import { validateLogging } from "./validate-logging.js";
|
|
64
|
+
import { validateModuleArtifact } from "./validate-module-artifact.js";
|
|
54
65
|
import { validateBaseMapping } from "./validate-base-mapping.js";
|
|
66
|
+
import { validateInvocationContract } from "./validate-invocation-contract.js";
|
|
67
|
+
import { collectStepInputIssues } from "./validate-step-inputs.js";
|
|
55
68
|
import { validateNestedInlineResources } from "./validate-nested-inline.js";
|
|
56
69
|
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
57
70
|
import { validateReferences } from "./validate-references.js";
|
|
@@ -109,22 +122,24 @@ function resolveSelfOrAlias(
|
|
|
109
122
|
return scopeResolver.resolveKind(value);
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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(
|
|
119
132
|
defs: DefinitionRegistry,
|
|
120
133
|
aliases: AliasResolver,
|
|
134
|
+
scopes: ModuleScopes,
|
|
121
135
|
allManifests: Record<string, any>[],
|
|
122
|
-
):
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
};
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
const SOURCE = "telo-analyzer";
|
|
@@ -169,42 +184,37 @@ function buildSelfSchema(
|
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
172
|
-
* invocable template body
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
* 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. */
|
|
179
193
|
function lookupTemplateInputsSchema(
|
|
180
194
|
definition: Record<string, any>,
|
|
181
195
|
defs: DefinitionRegistry,
|
|
182
196
|
aliases: AliasResolver,
|
|
183
197
|
allManifests: Record<string, any>[],
|
|
198
|
+
scopes: ModuleScopes,
|
|
184
199
|
): Record<string, any> | undefined {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (abstractDef) {
|
|
192
|
-
const inherited = resolveTypeFieldToSchema(
|
|
193
|
-
(abstractDef as unknown as Record<string, unknown>).inputType,
|
|
194
|
-
allManifests,
|
|
195
|
-
);
|
|
196
|
-
if (inherited) return inherited;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return undefined;
|
|
200
|
+
return resolveContract(
|
|
201
|
+
"inputType",
|
|
202
|
+
undefined,
|
|
203
|
+
definition as unknown as ResourceDefinition,
|
|
204
|
+
analyzerContractScope(defs, aliases, scopes, allManifests),
|
|
205
|
+
)?.schema;
|
|
200
206
|
}
|
|
201
207
|
|
|
202
208
|
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
203
209
|
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
204
210
|
* have been pre-augmented:
|
|
205
211
|
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
206
|
-
* - `inputType` → resolved
|
|
207
|
-
*
|
|
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.
|
|
208
218
|
*
|
|
209
219
|
* For non-definition manifests the original object is returned. */
|
|
210
220
|
function manifestRootForResolver(
|
|
@@ -212,9 +222,10 @@ function manifestRootForResolver(
|
|
|
212
222
|
defs: DefinitionRegistry,
|
|
213
223
|
aliases: AliasResolver,
|
|
214
224
|
allManifests: Record<string, any>[],
|
|
225
|
+
scopes: ModuleScopes,
|
|
215
226
|
): Record<string, any> {
|
|
216
227
|
if (m.kind !== "Telo.Definition") return m;
|
|
217
|
-
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests);
|
|
228
|
+
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests, scopes);
|
|
218
229
|
return {
|
|
219
230
|
...m,
|
|
220
231
|
schema: buildSelfSchema(m, defs, aliases),
|
|
@@ -222,9 +233,42 @@ function manifestRootForResolver(
|
|
|
222
233
|
};
|
|
223
234
|
}
|
|
224
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
|
+
|
|
225
269
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
226
270
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
227
|
-
function resolveLocalRef(
|
|
271
|
+
export function resolveLocalRef(
|
|
228
272
|
schema: Record<string, any> | undefined,
|
|
229
273
|
root: Record<string, any>,
|
|
230
274
|
): Record<string, any> | undefined {
|
|
@@ -240,7 +284,7 @@ function resolveLocalRef(
|
|
|
240
284
|
|
|
241
285
|
/** Gather property schemas from a (possibly variant-bearing) object schema:
|
|
242
286
|
* top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch. */
|
|
243
|
-
function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Record<string, any>]> {
|
|
287
|
+
export function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Record<string, any>]> {
|
|
244
288
|
const out: Array<[string, Record<string, any>]> = [];
|
|
245
289
|
if (schema.properties && typeof schema.properties === "object") {
|
|
246
290
|
for (const [k, v] of Object.entries(schema.properties as Record<string, any>)) {
|
|
@@ -271,7 +315,7 @@ function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Reco
|
|
|
271
315
|
* role or nesting form updates both consumers at once. No resource kind is
|
|
272
316
|
* hardcoded; recursion is driven entirely by the schema annotations.
|
|
273
317
|
*/
|
|
274
|
-
function walkStepArray(
|
|
318
|
+
export function walkStepArray(
|
|
275
319
|
steps: unknown[],
|
|
276
320
|
stepItemSchema: Record<string, any> | undefined,
|
|
277
321
|
rootSchema: Record<string, any>,
|
|
@@ -330,20 +374,18 @@ function walkStepArray(
|
|
|
330
374
|
|
|
331
375
|
/**
|
|
332
376
|
* Build a `steps` context schema from `x-telo-step-context` annotation.
|
|
333
|
-
* Walks each step in the manifest array, resolves the invoked resource's
|
|
334
|
-
* 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.
|
|
335
379
|
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* 3. Permissive `{type: object, additionalProperties: true}` if neither
|
|
342
|
-
* 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.
|
|
343
385
|
*
|
|
344
|
-
*
|
|
345
|
-
* govern step-result chain validation — without it, the validator falls
|
|
346
|
-
* 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.
|
|
347
389
|
*
|
|
348
390
|
* Recursion into nested step arrays is annotation-driven via
|
|
349
391
|
* `x-telo-topology-role`. The analyzer recognises three role values:
|
|
@@ -360,10 +402,14 @@ function buildStepContextSchema(
|
|
|
360
402
|
allManifests: Record<string, any>[],
|
|
361
403
|
defs: DefinitionRegistry,
|
|
362
404
|
aliases: AliasResolver,
|
|
405
|
+
scopes: ModuleScopes,
|
|
363
406
|
): Record<string, any> | undefined {
|
|
364
407
|
const props = defSchema.properties as Record<string, any> | undefined;
|
|
365
408
|
if (!props) return undefined;
|
|
366
409
|
|
|
410
|
+
const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
|
|
411
|
+
const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
412
|
+
|
|
367
413
|
for (const [fieldName, fieldSchema] of Object.entries(props)) {
|
|
368
414
|
const stepCtx = fieldSchema["x-telo-step-context"] as Record<string, string> | undefined;
|
|
369
415
|
if (!stepCtx) continue;
|
|
@@ -390,36 +436,30 @@ function buildStepContextSchema(
|
|
|
390
436
|
// not shadow real entries with a permissive `additionalProperties: true`,
|
|
391
437
|
// or unknown step references slip through chain validation.
|
|
392
438
|
if (typeof name !== "string" || !invoke || typeof invoke !== "object") return;
|
|
393
|
-
let outputSchema: Record<string, any> | undefined;
|
|
394
439
|
const invokedKind = invoke.kind as string | undefined;
|
|
395
440
|
const invokedName = invoke.name as string | undefined;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
defs,
|
|
415
|
-
aliases,
|
|
416
|
-
allManifests,
|
|
417
|
-
);
|
|
418
|
-
}
|
|
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;
|
|
419
459
|
stepProperties[name] = {
|
|
420
460
|
type: "object",
|
|
421
461
|
properties: {
|
|
422
|
-
result: outputSchema ??
|
|
462
|
+
result: outputSchema ?? PERMISSIVE_CONTRACT,
|
|
423
463
|
},
|
|
424
464
|
};
|
|
425
465
|
});
|
|
@@ -1202,6 +1242,11 @@ export class StaticAnalyzer {
|
|
|
1202
1242
|
// §14.1 / §10.3: redaction paths and `on_full: block` are statically
|
|
1203
1243
|
// detectable, so they fail `telo check` rather than only at boot.
|
|
1204
1244
|
diagnostics.push(...validateLogging(allManifests, defs, aliases, aliasesByModule));
|
|
1245
|
+
// Module-artifact surface: bundled-controller selector qualifiers and the
|
|
1246
|
+
// published `layers:` index. Every case is decidable from the manifest and
|
|
1247
|
+
// would otherwise fail on a consumer's machine — or, for a mistyped platform
|
|
1248
|
+
// axis, silently offer one platform's binary to every host.
|
|
1249
|
+
diagnostics.push(...validateModuleArtifact(allManifests));
|
|
1205
1250
|
}
|
|
1206
1251
|
resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
|
|
1207
1252
|
|
|
@@ -1543,6 +1588,13 @@ export class StaticAnalyzer {
|
|
|
1543
1588
|
// the abstract this definition `extends`. CEL fields inside the templated
|
|
1544
1589
|
// values are replaced with type-appropriate placeholders before AJV runs —
|
|
1545
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
|
+
|
|
1546
1598
|
for (const m of allManifests) {
|
|
1547
1599
|
if (m.kind !== "Telo.Definition") continue;
|
|
1548
1600
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
@@ -1592,20 +1644,20 @@ export class StaticAnalyzer {
|
|
|
1592
1644
|
// values passed to the dispatch target's invoke(). Validate against the
|
|
1593
1645
|
// target's declared `inputType` when both sides have one.
|
|
1594
1646
|
if (dispatchKind && md.inputs && typeof md.inputs === "object") {
|
|
1595
|
-
const targetSchema =
|
|
1596
|
-
dispatchKind,
|
|
1647
|
+
const targetSchema = resolveContract(
|
|
1597
1648
|
"inputType",
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
);
|
|
1649
|
+
undefined,
|
|
1650
|
+
contractScope.resolveIn(dispatchKind, (md.metadata as any)?.module),
|
|
1651
|
+
contractScope,
|
|
1652
|
+
)?.schema;
|
|
1602
1653
|
if (targetSchema) {
|
|
1603
1654
|
emitTargetMismatch(dispatchKind, targetSchema, md.inputs, "inputs");
|
|
1604
1655
|
}
|
|
1605
1656
|
}
|
|
1606
1657
|
|
|
1607
|
-
// Top-level `result:` is a post-call mapping that must satisfy
|
|
1608
|
-
//
|
|
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
|
|
1609
1661
|
// dispatch entry-point declared a kind-typed target (`provide:` or
|
|
1610
1662
|
// `invoke:`). The target's outputType lives on the dispatcher's `kind`
|
|
1611
1663
|
// and is what `result` is typed against *inside* CEL — separate role.
|
|
@@ -1613,18 +1665,14 @@ export class StaticAnalyzer {
|
|
|
1613
1665
|
(provide && typeof provide === "object" && !Array.isArray(provide)) ||
|
|
1614
1666
|
(invoke && typeof invoke === "object" && !Array.isArray(invoke));
|
|
1615
1667
|
if (hasDispatchObject && md.result && typeof md.result === "object") {
|
|
1616
|
-
const
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
);
|
|
1625
|
-
if (abstractSchema) {
|
|
1626
|
-
emitTargetMismatch(extendsValue, abstractSchema, md.result, "result");
|
|
1627
|
-
}
|
|
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");
|
|
1628
1676
|
}
|
|
1629
1677
|
}
|
|
1630
1678
|
}
|
|
@@ -1669,8 +1717,33 @@ export class StaticAnalyzer {
|
|
|
1669
1717
|
allManifests as Record<string, any>[],
|
|
1670
1718
|
defs,
|
|
1671
1719
|
aliases,
|
|
1720
|
+
{ aliasesByModule, rootModules },
|
|
1672
1721
|
)
|
|
1673
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
|
+
}
|
|
1674
1747
|
celErrorScopes = collectErrorContextScopes(
|
|
1675
1748
|
e.definition?.schema as Record<string, any> | undefined,
|
|
1676
1749
|
);
|
|
@@ -1808,6 +1881,7 @@ export class StaticAnalyzer {
|
|
|
1808
1881
|
defs,
|
|
1809
1882
|
aliases,
|
|
1810
1883
|
allManifests as Record<string, any>[],
|
|
1884
|
+
{ aliasesByModule, rootModules },
|
|
1811
1885
|
);
|
|
1812
1886
|
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
1813
1887
|
manifestRoot: rootForResolver,
|
|
@@ -1912,6 +1986,9 @@ export class StaticAnalyzer {
|
|
|
1912
1986
|
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
1913
1987
|
|
|
1914
1988
|
diagnostics.push(...validateBaseMapping(allManifests, defs, aliases));
|
|
1989
|
+
diagnostics.push(
|
|
1990
|
+
...validateInvocationContract(allManifests, defs, aliases, aliasesByModule),
|
|
1991
|
+
);
|
|
1915
1992
|
|
|
1916
1993
|
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
1917
1994
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|