@telorun/analyzer 0.24.1 → 0.26.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
@@ -23,7 +23,7 @@ Built to be language-agnostic and infinitely extensible.
23
23
 
24
24
  ```bash
25
25
  # Reconcile your manifest into a running backend
26
- $ telo ./examples/hello-api.yaml
26
+ $ telo ./examples/hello-api
27
27
 
28
28
  {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
29
29
  ```
@@ -55,8 +55,8 @@ 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.11.0
59
- Sql: std/sql@0.9.0
58
+ Http: std/http-server@0.12.0
59
+ Sql: std/sql@0.9.2
60
60
  targets:
61
61
  - !ref Migrations
62
62
  - !ref Server
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAiB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAstBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAerB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAmB9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AA4hB/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAgvBvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAmBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
package/dist/analyzer.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { canonicalTypeSchemaId } from "@telorun/sdk";
1
2
  import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
2
3
  import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
3
4
  import { buildCelEnvironment, buildImportInputCelEnvironment, buildTypedCelEnvironment, } from "./cel-environment.js";
@@ -10,6 +11,8 @@ import { isModuleKind } from "./module-kinds.js";
10
11
  import { normalizeInlineResources } from "./normalize-inline-resources.js";
11
12
  import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
12
13
  import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
14
+ import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
15
+ import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
13
16
  import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
14
17
  import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
15
18
  import { DiagnosticSeverity } from "./types.js";
@@ -663,6 +666,26 @@ export class StaticAnalyzer {
663
666
  // kernel controllers) see a uniform shape. Runs after normalize so both
664
667
  // original and inline-extracted manifests have their sentinels resolved.
665
668
  resolveRefSentinels(allManifests, aliases, aliasesByModule);
669
+ // Phase 2.6: register each named `Telo.Type` resource's schema under its
670
+ // canonical module-scoped id (`telo://<module>/<name>`), validate
671
+ // `telo://Self|Alias/Type` schema refs resolve to one, then rewrite those
672
+ // refs to the canonical id so AJV resolves them at compile time. Register
673
+ // and validate BEFORE the rewrite, while the authored authority is intact.
674
+ for (const m of allManifests) {
675
+ const ownModule = m.metadata?.module;
676
+ if (!ownModule || !m.metadata?.name || typeof m.schema !== "object" || m.schema === null) {
677
+ continue;
678
+ }
679
+ const scopeResolver = rootModules.has(ownModule) ? aliases : (aliasesByModule.get(ownModule) ?? new AliasResolver());
680
+ const canonicalKind = scopeResolver.resolveKind(m.kind) ?? m.kind;
681
+ if (defs.resolve(canonicalKind)?.capability !== "Telo.Type")
682
+ continue;
683
+ defs.registerNamedTypeSchema(canonicalTypeSchemaId(ownModule, m.metadata.name), m.schema);
684
+ }
685
+ if (!options?.skipValidation) {
686
+ diagnostics.push(...validateSchemaTypeRefs(allManifests, defs, aliases, aliasesByModule, rootModules));
687
+ }
688
+ resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
666
689
  // Trusted-input fast path: when the caller has already attested that
667
690
  // this exact manifest set passes analysis (e.g. via the kernel's
668
691
  // hash-stamped `.validated.json` cache), skip the validation walk.
@@ -1109,6 +1132,10 @@ export class StaticAnalyzer {
1109
1132
  // inline-extracted manifests get their refs canonicalized to
1110
1133
  // {kind, name} for the kernel that consumes this output.
1111
1134
  resolveRefSentinels(normalized, ctx.aliases, ctx.aliasesByModule, crossModuleTargets ?? []);
1135
+ // Canonicalize import-scoped schema `$ref`s (`telo://Self|Alias/Type`) so the
1136
+ // kernel that executes this output compiles inputs/outputs against the same
1137
+ // ids the type controllers register their schemas under.
1138
+ resolveSchemaTypeRefs(normalized, ctx.aliases, ctx.aliasesByModule);
1112
1139
  return normalized;
1113
1140
  }
1114
1141
  prepare(manifests, registry) {
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAud/C,CAAC"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAwe/C,CAAC"}
package/dist/builtins.js CHANGED
@@ -298,6 +298,16 @@ export const KERNEL_BUILTINS = [
298
298
  type: "array",
299
299
  items: { type: "string" },
300
300
  },
301
+ // Files bundled alongside `telo.yaml` into the module's registry
302
+ // artifact (`module.tar.gz`) — static assets served by Http.Static,
303
+ // templates, etc. Ordered `.gitignore`-style patterns resolved against
304
+ // the manifest dir at publish time. Analyzer-only role: accept the
305
+ // field (the schema is additionalProperties:false); the analyzer never
306
+ // reads the assets. See kernel/nodejs/plans/bundle-controllers.md.
307
+ files: {
308
+ type: "array",
309
+ items: { type: "string" },
310
+ },
301
311
  // Inline imports — name-keyed map sugar for separate `Telo.Import`
302
312
  // documents. The key is the PascalCase alias (the import's
303
313
  // `metadata.name`). Each value is either a bare source string
@@ -422,6 +432,13 @@ export const KERNEL_BUILTINS = [
422
432
  type: "array",
423
433
  items: { type: "string" },
424
434
  },
435
+ // Files bundled into the module's registry artifact — same semantics as
436
+ // the Telo.Application `files` field above (a library may ship bundled
437
+ // templates, migrations, seed data).
438
+ files: {
439
+ type: "array",
440
+ items: { type: "string" },
441
+ },
425
442
  // Inline imports — same name-keyed map sugar as Telo.Application; the
426
443
  // loader desugars each entry into a synthetic Telo.Import. See the
427
444
  // Application schema above and analyzer/nodejs/src/inline-imports.ts.
@@ -27,6 +27,15 @@ export declare class DefinitionRegistry {
27
27
  * @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
28
28
  * @param moduleName The module's metadata.name (e.g. "pipeline", "http-server"). */
29
29
  registerModuleIdentity(namespace: string | null, moduleName: string): void;
30
+ /** Registers a named `Telo.Type` resource's schema under its canonical
31
+ * module-scoped URI `$id` (`telo://<module>/<name>`), so a sibling schema's
32
+ * `$ref: "telo://Self/<name>"` (rewritten to the canonical form by
33
+ * `resolveSchemaTypeRefs`) resolves during AJV compilation. Mirrors the
34
+ * kernel type controller's `registerSchema(canonicalTypeSchemaId(...))`. */
35
+ registerNamedTypeSchema(id: string, schema: Record<string, any>): void;
36
+ /** True when a schema is registered under `id` (a canonical `telo://` type id
37
+ * or a definition `$id`). Used to flag schema `$ref`s that resolve to nothing. */
38
+ hasSchemaId(id: string): boolean;
30
39
  /** Computes the $id for a definition schema: "<identity>/<TypeName>".
31
40
  * Returns undefined when the module identity is not yet registered. */
32
41
  computeId(moduleName: string, typeName: string): string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAGlC,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IA+B1E;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;;;;oFAIgF;IAChF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE;;;;;;iCAM6B;IAC7B,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,SAAS;IASnE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;;;;;;;qFAQiF;IACjF,2BAA2B,CACzB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,iBAAiB,GAAG,SAAS;IAuBhC,OAAO,CAAC,uBAAuB;IA+B/B;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
1
+ {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAGlC,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IA+B1E;;;;iFAI6E;IAC7E,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAMtE;uFACmF;IACnF,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAIhC;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;;;;oFAIgF;IAChF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE;;;;;;iCAM6B;IAC7B,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,SAAS;IASnE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;;;;;;;qFAQiF;IACjF,2BAA2B,CACzB,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC1C,iBAAiB,GAAG,SAAS;IAuBhC,OAAO,CAAC,uBAAuB;IA+B/B;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
@@ -96,6 +96,22 @@ export class DefinitionRegistry {
96
96
  }
97
97
  }
98
98
  }
99
+ /** Registers a named `Telo.Type` resource's schema under its canonical
100
+ * module-scoped URI `$id` (`telo://<module>/<name>`), so a sibling schema's
101
+ * `$ref: "telo://Self/<name>"` (rewritten to the canonical form by
102
+ * `resolveSchemaTypeRefs`) resolves during AJV compilation. Mirrors the
103
+ * kernel type controller's `registerSchema(canonicalTypeSchemaId(...))`. */
104
+ registerNamedTypeSchema(id, schema) {
105
+ if (this.registeredSchemaIds.has(id) || this.ajv.getSchema(id))
106
+ return;
107
+ this.ajv.addSchema(schema, id);
108
+ this.registeredSchemaIds.add(id);
109
+ }
110
+ /** True when a schema is registered under `id` (a canonical `telo://` type id
111
+ * or a definition `$id`). Used to flag schema `$ref`s that resolve to nothing. */
112
+ hasSchemaId(id) {
113
+ return this.registeredSchemaIds.has(id) || this.ajv.getSchema(id) !== undefined;
114
+ }
99
115
  /** Computes the $id for a definition schema: "<identity>/<TypeName>".
100
116
  * Returns undefined when the module identity is not yet registered. */
101
117
  computeId(moduleName, typeName) {
@@ -16,7 +16,7 @@ import type { AliasResolver } from "./alias-resolver.js";
16
16
  * Resolving a sentinel here does NOT cause Phase-5 injection: that pass is
17
17
  * driven by the field map, which still excludes step `invoke`s, so a resolved
18
18
  * step invoke stays `{kind, name}` and is dispatched through
19
- * `executeInvokeStep` (preserving `<Kind>.<Name>.Invoked` events) rather than
19
+ * `executeInvokeStep` (preserving `<name>.Invoked` events) rather than
20
20
  * being replaced with a live instance.
21
21
  *
22
22
  * Reference grammar — the tag's source string is split on the FIRST dot:
@@ -16,7 +16,7 @@ import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
16
16
  * Resolving a sentinel here does NOT cause Phase-5 injection: that pass is
17
17
  * driven by the field map, which still excludes step `invoke`s, so a resolved
18
18
  * step invoke stays `{kind, name}` and is dispatched through
19
- * `executeInvokeStep` (preserving `<Kind>.<Name>.Invoked` events) rather than
19
+ * `executeInvokeStep` (preserving `<name>.Invoked` events) rather than
20
20
  * being replaced with a live instance.
21
21
  *
22
22
  * Reference grammar — the tag's source string is split on the FIRST dot:
@@ -0,0 +1,20 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ /**
4
+ * Rewrites import-scoped schema references in place. A `$ref` of the form
5
+ * `telo://<authority>/<typeName>` names a `Type.JsonSchema` (or any `Telo.Type`)
6
+ * reached through an import: `telo://Self/<type>` for the declaring module's own
7
+ * type, `telo://<Alias>/<type>` for an imported module's. Each authority is
8
+ * resolved to the owning module's name and the ref is rewritten to the canonical
9
+ * `telo://<module>/<type>` the type registered its schema under.
10
+ *
11
+ * The version lives on the `imports:` entry, never the URI — only the pinned
12
+ * version is loaded, so the canonical id is version-free.
13
+ *
14
+ * Already-canonical refs (authority is a real module name, not an alias) and
15
+ * fragment-bearing built-ins (`telo://manifest#/$defs/ResourceRef`) are left
16
+ * untouched: the former because the authority resolves to nothing, the latter
17
+ * because they don't match the `authority/type` grammar.
18
+ */
19
+ export declare function resolveSchemaTypeRefs(resources: ResourceManifest[], aliases?: AliasResolver, aliasesByModule?: Map<string, AliasResolver>): void;
20
+ //# sourceMappingURL=resolve-schema-type-refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-schema-type-refs.d.ts","sourceRoot":"","sources":["../src/resolve-schema-type-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAKzD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,aAAa,EACvB,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,GAC3C,IAAI,CAyBN"}
@@ -0,0 +1,47 @@
1
+ import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
2
+ /** Schema-bearing fields on a Telo.Definition / Telo.Type resource. */
3
+ const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
4
+ /**
5
+ * Rewrites import-scoped schema references in place. A `$ref` of the form
6
+ * `telo://<authority>/<typeName>` names a `Type.JsonSchema` (or any `Telo.Type`)
7
+ * reached through an import: `telo://Self/<type>` for the declaring module's own
8
+ * type, `telo://<Alias>/<type>` for an imported module's. Each authority is
9
+ * resolved to the owning module's name and the ref is rewritten to the canonical
10
+ * `telo://<module>/<type>` the type registered its schema under.
11
+ *
12
+ * The version lives on the `imports:` entry, never the URI — only the pinned
13
+ * version is loaded, so the canonical id is version-free.
14
+ *
15
+ * Already-canonical refs (authority is a real module name, not an alias) and
16
+ * fragment-bearing built-ins (`telo://manifest#/$defs/ResourceRef`) are left
17
+ * untouched: the former because the authority resolves to nothing, the latter
18
+ * because they don't match the `authority/type` grammar.
19
+ */
20
+ export function resolveSchemaTypeRefs(resources, aliases, aliasesByModule) {
21
+ const walk = (value, resolveAuthority) => {
22
+ if (value === null || typeof value !== "object")
23
+ return;
24
+ if (Array.isArray(value)) {
25
+ for (const item of value)
26
+ walk(item, resolveAuthority);
27
+ return;
28
+ }
29
+ const obj = value;
30
+ const parsed = parseTeloTypeRef(obj.$ref);
31
+ if (parsed) {
32
+ const module = resolveAuthority(parsed.authority);
33
+ if (module)
34
+ obj.$ref = canonicalTypeSchemaId(module, parsed.typeName);
35
+ }
36
+ for (const key of Object.keys(obj))
37
+ walk(obj[key], resolveAuthority);
38
+ };
39
+ for (const r of resources) {
40
+ const ownModule = r.metadata?.module;
41
+ const resolver = (ownModule ? aliasesByModule?.get(ownModule) : undefined) ?? aliases;
42
+ const resolveAuthority = (authority) => authority === "Self" ? ownModule : resolver?.moduleForAlias(authority);
43
+ for (const field of SCHEMA_FIELDS) {
44
+ walk(r[field], resolveAuthority);
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,21 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ import type { DefinitionRegistry } from "./definition-registry.js";
4
+ import { type AnalysisDiagnostic } from "./types.js";
5
+ /**
6
+ * Validates module-scoped schema `$ref`s of the form `telo://<authority>/<type>`.
7
+ * The authority is an import alias (or `Self`) declared in the resource's module;
8
+ * the type must be a registered `Telo.Type` resource in the module it resolves to.
9
+ *
10
+ * Diagnostics (both errors — a `$ref` that resolves to nothing is never validated
11
+ * by AJV, so without this it would silently pass):
12
+ * - SCHEMA_TYPE_REF_UNKNOWN_ALIAS: the authority is neither `Self` nor a declared import.
13
+ * - SCHEMA_TYPE_REF_UNRESOLVED: the authority resolves to a module, but that module
14
+ * declares no `Telo.Type` named `<type>`.
15
+ *
16
+ * Forwarded/imported definitions are skipped — their own refs are validated when the
17
+ * owning library is analyzed as a root, and the consumer's scope can't see the
18
+ * library's internal aliases (mirrors `validateExtends`).
19
+ */
20
+ export declare function validateSchemaTypeRefs(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver, aliasesByModule: Map<string, AliasResolver>, rootModules: Set<string>): AnalysisDiagnostic[];
21
+ //# sourceMappingURL=validate-schema-type-refs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-schema-type-refs.d.ts","sourceRoot":"","sources":["../src/validate-schema-type-refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKzE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,EAC3C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,GACvB,kBAAkB,EAAE,CAuDtB"}
@@ -0,0 +1,74 @@
1
+ import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
2
+ import { DiagnosticSeverity } from "./types.js";
3
+ const SOURCE = "telo-analyzer";
4
+ const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
5
+ /**
6
+ * Validates module-scoped schema `$ref`s of the form `telo://<authority>/<type>`.
7
+ * The authority is an import alias (or `Self`) declared in the resource's module;
8
+ * the type must be a registered `Telo.Type` resource in the module it resolves to.
9
+ *
10
+ * Diagnostics (both errors — a `$ref` that resolves to nothing is never validated
11
+ * by AJV, so without this it would silently pass):
12
+ * - SCHEMA_TYPE_REF_UNKNOWN_ALIAS: the authority is neither `Self` nor a declared import.
13
+ * - SCHEMA_TYPE_REF_UNRESOLVED: the authority resolves to a module, but that module
14
+ * declares no `Telo.Type` named `<type>`.
15
+ *
16
+ * Forwarded/imported definitions are skipped — their own refs are validated when the
17
+ * owning library is analyzed as a root, and the consumer's scope can't see the
18
+ * library's internal aliases (mirrors `validateExtends`).
19
+ */
20
+ export function validateSchemaTypeRefs(manifests, registry, aliases, aliasesByModule, rootModules) {
21
+ const diagnostics = [];
22
+ for (const m of manifests) {
23
+ const ownModule = m.metadata?.module;
24
+ const name = m.metadata?.name;
25
+ if (!name)
26
+ continue;
27
+ // Only validate refs authored in a root module's scope; imported defs are
28
+ // validated against their own library when it's analyzed as a root.
29
+ if (ownModule && !rootModules.has(ownModule))
30
+ continue;
31
+ const resolver = (ownModule ? aliasesByModule.get(ownModule) : undefined) ?? aliases;
32
+ const filePath = m.metadata?.source;
33
+ const label = `${m.kind}/${name}`;
34
+ const walk = (value, path) => {
35
+ if (value === null || typeof value !== "object")
36
+ return;
37
+ if (Array.isArray(value)) {
38
+ value.forEach((item, i) => walk(item, `${path}[${i}]`));
39
+ return;
40
+ }
41
+ const obj = value;
42
+ const parsed = parseTeloTypeRef(obj.$ref);
43
+ if (parsed) {
44
+ const module = parsed.authority === "Self" ? ownModule : resolver.moduleForAlias(parsed.authority);
45
+ if (!module) {
46
+ diagnostics.push({
47
+ severity: DiagnosticSeverity.Error,
48
+ code: "SCHEMA_TYPE_REF_UNKNOWN_ALIAS",
49
+ source: SOURCE,
50
+ message: `${label}: schema $ref '${obj.$ref}' — '${parsed.authority}' is not 'Self' or a ` +
51
+ `Telo.Import in this module. Declare the import or correct the authority.`,
52
+ data: { resource: { kind: m.kind, name }, filePath, path: `${path}/$ref` },
53
+ });
54
+ }
55
+ else if (!registry.hasSchemaId(canonicalTypeSchemaId(module, parsed.typeName))) {
56
+ diagnostics.push({
57
+ severity: DiagnosticSeverity.Error,
58
+ code: "SCHEMA_TYPE_REF_UNRESOLVED",
59
+ source: SOURCE,
60
+ message: `${label}: schema $ref '${obj.$ref}' resolves to module '${module}', which declares ` +
61
+ `no Telo.Type named '${parsed.typeName}'.`,
62
+ data: { resource: { kind: m.kind, name }, filePath, path: `${path}/$ref` },
63
+ });
64
+ }
65
+ }
66
+ for (const key of Object.keys(obj))
67
+ walk(obj[key], `${path}/${key}`);
68
+ };
69
+ for (const field of SCHEMA_FIELDS) {
70
+ walk(m[field], field);
71
+ }
72
+ }
73
+ return diagnostics;
74
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.24.1",
3
+ "version": "0.26.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.26.0"
51
+ "@telorun/sdk": "0.34.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
package/src/analyzer.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import { canonicalTypeSchemaId } from "@telorun/sdk";
2
3
  import type { Environment } from "@marcbachmann/cel-js";
3
4
  import { defaultRegistry, isTaggedSentinel } from "@telorun/templating";
4
5
  import { AliasResolver, scopeResolverForModule } from "./alias-resolver.js";
@@ -18,6 +19,8 @@ import { isModuleKind } from "./module-kinds.js";
18
19
  import { normalizeInlineResources } from "./normalize-inline-resources.js";
19
20
  import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
20
21
  import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
22
+ import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
23
+ import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
21
24
  import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
22
25
  import {
23
26
  celTypeSatisfiesJsonSchema,
@@ -800,6 +803,32 @@ export class StaticAnalyzer {
800
803
  // original and inline-extracted manifests have their sentinels resolved.
801
804
  resolveRefSentinels(allManifests, aliases, aliasesByModule);
802
805
 
806
+ // Phase 2.6: register each named `Telo.Type` resource's schema under its
807
+ // canonical module-scoped id (`telo://<module>/<name>`), validate
808
+ // `telo://Self|Alias/Type` schema refs resolve to one, then rewrite those
809
+ // refs to the canonical id so AJV resolves them at compile time. Register
810
+ // and validate BEFORE the rewrite, while the authored authority is intact.
811
+ for (const m of allManifests) {
812
+ const ownModule = (m.metadata as { module?: string } | undefined)?.module;
813
+ if (!ownModule || !m.metadata?.name || typeof m.schema !== "object" || m.schema === null) {
814
+ continue;
815
+ }
816
+ const scopeResolver =
817
+ rootModules.has(ownModule) ? aliases : (aliasesByModule.get(ownModule) ?? new AliasResolver());
818
+ const canonicalKind = scopeResolver.resolveKind(m.kind as string) ?? (m.kind as string);
819
+ if (defs.resolve(canonicalKind)?.capability !== "Telo.Type") continue;
820
+ defs.registerNamedTypeSchema(
821
+ canonicalTypeSchemaId(ownModule, m.metadata.name as string),
822
+ m.schema as Record<string, any>,
823
+ );
824
+ }
825
+ if (!options?.skipValidation) {
826
+ diagnostics.push(
827
+ ...validateSchemaTypeRefs(allManifests, defs, aliases, aliasesByModule, rootModules),
828
+ );
829
+ }
830
+ resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
831
+
803
832
  // Trusted-input fast path: when the caller has already attested that
804
833
  // this exact manifest set passes analysis (e.g. via the kernel's
805
834
  // hash-stamped `.validated.json` cache), skip the validation walk.
@@ -1350,6 +1379,10 @@ export class StaticAnalyzer {
1350
1379
  // inline-extracted manifests get their refs canonicalized to
1351
1380
  // {kind, name} for the kernel that consumes this output.
1352
1381
  resolveRefSentinels(normalized, ctx.aliases, ctx.aliasesByModule, crossModuleTargets ?? []);
1382
+ // Canonicalize import-scoped schema `$ref`s (`telo://Self|Alias/Type`) so the
1383
+ // kernel that executes this output compiles inputs/outputs against the same
1384
+ // ids the type controllers register their schemas under.
1385
+ resolveSchemaTypeRefs(normalized, ctx.aliases, ctx.aliasesByModule);
1353
1386
  return normalized;
1354
1387
  }
1355
1388
 
package/src/builtins.ts CHANGED
@@ -300,6 +300,16 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
300
300
  type: "array",
301
301
  items: { type: "string" },
302
302
  },
303
+ // Files bundled alongside `telo.yaml` into the module's registry
304
+ // artifact (`module.tar.gz`) — static assets served by Http.Static,
305
+ // templates, etc. Ordered `.gitignore`-style patterns resolved against
306
+ // the manifest dir at publish time. Analyzer-only role: accept the
307
+ // field (the schema is additionalProperties:false); the analyzer never
308
+ // reads the assets. See kernel/nodejs/plans/bundle-controllers.md.
309
+ files: {
310
+ type: "array",
311
+ items: { type: "string" },
312
+ },
303
313
  // Inline imports — name-keyed map sugar for separate `Telo.Import`
304
314
  // documents. The key is the PascalCase alias (the import's
305
315
  // `metadata.name`). Each value is either a bare source string
@@ -424,6 +434,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
424
434
  type: "array",
425
435
  items: { type: "string" },
426
436
  },
437
+ // Files bundled into the module's registry artifact — same semantics as
438
+ // the Telo.Application `files` field above (a library may ship bundled
439
+ // templates, migrations, seed data).
440
+ files: {
441
+ type: "array",
442
+ items: { type: "string" },
443
+ },
427
444
  // Inline imports — same name-keyed map sugar as Telo.Application; the
428
445
  // loader desugars each entry into a synthetic Telo.Import. See the
429
446
  // Application schema above and analyzer/nodejs/src/inline-imports.ts.
@@ -110,6 +110,23 @@ export class DefinitionRegistry {
110
110
  }
111
111
  }
112
112
 
113
+ /** Registers a named `Telo.Type` resource's schema under its canonical
114
+ * module-scoped URI `$id` (`telo://<module>/<name>`), so a sibling schema's
115
+ * `$ref: "telo://Self/<name>"` (rewritten to the canonical form by
116
+ * `resolveSchemaTypeRefs`) resolves during AJV compilation. Mirrors the
117
+ * kernel type controller's `registerSchema(canonicalTypeSchemaId(...))`. */
118
+ registerNamedTypeSchema(id: string, schema: Record<string, any>): void {
119
+ if (this.registeredSchemaIds.has(id) || this.ajv.getSchema(id)) return;
120
+ this.ajv.addSchema(schema, id);
121
+ this.registeredSchemaIds.add(id);
122
+ }
123
+
124
+ /** True when a schema is registered under `id` (a canonical `telo://` type id
125
+ * or a definition `$id`). Used to flag schema `$ref`s that resolve to nothing. */
126
+ hasSchemaId(id: string): boolean {
127
+ return this.registeredSchemaIds.has(id) || this.ajv.getSchema(id) !== undefined;
128
+ }
129
+
113
130
  /** Computes the $id for a definition schema: "<identity>/<TypeName>".
114
131
  * Returns undefined when the module identity is not yet registered. */
115
132
  computeId(moduleName: string, typeName: string): string | undefined {
@@ -23,7 +23,7 @@ type ResolvedRef = { kind: string; name: string; alias?: string };
23
23
  * Resolving a sentinel here does NOT cause Phase-5 injection: that pass is
24
24
  * driven by the field map, which still excludes step `invoke`s, so a resolved
25
25
  * step invoke stays `{kind, name}` and is dispatched through
26
- * `executeInvokeStep` (preserving `<Kind>.<Name>.Invoked` events) rather than
26
+ * `executeInvokeStep` (preserving `<name>.Invoked` events) rather than
27
27
  * being replaced with a live instance.
28
28
  *
29
29
  * Reference grammar — the tag's source string is split on the FIRST dot:
@@ -0,0 +1,53 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
3
+ import type { AliasResolver } from "./alias-resolver.js";
4
+
5
+ /** Schema-bearing fields on a Telo.Definition / Telo.Type resource. */
6
+ const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
7
+
8
+ /**
9
+ * Rewrites import-scoped schema references in place. A `$ref` of the form
10
+ * `telo://<authority>/<typeName>` names a `Type.JsonSchema` (or any `Telo.Type`)
11
+ * reached through an import: `telo://Self/<type>` for the declaring module's own
12
+ * type, `telo://<Alias>/<type>` for an imported module's. Each authority is
13
+ * resolved to the owning module's name and the ref is rewritten to the canonical
14
+ * `telo://<module>/<type>` the type registered its schema under.
15
+ *
16
+ * The version lives on the `imports:` entry, never the URI — only the pinned
17
+ * version is loaded, so the canonical id is version-free.
18
+ *
19
+ * Already-canonical refs (authority is a real module name, not an alias) and
20
+ * fragment-bearing built-ins (`telo://manifest#/$defs/ResourceRef`) are left
21
+ * untouched: the former because the authority resolves to nothing, the latter
22
+ * because they don't match the `authority/type` grammar.
23
+ */
24
+ export function resolveSchemaTypeRefs(
25
+ resources: ResourceManifest[],
26
+ aliases?: AliasResolver,
27
+ aliasesByModule?: Map<string, AliasResolver>,
28
+ ): void {
29
+ const walk = (value: unknown, resolveAuthority: (authority: string) => string | undefined): void => {
30
+ if (value === null || typeof value !== "object") return;
31
+ if (Array.isArray(value)) {
32
+ for (const item of value) walk(item, resolveAuthority);
33
+ return;
34
+ }
35
+ const obj = value as Record<string, unknown>;
36
+ const parsed = parseTeloTypeRef(obj.$ref);
37
+ if (parsed) {
38
+ const module = resolveAuthority(parsed.authority);
39
+ if (module) obj.$ref = canonicalTypeSchemaId(module, parsed.typeName);
40
+ }
41
+ for (const key of Object.keys(obj)) walk(obj[key], resolveAuthority);
42
+ };
43
+
44
+ for (const r of resources) {
45
+ const ownModule = (r.metadata as { module?: string } | undefined)?.module;
46
+ const resolver = (ownModule ? aliasesByModule?.get(ownModule) : undefined) ?? aliases;
47
+ const resolveAuthority = (authority: string): string | undefined =>
48
+ authority === "Self" ? ownModule : resolver?.moduleForAlias(authority);
49
+ for (const field of SCHEMA_FIELDS) {
50
+ walk((r as Record<string, unknown>)[field], resolveAuthority);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,86 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
3
+ import type { AliasResolver } from "./alias-resolver.js";
4
+ import type { DefinitionRegistry } from "./definition-registry.js";
5
+ import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
6
+
7
+ const SOURCE = "telo-analyzer";
8
+ const SCHEMA_FIELDS = ["schema", "inputType", "outputType"] as const;
9
+
10
+ /**
11
+ * Validates module-scoped schema `$ref`s of the form `telo://<authority>/<type>`.
12
+ * The authority is an import alias (or `Self`) declared in the resource's module;
13
+ * the type must be a registered `Telo.Type` resource in the module it resolves to.
14
+ *
15
+ * Diagnostics (both errors — a `$ref` that resolves to nothing is never validated
16
+ * by AJV, so without this it would silently pass):
17
+ * - SCHEMA_TYPE_REF_UNKNOWN_ALIAS: the authority is neither `Self` nor a declared import.
18
+ * - SCHEMA_TYPE_REF_UNRESOLVED: the authority resolves to a module, but that module
19
+ * declares no `Telo.Type` named `<type>`.
20
+ *
21
+ * Forwarded/imported definitions are skipped — their own refs are validated when the
22
+ * owning library is analyzed as a root, and the consumer's scope can't see the
23
+ * library's internal aliases (mirrors `validateExtends`).
24
+ */
25
+ export function validateSchemaTypeRefs(
26
+ manifests: ResourceManifest[],
27
+ registry: DefinitionRegistry,
28
+ aliases: AliasResolver,
29
+ aliasesByModule: Map<string, AliasResolver>,
30
+ rootModules: Set<string>,
31
+ ): AnalysisDiagnostic[] {
32
+ const diagnostics: AnalysisDiagnostic[] = [];
33
+
34
+ for (const m of manifests) {
35
+ const ownModule = (m.metadata as { module?: string } | undefined)?.module;
36
+ const name = m.metadata?.name as string | undefined;
37
+ if (!name) continue;
38
+ // Only validate refs authored in a root module's scope; imported defs are
39
+ // validated against their own library when it's analyzed as a root.
40
+ if (ownModule && !rootModules.has(ownModule)) continue;
41
+ const resolver = (ownModule ? aliasesByModule.get(ownModule) : undefined) ?? aliases;
42
+ const filePath = (m.metadata as { source?: string } | undefined)?.source;
43
+ const label = `${m.kind}/${name}`;
44
+
45
+ const walk = (value: unknown, path: string): void => {
46
+ if (value === null || typeof value !== "object") return;
47
+ if (Array.isArray(value)) {
48
+ value.forEach((item, i) => walk(item, `${path}[${i}]`));
49
+ return;
50
+ }
51
+ const obj = value as Record<string, unknown>;
52
+ const parsed = parseTeloTypeRef(obj.$ref);
53
+ if (parsed) {
54
+ const module = parsed.authority === "Self" ? ownModule : resolver.moduleForAlias(parsed.authority);
55
+ if (!module) {
56
+ diagnostics.push({
57
+ severity: DiagnosticSeverity.Error,
58
+ code: "SCHEMA_TYPE_REF_UNKNOWN_ALIAS",
59
+ source: SOURCE,
60
+ message:
61
+ `${label}: schema $ref '${obj.$ref}' — '${parsed.authority}' is not 'Self' or a ` +
62
+ `Telo.Import in this module. Declare the import or correct the authority.`,
63
+ data: { resource: { kind: m.kind, name }, filePath, path: `${path}/$ref` },
64
+ });
65
+ } else if (!registry.hasSchemaId(canonicalTypeSchemaId(module, parsed.typeName))) {
66
+ diagnostics.push({
67
+ severity: DiagnosticSeverity.Error,
68
+ code: "SCHEMA_TYPE_REF_UNRESOLVED",
69
+ source: SOURCE,
70
+ message:
71
+ `${label}: schema $ref '${obj.$ref}' resolves to module '${module}', which declares ` +
72
+ `no Telo.Type named '${parsed.typeName}'.`,
73
+ data: { resource: { kind: m.kind, name }, filePath, path: `${path}/$ref` },
74
+ });
75
+ }
76
+ }
77
+ for (const key of Object.keys(obj)) walk(obj[key], `${path}/${key}`);
78
+ };
79
+
80
+ for (const field of SCHEMA_FIELDS) {
81
+ walk((m as Record<string, unknown>)[field], field);
82
+ }
83
+ }
84
+
85
+ return diagnostics;
86
+ }