@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
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
* Phase 3b — Validate `extends` fields on Telo.Definition docs, and flag the legacy
|
|
7
|
+
* `capability: <UserAbstract>` overload with CAPABILITY_SHADOWS_EXTENDS so users migrate.
|
|
8
|
+
*
|
|
9
|
+
* `extends` uses alias form ("<Alias>.<Name>") resolved against the declaring file's
|
|
10
|
+
* Telo.Import declarations — same pattern as `kind:` prefixes. The analyzer pre-resolves
|
|
11
|
+
* via AliasResolver before register() is called, so by the time this validator runs,
|
|
12
|
+
* the definition's effective `extends` is either the canonical form (when the alias was
|
|
13
|
+
* known) or the original alias-prefixed string (when it wasn't).
|
|
14
|
+
*
|
|
15
|
+
* Diagnostics:
|
|
16
|
+
* - EXTENDS_MALFORMED: value not in "<Alias>.<Name>" alias form, or not resolvable
|
|
17
|
+
* via the declaring file's imports (alias unknown → can't distinguish from a typo).
|
|
18
|
+
* - EXTENDS_UNKNOWN_TARGET: alias resolves to a module, but that module has no
|
|
19
|
+
* registered definition with the target name.
|
|
20
|
+
* - EXTENDS_NON_ABSTRACT: target resolves to a Telo.Definition, not a Telo.Abstract.
|
|
21
|
+
* - CAPABILITY_SHADOWS_EXTENDS (warning): `capability` names a user-declared abstract
|
|
22
|
+
* (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
|
|
23
|
+
* never trigger this — they're lifecycle roles by design.
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateExtends(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
|
|
26
|
+
//# sourceMappingURL=validate-extends.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-extends.d.ts","sourceRoot":"","sources":["../src/validate-extends.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;AAOzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CA4ItB"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { DiagnosticSeverity } from "./types.js";
|
|
2
|
+
const SOURCE = "telo-analyzer";
|
|
3
|
+
/** Alias-form pattern for `extends`: "<Alias>.<AbstractName>", two PascalCase segments. */
|
|
4
|
+
const EXTENDS_ALIAS_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
|
|
5
|
+
/**
|
|
6
|
+
* Phase 3b — Validate `extends` fields on Telo.Definition docs, and flag the legacy
|
|
7
|
+
* `capability: <UserAbstract>` overload with CAPABILITY_SHADOWS_EXTENDS so users migrate.
|
|
8
|
+
*
|
|
9
|
+
* `extends` uses alias form ("<Alias>.<Name>") resolved against the declaring file's
|
|
10
|
+
* Telo.Import declarations — same pattern as `kind:` prefixes. The analyzer pre-resolves
|
|
11
|
+
* via AliasResolver before register() is called, so by the time this validator runs,
|
|
12
|
+
* the definition's effective `extends` is either the canonical form (when the alias was
|
|
13
|
+
* known) or the original alias-prefixed string (when it wasn't).
|
|
14
|
+
*
|
|
15
|
+
* Diagnostics:
|
|
16
|
+
* - EXTENDS_MALFORMED: value not in "<Alias>.<Name>" alias form, or not resolvable
|
|
17
|
+
* via the declaring file's imports (alias unknown → can't distinguish from a typo).
|
|
18
|
+
* - EXTENDS_UNKNOWN_TARGET: alias resolves to a module, but that module has no
|
|
19
|
+
* registered definition with the target name.
|
|
20
|
+
* - EXTENDS_NON_ABSTRACT: target resolves to a Telo.Definition, not a Telo.Abstract.
|
|
21
|
+
* - CAPABILITY_SHADOWS_EXTENDS (warning): `capability` names a user-declared abstract
|
|
22
|
+
* (metadata.module !== "Telo"). Builtin lifecycle capabilities (Telo.Invocable, etc.)
|
|
23
|
+
* never trigger this — they're lifecycle roles by design.
|
|
24
|
+
*/
|
|
25
|
+
export function validateExtends(manifests, registry, aliases) {
|
|
26
|
+
const diagnostics = [];
|
|
27
|
+
// Defs forwarded from imported libraries carry `metadata.module` set to that
|
|
28
|
+
// library's name (stamped by the loader). The analyzer's Phase 1 already normalized
|
|
29
|
+
// their `extends` against the declaring library's own alias scope (`aliasesByModule`
|
|
30
|
+
// in analyzer.ts), so the canonical-form value is correct. What this validator can't
|
|
31
|
+
// re-check is whether the original alias was well-formed in the library's source —
|
|
32
|
+
// that's the library author's concern, surfaced when the library is analyzed as a
|
|
33
|
+
// root. Re-validating here against the consumer's alias scope (which doesn't know
|
|
34
|
+
// the library's internal aliases) would produce false-positive EXTENDS_MALFORMED /
|
|
35
|
+
// EXTENDS_UNKNOWN_TARGET. Skip forwarded defs entirely.
|
|
36
|
+
const importedModules = new Set();
|
|
37
|
+
for (const m of manifests) {
|
|
38
|
+
if (m.kind !== "Telo.Import")
|
|
39
|
+
continue;
|
|
40
|
+
const resolved = m.metadata?.resolvedModuleName;
|
|
41
|
+
if (resolved)
|
|
42
|
+
importedModules.add(resolved);
|
|
43
|
+
}
|
|
44
|
+
for (const m of manifests) {
|
|
45
|
+
if (m.kind !== "Telo.Definition")
|
|
46
|
+
continue;
|
|
47
|
+
const name = m.metadata?.name;
|
|
48
|
+
if (!name)
|
|
49
|
+
continue;
|
|
50
|
+
const ownModule = m.metadata?.module;
|
|
51
|
+
if (ownModule && importedModules.has(ownModule))
|
|
52
|
+
continue;
|
|
53
|
+
const filePath = m.metadata?.source;
|
|
54
|
+
const resource = { kind: m.kind, name };
|
|
55
|
+
const label = `${m.kind}/${name}`;
|
|
56
|
+
// --- extends validation ---
|
|
57
|
+
// At this point `m.extends` is whatever the manifest declared (alias form, e.g.
|
|
58
|
+
// "Ai.Model"). Resolve through the AliasResolver to the canonical form before
|
|
59
|
+
// looking up the target definition.
|
|
60
|
+
const extendsValue = m.extends;
|
|
61
|
+
if (extendsValue !== undefined) {
|
|
62
|
+
if (typeof extendsValue !== "string") {
|
|
63
|
+
diagnostics.push({
|
|
64
|
+
severity: DiagnosticSeverity.Error,
|
|
65
|
+
code: "EXTENDS_MALFORMED",
|
|
66
|
+
source: SOURCE,
|
|
67
|
+
message: `${label}: 'extends' must be a string in alias form "<Alias>.<Name>"`,
|
|
68
|
+
data: { resource, filePath, path: "extends" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (!EXTENDS_ALIAS_RE.test(extendsValue)) {
|
|
72
|
+
diagnostics.push({
|
|
73
|
+
severity: DiagnosticSeverity.Error,
|
|
74
|
+
code: "EXTENDS_MALFORMED",
|
|
75
|
+
source: SOURCE,
|
|
76
|
+
message: `${label}: 'extends: ${extendsValue}' must be in alias form "<Alias>.<Name>" ` +
|
|
77
|
+
`(e.g. "Ai.Model"), resolved via this file's Telo.Import declarations.`,
|
|
78
|
+
data: { resource, filePath, path: "extends" },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const prefix = extendsValue.slice(0, extendsValue.indexOf("."));
|
|
83
|
+
if (!aliases.hasAlias(prefix)) {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
severity: DiagnosticSeverity.Error,
|
|
86
|
+
code: "EXTENDS_MALFORMED",
|
|
87
|
+
source: SOURCE,
|
|
88
|
+
message: `${label}: 'extends: ${extendsValue}' — alias '${prefix}' is not a Telo.Import ` +
|
|
89
|
+
`in this file's scope. Declare the import or correct the alias.`,
|
|
90
|
+
data: { resource, filePath, path: "extends" },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const canonical = aliases.resolveKind(extendsValue);
|
|
95
|
+
if (!canonical) {
|
|
96
|
+
// Alias exists but the suffix isn't in its exported kinds — behave like
|
|
97
|
+
// an unknown target so users see the symbol is wrong, not that the alias is.
|
|
98
|
+
diagnostics.push({
|
|
99
|
+
severity: DiagnosticSeverity.Error,
|
|
100
|
+
code: "EXTENDS_UNKNOWN_TARGET",
|
|
101
|
+
source: SOURCE,
|
|
102
|
+
message: `${label}: 'extends' target '${extendsValue}' is not an exported kind of alias '${prefix}'.`,
|
|
103
|
+
data: { resource, filePath, path: "extends" },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const targetDef = registry.resolve(canonical);
|
|
108
|
+
if (!targetDef) {
|
|
109
|
+
diagnostics.push({
|
|
110
|
+
severity: DiagnosticSeverity.Error,
|
|
111
|
+
code: "EXTENDS_UNKNOWN_TARGET",
|
|
112
|
+
source: SOURCE,
|
|
113
|
+
message: `${label}: 'extends' target '${extendsValue}' (resolved: '${canonical}') is not a registered definition.`,
|
|
114
|
+
data: { resource, filePath, path: "extends" },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
else if (targetDef.kind !== "Telo.Abstract") {
|
|
118
|
+
diagnostics.push({
|
|
119
|
+
severity: DiagnosticSeverity.Error,
|
|
120
|
+
code: "EXTENDS_NON_ABSTRACT",
|
|
121
|
+
source: SOURCE,
|
|
122
|
+
message: `${label}: 'extends' target '${extendsValue}' (resolved: '${canonical}') is a ${targetDef.kind}, not a Telo.Abstract. ` +
|
|
123
|
+
`Only Telo.Abstract declarations may be extended.`,
|
|
124
|
+
data: { resource, filePath, path: "extends" },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// --- legacy capability-as-abstract warning ---
|
|
132
|
+
// `capability` is expected to name either a builtin lifecycle (module "Telo") or a
|
|
133
|
+
// user-declared abstract. The latter is the pre-`extends` overload — emit a warning
|
|
134
|
+
// suggesting the canonical form. Resolve through aliases first because the manifest
|
|
135
|
+
// retains the alias-prefixed form (e.g. "AbstractLib.Greeter"); the registered key
|
|
136
|
+
// is the canonical form after alias resolution (e.g. "abstract-lib.Greeter").
|
|
137
|
+
const capability = m.capability;
|
|
138
|
+
if (typeof capability === "string") {
|
|
139
|
+
const resolvedCap = aliases.resolveKind(capability) ?? capability;
|
|
140
|
+
const capDef = registry.resolve(resolvedCap);
|
|
141
|
+
if (capDef &&
|
|
142
|
+
capDef.kind === "Telo.Abstract" &&
|
|
143
|
+
capDef.metadata.module !== "Telo") {
|
|
144
|
+
// Build suggestion using the original alias form if aliases can produce it.
|
|
145
|
+
const aliasesForModule = aliases.aliasesFor(capDef.metadata.module);
|
|
146
|
+
const suggestion = aliasesForModule.length > 0
|
|
147
|
+
? `${aliasesForModule[0]}.${capDef.metadata.name}`
|
|
148
|
+
: `${capDef.metadata.module}.${capDef.metadata.name}`;
|
|
149
|
+
diagnostics.push({
|
|
150
|
+
severity: DiagnosticSeverity.Warning,
|
|
151
|
+
code: "CAPABILITY_SHADOWS_EXTENDS",
|
|
152
|
+
source: SOURCE,
|
|
153
|
+
message: `${label}: 'capability: ${capability}' names a user-declared abstract. ` +
|
|
154
|
+
`Prefer 'extends' for implements-this-abstract declarations; 'capability' should ` +
|
|
155
|
+
`name a lifecycle role. Use \`extends: "${suggestion}"\` with a lifecycle ` +
|
|
156
|
+
`\`capability\` (e.g. Telo.Invocable, Telo.Provider, Telo.Service).`,
|
|
157
|
+
data: { resource, filePath, path: "capability" },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return diagnostics;
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"ajv-formats": "^3.0.1",
|
|
42
42
|
"jsonpath-plus": "^10.3.0",
|
|
43
43
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/sdk": "0.
|
|
44
|
+
"@telorun/sdk": "0.5.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^20.0.0",
|
package/src/alias-resolver.ts
CHANGED
|
@@ -34,4 +34,15 @@ export class AliasResolver {
|
|
|
34
34
|
knownAliases(): string[] {
|
|
35
35
|
return Array.from(this.importAliases.keys());
|
|
36
36
|
}
|
|
37
|
+
|
|
38
|
+
/** Returns every alias that currently points at `targetModule`.
|
|
39
|
+
* Used by clients that need to convert a canonical kind key (e.g. "http-server.Server")
|
|
40
|
+
* back into its user-facing alias form (e.g. "Http.Server"). */
|
|
41
|
+
aliasesFor(targetModule: string): string[] {
|
|
42
|
+
const result: string[] = [];
|
|
43
|
+
for (const [alias, mod] of this.importAliases) {
|
|
44
|
+
if (mod === targetModule) result.push(alias);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
37
48
|
}
|
package/src/analysis-registry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import { AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
4
4
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
+
import { computeSuggestKind, computeValidUserFacingKinds } from "./kind-suggest.js";
|
|
5
6
|
import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
6
7
|
import type { AnalysisContext } from "./types.js";
|
|
7
8
|
|
|
@@ -71,6 +72,26 @@ export class AnalysisRegistry {
|
|
|
71
72
|
return this._context().definitions?.kinds() ?? [];
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
/** Returns every import alias that points at `moduleName` (the canonical, kebab-case
|
|
76
|
+
* module name). Empty when no import declares that target. */
|
|
77
|
+
aliasesFor(moduleName: string): string[] {
|
|
78
|
+
return this.aliases.aliasesFor(moduleName);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Returns every user-facing kind that is legal in the current scope:
|
|
82
|
+
* Telo root kinds plus the alias form of each non-abstract imported definition.
|
|
83
|
+
* Used by editor hosts to drive completion and by the analyzer to produce
|
|
84
|
+
* "did you mean" hints. */
|
|
85
|
+
validUserFacingKinds(): string[] {
|
|
86
|
+
return computeValidUserFacingKinds(this.aliases, this.defs);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Returns the closest user-facing kind to `badKind`, or undefined when nothing
|
|
90
|
+
* is close enough (or multiple candidates tie). Case-sensitive. */
|
|
91
|
+
suggestKind(badKind: string): string | undefined {
|
|
92
|
+
return computeSuggestKind(badKind, this.aliases, this.defs);
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
75
96
|
_context(): AnalysisContext {
|
|
76
97
|
return { aliases: this.aliases, definitions: this.defs };
|
package/src/analyzer.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
11
11
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
12
12
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
13
|
+
import { computeSuggestKind } from "./kind-suggest.js";
|
|
13
14
|
import { isModuleKind } from "./module-kinds.js";
|
|
14
15
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
15
16
|
import {
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
resolveTypeFieldToSchema,
|
|
28
29
|
validateChainAgainstSchema,
|
|
29
30
|
} from "./validate-cel-context.js";
|
|
31
|
+
import { validateExtends } from "./validate-extends.js";
|
|
30
32
|
import { validateReferences } from "./validate-references.js";
|
|
31
33
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
32
34
|
|
|
@@ -292,9 +294,24 @@ export class StaticAnalyzer {
|
|
|
292
294
|
// Register module identities and aliases.
|
|
293
295
|
// The root module doc (Telo.Application or Telo.Library) provides its own
|
|
294
296
|
// identity; imported modules surface their identity via resolvedModuleName/
|
|
295
|
-
// resolvedNamespace stamped onto the Telo.Import by the loader
|
|
296
|
-
//
|
|
297
|
-
//
|
|
297
|
+
// resolvedNamespace stamped onto the Telo.Import by the loader.
|
|
298
|
+
//
|
|
299
|
+
// Two alias scopes are tracked:
|
|
300
|
+
// - `aliases` — the consumer's aliases, populated from Telo.Imports declared in
|
|
301
|
+
// the entry manifest (its own module).
|
|
302
|
+
// - `aliasesByModule` — per-imported-library aliases, populated from Telo.Imports
|
|
303
|
+
// forwarded by the loader from inside imported libraries. A library may use
|
|
304
|
+
// different alias names than the consumer for the same dependency; resolving
|
|
305
|
+
// a forwarded def's `extends` / `capability` against the consumer's scope
|
|
306
|
+
// would either fail or pick the wrong target. Each forwarded def is normalized
|
|
307
|
+
// in its own library's scope.
|
|
308
|
+
const rootModules = new Set<string>();
|
|
309
|
+
for (const m of manifests) {
|
|
310
|
+
if (isModuleKind(m.kind) && m.metadata?.name) {
|
|
311
|
+
rootModules.add(m.metadata.name as string);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const aliasesByModule = new Map<string, AliasResolver>();
|
|
298
315
|
for (const m of manifests) {
|
|
299
316
|
if (isModuleKind(m.kind)) {
|
|
300
317
|
const namespace = ((m.metadata as any).namespace as string | undefined) ?? null;
|
|
@@ -310,30 +327,60 @@ export class StaticAnalyzer {
|
|
|
310
327
|
| string
|
|
311
328
|
| null
|
|
312
329
|
| undefined;
|
|
330
|
+
const ownModule = (m.metadata as { module?: string } | undefined)?.module;
|
|
313
331
|
if (alias && source) {
|
|
314
332
|
const targetModule =
|
|
315
333
|
resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
|
|
316
|
-
|
|
334
|
+
// Module identity is registered globally so x-telo-ref resolution sees
|
|
335
|
+
// transitively-imported modules regardless of which scope brought them in.
|
|
317
336
|
if (resolvedModuleName) {
|
|
318
337
|
defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
|
|
319
338
|
}
|
|
339
|
+
// Alias registration is scoped: consumer imports vs. imported-library imports.
|
|
340
|
+
if (!ownModule || rootModules.has(ownModule)) {
|
|
341
|
+
aliases.registerImport(alias, targetModule, exportedKinds);
|
|
342
|
+
} else {
|
|
343
|
+
let libResolver = aliasesByModule.get(ownModule);
|
|
344
|
+
if (!libResolver) {
|
|
345
|
+
libResolver = new AliasResolver();
|
|
346
|
+
aliasesByModule.set(ownModule, libResolver);
|
|
347
|
+
}
|
|
348
|
+
libResolver.registerImport(alias, targetModule, exportedKinds);
|
|
349
|
+
}
|
|
320
350
|
}
|
|
321
351
|
}
|
|
322
352
|
}
|
|
323
353
|
|
|
324
|
-
// Register definitions from Telo.Definition resources.
|
|
325
|
-
//
|
|
326
|
-
//
|
|
354
|
+
// Register definitions from Telo.Definition AND Telo.Abstract resources.
|
|
355
|
+
// Abstracts declare contracts that implementations target via `extends` (canonical)
|
|
356
|
+
// or `capability: <AbstractKind>` (legacy). Until they're registered, validateReferences
|
|
357
|
+
// can't resolve x-telo-ref entries pointing at library-declared abstracts — so abstracts
|
|
358
|
+
// must go through register() too, not just the kernel builtins in the constructor.
|
|
359
|
+
//
|
|
360
|
+
// Normalize alias-prefixed `capability` and `extends` to canonical form using the
|
|
361
|
+
// declaring scope's resolver, so `extendedBy` is keyed by canonical kind regardless
|
|
362
|
+
// of alias choices. `capability` covers the legacy implements-this-abstract overload;
|
|
363
|
+
// `extends` is the canonical first-class form.
|
|
327
364
|
for (const m of manifests) {
|
|
328
|
-
if (m.kind
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
365
|
+
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
366
|
+
const def = m as unknown as ResourceDefinition;
|
|
367
|
+
const ownModule = (def.metadata as { module?: string } | undefined)?.module;
|
|
368
|
+
const scopeResolver =
|
|
369
|
+
ownModule && !rootModules.has(ownModule)
|
|
370
|
+
? (aliasesByModule.get(ownModule) ?? new AliasResolver())
|
|
371
|
+
: aliases;
|
|
372
|
+
const resolvedCapability = def.capability
|
|
373
|
+
? (scopeResolver.resolveKind(def.capability) ?? def.capability)
|
|
374
|
+
: def.capability;
|
|
375
|
+
const resolvedExtends = def.extends
|
|
376
|
+
? (scopeResolver.resolveKind(def.extends) ?? def.extends)
|
|
377
|
+
: def.extends;
|
|
378
|
+
const needsPatch =
|
|
379
|
+
resolvedCapability !== def.capability || resolvedExtends !== def.extends;
|
|
380
|
+
const normalized = needsPatch
|
|
381
|
+
? { ...def, capability: resolvedCapability, extends: resolvedExtends }
|
|
382
|
+
: def;
|
|
383
|
+
defs.register(normalized);
|
|
337
384
|
}
|
|
338
385
|
|
|
339
386
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
@@ -377,18 +424,14 @@ export class StaticAnalyzer {
|
|
|
377
424
|
const definition =
|
|
378
425
|
defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
379
426
|
if (!definition) {
|
|
380
|
-
const
|
|
381
|
-
const
|
|
382
|
-
const parts: string[] = [];
|
|
383
|
-
if (knownAliases.length > 0) parts.push(`imports: ${knownAliases.join(", ")}`);
|
|
384
|
-
if (knownKinds.length > 0) parts.push(`kinds: ${knownKinds.join(", ")}`);
|
|
385
|
-
const hint = parts.length > 0 ? ` Known ${parts.join(" | ")}` : "";
|
|
427
|
+
const suggestedKind = computeSuggestKind(m.kind, aliases, defs);
|
|
428
|
+
const hint = suggestedKind ? ` Did you mean '${suggestedKind}'?` : "";
|
|
386
429
|
diagnostics.push({
|
|
387
430
|
severity: DiagnosticSeverity.Error,
|
|
388
431
|
code: "UNDEFINED_KIND",
|
|
389
432
|
source: SOURCE,
|
|
390
433
|
message: `No Telo.Definition found for kind '${m.kind}'.${hint}`,
|
|
391
|
-
data: { resource, filePath, path: "kind" },
|
|
434
|
+
data: { resource, filePath, path: "kind", suggestedKind },
|
|
392
435
|
});
|
|
393
436
|
continue;
|
|
394
437
|
}
|
|
@@ -523,6 +566,9 @@ export class StaticAnalyzer {
|
|
|
523
566
|
// Validate resource references (Phase 3)
|
|
524
567
|
diagnostics.push(...validateReferences(allManifests, { aliases, definitions: defs }));
|
|
525
568
|
|
|
569
|
+
// Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
|
|
570
|
+
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
571
|
+
|
|
526
572
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
527
573
|
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
528
574
|
|
package/src/builtins.ts
CHANGED
|
@@ -27,9 +27,13 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
27
27
|
additionalProperties: true,
|
|
28
28
|
},
|
|
29
29
|
capability: { type: "string" },
|
|
30
|
+
schema: { type: "object", additionalProperties: true },
|
|
30
31
|
},
|
|
31
32
|
required: ["metadata"],
|
|
32
|
-
|
|
33
|
+
// Telo.Abstract is an extension point by design — it must accept forward-compatible
|
|
34
|
+
// fields (e.g. inputType/outputType from the typed-abstracts plan) without requiring
|
|
35
|
+
// the analyzer to enumerate them here.
|
|
36
|
+
additionalProperties: true,
|
|
33
37
|
},
|
|
34
38
|
},
|
|
35
39
|
{
|
|
@@ -55,6 +59,12 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
55
59
|
source: { type: "string" },
|
|
56
60
|
variables: { type: "object" },
|
|
57
61
|
secrets: { type: "object" },
|
|
62
|
+
runtime: {
|
|
63
|
+
oneOf: [
|
|
64
|
+
{ type: "string" },
|
|
65
|
+
{ type: "array", items: { type: "string" } },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
58
68
|
},
|
|
59
69
|
required: ["metadata", "source"],
|
|
60
70
|
additionalProperties: false,
|
|
@@ -32,13 +32,22 @@ export class DefinitionRegistry {
|
|
|
32
32
|
const key = mod ? `${mod}.${name}` : name;
|
|
33
33
|
this.defs.set(key, definition);
|
|
34
34
|
this.fieldMaps.set(key, buildReferenceFieldMap(definition.schema ?? {}));
|
|
35
|
+
// `capability` populates extendedBy for backward-compat with the legacy pattern where
|
|
36
|
+
// a concrete definition overloaded `capability: <AbstractKind>` to mean "implements
|
|
37
|
+
// this abstract." The canonical pattern is `extends` (below). Both populate the index,
|
|
38
|
+
// unioned — so in-flight modules pre-migration keep working.
|
|
35
39
|
if (definition.capability) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
this.addExtendedBy(definition.capability, key);
|
|
41
|
+
}
|
|
42
|
+
// `extends` — first-class "implements-this-abstract" edge. Alias-form resolution
|
|
43
|
+
// happens in the analyzer before register() is called (analyzer.ts pre-resolves
|
|
44
|
+
// via aliases.resolveKind), so the value here is already the canonical kind string
|
|
45
|
+
// (e.g. "workflow.Backend"). If the analyzer could not resolve the alias (partial
|
|
46
|
+
// context, or the declaring file doesn't import the target's alias), the value
|
|
47
|
+
// stays as the original alias-prefixed form; validateExtends emits EXTENDS_MALFORMED
|
|
48
|
+
// or EXTENDS_UNKNOWN_TARGET depending on the case.
|
|
49
|
+
if (definition.extends) {
|
|
50
|
+
this.addExtendedBy(definition.extends, key);
|
|
42
51
|
}
|
|
43
52
|
// Auto-register the telo identity when any Telo built-in is registered.
|
|
44
53
|
if (definition.kind === "Telo.Abstract" && mod === "Telo") {
|
|
@@ -51,6 +60,15 @@ export class DefinitionRegistry {
|
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
private addExtendedBy(parent: string, child: string): void {
|
|
64
|
+
const children = this.extendedBy.get(parent);
|
|
65
|
+
if (children) {
|
|
66
|
+
if (!children.includes(child)) children.push(child);
|
|
67
|
+
} else {
|
|
68
|
+
this.extendedBy.set(parent, [child]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
/** Register a module identity for x-telo-ref resolution.
|
|
55
73
|
* Call once per module doc (Telo.Application or Telo.Library) when the manifest is loaded.
|
|
56
74
|
* @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
2
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
3
|
+
import { distance } from "./levenshtein.js";
|
|
4
|
+
|
|
5
|
+
/** User-facing root kinds that are always legal regardless of imports. */
|
|
6
|
+
const ROOT_KINDS = [
|
|
7
|
+
"Telo.Application",
|
|
8
|
+
"Telo.Library",
|
|
9
|
+
"Telo.Import",
|
|
10
|
+
"Telo.Definition",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
/** Definition kinds that are not user-instantiable and should be excluded
|
|
14
|
+
* from completion / suggestion lists. */
|
|
15
|
+
const ABSTRACT_DEF_KINDS = new Set(["Telo.Abstract", "Telo.Template"]);
|
|
16
|
+
|
|
17
|
+
/** Computes the set of user-facing kind strings available in the given
|
|
18
|
+
* (aliases, defs) context:
|
|
19
|
+
* - The hardcoded Telo root kinds.
|
|
20
|
+
* - For every registered non-abstract definition, the alias form
|
|
21
|
+
* `${alias}.${TypeName}` for each import alias that points at its
|
|
22
|
+
* module. Canonical kebab-case forms (e.g. `http-server.Server`) are
|
|
23
|
+
* deliberately excluded — users write manifests via alias. */
|
|
24
|
+
export function computeValidUserFacingKinds(
|
|
25
|
+
aliases: AliasResolver,
|
|
26
|
+
defs: DefinitionRegistry,
|
|
27
|
+
): string[] {
|
|
28
|
+
const out = new Set<string>(ROOT_KINDS);
|
|
29
|
+
|
|
30
|
+
for (const kind of defs.kinds()) {
|
|
31
|
+
const def = defs.resolve(kind);
|
|
32
|
+
if (!def || ABSTRACT_DEF_KINDS.has(def.kind)) continue;
|
|
33
|
+
|
|
34
|
+
const dot = kind.indexOf(".");
|
|
35
|
+
if (dot === -1) continue;
|
|
36
|
+
const moduleName = kind.slice(0, dot);
|
|
37
|
+
const typeName = kind.slice(dot + 1);
|
|
38
|
+
|
|
39
|
+
for (const alias of aliases.aliasesFor(moduleName)) {
|
|
40
|
+
out.add(`${alias}.${typeName}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Array.from(out);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Returns the closest user-facing kind to `badKind` within an edit-distance
|
|
48
|
+
* threshold, or undefined when nothing close enough exists (including ties).
|
|
49
|
+
* Case-sensitive — kinds are PascalCase by contract. */
|
|
50
|
+
export function computeSuggestKind(
|
|
51
|
+
badKind: string,
|
|
52
|
+
aliases: AliasResolver,
|
|
53
|
+
defs: DefinitionRegistry,
|
|
54
|
+
): string | undefined {
|
|
55
|
+
if (!badKind) return undefined;
|
|
56
|
+
const candidates = computeValidUserFacingKinds(aliases, defs);
|
|
57
|
+
const threshold = Math.min(3, Math.floor(badKind.length / 3));
|
|
58
|
+
if (threshold < 1) return undefined;
|
|
59
|
+
|
|
60
|
+
let best: string | undefined;
|
|
61
|
+
let bestDist = threshold + 1;
|
|
62
|
+
let tied = false;
|
|
63
|
+
|
|
64
|
+
for (const c of candidates) {
|
|
65
|
+
const d = distance(badKind, c);
|
|
66
|
+
if (d < bestDist) {
|
|
67
|
+
best = c;
|
|
68
|
+
bestDist = d;
|
|
69
|
+
tied = false;
|
|
70
|
+
} else if (d === bestDist) {
|
|
71
|
+
tied = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!best || bestDist > threshold || tied) return undefined;
|
|
76
|
+
return best;
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Classic Wagner-Fischer Levenshtein distance. Iterative with two rolling rows
|
|
2
|
+
* so memory is O(min(a, b)); sufficient for short strings like resource kind
|
|
3
|
+
* identifiers (a few dozen characters at most). */
|
|
4
|
+
export function distance(a: string, b: string): number {
|
|
5
|
+
if (a === b) return 0;
|
|
6
|
+
if (a.length === 0) return b.length;
|
|
7
|
+
if (b.length === 0) return a.length;
|
|
8
|
+
|
|
9
|
+
// Ensure b is the shorter string so the row buffer stays small.
|
|
10
|
+
if (a.length < b.length) {
|
|
11
|
+
const tmp = a;
|
|
12
|
+
a = b;
|
|
13
|
+
b = tmp;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let prev = new Array<number>(b.length + 1);
|
|
17
|
+
let curr = new Array<number>(b.length + 1);
|
|
18
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
19
|
+
|
|
20
|
+
for (let i = 1; i <= a.length; i++) {
|
|
21
|
+
curr[0] = i;
|
|
22
|
+
for (let j = 1; j <= b.length; j++) {
|
|
23
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
24
|
+
curr[j] = Math.min(
|
|
25
|
+
curr[j - 1] + 1, // insertion
|
|
26
|
+
prev[j] + 1, // deletion
|
|
27
|
+
prev[j - 1] + cost, // substitution
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const swap = prev;
|
|
31
|
+
prev = curr;
|
|
32
|
+
curr = swap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return prev[b.length];
|
|
36
|
+
}
|