@telorun/analyzer 0.2.0 → 0.3.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 +4 -9
- 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/package.json +2 -2
- package/src/alias-resolver.ts +11 -0
- package/src/analysis-registry.ts +21 -0
- package/src/analyzer.ts +4 -7
- package/src/kind-suggest.ts +77 -0
- package/src/levenshtein.ts +36 -0
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Telo: YAML-driven execution engine for declarative backends with micro-kernel architecture and language-agnostic design"
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# ⚡ Telo
|
|
2
6
|
|
|
3
7
|
Runtime for declarative backends.
|
|
@@ -73,6 +77,7 @@ connection:
|
|
|
73
77
|
kind: Sql.Migration
|
|
74
78
|
metadata:
|
|
75
79
|
name: Migration_20260413_182154_CreateFeedback
|
|
80
|
+
version: 20260413_182154_CreateFeedback
|
|
76
81
|
sql: |
|
|
77
82
|
CREATE TABLE IF NOT EXISTS feedback (
|
|
78
83
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
package/dist/alias-resolver.d.ts
CHANGED
|
@@ -8,5 +8,9 @@ export declare class AliasResolver {
|
|
|
8
8
|
resolveKind(kind: string): string | undefined;
|
|
9
9
|
hasAlias(alias: string): boolean;
|
|
10
10
|
knownAliases(): string[];
|
|
11
|
+
/** Returns every alias that currently points at `targetModule`.
|
|
12
|
+
* Used by clients that need to convert a canonical kind key (e.g. "http-server.Server")
|
|
13
|
+
* back into its user-facing alias form (e.g. "Http.Server"). */
|
|
14
|
+
aliasesFor(targetModule: string): string[];
|
|
11
15
|
}
|
|
12
16
|
//# sourceMappingURL=alias-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAEhE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAe7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;
|
|
1
|
+
{"version":3,"file":"alias-resolver.d.ts","sourceRoot":"","sources":["../src/alias-resolver.ts"],"names":[],"mappings":"AAAA;gFACgF;AAChF,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAEhE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,IAAI;IAOlF,sFAAsF;IACtF,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAe7C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIhC,YAAY,IAAI,MAAM,EAAE;IAIxB;;qEAEiE;IACjE,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE;CAO3C"}
|
package/dist/alias-resolver.js
CHANGED
|
@@ -33,4 +33,15 @@ export class AliasResolver {
|
|
|
33
33
|
knownAliases() {
|
|
34
34
|
return Array.from(this.importAliases.keys());
|
|
35
35
|
}
|
|
36
|
+
/** Returns every alias that currently points at `targetModule`.
|
|
37
|
+
* Used by clients that need to convert a canonical kind key (e.g. "http-server.Server")
|
|
38
|
+
* back into its user-facing alias form (e.g. "Http.Server"). */
|
|
39
|
+
aliasesFor(targetModule) {
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const [alias, mod] of this.importAliases) {
|
|
42
|
+
if (mod === targetModule)
|
|
43
|
+
result.push(alias);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
36
47
|
}
|
|
@@ -25,6 +25,17 @@ export declare class AnalysisRegistry {
|
|
|
25
25
|
builtinDefinitions(): ResourceDefinition[];
|
|
26
26
|
resolveDefinition(kind: string): ResourceDefinition | undefined;
|
|
27
27
|
allKinds(): string[];
|
|
28
|
+
/** Returns every import alias that points at `moduleName` (the canonical, kebab-case
|
|
29
|
+
* module name). Empty when no import declares that target. */
|
|
30
|
+
aliasesFor(moduleName: string): string[];
|
|
31
|
+
/** Returns every user-facing kind that is legal in the current scope:
|
|
32
|
+
* Telo root kinds plus the alias form of each non-abstract imported definition.
|
|
33
|
+
* Used by editor hosts to drive completion and by the analyzer to produce
|
|
34
|
+
* "did you mean" hints. */
|
|
35
|
+
validUserFacingKinds(): string[];
|
|
36
|
+
/** Returns the closest user-facing kind to `badKind`, or undefined when nothing
|
|
37
|
+
* is close enough (or multiple candidates tie). Case-sensitive. */
|
|
38
|
+
suggestKind(badKind: string): string | undefined;
|
|
28
39
|
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
29
40
|
_context(): AnalysisContext;
|
|
30
41
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"analysis-registry.d.ts","sourceRoot":"","sources":["../src/analysis-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAMzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAuB;IAE/C,kBAAkB,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI;IAIjD,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAIpE,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI;IAIpE,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI7C;;;OAGG;IACH,mBAAmB,CACjB,QAAQ,EAAE,gBAAgB,EAC1B,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,EAClC,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GACnC,IAAI;IAcP;;;;OAIG;IACH,kBAAkB,IAAI,kBAAkB,EAAE;IAI1C,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAM/D,QAAQ,IAAI,MAAM,EAAE;IAIpB;mEAC+D;IAC/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE;IAIxC;;;gCAG4B;IAC5B,oBAAoB,IAAI,MAAM,EAAE;IAIhC;wEACoE;IACpE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIhD,qFAAqF;IACrF,QAAQ,IAAI,eAAe;CAG5B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AliasResolver } from "./alias-resolver.js";
|
|
2
2
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
3
3
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
4
|
+
import { computeSuggestKind, computeValidUserFacingKinds } from "./kind-suggest.js";
|
|
4
5
|
import { isRefEntry, isScopeEntry } from "./reference-field-map.js";
|
|
5
6
|
/**
|
|
6
7
|
* Accumulates type and alias knowledge for a running kernel or analysis session.
|
|
@@ -56,6 +57,23 @@ export class AnalysisRegistry {
|
|
|
56
57
|
allKinds() {
|
|
57
58
|
return this._context().definitions?.kinds() ?? [];
|
|
58
59
|
}
|
|
60
|
+
/** Returns every import alias that points at `moduleName` (the canonical, kebab-case
|
|
61
|
+
* module name). Empty when no import declares that target. */
|
|
62
|
+
aliasesFor(moduleName) {
|
|
63
|
+
return this.aliases.aliasesFor(moduleName);
|
|
64
|
+
}
|
|
65
|
+
/** Returns every user-facing kind that is legal in the current scope:
|
|
66
|
+
* Telo root kinds plus the alias form of each non-abstract imported definition.
|
|
67
|
+
* Used by editor hosts to drive completion and by the analyzer to produce
|
|
68
|
+
* "did you mean" hints. */
|
|
69
|
+
validUserFacingKinds() {
|
|
70
|
+
return computeValidUserFacingKinds(this.aliases, this.defs);
|
|
71
|
+
}
|
|
72
|
+
/** Returns the closest user-facing kind to `badKind`, or undefined when nothing
|
|
73
|
+
* is close enough (or multiple candidates tie). Case-sensitive. */
|
|
74
|
+
suggestKind(badKind) {
|
|
75
|
+
return computeSuggestKind(badKind, this.aliases, this.defs);
|
|
76
|
+
}
|
|
59
77
|
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
60
78
|
_context() {
|
|
61
79
|
return { aliases: this.aliases, definitions: this.defs };
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGzE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAa9B,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAsP/F,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAsPvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,GAC1B,kBAAkB,EAAE;IAMvB,SAAS,CAAC,SAAS,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,gBAAgB,GAAG,gBAAgB,EAAE;IAKxF,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAiB5F"}
|
package/dist/analyzer.js
CHANGED
|
@@ -3,6 +3,7 @@ import { buildCelEnvironment, buildTypedCelEnvironment, } from "./cel-environmen
|
|
|
3
3
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
|
|
5
5
|
import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kernel-globals.js";
|
|
6
|
+
import { computeSuggestKind } from "./kind-suggest.js";
|
|
6
7
|
import { isModuleKind } from "./module-kinds.js";
|
|
7
8
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
8
9
|
import { celTypeSatisfiesJsonSchema, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
|
|
@@ -281,20 +282,14 @@ export class StaticAnalyzer {
|
|
|
281
282
|
const resolvedKind = aliases.resolveKind(m.kind);
|
|
282
283
|
const definition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
283
284
|
if (!definition) {
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const parts = [];
|
|
287
|
-
if (knownAliases.length > 0)
|
|
288
|
-
parts.push(`imports: ${knownAliases.join(", ")}`);
|
|
289
|
-
if (knownKinds.length > 0)
|
|
290
|
-
parts.push(`kinds: ${knownKinds.join(", ")}`);
|
|
291
|
-
const hint = parts.length > 0 ? ` Known ${parts.join(" | ")}` : "";
|
|
285
|
+
const suggestedKind = computeSuggestKind(m.kind, aliases, defs);
|
|
286
|
+
const hint = suggestedKind ? ` Did you mean '${suggestedKind}'?` : "";
|
|
292
287
|
diagnostics.push({
|
|
293
288
|
severity: DiagnosticSeverity.Error,
|
|
294
289
|
code: "UNDEFINED_KIND",
|
|
295
290
|
source: SOURCE,
|
|
296
291
|
message: `No Telo.Definition found for kind '${m.kind}'.${hint}`,
|
|
297
|
-
data: { resource, filePath, path: "kind" },
|
|
292
|
+
data: { resource, filePath, path: "kind", suggestedKind },
|
|
298
293
|
});
|
|
299
294
|
continue;
|
|
300
295
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
2
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
3
|
+
/** Computes the set of user-facing kind strings available in the given
|
|
4
|
+
* (aliases, defs) context:
|
|
5
|
+
* - The hardcoded Telo root kinds.
|
|
6
|
+
* - For every registered non-abstract definition, the alias form
|
|
7
|
+
* `${alias}.${TypeName}` for each import alias that points at its
|
|
8
|
+
* module. Canonical kebab-case forms (e.g. `http-server.Server`) are
|
|
9
|
+
* deliberately excluded — users write manifests via alias. */
|
|
10
|
+
export declare function computeValidUserFacingKinds(aliases: AliasResolver, defs: DefinitionRegistry): string[];
|
|
11
|
+
/** Returns the closest user-facing kind to `badKind` within an edit-distance
|
|
12
|
+
* threshold, or undefined when nothing close enough exists (including ties).
|
|
13
|
+
* Case-sensitive — kinds are PascalCase by contract. */
|
|
14
|
+
export declare function computeSuggestKind(badKind: string, aliases: AliasResolver, defs: DefinitionRegistry): string | undefined;
|
|
15
|
+
//# sourceMappingURL=kind-suggest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kind-suggest.d.ts","sourceRoot":"","sources":["../src/kind-suggest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAenE;;;;;;mEAMmE;AACnE,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,kBAAkB,GACvB,MAAM,EAAE,CAkBV;AAED;;yDAEyD;AACzD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,kBAAkB,GACvB,MAAM,GAAG,SAAS,CAuBpB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { distance } from "./levenshtein.js";
|
|
2
|
+
/** User-facing root kinds that are always legal regardless of imports. */
|
|
3
|
+
const ROOT_KINDS = [
|
|
4
|
+
"Telo.Application",
|
|
5
|
+
"Telo.Library",
|
|
6
|
+
"Telo.Import",
|
|
7
|
+
"Telo.Definition",
|
|
8
|
+
];
|
|
9
|
+
/** Definition kinds that are not user-instantiable and should be excluded
|
|
10
|
+
* from completion / suggestion lists. */
|
|
11
|
+
const ABSTRACT_DEF_KINDS = new Set(["Telo.Abstract", "Telo.Template"]);
|
|
12
|
+
/** Computes the set of user-facing kind strings available in the given
|
|
13
|
+
* (aliases, defs) context:
|
|
14
|
+
* - The hardcoded Telo root kinds.
|
|
15
|
+
* - For every registered non-abstract definition, the alias form
|
|
16
|
+
* `${alias}.${TypeName}` for each import alias that points at its
|
|
17
|
+
* module. Canonical kebab-case forms (e.g. `http-server.Server`) are
|
|
18
|
+
* deliberately excluded — users write manifests via alias. */
|
|
19
|
+
export function computeValidUserFacingKinds(aliases, defs) {
|
|
20
|
+
const out = new Set(ROOT_KINDS);
|
|
21
|
+
for (const kind of defs.kinds()) {
|
|
22
|
+
const def = defs.resolve(kind);
|
|
23
|
+
if (!def || ABSTRACT_DEF_KINDS.has(def.kind))
|
|
24
|
+
continue;
|
|
25
|
+
const dot = kind.indexOf(".");
|
|
26
|
+
if (dot === -1)
|
|
27
|
+
continue;
|
|
28
|
+
const moduleName = kind.slice(0, dot);
|
|
29
|
+
const typeName = kind.slice(dot + 1);
|
|
30
|
+
for (const alias of aliases.aliasesFor(moduleName)) {
|
|
31
|
+
out.add(`${alias}.${typeName}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Array.from(out);
|
|
35
|
+
}
|
|
36
|
+
/** Returns the closest user-facing kind to `badKind` within an edit-distance
|
|
37
|
+
* threshold, or undefined when nothing close enough exists (including ties).
|
|
38
|
+
* Case-sensitive — kinds are PascalCase by contract. */
|
|
39
|
+
export function computeSuggestKind(badKind, aliases, defs) {
|
|
40
|
+
if (!badKind)
|
|
41
|
+
return undefined;
|
|
42
|
+
const candidates = computeValidUserFacingKinds(aliases, defs);
|
|
43
|
+
const threshold = Math.min(3, Math.floor(badKind.length / 3));
|
|
44
|
+
if (threshold < 1)
|
|
45
|
+
return undefined;
|
|
46
|
+
let best;
|
|
47
|
+
let bestDist = threshold + 1;
|
|
48
|
+
let tied = false;
|
|
49
|
+
for (const c of candidates) {
|
|
50
|
+
const d = distance(badKind, c);
|
|
51
|
+
if (d < bestDist) {
|
|
52
|
+
best = c;
|
|
53
|
+
bestDist = d;
|
|
54
|
+
tied = false;
|
|
55
|
+
}
|
|
56
|
+
else if (d === bestDist) {
|
|
57
|
+
tied = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!best || bestDist > threshold || tied)
|
|
61
|
+
return undefined;
|
|
62
|
+
return best;
|
|
63
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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 declare function distance(a: string, b: string): number;
|
|
5
|
+
//# sourceMappingURL=levenshtein.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"levenshtein.d.ts","sourceRoot":"","sources":["../src/levenshtein.ts"],"names":[],"mappings":"AAAA;;oDAEoD;AACpD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgCrD"}
|
|
@@ -0,0 +1,34 @@
|
|
|
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, b) {
|
|
5
|
+
if (a === b)
|
|
6
|
+
return 0;
|
|
7
|
+
if (a.length === 0)
|
|
8
|
+
return b.length;
|
|
9
|
+
if (b.length === 0)
|
|
10
|
+
return a.length;
|
|
11
|
+
// Ensure b is the shorter string so the row buffer stays small.
|
|
12
|
+
if (a.length < b.length) {
|
|
13
|
+
const tmp = a;
|
|
14
|
+
a = b;
|
|
15
|
+
b = tmp;
|
|
16
|
+
}
|
|
17
|
+
let prev = new Array(b.length + 1);
|
|
18
|
+
let curr = new Array(b.length + 1);
|
|
19
|
+
for (let j = 0; j <= b.length; j++)
|
|
20
|
+
prev[j] = j;
|
|
21
|
+
for (let i = 1; i <= a.length; i++) {
|
|
22
|
+
curr[0] = i;
|
|
23
|
+
for (let j = 1; j <= b.length; j++) {
|
|
24
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
25
|
+
curr[j] = Math.min(curr[j - 1] + 1, // insertion
|
|
26
|
+
prev[j] + 1, // deletion
|
|
27
|
+
prev[j - 1] + cost);
|
|
28
|
+
}
|
|
29
|
+
const swap = prev;
|
|
30
|
+
prev = curr;
|
|
31
|
+
curr = swap;
|
|
32
|
+
}
|
|
33
|
+
return prev[b.length];
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.
|
|
44
|
+
"@telorun/sdk": "0.3.2"
|
|
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 {
|
|
@@ -377,18 +378,14 @@ export class StaticAnalyzer {
|
|
|
377
378
|
const definition =
|
|
378
379
|
defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
|
|
379
380
|
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(" | ")}` : "";
|
|
381
|
+
const suggestedKind = computeSuggestKind(m.kind, aliases, defs);
|
|
382
|
+
const hint = suggestedKind ? ` Did you mean '${suggestedKind}'?` : "";
|
|
386
383
|
diagnostics.push({
|
|
387
384
|
severity: DiagnosticSeverity.Error,
|
|
388
385
|
code: "UNDEFINED_KIND",
|
|
389
386
|
source: SOURCE,
|
|
390
387
|
message: `No Telo.Definition found for kind '${m.kind}'.${hint}`,
|
|
391
|
-
data: { resource, filePath, path: "kind" },
|
|
388
|
+
data: { resource, filePath, path: "kind", suggestedKind },
|
|
392
389
|
});
|
|
393
390
|
continue;
|
|
394
391
|
}
|
|
@@ -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
|
+
}
|