@telorun/ide-support 0.5.0 → 0.7.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.
Files changed (53) hide show
  1. package/dist/completions/build.d.ts +2 -2
  2. package/dist/completions/build.d.ts.map +1 -1
  3. package/dist/completions/build.js +39 -64
  4. package/dist/completions/detect-context.d.ts +16 -43
  5. package/dist/completions/detect-context.d.ts.map +1 -1
  6. package/dist/completions/detect-context.js +63 -262
  7. package/dist/completions/import-source.d.ts +12 -8
  8. package/dist/completions/import-source.d.ts.map +1 -1
  9. package/dist/completions/import-source.js +67 -49
  10. package/dist/completions/resolve-node.d.ts +50 -0
  11. package/dist/completions/resolve-node.d.ts.map +1 -0
  12. package/dist/completions/resolve-node.js +269 -0
  13. package/dist/completions/valid-capabilities.d.ts +3 -0
  14. package/dist/completions/valid-capabilities.d.ts.map +1 -1
  15. package/dist/completions/valid-capabilities.js +10 -0
  16. package/dist/definition/build-definition.d.ts +13 -0
  17. package/dist/definition/build-definition.d.ts.map +1 -0
  18. package/dist/definition/build-definition.js +98 -0
  19. package/dist/definition/index.d.ts +2 -0
  20. package/dist/definition/index.d.ts.map +1 -0
  21. package/dist/definition/index.js +1 -0
  22. package/dist/hover/build-hover.d.ts +4 -0
  23. package/dist/hover/build-hover.d.ts.map +1 -0
  24. package/dist/hover/build-hover.js +125 -0
  25. package/dist/hover/index.d.ts +2 -0
  26. package/dist/hover/index.d.ts.map +1 -0
  27. package/dist/hover/index.js +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -0
  31. package/dist/semantic-tokens/build-semantic-tokens.d.ts +12 -0
  32. package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -0
  33. package/dist/semantic-tokens/build-semantic-tokens.js +55 -0
  34. package/dist/semantic-tokens/index.d.ts +2 -0
  35. package/dist/semantic-tokens/index.d.ts.map +1 -0
  36. package/dist/semantic-tokens/index.js +1 -0
  37. package/dist/types.d.ts +66 -18
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js +3 -0
  40. package/package.json +2 -2
  41. package/src/completions/build.ts +40 -72
  42. package/src/completions/detect-context.ts +74 -291
  43. package/src/completions/import-source.ts +71 -53
  44. package/src/completions/resolve-node.ts +405 -0
  45. package/src/completions/valid-capabilities.ts +11 -0
  46. package/src/definition/build-definition.ts +123 -0
  47. package/src/definition/index.ts +1 -0
  48. package/src/hover/build-hover.ts +149 -0
  49. package/src/hover/index.ts +1 -0
  50. package/src/index.ts +3 -0
  51. package/src/semantic-tokens/build-semantic-tokens.ts +64 -0
  52. package/src/semantic-tokens/index.ts +1 -0
  53. package/src/types.ts +78 -18
@@ -0,0 +1,123 @@
1
+ import {
2
+ parseToAst,
3
+ type AstDocument,
4
+ type LoadedFile,
5
+ type LoadedGraph,
6
+ type LoadedModule,
7
+ type Range,
8
+ } from "@telorun/analyzer";
9
+ import type { DefinitionResult } from "../types.js";
10
+ import { resolveNodeAtPosition } from "../completions/resolve-node.js";
11
+
12
+ /** The module whose owner or partials includes `filePath`. */
13
+ function moduleForFile(graph: LoadedGraph, filePath: string): LoadedModule | undefined {
14
+ for (const mod of graph.modules.values()) {
15
+ if (mod.owner.source === filePath) return mod;
16
+ if (mod.partials.some((p) => p.source === filePath)) return mod;
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ /** First resource named `name` across `files`, located at its `metadata.name`
22
+ * (or its first line as a fallback). Names are unique within a module scope, so
23
+ * the first hit is the definition. */
24
+ function locateResource(files: LoadedFile[], name: string): DefinitionResult | undefined {
25
+ for (const file of files) {
26
+ for (let i = 0; i < file.manifests.length; i++) {
27
+ const manifest = file.manifests[i];
28
+ if (!manifest || manifest.metadata?.name !== name) continue;
29
+ const pos = file.positions[i];
30
+ const range: Range | undefined =
31
+ pos?.positionIndex.get("metadata.name") ??
32
+ pos?.positionIndex.get("@key:metadata.name") ??
33
+ (pos ? { start: { line: pos.sourceLine, character: 0 }, end: { line: pos.sourceLine, character: 0 } } : undefined);
34
+ if (range) return { uri: file.source, range };
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ /** The `exports.resources` list of a module's owner doc (empty when absent). */
41
+ function exportedResources(mod: LoadedModule): string[] {
42
+ const doc = mod.owner.manifests.find(
43
+ (m) => m?.kind === "Telo.Library" || m?.kind === "Telo.Application",
44
+ ) as { exports?: { resources?: unknown } } | undefined;
45
+ const list = doc?.exports?.resources;
46
+ return Array.isArray(list) ? list.filter((e): e is string => typeof e === "string") : [];
47
+ }
48
+
49
+ /** Follow `name` into `moduleSource` across the export boundary, honoring the
50
+ * `exports.resources` gate and re-export chains (`app → api → domain → …`). A
51
+ * terminal match is a locally-owned instance the module actually exports; a
52
+ * re-export entry `InnerAlias.name` hops through that module's own import edge.
53
+ * `seen` bounds cyclic import graphs. */
54
+ function resolveExported(
55
+ graph: LoadedGraph,
56
+ moduleSource: string,
57
+ name: string,
58
+ seen: Set<string> = new Set(),
59
+ ): DefinitionResult | undefined {
60
+ if (seen.has(moduleSource)) return undefined;
61
+ seen.add(moduleSource);
62
+ const mod = graph.modules.get(moduleSource);
63
+ if (!mod) return undefined;
64
+
65
+ const exports = exportedResources(mod);
66
+
67
+ // No `exports.resources` block → ungated (the module hasn't opted into the
68
+ // gate, same as `exports.kinds`): a plain name match keeps navigation working
69
+ // for modules that predate explicit exports.
70
+ if (exports.length === 0) {
71
+ return locateResource([mod.owner, ...mod.partials], name);
72
+ }
73
+
74
+ if (exports.includes(name)) {
75
+ const local = locateResource([mod.owner, ...mod.partials], name);
76
+ if (local) return local;
77
+ }
78
+
79
+ const reexport = exports.find((e) => e.endsWith(`.${name}`));
80
+ if (!reexport) return undefined;
81
+ const innerAlias = reexport.slice(0, reexport.length - name.length - 1);
82
+ const edge = graph.importEdges.get(mod.owner.source)?.get(innerAlias);
83
+ return edge ? resolveExported(graph, edge.targetSource, name, seen) : undefined;
84
+ }
85
+
86
+ /** Resolve the `!ref` under the cursor to its target resource's definition.
87
+ *
88
+ * The ref grammar mirrors `resolveRefSentinels`: the tag's value is split on
89
+ * the first dot — a bare name (or `Self.name`) is a local resource in the
90
+ * current module; `Alias.name` is an exported instance of the module the import
91
+ * `Alias` points at, followed transitively through re-exports and gated on each
92
+ * module's `exports.resources`. Returns `undefined` when the cursor isn't on a
93
+ * `!ref`, or the target can't be found (e.g. a scope-local name, an unexported
94
+ * instance, or an import that failed to load). */
95
+ export function buildDefinition(
96
+ text: string,
97
+ line: number,
98
+ character: number,
99
+ graph: LoadedGraph,
100
+ currentFilePath: string,
101
+ docs?: AstDocument[],
102
+ ): DefinitionResult | undefined {
103
+ const astDocs = docs ?? parseToAst(text);
104
+ const node = resolveNodeAtPosition(text, astDocs, line, character)?.node;
105
+ if (!node || node.kind !== "scalar" || node.tag !== "!ref") return undefined;
106
+
107
+ // The scalar's range covers the ref target text (the value after `!ref`).
108
+ const source = text.slice(node.range[0], node.range[1]).trim();
109
+ if (!source) return undefined;
110
+
111
+ const currentModule = moduleForFile(graph, currentFilePath) ?? graph.entry;
112
+ const dot = source.indexOf(".");
113
+ const alias = dot === -1 ? undefined : source.slice(0, dot);
114
+ const name = dot === -1 ? source : source.slice(dot + 1);
115
+
116
+ if (alias === undefined || alias === "Self") {
117
+ return locateResource([currentModule.owner, ...currentModule.partials], name);
118
+ }
119
+
120
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
121
+ if (!edge) return undefined;
122
+ return resolveExported(graph, edge.targetSource, name);
123
+ }
@@ -0,0 +1 @@
1
+ export { buildDefinition } from "./build-definition.js";
@@ -0,0 +1,149 @@
1
+ import {
2
+ parseToAst,
3
+ type AnalysisRegistry,
4
+ type AstDocument,
5
+ } from "@telorun/analyzer";
6
+ import type { HoverResult } from "../types.js";
7
+ import { navigateSchema } from "../completions/detect-context.js";
8
+ import {
9
+ resolveNodeAtPosition,
10
+ scalarString,
11
+ type ResolvedCursor,
12
+ } from "../completions/resolve-node.js";
13
+ import { CAPABILITY_DOCS } from "../completions/valid-capabilities.js";
14
+
15
+ type Definition = NonNullable<ReturnType<AnalysisRegistry["resolveDefinition"]>>;
16
+
17
+ /** Docs for the structural keys shared by every module doc, so hover is useful
18
+ * even at the root, where there is no user-authored schema to navigate. */
19
+ const STRUCTURAL_KEY_DOCS: Record<string, string> = {
20
+ kind: "The resource kind — `Alias.Name` for an imported kind, or a `Telo.*` root kind.",
21
+ metadata: "Resource identity: `name` (kebab-case, dot-free) and optional `namespace`.",
22
+ imports: "Dependency map: PascalCase alias → `namespace/name@version` source string or object.",
23
+ targets: "Boot sequence run after init — references to `Runnable`/`Service` resources or inline invoke steps.",
24
+ variables: "Typed inputs bound from host env vars (`env:` + JSON-Schema `type:`).",
25
+ secrets: "Secret inputs bound from host env vars (`env:` + `type:`).",
26
+ ports: "Inbound ports the app listens on, each bound to a host env var (Application only).",
27
+ exports: "What importers may reference: `kinds` (kind gate) and `resources` (instance singletons).",
28
+ include: "Partial files loaded into this module scope (paths / globs).",
29
+ capability: "The lifecycle role of the kind this definition registers.",
30
+ schema: "JSON Schema for the kind's config fields, with `x-telo-*` annotations.",
31
+ extends: "Alias-form kind this definition specializes (abstract contract or concrete parent).",
32
+ base: "Construction mapping (`super(...)`) over `self` for a concrete-`extends` definition.",
33
+ controllers: "Controller locator (`pkg:npm`) implementing this kind.",
34
+ };
35
+
36
+ /** The kind value of the map that directly encloses `keyName` in the value slot. */
37
+ function typeName(t: string | Record<string, any> | undefined): string | undefined {
38
+ if (typeof t === "string") return t;
39
+ if (t && typeof t === "object" && typeof t.title === "string") return t.title;
40
+ return undefined;
41
+ }
42
+
43
+ function kindHover(kind: string, def: Definition | undefined): string {
44
+ if (!def) return `\`${kind}\``;
45
+ const lines: string[] = [`### ${kind}`];
46
+ const role = def.capability ? `\`${def.capability}\`` : "resource";
47
+ const module = def.metadata?.module ? ` · module \`${def.metadata.module}\`` : "";
48
+ lines.push(`${role}${module}`);
49
+ const schema = def.schema as Record<string, any> | undefined;
50
+ const desc = schema?.description ?? schema?.title;
51
+ if (typeof desc === "string" && desc) lines.push("", desc);
52
+ if (def.extends) lines.push("", `Extends \`${def.extends}\``);
53
+ const input = typeName(def.inputType);
54
+ const output = typeName(def.outputType);
55
+ if (input) lines.push(`Input \`${input}\``);
56
+ if (output) lines.push(`Output \`${output}\``);
57
+ return lines.join("\n");
58
+ }
59
+
60
+ function fieldHover(keyName: string, field: Record<string, any>): string {
61
+ const lines: string[] = [];
62
+ const type = Array.isArray(field.type) ? field.type.join(" | ") : field.type;
63
+ const head = type ? `**${keyName}**: \`${type}\`` : `**${keyName}**`;
64
+ lines.push(head);
65
+ if (typeof field.description === "string" && field.description) {
66
+ lines.push("", field.description);
67
+ }
68
+ const ref = field["x-telo-ref"];
69
+ if (typeof ref === "string") lines.push("", `Reference → \`${ref}\``);
70
+ if (Array.isArray(field.enum) && field.enum.length > 0) {
71
+ lines.push("", `Allowed: ${field.enum.map((v: unknown) => `\`${v}\``).join(", ")}`);
72
+ }
73
+ if (field.default !== undefined) lines.push(`Default: \`${JSON.stringify(field.default)}\``);
74
+ return lines.length > 0 ? lines.join("\n") : `**${keyName}**`;
75
+ }
76
+
77
+ /** Field schema at the nearest enclosing resource, or undefined when the scope
78
+ * can't be resolved (no kind-bearing ancestor, or the path doesn't navigate). */
79
+ function fieldSchemaFor(
80
+ resourceKind: string | undefined,
81
+ relativePath: string[],
82
+ registry: AnalysisRegistry | undefined,
83
+ ): Record<string, any> | undefined {
84
+ if (!resourceKind || !registry) return undefined;
85
+ const def = registry.resolveDefinition(resourceKind);
86
+ if (!def?.schema) return undefined;
87
+ return navigateSchema(def.schema as Record<string, any>, relativePath);
88
+ }
89
+
90
+ export function buildHover(
91
+ text: string,
92
+ line: number,
93
+ character: number,
94
+ registry: AnalysisRegistry | undefined,
95
+ docs?: AstDocument[],
96
+ ): HoverResult | undefined {
97
+ const astDocs = docs ?? parseToAst(text);
98
+ const resolved = resolveNodeAtPosition(text, astDocs, line, character);
99
+ if (!resolved) return undefined;
100
+
101
+ if (resolved.slot === "value") return hoverForValue(resolved, registry);
102
+ return hoverForKey(resolved, registry);
103
+ }
104
+
105
+ function hoverForValue(
106
+ resolved: ResolvedCursor,
107
+ registry: AnalysisRegistry | undefined,
108
+ ): HoverResult | undefined {
109
+ const key = resolved.path[resolved.path.length - 1];
110
+ const value = scalarString(resolved.node);
111
+ const range = resolved.replaceRange;
112
+
113
+ if (key === "kind" && value) {
114
+ return { contents: kindHover(value, registry?.resolveDefinition(value)), range };
115
+ }
116
+ if (key === "capability" && resolved.docKind === "Telo.Definition" && value) {
117
+ const doc = CAPABILITY_DOCS[value];
118
+ return doc ? { contents: `**${value}**\n\n${doc}`, range } : undefined;
119
+ }
120
+
121
+ // Field value: describe the field via the enclosing resource's schema. Works
122
+ // when the value sits directly under a kind-bearing map (`siblingKind`); the
123
+ // field path relative to that map is just the key.
124
+ if (key) {
125
+ const field = fieldSchemaFor(resolved.siblingKind, [key], registry);
126
+ if (field) return { contents: fieldHover(key, field), range };
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ function hoverForKey(
132
+ resolved: ResolvedCursor,
133
+ registry: AnalysisRegistry | undefined,
134
+ ): HoverResult | undefined {
135
+ const keyName = scalarString(resolved.node);
136
+ if (!keyName) return undefined;
137
+ const range = resolved.replaceRange;
138
+
139
+ const resourceKind = resolved.resourceKind ?? resolved.docKind;
140
+ const relativePath = [...resolved.path.slice(resolved.resourceDepth ?? 0), keyName];
141
+ const field = fieldSchemaFor(resourceKind, relativePath, registry);
142
+ if (field) return { contents: fieldHover(keyName, field), range };
143
+
144
+ const structural = STRUCTURAL_KEY_DOCS[keyName];
145
+ if (structural && relativePath.length === 1) {
146
+ return { contents: `**${keyName}**\n\n${structural}`, range };
147
+ }
148
+ return undefined;
149
+ }
@@ -0,0 +1 @@
1
+ export { buildHover } from "./build-hover.js";
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from "./types.js";
2
2
  export * from "./completions/index.js";
3
3
  export * from "./diagnostics/index.js";
4
+ export * from "./hover/index.js";
5
+ export * from "./semantic-tokens/index.js";
6
+ export * from "./definition/index.js";
@@ -0,0 +1,64 @@
1
+ import {
2
+ buildLineOffsets,
3
+ offsetToPosition,
4
+ parseToAst,
5
+ type AnalysisRegistry,
6
+ type AstDocument,
7
+ type AstNode,
8
+ } from "@telorun/analyzer";
9
+ import type { SemanticToken } from "../types.js";
10
+ import { scalarString } from "../completions/resolve-node.js";
11
+ import { CAPABILITY_VALUES } from "../completions/valid-capabilities.js";
12
+
13
+ const CAPABILITIES = new Set<string>(CAPABILITY_VALUES);
14
+
15
+ /** Registry-aware semantic tokens: a `kind:` value that resolves to a known
16
+ * definition is a `type`; a `capability:` value is an `interface`; a `!ref`
17
+ * target is a `variable`. Everything else (structure, CEL, tags) is left to the
18
+ * TextMate grammar. Ref targets are colored here rather than in the grammar
19
+ * because a `!ref` after a `key:` is tokenized by the bundled YAML grammar
20
+ * before a Telo pattern can claim it — the AST sees it unambiguously. An
21
+ * unresolved kind gets no token, so a typo stays uncolored — a quiet signal
22
+ * that pairs with the analyzer's `UNDEFINED_KIND` diagnostic. */
23
+ export function buildSemanticTokens(
24
+ text: string,
25
+ registry: AnalysisRegistry | undefined,
26
+ docs?: AstDocument[],
27
+ ): SemanticToken[] {
28
+ const astDocs = docs ?? parseToAst(text);
29
+ const lineOffsets = buildLineOffsets(text);
30
+
31
+ const tokens: SemanticToken[] = [];
32
+ const emit = (node: AstNode | undefined, type: SemanticToken["type"]): void => {
33
+ if (!node) return;
34
+ const start = offsetToPosition(node.range[0], lineOffsets);
35
+ const end = offsetToPosition(node.range[1], lineOffsets);
36
+ // Kind / capability values never span lines; a clamped single-line token.
37
+ if (start.line !== end.line) return;
38
+ tokens.push({ line: start.line, character: start.character, length: end.character - start.character, type });
39
+ };
40
+
41
+ const walk = (node: AstNode): void => {
42
+ if (node.kind === "map") {
43
+ for (const pair of node.entries) {
44
+ const key = scalarString(pair.key);
45
+ const value = scalarString(pair.value);
46
+ if (key === "kind" && value && registry?.resolveDefinition(value)) {
47
+ emit(pair.value, "type");
48
+ } else if (key === "capability" && value && CAPABILITIES.has(value)) {
49
+ emit(pair.value, "interface");
50
+ }
51
+ if (pair.value) walk(pair.value);
52
+ }
53
+ } else if (node.kind === "seq") {
54
+ for (const item of node.items) walk(item);
55
+ } else if (node.kind === "scalar" && node.tag === "!ref") {
56
+ emit(node, "variable");
57
+ }
58
+ };
59
+
60
+ for (const doc of astDocs) {
61
+ if (doc.root) walk(doc.root);
62
+ }
63
+ return tokens;
64
+ }
@@ -0,0 +1 @@
1
+ export { buildSemanticTokens } from "./build-semantic-tokens.js";
package/src/types.ts CHANGED
@@ -10,10 +10,22 @@ export type {
10
10
  PositionIndex,
11
11
  } from "@telorun/analyzer";
12
12
 
13
- import type { AnalysisRegistry, DiagnosticSeverity, PositionIndex, Range } from "@telorun/analyzer";
13
+ import type {
14
+ AnalysisRegistry,
15
+ DiagnosticSeverity,
16
+ Position,
17
+ PositionIndex,
18
+ Range,
19
+ } from "@telorun/analyzer";
14
20
 
15
21
  export type CompletionKind = "class" | "enumMember" | "property" | "folder" | "module" | "value";
16
22
 
23
+ /** A source span the host replaces wholesale when a completion is accepted. */
24
+ export interface ReplaceRange {
25
+ start: Position;
26
+ end: Position;
27
+ }
28
+
17
29
  export interface CompletionResult {
18
30
  label: string;
19
31
  kind: CompletionKind;
@@ -24,24 +36,69 @@ export interface CompletionResult {
24
36
  preselect?: boolean;
25
37
  sortText?: string;
26
38
  filterText?: string;
27
- /** When set, the host should replace text from this 0-based column on the
28
- * cursor's line up to the cursor. Required when the completion value
29
- * contains non-word characters (`/`, `@`, `.`) that the host's default
30
- * word boundary would not include in the replaced range. */
31
- replaceFromColumn?: number;
39
+ /** When set, the host replaces this whole source range with the accepted
40
+ * value the full span of the existing node, not just the prefix up to the
41
+ * cursor. This overwrites any suffix after the cursor (`Sql.Co|nnection` +
42
+ * `Sql.Connection` no leftover `nnection`) and cleanly replaces values
43
+ * containing non-word characters (`/`, `@`, `.`). A zero-width range is a
44
+ * pure insert. */
45
+ replaceRange?: ReplaceRange;
46
+ }
47
+
48
+ /** Rendered hover for the symbol under the cursor. `contents` is GitHub-flavored
49
+ * markdown; `range` (when present) is the source span the host underlines. */
50
+ export interface HoverResult {
51
+ contents: string;
52
+ range?: ReplaceRange;
53
+ }
54
+
55
+ /** Semantic token type names emitted by `buildSemanticTokens`. Kept to the
56
+ * standard VS Code / LSP set so hosts register them against a stock legend and
57
+ * every theme colors them without extra configuration. `type` marks a resolved
58
+ * resource kind; `interface` marks a capability value; `variable` marks a
59
+ * `!ref` target. */
60
+ export type SemanticTokenType = "type" | "interface" | "variable";
61
+
62
+ /** The legend a host registers before mapping `buildSemanticTokens` output. The
63
+ * numeric token-type of each `SemanticToken` is its index in this array. */
64
+ export const SEMANTIC_TOKEN_LEGEND: readonly SemanticTokenType[] = ["type", "interface", "variable"];
65
+
66
+ /** One absolute-positioned semantic token. Every Telo semantic token is
67
+ * single-line (kinds and capabilities never wrap), so a `{line, char, length}`
68
+ * triple is sufficient; the host encodes it into its own builder. */
69
+ export interface SemanticToken {
70
+ line: number;
71
+ character: number;
72
+ length: number;
73
+ type: SemanticTokenType;
74
+ }
75
+
76
+ /** Where a `!ref` target is defined — for go-to-definition. `uri` is the
77
+ * target file's canonical source (absolute path for local files, an http/oci
78
+ * URL for a registry import); `range` spans the target resource's
79
+ * `metadata.name` (falling back to its first line). */
80
+ export interface DefinitionResult {
81
+ uri: string;
82
+ range: Range;
32
83
  }
33
84
 
34
- export interface RegistryModule {
35
- namespace: string;
36
- name: string;
37
- version: string;
85
+ /** A candidate module ref surfaced by the hub's `/refs` lexical autocomplete.
86
+ * Identity is the location ref, never `namespace/name` — an OCI module has no
87
+ * addressable `namespace/name`. `latestVersion` seeds a pinned `ref@version`
88
+ * insert so a picked completion is directly usable. */
89
+ export interface HubRef {
90
+ ref: string;
91
+ latestVersion: string;
38
92
  description?: string;
39
93
  }
40
94
 
41
95
  /** Host-supplied bridge that lets ide-support reach the filesystem and the
42
- * module registry without depending on Node, Tauri, or vscode APIs. Each
96
+ * federated telo hub without depending on Node, Tauri, or vscode APIs. Each
43
97
  * host (VSCode extension, Telo editor) builds an adapter scoped to the
44
- * currently-edited manifest before calling `buildCompletions`. */
98
+ * currently-edited manifest before calling `buildCompletions`. Hub lookups are
99
+ * ref-keyed: the hub aggregates modules across every transport (OCI, HTTP,
100
+ * direct URL), so completion speaks its `/refs` + `/module/versions` verbs
101
+ * rather than any single registry. */
45
102
  export interface IdeEnvironmentAdapter {
46
103
  /** Subdirectory names within `relPath` (resolved against the manifest's
47
104
  * directory). Returns [] if the path doesn't exist or isn't a directory.
@@ -50,12 +107,15 @@ export interface IdeEnvironmentAdapter {
50
107
  /** True iff `<relPath>/telo.yaml` exists relative to the manifest dir.
51
108
  * Used to mark directories that are valid import targets. */
52
109
  hasManifest(relPath: string): Promise<boolean>;
53
- /** Free-text search against the configured module registry. Matches against
54
- * name, namespace, and description. Empty `query` should return the full
55
- * (capped) catalog. */
56
- searchRegistry(query: string): Promise<RegistryModule[]>;
57
- /** All published versions for a module, newest first. */
58
- listRegistryVersions(namespace: string, name: string): Promise<string[]>;
110
+ /** Fuzzy lexical ref autocomplete against the configured telo hub
111
+ * (`GET /refs?q=`). The query is matched as a substring over every
112
+ * registered ref, so a bare token (`youtrack`) hits the same ref as its full
113
+ * `oci://…` form. Best-effort — hosts swallow network errors and return []. */
114
+ searchRefs(query: string): Promise<HubRef[]>;
115
+ /** All tracked versions for a location ref, newest first
116
+ * (`GET /module/versions?ref=`). The browser cannot call OCI `tags/list`;
117
+ * the hub holds them from ingest. */
118
+ listVersionsForRef(ref: string): Promise<string[]>;
59
119
  }
60
120
 
61
121
  export interface NormalizedDiagnostic {