@telorun/templating 0.11.1 → 0.13.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 +5 -3
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +3 -0
- package/dist/cel/catalog.d.ts.map +1 -1
- package/dist/cel/catalog.js +27 -0
- package/dist/cel/diagnose.d.ts +50 -0
- package/dist/cel/diagnose.d.ts.map +1 -0
- package/dist/cel/diagnose.js +223 -0
- package/dist/cel/environment.d.ts +7 -0
- package/dist/cel/environment.d.ts.map +1 -1
- package/dist/cel/environment.js +19 -5
- package/dist/cel/walk.d.ts +17 -1
- package/dist/cel/walk.d.ts.map +1 -1
- package/dist/cel/walk.js +21 -3
- package/dist/engine.d.ts +91 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engines/cel.d.ts +13 -9
- package/dist/engines/cel.d.ts.map +1 -1
- package/dist/engines/cel.js +86 -28
- package/dist/engines/include.d.ts +25 -0
- package/dist/engines/include.d.ts.map +1 -0
- package/dist/engines/include.js +132 -0
- package/dist/engines/literal.js +1 -1
- package/dist/engines/ref.js +1 -1
- package/dist/engines/sql.d.ts.map +1 -1
- package/dist/engines/sql.js +32 -5
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +6 -1
- package/dist/sentinel.d.ts +19 -0
- package/dist/sentinel.d.ts.map +1 -1
- package/dist/sentinel.js +22 -0
- package/package.json +2 -2
- package/src/builtins.ts +3 -0
- package/src/cel/catalog.ts +27 -0
- package/src/cel/diagnose.ts +293 -0
- package/src/cel/environment.ts +21 -5
- package/src/cel/walk.ts +37 -4
- package/src/engine.ts +96 -3
- package/src/engines/cel.ts +91 -31
- package/src/engines/include.ts +153 -0
- package/src/engines/literal.ts +1 -1
- package/src/engines/ref.ts +1 -1
- package/src/engines/sql.ts +41 -7
- package/src/index.ts +28 -3
- package/src/manifest-schemas.ts +6 -1
- package/src/sentinel.ts +29 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cel.d.ts","sourceRoot":"","sources":["../../src/engines/cel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cel.d.ts","sourceRoot":"","sources":["../../src/engines/cel.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAoB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAElG;;;;;;;;;;2BAU2B;AAC3B,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,CAgFnF;AAYD;yCACyC;AACzC,eAAO,MAAM,SAAS,EAAE,gBAWvB,CAAC"}
|
package/dist/engines/cel.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { extractAccessChains, findNullableAccessIssues, validateChainAgainstSchema, } from "../cel/analyze.js";
|
|
2
2
|
import { compileExpression } from "../cel/compile.js";
|
|
3
|
+
import { auditCalls, explainUnresolved } from "../cel/diagnose.js";
|
|
3
4
|
/** Statically analyze one CEL expression against the effective context schema:
|
|
4
|
-
* parse →
|
|
5
|
-
* access. Single source of truth shared by the `!cel` engine
|
|
6
|
-
* and the `!sql` engine (one per `${{ }}` interpolation), so
|
|
7
|
-
* can't drift between them.
|
|
5
|
+
* parse → classify every call → type-check → validate member-access chains →
|
|
6
|
+
* flag nullable access. Single source of truth shared by the `!cel` engine
|
|
7
|
+
* (one expression) and the `!sql` engine (one per `${{ }}` interpolation), so
|
|
8
|
+
* diagnostic wording can't drift between them.
|
|
9
|
+
*
|
|
10
|
+
* The type-check lives here, not in the analyzer, so one expression produces
|
|
11
|
+
* one verdict against one environment. Splitting them let an opaque
|
|
12
|
+
* "no matching overload" survive next to the diagnostic that actually
|
|
13
|
+
* explained it, and left `${{ }}` interpolations chain-validated but never
|
|
14
|
+
* type-checked at all. */
|
|
8
15
|
export function analyzeCelExpression(source, env) {
|
|
9
16
|
const out = [];
|
|
10
17
|
let parsed;
|
|
@@ -12,35 +19,86 @@ export function analyzeCelExpression(source, env) {
|
|
|
12
19
|
parsed = env.celEnv.parse(source);
|
|
13
20
|
}
|
|
14
21
|
catch (e) {
|
|
15
|
-
|
|
16
|
-
code: "CEL_SYNTAX_ERROR",
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
return out;
|
|
22
|
+
return {
|
|
23
|
+
diagnostics: [{ code: "CEL_SYNTAX_ERROR", message: e instanceof Error ? e.message : String(e) }],
|
|
24
|
+
calls: [],
|
|
25
|
+
};
|
|
20
26
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
27
|
-
|
|
27
|
+
const audit = auditCalls(source, parsed.ast, env.celEnv);
|
|
28
|
+
let type;
|
|
29
|
+
let checkError;
|
|
30
|
+
try {
|
|
31
|
+
const result = env.celEnv.check(source);
|
|
32
|
+
if (result.valid)
|
|
33
|
+
type = result.type;
|
|
34
|
+
else if (result.error) {
|
|
35
|
+
checkError = String(result.error.message ?? result.error)
|
|
36
|
+
.split("\n")[0]
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// The checker is now the ONLY type verdict for every CEL expression, so a
|
|
42
|
+
// crash here silently retires static typing for that expression. Report it
|
|
43
|
+
// instead: degrading is acceptable, degrading invisibly is not.
|
|
44
|
+
return {
|
|
45
|
+
diagnostics: [
|
|
46
|
+
{
|
|
47
|
+
code: "CEL_TYPE_ERROR",
|
|
48
|
+
message: `the CEL type-checker failed on this expression: ${e instanceof Error ? e.message : String(e)}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
calls: audit.calls,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// The audit only ever EXPLAINS a rejection — it never overrules acceptance.
|
|
55
|
+
// Its classification is decided from the registry, so a call cel-js accepts
|
|
56
|
+
// but the registry cannot account for (a macro the parser expands and the
|
|
57
|
+
// registry never sees, which a cel-js upgrade can introduce at any time) must
|
|
58
|
+
// not become a hard error on valid CEL. Reporting nothing where cel-js is
|
|
59
|
+
// happy makes an unknown future macro a silent no-op rather than a manifest
|
|
60
|
+
// this analyzer refuses and the kernel would run fine.
|
|
61
|
+
if (checkError !== undefined) {
|
|
62
|
+
// `check()` stops at its first problem; the audit enumerates every bad call,
|
|
63
|
+
// which is the whole reason it exists as more than a message rewriter.
|
|
64
|
+
out.push(...audit.diagnostics);
|
|
65
|
+
if (audit.diagnostics.length === 0) {
|
|
66
|
+
out.push({
|
|
67
|
+
code: "CEL_TYPE_ERROR",
|
|
68
|
+
message: checkError + explainUnresolved(audit.unresolved, env.celEnv) + DYN_HINT(checkError),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
28
71
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
72
|
+
if (env.contextSchema) {
|
|
73
|
+
const contextSchema = env.contextSchema;
|
|
74
|
+
for (const chain of extractAccessChains(parsed.ast)) {
|
|
75
|
+
const err = validateChainAgainstSchema(chain, contextSchema);
|
|
76
|
+
if (err)
|
|
77
|
+
out.push({ code: "CEL_UNKNOWN_FIELD", message: err });
|
|
78
|
+
}
|
|
79
|
+
for (const issue of findNullableAccessIssues(parsed.ast, contextSchema)) {
|
|
80
|
+
// Index access (member "[index]") attaches without a dot; a named field
|
|
81
|
+
// attaches with one — so the suggested CEL stays valid either way.
|
|
82
|
+
const access = issue.member === "[index]" ? issue.member : `.${issue.member}`;
|
|
83
|
+
out.push({
|
|
84
|
+
code: "CEL_NULLABLE_ACCESS",
|
|
85
|
+
message: `'${issue.path}' may be null — guard it (e.g. '${issue.path} != null && …' or '${issue.path} == null ? … : ${issue.path}${access}') before accessing '${access}'`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
37
88
|
}
|
|
38
|
-
return out;
|
|
89
|
+
return { diagnostics: out, calls: audit.calls, ...(type === undefined ? {} : { type }) };
|
|
39
90
|
}
|
|
91
|
+
/** `dyn` in a checker message means an operand whose type is unknown here —
|
|
92
|
+
* almost always a step result whose invoked resource declares no
|
|
93
|
+
* `outputType:`. Without this, the reader takes `dyn` for a cast problem. */
|
|
94
|
+
const DYN_HINT = (message) =>
|
|
95
|
+
// Word-bounded: "dynamic" appears in unrelated checker messages, and the
|
|
96
|
+
// hint is wrong for those.
|
|
97
|
+
/\bdyn\b/.test(message)
|
|
98
|
+
? " (`dyn` is a value with no static type here — declare `outputType:` on the resource producing it, or convert at the call site.)"
|
|
99
|
+
: "";
|
|
40
100
|
/** The `!cel` engine. Treats the entire tagged scalar as a single CEL
|
|
41
|
-
* expression — no `${{ }}` wrapping.
|
|
42
|
-
* as the untagged path: parse → extract member-access chains → validate each
|
|
43
|
-
* chain against the effective context schema. */
|
|
101
|
+
* expression — no `${{ }}` wrapping. */
|
|
44
102
|
export const celEngine = {
|
|
45
103
|
name: "cel",
|
|
46
104
|
language: "cel",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EngineDiagnostic, TemplatingEngine } from "../engine.js";
|
|
2
|
+
export interface NormalizedIncludePath {
|
|
3
|
+
/** Module-root-relative path with `./` and `.` segments folded out, `/`
|
|
4
|
+
* separated. Absent when the source is not a usable path. */
|
|
5
|
+
readonly path?: string;
|
|
6
|
+
readonly diagnostic?: EngineDiagnostic;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Normalize an `!include-*` source to a module-root-relative path, or explain
|
|
10
|
+
* why it is not one.
|
|
11
|
+
*
|
|
12
|
+
* Pure string work, deliberately: this runs in the analyzer, which must load in
|
|
13
|
+
* a browser, so confinement is decided from the written path alone and never by
|
|
14
|
+
* asking a filesystem where it lands. That is possible because the path is
|
|
15
|
+
* root-relative by definition — the module root is the one directory every path
|
|
16
|
+
* is measured from, so `..` below depth zero is an escape regardless of where
|
|
17
|
+
* the module happens to sit on disk.
|
|
18
|
+
*/
|
|
19
|
+
export declare function normalizeIncludePath(source: string): NormalizedIncludePath;
|
|
20
|
+
/** Embeds a file's contents as a UTF-8 string. */
|
|
21
|
+
export declare const includeTextEngine: TemplatingEngine;
|
|
22
|
+
/** Embeds a file's contents as raw bytes — a `Uint8Array`, the shape every
|
|
23
|
+
* `x-telo-binary` slot accepts. */
|
|
24
|
+
export declare const includeBytesEngine: TemplatingEngine;
|
|
25
|
+
//# sourceMappingURL=include.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../../src/engines/include.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAaxF,MAAM,WAAW,qBAAqB;IACpC;kEAC8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC;CACxC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,qBAAqB,CA0E1E;AA0CD,kDAAkD;AAClD,eAAO,MAAM,iBAAiB,EAAE,gBAAqD,CAAC;AAEtF;oCACoC;AACpC,eAAO,MAAM,kBAAkB,EAAE,gBAAsD,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { INCLUDE_BYTES_ENGINE, INCLUDE_TEXT_ENGINE, makeTaggedSentinel } from "../sentinel.js";
|
|
2
|
+
/** Characters that would make a path a pattern rather than a name. Globs are
|
|
3
|
+
* excluded deliberately: a claim has to name one file for publish to place it
|
|
4
|
+
* in a layer, and a pattern that matches nothing on the publishing machine
|
|
5
|
+
* would ship an artifact missing a file the manifest reads. */
|
|
6
|
+
const GLOB_CHARS = /[*?[\]{}]/;
|
|
7
|
+
/** `scheme:` prefix — a URL, or a Windows drive letter. Either way not a
|
|
8
|
+
* module-relative path. */
|
|
9
|
+
const URI_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
10
|
+
/**
|
|
11
|
+
* Normalize an `!include-*` source to a module-root-relative path, or explain
|
|
12
|
+
* why it is not one.
|
|
13
|
+
*
|
|
14
|
+
* Pure string work, deliberately: this runs in the analyzer, which must load in
|
|
15
|
+
* a browser, so confinement is decided from the written path alone and never by
|
|
16
|
+
* asking a filesystem where it lands. That is possible because the path is
|
|
17
|
+
* root-relative by definition — the module root is the one directory every path
|
|
18
|
+
* is measured from, so `..` below depth zero is an escape regardless of where
|
|
19
|
+
* the module happens to sit on disk.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizeIncludePath(source) {
|
|
22
|
+
const raw = source.trim();
|
|
23
|
+
if (raw === "") {
|
|
24
|
+
return {
|
|
25
|
+
diagnostic: {
|
|
26
|
+
code: "INCLUDE_PATH_INVALID",
|
|
27
|
+
message: "the path is empty — name a file relative to the module root.",
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (URI_SCHEME.test(raw)) {
|
|
32
|
+
return {
|
|
33
|
+
diagnostic: {
|
|
34
|
+
code: "INCLUDE_PATH_INVALID",
|
|
35
|
+
message: `'${raw}' names a location outside the module. An include path is a file that ships ` +
|
|
36
|
+
`inside the module artifact, written relative to the module root. To read a file at ` +
|
|
37
|
+
`runtime from somewhere else, use Fs.File.`,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (GLOB_CHARS.test(raw)) {
|
|
42
|
+
return {
|
|
43
|
+
diagnostic: {
|
|
44
|
+
code: "INCLUDE_PATH_INVALID",
|
|
45
|
+
message: `'${raw}' looks like a pattern. An include path names exactly one file, because ` +
|
|
46
|
+
`publish places each claimed file into a layer by name.`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (raw.startsWith("/") || raw.startsWith("\\")) {
|
|
51
|
+
return {
|
|
52
|
+
diagnostic: {
|
|
53
|
+
code: "INCLUDE_PATH_ESCAPES_MODULE",
|
|
54
|
+
message: `'${raw}' is an absolute path. An include path is written relative to the module ` +
|
|
55
|
+
`root, so the same manifest resolves identically from a checkout and from a ` +
|
|
56
|
+
`published artifact.`,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const segment of raw.split(/[/\\]+/)) {
|
|
62
|
+
if (segment === "" || segment === ".")
|
|
63
|
+
continue;
|
|
64
|
+
if (segment !== "..") {
|
|
65
|
+
out.push(segment);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Depth zero is the module root. Popping past it would read a file the
|
|
69
|
+
// artifact cannot contain, so it is an escape rather than a path to
|
|
70
|
+
// normalize — and catching it here is what keeps the check static.
|
|
71
|
+
if (out.length === 0) {
|
|
72
|
+
return {
|
|
73
|
+
diagnostic: {
|
|
74
|
+
code: "INCLUDE_PATH_ESCAPES_MODULE",
|
|
75
|
+
message: `'${raw}' points above the module root. An include path may only name a file ` +
|
|
76
|
+
`inside the module, since that is the only thing its artifact can carry.`,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
out.pop();
|
|
81
|
+
}
|
|
82
|
+
if (out.length === 0) {
|
|
83
|
+
return {
|
|
84
|
+
diagnostic: {
|
|
85
|
+
code: "INCLUDE_PATH_INVALID",
|
|
86
|
+
message: `'${raw}' resolves to the module root, not to a file.`,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { path: out.join("/") };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The `!include-text` / `!include-bytes` engines — a file that ships inside the
|
|
94
|
+
* module artifact, embedded as a manifest value.
|
|
95
|
+
*
|
|
96
|
+
* `compile` returns the sentinel unchanged rather than a value. Two things
|
|
97
|
+
* follow, and both are the point. The read is deferred to resource creation, so
|
|
98
|
+
* loading a manifest does not pull payload layers — the artifact spec makes
|
|
99
|
+
* `telo.yaml` its own layer precisely so that reading a manifest cannot drag the
|
|
100
|
+
* whole artifact in, and an app loads every imported library's manifest. And
|
|
101
|
+
* because the marker survives precompile the way a `!ref` does, the analyzer can
|
|
102
|
+
* type the slot without opening the file, which is what keeps it browser-safe.
|
|
103
|
+
* Unlike `!ref`, no special case is needed in `precompileDoc`: an engine whose
|
|
104
|
+
* `compile` is identity on its own marker passes through the generic path.
|
|
105
|
+
*
|
|
106
|
+
* `analyze` reports why a path is unusable; `fileClaims` reports the path itself
|
|
107
|
+
* so publish can place the file in a layer without recognising the tag by name.
|
|
108
|
+
*/
|
|
109
|
+
function includeEngine(name) {
|
|
110
|
+
return {
|
|
111
|
+
name,
|
|
112
|
+
compile(source) {
|
|
113
|
+
return makeTaggedSentinel(name, source);
|
|
114
|
+
},
|
|
115
|
+
analyze(source) {
|
|
116
|
+
const { diagnostic } = normalizeIncludePath(source);
|
|
117
|
+
return { diagnostics: diagnostic ? [diagnostic] : [], calls: [] };
|
|
118
|
+
},
|
|
119
|
+
fileClaims(source) {
|
|
120
|
+
const { path } = normalizeIncludePath(source);
|
|
121
|
+
// A malformed path claims nothing. `analyze` is what says why, so
|
|
122
|
+
// claiming a half-understood path here would produce a second, worse
|
|
123
|
+
// report from publish about the same mistake.
|
|
124
|
+
return path ? [{ path }] : [];
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/** Embeds a file's contents as a UTF-8 string. */
|
|
129
|
+
export const includeTextEngine = includeEngine(INCLUDE_TEXT_ENGINE);
|
|
130
|
+
/** Embeds a file's contents as raw bytes — a `Uint8Array`, the shape every
|
|
131
|
+
* `x-telo-binary` slot accepts. */
|
|
132
|
+
export const includeBytesEngine = includeEngine(INCLUDE_BYTES_ENGINE);
|
package/dist/engines/literal.js
CHANGED
package/dist/engines/ref.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/engines/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAsB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG7F,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../src/engines/sql.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAsB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAG7F,OAAO,KAAK,EAIV,gBAAgB,EACjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,CAAC;AAErD;;;;;kFAKkF;AAClF,eAAO,MAAM,SAAS,EAAE,gBAqCvB,CAAC"}
|
package/dist/engines/sql.js
CHANGED
|
@@ -25,14 +25,41 @@ export const sqlEngine = {
|
|
|
25
25
|
analyze(source, env) {
|
|
26
26
|
// Each `${{ }}` interpolation is its own CEL expression; reuse the shared
|
|
27
27
|
// per-expression analyzer so diagnostics match the `!cel` engine exactly.
|
|
28
|
-
|
|
28
|
+
// Each interpolation's offset is kept so a fix computed against the bare
|
|
29
|
+
// expression is re-anchored to the whole SQL scalar — the only thing that
|
|
30
|
+
// ever made `!sql` fix-less was dropping it here.
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
const calls = [];
|
|
33
|
+
for (const { expr, start } of expressionsOf(source)) {
|
|
34
|
+
const result = analyzeCelExpression(expr, env);
|
|
35
|
+
for (const d of result.diagnostics) {
|
|
36
|
+
diagnostics.push(d.fix ? { ...d, fix: reanchor(source, expr, start, d.fix) } : d);
|
|
37
|
+
}
|
|
38
|
+
for (const c of result.calls) {
|
|
39
|
+
calls.push({ ...c, start: start + c.start, end: start + c.end });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// No `type`: a SQL template is a string built from many expressions, so
|
|
43
|
+
// there is no single checked type to report.
|
|
44
|
+
return { diagnostics, calls };
|
|
29
45
|
},
|
|
30
46
|
};
|
|
31
|
-
/**
|
|
47
|
+
/** Re-anchor a fix computed against one interpolation onto the full source. */
|
|
48
|
+
function reanchor(source, expr, start, fix) {
|
|
49
|
+
return {
|
|
50
|
+
replacement: source.slice(0, start) + fix.replacement + source.slice(start + expr.length),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** Each `${{ expr }}` body in a `!sql` template, with the body's offset in the
|
|
54
|
+
* source. The offset is derived from the opening delimiter and the leading
|
|
55
|
+
* whitespace the capture group already trims, never by searching for the
|
|
56
|
+
* body text — which a body appearing twice would defeat. */
|
|
32
57
|
function expressionsOf(source) {
|
|
33
|
-
const
|
|
58
|
+
const out = [];
|
|
34
59
|
for (const m of source.matchAll(TEMPLATE_REGEX)) {
|
|
35
|
-
|
|
60
|
+
const lead = /^\s*/.exec(m[0].slice(OPEN.length))[0].length;
|
|
61
|
+
out.push({ expr: m[1], start: m.index + OPEN.length + lead });
|
|
36
62
|
}
|
|
37
|
-
return
|
|
63
|
+
return out;
|
|
38
64
|
}
|
|
65
|
+
const OPEN = "${{";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
export { buildCelEnvironment, type CelHandlers } from "./cel/environment.js";
|
|
1
|
+
export { buildCelEnvironment, celBuiltinFunctions, deriveSignatures, type CelHandlers, } from "./cel/environment.js";
|
|
2
2
|
export { celFunctionCatalog, CEL_FUNCTIONS, type CelFunctionInfo, type CelFunctionDoc, type CelFunctionCategory, } from "./cel/catalog.js";
|
|
3
3
|
export { compileExpression, compileString, toParameterized, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
4
4
|
export { extractAccessChains, findNullableAccessIssues, INDEX_SEGMENT, validateChainAgainstSchema, } from "./cel/analyze.js";
|
|
5
|
-
export {
|
|
5
|
+
export { auditCalls, explainUnresolved, functionIndex, type CallAudit } from "./cel/diagnose.js";
|
|
6
|
+
export { walkCelExpressions, type CelSurface } from "./cel/walk.js";
|
|
6
7
|
export { celEngine } from "./engines/cel.js";
|
|
8
|
+
export { includeBytesEngine, includeTextEngine, normalizeIncludePath, type NormalizedIncludePath, } from "./engines/include.js";
|
|
7
9
|
export { literalEngine } from "./engines/literal.js";
|
|
8
10
|
export { refEngine } from "./engines/ref.js";
|
|
9
11
|
export { sqlEngine, isParameterizedSql, type ParameterizedSql } from "./engines/sql.js";
|
|
10
12
|
export { TemplatingEngineRegistry } from "./registry.js";
|
|
11
13
|
export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
|
|
12
|
-
export type { AnalyzeEnv, CompileEnv, EngineDiagnostic, TemplatingEngine, } from "./engine.js";
|
|
13
|
-
export { isRefSentinel, isTaggedSentinel, makeTaggedSentinel, type TaggedSentinel } from "./sentinel.js";
|
|
14
|
+
export type { AnalyzeEnv, AnalyzeResult, CallSite, CompileEnv, DiagnosticFix, EngineDiagnostic, EngineFileClaim, TemplatingEngine, } from "./engine.js";
|
|
15
|
+
export { INCLUDE_BYTES_ENGINE, INCLUDE_ENGINE_NAMES, INCLUDE_TEXT_ENGINE, isIncludeSentinel, isRefSentinel, isTaggedSentinel, makeTaggedSentinel, type TaggedSentinel, } from "./sentinel.js";
|
|
14
16
|
export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
|
|
15
17
|
export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, normalizeRefSlots, } from "./manifest-schemas.js";
|
|
16
18
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,gBAAgB,EAChB,KAAK,WAAW,GACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,iBAAiB,EACjB,aAAa,EACb,eAAe,EACf,cAAc,EACd,oBAAoB,GACrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,aAAa,EACb,0BAA0B,GAC3B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACjG,OAAO,EAAE,kBAAkB,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEpE,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,qBAAqB,GAC3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAExF,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACvF,YAAY,EACV,UAAU,EACV,aAAa,EACb,QAAQ,EACR,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
export { buildCelEnvironment } from "./cel/environment.js";
|
|
1
|
+
export { buildCelEnvironment, celBuiltinFunctions, deriveSignatures, } from "./cel/environment.js";
|
|
2
2
|
export { celFunctionCatalog, CEL_FUNCTIONS, } from "./cel/catalog.js";
|
|
3
3
|
export { compileExpression, compileString, toParameterized, TEMPLATE_REGEX, EXACT_TEMPLATE_REGEX, } from "./cel/compile.js";
|
|
4
4
|
export { extractAccessChains, findNullableAccessIssues, INDEX_SEGMENT, validateChainAgainstSchema, } from "./cel/analyze.js";
|
|
5
|
+
export { auditCalls, explainUnresolved, functionIndex } from "./cel/diagnose.js";
|
|
5
6
|
export { walkCelExpressions } from "./cel/walk.js";
|
|
6
7
|
export { celEngine } from "./engines/cel.js";
|
|
8
|
+
export { includeBytesEngine, includeTextEngine, normalizeIncludePath, } from "./engines/include.js";
|
|
7
9
|
export { literalEngine } from "./engines/literal.js";
|
|
8
10
|
export { refEngine } from "./engines/ref.js";
|
|
9
11
|
export { sqlEngine, isParameterizedSql } from "./engines/sql.js";
|
|
10
12
|
export { TemplatingEngineRegistry } from "./registry.js";
|
|
11
13
|
export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
|
|
12
|
-
export { isRefSentinel, isTaggedSentinel, makeTaggedSentinel } from "./sentinel.js";
|
|
14
|
+
export { INCLUDE_BYTES_ENGINE, INCLUDE_ENGINE_NAMES, INCLUDE_TEXT_ENGINE, isIncludeSentinel, isRefSentinel, isTaggedSentinel, makeTaggedSentinel, } from "./sentinel.js";
|
|
13
15
|
export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
|
|
14
16
|
export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, normalizeRefSlots, } from "./manifest-schemas.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;kBAMkB;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yEA+ByE;AACzE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoB7B,CAAC;AAqCF;;;;;;;;;;;;;;uDAcuD;AACvD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;kBAMkB;AAElB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yEA+ByE;AACzE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoB7B,CAAC;AAqCF;;;;;;;;;;;;;;uDAcuD;AACvD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CA+C1D;AAED;;;;;+BAK+B;AAC/B,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;8BAG8B;AAC9B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK9B,CAAC"}
|
package/dist/manifest-schemas.js
CHANGED
|
@@ -113,7 +113,12 @@ export function normalizeRefSlots(schema) {
|
|
|
113
113
|
const out = { ...node };
|
|
114
114
|
// Reference slot with a stale scalar `type` (legacy string-ref encoding):
|
|
115
115
|
// drop the constraint so the resolved reference object / sentinel validates.
|
|
116
|
-
|
|
116
|
+
//
|
|
117
|
+
// A presence test, not a shape test — deliberately, since `templating` sits
|
|
118
|
+
// BELOW the analyzer in the dependency order and cannot reach the shared
|
|
119
|
+
// `readRefSlot` accessor. Presence is the only thing this rule needs, and it
|
|
120
|
+
// is stable across every annotation shape.
|
|
121
|
+
if (node[REF_ANNOTATION] !== undefined &&
|
|
117
122
|
typeof node.type === "string" &&
|
|
118
123
|
LEGACY_REF_SCALAR_TYPES.has(node.type)) {
|
|
119
124
|
delete out.type;
|
package/dist/sentinel.d.ts
CHANGED
|
@@ -19,4 +19,23 @@ export declare function makeTaggedSentinel(engine: string, source: string): Tagg
|
|
|
19
19
|
export declare function isRefSentinel(v: unknown): v is TaggedSentinel & {
|
|
20
20
|
engine: "ref";
|
|
21
21
|
};
|
|
22
|
+
/** Engine names of the two file-embedding tags. Named here beside the other
|
|
23
|
+
* sentinel predicates so the kernel's resolution pass and the engines
|
|
24
|
+
* themselves agree on one spelling. */
|
|
25
|
+
export declare const INCLUDE_TEXT_ENGINE = "include-text";
|
|
26
|
+
export declare const INCLUDE_BYTES_ENGINE = "include-bytes";
|
|
27
|
+
/** Both file-embedding tag names, for a consumer holding an engine NAME rather
|
|
28
|
+
* than a value (the analyzer's expression walk reports names). */
|
|
29
|
+
export declare const INCLUDE_ENGINE_NAMES: ReadonlySet<string>;
|
|
30
|
+
/** True when `v` is an `!include-text` / `!include-bytes` sentinel — a file
|
|
31
|
+
* embed marked at parse time and still unresolved.
|
|
32
|
+
*
|
|
33
|
+
* The kernel's creation-time resolution keys off this the way Phase-5
|
|
34
|
+
* injection keys off {@link isRefSentinel}. Both tags survive precompile as
|
|
35
|
+
* markers rather than collapsing to a value, because the read is deferred: a
|
|
36
|
+
* manifest load must not pull payload layers, and the analyzer that types the
|
|
37
|
+
* slot cannot open files at all. */
|
|
38
|
+
export declare function isIncludeSentinel(v: unknown): v is TaggedSentinel & {
|
|
39
|
+
engine: typeof INCLUDE_TEXT_ENGINE | typeof INCLUDE_BYTES_ENGINE;
|
|
40
|
+
};
|
|
22
41
|
//# sourceMappingURL=sentinel.d.ts.map
|
package/dist/sentinel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../src/sentinel.ts"],"names":[],"mappings":"AAAA;;;;;YAKY;AACZ,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,cAAc,CAQhE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAEjF;AAED;;;;4CAI4C;AAC5C,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,cAAc,GAAG;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,CAEjF"}
|
|
1
|
+
{"version":3,"file":"sentinel.d.ts","sourceRoot":"","sources":["../src/sentinel.ts"],"names":[],"mappings":"AAAA;;;;;YAKY;AACZ,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,cAAc,CAQhE;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc,CAEjF;AAED;;;;4CAI4C;AAC5C,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,cAAc,GAAG;IAAE,MAAM,EAAE,KAAK,CAAA;CAAE,CAEjF;AAED;;wCAEwC;AACxC,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD;mEACmE;AACnE,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,CAGnD,CAAC;AAEH;;;;;;;qCAOqC;AACrC,wBAAgB,iBAAiB,CAC/B,CAAC,EAAE,OAAO,GACT,CAAC,IAAI,cAAc,GAAG;IAAE,MAAM,EAAE,OAAO,mBAAmB,GAAG,OAAO,oBAAoB,CAAA;CAAE,CAI5F"}
|
package/dist/sentinel.js
CHANGED
|
@@ -16,3 +16,25 @@ export function makeTaggedSentinel(engine, source) {
|
|
|
16
16
|
export function isRefSentinel(v) {
|
|
17
17
|
return isTaggedSentinel(v) && v.engine === "ref";
|
|
18
18
|
}
|
|
19
|
+
/** Engine names of the two file-embedding tags. Named here beside the other
|
|
20
|
+
* sentinel predicates so the kernel's resolution pass and the engines
|
|
21
|
+
* themselves agree on one spelling. */
|
|
22
|
+
export const INCLUDE_TEXT_ENGINE = "include-text";
|
|
23
|
+
export const INCLUDE_BYTES_ENGINE = "include-bytes";
|
|
24
|
+
/** Both file-embedding tag names, for a consumer holding an engine NAME rather
|
|
25
|
+
* than a value (the analyzer's expression walk reports names). */
|
|
26
|
+
export const INCLUDE_ENGINE_NAMES = new Set([
|
|
27
|
+
INCLUDE_TEXT_ENGINE,
|
|
28
|
+
INCLUDE_BYTES_ENGINE,
|
|
29
|
+
]);
|
|
30
|
+
/** True when `v` is an `!include-text` / `!include-bytes` sentinel — a file
|
|
31
|
+
* embed marked at parse time and still unresolved.
|
|
32
|
+
*
|
|
33
|
+
* The kernel's creation-time resolution keys off this the way Phase-5
|
|
34
|
+
* injection keys off {@link isRefSentinel}. Both tags survive precompile as
|
|
35
|
+
* markers rather than collapsing to a value, because the read is deferred: a
|
|
36
|
+
* manifest load must not pull payload layers, and the analyzer that types the
|
|
37
|
+
* slot cannot open files at all. */
|
|
38
|
+
export function isIncludeSentinel(v) {
|
|
39
|
+
return (isTaggedSentinel(v) && (v.engine === INCLUDE_TEXT_ENGINE || v.engine === INCLUDE_BYTES_ENGINE));
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/templating",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@types/node": "^20.0.0",
|
|
45
45
|
"typescript": "^5.0.0",
|
|
46
46
|
"vitest": "^2.1.8",
|
|
47
|
-
"@telorun/sdk": "0.
|
|
47
|
+
"@telorun/sdk": "0.72.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@telorun/sdk": "*"
|
package/src/builtins.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { celEngine } from "./engines/cel.js";
|
|
2
|
+
import { includeBytesEngine, includeTextEngine } from "./engines/include.js";
|
|
2
3
|
import { literalEngine } from "./engines/literal.js";
|
|
3
4
|
import { refEngine } from "./engines/ref.js";
|
|
4
5
|
import { sqlEngine } from "./engines/sql.js";
|
|
@@ -13,6 +14,8 @@ import type { TemplatingEngine } from "./engine.js";
|
|
|
13
14
|
* another (e.g. `cel + literal`); always ship the same set. */
|
|
14
15
|
export const builtinEngines: readonly TemplatingEngine[] = [
|
|
15
16
|
celEngine,
|
|
17
|
+
includeBytesEngine,
|
|
18
|
+
includeTextEngine,
|
|
16
19
|
literalEngine,
|
|
17
20
|
refEngine,
|
|
18
21
|
sqlEngine,
|
package/src/cel/catalog.ts
CHANGED
|
@@ -613,6 +613,33 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
|
|
|
613
613
|
hostBacked: false,
|
|
614
614
|
build: () => () => BigInt(Math.floor(Date.now() / 1000)),
|
|
615
615
|
},
|
|
616
|
+
// Timestamp conversions. cel-go's standard library defines both and cel-js
|
|
617
|
+
// ships neither, which is what made an instant a one-way door: timestamp
|
|
618
|
+
// arithmetic and the `getFullYear` / `getHours` family already work, but
|
|
619
|
+
// nothing converted the result back into a value a manifest field accepts,
|
|
620
|
+
// so an expiry could be computed and not stored. Semantics follow cel-go
|
|
621
|
+
// exactly — RFC 3339 and epoch SECONDS — so `int(timestamp)` and
|
|
622
|
+
// `timestamp(int)` round-trip in one unit.
|
|
623
|
+
{
|
|
624
|
+
name: "string",
|
|
625
|
+
signature: "string(timestamp): string",
|
|
626
|
+
register: ["string(google.protobuf.Timestamp): string"],
|
|
627
|
+
category: "conversion",
|
|
628
|
+
summary: "Format an instant as RFC 3339 (ISO-8601, UTC).",
|
|
629
|
+
deterministic: true,
|
|
630
|
+
hostBacked: false,
|
|
631
|
+
build: () => (t: Date) => t.toISOString(),
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
name: "int",
|
|
635
|
+
signature: "int(timestamp): int",
|
|
636
|
+
register: ["int(google.protobuf.Timestamp): int"],
|
|
637
|
+
category: "conversion",
|
|
638
|
+
summary: "Epoch seconds of an instant (the unit `timestamp(int)` reads back).",
|
|
639
|
+
deterministic: true,
|
|
640
|
+
hostBacked: false,
|
|
641
|
+
build: () => (t: Date) => BigInt(Math.floor(t.getTime() / 1000)),
|
|
642
|
+
},
|
|
616
643
|
// UUID
|
|
617
644
|
{
|
|
618
645
|
name: "uuidv1",
|