@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 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;AAuP/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;IAsSvB,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,11 +3,13 @@ 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";
9
10
  import { DiagnosticSeverity } from "./types.js";
10
11
  import { extractAccessChains, getManifestItem, pathMatchesScope, resolveContextAnnotations, resolveTypeFieldToSchema, validateChainAgainstSchema, } from "./validate-cel-context.js";
12
+ import { validateExtends } from "./validate-extends.js";
11
13
  import { validateReferences } from "./validate-references.js";
12
14
  import { validateThrowsCoverage } from "./validate-throws-coverage.js";
13
15
  const TEMPLATE_REGEX = /\$\{\{\s*([^}]+?)\s*\}\}/g;
@@ -209,9 +211,24 @@ export class StaticAnalyzer {
209
211
  // Register module identities and aliases.
210
212
  // The root module doc (Telo.Application or Telo.Library) provides its own
211
213
  // identity; imported modules surface their identity via resolvedModuleName/
212
- // resolvedNamespace stamped onto the Telo.Import by the loader (so we don't
213
- // need to include imported module manifests in the analysis set, avoiding false
214
- // reference errors in the parent context).
214
+ // resolvedNamespace stamped onto the Telo.Import by the loader.
215
+ //
216
+ // Two alias scopes are tracked:
217
+ // - `aliases` — the consumer's aliases, populated from Telo.Imports declared in
218
+ // the entry manifest (its own module).
219
+ // - `aliasesByModule` — per-imported-library aliases, populated from Telo.Imports
220
+ // forwarded by the loader from inside imported libraries. A library may use
221
+ // different alias names than the consumer for the same dependency; resolving
222
+ // a forwarded def's `extends` / `capability` against the consumer's scope
223
+ // would either fail or pick the wrong target. Each forwarded def is normalized
224
+ // in its own library's scope.
225
+ const rootModules = new Set();
226
+ for (const m of manifests) {
227
+ if (isModuleKind(m.kind) && m.metadata?.name) {
228
+ rootModules.add(m.metadata.name);
229
+ }
230
+ }
231
+ const aliasesByModule = new Map();
215
232
  for (const m of manifests) {
216
233
  if (isModuleKind(m.kind)) {
217
234
  const namespace = m.metadata.namespace ?? null;
@@ -225,26 +242,58 @@ export class StaticAnalyzer {
225
242
  const exportedKinds = m.exports?.kinds ?? [];
226
243
  const resolvedModuleName = m.metadata.resolvedModuleName;
227
244
  const resolvedNamespace = m.metadata.resolvedNamespace;
245
+ const ownModule = m.metadata?.module;
228
246
  if (alias && source) {
229
247
  const targetModule = resolvedModuleName ?? source.split("/").filter(Boolean).pop() ?? source;
230
- aliases.registerImport(alias, targetModule, exportedKinds);
248
+ // Module identity is registered globally so x-telo-ref resolution sees
249
+ // transitively-imported modules regardless of which scope brought them in.
231
250
  if (resolvedModuleName) {
232
251
  defs.registerModuleIdentity(resolvedNamespace ?? null, resolvedModuleName);
233
252
  }
253
+ // Alias registration is scoped: consumer imports vs. imported-library imports.
254
+ if (!ownModule || rootModules.has(ownModule)) {
255
+ aliases.registerImport(alias, targetModule, exportedKinds);
256
+ }
257
+ else {
258
+ let libResolver = aliasesByModule.get(ownModule);
259
+ if (!libResolver) {
260
+ libResolver = new AliasResolver();
261
+ aliasesByModule.set(ownModule, libResolver);
262
+ }
263
+ libResolver.registerImport(alias, targetModule, exportedKinds);
264
+ }
234
265
  }
235
266
  }
236
267
  }
237
- // Register definitions from Telo.Definition resources.
238
- // Normalize alias-prefixed `capability` to canonical form so extendedBy lookup works
239
- // (e.g. "Workflow.Backend" "workflow.Backend" when "Workflow" is a known alias).
268
+ // Register definitions from Telo.Definition AND Telo.Abstract resources.
269
+ // Abstracts declare contracts that implementations target via `extends` (canonical)
270
+ // or `capability: <AbstractKind>` (legacy). Until they're registered, validateReferences
271
+ // can't resolve x-telo-ref entries pointing at library-declared abstracts — so abstracts
272
+ // must go through register() too, not just the kernel builtins in the constructor.
273
+ //
274
+ // Normalize alias-prefixed `capability` and `extends` to canonical form using the
275
+ // declaring scope's resolver, so `extendedBy` is keyed by canonical kind regardless
276
+ // of alias choices. `capability` covers the legacy implements-this-abstract overload;
277
+ // `extends` is the canonical first-class form.
240
278
  for (const m of manifests) {
241
- if (m.kind === "Telo.Definition") {
242
- const def = m;
243
- const resolvedCapability = def.capability
244
- ? (aliases.resolveKind(def.capability) ?? def.capability)
245
- : def.capability;
246
- defs.register(resolvedCapability !== def.capability ? { ...def, capability: resolvedCapability } : def);
247
- }
279
+ if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract")
280
+ continue;
281
+ const def = m;
282
+ const ownModule = def.metadata?.module;
283
+ const scopeResolver = ownModule && !rootModules.has(ownModule)
284
+ ? (aliasesByModule.get(ownModule) ?? new AliasResolver())
285
+ : aliases;
286
+ const resolvedCapability = def.capability
287
+ ? (scopeResolver.resolveKind(def.capability) ?? def.capability)
288
+ : def.capability;
289
+ const resolvedExtends = def.extends
290
+ ? (scopeResolver.resolveKind(def.extends) ?? def.extends)
291
+ : def.extends;
292
+ const needsPatch = resolvedCapability !== def.capability || resolvedExtends !== def.extends;
293
+ const normalized = needsPatch
294
+ ? { ...def, capability: resolvedCapability, extends: resolvedExtends }
295
+ : def;
296
+ defs.register(normalized);
248
297
  }
249
298
  // Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
250
299
  const allManifests = normalizeInlineResources(manifests, defs, aliases);
@@ -281,20 +330,14 @@ export class StaticAnalyzer {
281
330
  const resolvedKind = aliases.resolveKind(m.kind);
282
331
  const definition = defs.resolve(m.kind) ?? (resolvedKind ? defs.resolve(resolvedKind) : undefined);
283
332
  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(" | ")}` : "";
333
+ const suggestedKind = computeSuggestKind(m.kind, aliases, defs);
334
+ const hint = suggestedKind ? ` Did you mean '${suggestedKind}'?` : "";
292
335
  diagnostics.push({
293
336
  severity: DiagnosticSeverity.Error,
294
337
  code: "UNDEFINED_KIND",
295
338
  source: SOURCE,
296
339
  message: `No Telo.Definition found for kind '${m.kind}'.${hint}`,
297
- data: { resource, filePath, path: "kind" },
340
+ data: { resource, filePath, path: "kind", suggestedKind },
298
341
  });
299
342
  continue;
300
343
  }
@@ -406,6 +449,8 @@ export class StaticAnalyzer {
406
449
  }
407
450
  // Validate resource references (Phase 3)
408
451
  diagnostics.push(...validateReferences(allManifests, { aliases, definitions: defs }));
452
+ // Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
453
+ diagnostics.push(...validateExtends(allManifests, defs, aliases));
409
454
  // Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
410
455
  diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
411
456
  return diagnostics;
@@ -1 +1 @@
1
- {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EA4I/C,CAAC"}
1
+ {"version":3,"file":"builtins.d.ts","sourceRoot":"","sources":["../src/builtins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,eAAO,MAAM,eAAe,EAAE,kBAAkB,EAsJ/C,CAAC"}
package/dist/builtins.js CHANGED
@@ -25,9 +25,13 @@ export const KERNEL_BUILTINS = [
25
25
  additionalProperties: true,
26
26
  },
27
27
  capability: { type: "string" },
28
+ schema: { type: "object", additionalProperties: true },
28
29
  },
29
30
  required: ["metadata"],
30
- additionalProperties: false,
31
+ // Telo.Abstract is an extension point by design — it must accept forward-compatible
32
+ // fields (e.g. inputType/outputType from the typed-abstracts plan) without requiring
33
+ // the analyzer to enumerate them here.
34
+ additionalProperties: true,
31
35
  },
32
36
  },
33
37
  {
@@ -53,6 +57,12 @@ export const KERNEL_BUILTINS = [
53
57
  source: { type: "string" },
54
58
  variables: { type: "object" },
55
59
  secrets: { type: "object" },
60
+ runtime: {
61
+ oneOf: [
62
+ { type: "string" },
63
+ { type: "array", items: { type: "string" } },
64
+ ],
65
+ },
56
66
  },
57
67
  required: ["metadata", "source"],
58
68
  additionalProperties: false,
@@ -20,6 +20,7 @@ export declare class DefinitionRegistry {
20
20
  * Used to compute definition $id values for the AJV schema store. */
21
21
  private readonly reverseIdentityMap;
22
22
  register(definition: ResourceDefinition): void;
23
+ private addExtendedBy;
23
24
  /** Register a module identity for x-telo-ref resolution.
24
25
  * Call once per module doc (Telo.Application or Telo.Library) when the manifest is loaded.
25
26
  * @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
@@ -1 +1 @@
1
- {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG1F,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAwB9C;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAgB1E;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;wFACoF;IACpF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
1
+ {"version":3,"file":"definition-registry.d.ts","sourceRoot":"","sources":["../src/definition-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAA0B,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG1F,+EAA+E;AAC/E,qBAAa,kBAAkB;;IAK7B;;sFAEkF;IAClF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IAEzD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA+B;IAC1D;6DACyD;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD;;0EAEsE;IACtE,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAEhE,QAAQ,CAAC,UAAU,EAAE,kBAAkB,GAAG,IAAI;IAiC9C,OAAO,CAAC,aAAa;IASrB;;;yFAGqF;IACrF,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI;IAgB1E;4EACwE;IACxE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMnE;wFACoF;IACpF,gBAAgB,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,EAAE;IAWtE,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;4FAQwF;IACxF,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAUhD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIrD,+FAA+F;IAC/F,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAIxD,gGAAgG;IAChG,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;KAAE,GACvD,iBAAiB,GAAG,SAAS;IAOhC;;qEAEiE;IACjE,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAgBxD,KAAK,IAAI,MAAM,EAAE;CAGlB"}
@@ -28,14 +28,22 @@ export class DefinitionRegistry {
28
28
  const key = mod ? `${mod}.${name}` : name;
29
29
  this.defs.set(key, definition);
30
30
  this.fieldMaps.set(key, buildReferenceFieldMap(definition.schema ?? {}));
31
+ // `capability` populates extendedBy for backward-compat with the legacy pattern where
32
+ // a concrete definition overloaded `capability: <AbstractKind>` to mean "implements
33
+ // this abstract." The canonical pattern is `extends` (below). Both populate the index,
34
+ // unioned — so in-flight modules pre-migration keep working.
31
35
  if (definition.capability) {
32
- const children = this.extendedBy.get(definition.capability);
33
- if (children) {
34
- children.push(key);
35
- }
36
- else {
37
- this.extendedBy.set(definition.capability, [key]);
38
- }
36
+ this.addExtendedBy(definition.capability, key);
37
+ }
38
+ // `extends` — first-class "implements-this-abstract" edge. Alias-form resolution
39
+ // happens in the analyzer before register() is called (analyzer.ts pre-resolves
40
+ // via aliases.resolveKind), so the value here is already the canonical kind string
41
+ // (e.g. "workflow.Backend"). If the analyzer could not resolve the alias (partial
42
+ // context, or the declaring file doesn't import the target's alias), the value
43
+ // stays as the original alias-prefixed form; validateExtends emits EXTENDS_MALFORMED
44
+ // or EXTENDS_UNKNOWN_TARGET depending on the case.
45
+ if (definition.extends) {
46
+ this.addExtendedBy(definition.extends, key);
39
47
  }
40
48
  // Auto-register the telo identity when any Telo built-in is registered.
41
49
  if (definition.kind === "Telo.Abstract" && mod === "Telo") {
@@ -47,6 +55,16 @@ export class DefinitionRegistry {
47
55
  this.tryRegisterSchema(mod, name, definition.schema);
48
56
  }
49
57
  }
58
+ addExtendedBy(parent, child) {
59
+ const children = this.extendedBy.get(parent);
60
+ if (children) {
61
+ if (!children.includes(child))
62
+ children.push(child);
63
+ }
64
+ else {
65
+ this.extendedBy.set(parent, [child]);
66
+ }
67
+ }
50
68
  /** Register a module identity for x-telo-ref resolution.
51
69
  * Call once per module doc (Telo.Application or Telo.Library) when the manifest is loaded.
52
70
  * @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
@@ -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
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOtE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAGrB,MAAM,YAAY,CAAC;AASpB,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAG/B;IAEJ,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,sBAAsB,GAAE,eAAe,EAAE,GAAG,iBAAsB;IAmB9E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAKxC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAqGnE,eAAe;YAoBf,eAAe;IAgEvB,iBAAiB,CACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;QAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;KAClD,GAAG,IAAI,CAAC;IAiCH,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CAiEnE"}
1
+ {"version":3,"file":"manifest-loader.d.ts","sourceRoot":"","sources":["../src/manifest-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAOtE,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAGrB,MAAM,YAAY,CAAC;AASpB,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAG/B;IAEJ,SAAS,CAAC,QAAQ,EAAE,eAAe,EAAE,CAAC;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,sBAAsB,GAAE,eAAe,EAAE,GAAG,iBAAsB;IAmB9E,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI;IAKxC,OAAO,CAAC,IAAI;IAMN,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK/C,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAqGnE,eAAe;YAoBf,eAAe;IAgEvB,iBAAiB,CACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;QAC9B,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;KAClD,GAAG,IAAI,CAAC;IAiCH,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,KAAK,IAAI,GAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAsCrC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;CA2GnE"}
@@ -277,7 +277,30 @@ export class Loader {
277
277
  }
278
278
  async loadManifests(entryUrl) {
279
279
  const visited = new Set([entryUrl]);
280
+ // Cache resolved library identity per import URL so a Telo.Import re-encountered
281
+ // through a different chain still gets `resolvedModuleName` / `resolvedNamespace`
282
+ // stamped — without re-loading the target. The early `visited` short-circuit used
283
+ // to silently leave duplicate Telo.Imports unstamped, which broke alias resolution
284
+ // when the same library was imported by two different files in the same analysis set.
285
+ const libraryIdentityByUrl = new Map();
280
286
  const entry = await this.loadModule(entryUrl);
287
+ // Forward Telo.Definition, Telo.Abstract, AND Telo.Import docs from imported
288
+ // libraries to the analyzer so its downstream passes can see them:
289
+ // - Definitions / Abstracts feed cross-package `x-telo-ref` resolution and
290
+ // `extends` target validation.
291
+ // - Imports feed the per-library alias resolver — alias-form `extends` inside
292
+ // a library (e.g. ai-openai's `extends: Ai.Model`) resolves against THAT
293
+ // library's own `Telo.Import` declarations, not the root manifest's. Without
294
+ // forwarding the imports, importing such a library would surface a spurious
295
+ // EXTENDS_MALFORMED for an alias the library legitimately uses internally.
296
+ // Alias resolution itself stays in the analyzer; the loader's only semantic
297
+ // action is stamping `resolvedModuleName` / `resolvedNamespace` — recording the
298
+ // result of loading. Identity is cached per URL (see libraryIdentityByUrl above)
299
+ // because the same library can be reached through multiple chains, and every
300
+ // Telo.Import doc — including the duplicates short-circuited by `visited` —
301
+ // must end up stamped, otherwise per-scope alias resolution falls back to a
302
+ // path-derived string (e.g. "abstract-lib.yaml") and produces wrong canonical
303
+ // kinds.
281
304
  const importedDefs = [];
282
305
  const queue = [...entry];
283
306
  while (queue.length > 0) {
@@ -291,36 +314,52 @@ export class Loader {
291
314
  const importUrl = importSource.startsWith(".") || importSource.startsWith("/")
292
315
  ? this.pick(base).resolveRelative(base, importSource)
293
316
  : importSource;
294
- if (visited.has(importUrl))
295
- continue;
296
- visited.add(importUrl);
297
- let imported;
298
- try {
299
- imported = await this.loadModule(importUrl);
300
- }
301
- catch (err) {
302
- const e = err instanceof Error ? err : new Error(String(err));
303
- e.sourceLine = m.metadata?.sourceLine ?? 0;
304
- throw e;
305
- }
306
- // Import target must be a Telo.Library. Check the Library branch
307
- // explicitly rather than "anything that's a module kind" so that a
308
- // future third kind can't silently slip past as a valid import target.
309
- const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
310
- const importedApplication = imported.find((im) => im.kind === "Telo.Application");
311
- if (importedApplication) {
312
- const e = new Error(`Telo.Import target '${importSource}' is a Telo.Application. ` +
313
- `Only Telo.Library modules may be imported. Applications are run directly, not imported.`);
314
- e.sourceLine = m.metadata?.sourceLine ?? 0;
315
- throw e;
317
+ if (!visited.has(importUrl)) {
318
+ visited.add(importUrl);
319
+ let imported;
320
+ try {
321
+ imported = await this.loadModule(importUrl);
322
+ }
323
+ catch (err) {
324
+ const e = err instanceof Error ? err : new Error(String(err));
325
+ e.sourceLine = m.metadata?.sourceLine ?? 0;
326
+ throw e;
327
+ }
328
+ // Import target must be a Telo.Library. Check the Library branch
329
+ // explicitly rather than "anything that's a module kind" so that a
330
+ // future third kind can't silently slip past as a valid import target.
331
+ const importedLibrary = imported.find((im) => im.kind === "Telo.Library");
332
+ const importedApplication = imported.find((im) => im.kind === "Telo.Application");
333
+ if (importedApplication) {
334
+ const e = new Error(`Telo.Import target '${importSource}' is a Telo.Application. ` +
335
+ `Only Telo.Library modules may be imported. Applications are run directly, not imported.`);
336
+ e.sourceLine = m.metadata?.sourceLine ?? 0;
337
+ throw e;
338
+ }
339
+ if (importedLibrary?.metadata?.name) {
340
+ libraryIdentityByUrl.set(importUrl, {
341
+ name: importedLibrary.metadata.name,
342
+ namespace: importedLibrary.metadata.namespace ?? null,
343
+ });
344
+ }
345
+ for (const im of imported) {
346
+ if (im.kind === "Telo.Definition" ||
347
+ im.kind === "Telo.Abstract" ||
348
+ im.kind === "Telo.Import") {
349
+ importedDefs.push(im);
350
+ }
351
+ if (im.kind === "Telo.Import")
352
+ queue.push(im);
353
+ }
316
354
  }
317
- const importedModule = importedLibrary;
318
- if (importedModule?.metadata?.name) {
355
+ // Stamp m with cached identity (works for both fresh and duplicate visits).
356
+ const identity = libraryIdentityByUrl.get(importUrl);
357
+ if (identity) {
319
358
  const pi = m.metadata?.positionIndex;
320
359
  m.metadata = {
321
360
  ...m.metadata,
322
- resolvedModuleName: importedModule.metadata.name,
323
- resolvedNamespace: importedModule.metadata.namespace ?? null,
361
+ resolvedModuleName: identity.name,
362
+ resolvedNamespace: identity.namespace,
324
363
  };
325
364
  if (pi) {
326
365
  Object.defineProperty(m.metadata, "positionIndex", {
@@ -331,12 +370,6 @@ export class Loader {
331
370
  });
332
371
  }
333
372
  }
334
- for (const im of imported) {
335
- if (im.kind === "Telo.Definition")
336
- importedDefs.push(im);
337
- if (im.kind === "Telo.Import")
338
- queue.push(im);
339
- }
340
373
  }
341
374
  return [...entry, ...importedDefs];
342
375
  }