@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 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,
@@ -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;CAGzB"}
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"}
@@ -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;AAKzE,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,qFAAqF;IACrF,QAAQ,IAAI,eAAe;CAG5B"}
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 };
@@ -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;AAY9B,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;IA0PvB,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"}
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 knownAliases = aliases.knownAliases();
285
- const knownKinds = defs.kinds();
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.2.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.0"
44
+ "@telorun/sdk": "0.3.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^20.0.0",
@@ -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
  }
@@ -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 knownAliases = aliases.knownAliases();
381
- const knownKinds = defs.kinds();
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
+ }