@telorun/ide-support 0.14.1 → 0.16.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/dist/cel/cursor-chain.d.ts +30 -0
- package/dist/cel/cursor-chain.d.ts.map +1 -0
- package/dist/cel/cursor-chain.js +30 -0
- package/dist/cel/symbols.d.ts +82 -0
- package/dist/cel/symbols.d.ts.map +1 -0
- package/dist/cel/symbols.js +147 -0
- package/dist/cel/tokens.d.ts +32 -0
- package/dist/cel/tokens.d.ts.map +1 -0
- package/dist/cel/tokens.js +162 -0
- package/dist/completions/build.d.ts +6 -2
- package/dist/completions/build.d.ts.map +1 -1
- package/dist/completions/build.js +52 -24
- package/dist/completions/call-inputs.d.ts +25 -0
- package/dist/completions/call-inputs.d.ts.map +1 -0
- package/dist/completions/call-inputs.js +78 -0
- package/dist/completions/cel-completions.d.ts +26 -0
- package/dist/completions/cel-completions.d.ts.map +1 -0
- package/dist/completions/cel-completions.js +78 -0
- package/dist/completions/detect-context.d.ts +47 -8
- package/dist/completions/detect-context.d.ts.map +1 -1
- package/dist/completions/detect-context.js +51 -15
- package/dist/completions/prop-keys.d.ts +5 -1
- package/dist/completions/prop-keys.d.ts.map +1 -1
- package/dist/completions/prop-keys.js +51 -3
- package/dist/completions/resolve-node.d.ts +9 -2
- package/dist/completions/resolve-node.d.ts.map +1 -1
- package/dist/completions/resolve-node.js +63 -21
- package/dist/definition/build-definition.d.ts +6 -2
- package/dist/definition/build-definition.d.ts.map +1 -1
- package/dist/definition/build-definition.js +16 -3
- package/dist/definition/locate-context-binding.d.ts +15 -0
- package/dist/definition/locate-context-binding.d.ts.map +1 -0
- package/dist/definition/locate-context-binding.js +35 -0
- package/dist/definition/locate-step.d.ts +13 -0
- package/dist/definition/locate-step.d.ts.map +1 -0
- package/dist/definition/locate-step.js +33 -0
- package/dist/definition/resolve-cel-target.d.ts +11 -1
- package/dist/definition/resolve-cel-target.d.ts.map +1 -1
- package/dist/definition/resolve-cel-target.js +14 -14
- package/dist/doc-identity.d.ts +17 -0
- package/dist/doc-identity.d.ts.map +1 -0
- package/dist/doc-identity.js +19 -0
- package/dist/hover/build-hover.d.ts +6 -2
- package/dist/hover/build-hover.d.ts.map +1 -1
- package/dist/hover/build-hover.js +64 -3
- package/dist/import-upgrades/build-import-upgrades.d.ts +57 -3
- package/dist/import-upgrades/build-import-upgrades.d.ts.map +1 -1
- package/dist/import-upgrades/build-import-upgrades.js +67 -10
- package/dist/import-upgrades/index.d.ts +5 -2
- package/dist/import-upgrades/index.d.ts.map +1 -1
- package/dist/import-upgrades/index.js +3 -1
- package/dist/import-upgrades/manifest-cache-url.d.ts +17 -0
- package/dist/import-upgrades/manifest-cache-url.d.ts.map +1 -0
- package/dist/import-upgrades/manifest-cache-url.js +30 -0
- package/dist/import-upgrades/version-compatibility.d.ts +92 -0
- package/dist/import-upgrades/version-compatibility.d.ts.map +1 -0
- package/dist/import-upgrades/version-compatibility.js +121 -0
- package/dist/semantic-tokens/build-semantic-tokens.d.ts +13 -8
- package/dist/semantic-tokens/build-semantic-tokens.d.ts.map +1 -1
- package/dist/semantic-tokens/build-semantic-tokens.js +81 -37
- package/dist/types.d.ts +25 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +16 -2
- package/package.json +2 -2
- package/src/cel/cursor-chain.ts +58 -0
- package/src/cel/symbols.ts +189 -0
- package/src/cel/tokens.ts +169 -0
- package/src/completions/build.ts +85 -22
- package/src/completions/call-inputs.ts +92 -0
- package/src/completions/cel-completions.ts +108 -0
- package/src/completions/detect-context.ts +107 -13
- package/src/completions/prop-keys.ts +59 -2
- package/src/completions/resolve-node.ts +82 -17
- package/src/definition/build-definition.ts +30 -2
- package/src/definition/locate-context-binding.ts +53 -0
- package/src/definition/locate-step.ts +50 -0
- package/src/definition/resolve-cel-target.ts +25 -0
- package/src/doc-identity.ts +31 -0
- package/src/hover/build-hover.ts +67 -1
- package/src/import-upgrades/build-import-upgrades.ts +115 -11
- package/src/import-upgrades/index.ts +22 -1
- package/src/import-upgrades/manifest-cache-url.ts +40 -0
- package/src/import-upgrades/version-compatibility.ts +196 -0
- package/src/semantic-tokens/build-semantic-tokens.ts +84 -30
- package/src/types.ts +47 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
TELO_SURFACE_VERSION,
|
|
2
3
|
buildLineOffsets,
|
|
3
4
|
foldIntegrity,
|
|
4
5
|
isCanonicalIntegrity,
|
|
@@ -14,6 +15,11 @@ import {
|
|
|
14
15
|
type Range,
|
|
15
16
|
} from "@telorun/analyzer";
|
|
16
17
|
import { findImportEntries, type ImportEntry } from "./find-import-entries.js";
|
|
18
|
+
import {
|
|
19
|
+
selectCompatibleVersion,
|
|
20
|
+
type IncompatibilityReason,
|
|
21
|
+
type VersionCompatibilityCheck,
|
|
22
|
+
} from "./version-compatibility.js";
|
|
17
23
|
|
|
18
24
|
/** One version of a module, as the hub reports it.
|
|
19
25
|
*
|
|
@@ -41,6 +47,23 @@ export interface ModuleVersion {
|
|
|
41
47
|
* {@link parseModuleVersions}. */
|
|
42
48
|
export type ModuleVersionLookup = (baseRef: string) => Promise<ModuleVersion[]>;
|
|
43
49
|
|
|
50
|
+
/** Everything an upgrade needs from its host: what versions exist, and whether
|
|
51
|
+
* this runtime can host one.
|
|
52
|
+
*
|
|
53
|
+
* `isCompatible` is required rather than optional so no host can quietly skip
|
|
54
|
+
* the check and walk an author onto a version their telo cannot load — the gap
|
|
55
|
+
* this package existed with. A host that genuinely cannot fetch candidate
|
|
56
|
+
* manifests passes {@link uncheckedVersionCompatibility}, which says so
|
|
57
|
+
* instead of pretending. */
|
|
58
|
+
export interface ImportUpgradeEnvironment {
|
|
59
|
+
listVersions: ModuleVersionLookup;
|
|
60
|
+
isCompatible: VersionCompatibilityCheck;
|
|
61
|
+
/** Offer prereleases as automatic upgrade targets. Off by default, matching
|
|
62
|
+
* `telo upgrade`: an upgrade nobody asked for must not walk a caller onto an
|
|
63
|
+
* `-rc` build. */
|
|
64
|
+
includePrerelease?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
/** A source edit a host applies verbatim to upgrade an import. Ranges never
|
|
45
68
|
* overlap, within an upgrade or across a batch, so a host may apply the whole
|
|
46
69
|
* set in one pass without ordering them. */
|
|
@@ -69,6 +92,11 @@ export interface ImportUpgrade {
|
|
|
69
92
|
* means no pin was available for the target version — a host should say so
|
|
70
93
|
* when `wasPinned`, since the rewrite silently drops a hash the author had. */
|
|
71
94
|
repinned: boolean;
|
|
95
|
+
/** A newer version that exists but this runtime cannot host, when
|
|
96
|
+
* `latestVersion` is not the newest published. Surfacing it is not optional:
|
|
97
|
+
* an upgrade that quietly stops short of the newest version, with no reason
|
|
98
|
+
* given, reads as a bug in the tooling. */
|
|
99
|
+
heldBack?: { version: string; reason: IncompatibilityReason };
|
|
72
100
|
/** Span of the alias key — where a per-entry affordance anchors. */
|
|
73
101
|
keyRange: Range;
|
|
74
102
|
/** Apply all of these to upgrade this one import. */
|
|
@@ -100,8 +128,18 @@ export interface ImportUpgradeSkip {
|
|
|
100
128
|
latestVersion: string;
|
|
101
129
|
/** Span of the alias key — where a per-entry affordance anchors. */
|
|
102
130
|
keyRange: Range;
|
|
103
|
-
/**
|
|
104
|
-
|
|
131
|
+
/** Which decision this was, for a host that styles or filters them.
|
|
132
|
+
* `incompatible` — every newer version declares a telo this runtime is not;
|
|
133
|
+
* `stale-inline-pin` — the rewrite could not carry or drop the author's pin. */
|
|
134
|
+
code: "incompatible" | "stale-inline-pin";
|
|
135
|
+
/** Which rejection produced an `incompatible` skip, so a host can phrase its
|
|
136
|
+
* own affordance without re-deriving one. Carried BESIDE `code` because the
|
|
137
|
+
* code says what was not done while this says why, and the remedies differ:
|
|
138
|
+
* updating telo cannot fix a requirement the module failed to state. Absent
|
|
139
|
+
* for `stale-inline-pin`, which is not a compatibility decision at all. */
|
|
140
|
+
reason?: IncompatibilityReason;
|
|
141
|
+
/** Author-facing sentence: what was not done, and what to do instead. */
|
|
142
|
+
message: string;
|
|
105
143
|
}
|
|
106
144
|
|
|
107
145
|
export interface ImportUpgradeSet {
|
|
@@ -122,6 +160,13 @@ export interface ImportUpgradeSet {
|
|
|
122
160
|
* re-point it — plus every entry already at the newest version that carries no
|
|
123
161
|
* integrity pin, and the edits that pin it.
|
|
124
162
|
*
|
|
163
|
+
* The target is the newest version this runtime can HOST, which is not always
|
|
164
|
+
* the newest one published: each candidate's declared `requires.telo` is checked
|
|
165
|
+
* newest-first, and a newer version that was held back is reported on the
|
|
166
|
+
* upgrade rather than dropped. An import whose every newer version needs a newer
|
|
167
|
+
* telo produces a `skipped` entry instead, since the alternative — offering it
|
|
168
|
+
* anyway — is a manifest that fails at the load gate.
|
|
169
|
+
*
|
|
125
170
|
* Skips what carries no upgradeable version: local path imports, bare URLs,
|
|
126
171
|
* untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
|
|
127
172
|
* `latest`) — `parseVersionedRef` and `isNewerModuleVersion` both decline to
|
|
@@ -133,7 +178,7 @@ export interface ImportUpgradeSet {
|
|
|
133
178
|
*/
|
|
134
179
|
export async function buildImportUpgrades(
|
|
135
180
|
text: string,
|
|
136
|
-
|
|
181
|
+
env: ImportUpgradeEnvironment,
|
|
137
182
|
docs?: AstDocument[],
|
|
138
183
|
): Promise<ImportUpgradeSet | undefined> {
|
|
139
184
|
const lineOffsets = buildLineOffsets(text);
|
|
@@ -149,7 +194,7 @@ export async function buildImportUpgrades(
|
|
|
149
194
|
const failures: Array<{ baseRef: string; message: string }> = [];
|
|
150
195
|
const known = await resolveVersions(
|
|
151
196
|
[...new Set(candidates.map((c) => c.ref.baseRef))],
|
|
152
|
-
listVersions,
|
|
197
|
+
env.listVersions,
|
|
153
198
|
failures,
|
|
154
199
|
);
|
|
155
200
|
|
|
@@ -160,7 +205,9 @@ export async function buildImportUpgrades(
|
|
|
160
205
|
for (const { entry, ref } of candidates) {
|
|
161
206
|
const versions = known.get(ref.baseRef);
|
|
162
207
|
if (!versions) continue;
|
|
163
|
-
const newest = newestModuleVersion(versions.map((v) => v.version)
|
|
208
|
+
const newest = newestModuleVersion(versions.map((v) => v.version), {
|
|
209
|
+
includePrerelease: env.includePrerelease,
|
|
210
|
+
});
|
|
164
211
|
if (!newest) continue;
|
|
165
212
|
|
|
166
213
|
if (!isNewerModuleVersion(newest, ref.version)) {
|
|
@@ -169,7 +216,40 @@ export async function buildImportUpgrades(
|
|
|
169
216
|
continue;
|
|
170
217
|
}
|
|
171
218
|
|
|
172
|
-
|
|
219
|
+
// The target is the newest version this runtime can actually host, not
|
|
220
|
+
// simply the newest one published. An upgrade the author's telo cannot load
|
|
221
|
+
// is not an upgrade — it is a load failure they were walked into.
|
|
222
|
+
const { best, heldBack } = await selectCompatibleVersion(
|
|
223
|
+
ref.baseRef,
|
|
224
|
+
versions,
|
|
225
|
+
ref.version,
|
|
226
|
+
env.isCompatible,
|
|
227
|
+
{ includePrerelease: env.includePrerelease },
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// `heldBack` is non-null whenever a candidate existed, which `newest` being
|
|
231
|
+
// newer than the current version already established. Narrowed rather than
|
|
232
|
+
// defaulted: a reason nothing established is exactly what the four-verdict
|
|
233
|
+
// split exists to keep out of a message.
|
|
234
|
+
if (!best) {
|
|
235
|
+
if (!heldBack) continue;
|
|
236
|
+
skipped.push({
|
|
237
|
+
alias: entry.alias,
|
|
238
|
+
currentVersion: ref.version,
|
|
239
|
+
latestVersion: newest,
|
|
240
|
+
keyRange: entry.keyRange,
|
|
241
|
+
code: "incompatible",
|
|
242
|
+
reason: heldBack.reason,
|
|
243
|
+
message:
|
|
244
|
+
`'${entry.alias}' has newer versions (up to ${newest}), but none runs on telo ` +
|
|
245
|
+
`${TELO_SURFACE_VERSION} — ${describeReason(heldBack.reason)}. ` +
|
|
246
|
+
describeRemedy(heldBack.reason),
|
|
247
|
+
});
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const target = best.version;
|
|
252
|
+
const integrity = integrityFor(versions, target);
|
|
173
253
|
// A stale pin that can be neither replaced nor removed is the one case left
|
|
174
254
|
// that has to be declined: re-pointing the source while leaving a hash for
|
|
175
255
|
// the version it replaced turns the next install into a tamper error.
|
|
@@ -177,26 +257,28 @@ export async function buildImportUpgrades(
|
|
|
177
257
|
skipped.push({
|
|
178
258
|
alias: entry.alias,
|
|
179
259
|
currentVersion: ref.version,
|
|
180
|
-
latestVersion:
|
|
260
|
+
latestVersion: target,
|
|
181
261
|
keyRange: entry.keyRange,
|
|
182
|
-
|
|
262
|
+
code: "stale-inline-pin",
|
|
263
|
+
message:
|
|
183
264
|
`'${entry.alias}' carries an inline 'integrity:' that shares a line with other ` +
|
|
184
|
-
`fields, and no pin is published for ${
|
|
265
|
+
`fields, and no pin is published for ${target}, so the stale one cannot be ` +
|
|
185
266
|
`removed by a line edit. Run \`telo upgrade\`.`,
|
|
186
267
|
});
|
|
187
268
|
continue;
|
|
188
269
|
}
|
|
189
270
|
|
|
190
|
-
const newSource = withRefVersion(entry.source,
|
|
271
|
+
const newSource = withRefVersion(entry.source, target);
|
|
191
272
|
upgrades.push({
|
|
192
273
|
alias: entry.alias,
|
|
193
274
|
source: entry.source,
|
|
194
275
|
currentVersion: ref.version,
|
|
195
|
-
latestVersion:
|
|
276
|
+
latestVersion: target,
|
|
196
277
|
newSource: foldIntegrity(newSource, integrity),
|
|
197
278
|
wasPinned: ref.integrity != null,
|
|
198
279
|
repinned: integrity != null,
|
|
199
280
|
keyRange: entry.keyRange,
|
|
281
|
+
...(heldBack ? { heldBack } : {}),
|
|
200
282
|
edits: buildEdits(entry, newSource, integrity),
|
|
201
283
|
});
|
|
202
284
|
}
|
|
@@ -204,6 +286,28 @@ export async function buildImportUpgrades(
|
|
|
204
286
|
return { importsKeyRange: block.keyRange, upgrades, pins, skipped, failures };
|
|
205
287
|
}
|
|
206
288
|
|
|
289
|
+
/** How a held-back version reads in a sentence, with the version itself as the
|
|
290
|
+
* implied subject. Names the cause the check actually established.
|
|
291
|
+
*
|
|
292
|
+
* The parameter is REQUIRED, so a caller cannot reach a sentence for a cause
|
|
293
|
+
* nothing established — defaulting an absent reason to "requires a newer telo"
|
|
294
|
+
* is precisely the invented verdict the four-way split exists to prevent. */
|
|
295
|
+
export function describeReason(reason: IncompatibilityReason): string {
|
|
296
|
+
return reason === "unreadable"
|
|
297
|
+
? "its declared requirement cannot be read"
|
|
298
|
+
: "it requires a newer telo";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** What the author can do about it. Split from {@link describeReason} because
|
|
302
|
+
* the two rejections have DIFFERENT remedies: an unreadable requirement is not
|
|
303
|
+
* a version skew, so telling the author to update telo would send them after a
|
|
304
|
+
* fix that cannot work — only the module's own author can repair it. */
|
|
305
|
+
export function describeRemedy(reason: IncompatibilityReason): string {
|
|
306
|
+
return reason === "unreadable"
|
|
307
|
+
? "Only the module's author can fix that."
|
|
308
|
+
: "Update telo to upgrade it.";
|
|
309
|
+
}
|
|
310
|
+
|
|
207
311
|
/** The edits that re-point an entry at `newSource` and settle its pin.
|
|
208
312
|
*
|
|
209
313
|
* Where the pin is written follows the shape the author chose: an entry with an
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
buildImportUpgrades,
|
|
3
|
+
describeReason,
|
|
4
|
+
describeRemedy,
|
|
5
|
+
} from "./build-import-upgrades.js";
|
|
2
6
|
export type {
|
|
3
7
|
ImportPin,
|
|
4
8
|
ImportUpgrade,
|
|
5
9
|
ImportUpgradeEdit,
|
|
10
|
+
ImportUpgradeEnvironment,
|
|
6
11
|
ImportUpgradeSet,
|
|
7
12
|
ImportUpgradeSkip,
|
|
8
13
|
ModuleVersion,
|
|
9
14
|
ModuleVersionLookup,
|
|
10
15
|
} from "./build-import-upgrades.js";
|
|
16
|
+
export { moduleManifestCacheUrl } from "./manifest-cache-url.js";
|
|
11
17
|
export { parseModuleVersions } from "./parse-module-versions.js";
|
|
12
18
|
export { findImportEntries } from "./find-import-entries.js";
|
|
13
19
|
export type {
|
|
@@ -15,3 +21,18 @@ export type {
|
|
|
15
21
|
ImportEntryIntegrity,
|
|
16
22
|
ImportsBlock,
|
|
17
23
|
} from "./find-import-entries.js";
|
|
24
|
+
export {
|
|
25
|
+
createVersionCompatibility,
|
|
26
|
+
markVersionCompatibility,
|
|
27
|
+
noneRunnableReason,
|
|
28
|
+
selectCompatibleVersion,
|
|
29
|
+
uncheckedVersionCompatibility,
|
|
30
|
+
} from "./version-compatibility.js";
|
|
31
|
+
export type {
|
|
32
|
+
IncompatibilityReason,
|
|
33
|
+
MarkedVersion,
|
|
34
|
+
ModuleCompatibility,
|
|
35
|
+
ModuleManifestReader,
|
|
36
|
+
VersionCompatibilityCheck,
|
|
37
|
+
VersionSelection,
|
|
38
|
+
} from "./version-compatibility.js";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MANIFEST_CACHE_BASE_URL,
|
|
3
|
+
isOciRef,
|
|
4
|
+
manifestCacheUrl,
|
|
5
|
+
urlManifestCacheCoords,
|
|
6
|
+
withRefVersion,
|
|
7
|
+
} from "@telorun/analyzer";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where one version of a module's `telo.yaml` can be read from the hub's static
|
|
11
|
+
* manifest cache, or `null` when this ref is not addressable there.
|
|
12
|
+
*
|
|
13
|
+
* This is the read path an upgrade check uses to ask a candidate version what
|
|
14
|
+
* runtime it requires: a plain CORS GET of a single small file, no transport
|
|
15
|
+
* protocol and no artifact payload, so it works identically from a browser and
|
|
16
|
+
* from an extension host. The `fetch` itself stays with the host — everything
|
|
17
|
+
* in this package leaves the network to its caller.
|
|
18
|
+
*
|
|
19
|
+
* A ref the cache cannot address (a registry ref, whose origin this side does
|
|
20
|
+
* not know; a URL with no version segment) returns `null`, which the caller
|
|
21
|
+
* reads as "not known" and never as "incompatible". Losing the check for those
|
|
22
|
+
* refs is the honest outcome: the manifest is still gated at load time.
|
|
23
|
+
*/
|
|
24
|
+
export function moduleManifestCacheUrl(
|
|
25
|
+
baseRef: string,
|
|
26
|
+
version: string,
|
|
27
|
+
baseUrl: string = MANIFEST_CACHE_BASE_URL,
|
|
28
|
+
): string | null {
|
|
29
|
+
if (isOciRef(baseRef)) {
|
|
30
|
+
let versioned: string;
|
|
31
|
+
try {
|
|
32
|
+
versioned = withRefVersion(baseRef, version);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return manifestCacheUrl(versioned, baseUrl);
|
|
37
|
+
}
|
|
38
|
+
const coords = urlManifestCacheCoords(baseRef, version);
|
|
39
|
+
return coords ? manifestCacheUrl(coords, baseUrl) : null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TELO_SURFACE_VERSION,
|
|
3
|
+
compareModuleVersions,
|
|
4
|
+
isNewerModuleVersion,
|
|
5
|
+
manifestCompatibility,
|
|
6
|
+
parseModuleVersion,
|
|
7
|
+
type ModuleCompatibility,
|
|
8
|
+
} from "@telorun/analyzer";
|
|
9
|
+
import type { ModuleVersion } from "./build-import-upgrades.js";
|
|
10
|
+
|
|
11
|
+
export type { ModuleCompatibility };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Host capability: the `telo.yaml` text of one version of a module, or `null`
|
|
15
|
+
* when this host cannot address it (a transport it does not speak, a ref with
|
|
16
|
+
* no version segment). Rejecting is equally fine — both read as "not known",
|
|
17
|
+
* never as "incompatible".
|
|
18
|
+
*
|
|
19
|
+
* Narrow on purpose, and separate from {@link ModuleVersionLookup}: enumerating
|
|
20
|
+
* versions and reading one manifest are different routes with different costs,
|
|
21
|
+
* and a host caches or throttles them differently.
|
|
22
|
+
*/
|
|
23
|
+
export type ModuleManifestReader = (
|
|
24
|
+
baseRef: string,
|
|
25
|
+
version: string,
|
|
26
|
+
) => Promise<string | null | undefined>;
|
|
27
|
+
|
|
28
|
+
/** The question an upgrade asks of a candidate version. */
|
|
29
|
+
export type VersionCompatibilityCheck = (
|
|
30
|
+
baseRef: string,
|
|
31
|
+
version: string,
|
|
32
|
+
) => Promise<ModuleCompatibility>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A compatibility check backed by `read`, memoized per module and version for
|
|
36
|
+
* the lifetime of the returned function.
|
|
37
|
+
*
|
|
38
|
+
* The cache is not an optimization detail — an IDE re-derives its upgrade
|
|
39
|
+
* affordances on every keystroke, and a published version's declared
|
|
40
|
+
* requirement is immutable, so refetching it would be pure waste against an
|
|
41
|
+
* answer that cannot have changed. In-flight promises are shared, so the
|
|
42
|
+
* concurrent lookups one file's imports produce collapse to one read each.
|
|
43
|
+
*
|
|
44
|
+
* The verdict is always the analyzer's {@link manifestCompatibility}: an IDE
|
|
45
|
+
* reports NO host versions, because it is not the machine that will run the
|
|
46
|
+
* manifest. Only the telo surface is checked here; a host requirement still
|
|
47
|
+
* surfaces at the load gate when the manifest is actually run.
|
|
48
|
+
*/
|
|
49
|
+
export function createVersionCompatibility(
|
|
50
|
+
read: ModuleManifestReader,
|
|
51
|
+
teloVersion: string = TELO_SURFACE_VERSION,
|
|
52
|
+
): VersionCompatibilityCheck {
|
|
53
|
+
const cache = new Map<string, Promise<ModuleCompatibility>>();
|
|
54
|
+
return (baseRef, version) => {
|
|
55
|
+
const key = `${baseRef}@${version}`;
|
|
56
|
+
const cached = cache.get(key);
|
|
57
|
+
if (cached) return cached;
|
|
58
|
+
const pending = read(baseRef, version)
|
|
59
|
+
.then((text) => (text ? manifestCompatibility(text, teloVersion) : "unknown"))
|
|
60
|
+
.catch(() => "unknown" as const);
|
|
61
|
+
cache.set(key, pending);
|
|
62
|
+
// Only a DECIDED verdict is kept. What is immutable is a published version's
|
|
63
|
+
// declared requirement, not the network: caching `unknown` would let one
|
|
64
|
+
// offline moment disable the check for the rest of the session, leaving the
|
|
65
|
+
// UI reading as though every version had been checked and cleared. Dropped
|
|
66
|
+
// after it settles rather than before, so concurrent asks still share the
|
|
67
|
+
// one in-flight read.
|
|
68
|
+
void pending.then((verdict) => {
|
|
69
|
+
if (verdict === "unknown" && cache.get(key) === pending) cache.delete(key);
|
|
70
|
+
});
|
|
71
|
+
return pending;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A check that answers `unknown` for everything — the honest reading for a
|
|
76
|
+
* host with no way to fetch a candidate manifest. Never blocks an upgrade. */
|
|
77
|
+
export const uncheckedVersionCompatibility: VersionCompatibilityCheck = async () => "unknown";
|
|
78
|
+
|
|
79
|
+
/** Why a candidate was not offered. `unknown` never appears: a version that
|
|
80
|
+
* could not be read is offered, since an unreachable source must not silently
|
|
81
|
+
* freeze an author's imports. */
|
|
82
|
+
export type IncompatibilityReason = Exclude<ModuleCompatibility, "yes" | "unknown">;
|
|
83
|
+
|
|
84
|
+
export interface VersionSelection {
|
|
85
|
+
/** The newest version this runtime can host that is also newer than the
|
|
86
|
+
* current one, or `null` when there is none. */
|
|
87
|
+
best: ModuleVersion | null;
|
|
88
|
+
/** The newest candidate overall, when it is NOT `best` — what was held back.
|
|
89
|
+
* Reported rather than swallowed: without it an upgrade affordance says
|
|
90
|
+
* "up to date" while newer versions exist, which is a silent ceiling and a
|
|
91
|
+
* worse report than the load failure this whole check replaces. */
|
|
92
|
+
heldBack: { version: string; reason: IncompatibilityReason } | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The newest version whose declared requirement accepts this runtime, among
|
|
97
|
+
* those newer than `currentVersion`.
|
|
98
|
+
*
|
|
99
|
+
* Walks newest-first and stops at the first satisfied candidate, so the common
|
|
100
|
+
* case — the newest version is compatible — costs a single manifest read.
|
|
101
|
+
* Candidates older than the current version are never considered: an upgrade
|
|
102
|
+
* that walks backwards is a downgrade nobody asked for. Pass `null` for an
|
|
103
|
+
* import that does not exist yet, where every published version is a candidate
|
|
104
|
+
* and there is nothing to walk backwards from.
|
|
105
|
+
*/
|
|
106
|
+
export async function selectCompatibleVersion(
|
|
107
|
+
baseRef: string,
|
|
108
|
+
versions: readonly ModuleVersion[],
|
|
109
|
+
currentVersion: string | null,
|
|
110
|
+
isCompatible: VersionCompatibilityCheck,
|
|
111
|
+
options: { includePrerelease?: boolean } = {},
|
|
112
|
+
): Promise<VersionSelection> {
|
|
113
|
+
const candidates = upgradeCandidates(versions, currentVersion, options);
|
|
114
|
+
if (candidates.length === 0) return { best: null, heldBack: null };
|
|
115
|
+
|
|
116
|
+
const newest = candidates[0]!;
|
|
117
|
+
let firstReason: IncompatibilityReason | null = null;
|
|
118
|
+
for (const candidate of candidates) {
|
|
119
|
+
const verdict = await isCompatible(baseRef, candidate.version);
|
|
120
|
+
if (verdict === "too-new" || verdict === "unreadable") {
|
|
121
|
+
firstReason ??= verdict;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const held = candidate === newest ? null : { version: newest.version, reason: firstReason! };
|
|
125
|
+
return { best: candidate, heldBack: held };
|
|
126
|
+
}
|
|
127
|
+
return { best: null, heldBack: { version: newest.version, reason: firstReason! } };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** One version as a picker renders it: what it is, and what this runtime makes
|
|
131
|
+
* of it. */
|
|
132
|
+
export interface MarkedVersion extends ModuleVersion {
|
|
133
|
+
compatibility: ModuleCompatibility;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every version, each carrying its own verdict — what a deliberate pick needs.
|
|
138
|
+
*
|
|
139
|
+
* Unlike {@link selectCompatibleVersion} this cannot short-circuit: a picker
|
|
140
|
+
* marks entries it is not going to choose, so every entry has to be asked. That
|
|
141
|
+
* is why the two are separate operations rather than one with a flag — the
|
|
142
|
+
* automatic path must stay one read, and the picker is an explicit action on a
|
|
143
|
+
* single import whose reads are cached from then on.
|
|
144
|
+
*/
|
|
145
|
+
export async function markVersionCompatibility(
|
|
146
|
+
baseRef: string,
|
|
147
|
+
versions: readonly ModuleVersion[],
|
|
148
|
+
isCompatible: VersionCompatibilityCheck,
|
|
149
|
+
): Promise<MarkedVersion[]> {
|
|
150
|
+
return Promise.all(
|
|
151
|
+
versions.map(async (version) => ({
|
|
152
|
+
...version,
|
|
153
|
+
compatibility: await isCompatible(baseRef, version.version),
|
|
154
|
+
})),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Why nothing in a marked list can be offered, or `null` when something can.
|
|
160
|
+
*
|
|
161
|
+
* A picker has to state this outright: a list where every row is marked
|
|
162
|
+
* explains nothing on its own, and the reader is left thinking the tool is
|
|
163
|
+
* broken. A reason rather than a boolean because the two rejections have
|
|
164
|
+
* different remedies — see {@link describeRemedy}. `unknown` counts as
|
|
165
|
+
* offerable: a version this host could not check is not one it may refuse.
|
|
166
|
+
*/
|
|
167
|
+
export function noneRunnableReason(
|
|
168
|
+
versions: readonly MarkedVersion[],
|
|
169
|
+
): IncompatibilityReason | null {
|
|
170
|
+
if (versions.length === 0) return null;
|
|
171
|
+
if (versions.some((v) => v.compatibility === "yes" || v.compatibility === "unknown")) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
// The newest blocked version's reason, in the list's own (newest-first)
|
|
175
|
+
// order: it is the one the reader is asking about.
|
|
176
|
+
return versions[0]!.compatibility as IncompatibilityReason;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Candidates strictly newer than `currentVersion`, newest first.
|
|
180
|
+
*
|
|
181
|
+
* Ordering and prerelease exclusion come from the analyzer's shared rule, so
|
|
182
|
+
* "is this import behind" and "what would it move to" cannot disagree. */
|
|
183
|
+
function upgradeCandidates(
|
|
184
|
+
versions: readonly ModuleVersion[],
|
|
185
|
+
currentVersion: string | null,
|
|
186
|
+
options: { includePrerelease?: boolean },
|
|
187
|
+
): ModuleVersion[] {
|
|
188
|
+
return versions
|
|
189
|
+
.filter((v) => {
|
|
190
|
+
const parsed = parseModuleVersion(v.version);
|
|
191
|
+
if (parsed === null) return false;
|
|
192
|
+
if (parsed.pre !== null && !options.includePrerelease) return false;
|
|
193
|
+
return currentVersion === null || isNewerModuleVersion(v.version, currentVersion);
|
|
194
|
+
})
|
|
195
|
+
.sort((a, b) => compareModuleVersions(b.version, a.version) ?? 0);
|
|
196
|
+
}
|
|
@@ -5,60 +5,114 @@ import {
|
|
|
5
5
|
type AnalysisRegistry,
|
|
6
6
|
type AstDocument,
|
|
7
7
|
type AstNode,
|
|
8
|
+
type AstScalar,
|
|
9
|
+
type CelScope,
|
|
10
|
+
type ManifestAnalysis,
|
|
8
11
|
} from "@telorun/analyzer";
|
|
9
12
|
import type { SemanticToken } from "../types.js";
|
|
13
|
+
import { celSegmentTokens } from "../cel/tokens.js";
|
|
14
|
+
import { docIdentity } from "../doc-identity.js";
|
|
10
15
|
import { scalarString } from "../completions/resolve-node.js";
|
|
11
16
|
import { CAPABILITY_VALUES } from "../completions/valid-capabilities.js";
|
|
12
17
|
|
|
13
18
|
const CAPABILITIES = new Set<string>(CAPABILITY_VALUES);
|
|
14
19
|
|
|
20
|
+
/** Append one key segment to a concrete path (`routes[0]` + `handler`). */
|
|
21
|
+
function joinKey(concrete: string, key: string): string {
|
|
22
|
+
return concrete ? `${concrete}.${key}` : key;
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
/** Registry-aware semantic tokens: a `kind:` value that resolves to a known
|
|
16
26
|
* definition is a `type`; a `capability:` value is an `interface`; a `!ref`
|
|
17
|
-
* target is a `variable`.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
27
|
+
* target is a `variable`. Ref targets are colored here rather than in the
|
|
28
|
+
* grammar because a `!ref` after a `key:` is tokenized by the bundled YAML
|
|
29
|
+
* grammar before a Telo pattern can claim it — the AST sees it unambiguously.
|
|
30
|
+
* An unresolved kind gets no token, so a typo stays uncolored — a quiet signal
|
|
31
|
+
* that pairs with the analyzer's `UNDEFINED_KIND` diagnostic.
|
|
32
|
+
*
|
|
33
|
+
* The inside of a `!cel` / `${{ }}` body is colored here too, and for the same
|
|
34
|
+
* reason one level down: a grammar can only know the roots someone hardcoded
|
|
35
|
+
* into it, while `scopeQuery` knows what is in scope at this exact site. With
|
|
36
|
+
* no query the names are colored syntactically instead — a CEL body must never
|
|
37
|
+
* read as a plain string, which is what the stock YAML grammar makes of it. */
|
|
23
38
|
export function buildSemanticTokens(
|
|
24
39
|
text: string,
|
|
25
40
|
registry: AnalysisRegistry | undefined,
|
|
26
41
|
docs?: AstDocument[],
|
|
42
|
+
analysis?: ManifestAnalysis,
|
|
27
43
|
): SemanticToken[] {
|
|
28
44
|
const astDocs = docs ?? parseToAst(text);
|
|
29
45
|
const lineOffsets = buildLineOffsets(text);
|
|
30
46
|
|
|
31
47
|
const tokens: SemanticToken[] = [];
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
//
|
|
48
|
+
const emitRange = (range: [number, number], type: SemanticToken["type"]): void => {
|
|
49
|
+
const start = offsetToPosition(range[0], lineOffsets);
|
|
50
|
+
const end = offsetToPosition(range[1], lineOffsets);
|
|
51
|
+
// Kind / capability values and CEL identifiers never span lines; a clamped
|
|
52
|
+
// single-line token.
|
|
37
53
|
if (start.line !== end.line) return;
|
|
38
|
-
tokens.push({
|
|
54
|
+
tokens.push({
|
|
55
|
+
line: start.line,
|
|
56
|
+
character: start.character,
|
|
57
|
+
length: end.character - start.character,
|
|
58
|
+
type,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
const emit = (node: AstNode | undefined, type: SemanticToken["type"]): void => {
|
|
62
|
+
if (node) emitRange(node.range, type);
|
|
39
63
|
};
|
|
40
64
|
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
for (const doc of astDocs) {
|
|
66
|
+
if (!doc.root) continue;
|
|
67
|
+
|
|
68
|
+
// The scope is resolved per SITE. `scopeAt` caches per (resource, path) for
|
|
69
|
+
// the analysis's lifetime, which is what keeps a whole-file colourizer off
|
|
70
|
+
// the per-keystroke cost of rebuilding a context-matched environment.
|
|
71
|
+
const identity = docIdentity(doc);
|
|
72
|
+
const scopeQuery = analysis?.celScope;
|
|
73
|
+
const resource = scopeQuery?.resourceFor(identity.kind, identity.name);
|
|
74
|
+
const scopeAt = (path: string): CelScope | undefined =>
|
|
75
|
+
scopeQuery && resource ? scopeQuery.scopeAt(resource, path) : undefined;
|
|
76
|
+
|
|
77
|
+
const celTokens = (node: AstScalar, path: string): void => {
|
|
78
|
+
const segments = node.celSegments();
|
|
79
|
+
if (segments.length === 0) return;
|
|
80
|
+
const scope = scopeAt(path);
|
|
81
|
+
for (const segment of segments) {
|
|
82
|
+
for (const span of celSegmentTokens(text, segment, scope)) emitRange(span.range, span.type);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const walk = (node: AstNode, concrete: string): void => {
|
|
87
|
+
if (node.kind === "map") {
|
|
88
|
+
for (const pair of node.entries) {
|
|
89
|
+
const key = scalarString(pair.key);
|
|
90
|
+
const value = scalarString(pair.value);
|
|
91
|
+
if (key === "kind" && value && registry?.resolveDefinition(value)) {
|
|
92
|
+
emit(pair.value, "type");
|
|
93
|
+
} else if (key === "capability" && value && CAPABILITIES.has(value)) {
|
|
94
|
+
emit(pair.value, "interface");
|
|
95
|
+
}
|
|
96
|
+
if (pair.value) walk(pair.value, key != null ? joinKey(concrete, key) : concrete);
|
|
50
97
|
}
|
|
51
|
-
|
|
98
|
+
return;
|
|
52
99
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
100
|
+
if (node.kind === "seq") {
|
|
101
|
+
// Indices are kept: a CEL site's scope is addressed per item, so an
|
|
102
|
+
// index-erased path resolves the wrong context or none.
|
|
103
|
+
node.items.forEach((item, index) => walk(item, `${concrete}[${index}]`));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (node.kind === "scalar") {
|
|
107
|
+
if (node.tag === "!ref") {
|
|
108
|
+
emit(node, "variable");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
celTokens(node, concrete);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
59
114
|
|
|
60
|
-
|
|
61
|
-
if (doc.root) walk(doc.root);
|
|
115
|
+
walk(doc.root, "");
|
|
62
116
|
}
|
|
63
117
|
return tokens;
|
|
64
118
|
}
|