@telorun/templating 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,11 +55,11 @@ metadata:
55
55
  A complete feedback collection REST API — no code, pure YAML.
56
56
  Persists entries to SQLite and serves them over HTTP.
57
57
  imports:
58
- Http: std/http-server@0.8.0
59
- Sql: std/sql@0.5.1
58
+ Http: std/http-server@0.9.0
59
+ Sql: std/sql@0.8.0
60
60
  targets:
61
- - Migrations
62
- - Server
61
+ - !ref Migrations
62
+ - !ref Server
63
63
  ---
64
64
  # SQLite database — swap driver/host/database for PostgreSQL with zero YAML changes
65
65
  kind: Sql.Connection
@@ -72,9 +72,7 @@ file: ./tmp/feedback.db
72
72
  kind: Sql.Migrations
73
73
  metadata:
74
74
  name: Migrations
75
- connection:
76
- kind: Sql.Connection
77
- name: Db
75
+ connection: !ref Db
78
76
  ---
79
77
  kind: Sql.Migration
80
78
  metadata:
@@ -101,7 +99,7 @@ openapi:
101
99
  version: 1.0.0
102
100
  mounts:
103
101
  - path: /v1
104
- type: Http.Api.FeedbackRoutes
102
+ mount: !ref FeedbackRoutes
105
103
  ---
106
104
  kind: Http.Api
107
105
  metadata:
@@ -123,9 +121,7 @@ routes:
123
121
  required: [ text ]
124
122
  handler:
125
123
  kind: Sql.Exec
126
- connection:
127
- kind: Sql.Connection
128
- name: Db
124
+ connection: !ref Db
129
125
  inputs:
130
126
  sql: "INSERT INTO feedback (text, source, score) VALUES (?, ?, ?)"
131
127
  bindings:
@@ -146,9 +142,7 @@ routes:
146
142
  method: GET
147
143
  handler:
148
144
  kind: Sql.Select
149
- connection:
150
- kind: Sql.Connection
151
- name: Db
145
+ connection: !ref Db
152
146
  from: feedback
153
147
  columns: [ id, text, source, score, created_at ]
154
148
  orderBy:
@@ -172,9 +166,7 @@ routes:
172
166
  required: [ id ]
173
167
  handler:
174
168
  kind: Sql.Select
175
- connection:
176
- kind: Sql.Connection
177
- name: Db
169
+ connection: !ref Db
178
170
  from: feedback
179
171
  columns: [ id, text, source, score, created_at ]
180
172
  where:
@@ -24,8 +24,10 @@ export interface CelFunctionDoc {
24
24
  * optional args even though cel-js itself has no optional syntax. */
25
25
  readonly signature: string;
26
26
  /** Actual cel-js signatures to register — one per arity for an overloaded
27
- * function. Defaults to `[signature]` when omitted (the common single-arity
28
- * case, where `signature` is itself a valid cel-js signature). */
27
+ * function. When omitted, `deriveSignatures(signature)` is used: if the
28
+ * signature contains `type?`-marked optional params (e.g. `fn(string?): T`),
29
+ * it auto-expands to one registration per arity. Set `register` explicitly
30
+ * only when the auto-derivation is insufficient. */
29
31
  readonly register?: readonly string[];
30
32
  readonly category: CelFunctionCategory;
31
33
  readonly summary: string;
@@ -1 +1 @@
1
- {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAEA;;;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;AAED,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;;uEAEmE;IACnE,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,EAsZlD,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":"AAEA;;;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;AAED,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,EAoZlD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
@@ -359,8 +359,7 @@ export const CEL_FUNCTIONS = [
359
359
  // (default "UTC"); epoch values are absolute and take none.
360
360
  {
361
361
  name: "nowIso",
362
- signature: "nowIso(tz?): string",
363
- register: ["nowIso(): string", "nowIso(string): string"],
362
+ signature: "nowIso(string?): string",
364
363
  category: "time",
365
364
  summary: "Current time as ISO-8601; UTC by default, or in the given IANA timezone.",
366
365
  deterministic: false,
@@ -369,8 +368,7 @@ export const CEL_FUNCTIONS = [
369
368
  },
370
369
  {
371
370
  name: "today",
372
- signature: "today(tz?): string",
373
- register: ["today(): string", "today(string): string"],
371
+ signature: "today(string?): string",
374
372
  category: "time",
375
373
  summary: "Current calendar date (YYYY-MM-DD); UTC by default, or in the given IANA timezone.",
376
374
  deterministic: false,
@@ -17,5 +17,12 @@ export type { CelHandlers } from "./catalog.js";
17
17
  * no fields, so terminal access (passing the value through CEL) succeeds but
18
18
  * member access raises a CEL error at runtime — matching the analyzer's
19
19
  * static check on `x-telo-stream`-marked properties. */
20
+ /** Expand a documented signature that may contain `type?`-marked optional
21
+ * parameters into one cel-js registration signature per arity. For example,
22
+ * `"nowIso(string?): string"` produces `["nowIso(): string",
23
+ * "nowIso(string): string"]`. Required parameters must precede optional ones.
24
+ * Returns `[signature]` unchanged when no `?` is present or the signature
25
+ * cannot be parsed. */
26
+ export declare function deriveSignatures(signature: string): string[];
20
27
  export declare function buildCelEnvironment(handlers?: Partial<CelHandlers>): Environment;
21
28
  //# sourceMappingURL=environment.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cel/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE/D,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAoBhD;;;;;;;;;;;;;;;yDAeyD;AACzD,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAYpF"}
1
+ {"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/cel/environment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE/D,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAoBhD;;;;;;;;;;;;;;;yDAeyD;AACzD;;;;;wBAKwB;AACxB,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAyB5D;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,OAAO,CAAC,WAAW,CAAM,GAAG,WAAW,CAcpF"}
@@ -31,14 +31,51 @@ const STUB_HANDLERS = {
31
31
  * no fields, so terminal access (passing the value through CEL) succeeds but
32
32
  * member access raises a CEL error at runtime — matching the analyzer's
33
33
  * static check on `x-telo-stream`-marked properties. */
34
+ /** Expand a documented signature that may contain `type?`-marked optional
35
+ * parameters into one cel-js registration signature per arity. For example,
36
+ * `"nowIso(string?): string"` produces `["nowIso(): string",
37
+ * "nowIso(string): string"]`. Required parameters must precede optional ones.
38
+ * Returns `[signature]` unchanged when no `?` is present or the signature
39
+ * cannot be parsed. */
40
+ export function deriveSignatures(signature) {
41
+ const m = signature.match(/^(\w+)\((.*?)\):\s*(.+)$/);
42
+ if (!m)
43
+ return [signature];
44
+ const name = m[1];
45
+ const paramsStr = m[2].trim();
46
+ const returnType = m[3].trim();
47
+ if (!paramsStr.includes("?"))
48
+ return [signature];
49
+ const params = paramsStr.split(",").map((p) => p.trim());
50
+ const required = [];
51
+ const optional = [];
52
+ for (const p of params) {
53
+ if (p.endsWith("?")) {
54
+ optional.push(p.slice(0, -1));
55
+ }
56
+ else {
57
+ if (optional.length > 0)
58
+ return [signature];
59
+ required.push(p);
60
+ }
61
+ }
62
+ if (optional.length === 0)
63
+ return [signature];
64
+ return Array.from({ length: optional.length + 1 }, (_, i) => {
65
+ const allParams = [...required, ...optional.slice(0, i)];
66
+ return `${name}(${allParams.join(", ")}): ${returnType}`;
67
+ });
68
+ }
34
69
  export function buildCelEnvironment(handlers = {}) {
35
70
  const h = { ...STUB_HANDLERS, ...handlers };
36
71
  let env = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
37
72
  for (const fn of CEL_FUNCTIONS) {
38
73
  const impl = fn.build(h);
39
- // `register` lists one cel-js signature per arity (overloaded functions);
40
- // it falls back to `signature` for the single-arity common case.
41
- for (const sig of fn.register ?? [fn.signature]) {
74
+ // `register` lists one cel-js signature per arity (overloaded functions).
75
+ // When absent, `deriveSignatures` expands `type?` optional-param notation
76
+ // into one registration per arity — so `nowIso(string?): string` registers
77
+ // both `nowIso(): string` and `nowIso(string): string` automatically.
78
+ for (const sig of fn.register ?? deriveSignatures(fn.signature)) {
42
79
  env = env.registerFunction(sig, impl);
43
80
  }
44
81
  }
package/dist/index.d.ts CHANGED
@@ -12,5 +12,5 @@ export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builti
12
12
  export type { AnalyzeEnv, CompileEnv, EngineDiagnostic, TemplatingEngine, } from "./engine.js";
13
13
  export { isRefSentinel, isTaggedSentinel, makeTaggedSentinel, type TaggedSentinel } from "./sentinel.js";
14
14
  export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
15
- export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, } from "./manifest-schemas.js";
15
+ export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, normalizeRefSlots, } from "./manifest-schemas.js";
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC7E,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,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,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,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC7E,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,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,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,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AACzG,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,uBAAuB,CAAC"}
package/dist/index.js CHANGED
@@ -11,4 +11,4 @@ export { TemplatingEngineRegistry } from "./registry.js";
11
11
  export { builtinEngines, createDefaultRegistry, defaultRegistry } from "./builtins.js";
12
12
  export { isRefSentinel, isTaggedSentinel, makeTaggedSentinel } from "./sentinel.js";
13
13
  export { buildCustomTags, defaultCustomTags } from "./yaml-tags.js";
14
- export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, } from "./manifest-schemas.js";
14
+ export { MANIFEST_SCHEMA_URI, ManifestRootSchema, ResourceRefSchema, normalizeRefSlots, } from "./manifest-schemas.js";
@@ -5,35 +5,87 @@
5
5
  * layer where a single source of truth can sit. The kernel re-exports
6
6
  * these symbols from its own `manifest-schemas` surface for downstream
7
7
  * ergonomics. */
8
- /** Schema fragment for a resource reference produced by the `!ref` YAML
9
- * tag. After parsing, the value is a `TaggedSentinel` (engine "ref")
10
- * whose `source` is the bare resource name. Module schemas declare a
11
- * ref slot as `$ref: "telo://manifest#/$defs/ResourceRef"`; the
12
- * analyzer's reference walker is what looks the name up against the
13
- * slot's `x-telo-ref` constraint.
8
+ /** Schema fragment for a resource-reference slot. The only form a manifest
9
+ * author writes is the `!ref <name>` (or `!ref <Alias>.<name>`) YAML tag,
10
+ * which parses to a `TaggedSentinel` (engine "ref") whose `source` is the
11
+ * bare resource name. In practice module schemas mark a ref slot with a bare
12
+ * `x-telo-ref` annotation (plus, where the slot only ever holds a reference,
13
+ * `type: object` to reject a stray scalar); the analyzer's reference walker
14
+ * reads `x-telo-ref` to look the name up against that constraint, independent
15
+ * of this fragment. A slot opts into `$ref:
16
+ * "telo://manifest#/$defs/ResourceRef"` only when it wants this exact
17
+ * two-branch shape enforced at the AJV layer too — it is not required, and
18
+ * slots that also accept an inline value (e.g. `inputType` / `outputType`)
19
+ * deliberately do not use it (an inline JSON Schema has no `kind`).
14
20
  *
15
- * Migration note: while the legacy bare-name string and `{kind, name}`
16
- * object forms are still accepted by the analyzer/kernel walkers (in an
17
- * additive transitional state), those shapes are *not* part of this
18
- * schema. Slots that need to accept legacy forms during migration keep
19
- * their hand-rolled `oneOf` until the cutover lands. */
21
+ * Two `anyOf` branches because the value's shape depends on the phase at
22
+ * which it is validated:
23
+ *
24
+ * 1. The raw `!ref` sentinel what survives to AJV when a cross-module
25
+ * reference can't be resolved in standalone single-file analysis (the
26
+ * imported module isn't loaded). `substituteCelFields` deliberately
27
+ * keeps the sentinel so this branch matches.
28
+ * 2. A resolved reference object — `{kind, name, alias?}` substituted in
29
+ * place of a sentinel (or an inline definition `{kind, ...config}`
30
+ * reached through a local `$ref` that escapes extraction). Both the
31
+ * kernel and the analyzer validate ref slots *after* sentinel
32
+ * resolution, so this is the shape AJV usually sees.
33
+ *
34
+ * The object-form `{kind, name}` reference a user could once type directly
35
+ * is gone: a plain object at a ref slot is only ever an inline definition
36
+ * or the resolver's own substitution, never an author-written reference.
37
+ * That removal is enforced by the analyzer (it rejects an author-written
38
+ * `{kind, name}` before normalization), not by this schema — branch 2
39
+ * cannot distinguish an author's `{kind, name}` from the resolver's. */
20
40
  export declare const ResourceRefSchema: {
21
- type: string;
22
- required: string[];
23
- properties: {
24
- __tagged: {
25
- const: boolean;
26
- };
27
- engine: {
28
- const: string;
41
+ title: string;
42
+ anyOf: ({
43
+ type: string;
44
+ required: string[];
45
+ properties: {
46
+ __tagged: {
47
+ const: boolean;
48
+ };
49
+ engine: {
50
+ const: string;
51
+ };
52
+ source: {
53
+ type: string;
54
+ minLength: number;
55
+ };
56
+ kind?: undefined;
29
57
  };
30
- source: {
31
- type: string;
32
- minLength: number;
58
+ additionalProperties: boolean;
59
+ } | {
60
+ type: string;
61
+ required: string[];
62
+ properties: {
63
+ kind: {
64
+ type: string;
65
+ };
66
+ __tagged?: undefined;
67
+ engine?: undefined;
68
+ source?: undefined;
33
69
  };
34
- };
35
- additionalProperties: boolean;
70
+ additionalProperties: boolean;
71
+ })[];
36
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;
37
89
  /** Stable URI under which the shared manifest root schema is registered
38
90
  * with module-side AJV instances. Module YAMLs reach the fragments via
39
91
  * `$ref: "telo://manifest#/$defs/<Name>"`. The URI is the contract;
@@ -49,21 +101,37 @@ export declare const ManifestRootSchema: {
49
101
  $id: string;
50
102
  $defs: {
51
103
  ResourceRef: {
52
- type: string;
53
- required: string[];
54
- properties: {
55
- __tagged: {
56
- const: boolean;
104
+ title: string;
105
+ anyOf: ({
106
+ type: string;
107
+ required: string[];
108
+ properties: {
109
+ __tagged: {
110
+ const: boolean;
111
+ };
112
+ engine: {
113
+ const: string;
114
+ };
115
+ source: {
116
+ type: string;
117
+ minLength: number;
118
+ };
119
+ kind?: undefined;
57
120
  };
58
- engine: {
59
- const: string;
121
+ additionalProperties: boolean;
122
+ } | {
123
+ type: string;
124
+ required: string[];
125
+ properties: {
126
+ kind: {
127
+ type: string;
128
+ };
129
+ __tagged?: undefined;
130
+ engine?: undefined;
131
+ source?: undefined;
60
132
  };
61
- source: {
62
- type: string;
63
- minLength: number;
64
- };
65
- };
66
- additionalProperties: boolean;
133
+ additionalProperties: boolean;
134
+ })[];
67
135
  };
68
136
  };
69
137
  };
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-schemas.d.ts","sourceRoot":"","sources":["../src/manifest-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;kBAMkB;AAElB;;;;;;;;;;;yDAWyD;AACzD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;CAS7B,CAAC;AAEF;;;;;+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;AAqCF;;;;;;;;;;;;;;uDAcuD;AACvD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CA0C1D;AAED;;;;;+BAK+B;AAC/B,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;8BAG8B;AAC9B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAK9B,CAAC"}
@@ -5,28 +5,149 @@
5
5
  * layer where a single source of truth can sit. The kernel re-exports
6
6
  * these symbols from its own `manifest-schemas` surface for downstream
7
7
  * ergonomics. */
8
- /** Schema fragment for a resource reference produced by the `!ref` YAML
9
- * tag. After parsing, the value is a `TaggedSentinel` (engine "ref")
10
- * whose `source` is the bare resource name. Module schemas declare a
11
- * ref slot as `$ref: "telo://manifest#/$defs/ResourceRef"`; the
12
- * analyzer's reference walker is what looks the name up against the
13
- * slot's `x-telo-ref` constraint.
8
+ /** Schema fragment for a resource-reference slot. The only form a manifest
9
+ * author writes is the `!ref <name>` (or `!ref <Alias>.<name>`) YAML tag,
10
+ * which parses to a `TaggedSentinel` (engine "ref") whose `source` is the
11
+ * bare resource name. In practice module schemas mark a ref slot with a bare
12
+ * `x-telo-ref` annotation (plus, where the slot only ever holds a reference,
13
+ * `type: object` to reject a stray scalar); the analyzer's reference walker
14
+ * reads `x-telo-ref` to look the name up against that constraint, independent
15
+ * of this fragment. A slot opts into `$ref:
16
+ * "telo://manifest#/$defs/ResourceRef"` only when it wants this exact
17
+ * two-branch shape enforced at the AJV layer too — it is not required, and
18
+ * slots that also accept an inline value (e.g. `inputType` / `outputType`)
19
+ * deliberately do not use it (an inline JSON Schema has no `kind`).
14
20
  *
15
- * Migration note: while the legacy bare-name string and `{kind, name}`
16
- * object forms are still accepted by the analyzer/kernel walkers (in an
17
- * additive transitional state), those shapes are *not* part of this
18
- * schema. Slots that need to accept legacy forms during migration keep
19
- * their hand-rolled `oneOf` until the cutover lands. */
21
+ * Two `anyOf` branches because the value's shape depends on the phase at
22
+ * which it is validated:
23
+ *
24
+ * 1. The raw `!ref` sentinel what survives to AJV when a cross-module
25
+ * reference can't be resolved in standalone single-file analysis (the
26
+ * imported module isn't loaded). `substituteCelFields` deliberately
27
+ * keeps the sentinel so this branch matches.
28
+ * 2. A resolved reference object — `{kind, name, alias?}` substituted in
29
+ * place of a sentinel (or an inline definition `{kind, ...config}`
30
+ * reached through a local `$ref` that escapes extraction). Both the
31
+ * kernel and the analyzer validate ref slots *after* sentinel
32
+ * resolution, so this is the shape AJV usually sees.
33
+ *
34
+ * The object-form `{kind, name}` reference a user could once type directly
35
+ * is gone: a plain object at a ref slot is only ever an inline definition
36
+ * or the resolver's own substitution, never an author-written reference.
37
+ * That removal is enforced by the analyzer (it rejects an author-written
38
+ * `{kind, name}` before normalization), not by this schema — branch 2
39
+ * cannot distinguish an author's `{kind, name}` from the resolver's. */
20
40
  export const ResourceRefSchema = {
21
- type: "object",
22
- required: ["__tagged", "engine", "source"],
23
- properties: {
24
- __tagged: { const: true },
25
- engine: { const: "ref" },
26
- source: { type: "string", minLength: 1 },
27
- },
28
- additionalProperties: false,
41
+ title: "Resource reference",
42
+ anyOf: [
43
+ {
44
+ type: "object",
45
+ required: ["__tagged", "engine", "source"],
46
+ properties: {
47
+ __tagged: { const: true },
48
+ engine: { const: "ref" },
49
+ source: { type: "string", minLength: 1 },
50
+ },
51
+ additionalProperties: false,
52
+ },
53
+ {
54
+ type: "object",
55
+ required: ["kind"],
56
+ properties: { kind: { type: "string" } },
57
+ additionalProperties: true,
58
+ },
59
+ ],
29
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
+ if (typeof node[REF_ANNOTATION] === "string" &&
117
+ typeof node.type === "string" &&
118
+ LEGACY_REF_SCALAR_TYPES.has(node.type)) {
119
+ delete out.type;
120
+ }
121
+ for (const key of SUBSCHEMA_SINGLE) {
122
+ const value = node[key];
123
+ if (value && typeof value === "object" && !Array.isArray(value)) {
124
+ out[key] = normalizeRefSlots(value);
125
+ }
126
+ }
127
+ for (const key of SUBSCHEMA_LIST) {
128
+ const value = node[key];
129
+ if (Array.isArray(value))
130
+ out[key] = value.map(normalizeRefSlots);
131
+ }
132
+ // `items` is either a single subschema or a tuple of subschemas.
133
+ if (Array.isArray(node.items)) {
134
+ out.items = node.items.map(normalizeRefSlots);
135
+ }
136
+ else if (node.items && typeof node.items === "object") {
137
+ out.items = normalizeRefSlots(node.items);
138
+ }
139
+ for (const key of SUBSCHEMA_MAP) {
140
+ const value = node[key];
141
+ if (value && typeof value === "object" && !Array.isArray(value)) {
142
+ const mapped = {};
143
+ for (const [name, sub] of Object.entries(value)) {
144
+ mapped[name] = normalizeRefSlots(sub);
145
+ }
146
+ out[key] = mapped;
147
+ }
148
+ }
149
+ return out;
150
+ }
30
151
  /** Stable URI under which the shared manifest root schema is registered
31
152
  * with module-side AJV instances. Module YAMLs reach the fragments via
32
153
  * `$ref: "telo://manifest#/$defs/<Name>"`. The URI is the contract;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/templating",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Telo Templating - Engine registry and shared CEL core for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -35,7 +35,7 @@
35
35
  "src/**"
36
36
  ],
37
37
  "dependencies": {
38
- "@marcbachmann/cel-js": "^7.5.3",
38
+ "@marcbachmann/cel-js": "^7.6.1",
39
39
  "uuid": "^10.0.0",
40
40
  "yaml": "^2.8.3"
41
41
  },
@@ -44,7 +44,7 @@
44
44
  "@types/uuid": "^10.0.0",
45
45
  "typescript": "^5.0.0",
46
46
  "vitest": "^2.1.8",
47
- "@telorun/sdk": "0.21.0"
47
+ "@telorun/sdk": "0.23.0"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@telorun/sdk": "*"
@@ -38,8 +38,10 @@ export interface CelFunctionDoc {
38
38
  * optional args even though cel-js itself has no optional syntax. */
39
39
  readonly signature: string;
40
40
  /** Actual cel-js signatures to register — one per arity for an overloaded
41
- * function. Defaults to `[signature]` when omitted (the common single-arity
42
- * case, where `signature` is itself a valid cel-js signature). */
41
+ * function. When omitted, `deriveSignatures(signature)` is used: if the
42
+ * signature contains `type?`-marked optional params (e.g. `fn(string?): T`),
43
+ * it auto-expands to one registration per arity. Set `register` explicitly
44
+ * only when the auto-derivation is insufficient. */
43
45
  readonly register?: readonly string[];
44
46
  readonly category: CelFunctionCategory;
45
47
  readonly summary: string;
@@ -423,8 +425,7 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
423
425
  // (default "UTC"); epoch values are absolute and take none.
424
426
  {
425
427
  name: "nowIso",
426
- signature: "nowIso(tz?): string",
427
- register: ["nowIso(): string", "nowIso(string): string"],
428
+ signature: "nowIso(string?): string",
428
429
  category: "time",
429
430
  summary: "Current time as ISO-8601; UTC by default, or in the given IANA timezone.",
430
431
  deterministic: false,
@@ -433,8 +434,7 @@ export const CEL_FUNCTIONS: readonly CelFunctionDoc[] = [
433
434
  },
434
435
  {
435
436
  name: "today",
436
- signature: "today(tz?): string",
437
- register: ["today(): string", "today(string): string"],
437
+ signature: "today(string?): string",
438
438
  category: "time",
439
439
  summary: "Current calendar date (YYYY-MM-DD); UTC by default, or in the given IANA timezone.",
440
440
  deterministic: false,
@@ -38,14 +38,49 @@ const STUB_HANDLERS: CelHandlers = {
38
38
  * no fields, so terminal access (passing the value through CEL) succeeds but
39
39
  * member access raises a CEL error at runtime — matching the analyzer's
40
40
  * static check on `x-telo-stream`-marked properties. */
41
+ /** Expand a documented signature that may contain `type?`-marked optional
42
+ * parameters into one cel-js registration signature per arity. For example,
43
+ * `"nowIso(string?): string"` produces `["nowIso(): string",
44
+ * "nowIso(string): string"]`. Required parameters must precede optional ones.
45
+ * Returns `[signature]` unchanged when no `?` is present or the signature
46
+ * cannot be parsed. */
47
+ export function deriveSignatures(signature: string): string[] {
48
+ const m = signature.match(/^(\w+)\((.*?)\):\s*(.+)$/);
49
+ if (!m) return [signature];
50
+ const name = m[1]!;
51
+ const paramsStr = m[2]!.trim();
52
+ const returnType = m[3]!.trim();
53
+ if (!paramsStr.includes("?")) return [signature];
54
+
55
+ const params = paramsStr.split(",").map((p) => p.trim());
56
+ const required: string[] = [];
57
+ const optional: string[] = [];
58
+ for (const p of params) {
59
+ if (p.endsWith("?")) {
60
+ optional.push(p.slice(0, -1));
61
+ } else {
62
+ if (optional.length > 0) return [signature];
63
+ required.push(p);
64
+ }
65
+ }
66
+ if (optional.length === 0) return [signature];
67
+
68
+ return Array.from({ length: optional.length + 1 }, (_, i) => {
69
+ const allParams = [...required, ...optional.slice(0, i)];
70
+ return `${name}(${allParams.join(", ")}): ${returnType}`;
71
+ });
72
+ }
73
+
41
74
  export function buildCelEnvironment(handlers: Partial<CelHandlers> = {}): Environment {
42
75
  const h: CelHandlers = { ...STUB_HANDLERS, ...handlers };
43
76
  let env = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
44
77
  for (const fn of CEL_FUNCTIONS) {
45
78
  const impl = fn.build(h);
46
- // `register` lists one cel-js signature per arity (overloaded functions);
47
- // it falls back to `signature` for the single-arity common case.
48
- for (const sig of fn.register ?? [fn.signature]) {
79
+ // `register` lists one cel-js signature per arity (overloaded functions).
80
+ // When absent, `deriveSignatures` expands `type?` optional-param notation
81
+ // into one registration per arity — so `nowIso(string?): string` registers
82
+ // both `nowIso(): string` and `nowIso(string): string` automatically.
83
+ for (const sig of fn.register ?? deriveSignatures(fn.signature)) {
49
84
  env = env.registerFunction(sig, impl);
50
85
  }
51
86
  }
package/src/index.ts CHANGED
@@ -41,4 +41,5 @@ export {
41
41
  MANIFEST_SCHEMA_URI,
42
42
  ManifestRootSchema,
43
43
  ResourceRefSchema,
44
+ normalizeRefSlots,
44
45
  } from "./manifest-schemas.js";
@@ -6,29 +6,154 @@
6
6
  * these symbols from its own `manifest-schemas` surface for downstream
7
7
  * ergonomics. */
8
8
 
9
- /** Schema fragment for a resource reference produced by the `!ref` YAML
10
- * tag. After parsing, the value is a `TaggedSentinel` (engine "ref")
11
- * whose `source` is the bare resource name. Module schemas declare a
12
- * ref slot as `$ref: "telo://manifest#/$defs/ResourceRef"`; the
13
- * analyzer's reference walker is what looks the name up against the
14
- * slot's `x-telo-ref` constraint.
9
+ /** Schema fragment for a resource-reference slot. The only form a manifest
10
+ * author writes is the `!ref <name>` (or `!ref <Alias>.<name>`) YAML tag,
11
+ * which parses to a `TaggedSentinel` (engine "ref") whose `source` is the
12
+ * bare resource name. In practice module schemas mark a ref slot with a bare
13
+ * `x-telo-ref` annotation (plus, where the slot only ever holds a reference,
14
+ * `type: object` to reject a stray scalar); the analyzer's reference walker
15
+ * reads `x-telo-ref` to look the name up against that constraint, independent
16
+ * of this fragment. A slot opts into `$ref:
17
+ * "telo://manifest#/$defs/ResourceRef"` only when it wants this exact
18
+ * two-branch shape enforced at the AJV layer too — it is not required, and
19
+ * slots that also accept an inline value (e.g. `inputType` / `outputType`)
20
+ * deliberately do not use it (an inline JSON Schema has no `kind`).
15
21
  *
16
- * Migration note: while the legacy bare-name string and `{kind, name}`
17
- * object forms are still accepted by the analyzer/kernel walkers (in an
18
- * additive transitional state), those shapes are *not* part of this
19
- * schema. Slots that need to accept legacy forms during migration keep
20
- * their hand-rolled `oneOf` until the cutover lands. */
22
+ * Two `anyOf` branches because the value's shape depends on the phase at
23
+ * which it is validated:
24
+ *
25
+ * 1. The raw `!ref` sentinel what survives to AJV when a cross-module
26
+ * reference can't be resolved in standalone single-file analysis (the
27
+ * imported module isn't loaded). `substituteCelFields` deliberately
28
+ * keeps the sentinel so this branch matches.
29
+ * 2. A resolved reference object — `{kind, name, alias?}` substituted in
30
+ * place of a sentinel (or an inline definition `{kind, ...config}`
31
+ * reached through a local `$ref` that escapes extraction). Both the
32
+ * kernel and the analyzer validate ref slots *after* sentinel
33
+ * resolution, so this is the shape AJV usually sees.
34
+ *
35
+ * The object-form `{kind, name}` reference a user could once type directly
36
+ * is gone: a plain object at a ref slot is only ever an inline definition
37
+ * or the resolver's own substitution, never an author-written reference.
38
+ * That removal is enforced by the analyzer (it rejects an author-written
39
+ * `{kind, name}` before normalization), not by this schema — branch 2
40
+ * cannot distinguish an author's `{kind, name}` from the resolver's. */
21
41
  export const ResourceRefSchema = {
22
- type: "object",
23
- required: ["__tagged", "engine", "source"],
24
- properties: {
25
- __tagged: { const: true },
26
- engine: { const: "ref" },
27
- source: { type: "string", minLength: 1 },
28
- },
29
- additionalProperties: false,
42
+ title: "Resource reference",
43
+ anyOf: [
44
+ {
45
+ type: "object",
46
+ required: ["__tagged", "engine", "source"],
47
+ properties: {
48
+ __tagged: { const: true },
49
+ engine: { const: "ref" },
50
+ source: { type: "string", minLength: 1 },
51
+ },
52
+ additionalProperties: false,
53
+ },
54
+ {
55
+ type: "object",
56
+ required: ["kind"],
57
+ properties: { kind: { type: "string" } },
58
+ additionalProperties: true,
59
+ },
60
+ ],
30
61
  };
31
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
+ if (
122
+ typeof node[REF_ANNOTATION] === "string" &&
123
+ typeof node.type === "string" &&
124
+ LEGACY_REF_SCALAR_TYPES.has(node.type)
125
+ ) {
126
+ delete out.type;
127
+ }
128
+ for (const key of SUBSCHEMA_SINGLE) {
129
+ const value = node[key];
130
+ if (value && typeof value === "object" && !Array.isArray(value)) {
131
+ out[key] = normalizeRefSlots(value);
132
+ }
133
+ }
134
+ for (const key of SUBSCHEMA_LIST) {
135
+ const value = node[key];
136
+ if (Array.isArray(value)) out[key] = value.map(normalizeRefSlots);
137
+ }
138
+ // `items` is either a single subschema or a tuple of subschemas.
139
+ if (Array.isArray(node.items)) {
140
+ out.items = node.items.map(normalizeRefSlots);
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: Record<string, unknown> = {};
148
+ for (const [name, sub] of Object.entries(value as Record<string, unknown>)) {
149
+ mapped[name] = normalizeRefSlots(sub);
150
+ }
151
+ out[key] = mapped;
152
+ }
153
+ }
154
+ return out;
155
+ }
156
+
32
157
  /** Stable URI under which the shared manifest root schema is registered
33
158
  * with module-side AJV instances. Module YAMLs reach the fragments via
34
159
  * `$ref: "telo://manifest#/$defs/<Name>"`. The URI is the contract;