@telorun/analyzer 0.26.0 → 0.27.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.
@@ -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,EAwe/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,EA2hB/C,CAAC"}
package/dist/builtins.js CHANGED
@@ -59,12 +59,28 @@ export const KERNEL_BUILTINS = [
59
59
  items: {
60
60
  type: "object",
61
61
  additionalProperties: true,
62
+ // Resource bodies are `self`-only for config: per-call `inputs` is
63
+ // NOT in scope here. Each entry is a persistent child created once at
64
+ // init() and reused, so its config cannot depend on call-time data —
65
+ // that flows through the top-level `inputs:` sibling into the dispatch
66
+ // target's invoke().
67
+ //
68
+ // The exception is CEL the child's OWN controller evaluates later
69
+ // against a runtime context it owns (e.g. an Http.Api evaluating route
70
+ // CEL per request). Those `request` / `result` / `steps` / `error`
71
+ // variables are deferred — the template controller preserves them
72
+ // untouched (see resource-template-controller.ts) — so they are
73
+ // exposed here permissively. Their deep shape is the child kind's
74
+ // concern, not the template's, so they type as open values.
62
75
  "x-telo-context": {
63
76
  type: "object",
64
77
  additionalProperties: false,
65
78
  properties: {
66
79
  self: { "x-telo-context-from-root": "schema" },
67
- inputs: { "x-telo-context-from-root": "inputType" },
80
+ request: {},
81
+ result: {},
82
+ steps: {},
83
+ error: {},
68
84
  },
69
85
  },
70
86
  },
@@ -127,6 +143,41 @@ export const KERNEL_BUILTINS = [
127
143
  },
128
144
  },
129
145
  },
146
+ // Mount dispatch: names the `resources:` entry (a Telo.Mount, e.g. an
147
+ // Http.Api) whose `register()` this definition delegates to. Same
148
+ // string / { kind, name } grammar as `invoke:`. The named child stays
149
+ // persistent so the produced mount's routes can `!ref` its siblings.
150
+ mount: {
151
+ oneOf: [
152
+ {
153
+ type: "string",
154
+ "x-telo-context": {
155
+ type: "object",
156
+ additionalProperties: false,
157
+ properties: {
158
+ self: { "x-telo-context-from-root": "schema" },
159
+ },
160
+ },
161
+ },
162
+ {
163
+ type: "object",
164
+ additionalProperties: true,
165
+ properties: {
166
+ kind: { type: "string" },
167
+ name: {
168
+ type: "string",
169
+ "x-telo-context": {
170
+ type: "object",
171
+ additionalProperties: false,
172
+ properties: {
173
+ self: { "x-telo-context-from-root": "schema" },
174
+ },
175
+ },
176
+ },
177
+ },
178
+ },
179
+ ],
180
+ },
130
181
  inputs: {
131
182
  type: "object",
132
183
  additionalProperties: true,
@@ -18,6 +18,14 @@ import { type AnalysisDiagnostic } from "./types.js";
18
18
  * kind is registered but not a `Telo.Invocable`.
19
19
  * - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
20
20
  * declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
21
+ * - MOUNT_ON_NON_MOUNT: `mount:` declared on a definition whose `capability` is
22
+ * not `Telo.Mount`.
23
+ * - MOUNT_DISPATCHER_CONFLICT: `mount:` co-exists with another dispatch
24
+ * entry-point (`invoke:` / `run:` / `provide:`).
25
+ * - MOUNT_TARGET_UNKNOWN: `mount.name` does not resolve to an entry in
26
+ * `resources:`.
27
+ * - MOUNT_TARGET_NOT_MOUNTABLE: `mount.name` resolves to a resource whose kind
28
+ * is registered but not a `Telo.Mount`.
21
29
  */
22
30
  export declare function validateProviderCoherence(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
23
31
  //# sourceMappingURL=validate-provider-coherence.d.ts.map
@@ -1 +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"}
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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CAsNtB"}
@@ -16,6 +16,14 @@ const SOURCE = "telo-analyzer";
16
16
  * kind is registered but not a `Telo.Invocable`.
17
17
  * - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
18
18
  * declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
19
+ * - MOUNT_ON_NON_MOUNT: `mount:` declared on a definition whose `capability` is
20
+ * not `Telo.Mount`.
21
+ * - MOUNT_DISPATCHER_CONFLICT: `mount:` co-exists with another dispatch
22
+ * entry-point (`invoke:` / `run:` / `provide:`).
23
+ * - MOUNT_TARGET_UNKNOWN: `mount.name` does not resolve to an entry in
24
+ * `resources:`.
25
+ * - MOUNT_TARGET_NOT_MOUNTABLE: `mount.name` resolves to a resource whose kind
26
+ * is registered but not a `Telo.Mount`.
19
27
  */
20
28
  export function validateProviderCoherence(manifests, registry, aliases) {
21
29
  const diagnostics = [];
@@ -45,11 +53,13 @@ export function validateProviderCoherence(manifests, registry, aliases) {
45
53
  const provide = md.provide;
46
54
  const invoke = md.invoke;
47
55
  const run = md.run;
56
+ const mount = md.mount;
48
57
  const controllers = md.controllers;
49
58
  const resources = md.resources;
50
59
  const hasProvide = provide !== undefined && provide !== null;
51
60
  const hasInvoke = invoke !== undefined && invoke !== null;
52
61
  const hasRun = run !== undefined && run !== null;
62
+ const hasMount = mount !== undefined && mount !== null;
53
63
  const hasControllers = Array.isArray(controllers) && controllers.length > 0;
54
64
  if (hasProvide && capability !== "Telo.Provider") {
55
65
  diagnostics.push({
@@ -133,6 +143,77 @@ export function validateProviderCoherence(manifests, registry, aliases) {
133
143
  }
134
144
  }
135
145
  }
146
+ if (hasMount && capability !== "Telo.Mount") {
147
+ diagnostics.push({
148
+ severity: DiagnosticSeverity.Error,
149
+ code: "MOUNT_ON_NON_MOUNT",
150
+ source: SOURCE,
151
+ message: `${label}: 'mount:' is only valid on definitions with 'capability: Telo.Mount' ` +
152
+ `(found '${capability ?? "<unset>"}'). Use 'invoke:' / 'run:' / 'provide:' for other capabilities.`,
153
+ data: { resource, filePath, path: "mount" },
154
+ });
155
+ }
156
+ if (hasMount && (hasInvoke || hasRun || hasProvide)) {
157
+ const conflict = hasInvoke ? "invoke" : hasRun ? "run" : "provide";
158
+ diagnostics.push({
159
+ severity: DiagnosticSeverity.Error,
160
+ code: "MOUNT_DISPATCHER_CONFLICT",
161
+ source: SOURCE,
162
+ message: `${label}: 'mount:' cannot co-exist with '${conflict}:'. ` +
163
+ `A definition declares exactly one dispatch entry-point.`,
164
+ data: { resource, filePath, path: "mount" },
165
+ });
166
+ }
167
+ if (hasMount) {
168
+ // Resolve the target's name from either form: the bare string (the
169
+ // primary, documented form — `mount: api`) or the object's `name`. A CEL
170
+ // target (`${{ … }}`) can only be checked at runtime, so skip those.
171
+ let mountedName;
172
+ if (typeof mount === "string") {
173
+ if (!mount.includes("${{"))
174
+ mountedName = mount;
175
+ }
176
+ else if (typeof mount === "object" && !Array.isArray(mount)) {
177
+ const mountObj = mount;
178
+ if (typeof mountObj.name === "string" && !mountObj.name.includes("${{")) {
179
+ mountedName = mountObj.name;
180
+ }
181
+ }
182
+ const mountPath = typeof mount === "string" ? "mount" : "mount.name";
183
+ if (mountedName && Array.isArray(resources)) {
184
+ const match = resources.find((r) => {
185
+ const meta = r?.metadata;
186
+ return typeof meta?.name === "string" && meta.name === mountedName;
187
+ });
188
+ if (!match) {
189
+ diagnostics.push({
190
+ severity: DiagnosticSeverity.Error,
191
+ code: "MOUNT_TARGET_UNKNOWN",
192
+ source: SOURCE,
193
+ message: `${label}: '${mountPath}: ${mountedName}' does not match any entry's ` +
194
+ `metadata.name in 'resources:'.`,
195
+ data: { resource, filePath, path: mountPath },
196
+ });
197
+ }
198
+ else if (typeof match.kind === "string") {
199
+ const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
200
+ const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
201
+ if (targetDef && targetDef.kind === "Telo.Definition") {
202
+ const targetCap = targetDef.capability;
203
+ if (typeof targetCap === "string" && targetCap !== "Telo.Mount") {
204
+ diagnostics.push({
205
+ severity: DiagnosticSeverity.Error,
206
+ code: "MOUNT_TARGET_NOT_MOUNTABLE",
207
+ source: SOURCE,
208
+ message: `${label}: '${mountPath}: ${mountedName}' resolves to a ${match.kind} ` +
209
+ `(capability '${targetCap}'); 'mount:' requires a Telo.Mount target.`,
210
+ data: { resource, filePath, path: mountPath },
211
+ });
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
136
217
  if (capability === "Telo.Provider" && !hasControllers && !hasProvide) {
137
218
  diagnostics.push({
138
219
  severity: DiagnosticSeverity.Error,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.26.0",
3
+ "version": "0.27.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.34.0"
51
+ "@telorun/sdk": "0.36.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
package/src/builtins.ts CHANGED
@@ -61,12 +61,28 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
61
61
  items: {
62
62
  type: "object",
63
63
  additionalProperties: true,
64
+ // Resource bodies are `self`-only for config: per-call `inputs` is
65
+ // NOT in scope here. Each entry is a persistent child created once at
66
+ // init() and reused, so its config cannot depend on call-time data —
67
+ // that flows through the top-level `inputs:` sibling into the dispatch
68
+ // target's invoke().
69
+ //
70
+ // The exception is CEL the child's OWN controller evaluates later
71
+ // against a runtime context it owns (e.g. an Http.Api evaluating route
72
+ // CEL per request). Those `request` / `result` / `steps` / `error`
73
+ // variables are deferred — the template controller preserves them
74
+ // untouched (see resource-template-controller.ts) — so they are
75
+ // exposed here permissively. Their deep shape is the child kind's
76
+ // concern, not the template's, so they type as open values.
64
77
  "x-telo-context": {
65
78
  type: "object",
66
79
  additionalProperties: false,
67
80
  properties: {
68
81
  self: { "x-telo-context-from-root": "schema" },
69
- inputs: { "x-telo-context-from-root": "inputType" },
82
+ request: {},
83
+ result: {},
84
+ steps: {},
85
+ error: {},
70
86
  },
71
87
  },
72
88
  },
@@ -129,6 +145,41 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
129
145
  },
130
146
  },
131
147
  },
148
+ // Mount dispatch: names the `resources:` entry (a Telo.Mount, e.g. an
149
+ // Http.Api) whose `register()` this definition delegates to. Same
150
+ // string / { kind, name } grammar as `invoke:`. The named child stays
151
+ // persistent so the produced mount's routes can `!ref` its siblings.
152
+ mount: {
153
+ oneOf: [
154
+ {
155
+ type: "string",
156
+ "x-telo-context": {
157
+ type: "object",
158
+ additionalProperties: false,
159
+ properties: {
160
+ self: { "x-telo-context-from-root": "schema" },
161
+ },
162
+ },
163
+ },
164
+ {
165
+ type: "object",
166
+ additionalProperties: true,
167
+ properties: {
168
+ kind: { type: "string" },
169
+ name: {
170
+ type: "string",
171
+ "x-telo-context": {
172
+ type: "object",
173
+ additionalProperties: false,
174
+ properties: {
175
+ self: { "x-telo-context-from-root": "schema" },
176
+ },
177
+ },
178
+ },
179
+ },
180
+ },
181
+ ],
182
+ },
132
183
  inputs: {
133
184
  type: "object",
134
185
  additionalProperties: true,
@@ -21,6 +21,14 @@ const SOURCE = "telo-analyzer";
21
21
  * kind is registered but not a `Telo.Invocable`.
22
22
  * - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
23
23
  * declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
24
+ * - MOUNT_ON_NON_MOUNT: `mount:` declared on a definition whose `capability` is
25
+ * not `Telo.Mount`.
26
+ * - MOUNT_DISPATCHER_CONFLICT: `mount:` co-exists with another dispatch
27
+ * entry-point (`invoke:` / `run:` / `provide:`).
28
+ * - MOUNT_TARGET_UNKNOWN: `mount.name` does not resolve to an entry in
29
+ * `resources:`.
30
+ * - MOUNT_TARGET_NOT_MOUNTABLE: `mount.name` resolves to a resource whose kind
31
+ * is registered but not a `Telo.Mount`.
24
32
  */
25
33
  export function validateProviderCoherence(
26
34
  manifests: ResourceManifest[],
@@ -52,12 +60,14 @@ export function validateProviderCoherence(
52
60
  const provide = md.provide;
53
61
  const invoke = md.invoke;
54
62
  const run = md.run;
63
+ const mount = md.mount;
55
64
  const controllers = md.controllers;
56
65
  const resources = md.resources;
57
66
 
58
67
  const hasProvide = provide !== undefined && provide !== null;
59
68
  const hasInvoke = invoke !== undefined && invoke !== null;
60
69
  const hasRun = run !== undefined && run !== null;
70
+ const hasMount = mount !== undefined && mount !== null;
61
71
  const hasControllers = Array.isArray(controllers) && controllers.length > 0;
62
72
 
63
73
  if (hasProvide && capability !== "Telo.Provider") {
@@ -149,6 +159,81 @@ export function validateProviderCoherence(
149
159
  }
150
160
  }
151
161
 
162
+ if (hasMount && capability !== "Telo.Mount") {
163
+ diagnostics.push({
164
+ severity: DiagnosticSeverity.Error,
165
+ code: "MOUNT_ON_NON_MOUNT",
166
+ source: SOURCE,
167
+ message:
168
+ `${label}: 'mount:' is only valid on definitions with 'capability: Telo.Mount' ` +
169
+ `(found '${capability ?? "<unset>"}'). Use 'invoke:' / 'run:' / 'provide:' for other capabilities.`,
170
+ data: { resource, filePath, path: "mount" },
171
+ });
172
+ }
173
+
174
+ if (hasMount && (hasInvoke || hasRun || hasProvide)) {
175
+ const conflict = hasInvoke ? "invoke" : hasRun ? "run" : "provide";
176
+ diagnostics.push({
177
+ severity: DiagnosticSeverity.Error,
178
+ code: "MOUNT_DISPATCHER_CONFLICT",
179
+ source: SOURCE,
180
+ message:
181
+ `${label}: 'mount:' cannot co-exist with '${conflict}:'. ` +
182
+ `A definition declares exactly one dispatch entry-point.`,
183
+ data: { resource, filePath, path: "mount" },
184
+ });
185
+ }
186
+
187
+ if (hasMount) {
188
+ // Resolve the target's name from either form: the bare string (the
189
+ // primary, documented form — `mount: api`) or the object's `name`. A CEL
190
+ // target (`${{ … }}`) can only be checked at runtime, so skip those.
191
+ let mountedName: string | undefined;
192
+ if (typeof mount === "string") {
193
+ if (!mount.includes("${{")) mountedName = mount;
194
+ } else if (typeof mount === "object" && !Array.isArray(mount)) {
195
+ const mountObj = mount as { name?: unknown };
196
+ if (typeof mountObj.name === "string" && !mountObj.name.includes("${{")) {
197
+ mountedName = mountObj.name;
198
+ }
199
+ }
200
+ const mountPath = typeof mount === "string" ? "mount" : "mount.name";
201
+ if (mountedName && Array.isArray(resources)) {
202
+ const match = resources.find((r) => {
203
+ const meta = (r as { metadata?: { name?: unknown } })?.metadata;
204
+ return typeof meta?.name === "string" && meta.name === mountedName;
205
+ }) as { kind?: unknown } | undefined;
206
+ if (!match) {
207
+ diagnostics.push({
208
+ severity: DiagnosticSeverity.Error,
209
+ code: "MOUNT_TARGET_UNKNOWN",
210
+ source: SOURCE,
211
+ message:
212
+ `${label}: '${mountPath}: ${mountedName}' does not match any entry's ` +
213
+ `metadata.name in 'resources:'.`,
214
+ data: { resource, filePath, path: mountPath },
215
+ });
216
+ } else if (typeof match.kind === "string") {
217
+ const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
218
+ const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
219
+ if (targetDef && targetDef.kind === "Telo.Definition") {
220
+ const targetCap = (targetDef as { capability?: unknown }).capability;
221
+ if (typeof targetCap === "string" && targetCap !== "Telo.Mount") {
222
+ diagnostics.push({
223
+ severity: DiagnosticSeverity.Error,
224
+ code: "MOUNT_TARGET_NOT_MOUNTABLE",
225
+ source: SOURCE,
226
+ message:
227
+ `${label}: '${mountPath}: ${mountedName}' resolves to a ${match.kind} ` +
228
+ `(capability '${targetCap}'); 'mount:' requires a Telo.Mount target.`,
229
+ data: { resource, filePath, path: mountPath },
230
+ });
231
+ }
232
+ }
233
+ }
234
+ }
235
+ }
236
+
152
237
  if (capability === "Telo.Provider" && !hasControllers && !hasProvide) {
153
238
  diagnostics.push({
154
239
  severity: DiagnosticSeverity.Error,