@telorun/analyzer 0.5.0 → 0.6.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/README.md CHANGED
@@ -1,6 +1,21 @@
1
- # ⚡ Telo
2
-
3
- Runtime for declarative backends.
1
+ <p align="center">
2
+ <img src="./assets/telo.png" alt="Telo" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">Telo</h1>
6
+
7
+ <p align="center">Runtime for declarative backends.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://github.com/telorun/telo/actions/workflows/test.yml"><img alt="Tests" src="https://github.com/telorun/telo/actions/workflows/test.yml/badge.svg" /></a>
11
+ <a href="https://www.npmjs.com/package/@telorun/cli"><img alt="node" src="https://img.shields.io/node/v/@telorun/cli" /></a>
12
+ <br />
13
+ <a href="https://github.com/telorun/telo/commits/main"><img alt="Last commit" src="https://img.shields.io/github/last-commit/telorun/telo" /></a>
14
+ <a href="https://github.com/telorun/telo/issues"><img alt="Issues" src="https://img.shields.io/github/issues/telorun/telo" /></a>
15
+ <a href="https://github.com/telorun/telo/pulls"><img alt="Pull requests" src="https://img.shields.io/github/issues-pr/telorun/telo" /></a>
16
+ <br />
17
+ <img alt="Changesets" src="https://img.shields.io/badge/maintained%20with-changesets-176de3" />
18
+ </p>
4
19
 
5
20
  Telo is an execution engine (Micro-Kernel) that runs logic defined entirely in YAML manifests. Instead of writing imperative backend code, you define your routes, databases, schemas, and AI workflows as atomic, interconnected YAML documents. Telo takes those manifests and runs them.
6
21
 
@@ -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;AAuP/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;IAsSvB,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"}
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;AAoY/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)) {
@@ -58,12 +81,70 @@ function extractContextsFromSchema(schema, path = "$") {
58
81
  }
59
82
  return results;
60
83
  }
84
+ /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
85
+ * Non-refs and unresolved refs pass through unchanged. */
86
+ function resolveLocalRef(schema, root) {
87
+ if (!schema)
88
+ return undefined;
89
+ const ref = schema.$ref;
90
+ if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
91
+ const defName = ref.slice("#/$defs/".length);
92
+ const resolved = root.$defs?.[defName];
93
+ if (resolved && typeof resolved === "object")
94
+ return resolved;
95
+ }
96
+ return schema;
97
+ }
98
+ /** Gather property schemas from a (possibly variant-bearing) object schema:
99
+ * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch. */
100
+ function gatherPropertySchemas(schema) {
101
+ const out = [];
102
+ if (schema.properties && typeof schema.properties === "object") {
103
+ for (const [k, v] of Object.entries(schema.properties)) {
104
+ out.push([k, v]);
105
+ }
106
+ }
107
+ for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
108
+ const arr = schema[variantKey];
109
+ if (!Array.isArray(arr))
110
+ continue;
111
+ for (const variant of arr) {
112
+ if (variant && typeof variant === "object" && variant.properties) {
113
+ for (const [k, v] of Object.entries(variant.properties)) {
114
+ out.push([k, v]);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ return out;
120
+ }
61
121
  /**
62
122
  * Build a `steps` context schema from `x-telo-step-context` annotation.
63
123
  * Walks each step in the manifest array, resolves the invoked resource's outputType,
64
124
  * and builds `steps.<name>.result` context entries.
125
+ *
126
+ * outputType resolution falls through three layers:
127
+ * 1. The invoked resource manifest's own `outputType` field (rare — most
128
+ * resources don't declare outputType inline).
129
+ * 2. The kind's `Telo.Definition` outputType (the common case for kinds that
130
+ * declare a stable result shape, e.g. `Ai.TextStream` ↦ `{output: stream}`).
131
+ * 3. Permissive `{type: object, additionalProperties: true}` if neither
132
+ * yields a schema.
133
+ *
134
+ * Layer 2 is what makes `x-telo-stream` properties on definitions actually
135
+ * govern step-result chain validation — without it, the validator falls back
136
+ * to permissive and the stream-opacity rule never fires.
137
+ *
138
+ * Recursion into nested step arrays is annotation-driven via
139
+ * `x-telo-topology-role`. The analyzer recognises three role values:
140
+ * - `branch` — value is an array of steps (e.g. then / else / do / catch).
141
+ * - `branch-list`— value is an array of objects each carrying further roled
142
+ * sub-properties (e.g. elseif: [{ if, then }]).
143
+ * - `case-map` — value is an object whose values are step arrays (e.g. cases).
144
+ * No specific Run.Sequence field name is hardcoded; any kind that uses
145
+ * `x-telo-step-context` and tags its branch fields with these roles works.
65
146
  */
66
- function buildStepContextSchema(manifest, defSchema, allManifests) {
147
+ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases) {
67
148
  const props = defSchema.properties;
68
149
  if (!props)
69
150
  return undefined;
@@ -78,8 +159,35 @@ function buildStepContextSchema(manifest, defSchema, allManifests) {
78
159
  const steps = manifest[fieldName];
79
160
  if (!Array.isArray(steps))
80
161
  continue;
162
+ const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
81
163
  const stepProperties = {};
82
- const collectSteps = (items) => {
164
+ const dispatchRole = (data, role, itemsSchema) => {
165
+ if (role === "branch" && Array.isArray(data)) {
166
+ collectSteps(data);
167
+ }
168
+ else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
169
+ for (const arr of Object.values(data)) {
170
+ if (Array.isArray(arr))
171
+ collectSteps(arr);
172
+ }
173
+ }
174
+ else if (role === "branch-list" && Array.isArray(data)) {
175
+ const entrySchema = resolveLocalRef(itemsSchema, defSchema);
176
+ if (!entrySchema)
177
+ return;
178
+ for (const entry of data) {
179
+ if (!entry || typeof entry !== "object")
180
+ continue;
181
+ for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
182
+ const subRole = subSchema["x-telo-topology-role"];
183
+ if (typeof subRole !== "string")
184
+ continue;
185
+ dispatchRole(entry[subKey], subRole, subSchema.items);
186
+ }
187
+ }
188
+ }
189
+ };
190
+ function collectSteps(items) {
83
191
  for (const step of items) {
84
192
  if (!step || typeof step !== "object")
85
193
  continue;
@@ -101,6 +209,11 @@ function buildStepContextSchema(manifest, defSchema, allManifests) {
101
209
  else {
102
210
  outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
103
211
  }
212
+ // Fallback: pull outputType from the kind's Telo.Definition. The
213
+ // resource manifest typically doesn't carry outputType; the def does.
214
+ if (!outputSchema && invokedKind) {
215
+ outputSchema = lookupDefinitionTypeField(invokedKind, outputTypeField, defs, aliases, allManifests);
216
+ }
104
217
  }
105
218
  stepProperties[name] = {
106
219
  type: "object",
@@ -109,20 +222,16 @@ function buildStepContextSchema(manifest, defSchema, allManifests) {
109
222
  },
110
223
  };
111
224
  }
112
- // Recurse into nested step arrays (then, else, do, catch, finally, try, default, cases)
113
- for (const nested of ["then", "else", "do", "catch", "finally", "try", "default"]) {
114
- if (Array.isArray(s[nested]))
115
- collectSteps(s[nested]);
116
- }
117
- // cases is an object map of arrays
118
- if (s.cases && typeof s.cases === "object") {
119
- for (const arr of Object.values(s.cases)) {
120
- if (Array.isArray(arr))
121
- collectSteps(arr);
225
+ if (stepItemSchema) {
226
+ for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
227
+ const role = propSchema["x-telo-topology-role"];
228
+ if (typeof role !== "string")
229
+ continue;
230
+ dispatchRole(s[propKey], role, propSchema.items);
122
231
  }
123
232
  }
124
233
  }
125
- };
234
+ }
126
235
  collectSteps(steps);
127
236
  if (Object.keys(stepProperties).length > 0) {
128
237
  return {
@@ -235,6 +344,26 @@ export class StaticAnalyzer {
235
344
  const moduleName = m.metadata.name;
236
345
  if (moduleName)
237
346
  defs.registerModuleIdentity(namespace, moduleName);
347
+ // Auto-register `Self` as an alias for this library's own module name.
348
+ // Lets same-library `extends:` work (e.g. `extends: Self.Encoder` for a
349
+ // concrete kind whose abstract lives in the same Telo.Library) without
350
+ // requiring a self-import (which would loop the loader). Resolves
351
+ // through the same alias machinery as user-declared Telo.Imports —
352
+ // honours the library's `exports.kinds` list, no special cases.
353
+ if (moduleName) {
354
+ const exportedKinds = m.exports?.kinds ?? [];
355
+ if (rootModules.has(moduleName)) {
356
+ aliases.registerImport("Self", moduleName, exportedKinds);
357
+ }
358
+ else {
359
+ let libResolver = aliasesByModule.get(moduleName);
360
+ if (!libResolver) {
361
+ libResolver = new AliasResolver();
362
+ aliasesByModule.set(moduleName, libResolver);
363
+ }
364
+ libResolver.registerImport("Self", moduleName, exportedKinds);
365
+ }
366
+ }
238
367
  }
239
368
  if (m.kind === "Telo.Import") {
240
369
  const alias = m.metadata.name;
@@ -381,7 +510,7 @@ export class StaticAnalyzer {
381
510
  const mDefinition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
382
511
  // Pre-compute step context for manifests with x-telo-step-context
383
512
  const stepContextSchema = mDefinition?.schema
384
- ? buildStepContextSchema(m, mDefinition.schema, allManifests)
513
+ ? buildStepContextSchema(m, mDefinition.schema, allManifests, defs, aliases)
385
514
  : undefined;
386
515
  walkCelExpressions(m, "", (expr, path) => {
387
516
  let parsed;
@@ -1,12 +1,21 @@
1
1
  import { Environment } from "@marcbachmann/cel-js";
2
- import type { ResourceManifest } from "@telorun/sdk";
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,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/B;AAaD;;;iFAGiF;AACjF,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,WAA2B,GAAG,WAAW,CActF;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"}
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"}
@@ -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.
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOtE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAGpB,MAAM,YAAY,CAAC;AASpB,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;IAEJ,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,qBAAqB,GAAE,cAAc,EAAE,GAAG,iBAAsB;IAmB5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAqGnE,eAAe;YAoBf,eAAe;IAgEvB,iBAAiB,CACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;QAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;KAClD,GAAG,IAAI,CAAC;IAiCH,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CA2GnE"}
1
+ {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOtE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAGpB,MAAM,YAAY,CAAC;AASpB,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAGxB;IAEJ,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,CAAC;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,qBAAqB,GAAE,cAAc,EAAE,GAAG,iBAAsB;IAmB5E,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAKtC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAqGnE,eAAe;YAoBf,eAAe;IAgEvB,iBAAiB,CACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;QAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;KAClD,GAAG,IAAI,CAAC;IAiCH,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CA+HnE"}
@@ -336,6 +336,23 @@ export class Loader {
336
336
  e.sourceLine = m.metadata?.sourceLine ?? 0;
337
337
  throw e;
338
338
  }
339
+ if (!importedLibrary) {
340
+ const kinds = imported
341
+ .map((im) => im.kind)
342
+ .filter((k) => typeof k === "string");
343
+ // Prefer the URL the source layer actually fetched (stamped onto every
344
+ // loaded manifest's metadata.source) over the raw input — for registry
345
+ // refs the input is e.g. "std/foo@1.0.0", not a URL.
346
+ const fetchedFrom = (imported[0]?.metadata?.source) ?? importUrl;
347
+ const detail = kinds.length
348
+ ? `Fetched ${imported.length} document(s) with kinds [${kinds.join(", ")}].`
349
+ : `Fetched manifest contained no recognizable Telo documents — check that the source ` +
350
+ `serves a Telo.Library manifest and not an upstream error page.`;
351
+ const e = new Error(`Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
352
+ `Fetched from: ${fetchedFrom}. ${detail}`);
353
+ e.sourceLine = m.metadata?.sourceLine ?? 0;
354
+ throw e;
355
+ }
339
356
  if (importedLibrary?.metadata?.name) {
340
357
  libraryIdentityByUrl.set(importUrl, {
341
358
  name: importedLibrary.metadata.name,
@@ -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
- * A named reference (has string `name`) may carry extra keys (e.g. `inputs`) that
37
- * are runtime call parameters those are never inline resources. */
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
- * `[]` in a path segment means "iterate array at this key". */
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;;sEAEsE;AACtE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAGtE;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CAiBxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,iBAAiB,CAQrF"}
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
- * A named reference (has string `name`) may carry extra keys (e.g. `inputs`) that
14
- * are runtime call parameters those are never inline resources. */
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
- return Object.keys(val).some((k) => !REFERENCE_KEYS.has(k));
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
- * `[]` in a path segment means "iterate array at this key". */
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"registry-source.d.ts","sourceRoot":"","sources":["../../src/sources/registry-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAI7E,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAWxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAWxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;CAmBvB"}
1
+ {"version":3,"file":"registry-source.d.ts","sourceRoot":"","sources":["../../src/sources/registry-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAI7E,qBAAa,cAAe,YAAW,cAAc;IACvC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAWxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IA4BxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;CAmBvB"}
@@ -19,7 +19,21 @@ export class RegistrySource {
19
19
  if (!response.ok) {
20
20
  throw new Error(`Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`);
21
21
  }
22
- return { text: await response.text(), source: fetchUrl };
22
+ const text = await response.text();
23
+ // Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
24
+ // permission failures by returning a 200 status with an XML error body.
25
+ // Catch this here so the loader produces a precise error rather than
26
+ // silently parsing the XML as YAML and reporting a downstream UNDEFINED_KIND.
27
+ if (text.trimStart().startsWith("<?xml") || text.trimStart().startsWith("<Error")) {
28
+ const codeMatch = text.match(/<Code>([^<]+)<\/Code>/);
29
+ const messageMatch = text.match(/<Message>([^<]+)<\/Message>/);
30
+ const detail = codeMatch && messageMatch
31
+ ? `${codeMatch[1]}: ${messageMatch[1]}`
32
+ : text.slice(0, 200);
33
+ throw new Error(`Registry returned a non-manifest response for ${moduleRef} ` +
34
+ `(URL: ${fetchUrl}): ${detail}`);
35
+ }
36
+ return { text, source: fetchUrl };
23
37
  }
24
38
  resolveRelative(base, relative) {
25
39
  const baseUrl = this.supports(base) ? this.toRegistryModuleBase(base) : base;
@@ -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;AAwED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CAmBf;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"}
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
- /** Returns the member-access chain for a node if it is purely an id or "." chain; else null. */
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 = props[key];
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.5.0",
3
+ "version": "0.6.1",
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.6.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,
@@ -91,15 +126,77 @@ function extractContextsFromSchema(
91
126
  return results;
92
127
  }
93
128
 
129
+ /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
130
+ * Non-refs and unresolved refs pass through unchanged. */
131
+ function resolveLocalRef(
132
+ schema: Record<string, any> | undefined,
133
+ root: Record<string, any>,
134
+ ): Record<string, any> | undefined {
135
+ if (!schema) return undefined;
136
+ const ref = schema.$ref;
137
+ if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
138
+ const defName = ref.slice("#/$defs/".length);
139
+ const resolved = root.$defs?.[defName];
140
+ if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
141
+ }
142
+ return schema;
143
+ }
144
+
145
+ /** Gather property schemas from a (possibly variant-bearing) object schema:
146
+ * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch. */
147
+ function gatherPropertySchemas(schema: Record<string, any>): Array<[string, Record<string, any>]> {
148
+ const out: Array<[string, Record<string, any>]> = [];
149
+ if (schema.properties && typeof schema.properties === "object") {
150
+ for (const [k, v] of Object.entries(schema.properties as Record<string, any>)) {
151
+ out.push([k, v as Record<string, any>]);
152
+ }
153
+ }
154
+ for (const variantKey of ["oneOf", "anyOf", "allOf"] as const) {
155
+ const arr = schema[variantKey];
156
+ if (!Array.isArray(arr)) continue;
157
+ for (const variant of arr) {
158
+ if (variant && typeof variant === "object" && variant.properties) {
159
+ for (const [k, v] of Object.entries(variant.properties as Record<string, any>)) {
160
+ out.push([k, v as Record<string, any>]);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ return out;
166
+ }
167
+
94
168
  /**
95
169
  * Build a `steps` context schema from `x-telo-step-context` annotation.
96
170
  * Walks each step in the manifest array, resolves the invoked resource's outputType,
97
171
  * and builds `steps.<name>.result` context entries.
172
+ *
173
+ * outputType resolution falls through three layers:
174
+ * 1. The invoked resource manifest's own `outputType` field (rare — most
175
+ * resources don't declare outputType inline).
176
+ * 2. The kind's `Telo.Definition` outputType (the common case for kinds that
177
+ * declare a stable result shape, e.g. `Ai.TextStream` ↦ `{output: stream}`).
178
+ * 3. Permissive `{type: object, additionalProperties: true}` if neither
179
+ * yields a schema.
180
+ *
181
+ * Layer 2 is what makes `x-telo-stream` properties on definitions actually
182
+ * govern step-result chain validation — without it, the validator falls back
183
+ * to permissive and the stream-opacity rule never fires.
184
+ *
185
+ * Recursion into nested step arrays is annotation-driven via
186
+ * `x-telo-topology-role`. The analyzer recognises three role values:
187
+ * - `branch` — value is an array of steps (e.g. then / else / do / catch).
188
+ * - `branch-list`— value is an array of objects each carrying further roled
189
+ * sub-properties (e.g. elseif: [{ if, then }]).
190
+ * - `case-map` — value is an object whose values are step arrays (e.g. cases).
191
+ * No specific Run.Sequence field name is hardcoded; any kind that uses
192
+ * `x-telo-step-context` and tags its branch fields with these roles works.
98
193
  */
99
194
  function buildStepContextSchema(
100
195
  manifest: Record<string, any>,
101
196
  defSchema: Record<string, any>,
102
197
  allManifests: Record<string, any>[],
198
+ defs: DefinitionRegistry,
199
+ aliases: AliasResolver,
103
200
  ): Record<string, any> | undefined {
104
201
  const props = defSchema.properties as Record<string, any> | undefined;
105
202
  if (!props) return undefined;
@@ -115,8 +212,43 @@ function buildStepContextSchema(
115
212
  const steps = manifest[fieldName];
116
213
  if (!Array.isArray(steps)) continue;
117
214
 
215
+ const stepItemSchema = resolveLocalRef(
216
+ fieldSchema.items as Record<string, any> | undefined,
217
+ defSchema,
218
+ );
219
+
118
220
  const stepProperties: Record<string, any> = {};
119
- const collectSteps = (items: unknown[]) => {
221
+
222
+ const dispatchRole = (
223
+ data: unknown,
224
+ role: string,
225
+ itemsSchema: Record<string, any> | undefined,
226
+ ): void => {
227
+ if (role === "branch" && Array.isArray(data)) {
228
+ collectSteps(data);
229
+ } else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
230
+ for (const arr of Object.values(data as Record<string, unknown>)) {
231
+ if (Array.isArray(arr)) collectSteps(arr);
232
+ }
233
+ } else if (role === "branch-list" && Array.isArray(data)) {
234
+ const entrySchema = resolveLocalRef(itemsSchema, defSchema);
235
+ if (!entrySchema) return;
236
+ for (const entry of data) {
237
+ if (!entry || typeof entry !== "object") continue;
238
+ for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
239
+ const subRole = subSchema["x-telo-topology-role"];
240
+ if (typeof subRole !== "string") continue;
241
+ dispatchRole(
242
+ (entry as Record<string, any>)[subKey],
243
+ subRole,
244
+ subSchema.items as Record<string, any> | undefined,
245
+ );
246
+ }
247
+ }
248
+ }
249
+ };
250
+
251
+ function collectSteps(items: unknown[]): void {
120
252
  for (const step of items) {
121
253
  if (!step || typeof step !== "object") continue;
122
254
  const s = step as Record<string, any>;
@@ -139,6 +271,17 @@ function buildStepContextSchema(
139
271
  } else {
140
272
  outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
141
273
  }
274
+ // Fallback: pull outputType from the kind's Telo.Definition. The
275
+ // resource manifest typically doesn't carry outputType; the def does.
276
+ if (!outputSchema && invokedKind) {
277
+ outputSchema = lookupDefinitionTypeField(
278
+ invokedKind,
279
+ outputTypeField,
280
+ defs,
281
+ aliases,
282
+ allManifests,
283
+ );
284
+ }
142
285
  }
143
286
  stepProperties[name] = {
144
287
  type: "object",
@@ -147,18 +290,16 @@ function buildStepContextSchema(
147
290
  },
148
291
  };
149
292
  }
150
- // Recurse into nested step arrays (then, else, do, catch, finally, try, default, cases)
151
- for (const nested of ["then", "else", "do", "catch", "finally", "try", "default"]) {
152
- if (Array.isArray(s[nested])) collectSteps(s[nested]);
153
- }
154
- // cases is an object map of arrays
155
- if (s.cases && typeof s.cases === "object") {
156
- for (const arr of Object.values(s.cases)) {
157
- if (Array.isArray(arr)) collectSteps(arr);
293
+ if (stepItemSchema) {
294
+ for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
295
+ const role = propSchema["x-telo-topology-role"];
296
+ if (typeof role !== "string") continue;
297
+ dispatchRole(s[propKey], role, propSchema.items as Record<string, any> | undefined);
158
298
  }
159
299
  }
160
300
  }
161
- };
301
+ }
302
+
162
303
  collectSteps(steps);
163
304
 
164
305
  if (Object.keys(stepProperties).length > 0) {
@@ -317,6 +458,25 @@ export class StaticAnalyzer {
317
458
  const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
318
459
  const moduleName = m.metadata.name as string;
319
460
  if (moduleName) defs.registerModuleIdentity(namespace, moduleName);
461
+ // Auto-register `Self` as an alias for this library's own module name.
462
+ // Lets same-library `extends:` work (e.g. `extends: Self.Encoder` for a
463
+ // concrete kind whose abstract lives in the same Telo.Library) without
464
+ // requiring a self-import (which would loop the loader). Resolves
465
+ // through the same alias machinery as user-declared Telo.Imports —
466
+ // honours the library's `exports.kinds` list, no special cases.
467
+ if (moduleName) {
468
+ const exportedKinds: string[] = ((m as any).exports?.kinds as string[] | undefined) ?? [];
469
+ if (rootModules.has(moduleName)) {
470
+ aliases.registerImport("Self", moduleName, exportedKinds);
471
+ } else {
472
+ let libResolver = aliasesByModule.get(moduleName);
473
+ if (!libResolver) {
474
+ libResolver = new AliasResolver();
475
+ aliasesByModule.set(moduleName, libResolver);
476
+ }
477
+ libResolver.registerImport("Self", moduleName, exportedKinds);
478
+ }
479
+ }
320
480
  }
321
481
  if (m.kind === "Telo.Import") {
322
482
  const alias = m.metadata.name as string;
@@ -486,6 +646,8 @@ export class StaticAnalyzer {
486
646
  m as Record<string, any>,
487
647
  mDefinition.schema as Record<string, any>,
488
648
  allManifests as Record<string, any>[],
649
+ defs,
650
+ aliases,
489
651
  )
490
652
  : undefined;
491
653
 
@@ -1,5 +1,5 @@
1
1
  import { Environment } from "@marcbachmann/cel-js";
2
- import type { ResourceManifest } from "@telorun/sdk";
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
@@ -398,6 +398,26 @@ export class Loader {
398
398
  (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
399
399
  throw e;
400
400
  }
401
+ if (!importedLibrary) {
402
+ const kinds = imported
403
+ .map((im) => im.kind)
404
+ .filter((k): k is string => typeof k === "string");
405
+ // Prefer the URL the source layer actually fetched (stamped onto every
406
+ // loaded manifest's metadata.source) over the raw input — for registry
407
+ // refs the input is e.g. "std/foo@1.0.0", not a URL.
408
+ const fetchedFrom =
409
+ ((imported[0]?.metadata as { source?: string } | undefined)?.source) ?? importUrl;
410
+ const detail = kinds.length
411
+ ? `Fetched ${imported.length} document(s) with kinds [${kinds.join(", ")}].`
412
+ : `Fetched manifest contained no recognizable Telo documents — check that the source ` +
413
+ `serves a Telo.Library manifest and not an upstream error page.`;
414
+ const e = new Error(
415
+ `Telo.Import target '${importSource}' did not resolve to a Telo.Library. ` +
416
+ `Fetched from: ${fetchedFrom}. ${detail}`,
417
+ );
418
+ (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
419
+ throw e;
420
+ }
401
421
  if (importedLibrary?.metadata?.name) {
402
422
  libraryIdentityByUrl.set(importUrl, {
403
423
  name: importedLibrary.metadata.name as string,
@@ -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
- * A named reference (has string `name`) may carry extra keys (e.g. `inputs`) that
52
- * are runtime call parameters those are never inline resources. */
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
- return Object.keys(val).some((k) => !REFERENCE_KEYS.has(k));
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
- * `[]` in a path segment means "iterate array at this key". */
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
  }
@@ -24,7 +24,24 @@ export class RegistrySource implements ManifestSource {
24
24
  `Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`,
25
25
  );
26
26
  }
27
- return { text: await response.text(), source: fetchUrl };
27
+ const text = await response.text();
28
+ // Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
29
+ // permission failures by returning a 200 status with an XML error body.
30
+ // Catch this here so the loader produces a precise error rather than
31
+ // silently parsing the XML as YAML and reporting a downstream UNDEFINED_KIND.
32
+ if (text.trimStart().startsWith("<?xml") || text.trimStart().startsWith("<Error")) {
33
+ const codeMatch = text.match(/<Code>([^<]+)<\/Code>/);
34
+ const messageMatch = text.match(/<Message>([^<]+)<\/Message>/);
35
+ const detail =
36
+ codeMatch && messageMatch
37
+ ? `${codeMatch[1]}: ${messageMatch[1]}`
38
+ : text.slice(0, 200);
39
+ throw new Error(
40
+ `Registry returned a non-manifest response for ${moduleRef} ` +
41
+ `(URL: ${fetchUrl}): ${detail}`,
42
+ );
43
+ }
44
+ return { text, source: fetchUrl };
28
45
  }
29
46
 
30
47
  resolveRelative(base: string, relative: string): string {
@@ -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
- /** Returns the member-access chain for a node if it is purely an id or "." chain; else null. */
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 = props[key];
172
+ current = propSchema;
144
173
  continue;
145
174
  }
146
175
  // Unknown property — only flag if schema is closed