@telorun/analyzer 0.6.0 → 0.7.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/README.md CHANGED
@@ -1,6 +1,21 @@
1
- # ⚡ Telo
1
+ <p align="center">
2
+ <img src="./assets/telo.png" alt="Telo" width="200" />
3
+ </p>
2
4
 
3
- Runtime for declarative backends.
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
 
@@ -27,8 +42,6 @@ $ telo ./examples/hello-api.yaml
27
42
  - **Indexes** resources by Kind and Name for constant-time lookup.
28
43
  - **Dispatches** execution to the controller that owns each Kind.
29
44
 
30
- Manifests also support directives for dynamic generation: `$let`, `$if`, `$for`, `$eval`, and `$include`. See [CEL-YAML Templating](./yaml-cel-templating/README.md) for documentation.
31
-
32
45
  ## Example manifest
33
46
 
34
47
  Here is an example Telo application that defines a simple HTTP API:
@@ -221,7 +234,6 @@ Those manifests were taken to the next level by allowing them to run inside a st
221
234
  ## See more at
222
235
 
223
236
  - [Telo Kernel](./kernel/README.md)
224
- - [CEL-YAML Templating](./yaml-cel-templating/README.md)
225
237
  - [Telo SDK for module authors](sdk/README.md)
226
238
  - [Modules](modules/README.md)
227
239
 
@@ -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;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"}
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
@@ -81,6 +81,43 @@ function extractContextsFromSchema(schema, path = "$") {
81
81
  }
82
82
  return results;
83
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
+ }
84
121
  /**
85
122
  * Build a `steps` context schema from `x-telo-step-context` annotation.
86
123
  * Walks each step in the manifest array, resolves the invoked resource's outputType,
@@ -97,6 +134,15 @@ function extractContextsFromSchema(schema, path = "$") {
97
134
  * Layer 2 is what makes `x-telo-stream` properties on definitions actually
98
135
  * govern step-result chain validation — without it, the validator falls back
99
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.
100
146
  */
101
147
  function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases) {
102
148
  const props = defSchema.properties;
@@ -113,8 +159,35 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
113
159
  const steps = manifest[fieldName];
114
160
  if (!Array.isArray(steps))
115
161
  continue;
162
+ const stepItemSchema = resolveLocalRef(fieldSchema.items, defSchema);
116
163
  const stepProperties = {};
117
- 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) {
118
191
  for (const step of items) {
119
192
  if (!step || typeof step !== "object")
120
193
  continue;
@@ -149,20 +222,16 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
149
222
  },
150
223
  };
151
224
  }
152
- // Recurse into nested step arrays (then, else, do, catch, finally, try, default, cases)
153
- for (const nested of ["then", "else", "do", "catch", "finally", "try", "default"]) {
154
- if (Array.isArray(s[nested]))
155
- collectSteps(s[nested]);
156
- }
157
- // cases is an object map of arrays
158
- if (s.cases && typeof s.cases === "object") {
159
- for (const arr of Object.values(s.cases)) {
160
- if (Array.isArray(arr))
161
- 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);
162
231
  }
163
232
  }
164
233
  }
165
- };
234
+ }
166
235
  collectSteps(steps);
167
236
  if (Object.keys(stepProperties).length > 0) {
168
237
  return {
@@ -2,6 +2,7 @@ import { Environment } from "@marcbachmann/cel-js";
2
2
  import { type ResourceManifest } from "@telorun/sdk";
3
3
  export interface CelHandlers {
4
4
  sha256: (s: string) => string;
5
+ json: (value: unknown) => string;
5
6
  }
6
7
  /** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
7
8
  * same function signatures (so `env.check()` succeeds for type-inference) — the
@@ -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,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
+ {"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;IAC9B,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AAcD;;;;;;;;;;;;yDAYyD;AACzD,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,WAA2B,GAAG,WAAW,CAgBtF;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"}
@@ -7,6 +7,7 @@ const stub = (name) => () => {
7
7
  };
8
8
  const STUB_HANDLERS = {
9
9
  sha256: stub("sha256"),
10
+ json: stub("json"),
10
11
  };
11
12
  /** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
12
13
  * same function signatures (so `env.check()` succeeds for type-inference) — the
@@ -22,7 +23,7 @@ const STUB_HANDLERS = {
22
23
  * member access raises a CEL error at runtime — matching the analyzer's
23
24
  * static check on `x-telo-stream`-marked properties. */
24
25
  export function buildCelEnvironment(handlers = STUB_HANDLERS) {
25
- return new Environment({ unlistedVariablesAreDyn: true })
26
+ return new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true })
26
27
  .registerFunction("join(list, string): string", (list, sep) => list.map(String).join(sep))
27
28
  .registerFunction("keys(map): list", (map) => {
28
29
  if (map instanceof Map)
@@ -35,6 +36,7 @@ export function buildCelEnvironment(handlers = STUB_HANDLERS) {
35
36
  return Object.values(map);
36
37
  })
37
38
  .registerFunction("sha256(string): string", (s) => handlers.sha256(s))
39
+ .registerFunction("json(dyn): string", (value) => handlers.json(value))
38
40
  .registerType("Stream", Stream);
39
41
  }
40
42
  /** Clone `baseEnv` and register typed variable declarations so that
@@ -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,
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
package/src/analyzer.ts CHANGED
@@ -126,6 +126,45 @@ function extractContextsFromSchema(
126
126
  return results;
127
127
  }
128
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
+
129
168
  /**
130
169
  * Build a `steps` context schema from `x-telo-step-context` annotation.
131
170
  * Walks each step in the manifest array, resolves the invoked resource's outputType,
@@ -142,6 +181,15 @@ function extractContextsFromSchema(
142
181
  * Layer 2 is what makes `x-telo-stream` properties on definitions actually
143
182
  * govern step-result chain validation — without it, the validator falls back
144
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.
145
193
  */
146
194
  function buildStepContextSchema(
147
195
  manifest: Record<string, any>,
@@ -164,8 +212,43 @@ function buildStepContextSchema(
164
212
  const steps = manifest[fieldName];
165
213
  if (!Array.isArray(steps)) continue;
166
214
 
215
+ const stepItemSchema = resolveLocalRef(
216
+ fieldSchema.items as Record<string, any> | undefined,
217
+ defSchema,
218
+ );
219
+
167
220
  const stepProperties: Record<string, any> = {};
168
- 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 {
169
252
  for (const step of items) {
170
253
  if (!step || typeof step !== "object") continue;
171
254
  const s = step as Record<string, any>;
@@ -207,18 +290,16 @@ function buildStepContextSchema(
207
290
  },
208
291
  };
209
292
  }
210
- // Recurse into nested step arrays (then, else, do, catch, finally, try, default, cases)
211
- for (const nested of ["then", "else", "do", "catch", "finally", "try", "default"]) {
212
- if (Array.isArray(s[nested])) collectSteps(s[nested]);
213
- }
214
- // cases is an object map of arrays
215
- if (s.cases && typeof s.cases === "object") {
216
- for (const arr of Object.values(s.cases)) {
217
- 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);
218
298
  }
219
299
  }
220
300
  }
221
- };
301
+ }
302
+
222
303
  collectSteps(steps);
223
304
 
224
305
  if (Object.keys(stepProperties).length > 0) {
@@ -4,6 +4,7 @@ import { jsonSchemaToCelType } from "./schema-compat.js";
4
4
 
5
5
  export interface CelHandlers {
6
6
  sha256: (s: string) => string;
7
+ json: (value: unknown) => string;
7
8
  }
8
9
 
9
10
  const stub = (name: string) => () => {
@@ -15,6 +16,7 @@ const stub = (name: string) => () => {
15
16
 
16
17
  const STUB_HANDLERS: CelHandlers = {
17
18
  sha256: stub("sha256"),
19
+ json: stub("json"),
18
20
  };
19
21
 
20
22
  /** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
@@ -31,7 +33,7 @@ const STUB_HANDLERS: CelHandlers = {
31
33
  * member access raises a CEL error at runtime — matching the analyzer's
32
34
  * static check on `x-telo-stream`-marked properties. */
33
35
  export function buildCelEnvironment(handlers: CelHandlers = STUB_HANDLERS): Environment {
34
- return new Environment({ unlistedVariablesAreDyn: true })
36
+ return new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true })
35
37
  .registerFunction("join(list, string): string", (list: unknown[], sep: string) =>
36
38
  list.map(String).join(sep),
37
39
  )
@@ -44,6 +46,7 @@ export function buildCelEnvironment(handlers: CelHandlers = STUB_HANDLERS): Envi
44
46
  return Object.values(map as Record<string, unknown>);
45
47
  })
46
48
  .registerFunction("sha256(string): string", (s: string) => handlers.sha256(s))
49
+ .registerFunction("json(dyn): string", (value: unknown) => handlers.json(value))
47
50
  .registerType("Stream", Stream as unknown as new (...args: unknown[]) => unknown);
48
51
  }
49
52
 
@@ -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,
@@ -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 {