@telorun/analyzer 0.2.1 → 0.4.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 +5 -0
- package/dist/alias-resolver.d.ts +4 -0
- package/dist/alias-resolver.d.ts.map +1 -1
- package/dist/alias-resolver.js +11 -0
- package/dist/analysis-registry.d.ts +11 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +18 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +68 -23
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +11 -1
- package/dist/definition-registry.d.ts +1 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +25 -7
- package/dist/kind-suggest.d.ts +15 -0
- package/dist/kind-suggest.d.ts.map +1 -0
- package/dist/kind-suggest.js +63 -0
- package/dist/levenshtein.d.ts +5 -0
- package/dist/levenshtein.d.ts.map +1 -0
- package/dist/levenshtein.js +34 -0
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +65 -32
- package/dist/validate-extends.d.ts +26 -0
- package/dist/validate-extends.d.ts.map +1 -0
- package/dist/validate-extends.js +163 -0
- package/package.json +2 -2
- package/src/alias-resolver.ts +11 -0
- package/src/analysis-registry.ts +21 -0
- package/src/analyzer.ts +69 -23
- package/src/builtins.ts +11 -1
- package/src/definition-registry.ts +24 -6
- package/src/kind-suggest.ts +77 -0
- package/src/levenshtein.ts +36 -0
- package/src/manifest-loader.ts +72 -30
- package/src/validate-extends.ts +175 -0
package/src/manifest-loader.ts
CHANGED
|
@@ -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
|
-
|
|
352
|
-
visited.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
`
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
375
|
-
|
|
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:
|
|
380
|
-
resolvedNamespace:
|
|
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];
|
|
@@ -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
|
+
}
|