@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 +17 -5
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +81 -12
- package/dist/cel-environment.d.ts +1 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +3 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +17 -0
- package/dist/sources/registry-source.d.ts.map +1 -1
- package/dist/sources/registry-source.js +15 -1
- package/package.json +1 -1
- package/src/analyzer.ts +91 -10
- package/src/cel-environment.ts +4 -1
- package/src/manifest-loader.ts +20 -0
- package/src/sources/registry-source.ts +18 -1
package/README.md
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/telo.png" alt="Telo" width="200" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
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
|
|
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;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
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
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"}
|
package/dist/cel-environment.js
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/manifest-loader.js
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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) {
|
package/src/cel-environment.ts
CHANGED
|
@@ -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
|
|
package/src/manifest-loader.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|