@telorun/analyzer 0.11.0 → 1.1.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 +3 -3
- package/dist/analysis-registry.d.ts +7 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +38 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +44 -9
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +44 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +9 -11
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +26 -14
- package/dist/position-metadata.d.ts +5 -1
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +8 -1
- package/dist/reference-field-map.d.ts +21 -4
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +35 -19
- package/dist/residual-schema.d.ts +23 -0
- package/dist/residual-schema.d.ts.map +1 -0
- package/dist/residual-schema.js +45 -0
- package/dist/rewrite-synthetic-origins.d.ts +10 -0
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
- package/dist/rewrite-synthetic-origins.js +55 -0
- package/dist/validate-cel-context.d.ts +5 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +27 -15
- package/dist/validate-provider-coherence.d.ts +23 -0
- package/dist/validate-provider-coherence.d.ts.map +1 -0
- package/dist/validate-provider-coherence.js +148 -0
- package/dist/validate-references.js +24 -24
- package/package.json +5 -3
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +45 -11
- package/src/builtins.ts +44 -1
- package/src/index.ts +1 -0
- package/src/kernel-globals.ts +9 -11
- package/src/normalize-inline-resources.ts +48 -13
- package/src/position-metadata.ts +8 -1
- package/src/reference-field-map.ts +46 -18
- package/src/residual-schema.ts +49 -0
- package/src/rewrite-synthetic-origins.ts +75 -0
- package/src/validate-cel-context.ts +28 -15
- package/src/validate-provider-coherence.ts +166 -0
- package/src/validate-references.ts +24 -24
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the residual JSON Schema for a `variables` / `secrets` entry.
|
|
3
|
+
*
|
|
4
|
+
* For Telo.Application env-binding entries (those with an `env:` key), strips
|
|
5
|
+
* the kernel-specific wrapper keys `env` and `default` — `default` here is
|
|
6
|
+
* the *fallback host value* the kernel coerces when the env var is unset, not
|
|
7
|
+
* a JSON Schema annotation, so it must not leak into the validator.
|
|
8
|
+
*
|
|
9
|
+
* For Telo.Library entries (no `env:`), passes the entry through unchanged.
|
|
10
|
+
* Library `default:` is a standard JSON Schema annotation and stays.
|
|
11
|
+
*
|
|
12
|
+
* Single source of truth for "residual schema" referenced by both the
|
|
13
|
+
* analyzer's CEL globals normalization and the kernel's runtime env-var
|
|
14
|
+
* resolver — keeping them aligned prevents the two surfaces from drifting.
|
|
15
|
+
*/
|
|
16
|
+
export declare function residualEntrySchema(entry: Record<string, unknown> | null | undefined): Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Apply `residualEntrySchema` to every entry in a `variables` / `secrets` map.
|
|
19
|
+
* Returns a property-map suitable for use as the inner schema of CEL's
|
|
20
|
+
* `variables` / `secrets` namespaces.
|
|
21
|
+
*/
|
|
22
|
+
export declare function residualEntrySchemaMap(entries: Record<string, unknown> | null | undefined): Record<string, Record<string, unknown>>;
|
|
23
|
+
//# sourceMappingURL=residual-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"residual-schema.d.ts","sourceRoot":"","sources":["../src/residual-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAChD,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAWzB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAClD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAWzC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the residual JSON Schema for a `variables` / `secrets` entry.
|
|
3
|
+
*
|
|
4
|
+
* For Telo.Application env-binding entries (those with an `env:` key), strips
|
|
5
|
+
* the kernel-specific wrapper keys `env` and `default` — `default` here is
|
|
6
|
+
* the *fallback host value* the kernel coerces when the env var is unset, not
|
|
7
|
+
* a JSON Schema annotation, so it must not leak into the validator.
|
|
8
|
+
*
|
|
9
|
+
* For Telo.Library entries (no `env:`), passes the entry through unchanged.
|
|
10
|
+
* Library `default:` is a standard JSON Schema annotation and stays.
|
|
11
|
+
*
|
|
12
|
+
* Single source of truth for "residual schema" referenced by both the
|
|
13
|
+
* analyzer's CEL globals normalization and the kernel's runtime env-var
|
|
14
|
+
* resolver — keeping them aligned prevents the two surfaces from drifting.
|
|
15
|
+
*/
|
|
16
|
+
export function residualEntrySchema(entry) {
|
|
17
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
18
|
+
return { type: "object", additionalProperties: true };
|
|
19
|
+
}
|
|
20
|
+
const isAppEnvBinding = "env" in entry;
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
23
|
+
if (isAppEnvBinding && (key === "env" || key === "default"))
|
|
24
|
+
continue;
|
|
25
|
+
out[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Apply `residualEntrySchema` to every entry in a `variables` / `secrets` map.
|
|
31
|
+
* Returns a property-map suitable for use as the inner schema of CEL's
|
|
32
|
+
* `variables` / `secrets` namespaces.
|
|
33
|
+
*/
|
|
34
|
+
export function residualEntrySchemaMap(entries) {
|
|
35
|
+
const out = {};
|
|
36
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
40
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
41
|
+
out[name] = residualEntrySchema(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import type { AnalysisDiagnostic } from "./types.js";
|
|
3
|
+
/** Diagnostics emitted on synthetic manifests (resources extracted by
|
|
4
|
+
* `normalizeInlineResources`) carry the synthetic's identity in
|
|
5
|
+
* `data.resource`, which has no YAML source. Rewrite each such diagnostic
|
|
6
|
+
* back to the chain root: walk up `metadata.xTeloOrigin` until a manifest
|
|
7
|
+
* with no origin is reached, and prepend each hop's `pathFromParent` to
|
|
8
|
+
* `data.path` so position-index lookups against the root doc resolve. */
|
|
9
|
+
export declare function rewriteSyntheticOrigins(diagnostics: AnalysisDiagnostic[], manifests: ResourceManifest[]): AnalysisDiagnostic[];
|
|
10
|
+
//# sourceMappingURL=rewrite-synthetic-origins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-synthetic-origins.d.ts","sourceRoot":"","sources":["../src/rewrite-synthetic-origins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAsBrD;;;;;0EAK0E;AAC1E,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,kBAAkB,EAAE,EACjC,SAAS,EAAE,gBAAgB,EAAE,GAC5B,kBAAkB,EAAE,CA0CtB"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
function readOrigin(manifest) {
|
|
2
|
+
if (!manifest)
|
|
3
|
+
return undefined;
|
|
4
|
+
const origin = manifest.metadata?.xTeloOrigin;
|
|
5
|
+
if (!origin ||
|
|
6
|
+
typeof origin.parentKind !== "string" ||
|
|
7
|
+
typeof origin.parentName !== "string" ||
|
|
8
|
+
typeof origin.pathFromParent !== "string") {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
return origin;
|
|
12
|
+
}
|
|
13
|
+
/** Diagnostics emitted on synthetic manifests (resources extracted by
|
|
14
|
+
* `normalizeInlineResources`) carry the synthetic's identity in
|
|
15
|
+
* `data.resource`, which has no YAML source. Rewrite each such diagnostic
|
|
16
|
+
* back to the chain root: walk up `metadata.xTeloOrigin` until a manifest
|
|
17
|
+
* with no origin is reached, and prepend each hop's `pathFromParent` to
|
|
18
|
+
* `data.path` so position-index lookups against the root doc resolve. */
|
|
19
|
+
export function rewriteSyntheticOrigins(diagnostics, manifests) {
|
|
20
|
+
const byName = new Map();
|
|
21
|
+
for (const m of manifests) {
|
|
22
|
+
const name = m.metadata?.name;
|
|
23
|
+
if (typeof name === "string")
|
|
24
|
+
byName.set(name, m);
|
|
25
|
+
}
|
|
26
|
+
return diagnostics.map((d) => {
|
|
27
|
+
const data = d.data;
|
|
28
|
+
if (!data?.resource?.name)
|
|
29
|
+
return d;
|
|
30
|
+
let current = byName.get(data.resource.name);
|
|
31
|
+
let origin = readOrigin(current);
|
|
32
|
+
if (!origin)
|
|
33
|
+
return d;
|
|
34
|
+
let accumPath = typeof data.path === "string" ? data.path : "";
|
|
35
|
+
let rootKind = origin.parentKind;
|
|
36
|
+
let rootName = origin.parentName;
|
|
37
|
+
while (origin) {
|
|
38
|
+
accumPath = accumPath ? `${origin.pathFromParent}.${accumPath}` : origin.pathFromParent;
|
|
39
|
+
rootKind = origin.parentKind;
|
|
40
|
+
rootName = origin.parentName;
|
|
41
|
+
current = byName.get(origin.parentName);
|
|
42
|
+
origin = readOrigin(current);
|
|
43
|
+
}
|
|
44
|
+
const rootFilePath = current?.metadata?.source ?? data.filePath;
|
|
45
|
+
return {
|
|
46
|
+
...d,
|
|
47
|
+
data: {
|
|
48
|
+
...data,
|
|
49
|
+
resource: { kind: rootKind, name: rootName },
|
|
50
|
+
filePath: rootFilePath,
|
|
51
|
+
path: accumPath,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -63,6 +63,11 @@ export declare function pathMatchesScope(exprPath: string, scope: string): boole
|
|
|
63
63
|
* `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
|
|
64
64
|
* and returns the `outputType` schema.
|
|
65
65
|
*
|
|
66
|
+
* Accepts either a single string or an array of strings. With an array, paths
|
|
67
|
+
* are tried in order and the first one that resolves to a usable schema wins —
|
|
68
|
+
* used by `result:` to find its dispatch target under whichever entry-point
|
|
69
|
+
* field (`provide:` or `invoke:`) the definition declares.
|
|
70
|
+
*
|
|
66
71
|
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
67
72
|
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
68
73
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED
|
|
1
|
+
{"version":3,"file":"validate-cel-context.d.ts","sourceRoot":"","sources":["../src/validate-cel-context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEtF,MAAM,WAAW,kBAAkB;IACjC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACnC;;kDAE8C;IAC9C,IAAI,CAAC,EAAE;QACL,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;KACxD,CAAC;IACF,OAAO,CAAC,EAAE;QACR,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;KAC/C,CAAC;IACF,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;CACtC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,OAAO,EACd,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CA6BjC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAoBzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACjC,IAAI,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAChD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAqGrB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC5B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQrB"}
|
|
@@ -94,6 +94,11 @@ export function pathMatchesScope(exprPath, scope) {
|
|
|
94
94
|
* `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
|
|
95
95
|
* and returns the `outputType` schema.
|
|
96
96
|
*
|
|
97
|
+
* Accepts either a single string or an array of strings. With an array, paths
|
|
98
|
+
* are tried in order and the first one that resolves to a usable schema wins —
|
|
99
|
+
* used by `result:` to find its dispatch target under whichever entry-point
|
|
100
|
+
* field (`provide:` or `invoke:`) the definition declares.
|
|
101
|
+
*
|
|
97
102
|
* - `x-telo-context-ref-from`: existing form — reads `{kind, name}` object from
|
|
98
103
|
* `manifestItem.<path>`, looks up the named manifest, returns its `<subpath>` field.
|
|
99
104
|
*
|
|
@@ -122,30 +127,37 @@ export function resolveContextAnnotations(schema, manifestItem, opts) {
|
|
|
122
127
|
};
|
|
123
128
|
}
|
|
124
129
|
const fromRoot = schema["x-telo-context-from-root"];
|
|
125
|
-
const
|
|
126
|
-
|
|
130
|
+
const fromRefKindRaw = schema["x-telo-context-from-ref-kind"];
|
|
131
|
+
const fromRefKinds = fromRefKindRaw == null
|
|
132
|
+
? []
|
|
133
|
+
: Array.isArray(fromRefKindRaw)
|
|
134
|
+
? fromRefKindRaw
|
|
135
|
+
: [fromRefKindRaw];
|
|
136
|
+
if (fromRoot || fromRefKinds.length > 0) {
|
|
127
137
|
if (fromRoot) {
|
|
128
138
|
const resolved = navigatePath(manifestRoot, fromRoot.split("/"));
|
|
129
139
|
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
130
140
|
return resolved;
|
|
131
141
|
}
|
|
132
142
|
}
|
|
133
|
-
if (
|
|
134
|
-
const
|
|
135
|
-
|
|
143
|
+
if (defs) {
|
|
144
|
+
for (const fromRefKind of fromRefKinds) {
|
|
145
|
+
const hashIdx = fromRefKind.indexOf("#");
|
|
146
|
+
if (hashIdx <= 0)
|
|
147
|
+
continue;
|
|
136
148
|
const refPath = fromRefKind.slice(0, hashIdx);
|
|
137
149
|
const field = fromRefKind.slice(hashIdx + 1);
|
|
138
150
|
const kindValue = navigatePath(manifestRoot, refPath.split("/"));
|
|
139
|
-
if (typeof kindValue
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
if (typeof kindValue !== "string" || kindValue.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
const canonical = aliases?.resolveKind(kindValue) ?? kindValue;
|
|
154
|
+
const def = defs.resolve(canonical);
|
|
155
|
+
const typeField = def
|
|
156
|
+
? def[field]
|
|
157
|
+
: undefined;
|
|
158
|
+
const resolved = resolveTypeFieldToSchema(typeField, allManifests ?? []);
|
|
159
|
+
if (resolved && typeof resolved === "object") {
|
|
160
|
+
return resolved;
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 coherence rules for `Telo.Definition` documents that use the `provide:`
|
|
7
|
+
* template target, plus the implementation-presence rule on `Telo.Provider`
|
|
8
|
+
* definitions.
|
|
9
|
+
*
|
|
10
|
+
* Diagnostics:
|
|
11
|
+
* - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
|
|
12
|
+
* `capability` is not `Telo.Provider`.
|
|
13
|
+
* - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
|
|
14
|
+
* on the same definition.
|
|
15
|
+
* - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
|
|
16
|
+
* `resources:`.
|
|
17
|
+
* - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
|
|
18
|
+
* kind is registered but not a `Telo.Invocable`.
|
|
19
|
+
* - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
|
|
20
|
+
* declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateProviderCoherence(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
|
|
23
|
+
//# sourceMappingURL=validate-provider-coherence.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-provider-coherence.d.ts","sourceRoot":"","sources":["../src/validate-provider-coherence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,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;AAIzE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CAyItB"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
2
|
+
const SOURCE = "telo-analyzer";
|
|
3
|
+
/**
|
|
4
|
+
* Validates coherence rules for `Telo.Definition` documents that use the `provide:`
|
|
5
|
+
* template target, plus the implementation-presence rule on `Telo.Provider`
|
|
6
|
+
* definitions.
|
|
7
|
+
*
|
|
8
|
+
* Diagnostics:
|
|
9
|
+
* - PROVIDE_ON_NON_PROVIDER: `provide:` declared on a definition whose
|
|
10
|
+
* `capability` is not `Telo.Provider`.
|
|
11
|
+
* - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
|
|
12
|
+
* on the same definition.
|
|
13
|
+
* - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
|
|
14
|
+
* `resources:`.
|
|
15
|
+
* - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
|
|
16
|
+
* kind is registered but not a `Telo.Invocable`.
|
|
17
|
+
* - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
|
|
18
|
+
* declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
|
|
19
|
+
*/
|
|
20
|
+
export function validateProviderCoherence(manifests, registry, aliases) {
|
|
21
|
+
const diagnostics = [];
|
|
22
|
+
const importedModules = new Set();
|
|
23
|
+
for (const m of manifests) {
|
|
24
|
+
if (m.kind !== "Telo.Import")
|
|
25
|
+
continue;
|
|
26
|
+
const resolved = m.metadata
|
|
27
|
+
?.resolvedModuleName;
|
|
28
|
+
if (resolved)
|
|
29
|
+
importedModules.add(resolved);
|
|
30
|
+
}
|
|
31
|
+
for (const m of manifests) {
|
|
32
|
+
if (m.kind !== "Telo.Definition")
|
|
33
|
+
continue;
|
|
34
|
+
const name = m.metadata?.name;
|
|
35
|
+
if (!name)
|
|
36
|
+
continue;
|
|
37
|
+
const ownModule = m.metadata?.module;
|
|
38
|
+
if (ownModule && importedModules.has(ownModule))
|
|
39
|
+
continue;
|
|
40
|
+
const filePath = m.metadata?.source;
|
|
41
|
+
const resource = { kind: m.kind, name };
|
|
42
|
+
const label = `${m.kind}/${name}`;
|
|
43
|
+
const md = m;
|
|
44
|
+
const capability = typeof md.capability === "string" ? md.capability : undefined;
|
|
45
|
+
const provide = md.provide;
|
|
46
|
+
const invoke = md.invoke;
|
|
47
|
+
const run = md.run;
|
|
48
|
+
const controllers = md.controllers;
|
|
49
|
+
const resources = md.resources;
|
|
50
|
+
const hasProvide = provide !== undefined && provide !== null;
|
|
51
|
+
const hasInvoke = invoke !== undefined && invoke !== null;
|
|
52
|
+
const hasRun = run !== undefined && run !== null;
|
|
53
|
+
const hasControllers = Array.isArray(controllers) && controllers.length > 0;
|
|
54
|
+
if (hasProvide && capability !== "Telo.Provider") {
|
|
55
|
+
diagnostics.push({
|
|
56
|
+
severity: DiagnosticSeverity.Error,
|
|
57
|
+
code: "PROVIDE_ON_NON_PROVIDER",
|
|
58
|
+
source: SOURCE,
|
|
59
|
+
message: `${label}: 'provide:' is only valid on definitions with 'capability: Telo.Provider' ` +
|
|
60
|
+
`(found '${capability ?? "<unset>"}'). Use 'invoke:' or 'run:' for other capabilities.`,
|
|
61
|
+
data: { resource, filePath, path: "provide" },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (hasProvide && (hasInvoke || hasRun)) {
|
|
65
|
+
const conflict = hasInvoke ? "invoke" : "run";
|
|
66
|
+
diagnostics.push({
|
|
67
|
+
severity: DiagnosticSeverity.Error,
|
|
68
|
+
code: "PROVIDE_DISPATCHER_CONFLICT",
|
|
69
|
+
source: SOURCE,
|
|
70
|
+
message: `${label}: 'provide:' cannot co-exist with '${conflict}:'. ` +
|
|
71
|
+
`A definition declares exactly one dispatch entry-point.`,
|
|
72
|
+
data: { resource, filePath, path: "provide" },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (hasProvide && typeof provide === "object" && !Array.isArray(provide)) {
|
|
76
|
+
const provideObj = provide;
|
|
77
|
+
const providedName = typeof provideObj.name === "string" ? provideObj.name : undefined;
|
|
78
|
+
const providedKind = typeof provideObj.kind === "string" ? provideObj.kind : undefined;
|
|
79
|
+
if (providedName && Array.isArray(resources)) {
|
|
80
|
+
const match = resources.find((r) => {
|
|
81
|
+
const meta = r?.metadata;
|
|
82
|
+
return typeof meta?.name === "string" && meta.name === providedName;
|
|
83
|
+
});
|
|
84
|
+
if (!match) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
severity: DiagnosticSeverity.Error,
|
|
87
|
+
code: "PROVIDE_TARGET_UNKNOWN",
|
|
88
|
+
source: SOURCE,
|
|
89
|
+
message: `${label}: 'provide.name: ${providedName}' does not match any entry's ` +
|
|
90
|
+
`metadata.name in 'resources:'.`,
|
|
91
|
+
data: { resource, filePath, path: "provide.name" },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (typeof match.kind === "string") {
|
|
95
|
+
// `provide.kind` is the type contract the analyzer uses to type
|
|
96
|
+
// `result` CEL against the target's `outputType`. The runtime
|
|
97
|
+
// dispatches on `provide.name` and ignores `provide.kind`, so a
|
|
98
|
+
// mismatch silently degrades `result` typing to an open schema
|
|
99
|
+
// (and at runtime quietly invokes the actually-matched resource).
|
|
100
|
+
// Flag the divergence so result-typing never lies.
|
|
101
|
+
if (providedKind) {
|
|
102
|
+
const providedCanonical = aliases.resolveKind(providedKind) ?? providedKind;
|
|
103
|
+
const matchCanonical = aliases.resolveKind(match.kind) ?? match.kind;
|
|
104
|
+
if (providedCanonical !== matchCanonical) {
|
|
105
|
+
diagnostics.push({
|
|
106
|
+
severity: DiagnosticSeverity.Error,
|
|
107
|
+
code: "PROVIDE_KIND_MISMATCH",
|
|
108
|
+
source: SOURCE,
|
|
109
|
+
message: `${label}: 'provide.kind: ${providedKind}' disagrees with the matched ` +
|
|
110
|
+
`'resources:' entry's kind '${match.kind}' (matched by metadata.name ` +
|
|
111
|
+
`'${providedName}'). The runtime dispatches by name, so 'provide.kind' ` +
|
|
112
|
+
`is decorative — but the analyzer types 'result:' against it, and a ` +
|
|
113
|
+
`mismatch silently turns off that typing.`,
|
|
114
|
+
data: { resource, filePath, path: "provide.kind" },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
|
|
119
|
+
const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
|
|
120
|
+
if (targetDef && targetDef.kind === "Telo.Definition") {
|
|
121
|
+
const targetCap = targetDef.capability;
|
|
122
|
+
if (typeof targetCap === "string" && targetCap !== "Telo.Invocable") {
|
|
123
|
+
diagnostics.push({
|
|
124
|
+
severity: DiagnosticSeverity.Error,
|
|
125
|
+
code: "PROVIDE_TARGET_NOT_INVOCABLE",
|
|
126
|
+
source: SOURCE,
|
|
127
|
+
message: `${label}: 'provide.name: ${providedName}' resolves to a ${match.kind} ` +
|
|
128
|
+
`(capability '${targetCap}'); 'provide:' requires a Telo.Invocable target.`,
|
|
129
|
+
data: { resource, filePath, path: "provide.name" },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (capability === "Telo.Provider" && !hasControllers && !hasProvide) {
|
|
137
|
+
diagnostics.push({
|
|
138
|
+
severity: DiagnosticSeverity.Error,
|
|
139
|
+
code: "PROVIDER_MISSING_IMPLEMENTATION",
|
|
140
|
+
source: SOURCE,
|
|
141
|
+
message: `${label}: 'capability: Telo.Provider' requires either 'controllers:' ` +
|
|
142
|
+
`(TS-backed) or 'provide:' (template-backed) to declare an implementation.`,
|
|
143
|
+
data: { resource, filePath, path: "capability" },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return diagnostics;
|
|
148
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldValues } from "./reference-field-map.js";
|
|
1
|
+
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldEntries, resolveFieldValues } from "./reference-field-map.js";
|
|
2
2
|
import { navigateJsonPointer } from "./schema-compat.js";
|
|
3
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
4
|
const SOURCE = "telo-analyzer";
|
|
@@ -119,7 +119,7 @@ export function validateReferences(resources, context) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
for (const val of
|
|
122
|
+
for (const { value: val, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
123
123
|
if (!val)
|
|
124
124
|
continue;
|
|
125
125
|
// Name-only reference (plain string) — look up by name to validate.
|
|
@@ -142,8 +142,8 @@ export function validateReferences(resources, context) {
|
|
|
142
142
|
severity: DiagnosticSeverity.Error,
|
|
143
143
|
code: "UNRESOLVED_REFERENCE",
|
|
144
144
|
source: SOURCE,
|
|
145
|
-
message: `${resourceLabel}: reference at '${
|
|
146
|
-
data: { resource: resourceData, filePath, path:
|
|
145
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${val}' not found`,
|
|
146
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
147
147
|
});
|
|
148
148
|
continue;
|
|
149
149
|
}
|
|
@@ -153,8 +153,8 @@ export function validateReferences(resources, context) {
|
|
|
153
153
|
severity: DiagnosticSeverity.Error,
|
|
154
154
|
code: "REFERENCE_KIND_MISMATCH",
|
|
155
155
|
source: SOURCE,
|
|
156
|
-
message: `${resourceLabel}: reference at '${
|
|
157
|
-
data: { resource: resourceData, filePath, path:
|
|
156
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
157
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
continue;
|
|
@@ -171,8 +171,8 @@ export function validateReferences(resources, context) {
|
|
|
171
171
|
severity: DiagnosticSeverity.Error,
|
|
172
172
|
code: "INVALID_REFERENCE",
|
|
173
173
|
source: SOURCE,
|
|
174
|
-
message: `${resourceLabel}: reference at '${
|
|
175
|
-
data: { resource: resourceData, filePath, path:
|
|
174
|
+
message: `${resourceLabel}: reference at '${concretePath}' must have string 'kind' and 'name' fields`,
|
|
175
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
176
176
|
});
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
@@ -183,8 +183,8 @@ export function validateReferences(resources, context) {
|
|
|
183
183
|
severity: DiagnosticSeverity.Error,
|
|
184
184
|
code: "REFERENCE_KIND_MISMATCH",
|
|
185
185
|
source: SOURCE,
|
|
186
|
-
message: `${resourceLabel}: reference at '${
|
|
187
|
-
data: { resource: resourceData, filePath, path:
|
|
186
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
187
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
190
|
// 3. Resolution check — resource with this name must exist.
|
|
@@ -195,8 +195,8 @@ export function validateReferences(resources, context) {
|
|
|
195
195
|
severity: DiagnosticSeverity.Error,
|
|
196
196
|
code: "UNRESOLVED_REFERENCE",
|
|
197
197
|
source: SOURCE,
|
|
198
|
-
message: `${resourceLabel}: reference at '${
|
|
199
|
-
data: { resource: resourceData, filePath, path:
|
|
198
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${refVal.name}' not found`,
|
|
199
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -279,7 +279,7 @@ export function validateReferences(resources, context) {
|
|
|
279
279
|
});
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
282
|
-
for (const fieldValue of
|
|
282
|
+
for (const { value: fieldValue, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
283
283
|
if (fieldValue == null)
|
|
284
284
|
continue;
|
|
285
285
|
const issues = registry.validateWithRefs(fieldValue, subSchema);
|
|
@@ -288,8 +288,8 @@ export function validateReferences(resources, context) {
|
|
|
288
288
|
severity: DiagnosticSeverity.Error,
|
|
289
289
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
290
290
|
source: SOURCE,
|
|
291
|
-
message: `${resourceLabel}: '${
|
|
292
|
-
data: { resource: resourceData, filePath, path:
|
|
291
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
|
|
292
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
293
293
|
});
|
|
294
294
|
}
|
|
295
295
|
}
|
|
@@ -309,9 +309,9 @@ export function validateReferences(resources, context) {
|
|
|
309
309
|
const anchorValues = resolveFieldValues(r, anchorPath);
|
|
310
310
|
if (anchorValues.length === 0)
|
|
311
311
|
continue; // anchor field not set — nothing to validate
|
|
312
|
-
const
|
|
313
|
-
for (let i = 0; i <
|
|
314
|
-
const fieldValue =
|
|
312
|
+
const fieldEntries = resolveFieldEntries(r, fieldPath);
|
|
313
|
+
for (let i = 0; i < fieldEntries.length; i++) {
|
|
314
|
+
const { value: fieldValue, path: concretePath } = fieldEntries[i];
|
|
315
315
|
if (fieldValue == null)
|
|
316
316
|
continue;
|
|
317
317
|
// For absolute paths, the single anchor applies to all field values.
|
|
@@ -328,8 +328,8 @@ export function validateReferences(resources, context) {
|
|
|
328
328
|
severity: DiagnosticSeverity.Error,
|
|
329
329
|
code: "SCHEMA_FROM_MISSING_PATH",
|
|
330
330
|
source: SOURCE,
|
|
331
|
-
message: `${resourceLabel}: x-telo-schema-from at '${
|
|
332
|
-
data: { resource: resourceData, filePath, path:
|
|
331
|
+
message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema`,
|
|
332
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
333
333
|
});
|
|
334
334
|
continue;
|
|
335
335
|
}
|
|
@@ -339,8 +339,8 @@ export function validateReferences(resources, context) {
|
|
|
339
339
|
severity: DiagnosticSeverity.Error,
|
|
340
340
|
code: "SCHEMA_FROM_MISSING_PATH",
|
|
341
341
|
source: SOURCE,
|
|
342
|
-
message: `${resourceLabel}: x-telo-schema-from at '${
|
|
343
|
-
data: { resource: resourceData, filePath, path:
|
|
342
|
+
message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
|
|
343
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
344
344
|
});
|
|
345
345
|
continue;
|
|
346
346
|
}
|
|
@@ -350,8 +350,8 @@ export function validateReferences(resources, context) {
|
|
|
350
350
|
severity: DiagnosticSeverity.Error,
|
|
351
351
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
352
352
|
source: SOURCE,
|
|
353
|
-
message: `${resourceLabel}: '${
|
|
354
|
-
data: { resource: resourceData, filePath, path:
|
|
353
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
|
|
354
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,14 +41,16 @@
|
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
43
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/
|
|
45
|
-
"@telorun/templating": "0.2.3"
|
|
44
|
+
"@telorun/templating": "1.0.0"
|
|
46
45
|
},
|
|
47
46
|
"devDependencies": {
|
|
48
47
|
"@types/node": "^20.0.0",
|
|
49
48
|
"typescript": "^5.0.0",
|
|
50
49
|
"vitest": "^2.1.8"
|
|
51
50
|
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@telorun/sdk": "1.0.0"
|
|
53
|
+
},
|
|
52
54
|
"scripts": {
|
|
53
55
|
"build": "tsc -p tsconfig.lib.json",
|
|
54
56
|
"test": "vitest run",
|
package/src/analysis-registry.ts
CHANGED
|
@@ -101,6 +101,43 @@ export class AnalysisRegistry {
|
|
|
101
101
|
return computeSuggestKind(badKind, this.aliases, this.defs);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Returns every user-facing (alias-form) kind that satisfies the given
|
|
105
|
+
* `x-telo-ref` constraint string (e.g. `"telo#Invocable"`, `"std/sql#Connection"`).
|
|
106
|
+
* Resolution mirrors `validateReferences.checkKind`: abstract targets expand to
|
|
107
|
+
* the set of definitions extending them; concrete targets yield just themselves.
|
|
108
|
+
* Returns `undefined` when the ref can't be resolved (e.g. unregistered identity),
|
|
109
|
+
* so callers can fall back to the unfiltered kind list. */
|
|
110
|
+
userFacingKindsForRef(xTeloRef: string): string[] | undefined {
|
|
111
|
+
const targetKind = this.defs.resolveRef(xTeloRef);
|
|
112
|
+
if (!targetKind) return undefined;
|
|
113
|
+
const targetDef = this.defs.resolve(targetKind);
|
|
114
|
+
if (!targetDef) return undefined;
|
|
115
|
+
|
|
116
|
+
const canonicalKinds: string[] = [];
|
|
117
|
+
if (targetDef.kind === "Telo.Abstract") {
|
|
118
|
+
for (const def of this.defs.getByExtends(targetKind)) {
|
|
119
|
+
const module = (def.metadata as { module?: string } | undefined)?.module;
|
|
120
|
+
if (module && def.metadata?.name) {
|
|
121
|
+
canonicalKinds.push(`${module}.${def.metadata.name as string}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
canonicalKinds.push(targetKind);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const out = new Set<string>();
|
|
129
|
+
for (const kind of canonicalKinds) {
|
|
130
|
+
const dot = kind.indexOf(".");
|
|
131
|
+
if (dot === -1) continue;
|
|
132
|
+
const moduleName = kind.slice(0, dot);
|
|
133
|
+
const typeName = kind.slice(dot + 1);
|
|
134
|
+
for (const alias of this.aliases.aliasesFor(moduleName)) {
|
|
135
|
+
out.add(`${alias}.${typeName}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Array.from(out);
|
|
139
|
+
}
|
|
140
|
+
|
|
104
141
|
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
105
142
|
_context(): AnalysisContext {
|
|
106
143
|
return { aliases: this.aliases, definitions: this.defs, aliasesByModule: this.aliasesByModule };
|