@telorun/templating 0.13.0 → 0.14.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.
@@ -8,6 +8,17 @@ import type { TemplatingEngine } from "./engine.js";
8
8
  * another (e.g. `cel + literal`); always ship the same set. */
9
9
  export declare const builtinEngines: readonly TemplatingEngine[];
10
10
  export declare function createDefaultRegistry(): TemplatingEngineRegistry;
11
+ /**
12
+ * The type a tag always produces, or undefined when its produced type is a
13
+ * function of the slot rather than of the tag (`!cel`, `!ref`).
14
+ *
15
+ * The single reader of `TemplatingEngine.producedType`, so a consumer asks the
16
+ * registry what a tag produces instead of recognising tag names — the same seam
17
+ * `fileClaims` opened for payload membership. Reads the default registry
18
+ * because a produced type is a property of the engine, not of a host's
19
+ * configuration, and every host ships the same built-in set.
20
+ */
21
+ export declare function producedTypeOf(engineName: string): Record<string, unknown> | undefined;
11
22
  /** Memoized singleton: returns the default registry. Hosts that don't need
12
23
  * per-instance isolation (precompile, the analyzer's tagged-value walker)
13
24
  * should use this so they share the same registry instance the YAML tag
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;gEAKgE;AAChE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAOrD,CAAC;AAEF,wBAAgB,qBAAqB,IAAI,wBAAwB,CAMhE;AAID;;;oBAGoB;AACpB,wBAAgB,eAAe,IAAI,wBAAwB,CAK1D"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;gEAKgE;AAChE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAOrD,CAAC;AAEF,wBAAgB,qBAAqB,IAAI,wBAAwB,CAMhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAEtF;AAID;;;oBAGoB;AACpB,wBAAgB,eAAe,IAAI,wBAAwB,CAK1D"}
package/dist/builtins.js CHANGED
@@ -25,6 +25,19 @@ export function createDefaultRegistry() {
25
25
  }
26
26
  return registry;
27
27
  }
28
+ /**
29
+ * The type a tag always produces, or undefined when its produced type is a
30
+ * function of the slot rather than of the tag (`!cel`, `!ref`).
31
+ *
32
+ * The single reader of `TemplatingEngine.producedType`, so a consumer asks the
33
+ * registry what a tag produces instead of recognising tag names — the same seam
34
+ * `fileClaims` opened for payload membership. Reads the default registry
35
+ * because a produced type is a property of the engine, not of a host's
36
+ * configuration, and every host ships the same built-in set.
37
+ */
38
+ export function producedTypeOf(engineName) {
39
+ return defaultRegistry().get(engineName)?.producedType?.();
40
+ }
28
41
  let defaultRegistryCache;
29
42
  /** Memoized singleton: returns the default registry. Hosts that don't need
30
43
  * per-instance isolation (precompile, the analyzer's tagged-value walker)
@@ -29,7 +29,8 @@ export declare function findNullableAccessIssues(node: ASTNode, contextSchema: R
29
29
  * Check whether a member-access chain accesses only fields declared in a JSON Schema.
30
30
  * Returns an error string if a field is unknown in a schema that declares explicit
31
31
  * properties without `additionalProperties: true`, or if the chain attempts to
32
- * reach inside an `x-telo-stream: true` property.
32
+ * reach inside a `live` value type — one whose consumption has effects, so its
33
+ * contents exist only for a consumer that drains it.
33
34
  * Returns null when the chain is valid or the schema is too open to judge.
34
35
  */
35
36
  export declare function validateChainAgainstSchema(chain: string[], schema: Record<string, any>): string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAEpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AA0DD;;+DAE+D;AAC/D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAqBnC,UAAU,aAAa;IACrB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;CAChB;AAoFD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,aAAa,EAAE,CAIjB;AA2FD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CA2Bf"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAGpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AA0DD;;+DAE+D;AAC/D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAqBnC,UAAU,aAAa;IACrB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;CAChB;AAoFD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,aAAa,EAAE,CAIjB;AA2FD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CAsBf"}
@@ -1,3 +1,4 @@
1
+ import { isLiveSlot } from "@telorun/sdk";
1
2
  /**
2
3
  * Extract all member-access chains from a CEL AST.
3
4
  * Returns arrays like ["request", "query", "name"] for `request.query.name`.
@@ -254,7 +255,8 @@ function walkNullable(node, nonNull, boundVars, issues, schema) {
254
255
  * Check whether a member-access chain accesses only fields declared in a JSON Schema.
255
256
  * Returns an error string if a field is unknown in a schema that declares explicit
256
257
  * properties without `additionalProperties: true`, or if the chain attempts to
257
- * reach inside an `x-telo-stream: true` property.
258
+ * reach inside a `live` value type — one whose consumption has effects, so its
259
+ * contents exist only for a consumer that drains it.
258
260
  * Returns null when the chain is valid or the schema is too open to judge.
259
261
  */
260
262
  export function validateChainAgainstSchema(chain, schema) {
@@ -268,10 +270,7 @@ export function validateChainAgainstSchema(chain, schema) {
268
270
  return null;
269
271
  if (key in props) {
270
272
  const propSchema = props[key];
271
- if (propSchema &&
272
- typeof propSchema === "object" &&
273
- propSchema["x-telo-stream"] === true &&
274
- i < chain.length - 1) {
273
+ if (isLiveSlot(propSchema) && i < chain.length - 1) {
275
274
  const path = chain.slice(0, i + 1).join(".");
276
275
  return `'${path}' yields a stream — pipe it through an Encoder or iterate in a JS.Script step (no member access on stream-typed values)`;
277
276
  }
package/dist/engine.d.ts CHANGED
@@ -131,5 +131,24 @@ export interface TemplatingEngine {
131
131
  * A source the engine considers malformed claims nothing; `analyze` is what
132
132
  * reports why. */
133
133
  fileClaims?(source: string): readonly EngineFileClaim[];
134
+ /** The type this tag ALWAYS produces, as a JSON Schema fragment.
135
+ *
136
+ * Declared by the engine, never recognised by a consumer — the `fileClaims`
137
+ * precedent applied to the one fact it left behind. Before this, the analyzer
138
+ * hardcoded two tag names to hand an `!include-bytes` a byte placeholder and
139
+ * an `!include-text` a string one; the only place a tag's produced type was
140
+ * written down was in its consumer, so a future tag producing bytes had to be
141
+ * added to a set rather than declaring it.
142
+ *
143
+ * Absent for an engine whose produced type is a function of the SLOT rather
144
+ * than of the tag — `!cel`, whose type is only derivable from the expression,
145
+ * and `!ref`, which is an identity marker. Their values keep taking a
146
+ * slot-shaped placeholder.
147
+ *
148
+ * What falls out is the property this preserves exactly: because an embed's
149
+ * type is a constant of the tag, a byte embed at a string slot and text at a
150
+ * byte slot both fail statically, through the ordinary schema check and with
151
+ * no diagnostic code of their own. */
152
+ producedType?(): Record<string, unknown>;
134
153
  }
135
154
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;wEAEwE;AACxE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;;;;;eASe;AACf,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;0EAa0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;oDAEoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED;;;;yBAIyB;AACzB,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;oDAEgD;IAChD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;iFAGiF;AACjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;4CAgB4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;wBAEwB;AACxB,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;2EAKuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAE3B;;;yDAGqD;IACrD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC;IAElE;;iEAE6D;IAC7D,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,CAAC;IAExD;;;;;;;;;;;;uBAYmB;IACnB,UAAU,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE,CAAC;CACzD"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;wEAEwE;AACxE,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;;;;;eASe;AACf,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;0EAa0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;oDAEoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,aAAa,CAAC;CAC9B;AAED;;;;yBAIyB;AACzB,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,wDAAwD;IACxD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;oDAEgD;IAChD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED;;;iFAGiF;AACjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAClD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;4CAgB4C;AAC5C,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;wBAEwB;AACxB,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;2EAKuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAE3B;;;yDAGqD;IACrD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,GAAG,OAAO,CAAC;IAElE;;iEAE6D;IAC7D,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,aAAa,CAAC;IAExD;;;;;;;;;;;;uBAYmB;IACnB,UAAU,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE,CAAC;IAExD;;;;;;;;;;;;;;;;;2CAiBuC;IACvC,YAAY,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC1C"}
@@ -19,7 +19,10 @@ export interface NormalizedIncludePath {
19
19
  export declare function normalizeIncludePath(source: string): NormalizedIncludePath;
20
20
  /** Embeds a file's contents as a UTF-8 string. */
21
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. */
22
+ /** Embeds a file's contents as raw bytes — the shape every `Telo.Bytes` slot
23
+ * accepts. The name is written as a literal rather than imported from the SDK:
24
+ * templating is the lower package, and a produced type is a schema fragment,
25
+ * which is data. What checks it is the registry the analyzer and the kernel both
26
+ * read. */
24
27
  export declare const includeBytesEngine: TemplatingEngine;
25
28
  //# sourceMappingURL=include.d.ts.map
@@ -1 +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"}
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;AA8CD,kDAAkD;AAClD,eAAO,MAAM,iBAAiB,EAAE,gBAE9B,CAAC;AAEH;;;;YAIY;AACZ,eAAO,MAAM,kBAAkB,EAAE,gBAE/B,CAAC"}
@@ -106,12 +106,15 @@ export function normalizeIncludePath(source) {
106
106
  * `analyze` reports why a path is unusable; `fileClaims` reports the path itself
107
107
  * so publish can place the file in a layer without recognising the tag by name.
108
108
  */
109
- function includeEngine(name) {
109
+ function includeEngine(name, produced) {
110
110
  return {
111
111
  name,
112
112
  compile(source) {
113
113
  return makeTaggedSentinel(name, source);
114
114
  },
115
+ producedType() {
116
+ return produced;
117
+ },
115
118
  analyze(source) {
116
119
  const { diagnostic } = normalizeIncludePath(source);
117
120
  return { diagnostics: diagnostic ? [diagnostic] : [], calls: [] };
@@ -126,7 +129,14 @@ function includeEngine(name) {
126
129
  };
127
130
  }
128
131
  /** 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);
132
+ export const includeTextEngine = includeEngine(INCLUDE_TEXT_ENGINE, {
133
+ type: "string",
134
+ });
135
+ /** Embeds a file's contents as raw bytes — the shape every `Telo.Bytes` slot
136
+ * accepts. The name is written as a literal rather than imported from the SDK:
137
+ * templating is the lower package, and a produced type is a schema fragment,
138
+ * which is data. What checks it is the registry the analyzer and the kernel both
139
+ * read. */
140
+ export const includeBytesEngine = includeEngine(INCLUDE_BYTES_ENGINE, {
141
+ "x-telo-type": "Telo.Bytes",
142
+ });
package/dist/index.d.ts CHANGED
@@ -10,9 +10,9 @@ export { literalEngine } from "./engines/literal.js";
10
10
  export { refEngine } from "./engines/ref.js";
11
11
  export { sqlEngine, isParameterizedSql, type ParameterizedSql } from "./engines/sql.js";
12
12
  export { TemplatingEngineRegistry } from "./registry.js";
13
- export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
13
+ export { builtinEngines, createDefaultRegistry, defaultRegistry, producedTypeOf, } from "./builtins.js";
14
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";
15
+ export { CEL_ENGINE, INCLUDE_BYTES_ENGINE, INCLUDE_ENGINE_NAMES, INCLUDE_TEXT_ENGINE, isIncludeSentinel, isRefSentinel, isTaggedSentinel, makeTaggedSentinel, plainChainOf, type TaggedSentinel, } from "./sentinel.js";
16
16
  export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
17
- export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, normalizeRefSlots, } from "./manifest-schemas.js";
17
+ export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, } from "./manifest-schemas.js";
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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"}
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,EACL,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,cAAc,GACf,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,UAAU,EACV,aAAa,EACb,QAAQ,EACR,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ export { literalEngine } from "./engines/literal.js";
10
10
  export { refEngine } from "./engines/ref.js";
11
11
  export { sqlEngine, isParameterizedSql } from "./engines/sql.js";
12
12
  export { TemplatingEngineRegistry } from "./registry.js";
13
- export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
14
- export { INCLUDE_BYTES_ENGINE, INCLUDE_ENGINE_NAMES, INCLUDE_TEXT_ENGINE, isIncludeSentinel, isRefSentinel, isTaggedSentinel, makeTaggedSentinel, } from "./sentinel.js";
13
+ export { builtinEngines, createDefaultRegistry, defaultRegistry, producedTypeOf, } from "./builtins.js";
14
+ export { CEL_ENGINE, INCLUDE_BYTES_ENGINE, INCLUDE_ENGINE_NAMES, INCLUDE_TEXT_ENGINE, isIncludeSentinel, isRefSentinel, isTaggedSentinel, makeTaggedSentinel, plainChainOf, } from "./sentinel.js";
15
15
  export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
16
- export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, normalizeRefSlots, } from "./manifest-schemas.js";
16
+ export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, } from "./manifest-schemas.js";
@@ -70,22 +70,6 @@ export declare const ResourceRefSchema: {
70
70
  additionalProperties: boolean;
71
71
  })[];
72
72
  };
73
- /** Deep-clone `schema`, dropping the stale scalar `type` constraint from every
74
- * reference-slot node — one carrying an `x-telo-ref` string annotation.
75
- *
76
- * A reference slot's value is always a `!ref` sentinel or its resolved
77
- * `{kind, name, alias?}` object (never a bare string, post-migration). Older
78
- * published modules still pin `type: "string"` on these slots — the encoding
79
- * references took when they were written as plain strings — which now rejects
80
- * the resolved object. Removing only the scalar `type` lets the analyzer and
81
- * kernel accept references uniformly across module versions during the
82
- * migration away from `{kind, name}` / string references, without disturbing
83
- * slots that legitimately accept an inline object (e.g. `inputType` /
84
- * `outputType`, which take a Telo.Type reference *or* an inline JSON schema).
85
- * The `x-telo-ref` constraint itself (which kind the reference must satisfy) is
86
- * checked separately by the analyzer's reference walker, which reads the
87
- * original schema — not this validation-only copy. */
88
- export declare function normalizeRefSlots(schema: unknown): unknown;
89
73
  /** Stable URI under which the shared manifest root schema is registered
90
74
  * with module-side AJV instances. Module YAMLs reach the fragments via
91
75
  * `$ref: "telo://manifest#/$defs/<Name>"`. The URI is the contract;
@@ -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,CA+C1D;AAED;;;;;+BAK+B;AAC/B,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;8BAG8B;AAC9B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK9B,CAAC"}
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;AAGF;;;;;+BAK+B;AAC/B,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;8BAG8B;AAC9B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK9B,CAAC"}
@@ -58,101 +58,6 @@ export const ResourceRefSchema = {
58
58
  },
59
59
  ],
60
60
  };
61
- const REF_ANNOTATION = "x-telo-ref";
62
- // The legacy base types a reference slot used to pin when references were
63
- // written as plain strings. Post-migration a reference resolves to an object
64
- // (the `{kind, name, alias?}` shape, or an unresolved `!ref` sentinel), so a
65
- // scalar `type` on a ref slot is a stale constraint that would reject the
66
- // resolved value. Object / array `type`s are left alone — they already admit
67
- // the reference object (and any inline value a slot like `inputType` accepts).
68
- const LEGACY_REF_SCALAR_TYPES = new Set(["string", "number", "integer", "boolean"]);
69
- // JSON Schema keywords whose values are themselves subschemas. Split by shape so
70
- // the ref-slot normalizer recurses only into schema positions — never into
71
- // data-bearing keywords (`default`, `const`, `enum`, `examples`), where a stray
72
- // `x-telo-ref` key would be data, not an annotation.
73
- const SUBSCHEMA_SINGLE = [
74
- "additionalProperties",
75
- "additionalItems",
76
- "contains",
77
- "not",
78
- "if",
79
- "then",
80
- "else",
81
- "propertyNames",
82
- "unevaluatedItems",
83
- "unevaluatedProperties",
84
- ];
85
- const SUBSCHEMA_LIST = ["allOf", "anyOf", "oneOf", "prefixItems"];
86
- const SUBSCHEMA_MAP = [
87
- "properties",
88
- "patternProperties",
89
- "$defs",
90
- "definitions",
91
- "dependentSchemas",
92
- ];
93
- /** Deep-clone `schema`, dropping the stale scalar `type` constraint from every
94
- * reference-slot node — one carrying an `x-telo-ref` string annotation.
95
- *
96
- * A reference slot's value is always a `!ref` sentinel or its resolved
97
- * `{kind, name, alias?}` object (never a bare string, post-migration). Older
98
- * published modules still pin `type: "string"` on these slots — the encoding
99
- * references took when they were written as plain strings — which now rejects
100
- * the resolved object. Removing only the scalar `type` lets the analyzer and
101
- * kernel accept references uniformly across module versions during the
102
- * migration away from `{kind, name}` / string references, without disturbing
103
- * slots that legitimately accept an inline object (e.g. `inputType` /
104
- * `outputType`, which take a Telo.Type reference *or* an inline JSON schema).
105
- * The `x-telo-ref` constraint itself (which kind the reference must satisfy) is
106
- * checked separately by the analyzer's reference walker, which reads the
107
- * original schema — not this validation-only copy. */
108
- export function normalizeRefSlots(schema) {
109
- if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
110
- return schema;
111
- }
112
- const node = schema;
113
- const out = { ...node };
114
- // Reference slot with a stale scalar `type` (legacy string-ref encoding):
115
- // drop the constraint so the resolved reference object / sentinel validates.
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 &&
122
- typeof node.type === "string" &&
123
- LEGACY_REF_SCALAR_TYPES.has(node.type)) {
124
- delete out.type;
125
- }
126
- for (const key of SUBSCHEMA_SINGLE) {
127
- const value = node[key];
128
- if (value && typeof value === "object" && !Array.isArray(value)) {
129
- out[key] = normalizeRefSlots(value);
130
- }
131
- }
132
- for (const key of SUBSCHEMA_LIST) {
133
- const value = node[key];
134
- if (Array.isArray(value))
135
- out[key] = value.map(normalizeRefSlots);
136
- }
137
- // `items` is either a single subschema or a tuple of subschemas.
138
- if (Array.isArray(node.items)) {
139
- out.items = node.items.map(normalizeRefSlots);
140
- }
141
- else if (node.items && typeof node.items === "object") {
142
- out.items = normalizeRefSlots(node.items);
143
- }
144
- for (const key of SUBSCHEMA_MAP) {
145
- const value = node[key];
146
- if (value && typeof value === "object" && !Array.isArray(value)) {
147
- const mapped = {};
148
- for (const [name, sub] of Object.entries(value)) {
149
- mapped[name] = normalizeRefSlots(sub);
150
- }
151
- out[key] = mapped;
152
- }
153
- }
154
- return out;
155
- }
156
61
  /** Stable URI under which the shared manifest root schema is registered
157
62
  * with module-side AJV instances. Module YAMLs reach the fragments via
158
63
  * `$ref: "telo://manifest#/$defs/<Name>"`. The URI is the contract;
@@ -19,11 +19,29 @@ 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
+ /** The CEL engine's name. Beside the other sentinel predicates for the same
23
+ * reason they are: a consumer that spells an engine name inline is a second
24
+ * place the registry's key is written down. */
25
+ export declare const CEL_ENGINE = "cel";
22
26
  /** Engine names of the two file-embedding tags. Named here beside the other
23
27
  * sentinel predicates so the kernel's resolution pass and the engines
24
28
  * themselves agree on one spelling. */
25
29
  export declare const INCLUDE_TEXT_ENGINE = "include-text";
26
30
  export declare const INCLUDE_BYTES_ENGINE = "include-bytes";
31
+ /**
32
+ * The dotted chain a value names, or undefined.
33
+ *
34
+ * Only a PLAIN CHAIN — `steps.encode.result.output` — in either spelling a
35
+ * manifest may carry it: a `!cel` sentinel or the `${{ }}` string form. An
36
+ * expression that COMPUTES rather than names has no schema to read off a context,
37
+ * so a caller that navigates one gets nothing and reports nothing: silence where
38
+ * the analyzer knows least is the conservative direction.
39
+ *
40
+ * Here rather than in each caller because "is this expression a plain chain" is
41
+ * one question, and two copies of the answer would eventually disagree about a
42
+ * shape like `a.b[0]`.
43
+ */
44
+ export declare function plainChainOf(value: unknown): string | undefined;
27
45
  /** Both file-embedding tag names, for a consumer holding an engine NAME rather
28
46
  * than a value (the analyzer's expression walk reports names). */
29
47
  export declare const INCLUDE_ENGINE_NAMES: ReadonlySet<string>;
@@ -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;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"}
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;;gDAEgD;AAChD,eAAO,MAAM,UAAU,QAAQ,CAAC;AAEhC;;wCAEwC;AACxC,eAAO,MAAM,mBAAmB,iBAAiB,CAAC;AAClD,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAa/D;AAED;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,11 +16,43 @@ export function makeTaggedSentinel(engine, source) {
16
16
  export function isRefSentinel(v) {
17
17
  return isTaggedSentinel(v) && v.engine === "ref";
18
18
  }
19
+ /** The CEL engine's name. Beside the other sentinel predicates for the same
20
+ * reason they are: a consumer that spells an engine name inline is a second
21
+ * place the registry's key is written down. */
22
+ export const CEL_ENGINE = "cel";
19
23
  /** Engine names of the two file-embedding tags. Named here beside the other
20
24
  * sentinel predicates so the kernel's resolution pass and the engines
21
25
  * themselves agree on one spelling. */
22
26
  export const INCLUDE_TEXT_ENGINE = "include-text";
23
27
  export const INCLUDE_BYTES_ENGINE = "include-bytes";
28
+ /**
29
+ * The dotted chain a value names, or undefined.
30
+ *
31
+ * Only a PLAIN CHAIN — `steps.encode.result.output` — in either spelling a
32
+ * manifest may carry it: a `!cel` sentinel or the `${{ }}` string form. An
33
+ * expression that COMPUTES rather than names has no schema to read off a context,
34
+ * so a caller that navigates one gets nothing and reports nothing: silence where
35
+ * the analyzer knows least is the conservative direction.
36
+ *
37
+ * Here rather than in each caller because "is this expression a plain chain" is
38
+ * one question, and two copies of the answer would eventually disagree about a
39
+ * shape like `a.b[0]`.
40
+ */
41
+ export function plainChainOf(value) {
42
+ const source = isTaggedSentinel(value)
43
+ ? value.engine === CEL_ENGINE
44
+ ? value.source
45
+ : undefined
46
+ : typeof value === "string"
47
+ ? /^\s*\$\{\{(.+)\}\}\s*$/.exec(value)?.[1]
48
+ : undefined;
49
+ if (typeof source !== "string")
50
+ return undefined;
51
+ const trimmed = source.trim();
52
+ return /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(trimmed)
53
+ ? trimmed
54
+ : undefined;
55
+ }
24
56
  /** Both file-embedding tag names, for a consumer holding an engine NAME rather
25
57
  * than a value (the analyzer's expression walk reports names). */
26
58
  export const INCLUDE_ENGINE_NAMES = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/templating",
3
- "version": "0.13.0",
3
+ "version": "0.14.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.72.0"
47
+ "@telorun/sdk": "0.73.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@telorun/sdk": "*"
package/src/builtins.ts CHANGED
@@ -29,6 +29,20 @@ export function createDefaultRegistry(): TemplatingEngineRegistry {
29
29
  return registry;
30
30
  }
31
31
 
32
+ /**
33
+ * The type a tag always produces, or undefined when its produced type is a
34
+ * function of the slot rather than of the tag (`!cel`, `!ref`).
35
+ *
36
+ * The single reader of `TemplatingEngine.producedType`, so a consumer asks the
37
+ * registry what a tag produces instead of recognising tag names — the same seam
38
+ * `fileClaims` opened for payload membership. Reads the default registry
39
+ * because a produced type is a property of the engine, not of a host's
40
+ * configuration, and every host ships the same built-in set.
41
+ */
42
+ export function producedTypeOf(engineName: string): Record<string, unknown> | undefined {
43
+ return defaultRegistry().get(engineName)?.producedType?.();
44
+ }
45
+
32
46
  let defaultRegistryCache: TemplatingEngineRegistry | undefined;
33
47
 
34
48
  /** Memoized singleton: returns the default registry. Hosts that don't need
@@ -1,4 +1,5 @@
1
1
  import type { ASTNode } from "@marcbachmann/cel-js";
2
+ import { isLiveSlot } from "@telorun/sdk";
2
3
 
3
4
  /**
4
5
  * Extract all member-access chains from a CEL AST.
@@ -291,7 +292,8 @@ function walkNullable(
291
292
  * Check whether a member-access chain accesses only fields declared in a JSON Schema.
292
293
  * Returns an error string if a field is unknown in a schema that declares explicit
293
294
  * properties without `additionalProperties: true`, or if the chain attempts to
294
- * reach inside an `x-telo-stream: true` property.
295
+ * reach inside a `live` value type — one whose consumption has effects, so its
296
+ * contents exist only for a consumer that drains it.
295
297
  * Returns null when the chain is valid or the schema is too open to judge.
296
298
  */
297
299
  export function validateChainAgainstSchema(
@@ -306,12 +308,7 @@ export function validateChainAgainstSchema(
306
308
  if (!props) return null;
307
309
  if (key in props) {
308
310
  const propSchema = props[key];
309
- if (
310
- propSchema &&
311
- typeof propSchema === "object" &&
312
- propSchema["x-telo-stream"] === true &&
313
- i < chain.length - 1
314
- ) {
311
+ if (isLiveSlot(propSchema) && i < chain.length - 1) {
315
312
  const path = chain.slice(0, i + 1).join(".");
316
313
  return `'${path}' yields a stream — pipe it through an Encoder or iterate in a JS.Script step (no member access on stream-typed values)`;
317
314
  }
package/src/engine.ts CHANGED
@@ -143,4 +143,24 @@ export interface TemplatingEngine {
143
143
  * A source the engine considers malformed claims nothing; `analyze` is what
144
144
  * reports why. */
145
145
  fileClaims?(source: string): readonly EngineFileClaim[];
146
+
147
+ /** The type this tag ALWAYS produces, as a JSON Schema fragment.
148
+ *
149
+ * Declared by the engine, never recognised by a consumer — the `fileClaims`
150
+ * precedent applied to the one fact it left behind. Before this, the analyzer
151
+ * hardcoded two tag names to hand an `!include-bytes` a byte placeholder and
152
+ * an `!include-text` a string one; the only place a tag's produced type was
153
+ * written down was in its consumer, so a future tag producing bytes had to be
154
+ * added to a set rather than declaring it.
155
+ *
156
+ * Absent for an engine whose produced type is a function of the SLOT rather
157
+ * than of the tag — `!cel`, whose type is only derivable from the expression,
158
+ * and `!ref`, which is an identity marker. Their values keep taking a
159
+ * slot-shaped placeholder.
160
+ *
161
+ * What falls out is the property this preserves exactly: because an embed's
162
+ * type is a constant of the tag, a byte embed at a string slot and text at a
163
+ * byte slot both fail statically, through the ordinary schema check and with
164
+ * no diagnostic code of their own. */
165
+ producedType?(): Record<string, unknown>;
146
166
  }
@@ -122,7 +122,7 @@ export function normalizeIncludePath(source: string): NormalizedIncludePath {
122
122
  * `analyze` reports why a path is unusable; `fileClaims` reports the path itself
123
123
  * so publish can place the file in a layer without recognising the tag by name.
124
124
  */
125
- function includeEngine(name: string): TemplatingEngine {
125
+ function includeEngine(name: string, produced: Record<string, unknown>): TemplatingEngine {
126
126
  return {
127
127
  name,
128
128
 
@@ -130,6 +130,10 @@ function includeEngine(name: string): TemplatingEngine {
130
130
  return makeTaggedSentinel(name, source);
131
131
  },
132
132
 
133
+ producedType() {
134
+ return produced;
135
+ },
136
+
133
137
  analyze(source) {
134
138
  const { diagnostic } = normalizeIncludePath(source);
135
139
  return { diagnostics: diagnostic ? [diagnostic] : [], calls: [] };
@@ -146,8 +150,15 @@ function includeEngine(name: string): TemplatingEngine {
146
150
  }
147
151
 
148
152
  /** Embeds a file's contents as a UTF-8 string. */
149
- export const includeTextEngine: TemplatingEngine = includeEngine(INCLUDE_TEXT_ENGINE);
153
+ export const includeTextEngine: TemplatingEngine = includeEngine(INCLUDE_TEXT_ENGINE, {
154
+ type: "string",
155
+ });
150
156
 
151
- /** Embeds a file's contents as raw bytes — a `Uint8Array`, the shape every
152
- * `x-telo-binary` slot accepts. */
153
- export const includeBytesEngine: TemplatingEngine = includeEngine(INCLUDE_BYTES_ENGINE);
157
+ /** Embeds a file's contents as raw bytes — the shape every `Telo.Bytes` slot
158
+ * accepts. The name is written as a literal rather than imported from the SDK:
159
+ * templating is the lower package, and a produced type is a schema fragment,
160
+ * which is data. What checks it is the registry the analyzer and the kernel both
161
+ * read. */
162
+ export const includeBytesEngine: TemplatingEngine = includeEngine(INCLUDE_BYTES_ENGINE, {
163
+ "x-telo-type": "Telo.Bytes",
164
+ });
package/src/index.ts CHANGED
@@ -39,7 +39,12 @@ export { refEngine } from "./engines/ref.js";
39
39
  export { sqlEngine, isParameterizedSql, type ParameterizedSql } from "./engines/sql.js";
40
40
 
41
41
  export { TemplatingEngineRegistry } from "./registry.js";
42
- export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
42
+ export {
43
+ builtinEngines,
44
+ createDefaultRegistry,
45
+ defaultRegistry,
46
+ producedTypeOf,
47
+ } from "./builtins.js";
43
48
  export type {
44
49
  AnalyzeEnv,
45
50
  AnalyzeResult,
@@ -52,6 +57,7 @@ export type {
52
57
  } from "./engine.js";
53
58
 
54
59
  export {
60
+ CEL_ENGINE,
55
61
  INCLUDE_BYTES_ENGINE,
56
62
  INCLUDE_ENGINE_NAMES,
57
63
  INCLUDE_TEXT_ENGINE,
@@ -59,6 +65,7 @@ export {
59
65
  isRefSentinel,
60
66
  isTaggedSentinel,
61
67
  makeTaggedSentinel,
68
+ plainChainOf,
62
69
  type TaggedSentinel,
63
70
  } from "./sentinel.js";
64
71
  export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
@@ -66,5 +73,4 @@ export {
66
73
  MANIFEST_SCHEMA_URI,
67
74
  ManifestRootSchema,
68
75
  ResourceRefSchema,
69
- normalizeRefSlots,
70
76
  } from "./manifest-schemas.js";
@@ -60,104 +60,6 @@ export const ResourceRefSchema = {
60
60
  ],
61
61
  };
62
62
 
63
- const REF_ANNOTATION = "x-telo-ref";
64
-
65
- // The legacy base types a reference slot used to pin when references were
66
- // written as plain strings. Post-migration a reference resolves to an object
67
- // (the `{kind, name, alias?}` shape, or an unresolved `!ref` sentinel), so a
68
- // scalar `type` on a ref slot is a stale constraint that would reject the
69
- // resolved value. Object / array `type`s are left alone — they already admit
70
- // the reference object (and any inline value a slot like `inputType` accepts).
71
- const LEGACY_REF_SCALAR_TYPES = new Set(["string", "number", "integer", "boolean"]);
72
-
73
- // JSON Schema keywords whose values are themselves subschemas. Split by shape so
74
- // the ref-slot normalizer recurses only into schema positions — never into
75
- // data-bearing keywords (`default`, `const`, `enum`, `examples`), where a stray
76
- // `x-telo-ref` key would be data, not an annotation.
77
- const SUBSCHEMA_SINGLE = [
78
- "additionalProperties",
79
- "additionalItems",
80
- "contains",
81
- "not",
82
- "if",
83
- "then",
84
- "else",
85
- "propertyNames",
86
- "unevaluatedItems",
87
- "unevaluatedProperties",
88
- ] as const;
89
- const SUBSCHEMA_LIST = ["allOf", "anyOf", "oneOf", "prefixItems"] as const;
90
- const SUBSCHEMA_MAP = [
91
- "properties",
92
- "patternProperties",
93
- "$defs",
94
- "definitions",
95
- "dependentSchemas",
96
- ] as const;
97
-
98
- /** Deep-clone `schema`, dropping the stale scalar `type` constraint from every
99
- * reference-slot node — one carrying an `x-telo-ref` string annotation.
100
- *
101
- * A reference slot's value is always a `!ref` sentinel or its resolved
102
- * `{kind, name, alias?}` object (never a bare string, post-migration). Older
103
- * published modules still pin `type: "string"` on these slots — the encoding
104
- * references took when they were written as plain strings — which now rejects
105
- * the resolved object. Removing only the scalar `type` lets the analyzer and
106
- * kernel accept references uniformly across module versions during the
107
- * migration away from `{kind, name}` / string references, without disturbing
108
- * slots that legitimately accept an inline object (e.g. `inputType` /
109
- * `outputType`, which take a Telo.Type reference *or* an inline JSON schema).
110
- * The `x-telo-ref` constraint itself (which kind the reference must satisfy) is
111
- * checked separately by the analyzer's reference walker, which reads the
112
- * original schema — not this validation-only copy. */
113
- export function normalizeRefSlots(schema: unknown): unknown {
114
- if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
115
- return schema;
116
- }
117
- const node = schema as Record<string, unknown>;
118
- const out: Record<string, unknown> = { ...node };
119
- // Reference slot with a stale scalar `type` (legacy string-ref encoding):
120
- // drop the constraint so the resolved reference object / sentinel validates.
121
- //
122
- // A presence test, not a shape test — deliberately, since `templating` sits
123
- // BELOW the analyzer in the dependency order and cannot reach the shared
124
- // `readRefSlot` accessor. Presence is the only thing this rule needs, and it
125
- // is stable across every annotation shape.
126
- if (
127
- node[REF_ANNOTATION] !== undefined &&
128
- typeof node.type === "string" &&
129
- LEGACY_REF_SCALAR_TYPES.has(node.type)
130
- ) {
131
- delete out.type;
132
- }
133
- for (const key of SUBSCHEMA_SINGLE) {
134
- const value = node[key];
135
- if (value && typeof value === "object" && !Array.isArray(value)) {
136
- out[key] = normalizeRefSlots(value);
137
- }
138
- }
139
- for (const key of SUBSCHEMA_LIST) {
140
- const value = node[key];
141
- if (Array.isArray(value)) out[key] = value.map(normalizeRefSlots);
142
- }
143
- // `items` is either a single subschema or a tuple of subschemas.
144
- if (Array.isArray(node.items)) {
145
- out.items = node.items.map(normalizeRefSlots);
146
- } else if (node.items && typeof node.items === "object") {
147
- out.items = normalizeRefSlots(node.items);
148
- }
149
- for (const key of SUBSCHEMA_MAP) {
150
- const value = node[key];
151
- if (value && typeof value === "object" && !Array.isArray(value)) {
152
- const mapped: Record<string, unknown> = {};
153
- for (const [name, sub] of Object.entries(value as Record<string, unknown>)) {
154
- mapped[name] = normalizeRefSlots(sub);
155
- }
156
- out[key] = mapped;
157
- }
158
- }
159
- return out;
160
- }
161
63
 
162
64
  /** Stable URI under which the shared manifest root schema is registered
163
65
  * with module-side AJV instances. Module YAMLs reach the fragments via
package/src/sentinel.ts CHANGED
@@ -33,12 +33,45 @@ export function isRefSentinel(v: unknown): v is TaggedSentinel & { engine: "ref"
33
33
  return isTaggedSentinel(v) && v.engine === "ref";
34
34
  }
35
35
 
36
+ /** The CEL engine's name. Beside the other sentinel predicates for the same
37
+ * reason they are: a consumer that spells an engine name inline is a second
38
+ * place the registry's key is written down. */
39
+ export const CEL_ENGINE = "cel";
40
+
36
41
  /** Engine names of the two file-embedding tags. Named here beside the other
37
42
  * sentinel predicates so the kernel's resolution pass and the engines
38
43
  * themselves agree on one spelling. */
39
44
  export const INCLUDE_TEXT_ENGINE = "include-text";
40
45
  export const INCLUDE_BYTES_ENGINE = "include-bytes";
41
46
 
47
+ /**
48
+ * The dotted chain a value names, or undefined.
49
+ *
50
+ * Only a PLAIN CHAIN — `steps.encode.result.output` — in either spelling a
51
+ * manifest may carry it: a `!cel` sentinel or the `${{ }}` string form. An
52
+ * expression that COMPUTES rather than names has no schema to read off a context,
53
+ * so a caller that navigates one gets nothing and reports nothing: silence where
54
+ * the analyzer knows least is the conservative direction.
55
+ *
56
+ * Here rather than in each caller because "is this expression a plain chain" is
57
+ * one question, and two copies of the answer would eventually disagree about a
58
+ * shape like `a.b[0]`.
59
+ */
60
+ export function plainChainOf(value: unknown): string | undefined {
61
+ const source = isTaggedSentinel(value)
62
+ ? value.engine === CEL_ENGINE
63
+ ? value.source
64
+ : undefined
65
+ : typeof value === "string"
66
+ ? /^\s*\$\{\{(.+)\}\}\s*$/.exec(value)?.[1]
67
+ : undefined;
68
+ if (typeof source !== "string") return undefined;
69
+ const trimmed = source.trim();
70
+ return /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(trimmed)
71
+ ? trimmed
72
+ : undefined;
73
+ }
74
+
42
75
  /** Both file-embedding tag names, for a consumer holding an engine NAME rather
43
76
  * than a value (the analyzer's expression walk reports names). */
44
77
  export const INCLUDE_ENGINE_NAMES: ReadonlySet<string> = new Set([