@telorun/templating 0.13.0 → 0.15.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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAGA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA2CD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;yDAIqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;CACjE;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAyE5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,EA+hBlD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAGA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA2CD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;yDAIqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;CACjE;AAED,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AAwH5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,EAslBlD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
@@ -106,6 +106,51 @@ const dateInZone = (tz) => {
106
106
  const p = zoneParts(now, tz, { year: "numeric", month: "2-digit", day: "2-digit" });
107
107
  return `${p.year}-${p.month}-${p.day}`;
108
108
  };
109
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
110
+ /** Base64 → bytes, written out rather than delegated: `Buffer` is not browser-
111
+ * safe and `atob` round-trips through a string, which is the corruption this
112
+ * pair exists to avoid. Accepts the URL-safe alphabet and tolerates missing
113
+ * padding, both of which appear in real API payloads; a character outside the
114
+ * alphabet is a hard error rather than a silently dropped byte. */
115
+ function decodeBase64ToBytes(input) {
116
+ const clean = input.replace(/[\r\n\t ]/g, "").replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
117
+ // A remainder of 1 cannot come from any byte sequence: base64 encodes 3 bytes
118
+ // as 4 characters, so the valid remainders are 0, 2 and 3. Left to the loop it
119
+ // would leave 6 bits unwritten and return a SHORT buffer — the silently dropped
120
+ // byte this function refuses for a bad character.
121
+ if (clean.length % 4 === 1) {
122
+ throw new Error("bytesFromBase64: input length is not valid base64");
123
+ }
124
+ const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
125
+ let bits = 0;
126
+ let acc = 0;
127
+ let written = 0;
128
+ for (const ch of clean) {
129
+ const value = BASE64_ALPHABET.indexOf(ch);
130
+ if (value < 0)
131
+ throw new Error(`bytesFromBase64: '${ch}' is not a base64 character`);
132
+ acc = (acc << 6) | value;
133
+ bits += 6;
134
+ if (bits >= 8) {
135
+ bits -= 8;
136
+ out[written++] = (acc >> bits) & 0xff;
137
+ }
138
+ }
139
+ return out.subarray(0, written);
140
+ }
141
+ function encodeBytesToBase64(input) {
142
+ let out = "";
143
+ for (let i = 0; i < input.length; i += 3) {
144
+ const a = input[i];
145
+ const b = i + 1 < input.length ? input[i + 1] : undefined;
146
+ const c = i + 2 < input.length ? input[i + 2] : undefined;
147
+ out += BASE64_ALPHABET[a >> 2];
148
+ out += BASE64_ALPHABET[((a & 0x03) << 4) | ((b ?? 0) >> 4)];
149
+ out += b === undefined ? "=" : BASE64_ALPHABET[((b & 0x0f) << 2) | ((c ?? 0) >> 6)];
150
+ out += c === undefined ? "=" : BASE64_ALPHABET[c & 0x3f];
151
+ }
152
+ return out;
153
+ }
109
154
  export const CEL_FUNCTIONS = [
110
155
  // Collections
111
156
  {
@@ -303,6 +348,37 @@ export const CEL_FUNCTIONS = [
303
348
  hostBacked: false,
304
349
  build: () => (s, suffix) => suffix && s.endsWith(suffix) ? s.slice(0, s.length - suffix.length) : s,
305
350
  },
351
+ // One `slice` over the three sequence types rather than three names: CEL
352
+ // already treats `size()` that way, and the alternative teaches an author that
353
+ // slicing bytes is a different operation from slicing a string.
354
+ //
355
+ // ONE `dyn` signature rather than a concrete overload per element type. The
356
+ // concrete set is more precise, but cel-js refuses overloads that overlap, so
357
+ // it cannot also carry the `dyn` case — and a step result whose producer
358
+ // declares no output type IS `dyn`, which is the common receiver. With only the
359
+ // concrete set the checker resolves such a call to whichever overload it tries
360
+ // first and pins the result to that type, so slicing an untyped byte buffer
361
+ // came back typed `string`. Precision here would be lost at the CEL boundary
362
+ // anyway, where type arguments are erased.
363
+ {
364
+ name: "slice",
365
+ signature: "slice(dyn, int, int): dyn",
366
+ // Not "string": it is the one function over strings, bytes AND lists, and the
367
+ // category is what groups it in the generated reference — filing it under
368
+ // strings hides it from the byte and list readers who need it most.
369
+ category: "collection",
370
+ summary: "Take the half-open range [start, end) of a string, bytes or list.",
371
+ deterministic: true,
372
+ hostBacked: false,
373
+ // Indices arrive as BigInt (a CEL int is int64), and both `String.slice` and
374
+ // `TypedArray.subarray` reject one. `subarray` rather than `slice` for bytes:
375
+ // a view costs no copy, and every consumer treats the result as read-only.
376
+ build: () => (value, start, end) => {
377
+ const from = Number(start);
378
+ const to = Number(end);
379
+ return value instanceof Uint8Array ? value.subarray(from, to) : value.slice(from, to);
380
+ },
381
+ },
306
382
  // Math
307
383
  {
308
384
  name: "abs",
@@ -416,6 +492,30 @@ export const CEL_FUNCTIONS = [
416
492
  hostBacked: true,
417
493
  build: (h) => (s) => h.base64Decode(s),
418
494
  },
495
+ // Base64 <-> BYTES, as distinct from the two above, which are UTF-8
496
+ // string<->string and silently corrupt anything that is not text. Those keep
497
+ // their meaning (changing it would change what shipped manifests mean); these
498
+ // are the correct path for binary, and the pair a byte-typed slot needs.
499
+ // Pure-JS on purpose: `Buffer` is what made the string pair host-backed, and
500
+ // this package has to stay browser-safe.
501
+ {
502
+ name: "bytesFromBase64",
503
+ signature: "bytesFromBase64(string): bytes",
504
+ category: "encoding",
505
+ summary: "Decode a base64 string to raw bytes.",
506
+ deterministic: true,
507
+ hostBacked: false,
508
+ build: () => (s) => decodeBase64ToBytes(s),
509
+ },
510
+ {
511
+ name: "bytesToBase64",
512
+ signature: "bytesToBase64(bytes): string",
513
+ category: "encoding",
514
+ summary: "Encode raw bytes as a base64 string.",
515
+ deterministic: true,
516
+ hostBacked: false,
517
+ build: () => (b) => encodeBytesToBase64(b),
518
+ },
419
519
  {
420
520
  name: "urlEncode",
421
521
  signature: "urlEncode(string): string",
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.15.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.74.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
  }
@@ -170,6 +170,53 @@ const dateInZone = (tz: string): string => {
170
170
  return `${p.year}-${p.month}-${p.day}`;
171
171
  };
172
172
 
173
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
174
+
175
+ /** Base64 → bytes, written out rather than delegated: `Buffer` is not browser-
176
+ * safe and `atob` round-trips through a string, which is the corruption this
177
+ * pair exists to avoid. Accepts the URL-safe alphabet and tolerates missing
178
+ * padding, both of which appear in real API payloads; a character outside the
179
+ * alphabet is a hard error rather than a silently dropped byte. */
180
+ function decodeBase64ToBytes(input: string): Uint8Array {
181
+ const clean = input.replace(/[\r\n\t ]/g, "").replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, "");
182
+ // A remainder of 1 cannot come from any byte sequence: base64 encodes 3 bytes
183
+ // as 4 characters, so the valid remainders are 0, 2 and 3. Left to the loop it
184
+ // would leave 6 bits unwritten and return a SHORT buffer — the silently dropped
185
+ // byte this function refuses for a bad character.
186
+ if (clean.length % 4 === 1) {
187
+ throw new Error("bytesFromBase64: input length is not valid base64");
188
+ }
189
+ const out = new Uint8Array(Math.floor((clean.length * 3) / 4));
190
+ let bits = 0;
191
+ let acc = 0;
192
+ let written = 0;
193
+ for (const ch of clean) {
194
+ const value = BASE64_ALPHABET.indexOf(ch);
195
+ if (value < 0) throw new Error(`bytesFromBase64: '${ch}' is not a base64 character`);
196
+ acc = (acc << 6) | value;
197
+ bits += 6;
198
+ if (bits >= 8) {
199
+ bits -= 8;
200
+ out[written++] = (acc >> bits) & 0xff;
201
+ }
202
+ }
203
+ return out.subarray(0, written);
204
+ }
205
+
206
+ function encodeBytesToBase64(input: Uint8Array): string {
207
+ let out = "";
208
+ for (let i = 0; i < input.length; i += 3) {
209
+ const a = input[i]!;
210
+ const b = i + 1 < input.length ? input[i + 1]! : undefined;
211
+ const c = i + 2 < input.length ? input[i + 2]! : undefined;
212
+ out += BASE64_ALPHABET[a >> 2];
213
+ out += BASE64_ALPHABET[((a & 0x03) << 4) | ((b ?? 0) >> 4)];
214
+ out += b === undefined ? "=" : BASE64_ALPHABET[((b & 0x0f) << 2) | ((c ?? 0) >> 6)];
215
+ out += c === undefined ? "=" : BASE64_ALPHABET[c & 0x3f];
216
+ }
217
+ return out;
218
+ }
219
+
173
220
  export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
174
221
  // Collections
175
222
  {
@@ -373,6 +420,37 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
373
420
  build: () => (s: string, suffix: string) =>
374
421
  suffix && s.endsWith(suffix) ? s.slice(0, s.length - suffix.length) : s,
375
422
  },
423
+ // One `slice` over the three sequence types rather than three names: CEL
424
+ // already treats `size()` that way, and the alternative teaches an author that
425
+ // slicing bytes is a different operation from slicing a string.
426
+ //
427
+ // ONE `dyn` signature rather than a concrete overload per element type. The
428
+ // concrete set is more precise, but cel-js refuses overloads that overlap, so
429
+ // it cannot also carry the `dyn` case — and a step result whose producer
430
+ // declares no output type IS `dyn`, which is the common receiver. With only the
431
+ // concrete set the checker resolves such a call to whichever overload it tries
432
+ // first and pins the result to that type, so slicing an untyped byte buffer
433
+ // came back typed `string`. Precision here would be lost at the CEL boundary
434
+ // anyway, where type arguments are erased.
435
+ {
436
+ name: "slice",
437
+ signature: "slice(dyn, int, int): dyn",
438
+ // Not "string": it is the one function over strings, bytes AND lists, and the
439
+ // category is what groups it in the generated reference — filing it under
440
+ // strings hides it from the byte and list readers who need it most.
441
+ category: "collection",
442
+ summary: "Take the half-open range [start, end) of a string, bytes or list.",
443
+ deterministic: true,
444
+ hostBacked: false,
445
+ // Indices arrive as BigInt (a CEL int is int64), and both `String.slice` and
446
+ // `TypedArray.subarray` reject one. `subarray` rather than `slice` for bytes:
447
+ // a view costs no copy, and every consumer treats the result as read-only.
448
+ build: () => (value: string | Uint8Array | unknown[], start: bigint, end: bigint) => {
449
+ const from = Number(start);
450
+ const to = Number(end);
451
+ return value instanceof Uint8Array ? value.subarray(from, to) : value.slice(from, to);
452
+ },
453
+ },
376
454
  // Math
377
455
  {
378
456
  name: "abs",
@@ -488,6 +566,30 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
488
566
  hostBacked: true,
489
567
  build: (h) => (s: string) => h.base64Decode(s),
490
568
  },
569
+ // Base64 <-> BYTES, as distinct from the two above, which are UTF-8
570
+ // string<->string and silently corrupt anything that is not text. Those keep
571
+ // their meaning (changing it would change what shipped manifests mean); these
572
+ // are the correct path for binary, and the pair a byte-typed slot needs.
573
+ // Pure-JS on purpose: `Buffer` is what made the string pair host-backed, and
574
+ // this package has to stay browser-safe.
575
+ {
576
+ name: "bytesFromBase64",
577
+ signature: "bytesFromBase64(string): bytes",
578
+ category: "encoding",
579
+ summary: "Decode a base64 string to raw bytes.",
580
+ deterministic: true,
581
+ hostBacked: false,
582
+ build: () => (s: string) => decodeBase64ToBytes(s),
583
+ },
584
+ {
585
+ name: "bytesToBase64",
586
+ signature: "bytesToBase64(bytes): string",
587
+ category: "encoding",
588
+ summary: "Encode raw bytes as a base64 string.",
589
+ deterministic: true,
590
+ hostBacked: false,
591
+ build: () => (b: Uint8Array) => encodeBytesToBase64(b),
592
+ },
491
593
  {
492
594
  name: "urlEncode",
493
595
  signature: "urlEncode(string): string",
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([