@telorun/analyzer 0.7.0 → 0.8.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/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +71 -73
- package/dist/cel-environment.d.ts +3 -19
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +1 -40
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +3 -2
- package/dist/precompile.d.ts +11 -5
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +35 -35
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +4 -0
- package/dist/validate-cel-context.d.ts +1 -16
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +1 -133
- package/package.json +8 -4
- package/src/analyzer.ts +81 -92
- package/src/cel-environment.ts +3 -48
- package/src/manifest-loader.ts +3 -2
- package/src/precompile.ts +35 -39
- package/src/schema-compat.ts +4 -0
- package/src/validate-cel-context.ts +1 -142
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;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,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;AAgX/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;IAmUvB,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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defaultRegistry, walkCelExpressions } from "@telorun/templating";
|
|
1
2
|
import { AliasResolver } from "./alias-resolver.js";
|
|
2
3
|
import { buildCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
|
|
3
4
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
@@ -8,11 +9,10 @@ import { isModuleKind } from "./module-kinds.js";
|
|
|
8
9
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
9
10
|
import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
10
11
|
import { DiagnosticSeverity } from "./types.js";
|
|
11
|
-
import {
|
|
12
|
+
import { getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, } from "./validate-cel-context.js";
|
|
12
13
|
import { validateExtends } from "./validate-extends.js";
|
|
13
14
|
import { validateReferences } from "./validate-references.js";
|
|
14
15
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
15
|
-
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
16
16
|
const SELF_PREFIX = "Self.";
|
|
17
17
|
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
18
18
|
* to its canonical form. `Self.<Name>` resolves to `<ownModule>.<Name>` —
|
|
@@ -36,21 +36,6 @@ function lookupDefinitionTypeField(invokedKind, fieldName, defs, aliases, allMan
|
|
|
36
36
|
const value = def[fieldName];
|
|
37
37
|
return resolveTypeFieldToSchema(value, allManifests);
|
|
38
38
|
}
|
|
39
|
-
function walkCelExpressions(value, path, cb) {
|
|
40
|
-
if (typeof value === "string") {
|
|
41
|
-
for (const m of value.matchAll(TEMPLATE_REGEX)) {
|
|
42
|
-
cb(m[1].trim(), path);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
else if (Array.isArray(value)) {
|
|
46
|
-
value.forEach((v, i) => walkCelExpressions(v, `${path}[${i}]`, cb));
|
|
47
|
-
}
|
|
48
|
-
else if (value !== null && typeof value === "object") {
|
|
49
|
-
for (const [k, v] of Object.entries(value)) {
|
|
50
|
-
walkCelExpressions(v, path ? `${path}.${k}` : k, cb);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
39
|
const SOURCE = "telo-analyzer";
|
|
55
40
|
/**
|
|
56
41
|
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
@@ -193,28 +178,30 @@ function buildStepContextSchema(manifest, defSchema, allManifests, defs, aliases
|
|
|
193
178
|
continue;
|
|
194
179
|
const s = step;
|
|
195
180
|
const name = s.name;
|
|
196
|
-
|
|
197
|
-
|
|
181
|
+
const invoke = s[invokeField];
|
|
182
|
+
// Only invoke steps register a `steps.<name>.result` entry — control-flow
|
|
183
|
+
// wrappers (try/if/while/switch/throw) don't produce a result and must
|
|
184
|
+
// not shadow real entries with a permissive `additionalProperties: true`,
|
|
185
|
+
// or unknown step references slip through chain validation.
|
|
186
|
+
if (typeof name === "string" && invoke && typeof invoke === "object") {
|
|
198
187
|
let outputSchema;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
outputSchema = resolveTypeFieldToSchema(invokedManifest[outputTypeField], allManifests);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
|
|
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);
|
|
188
|
+
const invokedKind = invoke.kind;
|
|
189
|
+
const invokedName = invoke.name;
|
|
190
|
+
if (invokedName) {
|
|
191
|
+
const invokedManifest = allManifests.find((m) => m.metadata?.name === invokedName &&
|
|
192
|
+
(!invokedKind || m.kind === invokedKind));
|
|
193
|
+
if (invokedManifest) {
|
|
194
|
+
outputSchema = resolveTypeFieldToSchema(invokedManifest[outputTypeField], allManifests);
|
|
216
195
|
}
|
|
217
196
|
}
|
|
197
|
+
else {
|
|
198
|
+
outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
|
|
199
|
+
}
|
|
200
|
+
// Fallback: pull outputType from the kind's Telo.Definition. The
|
|
201
|
+
// resource manifest typically doesn't carry outputType; the def does.
|
|
202
|
+
if (!outputSchema && invokedKind) {
|
|
203
|
+
outputSchema = lookupDefinitionTypeField(invokedKind, outputTypeField, defs, aliases, allManifests);
|
|
204
|
+
}
|
|
218
205
|
stepProperties[name] = {
|
|
219
206
|
type: "object",
|
|
220
207
|
properties: {
|
|
@@ -512,27 +499,14 @@ export class StaticAnalyzer {
|
|
|
512
499
|
const stepContextSchema = mDefinition?.schema
|
|
513
500
|
? buildStepContextSchema(m, mDefinition.schema, allManifests, defs, aliases)
|
|
514
501
|
: undefined;
|
|
515
|
-
walkCelExpressions(m, "", (expr, path) => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
diagnostics.push({
|
|
522
|
-
severity: DiagnosticSeverity.Error,
|
|
523
|
-
code: "CEL_SYNTAX_ERROR",
|
|
524
|
-
source: SOURCE,
|
|
525
|
-
message: `CEL syntax error at ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
526
|
-
data: { resource, filePath, path },
|
|
527
|
-
});
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
const accessChains = extractAccessChains(parsed.ast);
|
|
502
|
+
walkCelExpressions(m, "", (expr, path, engineName) => {
|
|
503
|
+
// Resolve the effective context for this expression's path. The
|
|
504
|
+
// engine receives a single closed schema and validates member-access
|
|
505
|
+
// chains against it; per-path resolution (step context, x-telo-context,
|
|
506
|
+
// kernel-globals merge) stays on the analyzer side because it depends
|
|
507
|
+
// on analyzer-internal state (definitions, aliases).
|
|
531
508
|
const contexts = mDefinition?.schema ? extractContextsFromSchema(mDefinition.schema) : [];
|
|
532
509
|
const invocationContext = m.metadata?.xTeloInvocationContext;
|
|
533
|
-
// If no static context but we have step context, inject it
|
|
534
|
-
if (contexts.length === 0 && !invocationContext && !stepContextSchema)
|
|
535
|
-
return;
|
|
536
510
|
let matchedContext;
|
|
537
511
|
let matchedScope;
|
|
538
512
|
for (const ctx of contexts) {
|
|
@@ -544,7 +518,6 @@ export class StaticAnalyzer {
|
|
|
544
518
|
}
|
|
545
519
|
if (!matchedContext)
|
|
546
520
|
matchedContext = invocationContext;
|
|
547
|
-
// Merge step context into the effective context
|
|
548
521
|
if (stepContextSchema) {
|
|
549
522
|
const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
550
523
|
matchedContext = {
|
|
@@ -555,24 +528,49 @@ export class StaticAnalyzer {
|
|
|
555
528
|
},
|
|
556
529
|
};
|
|
557
530
|
}
|
|
558
|
-
|
|
531
|
+
let effectiveContext = null;
|
|
532
|
+
if (matchedContext) {
|
|
533
|
+
const manifestItem = matchedScope
|
|
534
|
+
? getManifestItem(path, matchedScope, m)
|
|
535
|
+
: m;
|
|
536
|
+
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, allManifests);
|
|
537
|
+
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
538
|
+
}
|
|
539
|
+
const engine = defaultRegistry().get(engineName);
|
|
540
|
+
if (!engine)
|
|
559
541
|
return;
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
542
|
+
const findings = engine.analyze(expr, { celEnv: this.celEnv, contextSchema: effectiveContext });
|
|
543
|
+
for (const f of findings) {
|
|
544
|
+
if (f.code === "CEL_SYNTAX_ERROR") {
|
|
545
|
+
diagnostics.push({
|
|
546
|
+
severity: DiagnosticSeverity.Error,
|
|
547
|
+
code: "CEL_SYNTAX_ERROR",
|
|
548
|
+
source: SOURCE,
|
|
549
|
+
message: `CEL syntax error at ${path}: ${f.message}`,
|
|
550
|
+
data: { resource, filePath, path },
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
else if (f.code === "CEL_UNKNOWN_FIELD") {
|
|
554
|
+
diagnostics.push({
|
|
555
|
+
severity: DiagnosticSeverity.Error,
|
|
556
|
+
code: "CEL_UNKNOWN_FIELD",
|
|
557
|
+
source: SOURCE,
|
|
558
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
559
|
+
data: { resource, filePath, path },
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
// Unknown code from a future engine — pass the message through,
|
|
564
|
+
// tagged with a generic ENGINE_DIAGNOSTIC code so downstream
|
|
565
|
+
// filters can still bucket it.
|
|
566
|
+
diagnostics.push({
|
|
567
|
+
severity: DiagnosticSeverity.Error,
|
|
568
|
+
code: f.code ?? "ENGINE_DIAGNOSTIC",
|
|
569
|
+
source: SOURCE,
|
|
570
|
+
message: `${m.kind}/${resource.name}: !${engineName} at '${path}': ${f.message}`,
|
|
571
|
+
data: { resource, filePath, path },
|
|
572
|
+
});
|
|
573
|
+
}
|
|
576
574
|
}
|
|
577
575
|
});
|
|
578
576
|
}
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
-
import {
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
json: (value: unknown) => string;
|
|
6
|
-
}
|
|
7
|
-
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
8
|
-
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
9
|
-
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
10
|
-
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
11
|
-
*
|
|
12
|
-
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
13
|
-
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
14
|
-
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
15
|
-
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
16
|
-
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
17
|
-
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
18
|
-
* member access raises a CEL error at runtime — matching the analyzer's
|
|
19
|
-
* static check on `x-telo-stream`-marked properties. */
|
|
20
|
-
export declare function buildCelEnvironment(handlers?: CelHandlers): Environment;
|
|
2
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
|
+
export { buildCelEnvironment } from "@telorun/templating";
|
|
4
|
+
export type { CelHandlers } from "@telorun/templating";
|
|
21
5
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
22
6
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
|
23
7
|
*
|
|
@@ -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,
|
|
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,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,YAAY,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD;;;;;;;;;uEASuE;AACvE,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,GAC9C,WAAW,CAyCb"}
|
package/dist/cel-environment.js
CHANGED
|
@@ -1,44 +1,5 @@
|
|
|
1
|
-
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
-
import { Stream } from "@telorun/sdk";
|
|
3
1
|
import { jsonSchemaToCelType } from "./schema-compat.js";
|
|
4
|
-
|
|
5
|
-
throw new Error(`${name}() is not available in this environment. ` +
|
|
6
|
-
`Construct StaticAnalyzer or Loader with celHandlers to enable it.`);
|
|
7
|
-
};
|
|
8
|
-
const STUB_HANDLERS = {
|
|
9
|
-
sha256: stub("sha256"),
|
|
10
|
-
json: stub("json"),
|
|
11
|
-
};
|
|
12
|
-
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
13
|
-
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
14
|
-
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
15
|
-
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
16
|
-
*
|
|
17
|
-
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
18
|
-
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
19
|
-
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
20
|
-
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
21
|
-
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
22
|
-
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
23
|
-
* member access raises a CEL error at runtime — matching the analyzer's
|
|
24
|
-
* static check on `x-telo-stream`-marked properties. */
|
|
25
|
-
export function buildCelEnvironment(handlers = STUB_HANDLERS) {
|
|
26
|
-
return new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true })
|
|
27
|
-
.registerFunction("join(list, string): string", (list, sep) => list.map(String).join(sep))
|
|
28
|
-
.registerFunction("keys(map): list", (map) => {
|
|
29
|
-
if (map instanceof Map)
|
|
30
|
-
return [...map.keys()];
|
|
31
|
-
return Object.keys(map);
|
|
32
|
-
})
|
|
33
|
-
.registerFunction("values(map): list", (map) => {
|
|
34
|
-
if (map instanceof Map)
|
|
35
|
-
return [...map.values()];
|
|
36
|
-
return Object.values(map);
|
|
37
|
-
})
|
|
38
|
-
.registerFunction("sha256(string): string", (s) => handlers.sha256(s))
|
|
39
|
-
.registerFunction("json(dyn): string", (value) => handlers.json(value))
|
|
40
|
-
.registerType("Stream", Stream);
|
|
41
|
-
}
|
|
2
|
+
export { buildCelEnvironment } from "@telorun/templating";
|
|
42
3
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
43
4
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
|
44
5
|
*
|
|
@@ -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;
|
|
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;AAQtE,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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isCompiledValue } from "@telorun/sdk";
|
|
2
|
+
import { defaultCustomTags } from "@telorun/templating";
|
|
2
3
|
import { isMap, isPair, isScalar, isSeq, parseAllDocuments } from "yaml";
|
|
3
4
|
import { HttpSource } from "./sources/http-source.js";
|
|
4
5
|
import { RegistrySource } from "./sources/registry-source.js";
|
|
@@ -53,7 +54,7 @@ export class Loader {
|
|
|
53
54
|
if (cached && cached.text === text) {
|
|
54
55
|
return cloneManifestArray(cached.manifests);
|
|
55
56
|
}
|
|
56
|
-
const parsedDocuments = parseAllDocuments(text);
|
|
57
|
+
const parsedDocuments = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
57
58
|
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
58
59
|
const offsets = documentLineOffsets(text);
|
|
59
60
|
const lineOffsets = buildLineOffsets(text);
|
|
@@ -154,7 +155,7 @@ export class Loader {
|
|
|
154
155
|
}
|
|
155
156
|
async loadPartialFile(url, ownerModuleName, options) {
|
|
156
157
|
const { text, source } = await this.pick(url).read(url);
|
|
157
|
-
const parsedDocuments = parseAllDocuments(text);
|
|
158
|
+
const parsedDocuments = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
158
159
|
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
159
160
|
const offsets = documentLineOffsets(text);
|
|
160
161
|
const lineOffsets = buildLineOffsets(text);
|
package/dist/precompile.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
/**
|
|
3
|
-
* Walks a raw YAML document and replaces all
|
|
4
|
-
* CompiledValue wrappers. Throws on CEL syntax
|
|
5
|
-
* Intended to be called once per document at load time.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Walks a raw YAML document and replaces all `${{ expr }}` strings (and
|
|
4
|
+
* `!cel`-tagged sentinels) with CompiledValue wrappers. Throws on CEL syntax
|
|
5
|
+
* errors. Intended to be called once per document at load time.
|
|
6
|
+
*
|
|
7
|
+
* Note on Telo.Definition / Telo.Abstract: the walker traverses these too.
|
|
8
|
+
* Their `schema` fields are JSON Schema metadata and don't typically contain
|
|
9
|
+
* `${{ }}` text, so compile is a no-op there. Their `template` fields, on
|
|
10
|
+
* the other hand, *do* carry CEL — Definition-driven templates are expanded
|
|
11
|
+
* by the kernel and rely on the precompiled tree. If a description or
|
|
12
|
+
* example string inside a schema happens to contain `${{ }}`, it will be
|
|
13
|
+
* interpreted as CEL; tag it `!literal` to opt out.
|
|
8
14
|
*/
|
|
9
15
|
export declare function precompileDoc(doc: unknown, env: Environment): unknown;
|
|
10
16
|
//# sourceMappingURL=precompile.d.ts.map
|
package/dist/precompile.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"precompile.d.ts","sourceRoot":"","sources":["../src/precompile.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"precompile.d.ts","sourceRoot":"","sources":["../src/precompile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAIxD;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAmCrE"}
|
package/dist/precompile.js
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { isCompiledValue } from "@telorun/sdk";
|
|
2
|
+
import { compileString, defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
/**
|
|
4
|
-
* Walks a raw YAML document and replaces all
|
|
5
|
-
* CompiledValue wrappers. Throws on CEL syntax
|
|
6
|
-
* Intended to be called once per document at load time.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Walks a raw YAML document and replaces all `${{ expr }}` strings (and
|
|
5
|
+
* `!cel`-tagged sentinels) with CompiledValue wrappers. Throws on CEL syntax
|
|
6
|
+
* errors. Intended to be called once per document at load time.
|
|
7
|
+
*
|
|
8
|
+
* Note on Telo.Definition / Telo.Abstract: the walker traverses these too.
|
|
9
|
+
* Their `schema` fields are JSON Schema metadata and don't typically contain
|
|
10
|
+
* `${{ }}` text, so compile is a no-op there. Their `template` fields, on
|
|
11
|
+
* the other hand, *do* carry CEL — Definition-driven templates are expanded
|
|
12
|
+
* by the kernel and rely on the precompiled tree. If a description or
|
|
13
|
+
* example string inside a schema happens to contain `${{ }}`, it will be
|
|
14
|
+
* interpreted as CEL; tag it `!literal` to opt out.
|
|
9
15
|
*/
|
|
10
16
|
export function precompileDoc(doc, env) {
|
|
17
|
+
// Tagged sentinel: dispatch to the engine. The result is decorated with
|
|
18
|
+
// `__tagged` + `engine` + `source` when it's a CompiledValue so the
|
|
19
|
+
// analyzer's diagnostic walk can identify it on compiled trees too;
|
|
20
|
+
// engines returning plain values (e.g. `literal` → a string) pass through
|
|
21
|
+
// verbatim — the runtime contract is "any scalar value is fine."
|
|
22
|
+
if (isTaggedSentinel(doc)) {
|
|
23
|
+
const engine = defaultRegistry().get(doc.engine);
|
|
24
|
+
if (!engine) {
|
|
25
|
+
throw new Error(`Unknown templating engine: !${doc.engine}`);
|
|
26
|
+
}
|
|
27
|
+
const compiled = engine.compile(doc.source, { celEnv: env });
|
|
28
|
+
if (isCompiledValue(compiled)) {
|
|
29
|
+
return {
|
|
30
|
+
__tagged: true,
|
|
31
|
+
__compiled: true,
|
|
32
|
+
engine: doc.engine,
|
|
33
|
+
source: doc.source,
|
|
34
|
+
call: compiled.call.bind(compiled),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return compiled;
|
|
38
|
+
}
|
|
11
39
|
if (typeof doc === "string")
|
|
12
40
|
return compileString(doc, env);
|
|
13
41
|
if (Array.isArray(doc))
|
|
@@ -23,31 +51,3 @@ export function precompileDoc(doc, env) {
|
|
|
23
51
|
}
|
|
24
52
|
return doc;
|
|
25
53
|
}
|
|
26
|
-
function compileString(s, env) {
|
|
27
|
-
if (!s.includes("${{"))
|
|
28
|
-
return s;
|
|
29
|
-
const exact = s.match(EXACT_TEMPLATE_REGEX);
|
|
30
|
-
if (exact) {
|
|
31
|
-
const expr = exact[1].trim();
|
|
32
|
-
const fn = env.parse(expr);
|
|
33
|
-
return { __compiled: true, source: expr, call: (ctx) => fn(ctx) };
|
|
34
|
-
}
|
|
35
|
-
// Interpolated template — collect literal parts + compiled sub-expressions
|
|
36
|
-
const parts = [];
|
|
37
|
-
let last = 0;
|
|
38
|
-
for (const m of s.matchAll(TEMPLATE_REGEX)) {
|
|
39
|
-
if (m.index > last)
|
|
40
|
-
parts.push(s.slice(last, m.index));
|
|
41
|
-
const expr = m[1].trim();
|
|
42
|
-
const fn = env.parse(expr);
|
|
43
|
-
parts.push({ __compiled: true, source: expr, call: (ctx) => fn(ctx) });
|
|
44
|
-
last = m.index + m[0].length;
|
|
45
|
-
}
|
|
46
|
-
if (last < s.length)
|
|
47
|
-
parts.push(s.slice(last));
|
|
48
|
-
return {
|
|
49
|
-
__compiled: true,
|
|
50
|
-
source: s,
|
|
51
|
-
call: (ctx) => parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema-compat.d.ts","sourceRoot":"","sources":["../src/schema-compat.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,GAAG,KAA0C,CAAC;AAEpD;0FAC0F;AAC1F,wBAAgB,SAAS,IAAI,YAAY,CAAC,OAAO,GAAG,CAAC,CAMpD;AAKD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;oEAEoE;AACpE,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,mBAAmB,CAIrB;AAiDD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAelD;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGxE;AAuBD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,0GAA0G;AAC1G,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,EAAE,CAe/F;AAED;qFACqF;AACrF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQ7E;AAED;;;;6DAI6D;AAC7D,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,EAAE,MAAM,GACX,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAsBjC;AAED,8DAA8D;AAC9D,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,GAAG,MAAM,CAuBnF;AAED,wFAAwF;AACxF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAqBhG;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAiB5E;AA2BD;iGACiG;AACjG,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC/B,OAAO,CA2BT"}
|
package/dist/schema-compat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
+
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
4
|
const Ajv = AjvModule.default ?? AjvModule;
|
|
4
5
|
/** Creates a configured AJV instance (allErrors, strict: false, with formats).
|
|
5
6
|
* Called once for the module-level instance and once per DefinitionRegistry instance. */
|
|
@@ -269,6 +270,9 @@ export function substituteCelFields(data, schema, rootSchema) {
|
|
|
269
270
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
270
271
|
return celPlaceholderForSchema(resolved);
|
|
271
272
|
}
|
|
273
|
+
if (isTaggedSentinel(data)) {
|
|
274
|
+
return celPlaceholderForSchema(resolved);
|
|
275
|
+
}
|
|
272
276
|
if (Array.isArray(data)) {
|
|
273
277
|
const itemSchema = resolveRef((resolved.items ?? {}), root);
|
|
274
278
|
return data.map((item) => substituteCelFields(item, itemSchema, root));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
2
2
|
/**
|
|
3
3
|
* Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
|
|
4
4
|
* - String: look up the named type in allManifests (Type.JsonSchema resources)
|
|
@@ -6,21 +6,6 @@ import type { ASTNode } from "@marcbachmann/cel-js";
|
|
|
6
6
|
* - Object with `type` or `properties`: raw JSON Schema, return as-is
|
|
7
7
|
*/
|
|
8
8
|
export declare function resolveTypeFieldToSchema(value: unknown, allManifests: Record<string, any>[]): Record<string, any> | undefined;
|
|
9
|
-
/**
|
|
10
|
-
* Extract all member-access chains from a CEL AST.
|
|
11
|
-
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
12
|
-
* Chains that start with a call or non-identifier root are ignored.
|
|
13
|
-
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
14
|
-
*/
|
|
15
|
-
export declare function extractAccessChains(node: ASTNode): string[][];
|
|
16
|
-
/**
|
|
17
|
-
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
18
|
-
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
19
|
-
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
20
|
-
* reach inside an `x-telo-stream: true` property.
|
|
21
|
-
* Returns null when the chain is valid or the schema is too open to judge.
|
|
22
|
-
*/
|
|
23
|
-
export declare function validateChainAgainstSchema(chain: string[], schema: Record<string, any>): string | null;
|
|
24
9
|
/**
|
|
25
10
|
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
|
|
26
11
|
* falls within the scope of a context (e.g. "$.routes[*].inputs").
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF;;;;;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;;;;;;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,3 +1,4 @@
|
|
|
1
|
+
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
1
2
|
/**
|
|
2
3
|
* Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
|
|
3
4
|
* - String: look up the named type in allManifests (Type.JsonSchema resources)
|
|
@@ -29,139 +30,6 @@ export function resolveTypeFieldToSchema(value, allManifests) {
|
|
|
29
30
|
}
|
|
30
31
|
return undefined;
|
|
31
32
|
}
|
|
32
|
-
/**
|
|
33
|
-
* Extract all member-access chains from a CEL AST.
|
|
34
|
-
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
35
|
-
* Chains that start with a call or non-identifier root are ignored.
|
|
36
|
-
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
37
|
-
*/
|
|
38
|
-
export function extractAccessChains(node) {
|
|
39
|
-
const chains = [];
|
|
40
|
-
visitNode(node, chains, new Set());
|
|
41
|
-
return chains;
|
|
42
|
-
}
|
|
43
|
-
// CEL comprehension macros that bind a variable: list.filter(x, ...), list.map(x, ...), etc.
|
|
44
|
-
const COMPREHENSION_METHODS = new Set(["filter", "map", "exists", "all", "exists_one"]);
|
|
45
|
-
function visitNode(node, chains, boundVars) {
|
|
46
|
-
const chain = extractChain(node, boundVars);
|
|
47
|
-
if (chain !== null) {
|
|
48
|
-
chains.push(chain);
|
|
49
|
-
return; // don't recurse into parts of an already-collected chain
|
|
50
|
-
}
|
|
51
|
-
// Comprehension macros bind a variable in their body — handle them specially
|
|
52
|
-
// AST shape: { op: "rcall", args: [methodName, receiver, [boundVarId, body, ...]] }
|
|
53
|
-
if (node.op === "rcall" &&
|
|
54
|
-
Array.isArray(node.args) &&
|
|
55
|
-
typeof node.args[0] === "string" &&
|
|
56
|
-
COMPREHENSION_METHODS.has(node.args[0])) {
|
|
57
|
-
const receiver = node.args[1];
|
|
58
|
-
const comprehensionArgs = node.args[2];
|
|
59
|
-
if (isASTNode(receiver))
|
|
60
|
-
visitNode(receiver, chains, boundVars);
|
|
61
|
-
if (Array.isArray(comprehensionArgs) &&
|
|
62
|
-
comprehensionArgs.length >= 2 &&
|
|
63
|
-
isASTNode(comprehensionArgs[0]) &&
|
|
64
|
-
comprehensionArgs[0].op === "id") {
|
|
65
|
-
const newBoundVars = new Set(boundVars);
|
|
66
|
-
newBoundVars.add(comprehensionArgs[0].args);
|
|
67
|
-
for (let i = 1; i < comprehensionArgs.length; i++) {
|
|
68
|
-
const arg = comprehensionArgs[i];
|
|
69
|
-
if (isASTNode(arg))
|
|
70
|
-
visitNode(arg, chains, newBoundVars);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const args = node.args;
|
|
76
|
-
if (Array.isArray(args)) {
|
|
77
|
-
for (const arg of args) {
|
|
78
|
-
if (isASTNode(arg)) {
|
|
79
|
-
visitNode(arg, chains, boundVars);
|
|
80
|
-
}
|
|
81
|
-
else if (Array.isArray(arg)) {
|
|
82
|
-
for (const item of arg) {
|
|
83
|
-
if (isASTNode(item))
|
|
84
|
-
visitNode(item, chains, boundVars);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function isASTNode(v) {
|
|
91
|
-
return v !== null && typeof v === "object" && "op" in v;
|
|
92
|
-
}
|
|
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. */
|
|
102
|
-
function extractChain(node, boundVars) {
|
|
103
|
-
if (node.op === "id") {
|
|
104
|
-
const name = node.args;
|
|
105
|
-
if (boundVars.has(name))
|
|
106
|
-
return null; // bound by a comprehension macro, not a free access
|
|
107
|
-
return [name];
|
|
108
|
-
}
|
|
109
|
-
if (node.op === ".") {
|
|
110
|
-
const [obj, field] = node.args;
|
|
111
|
-
const parent = extractChain(obj, boundVars);
|
|
112
|
-
if (parent !== null)
|
|
113
|
-
return [...parent, field];
|
|
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
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
125
|
-
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
126
|
-
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
127
|
-
* reach inside an `x-telo-stream: true` property.
|
|
128
|
-
* Returns null when the chain is valid or the schema is too open to judge.
|
|
129
|
-
*/
|
|
130
|
-
export function validateChainAgainstSchema(chain, schema) {
|
|
131
|
-
let current = schema;
|
|
132
|
-
for (let i = 0; i < chain.length; i++) {
|
|
133
|
-
const key = chain[i];
|
|
134
|
-
if (!current || typeof current !== "object")
|
|
135
|
-
return null;
|
|
136
|
-
const props = current.properties;
|
|
137
|
-
if (!props)
|
|
138
|
-
return null;
|
|
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
|
-
}
|
|
152
|
-
// Known property — drill into it even if additionalProperties is true
|
|
153
|
-
current = propSchema;
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
// Unknown property — only flag if schema is closed
|
|
157
|
-
if (current.additionalProperties === true)
|
|
158
|
-
return null;
|
|
159
|
-
const path = chain.slice(0, i + 1).join(".");
|
|
160
|
-
const available = Object.keys(props).join(", ");
|
|
161
|
-
return `'${path}' is not defined (available: ${available})`;
|
|
162
|
-
}
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
33
|
/**
|
|
166
34
|
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
|
|
167
35
|
* falls within the scope of a context (e.g. "$.routes[*].inputs").
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,13 +41,17 @@
|
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
43
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/sdk": "0.7.0"
|
|
44
|
+
"@telorun/sdk": "0.7.0",
|
|
45
|
+
"@telorun/templating": "0.2.1"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^20.0.0",
|
|
48
|
-
"typescript": "^5.0.0"
|
|
49
|
+
"typescript": "^5.0.0",
|
|
50
|
+
"vitest": "^2.1.8"
|
|
49
51
|
},
|
|
50
52
|
"scripts": {
|
|
51
|
-
"build": "tsc -p tsconfig.lib.json"
|
|
53
|
+
"build": "tsc -p tsconfig.lib.json",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest"
|
|
52
56
|
}
|
|
53
57
|
}
|
package/src/analyzer.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
|
+
import { defaultRegistry, walkCelExpressions } from "@telorun/templating";
|
|
3
4
|
import { AliasResolver } from "./alias-resolver.js";
|
|
4
5
|
import { AnalysisRegistry } from "./analysis-registry.js";
|
|
5
6
|
import {
|
|
@@ -21,19 +22,15 @@ import {
|
|
|
21
22
|
} from "./schema-compat.js";
|
|
22
23
|
import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisOptions } from "./types.js";
|
|
23
24
|
import {
|
|
24
|
-
extractAccessChains,
|
|
25
25
|
getManifestItem,
|
|
26
26
|
pathMatchesScope,
|
|
27
27
|
resolveContextAnnotations,
|
|
28
28
|
resolveTypeFieldToSchema,
|
|
29
|
-
validateChainAgainstSchema,
|
|
30
29
|
} from "./validate-cel-context.js";
|
|
31
30
|
import { validateExtends } from "./validate-extends.js";
|
|
32
31
|
import { validateReferences } from "./validate-references.js";
|
|
33
32
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
34
33
|
|
|
35
|
-
const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
|
|
36
|
-
|
|
37
34
|
const SELF_PREFIX = "Self.";
|
|
38
35
|
|
|
39
36
|
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
@@ -69,24 +66,6 @@ function lookupDefinitionTypeField(
|
|
|
69
66
|
return resolveTypeFieldToSchema(value, allManifests);
|
|
70
67
|
}
|
|
71
68
|
|
|
72
|
-
function walkCelExpressions(
|
|
73
|
-
value: unknown,
|
|
74
|
-
path: string,
|
|
75
|
-
cb: (expr: string, path: string) => void,
|
|
76
|
-
): void {
|
|
77
|
-
if (typeof value === "string") {
|
|
78
|
-
for (const m of value.matchAll(TEMPLATE_REGEX)) {
|
|
79
|
-
cb(m[1].trim(), path);
|
|
80
|
-
}
|
|
81
|
-
} else if (Array.isArray(value)) {
|
|
82
|
-
value.forEach((v, i) => walkCelExpressions(v, `${path}[${i}]`, cb));
|
|
83
|
-
} else if (value !== null && typeof value === "object") {
|
|
84
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
85
|
-
walkCelExpressions(v, path ? `${path}.${k}` : k, cb);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
69
|
const SOURCE = "telo-analyzer";
|
|
91
70
|
|
|
92
71
|
/**
|
|
@@ -253,35 +232,37 @@ function buildStepContextSchema(
|
|
|
253
232
|
if (!step || typeof step !== "object") continue;
|
|
254
233
|
const s = step as Record<string, any>;
|
|
255
234
|
const name = s.name;
|
|
256
|
-
|
|
257
|
-
|
|
235
|
+
const invoke = s[invokeField] as Record<string, any> | undefined;
|
|
236
|
+
// Only invoke steps register a `steps.<name>.result` entry — control-flow
|
|
237
|
+
// wrappers (try/if/while/switch/throw) don't produce a result and must
|
|
238
|
+
// not shadow real entries with a permissive `additionalProperties: true`,
|
|
239
|
+
// or unknown step references slip through chain validation.
|
|
240
|
+
if (typeof name === "string" && invoke && typeof invoke === "object") {
|
|
258
241
|
let outputSchema: Record<string, any> | undefined;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
(m)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
outputSchema = resolveTypeFieldToSchema(invokedManifest[outputTypeField], allManifests);
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
|
|
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
|
-
);
|
|
242
|
+
const invokedKind = invoke.kind as string | undefined;
|
|
243
|
+
const invokedName = invoke.name as string | undefined;
|
|
244
|
+
if (invokedName) {
|
|
245
|
+
const invokedManifest = allManifests.find(
|
|
246
|
+
(m) =>
|
|
247
|
+
(m.metadata as any)?.name === invokedName &&
|
|
248
|
+
(!invokedKind || m.kind === invokedKind),
|
|
249
|
+
) as Record<string, any> | undefined;
|
|
250
|
+
if (invokedManifest) {
|
|
251
|
+
outputSchema = resolveTypeFieldToSchema(invokedManifest[outputTypeField], allManifests);
|
|
284
252
|
}
|
|
253
|
+
} else {
|
|
254
|
+
outputSchema = resolveTypeFieldToSchema(invoke[outputTypeField], allManifests);
|
|
255
|
+
}
|
|
256
|
+
// Fallback: pull outputType from the kind's Telo.Definition. The
|
|
257
|
+
// resource manifest typically doesn't carry outputType; the def does.
|
|
258
|
+
if (!outputSchema && invokedKind) {
|
|
259
|
+
outputSchema = lookupDefinitionTypeField(
|
|
260
|
+
invokedKind,
|
|
261
|
+
outputTypeField,
|
|
262
|
+
defs,
|
|
263
|
+
aliases,
|
|
264
|
+
allManifests,
|
|
265
|
+
);
|
|
285
266
|
}
|
|
286
267
|
stepProperties[name] = {
|
|
287
268
|
type: "object",
|
|
@@ -651,31 +632,17 @@ export class StaticAnalyzer {
|
|
|
651
632
|
)
|
|
652
633
|
: undefined;
|
|
653
634
|
|
|
654
|
-
walkCelExpressions(m, "", (expr, path) => {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
severity: DiagnosticSeverity.Error,
|
|
661
|
-
code: "CEL_SYNTAX_ERROR",
|
|
662
|
-
source: SOURCE,
|
|
663
|
-
message: `CEL syntax error at ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
664
|
-
data: { resource, filePath, path },
|
|
665
|
-
});
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
const accessChains = extractAccessChains(parsed.ast);
|
|
670
|
-
|
|
635
|
+
walkCelExpressions(m, "", (expr, path, engineName) => {
|
|
636
|
+
// Resolve the effective context for this expression's path. The
|
|
637
|
+
// engine receives a single closed schema and validates member-access
|
|
638
|
+
// chains against it; per-path resolution (step context, x-telo-context,
|
|
639
|
+
// kernel-globals merge) stays on the analyzer side because it depends
|
|
640
|
+
// on analyzer-internal state (definitions, aliases).
|
|
671
641
|
const contexts = mDefinition?.schema ? extractContextsFromSchema(mDefinition.schema) : [];
|
|
672
642
|
const invocationContext = (m.metadata as any)?.xTeloInvocationContext as
|
|
673
643
|
| Record<string, any>
|
|
674
644
|
| undefined;
|
|
675
645
|
|
|
676
|
-
// If no static context but we have step context, inject it
|
|
677
|
-
if (contexts.length === 0 && !invocationContext && !stepContextSchema) return;
|
|
678
|
-
|
|
679
646
|
let matchedContext: Record<string, any> | undefined;
|
|
680
647
|
let matchedScope: string | undefined;
|
|
681
648
|
for (const ctx of contexts) {
|
|
@@ -687,7 +654,6 @@ export class StaticAnalyzer {
|
|
|
687
654
|
}
|
|
688
655
|
if (!matchedContext) matchedContext = invocationContext;
|
|
689
656
|
|
|
690
|
-
// Merge step context into the effective context
|
|
691
657
|
if (stepContextSchema) {
|
|
692
658
|
const base = matchedContext ?? { type: "object", properties: {}, additionalProperties: true };
|
|
693
659
|
matchedContext = {
|
|
@@ -699,28 +665,51 @@ export class StaticAnalyzer {
|
|
|
699
665
|
};
|
|
700
666
|
}
|
|
701
667
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
668
|
+
let effectiveContext: Record<string, any> | null = null;
|
|
669
|
+
if (matchedContext) {
|
|
670
|
+
const manifestItem = matchedScope
|
|
671
|
+
? getManifestItem(path, matchedScope, m as Record<string, any>)
|
|
672
|
+
: (m as Record<string, any>);
|
|
673
|
+
const resolvedContext = resolveContextAnnotations(
|
|
674
|
+
matchedContext,
|
|
675
|
+
manifestItem,
|
|
676
|
+
allManifests as Record<string, any>[],
|
|
677
|
+
);
|
|
678
|
+
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const engine = defaultRegistry().get(engineName);
|
|
682
|
+
if (!engine) return;
|
|
683
|
+
const findings = engine.analyze(expr, { celEnv: this.celEnv, contextSchema: effectiveContext });
|
|
684
|
+
for (const f of findings) {
|
|
685
|
+
if (f.code === "CEL_SYNTAX_ERROR") {
|
|
686
|
+
diagnostics.push({
|
|
687
|
+
severity: DiagnosticSeverity.Error,
|
|
688
|
+
code: "CEL_SYNTAX_ERROR",
|
|
689
|
+
source: SOURCE,
|
|
690
|
+
message: `CEL syntax error at ${path}: ${f.message}`,
|
|
691
|
+
data: { resource, filePath, path },
|
|
692
|
+
});
|
|
693
|
+
} else if (f.code === "CEL_UNKNOWN_FIELD") {
|
|
694
|
+
diagnostics.push({
|
|
695
|
+
severity: DiagnosticSeverity.Error,
|
|
696
|
+
code: "CEL_UNKNOWN_FIELD",
|
|
697
|
+
source: SOURCE,
|
|
698
|
+
message: `${m.kind}/${resource.name}: CEL at '${path}': ${f.message}`,
|
|
699
|
+
data: { resource, filePath, path },
|
|
700
|
+
});
|
|
701
|
+
} else {
|
|
702
|
+
// Unknown code from a future engine — pass the message through,
|
|
703
|
+
// tagged with a generic ENGINE_DIAGNOSTIC code so downstream
|
|
704
|
+
// filters can still bucket it.
|
|
705
|
+
diagnostics.push({
|
|
706
|
+
severity: DiagnosticSeverity.Error,
|
|
707
|
+
code: f.code ?? "ENGINE_DIAGNOSTIC",
|
|
708
|
+
source: SOURCE,
|
|
709
|
+
message: `${m.kind}/${resource.name}: !${engineName} at '${path}': ${f.message}`,
|
|
710
|
+
data: { resource, filePath, path },
|
|
711
|
+
});
|
|
712
|
+
}
|
|
724
713
|
}
|
|
725
714
|
});
|
|
726
715
|
}
|
package/src/cel-environment.ts
CHANGED
|
@@ -1,54 +1,9 @@
|
|
|
1
1
|
import { Environment } from "@marcbachmann/cel-js";
|
|
2
|
-
import {
|
|
2
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
3
3
|
import { jsonSchemaToCelType } from "./schema-compat.js";
|
|
4
4
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
json: (value: unknown) => string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const stub = (name: string) => () => {
|
|
11
|
-
throw new Error(
|
|
12
|
-
`${name}() is not available in this environment. ` +
|
|
13
|
-
`Construct StaticAnalyzer or Loader with celHandlers to enable it.`,
|
|
14
|
-
);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const STUB_HANDLERS: CelHandlers = {
|
|
18
|
-
sha256: stub("sha256"),
|
|
19
|
-
json: stub("json"),
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/** Build a CEL `Environment` with Telo's stdlib of functions. Always registers the
|
|
23
|
-
* same function signatures (so `env.check()` succeeds for type-inference) — the
|
|
24
|
-
* handlers govern what the function does when called at runtime. Analyzer-only
|
|
25
|
-
* callers can omit handlers; runtime callers (kernel) must supply real ones.
|
|
26
|
-
*
|
|
27
|
-
* Also registers the `Stream` object type, backed by the `Stream` class from
|
|
28
|
-
* `@telorun/sdk`. CEL's type-checker rejects values whose constructor isn't
|
|
29
|
-
* Object/Map/Array/Set/registered; producers that need to expose an
|
|
30
|
-
* `AsyncIterable` through a stream-typed property must wrap the iterable in
|
|
31
|
-
* `new Stream(...)` so its constructor is the registered class. The type has
|
|
32
|
-
* no fields, so terminal access (passing the value through CEL) succeeds but
|
|
33
|
-
* member access raises a CEL error at runtime — matching the analyzer's
|
|
34
|
-
* static check on `x-telo-stream`-marked properties. */
|
|
35
|
-
export function buildCelEnvironment(handlers: CelHandlers = STUB_HANDLERS): Environment {
|
|
36
|
-
return new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true })
|
|
37
|
-
.registerFunction("join(list, string): string", (list: unknown[], sep: string) =>
|
|
38
|
-
list.map(String).join(sep),
|
|
39
|
-
)
|
|
40
|
-
.registerFunction("keys(map): list", (map: unknown) => {
|
|
41
|
-
if (map instanceof Map) return [...map.keys()];
|
|
42
|
-
return Object.keys(map as Record<string, unknown>);
|
|
43
|
-
})
|
|
44
|
-
.registerFunction("values(map): list", (map: unknown) => {
|
|
45
|
-
if (map instanceof Map) return [...map.values()];
|
|
46
|
-
return Object.values(map as Record<string, unknown>);
|
|
47
|
-
})
|
|
48
|
-
.registerFunction("sha256(string): string", (s: string) => handlers.sha256(s))
|
|
49
|
-
.registerFunction("json(dyn): string", (value: unknown) => handlers.json(value))
|
|
50
|
-
.registerType("Stream", Stream as unknown as new (...args: unknown[]) => unknown);
|
|
51
|
-
}
|
|
5
|
+
export { buildCelEnvironment } from "@telorun/templating";
|
|
6
|
+
export type { CelHandlers } from "@telorun/templating";
|
|
52
7
|
|
|
53
8
|
/** Clone `baseEnv` and register typed variable declarations so that
|
|
54
9
|
* `env.check(expr)` can infer return types for expressions referencing known variables.
|
package/src/manifest-loader.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
2
2
|
import { isCompiledValue, type ResourceManifest } from "@telorun/sdk";
|
|
3
|
+
import { defaultCustomTags } from "@telorun/templating";
|
|
3
4
|
import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from "yaml";
|
|
4
5
|
import { HttpSource } from "./sources/http-source.js";
|
|
5
6
|
import { RegistrySource } from "./sources/registry-source.js";
|
|
@@ -74,7 +75,7 @@ export class Loader {
|
|
|
74
75
|
return cloneManifestArray(cached.manifests);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
const parsedDocuments = parseAllDocuments(text);
|
|
78
|
+
const parsedDocuments = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
78
79
|
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
79
80
|
const offsets = documentLineOffsets(text);
|
|
80
81
|
const lineOffsets = buildLineOffsets(text);
|
|
@@ -194,7 +195,7 @@ export class Loader {
|
|
|
194
195
|
): Promise<ResourceManifest[]> {
|
|
195
196
|
const { text, source } = await this.pick(url).read(url);
|
|
196
197
|
|
|
197
|
-
const parsedDocuments = parseAllDocuments(text);
|
|
198
|
+
const parsedDocuments = parseAllDocuments(text, { customTags: defaultCustomTags() });
|
|
198
199
|
const rawDocs = parsedDocuments.map((d) => d.toJSON());
|
|
199
200
|
const offsets = documentLineOffsets(text);
|
|
200
201
|
const lineOffsets = buildLineOffsets(text);
|
package/src/precompile.ts
CHANGED
|
@@ -1,17 +1,43 @@
|
|
|
1
|
-
import type { CompiledValue } from "@telorun/sdk";
|
|
2
1
|
import type { Environment } from "@marcbachmann/cel-js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const EXACT_TEMPLATE_REGEX = /^\s*\$\{\{\s*([^}]+?)\s*\}\}\s*$/;
|
|
2
|
+
import { isCompiledValue } from "@telorun/sdk";
|
|
3
|
+
import { compileString, defaultRegistry, isTaggedSentinel } from "@telorun/templating";
|
|
6
4
|
|
|
7
5
|
/**
|
|
8
|
-
* Walks a raw YAML document and replaces all
|
|
9
|
-
* CompiledValue wrappers. Throws on CEL syntax
|
|
10
|
-
* Intended to be called once per document at load time.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* Walks a raw YAML document and replaces all `${{ expr }}` strings (and
|
|
7
|
+
* `!cel`-tagged sentinels) with CompiledValue wrappers. Throws on CEL syntax
|
|
8
|
+
* errors. Intended to be called once per document at load time.
|
|
9
|
+
*
|
|
10
|
+
* Note on Telo.Definition / Telo.Abstract: the walker traverses these too.
|
|
11
|
+
* Their `schema` fields are JSON Schema metadata and don't typically contain
|
|
12
|
+
* `${{ }}` text, so compile is a no-op there. Their `template` fields, on
|
|
13
|
+
* the other hand, *do* carry CEL — Definition-driven templates are expanded
|
|
14
|
+
* by the kernel and rely on the precompiled tree. If a description or
|
|
15
|
+
* example string inside a schema happens to contain `${{ }}`, it will be
|
|
16
|
+
* interpreted as CEL; tag it `!literal` to opt out.
|
|
13
17
|
*/
|
|
14
18
|
export function precompileDoc(doc: unknown, env: Environment): unknown {
|
|
19
|
+
// Tagged sentinel: dispatch to the engine. The result is decorated with
|
|
20
|
+
// `__tagged` + `engine` + `source` when it's a CompiledValue so the
|
|
21
|
+
// analyzer's diagnostic walk can identify it on compiled trees too;
|
|
22
|
+
// engines returning plain values (e.g. `literal` → a string) pass through
|
|
23
|
+
// verbatim — the runtime contract is "any scalar value is fine."
|
|
24
|
+
if (isTaggedSentinel(doc)) {
|
|
25
|
+
const engine = defaultRegistry().get(doc.engine);
|
|
26
|
+
if (!engine) {
|
|
27
|
+
throw new Error(`Unknown templating engine: !${doc.engine}`);
|
|
28
|
+
}
|
|
29
|
+
const compiled = engine.compile(doc.source, { celEnv: env });
|
|
30
|
+
if (isCompiledValue(compiled)) {
|
|
31
|
+
return {
|
|
32
|
+
__tagged: true,
|
|
33
|
+
__compiled: true,
|
|
34
|
+
engine: doc.engine,
|
|
35
|
+
source: doc.source,
|
|
36
|
+
call: compiled.call.bind(compiled),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return compiled;
|
|
40
|
+
}
|
|
15
41
|
if (typeof doc === "string") return compileString(doc, env);
|
|
16
42
|
if (Array.isArray(doc)) return doc.map((item) => precompileDoc(item, env));
|
|
17
43
|
// Only recurse into plain objects. Class instances (ResourceInstance, ScopeHandle, etc.)
|
|
@@ -25,33 +51,3 @@ export function precompileDoc(doc: unknown, env: Environment): unknown {
|
|
|
25
51
|
}
|
|
26
52
|
return doc;
|
|
27
53
|
}
|
|
28
|
-
|
|
29
|
-
function compileString(s: string, env: Environment): unknown {
|
|
30
|
-
if (!s.includes("${{")) return s;
|
|
31
|
-
|
|
32
|
-
const exact = s.match(EXACT_TEMPLATE_REGEX);
|
|
33
|
-
if (exact) {
|
|
34
|
-
const expr = exact[1].trim();
|
|
35
|
-
const fn = env.parse(expr);
|
|
36
|
-
return { __compiled: true, source: expr, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Interpolated template — collect literal parts + compiled sub-expressions
|
|
40
|
-
const parts: Array<string | CompiledValue> = [];
|
|
41
|
-
let last = 0;
|
|
42
|
-
for (const m of s.matchAll(TEMPLATE_REGEX)) {
|
|
43
|
-
if (m.index! > last) parts.push(s.slice(last, m.index));
|
|
44
|
-
const expr = m[1].trim();
|
|
45
|
-
const fn = env.parse(expr);
|
|
46
|
-
parts.push({ __compiled: true, source: expr, call: (ctx: Record<string, unknown>) => fn(ctx) } satisfies CompiledValue);
|
|
47
|
-
last = m.index! + m[0].length;
|
|
48
|
-
}
|
|
49
|
-
if (last < s.length) parts.push(s.slice(last));
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
__compiled: true,
|
|
53
|
-
source: s,
|
|
54
|
-
call: (ctx: Record<string, unknown>) =>
|
|
55
|
-
parts.map((p) => (typeof p === "string" ? p : String(p.call(ctx) ?? ""))).join(""),
|
|
56
|
-
} satisfies CompiledValue;
|
|
57
|
-
}
|
package/src/schema-compat.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import AjvModule from "ajv";
|
|
2
2
|
import addFormats from "ajv-formats";
|
|
3
|
+
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
4
|
|
|
4
5
|
const Ajv = (AjvModule as any).default ?? AjvModule;
|
|
5
6
|
|
|
@@ -300,6 +301,9 @@ export function substituteCelFields(
|
|
|
300
301
|
if (typeof data === "string" && CEL_PURE_RE.test(data)) {
|
|
301
302
|
return celPlaceholderForSchema(resolved);
|
|
302
303
|
}
|
|
304
|
+
if (isTaggedSentinel(data)) {
|
|
305
|
+
return celPlaceholderForSchema(resolved);
|
|
306
|
+
}
|
|
303
307
|
if (Array.isArray(data)) {
|
|
304
308
|
const itemSchema = resolveRef((resolved.items ?? {}) as Record<string, any>, root);
|
|
305
309
|
return data.map((item) => substituteCelFields(item, itemSchema, root));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export { extractAccessChains, validateChainAgainstSchema } from "@telorun/templating";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Resolve a type field value (string name, inline type, or raw schema) to a JSON Schema.
|
|
@@ -40,147 +40,6 @@ export function resolveTypeFieldToSchema(
|
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
* Extract all member-access chains from a CEL AST.
|
|
45
|
-
* Returns arrays like ["request", "query", "name"] for `request.query.name`.
|
|
46
|
-
* Chains that start with a call or non-identifier root are ignored.
|
|
47
|
-
* Bound variables in comprehension macros (filter, map, exists, all, exists_one) are excluded.
|
|
48
|
-
*/
|
|
49
|
-
export function extractAccessChains(node: ASTNode): string[][] {
|
|
50
|
-
const chains: string[][] = [];
|
|
51
|
-
visitNode(node, chains, new Set());
|
|
52
|
-
return chains;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// CEL comprehension macros that bind a variable: list.filter(x, ...), list.map(x, ...), etc.
|
|
56
|
-
const COMPREHENSION_METHODS = new Set(["filter", "map", "exists", "all", "exists_one"]);
|
|
57
|
-
|
|
58
|
-
function visitNode(node: ASTNode, chains: string[][], boundVars: Set<string>): void {
|
|
59
|
-
const chain = extractChain(node, boundVars);
|
|
60
|
-
if (chain !== null) {
|
|
61
|
-
chains.push(chain);
|
|
62
|
-
return; // don't recurse into parts of an already-collected chain
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Comprehension macros bind a variable in their body — handle them specially
|
|
66
|
-
// AST shape: { op: "rcall", args: [methodName, receiver, [boundVarId, body, ...]] }
|
|
67
|
-
if (
|
|
68
|
-
node.op === "rcall" &&
|
|
69
|
-
Array.isArray(node.args) &&
|
|
70
|
-
typeof node.args[0] === "string" &&
|
|
71
|
-
COMPREHENSION_METHODS.has(node.args[0])
|
|
72
|
-
) {
|
|
73
|
-
const receiver = node.args[1];
|
|
74
|
-
const comprehensionArgs = node.args[2];
|
|
75
|
-
if (isASTNode(receiver)) visitNode(receiver, chains, boundVars);
|
|
76
|
-
if (
|
|
77
|
-
Array.isArray(comprehensionArgs) &&
|
|
78
|
-
comprehensionArgs.length >= 2 &&
|
|
79
|
-
isASTNode(comprehensionArgs[0]) &&
|
|
80
|
-
(comprehensionArgs[0] as ASTNode).op === "id"
|
|
81
|
-
) {
|
|
82
|
-
const newBoundVars = new Set(boundVars);
|
|
83
|
-
newBoundVars.add((comprehensionArgs[0] as ASTNode).args as string);
|
|
84
|
-
for (let i = 1; i < comprehensionArgs.length; i++) {
|
|
85
|
-
const arg = comprehensionArgs[i];
|
|
86
|
-
if (isASTNode(arg)) visitNode(arg as ASTNode, chains, newBoundVars);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const args = node.args;
|
|
93
|
-
if (Array.isArray(args)) {
|
|
94
|
-
for (const arg of args) {
|
|
95
|
-
if (isASTNode(arg)) {
|
|
96
|
-
visitNode(arg, chains, boundVars);
|
|
97
|
-
} else if (Array.isArray(arg)) {
|
|
98
|
-
for (const item of arg) {
|
|
99
|
-
if (isASTNode(item)) visitNode(item, chains, boundVars);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function isASTNode(v: unknown): v is ASTNode {
|
|
107
|
-
return v !== null && typeof v === "object" && "op" in (v as object);
|
|
108
|
-
}
|
|
109
|
-
|
|
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. */
|
|
120
|
-
function extractChain(node: ASTNode, boundVars: Set<string>): string[] | null {
|
|
121
|
-
if (node.op === "id") {
|
|
122
|
-
const name = node.args as string;
|
|
123
|
-
if (boundVars.has(name)) return null; // bound by a comprehension macro, not a free access
|
|
124
|
-
return [name];
|
|
125
|
-
}
|
|
126
|
-
if (node.op === ".") {
|
|
127
|
-
const [obj, field] = node.args as [ASTNode, string];
|
|
128
|
-
const parent = extractChain(obj, boundVars);
|
|
129
|
-
if (parent !== null) return [...parent, field];
|
|
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
|
-
}
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Check whether a member-access chain accesses only fields declared in a JSON Schema.
|
|
141
|
-
* Returns an error string if a field is unknown in a schema that declares explicit
|
|
142
|
-
* properties without `additionalProperties: true`, or if the chain attempts to
|
|
143
|
-
* reach inside an `x-telo-stream: true` property.
|
|
144
|
-
* Returns null when the chain is valid or the schema is too open to judge.
|
|
145
|
-
*/
|
|
146
|
-
export function validateChainAgainstSchema(
|
|
147
|
-
chain: string[],
|
|
148
|
-
schema: Record<string, any>,
|
|
149
|
-
): string | null {
|
|
150
|
-
let current: Record<string, any> = schema;
|
|
151
|
-
for (let i = 0; i < chain.length; i++) {
|
|
152
|
-
const key = chain[i]!;
|
|
153
|
-
if (!current || typeof current !== "object") return null;
|
|
154
|
-
const props: Record<string, any> | undefined = current.properties;
|
|
155
|
-
if (!props) return null;
|
|
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
|
-
}
|
|
171
|
-
// Known property — drill into it even if additionalProperties is true
|
|
172
|
-
current = propSchema;
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
// Unknown property — only flag if schema is closed
|
|
176
|
-
if (current.additionalProperties === true) return null;
|
|
177
|
-
const path = chain.slice(0, i + 1).join(".");
|
|
178
|
-
const available = Object.keys(props).join(", ");
|
|
179
|
-
return `'${path}' is not defined (available: ${available})`;
|
|
180
|
-
}
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
43
|
/**
|
|
185
44
|
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
|
|
186
45
|
* falls within the scope of a context (e.g. "$.routes[*].inputs").
|