@telorun/analyzer 0.3.0 → 0.5.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.
Files changed (37) hide show
  1. package/README.md +0 -4
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +64 -14
  4. package/dist/builtins.d.ts.map +1 -1
  5. package/dist/builtins.js +11 -1
  6. package/dist/definition-registry.d.ts +1 -0
  7. package/dist/definition-registry.d.ts.map +1 -1
  8. package/dist/definition-registry.js +25 -7
  9. package/dist/index.d.ts +4 -4
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +3 -3
  12. package/dist/manifest-loader.d.ts +5 -5
  13. package/dist/manifest-loader.d.ts.map +1 -1
  14. package/dist/manifest-loader.js +100 -67
  15. package/dist/{adapters/http-adapter.d.ts → sources/http-source.d.ts} +3 -3
  16. package/dist/sources/http-source.d.ts.map +1 -0
  17. package/dist/{adapters/http-adapter.js → sources/http-source.js} +1 -1
  18. package/dist/{adapters/registry-adapter.d.ts → sources/registry-source.d.ts} +3 -3
  19. package/dist/sources/registry-source.d.ts.map +1 -0
  20. package/dist/{adapters/registry-adapter.js → sources/registry-source.js} +1 -1
  21. package/dist/types.d.ts +10 -10
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/validate-extends.d.ts +26 -0
  24. package/dist/validate-extends.d.ts.map +1 -0
  25. package/dist/validate-extends.js +163 -0
  26. package/package.json +2 -2
  27. package/src/analyzer.ts +65 -16
  28. package/src/builtins.ts +11 -1
  29. package/src/definition-registry.ts +24 -6
  30. package/src/index.ts +7 -4
  31. package/src/manifest-loader.ts +106 -64
  32. package/src/{adapters/http-adapter.ts → sources/http-source.ts} +2 -2
  33. package/src/{adapters/registry-adapter.ts → sources/registry-source.ts} +2 -2
  34. package/src/types.ts +10 -10
  35. package/src/validate-extends.ts +175 -0
  36. package/dist/adapters/http-adapter.d.ts.map +0 -1
  37. package/dist/adapters/registry-adapter.d.ts.map +0 -1
@@ -1,8 +1,8 @@
1
1
  import type { Environment } from "@marcbachmann/cel-js";
2
2
  import { isCompiledValue, type ResourceManifest } from "@telorun/sdk";
3
3
  import { isMap, isPair, isScalar, isSeq, parseAllDocuments, type Document } from "yaml";
4
- import { HttpAdapter } from "./adapters/http-adapter.js";
5
- import { RegistryAdapter } from "./adapters/registry-adapter.js";
4
+ import { HttpSource } from "./sources/http-source.js";
5
+ import { RegistrySource } from "./sources/registry-source.js";
6
6
  import { buildCelEnvironment } from "./cel-environment.js";
7
7
  import { isModuleKind } from "./module-kinds.js";
8
8
  import { precompileDoc } from "./precompile.js";
@@ -10,7 +10,7 @@ import {
10
10
  DEFAULT_MANIFEST_FILENAME,
11
11
  type LoadOptions,
12
12
  type LoaderInitOptions,
13
- type ManifestAdapter,
13
+ type ManifestSource,
14
14
  type Position,
15
15
  type PositionIndex,
16
16
  } from "./types.js";
@@ -23,42 +23,42 @@ const SYSTEM_KINDS = new Set([
23
23
  ]);
24
24
 
25
25
  export class Loader {
26
- private static readonly moduleCache = new Map<
26
+ private readonly moduleCache = new Map<
27
27
  string,
28
28
  { text: string; manifests: ResourceManifest[] }
29
29
  >();
30
30
 
31
- protected adapters: ManifestAdapter[];
31
+ protected sources: ManifestSource[];
32
32
  private readonly celEnv: Environment;
33
33
 
34
- constructor(extraAdaptersOrOptions: ManifestAdapter[] | LoaderInitOptions = []) {
35
- const options: LoaderInitOptions = Array.isArray(extraAdaptersOrOptions)
36
- ? { extraAdapters: extraAdaptersOrOptions }
37
- : extraAdaptersOrOptions;
34
+ constructor(extraSourcesOrOptions: ManifestSource[] | LoaderInitOptions = []) {
35
+ const options: LoaderInitOptions = Array.isArray(extraSourcesOrOptions)
36
+ ? { extraSources: extraSourcesOrOptions }
37
+ : extraSourcesOrOptions;
38
38
 
39
- const includeHttpAdapter = options.includeHttpAdapter ?? true;
40
- const includeRegistryAdapter = options.includeRegistryAdapter ?? true;
39
+ const includeHttpSource = options.includeHttpSource ?? true;
40
+ const includeRegistrySource = options.includeRegistrySource ?? true;
41
41
 
42
- this.adapters = [];
43
- if (includeHttpAdapter) this.adapters.push(new HttpAdapter());
44
- if (includeRegistryAdapter) this.adapters.push(new RegistryAdapter(options.registryUrl));
42
+ this.sources = [];
43
+ if (includeHttpSource) this.sources.push(new HttpSource());
44
+ if (includeRegistrySource) this.sources.push(new RegistrySource(options.registryUrl));
45
45
 
46
- if (options.extraAdapters?.length) {
47
- this.adapters.unshift(...options.extraAdapters);
46
+ if (options.extraSources?.length) {
47
+ this.sources.unshift(...options.extraSources);
48
48
  }
49
49
 
50
50
  this.celEnv = buildCelEnvironment(options.celHandlers);
51
51
  }
52
52
 
53
- register(adapter: ManifestAdapter): this {
54
- this.adapters.unshift(adapter);
53
+ register(source: ManifestSource): this {
54
+ this.sources.unshift(source);
55
55
  return this;
56
56
  }
57
57
 
58
- private pick(url: string): ManifestAdapter {
59
- const a = this.adapters.find((a) => a.supports(url));
60
- if (!a) throw new Error(`No adapter found for: ${url}`);
61
- return a;
58
+ private pick(url: string): ManifestSource {
59
+ const s = this.sources.find((s) => s.supports(url));
60
+ if (!s) throw new Error(`No source found for: ${url}`);
61
+ return s;
62
62
  }
63
63
 
64
64
  async resolveEntryPoint(url: string): Promise<string> {
@@ -69,7 +69,7 @@ export class Loader {
69
69
  async loadModule(url: string, options?: LoadOptions): Promise<ResourceManifest[]> {
70
70
  const { text, source } = await this.pick(url).read(url);
71
71
  const cacheKey = `${options?.compile ? "compiled" : "raw"}:${source}`;
72
- const cached = Loader.moduleCache.get(cacheKey);
72
+ const cached = this.moduleCache.get(cacheKey);
73
73
  if (cached && cached.text === text) {
74
74
  return cloneManifestArray(cached.manifests);
75
75
  }
@@ -152,8 +152,8 @@ export class Loader {
152
152
  const includePatterns = (moduleManifest as any).include as string[] | undefined;
153
153
  if (includePatterns?.length) {
154
154
  hasIncludes = true;
155
- const adapter = this.pick(source);
156
- const includedFiles = await this.resolveIncludes(source, includePatterns, adapter);
155
+ const picked = this.pick(source);
156
+ const includedFiles = await this.resolveIncludes(source, includePatterns, picked);
157
157
  for (const includedUrl of includedFiles) {
158
158
  const partialManifests = await this.loadPartialFile(includedUrl, moduleName, options);
159
159
  resolved.push(...partialManifests);
@@ -162,7 +162,7 @@ export class Loader {
162
162
  }
163
163
 
164
164
  if (!hasIncludes) {
165
- Loader.moduleCache.set(cacheKey, { text, manifests: resolved });
165
+ this.moduleCache.set(cacheKey, { text, manifests: resolved });
166
166
  }
167
167
  return cloneManifestArray(resolved);
168
168
  }
@@ -170,21 +170,21 @@ export class Loader {
170
170
  private async resolveIncludes(
171
171
  ownerSource: string,
172
172
  patterns: string[],
173
- adapter: ManifestAdapter,
173
+ source: ManifestSource,
174
174
  ): Promise<string[]> {
175
175
  const hasGlobs = patterns.some((p) => /[*?{}\[\]]/.test(p));
176
176
  if (hasGlobs) {
177
- if (!adapter.expandGlob) {
177
+ if (!source.expandGlob) {
178
178
  throw new Error(
179
- `Include patterns in '${ownerSource}' contain globs but the adapter for this source ` +
179
+ `Include patterns in '${ownerSource}' contain globs but the source for this URL ` +
180
180
  `does not support glob expansion. Use explicit file paths instead of patterns like: ` +
181
181
  patterns.filter((p) => /[*?{}\[\]]/.test(p)).join(", "),
182
182
  );
183
183
  }
184
- return adapter.expandGlob(ownerSource, patterns);
184
+ return source.expandGlob(ownerSource, patterns);
185
185
  }
186
186
  // Literal relative paths — deduplicate in case the same file appears under multiple patterns.
187
- return [...new Set(patterns.map((p) => adapter.resolveRelative(ownerSource, p)))];
187
+ return [...new Set(patterns.map((p) => source.resolveRelative(ownerSource, p)))];
188
188
  }
189
189
 
190
190
  private async loadPartialFile(
@@ -280,9 +280,9 @@ export class Loader {
280
280
  }
281
281
 
282
282
  // Find the owning telo.yaml via parent-directory traversal
283
- const adapter = this.pick(fileUrl);
284
- if (!adapter.resolveOwnerOf) return null;
285
- const ownerUrl = await adapter.resolveOwnerOf(fileUrl);
283
+ const source = this.pick(fileUrl);
284
+ if (!source.resolveOwnerOf) return null;
285
+ const ownerUrl = await source.resolveOwnerOf(fileUrl);
286
286
  if (!ownerUrl) return null;
287
287
 
288
288
  // Load the owner module (which will load included files via include expansion)
@@ -333,8 +333,34 @@ export class Loader {
333
333
 
334
334
  async loadManifests(entryUrl: string): Promise<ResourceManifest[]> {
335
335
  const visited = new Set<string>([entryUrl]);
336
+ // Cache resolved library identity per import URL so a Telo.Import re-encountered
337
+ // through a different chain still gets `resolvedModuleName` / `resolvedNamespace`
338
+ // stamped — without re-loading the target. The early `visited` short-circuit used
339
+ // to silently leave duplicate Telo.Imports unstamped, which broke alias resolution
340
+ // when the same library was imported by two different files in the same analysis set.
341
+ const libraryIdentityByUrl = new Map<
342
+ string,
343
+ { name: string; namespace: string | null }
344
+ >();
336
345
  const entry = await this.loadModule(entryUrl);
337
346
 
347
+ // Forward Telo.Definition, Telo.Abstract, AND Telo.Import docs from imported
348
+ // libraries to the analyzer so its downstream passes can see them:
349
+ // - Definitions / Abstracts feed cross-package `x-telo-ref` resolution and
350
+ // `extends` target validation.
351
+ // - Imports feed the per-library alias resolver — alias-form `extends` inside
352
+ // a library (e.g. ai-openai's `extends: Ai.Model`) resolves against THAT
353
+ // library's own `Telo.Import` declarations, not the root manifest's. Without
354
+ // forwarding the imports, importing such a library would surface a spurious
355
+ // EXTENDS_MALFORMED for an alias the library legitimately uses internally.
356
+ // Alias resolution itself stays in the analyzer; the loader's only semantic
357
+ // action is stamping `resolvedModuleName` / `resolvedNamespace` — recording the
358
+ // result of loading. Identity is cached per URL (see libraryIdentityByUrl above)
359
+ // because the same library can be reached through multiple chains, and every
360
+ // Telo.Import doc — including the duplicates short-circuited by `visited` —
361
+ // must end up stamped, otherwise per-scope alias resolution falls back to a
362
+ // path-derived string (e.g. "abstract-lib.yaml") and produces wrong canonical
363
+ // kinds.
338
364
  const importedDefs: ResourceManifest[] = [];
339
365
  const queue: ResourceManifest[] = [...entry];
340
366
 
@@ -348,36 +374,56 @@ export class Loader {
348
374
  importSource.startsWith(".") || importSource.startsWith("/")
349
375
  ? this.pick(base).resolveRelative(base, importSource)
350
376
  : importSource;
351
- if (visited.has(importUrl)) continue;
352
- visited.add(importUrl);
353
- let imported: ResourceManifest[];
354
- try {
355
- imported = await this.loadModule(importUrl);
356
- } catch (err) {
357
- const e = err instanceof Error ? err : new Error(String(err));
358
- (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
359
- throw e;
360
- }
361
- // Import target must be a Telo.Library. Check the Library branch
362
- // explicitly rather than "anything that's a module kind" so that a
363
- // future third kind can't silently slip past as a valid import target.
364
- const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
365
- const importedApplication = imported.find((im) => im.kind === "Telo.Application");
366
- if (importedApplication) {
367
- const e = new Error(
368
- `Telo.Import target '${importSource}' is a Telo.Application. ` +
369
- `Only Telo.Library modules may be imported. Applications are run directly, not imported.`,
370
- );
371
- (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
372
- throw e;
377
+
378
+ if (!visited.has(importUrl)) {
379
+ visited.add(importUrl);
380
+ let imported: ResourceManifest[];
381
+ try {
382
+ imported = await this.loadModule(importUrl);
383
+ } catch (err) {
384
+ const e = err instanceof Error ? err : new Error(String(err));
385
+ (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
386
+ throw e;
387
+ }
388
+ // Import target must be a Telo.Library. Check the Library branch
389
+ // explicitly rather than "anything that's a module kind" so that a
390
+ // future third kind can't silently slip past as a valid import target.
391
+ const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
392
+ const importedApplication = imported.find((im) => im.kind === "Telo.Application");
393
+ if (importedApplication) {
394
+ const e = new Error(
395
+ `Telo.Import target '${importSource}' is a Telo.Application. ` +
396
+ `Only Telo.Library modules may be imported. Applications are run directly, not imported.`,
397
+ );
398
+ (e as any).sourceLine = (m.metadata as any)?.sourceLine ?? 0;
399
+ throw e;
400
+ }
401
+ if (importedLibrary?.metadata?.name) {
402
+ libraryIdentityByUrl.set(importUrl, {
403
+ name: importedLibrary.metadata.name as string,
404
+ namespace: ((importedLibrary.metadata as any).namespace as string | null) ?? null,
405
+ });
406
+ }
407
+ for (const im of imported) {
408
+ if (
409
+ im.kind === "Telo.Definition" ||
410
+ im.kind === "Telo.Abstract" ||
411
+ im.kind === "Telo.Import"
412
+ ) {
413
+ importedDefs.push(im);
414
+ }
415
+ if (im.kind === "Telo.Import") queue.push(im);
416
+ }
373
417
  }
374
- const importedModule = importedLibrary;
375
- if (importedModule?.metadata?.name) {
418
+
419
+ // Stamp m with cached identity (works for both fresh and duplicate visits).
420
+ const identity = libraryIdentityByUrl.get(importUrl);
421
+ if (identity) {
376
422
  const pi = (m.metadata as any)?.positionIndex;
377
423
  m.metadata = {
378
424
  ...m.metadata,
379
- resolvedModuleName: importedModule.metadata.name as string,
380
- resolvedNamespace: (importedModule.metadata as any).namespace ?? null,
425
+ resolvedModuleName: identity.name,
426
+ resolvedNamespace: identity.namespace,
381
427
  };
382
428
  if (pi) {
383
429
  Object.defineProperty(m.metadata, "positionIndex", {
@@ -388,10 +434,6 @@ export class Loader {
388
434
  });
389
435
  }
390
436
  }
391
- for (const im of imported) {
392
- if (im.kind === "Telo.Definition") importedDefs.push(im);
393
- if (im.kind === "Telo.Import") queue.push(im);
394
- }
395
437
  }
396
438
 
397
439
  return [...entry, ...importedDefs];
@@ -1,6 +1,6 @@
1
- import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "../types.js";
1
+ import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "../types.js";
2
2
 
3
- export class HttpAdapter implements ManifestAdapter {
3
+ export class HttpSource implements ManifestSource {
4
4
  supports(url: string): boolean {
5
5
  return url.startsWith("http://") || url.startsWith("https://");
6
6
  }
@@ -1,8 +1,8 @@
1
- import { DEFAULT_MANIFEST_FILENAME, type ManifestAdapter } from "../types.js";
1
+ import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "../types.js";
2
2
 
3
3
  const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
4
4
 
5
- export class RegistryAdapter implements ManifestAdapter {
5
+ export class RegistrySource implements ManifestSource {
6
6
  constructor(private registryUrl = DEFAULT_REGISTRY_URL) {}
7
7
 
8
8
  supports(url: string): boolean {
package/src/types.ts CHANGED
@@ -41,19 +41,19 @@ export interface AnalysisDiagnostic {
41
41
  data?: unknown;
42
42
  }
43
43
 
44
- export interface ManifestAdapter {
44
+ export interface ManifestSource {
45
45
  supports(url: string): boolean;
46
46
  read(url: string): Promise<{ text: string; source: string }>;
47
47
  resolveRelative(base: string, relative: string): string;
48
48
 
49
49
  /** Expand glob patterns relative to a base source. Returns sources in the same
50
50
  * format as read().source — suitable to pass back into read() / resolveRelative().
51
- * Optional — only filesystem-capable adapters implement this. */
51
+ * Optional — only filesystem-capable sources implement this. */
52
52
  expandGlob?(base: string, patterns: string[]): Promise<string[]>;
53
53
 
54
54
  /** Walk parent directories from fileUrl looking for the nearest telo.yaml.
55
55
  * Returns the source in the same format as read().source, or null if none found.
56
- * Optional — only filesystem-capable adapters implement this. */
56
+ * Optional — only filesystem-capable sources implement this. */
57
57
  resolveOwnerOf?(fileUrl: string): Promise<string | null>;
58
58
  }
59
59
 
@@ -66,13 +66,13 @@ export interface LoadOptions {
66
66
  }
67
67
 
68
68
  export interface LoaderInitOptions {
69
- /** Adapters inserted with highest priority before built-ins. */
70
- extraAdapters?: ManifestAdapter[];
71
- /** Include built-in HttpAdapter. Defaults to true. */
72
- includeHttpAdapter?: boolean;
73
- /** Include built-in RegistryAdapter. Defaults to true. */
74
- includeRegistryAdapter?: boolean;
75
- /** Base URL used by built-in RegistryAdapter when enabled. */
69
+ /** Sources inserted with highest priority before built-ins. */
70
+ extraSources?: ManifestSource[];
71
+ /** Include built-in HttpSource. Defaults to true. */
72
+ includeHttpSource?: boolean;
73
+ /** Include built-in RegistrySource. Defaults to true. */
74
+ includeRegistrySource?: boolean;
75
+ /** Base URL used by built-in RegistrySource when enabled. */
76
76
  registryUrl?: string;
77
77
  /** Handlers for CEL stdlib functions (e.g. `sha256`). Analyzer-only callers may
78
78
  * omit this and get throwing stubs; runtime callers (kernel) must supply real impls. */
@@ -0,0 +1,175 @@
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 { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
5
+
6
+ const SOURCE = "telo-analyzer";
7
+
8
+ /** Alias-form pattern for `extends`: "<Alias>.<AbstractName>", two PascalCase segments. */
9
+ const EXTENDS_ALIAS_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
10
+
11
+ /**
12
+ * Phase 3b — Validate `extends` fields on Telo.Definition docs, and flag the legacy
13
+ * `capability: <UserAbstract>` overload with CAPABILITY_SHADOWS_EXTENDS so users migrate.
14
+ *
15
+ * `extends` uses alias form ("<Alias>.<Name>") resolved against the declaring file's
16
+ * Telo.Import declarations — same pattern as `kind:` prefixes. The analyzer pre-resolves
17
+ * via AliasResolver before register() is called, so by the time this validator runs,
18
+ * the definition's effective `extends` is either the canonical form (when the alias was
19
+ * known) or the original alias-prefixed string (when it wasn't).
20
+ *
21
+ * Diagnostics:
22
+ * - EXTENDS_MALFORMED: value not in "<Alias>.<Name>" alias form, or not resolvable
23
+ * via the declaring file's imports (alias unknown → can't distinguish from a typo).
24
+ * - EXTENDS_UNKNOWN_TARGET: alias resolves to a module, but that module has no
25
+ * registered definition with the target name.
26
+ * - EXTENDS_NON_ABSTRACT: target resolves to a Telo.Definition, not a Telo.Abstract.
27
+ * - CAPABILITY_SHADOWS_EXTENDS (warning): `capability` names a user-declared abstract
28
+ * (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
29
+ * never trigger this — they're lifecycle roles by design.
30
+ */
31
+ export function validateExtends(
32
+ manifests: ResourceManifest[],
33
+ registry: DefinitionRegistry,
34
+ aliases: AliasResolver,
35
+ ): AnalysisDiagnostic[] {
36
+ const diagnostics: AnalysisDiagnostic[] = [];
37
+
38
+ // Defs forwarded from imported libraries carry `metadata.module` set to that
39
+ // library's name (stamped by the loader). The analyzer's Phase 1 already normalized
40
+ // their `extends` against the declaring library's own alias scope (`aliasesByModule`
41
+ // in analyzer.ts), so the canonical-form value is correct. What this validator can't
42
+ // re-check is whether the original alias was well-formed in the library's source —
43
+ // that's the library author's concern, surfaced when the library is analyzed as a
44
+ // root. Re-validating here against the consumer's alias scope (which doesn't know
45
+ // the library's internal aliases) would produce false-positive EXTENDS_MALFORMED /
46
+ // EXTENDS_UNKNOWN_TARGET. Skip forwarded defs entirely.
47
+ const importedModules = new Set<string>();
48
+ for (const m of manifests) {
49
+ if (m.kind !== "Telo.Import") continue;
50
+ const resolved = (m.metadata as { resolvedModuleName?: string } | undefined)?.resolvedModuleName;
51
+ if (resolved) importedModules.add(resolved);
52
+ }
53
+
54
+ for (const m of manifests) {
55
+ if (m.kind !== "Telo.Definition") continue;
56
+ const name = m.metadata?.name as string | undefined;
57
+ if (!name) continue;
58
+ const ownModule = (m.metadata as { module?: string } | undefined)?.module;
59
+ if (ownModule && importedModules.has(ownModule)) continue;
60
+ const filePath = (m.metadata as { source?: string } | undefined)?.source;
61
+ const resource = { kind: m.kind, name };
62
+ const label = `${m.kind}/${name}`;
63
+
64
+ // --- extends validation ---
65
+ // At this point `m.extends` is whatever the manifest declared (alias form, e.g.
66
+ // "Ai.Model"). Resolve through the AliasResolver to the canonical form before
67
+ // looking up the target definition.
68
+ const extendsValue = (m as { extends?: unknown }).extends;
69
+ if (extendsValue !== undefined) {
70
+ if (typeof extendsValue !== "string") {
71
+ diagnostics.push({
72
+ severity: DiagnosticSeverity.Error,
73
+ code: "EXTENDS_MALFORMED",
74
+ source: SOURCE,
75
+ message: `${label}: 'extends' must be a string in alias form "<Alias>.<Name>"`,
76
+ data: { resource, filePath, path: "extends" },
77
+ });
78
+ } else if (!EXTENDS_ALIAS_RE.test(extendsValue)) {
79
+ diagnostics.push({
80
+ severity: DiagnosticSeverity.Error,
81
+ code: "EXTENDS_MALFORMED",
82
+ source: SOURCE,
83
+ message:
84
+ `${label}: 'extends: ${extendsValue}' must be in alias form "<Alias>.<Name>" ` +
85
+ `(e.g. "Ai.Model"), resolved via this file's Telo.Import declarations.`,
86
+ data: { resource, filePath, path: "extends" },
87
+ });
88
+ } else {
89
+ const prefix = extendsValue.slice(0, extendsValue.indexOf("."));
90
+ if (!aliases.hasAlias(prefix)) {
91
+ diagnostics.push({
92
+ severity: DiagnosticSeverity.Error,
93
+ code: "EXTENDS_MALFORMED",
94
+ source: SOURCE,
95
+ message:
96
+ `${label}: 'extends: ${extendsValue}' — alias '${prefix}' is not a Telo.Import ` +
97
+ `in this file's scope. Declare the import or correct the alias.`,
98
+ data: { resource, filePath, path: "extends" },
99
+ });
100
+ } else {
101
+ const canonical = aliases.resolveKind(extendsValue);
102
+ if (!canonical) {
103
+ // Alias exists but the suffix isn't in its exported kinds — behave like
104
+ // an unknown target so users see the symbol is wrong, not that the alias is.
105
+ diagnostics.push({
106
+ severity: DiagnosticSeverity.Error,
107
+ code: "EXTENDS_UNKNOWN_TARGET",
108
+ source: SOURCE,
109
+ message: `${label}: 'extends' target '${extendsValue}' is not an exported kind of alias '${prefix}'.`,
110
+ data: { resource, filePath, path: "extends" },
111
+ });
112
+ } else {
113
+ const targetDef = registry.resolve(canonical);
114
+ if (!targetDef) {
115
+ diagnostics.push({
116
+ severity: DiagnosticSeverity.Error,
117
+ code: "EXTENDS_UNKNOWN_TARGET",
118
+ source: SOURCE,
119
+ message: `${label}: 'extends' target '${extendsValue}' (resolved: '${canonical}') is not a registered definition.`,
120
+ data: { resource, filePath, path: "extends" },
121
+ });
122
+ } else if (targetDef.kind !== "Telo.Abstract") {
123
+ diagnostics.push({
124
+ severity: DiagnosticSeverity.Error,
125
+ code: "EXTENDS_NON_ABSTRACT",
126
+ source: SOURCE,
127
+ message:
128
+ `${label}: 'extends' target '${extendsValue}' (resolved: '${canonical}') is a ${targetDef.kind}, not a Telo.Abstract. ` +
129
+ `Only Telo.Abstract declarations may be extended.`,
130
+ data: { resource, filePath, path: "extends" },
131
+ });
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // --- legacy capability-as-abstract warning ---
139
+ // `capability` is expected to name either a builtin lifecycle (module "Telo") or a
140
+ // user-declared abstract. The latter is the pre-`extends` overload — emit a warning
141
+ // suggesting the canonical form. Resolve through aliases first because the manifest
142
+ // retains the alias-prefixed form (e.g. "AbstractLib.Greeter"); the registered key
143
+ // is the canonical form after alias resolution (e.g. "abstract-lib.Greeter").
144
+ const capability = (m as { capability?: unknown }).capability;
145
+ if (typeof capability === "string") {
146
+ const resolvedCap = aliases.resolveKind(capability) ?? capability;
147
+ const capDef = registry.resolve(resolvedCap);
148
+ if (
149
+ capDef &&
150
+ capDef.kind === "Telo.Abstract" &&
151
+ capDef.metadata.module !== "Telo"
152
+ ) {
153
+ // Build suggestion using the original alias form if aliases can produce it.
154
+ const aliasesForModule = aliases.aliasesFor(capDef.metadata.module);
155
+ const suggestion =
156
+ aliasesForModule.length > 0
157
+ ? `${aliasesForModule[0]}.${capDef.metadata.name}`
158
+ : `${capDef.metadata.module}.${capDef.metadata.name}`;
159
+ diagnostics.push({
160
+ severity: DiagnosticSeverity.Warning,
161
+ code: "CAPABILITY_SHADOWS_EXTENDS",
162
+ source: SOURCE,
163
+ message:
164
+ `${label}: 'capability: ${capability}' names a user-declared abstract. ` +
165
+ `Prefer 'extends' for implements-this-abstract declarations; 'capability' should ` +
166
+ `name a lifecycle role. Use \`extends: "${suggestion}"\` with a lifecycle ` +
167
+ `\`capability\` (e.g. Telo.Invocable, Telo.Provider, Telo.Service).`,
168
+ data: { resource, filePath, path: "capability" },
169
+ });
170
+ }
171
+ }
172
+ }
173
+
174
+ return diagnostics;
175
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"http-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/http-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9E,qBAAa,WAAY,YAAW,eAAe;IACjD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAWlE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;CAIxD"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"registry-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/registry-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAA6B,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAI9E,qBAAa,eAAgB,YAAW,eAAe;IACzC,OAAO,CAAC,WAAW;gBAAX,WAAW,SAAuB;IAEtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAWxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAWxE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAMvD,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,cAAc;CAmBvB"}