@telorun/analyzer 0.24.0 → 0.25.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 +2 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +27 -0
- package/dist/definition-registry.d.ts +9 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +16 -0
- package/dist/resolve-ref-sentinels.d.ts +1 -1
- package/dist/resolve-ref-sentinels.js +1 -1
- package/dist/resolve-schema-type-refs.d.ts +20 -0
- package/dist/resolve-schema-type-refs.d.ts.map +1 -0
- package/dist/resolve-schema-type-refs.js +47 -0
- package/dist/validate-schema-type-refs.d.ts +21 -0
- package/dist/validate-schema-type-refs.d.ts.map +1 -0
- package/dist/validate-schema-type-refs.js +74 -0
- package/package.json +3 -3
- package/src/analyzer.ts +33 -0
- package/src/definition-registry.ts +17 -0
- package/src/resolve-ref-sentinels.ts +1 -1
- package/src/resolve-schema-type-refs.ts +53 -0
- package/src/validate-schema-type-refs.ts +86 -0
package/README.md
CHANGED
|
@@ -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.
|
|
59
|
-
Sql: std/sql@0.9.
|
|
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
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -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;
|
|
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) {
|
|
@@ -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 `<
|
|
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 `<
|
|
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.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"ajv-formats": "^3.0.1",
|
|
43
43
|
"jsonpath-plus": "^10.3.0",
|
|
44
44
|
"yaml": "^2.8.3",
|
|
45
|
-
"@telorun/templating": "0.
|
|
45
|
+
"@telorun/templating": "0.10.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "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
|
|
|
@@ -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 `<
|
|
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
|
+
}
|