@telorun/analyzer 0.5.0 → 0.6.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +62 -2
- package/dist/cel-environment.d.ts +11 -2
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +13 -2
- package/dist/normalize-inline-resources.js +22 -0
- package/dist/reference-field-map.d.ts +14 -3
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +39 -4
- package/dist/validate-cel-context.d.ts +2 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +30 -3
- package/package.json +2 -2
- package/src/analyzer.ts +81 -0
- package/src/cel-environment.ts +13 -3
- package/src/normalize-inline-resources.ts +23 -0
- package/src/reference-field-map.ts +39 -4
- package/src/validate-cel-context.ts +32 -3
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAmT/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IA2TvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAKxF,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAiB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -13,6 +13,29 @@ import { validateExtends } from "./validate-extends.js";
|
|
|
13
13
|
import { validateReferences } from "./validate-references.js";
|
|
14
14
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
15
15
|
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
16
|
+
const SELF_PREFIX = "Self.";
|
|
17
|
+
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
18
|
+
* to its canonical form. `Self.<Name>` resolves to `<ownModule>.<Name>` —
|
|
19
|
+
* the magic alias for "this library's own module" — and other prefixes
|
|
20
|
+
* resolve through the declaring file's Telo.Import aliases. */
|
|
21
|
+
function resolveSelfOrAlias(value, ownModule, scopeResolver) {
|
|
22
|
+
if (value.startsWith(SELF_PREFIX) && ownModule) {
|
|
23
|
+
return `${ownModule}.${value.slice(SELF_PREFIX.length)}`;
|
|
24
|
+
}
|
|
25
|
+
return scopeResolver.resolveKind(value);
|
|
26
|
+
}
|
|
27
|
+
/** Look up a top-level field (`outputType`, `inputType`) on a kind's
|
|
28
|
+
* `Telo.Definition`. Used as a fallback by `buildStepContextSchema` when the
|
|
29
|
+
* invoked resource manifest doesn't carry the field inline — most kinds
|
|
30
|
+
* declare result shape on the definition, not the resource. */
|
|
31
|
+
function lookupDefinitionTypeField(invokedKind, fieldName, defs, aliases, allManifests) {
|
|
32
|
+
const canonical = aliases.resolveKind(invokedKind) ?? invokedKind;
|
|
33
|
+
const def = defs.resolve(canonical);
|
|
34
|
+
if (!def)
|
|
35
|
+
return undefined;
|
|
36
|
+
const value = def[fieldName];
|
|
37
|
+
return resolveTypeFieldToSchema(value, allManifests);
|
|
38
|
+
}
|
|
16
39
|
function walkCelExpressions(value, path, cb) {
|
|
17
40
|
if (typeof value === "string") {
|
|
18
41
|
for (const m of value.matchAll(TEMPLATE_REGEX)) {
|
|
@@ -62,8 +85,20 @@ function extractContextsFromSchema(schema, path = "$") {
|
|
|
62
85
|
* Build a `steps` context schema from `x-telo-step-context` annotation.
|
|
63
86
|
* Walks each step in the manifest array, resolves the invoked resource's outputType,
|
|
64
87
|
* and builds `steps.<name>.result` context entries.
|
|
88
|
+
*
|
|
89
|
+
* outputType resolution falls through three layers:
|
|
90
|
+
* 1. The invoked resource manifest's own `outputType` field (rare — most
|
|
91
|
+
* resources don't declare outputType inline).
|
|
92
|
+
* 2. The kind's `Telo.Definition` outputType (the common case for kinds that
|
|
93
|
+
* declare a stable result shape, e.g. `Ai.TextStream` ↦ `{output: stream}`).
|
|
94
|
+
* 3. Permissive `{type: object, additionalProperties: true}` if neither
|
|
95
|
+
* yields a schema.
|
|
96
|
+
*
|
|
97
|
+
* Layer 2 is what makes `x-telo-stream` properties on definitions actually
|
|
98
|
+
* govern step-result chain validation — without it, the validator falls back
|
|
99
|
+
* to permissive and the stream-opacity rule never fires.
|
|
65
100
|
*/
|
|
66
|
-
function buildStepContextSchema(manifest, defSchema, allManifests) {
|
|
101
|
+
function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases) {
|
|
67
102
|
const props = defSchema.properties;
|
|
68
103
|
if (!props)
|
|
69
104
|
return undefined;
|
|
@@ -101,6 +136,11 @@ function buildStepContextSchema(manifest, defSchema, allManifests) {
|
|
|
101
136
|
else {
|
|
102
137
|
outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
|
|
103
138
|
}
|
|
139
|
+
// Fallback: pull outputType from the kind's Telo.Definition. The
|
|
140
|
+
// resource manifest typically doesn't carry outputType; the def does.
|
|
141
|
+
if (!outputSchema && invokedKind) {
|
|
142
|
+
outputSchema = lookupDefinitionTypeField(invokedKind, outputTypeField, defs, aliases, allManifests);
|
|
143
|
+
}
|
|
104
144
|
}
|
|
105
145
|
stepProperties[name] = {
|
|
106
146
|
type: "object",
|
|
@@ -235,6 +275,26 @@ export class StaticAnalyzer {
|
|
|
235
275
|
const moduleName = m.metadata.name;
|
|
236
276
|
if (moduleName)
|
|
237
277
|
defs.registerModuleIdentity(namespace, moduleName);
|
|
278
|
+
// Auto-register `Self` as an alias for this library's own module name.
|
|
279
|
+
// Lets same-library `extends:` work (e.g. `extends: Self.Encoder` for a
|
|
280
|
+
// concrete kind whose abstract lives in the same Telo.Library) without
|
|
281
|
+
// requiring a self-import (which would loop the loader). Resolves
|
|
282
|
+
// through the same alias machinery as user-declared Telo.Imports —
|
|
283
|
+
// honours the library's `exports.kinds` list, no special cases.
|
|
284
|
+
if (moduleName) {
|
|
285
|
+
const exportedKinds = m.exports?.kinds ?? [];
|
|
286
|
+
if (rootModules.has(moduleName)) {
|
|
287
|
+
aliases.registerImport("Self", moduleName, exportedKinds);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
let libResolver = aliasesByModule.get(moduleName);
|
|
291
|
+
if (!libResolver) {
|
|
292
|
+
libResolver = new AliasResolver();
|
|
293
|
+
aliasesByModule.set(moduleName, libResolver);
|
|
294
|
+
}
|
|
295
|
+
libResolver.registerImport("Self", moduleName, exportedKinds);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
238
298
|
}
|
|
239
299
|
if (m.kind === "Telo.Import") {
|
|
240
300
|
const alias = m.metadata.name;
|
|
@@ -381,7 +441,7 @@ export class StaticAnalyzer {
|
|
|
381
441
|
const mDefinition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
382
442
|
// Pre-compute step context for manifests with x-telo-step-context
|
|
383
443
|
const stepContextSchema = mDefinition?.schema
|
|
384
|
-
? buildStepContextSchema(m, mDefinition.schema, allManifests)
|
|
444
|
+
? buildStepContextSchema(m, mDefinition.schema, allManifests, defs, aliases)
|
|
385
445
|
: undefined;
|
|
386
446
|
walkCelExpressions(m, "", (expr, path) => {
|
|
387
447
|
let parsed;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
-
import type
|
|
2
|
+
import { type ResourceManifest } from "@telorun/sdk";
|
|
3
3
|
export interface CelHandlers {
|
|
4
4
|
sha256: (s: string) => string;
|
|
5
5
|
}
|
|
6
6
|
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
7
7
|
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
8
8
|
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
9
|
-
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
9
|
+
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
10
|
+
*
|
|
11
|
+
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
12
|
+
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
13
|
+
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
14
|
+
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
15
|
+
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
16
|
+
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
17
|
+
* member access raises a CEL error at runtime — matching the analyzer's
|
|
18
|
+
* static check on `x-telo-stream`-marked properties. */
|
|
10
19
|
export declare function buildCelEnvironment(handlers?: CelHandlers): Environment;
|
|
11
20
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
12
21
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cel-environment.d.ts","sourceRoot":"","sources":["../src/cel-environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"cel-environment.d.ts","sourceRoot":"","sources":["../src/cel-environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAU,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG7D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/B;AAaD;;;;;;;;;;;;yDAYyD;AACzD,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,WAA2B,GAAG,WAAW,CAetF;AAED;;;;;;;;;uEASuE;AACvE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAC9C,WAAW,CAyCb"}
|
package/dist/cel-environment.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
+
import { Stream } from "@telorun/sdk";
|
|
2
3
|
import { jsonSchemaToCelType } from "./schema-compat.js";
|
|
3
4
|
const stub = (name) => () => {
|
|
4
5
|
throw new Error(`${name}() is not available in this environment. ` +
|
|
@@ -10,7 +11,16 @@ const STUB_HANDLERS = {
|
|
|
10
11
|
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
11
12
|
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
12
13
|
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
13
|
-
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
14
|
+
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
15
|
+
*
|
|
16
|
+
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
17
|
+
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
18
|
+
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
19
|
+
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
20
|
+
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
21
|
+
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
22
|
+
* member access raises a CEL error at runtime — matching the analyzer's
|
|
23
|
+
* static check on `x-telo-stream`-marked properties. */
|
|
14
24
|
export function buildCelEnvironment(handlers = STUB_HANDLERS) {
|
|
15
25
|
return new Environment({ unlistedVariablesAreDyn: true })
|
|
16
26
|
.registerFunction("join(list, string): string", (list, sep) => list.map(String).join(sep))
|
|
@@ -24,7 +34,8 @@ export function buildCelEnvironment(handlers = STUB_HANDLERS) {
|
|
|
24
34
|
return [...map.values()];
|
|
25
35
|
return Object.values(map);
|
|
26
36
|
})
|
|
27
|
-
.registerFunction("sha256(string): string", (s) => handlers.sha256(s))
|
|
37
|
+
.registerFunction("sha256(string): string", (s) => handlers.sha256(s))
|
|
38
|
+
.registerType("Stream", Stream);
|
|
28
39
|
}
|
|
29
40
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
30
41
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
|
@@ -78,6 +78,28 @@ function extractInlinesAtPath(resource, fieldPath, parentName, parentModule, inv
|
|
|
78
78
|
if (!obj || typeof obj !== "object" || partsLeft.length === 0)
|
|
79
79
|
return;
|
|
80
80
|
const [head, ...rest] = partsLeft;
|
|
81
|
+
// Map iteration: descend into every value of the current object (used for
|
|
82
|
+
// schema fields with `additionalProperties` like `content[mime]`).
|
|
83
|
+
if (head === "{}") {
|
|
84
|
+
const container = obj;
|
|
85
|
+
for (const mapKey of Object.keys(container)) {
|
|
86
|
+
const elem = container[mapKey];
|
|
87
|
+
if (!elem || typeof elem !== "object")
|
|
88
|
+
continue;
|
|
89
|
+
const sanitizedKey = sanitizeName(mapKey);
|
|
90
|
+
if (rest.length === 0) {
|
|
91
|
+
if (isInlineResource(elem)) {
|
|
92
|
+
const name = sanitizeName([parentName, ...nameParts, sanitizedKey].join("_"));
|
|
93
|
+
extracted.push(buildManifest(elem, name, parentModule, invocationContext));
|
|
94
|
+
container[mapKey] = { kind: elem.kind, name };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
traverse(elem, rest, [...nameParts, sanitizedKey]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
81
103
|
const isArr = head.endsWith("[]");
|
|
82
104
|
const key = isArr ? head.slice(0, -2) : head;
|
|
83
105
|
const container = obj;
|
|
@@ -33,11 +33,22 @@ export declare function isSchemaFromEntry(entry: FieldMapEntry): entry is Schema
|
|
|
33
33
|
/** Keys that a named reference object may have. Values beyond these indicate an inline resource. */
|
|
34
34
|
export declare const REFERENCE_KEYS: Set<string>;
|
|
35
35
|
/** True when `val` is an inline resource definition rather than a named reference.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Three shapes flow through here:
|
|
37
|
+
* - `{kind, name}` (optionally with runtime call args) → named reference, NOT inline.
|
|
38
|
+
* - `{kind, ...config}` with no name → inline definition with config; extract.
|
|
39
|
+
* - `{kind}` alone (bare kind, no name) → inline singleton — extract a fresh
|
|
40
|
+
* stateless resource. Lets simple stateless kinds be used inline without
|
|
41
|
+
* boilerplate (e.g. `encoder: {kind: Ndjson.Encoder}`, `invoke: {kind: Run.Throw}`).
|
|
42
|
+
*
|
|
43
|
+
* A named reference (has string `name`) may carry extra keys (e.g. `inputs`)
|
|
44
|
+
* that are runtime call parameters — those are never inline resources. */
|
|
38
45
|
export declare function isInlineResource(val: Record<string, unknown>): boolean;
|
|
39
46
|
/** Resolves all values at a field map path in a resource config.
|
|
40
|
-
*
|
|
47
|
+
* Path-segment markers:
|
|
48
|
+
* - `[]` iterate array values at this key
|
|
49
|
+
* - `{}` iterate map values (every value in an `additionalProperties`-typed
|
|
50
|
+
* object — used for fields like `content[mime]` whose schema declares
|
|
51
|
+
* a key-as-MIME map). The path is `<key>.{}.<rest>`. */
|
|
41
52
|
export declare function resolveFieldValues(obj: unknown, path: string): unknown[];
|
|
42
53
|
/**
|
|
43
54
|
* Traverses a definition's JSON Schema once and returns a field map recording every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;sDACkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE
|
|
1
|
+
{"version":3,"file":"reference-field-map.d.ts","sourceRoot":"","sources":["../src/reference-field-map.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B;sDACkD;IAClD,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,0FAA0F;IAC1F,OAAO,EAAE,OAAO,CAAC;IACjB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC/B;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAe;IAC9B;2CACuC;IACvC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC1B;AAED;8CAC8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC;;qFAEiF;IACjF,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEnF;0FAC0F;AAC1F,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,wBAAgB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,aAAa,CAEvE;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,eAAe,CAE3E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,aAAa,GAAG,KAAK,IAAI,oBAAoB,CAErF;AAED,oGAAoG;AACpG,eAAO,MAAM,cAAc,aAAwC,CAAC;AAEpE;;;;;;;;;2EAS2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAItE;AAED;;;;;kEAKkE;AAClE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CA6BxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF"}
|
|
@@ -10,19 +10,46 @@ export function isSchemaFromEntry(entry) {
|
|
|
10
10
|
/** Keys that a named reference object may have. Values beyond these indicate an inline resource. */
|
|
11
11
|
export const REFERENCE_KEYS = new Set(["kind", "name", "metadata"]);
|
|
12
12
|
/** True when `val` is an inline resource definition rather than a named reference.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Three shapes flow through here:
|
|
14
|
+
* - `{kind, name}` (optionally with runtime call args) → named reference, NOT inline.
|
|
15
|
+
* - `{kind, ...config}` with no name → inline definition with config; extract.
|
|
16
|
+
* - `{kind}` alone (bare kind, no name) → inline singleton — extract a fresh
|
|
17
|
+
* stateless resource. Lets simple stateless kinds be used inline without
|
|
18
|
+
* boilerplate (e.g. `encoder: {kind: Ndjson.Encoder}`, `invoke: {kind: Run.Throw}`).
|
|
19
|
+
*
|
|
20
|
+
* A named reference (has string `name`) may carry extra keys (e.g. `inputs`)
|
|
21
|
+
* that are runtime call parameters — those are never inline resources. */
|
|
15
22
|
export function isInlineResource(val) {
|
|
16
23
|
if (typeof val.name === "string")
|
|
17
24
|
return false;
|
|
18
|
-
|
|
25
|
+
if (typeof val.kind !== "string")
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
19
28
|
}
|
|
20
29
|
/** Resolves all values at a field map path in a resource config.
|
|
21
|
-
*
|
|
30
|
+
* Path-segment markers:
|
|
31
|
+
* - `[]` iterate array values at this key
|
|
32
|
+
* - `{}` iterate map values (every value in an `additionalProperties`-typed
|
|
33
|
+
* object — used for fields like `content[mime]` whose schema declares
|
|
34
|
+
* a key-as-MIME map). The path is `<key>.{}.<rest>`. */
|
|
22
35
|
export function resolveFieldValues(obj, path) {
|
|
23
36
|
const parts = path.split(".");
|
|
24
37
|
let current = [obj];
|
|
25
38
|
for (const part of parts) {
|
|
39
|
+
if (part === "{}") {
|
|
40
|
+
// Iterate the values of every map currently in `current`.
|
|
41
|
+
const next = [];
|
|
42
|
+
for (const item of current) {
|
|
43
|
+
if (!item || typeof item !== "object")
|
|
44
|
+
continue;
|
|
45
|
+
for (const v of Object.values(item)) {
|
|
46
|
+
if (v != null)
|
|
47
|
+
next.push(v);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
current = next;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
26
53
|
const isArray = part.endsWith("[]");
|
|
27
54
|
const key = isArray ? part.slice(0, -2) : part;
|
|
28
55
|
const next = [];
|
|
@@ -104,4 +131,12 @@ function traverseNode(node, path, map) {
|
|
|
104
131
|
traverseNode(propSchema, `${path}.${key}`, map);
|
|
105
132
|
}
|
|
106
133
|
}
|
|
134
|
+
// Map — `additionalProperties: { ... }` describes every value in an
|
|
135
|
+
// open-keyed object. Encoder refs nested inside `content[mime]` map
|
|
136
|
+
// entries reach Phase 5 through this branch.
|
|
137
|
+
if (node.additionalProperties &&
|
|
138
|
+
typeof node.additionalProperties === "object" &&
|
|
139
|
+
!Array.isArray(node.additionalProperties)) {
|
|
140
|
+
traverseNode(node.additionalProperties, `${path}.{}`, map);
|
|
141
|
+
}
|
|
107
142
|
}
|
|
@@ -16,7 +16,8 @@ export declare function extractAccessChains(node: ASTNode): string[][];
|
|
|
16
16
|
/**
|
|
17
17
|
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
18
18
|
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
19
|
-
* properties without `additionalProperties: true
|
|
19
|
+
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
20
|
+
* reach inside an `x-telo-stream: true` property.
|
|
20
21
|
* Returns null when the chain is valid or the schema is too open to judge.
|
|
21
22
|
*/
|
|
22
23
|
export declare function validateChainAgainstSchema(chain: string[], schema: Record<string, any>): string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AAsFD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CAiCf;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GACnC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAqDrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB"}
|
|
@@ -90,7 +90,15 @@ function visitNode(node, chains, boundVars) {
|
|
|
90
90
|
function isASTNode(v) {
|
|
91
91
|
return v !== null && typeof v === "object" && "op" in v;
|
|
92
92
|
}
|
|
93
|
-
|
|
93
|
+
// Sentinel used in extracted chains to represent an `[index]` access. The
|
|
94
|
+
// validator treats it like any other deeper segment, so a chain that reaches
|
|
95
|
+
// past an `x-telo-stream`-marked property via `[N]` is rejected the same way
|
|
96
|
+
// `.field` access is. The actual index expression is opaque to the chain
|
|
97
|
+
// validator (it only checks property names against the schema).
|
|
98
|
+
const INDEX_SEGMENT = "[*]";
|
|
99
|
+
/** Returns the member-access chain for a node if it is purely an id, ".", or
|
|
100
|
+
* "[]" chain; else null. Bracket index access is captured as `INDEX_SEGMENT`
|
|
101
|
+
* so it counts as a chain extension for the stream-property opacity rule. */
|
|
94
102
|
function extractChain(node, boundVars) {
|
|
95
103
|
if (node.op === "id") {
|
|
96
104
|
const name = node.args;
|
|
@@ -104,12 +112,19 @@ function extractChain(node, boundVars) {
|
|
|
104
112
|
if (parent !== null)
|
|
105
113
|
return [...parent, field];
|
|
106
114
|
}
|
|
115
|
+
if (node.op === "[]") {
|
|
116
|
+
const [obj] = node.args;
|
|
117
|
+
const parent = extractChain(obj, boundVars);
|
|
118
|
+
if (parent !== null)
|
|
119
|
+
return [...parent, INDEX_SEGMENT];
|
|
120
|
+
}
|
|
107
121
|
return null;
|
|
108
122
|
}
|
|
109
123
|
/**
|
|
110
124
|
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
111
125
|
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
112
|
-
* properties without `additionalProperties: true
|
|
126
|
+
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
127
|
+
* reach inside an `x-telo-stream: true` property.
|
|
113
128
|
* Returns null when the chain is valid or the schema is too open to judge.
|
|
114
129
|
*/
|
|
115
130
|
export function validateChainAgainstSchema(chain, schema) {
|
|
@@ -122,8 +137,20 @@ export function validateChainAgainstSchema(chain, schema) {
|
|
|
122
137
|
if (!props)
|
|
123
138
|
return null;
|
|
124
139
|
if (key in props) {
|
|
140
|
+
const propSchema = props[key];
|
|
141
|
+
// Stream-typed properties are opaque: pass through by reference, no
|
|
142
|
+
// member access inside. Reaching `result.output` is fine; reaching
|
|
143
|
+
// `result.output.text` is a wiring mistake — the consumer either iterates
|
|
144
|
+
// the stream in a JS.Script step or pipes it through an Encoder.
|
|
145
|
+
if (propSchema &&
|
|
146
|
+
typeof propSchema === "object" &&
|
|
147
|
+
propSchema["x-telo-stream"] === true &&
|
|
148
|
+
i < chain.length - 1) {
|
|
149
|
+
const path = chain.slice(0, i + 1).join(".");
|
|
150
|
+
return `'${path}' yields a stream — pipe it through an Encoder or iterate in a JS.Script step (no member access on stream-typed values)`;
|
|
151
|
+
}
|
|
125
152
|
// Known property — drill into it even if additionalProperties is true
|
|
126
|
-
current =
|
|
153
|
+
current = propSchema;
|
|
127
154
|
continue;
|
|
128
155
|
}
|
|
129
156
|
// Unknown property — only flag if schema is closed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
43
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/sdk": "0.
|
|
44
|
+
"@telorun/sdk": "0.7.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^20.0.0",
|
package/src/analyzer.ts
CHANGED
|
@@ -34,6 +34,41 @@ import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
|
34
34
|
|
|
35
35
|
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
36
36
|
|
|
37
|
+
const SELF_PREFIX = "Self.";
|
|
38
|
+
|
|
39
|
+
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
40
|
+
* to its canonical form. `Self.<Name>` resolves to `<ownModule>.<Name>` —
|
|
41
|
+
* the magic alias for "this library's own module" — and other prefixes
|
|
42
|
+
* resolve through the declaring file's Telo.Import aliases. */
|
|
43
|
+
function resolveSelfOrAlias(
|
|
44
|
+
value: string,
|
|
45
|
+
ownModule: string | undefined,
|
|
46
|
+
scopeResolver: AliasResolver,
|
|
47
|
+
): string | undefined {
|
|
48
|
+
if (value.startsWith(SELF_PREFIX) && ownModule) {
|
|
49
|
+
return `${ownModule}.${value.slice(SELF_PREFIX.length)}`;
|
|
50
|
+
}
|
|
51
|
+
return scopeResolver.resolveKind(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Look up a top-level field (`outputType`, `inputType`) on a kind's
|
|
55
|
+
* `Telo.Definition`. Used as a fallback by `buildStepContextSchema` when the
|
|
56
|
+
* invoked resource manifest doesn't carry the field inline — most kinds
|
|
57
|
+
* declare result shape on the definition, not the resource. */
|
|
58
|
+
function lookupDefinitionTypeField(
|
|
59
|
+
invokedKind: string,
|
|
60
|
+
fieldName: string,
|
|
61
|
+
defs: DefinitionRegistry,
|
|
62
|
+
aliases: AliasResolver,
|
|
63
|
+
allManifests: Record<string, any>[],
|
|
64
|
+
): Record<string, any> | undefined {
|
|
65
|
+
const canonical = aliases.resolveKind(invokedKind) ?? invokedKind;
|
|
66
|
+
const def = defs.resolve(canonical);
|
|
67
|
+
if (!def) return undefined;
|
|
68
|
+
const value = (def as unknown as Record<string, unknown>)[fieldName];
|
|
69
|
+
return resolveTypeFieldToSchema(value, allManifests);
|
|
70
|
+
}
|
|
71
|
+
|
|
37
72
|
function walkCelExpressions(
|
|
38
73
|
value: unknown,
|
|
39
74
|
path: string,
|
|
@@ -95,11 +130,25 @@ function extractContextsFromSchema(
|
|
|
95
130
|
* Build a `steps` context schema from `x-telo-step-context` annotation.
|
|
96
131
|
* Walks each step in the manifest array, resolves the invoked resource's outputType,
|
|
97
132
|
* and builds `steps.<name>.result` context entries.
|
|
133
|
+
*
|
|
134
|
+
* outputType resolution falls through three layers:
|
|
135
|
+
* 1. The invoked resource manifest's own `outputType` field (rare — most
|
|
136
|
+
* resources don't declare outputType inline).
|
|
137
|
+
* 2. The kind's `Telo.Definition` outputType (the common case for kinds that
|
|
138
|
+
* declare a stable result shape, e.g. `Ai.TextStream` ↦ `{output: stream}`).
|
|
139
|
+
* 3. Permissive `{type: object, additionalProperties: true}` if neither
|
|
140
|
+
* yields a schema.
|
|
141
|
+
*
|
|
142
|
+
* Layer 2 is what makes `x-telo-stream` properties on definitions actually
|
|
143
|
+
* govern step-result chain validation — without it, the validator falls back
|
|
144
|
+
* to permissive and the stream-opacity rule never fires.
|
|
98
145
|
*/
|
|
99
146
|
function buildStepContextSchema(
|
|
100
147
|
manifest: Record<string, any>,
|
|
101
148
|
defSchema: Record<string, any>,
|
|
102
149
|
allManifests: Record<string, any>[],
|
|
150
|
+
defs: DefinitionRegistry,
|
|
151
|
+
aliases: AliasResolver,
|
|
103
152
|
): Record<string, any> | undefined {
|
|
104
153
|
const props = defSchema.properties as Record<string, any> | undefined;
|
|
105
154
|
if (!props) return undefined;
|
|
@@ -139,6 +188,17 @@ function buildStepContextSchema(
|
|
|
139
188
|
} else {
|
|
140
189
|
outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
|
|
141
190
|
}
|
|
191
|
+
// Fallback: pull outputType from the kind's Telo.Definition. The
|
|
192
|
+
// resource manifest typically doesn't carry outputType; the def does.
|
|
193
|
+
if (!outputSchema && invokedKind) {
|
|
194
|
+
outputSchema = lookupDefinitionTypeField(
|
|
195
|
+
invokedKind,
|
|
196
|
+
outputTypeField,
|
|
197
|
+
defs,
|
|
198
|
+
aliases,
|
|
199
|
+
allManifests,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
142
202
|
}
|
|
143
203
|
stepProperties[name] = {
|
|
144
204
|
type: "object",
|
|
@@ -317,6 +377,25 @@ export class StaticAnalyzer {
|
|
|
317
377
|
const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
|
|
318
378
|
const moduleName = m.metadata.name as string;
|
|
319
379
|
if (moduleName) defs.registerModuleIdentity(namespace, moduleName);
|
|
380
|
+
// Auto-register `Self` as an alias for this library's own module name.
|
|
381
|
+
// Lets same-library `extends:` work (e.g. `extends: Self.Encoder` for a
|
|
382
|
+
// concrete kind whose abstract lives in the same Telo.Library) without
|
|
383
|
+
// requiring a self-import (which would loop the loader). Resolves
|
|
384
|
+
// through the same alias machinery as user-declared Telo.Imports —
|
|
385
|
+
// honours the library's `exports.kinds` list, no special cases.
|
|
386
|
+
if (moduleName) {
|
|
387
|
+
const exportedKinds: string[] = ((m as any).exports?.kinds as string[] | undefined) ?? [];
|
|
388
|
+
if (rootModules.has(moduleName)) {
|
|
389
|
+
aliases.registerImport("Self", moduleName, exportedKinds);
|
|
390
|
+
} else {
|
|
391
|
+
let libResolver = aliasesByModule.get(moduleName);
|
|
392
|
+
if (!libResolver) {
|
|
393
|
+
libResolver = new AliasResolver();
|
|
394
|
+
aliasesByModule.set(moduleName, libResolver);
|
|
395
|
+
}
|
|
396
|
+
libResolver.registerImport("Self", moduleName, exportedKinds);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
320
399
|
}
|
|
321
400
|
if (m.kind === "Telo.Import") {
|
|
322
401
|
const alias = m.metadata.name as string;
|
|
@@ -486,6 +565,8 @@ export class StaticAnalyzer {
|
|
|
486
565
|
m as Record<string, any>,
|
|
487
566
|
mDefinition.schema as Record<string, any>,
|
|
488
567
|
allManifests as Record<string, any>[],
|
|
568
|
+
defs,
|
|
569
|
+
aliases,
|
|
489
570
|
)
|
|
490
571
|
: undefined;
|
|
491
572
|
|
package/src/cel-environment.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
-
import type
|
|
2
|
+
import { Stream, type ResourceManifest } from "@telorun/sdk";
|
|
3
3
|
import { jsonSchemaToCelType } from "./schema-compat.js";
|
|
4
4
|
|
|
5
5
|
export interface CelHandlers {
|
|
@@ -20,7 +20,16 @@ const STUB_HANDLERS: CelHandlers = {
|
|
|
20
20
|
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
21
21
|
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
22
22
|
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
23
|
-
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
23
|
+
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
24
|
+
*
|
|
25
|
+
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
26
|
+
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
27
|
+
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
28
|
+
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
29
|
+
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
30
|
+
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
31
|
+
* member access raises a CEL error at runtime — matching the analyzer's
|
|
32
|
+
* static check on `x-telo-stream`-marked properties. */
|
|
24
33
|
export function buildCelEnvironment(handlers: CelHandlers = STUB_HANDLERS): Environment {
|
|
25
34
|
return new Environment({ unlistedVariablesAreDyn: true })
|
|
26
35
|
.registerFunction("join(list, string): string", (list: unknown[], sep: string) =>
|
|
@@ -34,7 +43,8 @@ export function buildCelEnvironment(handlers: CelHandlers = STUB_HANDLERS): Envi
|
|
|
34
43
|
if (map instanceof Map) return [...map.values()];
|
|
35
44
|
return Object.values(map as Record<string, unknown>);
|
|
36
45
|
})
|
|
37
|
-
.registerFunction("sha256(string): string", (s: string) => handlers.sha256(s))
|
|
46
|
+
.registerFunction("sha256(string): string", (s: string) => handlers.sha256(s))
|
|
47
|
+
.registerType("Stream", Stream as unknown as new (...args: unknown[]) => unknown);
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
@@ -107,6 +107,29 @@ function extractInlinesAtPath(
|
|
|
107
107
|
if (!obj || typeof obj !== "object" || partsLeft.length === 0) return;
|
|
108
108
|
|
|
109
109
|
const [head, ...rest] = partsLeft;
|
|
110
|
+
|
|
111
|
+
// Map iteration: descend into every value of the current object (used for
|
|
112
|
+
// schema fields with `additionalProperties` like `content[mime]`).
|
|
113
|
+
if (head === "{}") {
|
|
114
|
+
const container = obj as Record<string, unknown>;
|
|
115
|
+
for (const mapKey of Object.keys(container)) {
|
|
116
|
+
const elem = container[mapKey];
|
|
117
|
+
if (!elem || typeof elem !== "object") continue;
|
|
118
|
+
const sanitizedKey = sanitizeName(mapKey);
|
|
119
|
+
|
|
120
|
+
if (rest.length === 0) {
|
|
121
|
+
if (isInlineResource(elem as Record<string, unknown>)) {
|
|
122
|
+
const name = sanitizeName([parentName, ...nameParts, sanitizedKey].join("_"));
|
|
123
|
+
extracted.push(buildManifest(elem as Record<string, unknown>, name, parentModule, invocationContext));
|
|
124
|
+
container[mapKey] = { kind: (elem as Record<string, unknown>).kind, name };
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
traverse(elem, rest, [...nameParts, sanitizedKey]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
const isArr = head.endsWith("[]");
|
|
111
134
|
const key = isArr ? head.slice(0, -2) : head;
|
|
112
135
|
const container = obj as Record<string, unknown>;
|
|
@@ -48,19 +48,43 @@ export function isSchemaFromEntry(entry: FieldMapEntry): entry is SchemaFromFiel
|
|
|
48
48
|
export const REFERENCE_KEYS = new Set(["kind", "name", "metadata"]);
|
|
49
49
|
|
|
50
50
|
/** True when `val` is an inline resource definition rather than a named reference.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
51
|
+
* Three shapes flow through here:
|
|
52
|
+
* - `{kind, name}` (optionally with runtime call args) → named reference, NOT inline.
|
|
53
|
+
* - `{kind, ...config}` with no name → inline definition with config; extract.
|
|
54
|
+
* - `{kind}` alone (bare kind, no name) → inline singleton — extract a fresh
|
|
55
|
+
* stateless resource. Lets simple stateless kinds be used inline without
|
|
56
|
+
* boilerplate (e.g. `encoder: {kind: Ndjson.Encoder}`, `invoke: {kind: Run.Throw}`).
|
|
57
|
+
*
|
|
58
|
+
* A named reference (has string `name`) may carry extra keys (e.g. `inputs`)
|
|
59
|
+
* that are runtime call parameters — those are never inline resources. */
|
|
53
60
|
export function isInlineResource(val: Record<string, unknown>): boolean {
|
|
54
61
|
if (typeof val.name === "string") return false;
|
|
55
|
-
|
|
62
|
+
if (typeof val.kind !== "string") return false;
|
|
63
|
+
return true;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/** Resolves all values at a field map path in a resource config.
|
|
59
|
-
*
|
|
67
|
+
* Path-segment markers:
|
|
68
|
+
* - `[]` iterate array values at this key
|
|
69
|
+
* - `{}` iterate map values (every value in an `additionalProperties`-typed
|
|
70
|
+
* object — used for fields like `content[mime]` whose schema declares
|
|
71
|
+
* a key-as-MIME map). The path is `<key>.{}.<rest>`. */
|
|
60
72
|
export function resolveFieldValues(obj: unknown, path: string): unknown[] {
|
|
61
73
|
const parts = path.split(".");
|
|
62
74
|
let current: unknown[] = [obj];
|
|
63
75
|
for (const part of parts) {
|
|
76
|
+
if (part === "{}") {
|
|
77
|
+
// Iterate the values of every map currently in `current`.
|
|
78
|
+
const next: unknown[] = [];
|
|
79
|
+
for (const item of current) {
|
|
80
|
+
if (!item || typeof item !== "object") continue;
|
|
81
|
+
for (const v of Object.values(item as Record<string, unknown>)) {
|
|
82
|
+
if (v != null) next.push(v);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
current = next;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
64
88
|
const isArray = part.endsWith("[]");
|
|
65
89
|
const key = isArray ? part.slice(0, -2) : part;
|
|
66
90
|
const next: unknown[] = [];
|
|
@@ -144,4 +168,15 @@ function traverseNode(node: Record<string, any>, path: string, map: ReferenceFie
|
|
|
144
168
|
traverseNode(propSchema as Record<string, any>, `${path}.${key}`, map);
|
|
145
169
|
}
|
|
146
170
|
}
|
|
171
|
+
|
|
172
|
+
// Map — `additionalProperties: { ... }` describes every value in an
|
|
173
|
+
// open-keyed object. Encoder refs nested inside `content[mime]` map
|
|
174
|
+
// entries reach Phase 5 through this branch.
|
|
175
|
+
if (
|
|
176
|
+
node.additionalProperties &&
|
|
177
|
+
typeof node.additionalProperties === "object" &&
|
|
178
|
+
!Array.isArray(node.additionalProperties)
|
|
179
|
+
) {
|
|
180
|
+
traverseNode(node.additionalProperties as Record<string, any>, `${path}.{}`, map);
|
|
181
|
+
}
|
|
147
182
|
}
|
|
@@ -107,7 +107,16 @@ function isASTNode(v: unknown): v is ASTNode {
|
|
|
107
107
|
return v !== null && typeof v === "object" && "op" in (v as object);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
// Sentinel used in extracted chains to represent an `[index]` access. The
|
|
111
|
+
// validator treats it like any other deeper segment, so a chain that reaches
|
|
112
|
+
// past an `x-telo-stream`-marked property via `[N]` is rejected the same way
|
|
113
|
+
// `.field` access is. The actual index expression is opaque to the chain
|
|
114
|
+
// validator (it only checks property names against the schema).
|
|
115
|
+
const INDEX_SEGMENT = "[*]";
|
|
116
|
+
|
|
117
|
+
/** Returns the member-access chain for a node if it is purely an id, ".", or
|
|
118
|
+
* "[]" chain; else null. Bracket index access is captured as `INDEX_SEGMENT`
|
|
119
|
+
* so it counts as a chain extension for the stream-property opacity rule. */
|
|
111
120
|
function extractChain(node: ASTNode, boundVars: Set<string>): string[] | null {
|
|
112
121
|
if (node.op === "id") {
|
|
113
122
|
const name = node.args as string;
|
|
@@ -119,13 +128,19 @@ function extractChain(node: ASTNode, boundVars: Set<string>): string[] | null {
|
|
|
119
128
|
const parent = extractChain(obj, boundVars);
|
|
120
129
|
if (parent !== null) return [...parent, field];
|
|
121
130
|
}
|
|
131
|
+
if (node.op === "[]") {
|
|
132
|
+
const [obj] = node.args as [ASTNode, ASTNode];
|
|
133
|
+
const parent = extractChain(obj, boundVars);
|
|
134
|
+
if (parent !== null) return [...parent, INDEX_SEGMENT];
|
|
135
|
+
}
|
|
122
136
|
return null;
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
/**
|
|
126
140
|
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
127
141
|
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
128
|
-
* properties without `additionalProperties: true
|
|
142
|
+
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
143
|
+
* reach inside an `x-telo-stream: true` property.
|
|
129
144
|
* Returns null when the chain is valid or the schema is too open to judge.
|
|
130
145
|
*/
|
|
131
146
|
export function validateChainAgainstSchema(
|
|
@@ -139,8 +154,22 @@ export function validateChainAgainstSchema(
|
|
|
139
154
|
const props: Record<string, any> | undefined = current.properties;
|
|
140
155
|
if (!props) return null;
|
|
141
156
|
if (key in props) {
|
|
157
|
+
const propSchema = props[key];
|
|
158
|
+
// Stream-typed properties are opaque: pass through by reference, no
|
|
159
|
+
// member access inside. Reaching `result.output` is fine; reaching
|
|
160
|
+
// `result.output.text` is a wiring mistake — the consumer either iterates
|
|
161
|
+
// the stream in a JS.Script step or pipes it through an Encoder.
|
|
162
|
+
if (
|
|
163
|
+
propSchema &&
|
|
164
|
+
typeof propSchema === "object" &&
|
|
165
|
+
propSchema["x-telo-stream"] === true &&
|
|
166
|
+
i < chain.length - 1
|
|
167
|
+
) {
|
|
168
|
+
const path = chain.slice(0, i + 1).join(".");
|
|
169
|
+
return `'${path}' yields a stream — pipe it through an Encoder or iterate in a JS.Script step (no member access on stream-typed values)`;
|
|
170
|
+
}
|
|
142
171
|
// Known property — drill into it even if additionalProperties is true
|
|
143
|
-
current =
|
|
172
|
+
current = propSchema;
|
|
144
173
|
continue;
|
|
145
174
|
}
|
|
146
175
|
// Unknown property — only flag if schema is closed
|