@telorun/analyzer 0.44.0 → 0.45.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/index.d.ts CHANGED
@@ -20,6 +20,8 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
20
20
  export type { ParseOptions } from "./parse-loaded-file.js";
21
21
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
22
22
  export type { SyntheticImport } from "./inline-imports.js";
23
+ export { compareModuleVersions, compareParsedModuleVersions, isNewerModuleVersion, isSameModuleVersion, parseModuleVersion, } from "./module-version-order.js";
24
+ export type { ParsedModuleVersion } from "./module-version-order.js";
23
25
  export { reconcileModuleVersions } from "./reconcile-module-versions.js";
24
26
  export type { VersionReconciliation } from "./reconcile-module-versions.js";
25
27
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
@@ -33,6 +35,8 @@ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
33
35
  export type { ParsedModuleRef } from "./sources/module-ref.js";
34
36
  export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
35
37
  export type { ParsedOciRef } from "./sources/oci-ref.js";
38
+ export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
39
+ export type { ParsedVersionedRef } from "./sources/versioned-ref.js";
36
40
  export { isLocalPathSource } from "./sources/local-path-ref.js";
37
41
  export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifestCacheKey, manifestCacheUrl, ociManifestCacheCoords, urlManifestCacheCoords, } from "./sources/manifest-cache.js";
38
42
  export type { ManifestCacheCoords } from "./sources/manifest-cache.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { Loader } from "./manifest-loader.js";
11
11
  export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
12
12
  export { parseLoadedFile } from "./parse-loaded-file.js";
13
13
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
14
+ export { compareModuleVersions, compareParsedModuleVersions, isNewerModuleVersion, isSameModuleVersion, parseModuleVersion, } from "./module-version-order.js";
14
15
  export { reconcileModuleVersions } from "./reconcile-module-versions.js";
15
16
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
16
17
  export { buildDocumentPositions, buildLineOffsets, buildPositionIndex, documentLineOffsets, offsetToPosition, } from "./position-metadata.js";
@@ -20,6 +21,7 @@ export { defaultSources } from "./sources/default-sources.js";
20
21
  export { splitIntegrity, foldIntegrity, verifyIntegrity, verifiedFetch, sha256Base64Url, IntegrityError, } from "./sources/integrity.js";
21
22
  export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
22
23
  export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
24
+ export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
23
25
  export { isLocalPathSource } from "./sources/local-path-ref.js";
24
26
  export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifestCacheKey, manifestCacheUrl, ociManifestCacheCoords, urlManifestCacheCoords, } from "./sources/manifest-cache.js";
25
27
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
@@ -0,0 +1,36 @@
1
+ /** SemVer precedence for module versions — the single ordering rule shared by
2
+ * version reconciliation and by any host deciding whether an import is behind.
3
+ *
4
+ * Pure and dependency-free (no `semver` package), so the analyzer stays
5
+ * browser-safe and the editor can reach the same rule the kernel-side analysis
6
+ * uses. A caller that cannot parse a version must not guess: an OCI digest, a
7
+ * moving tag like `latest`, and a malformed pin all come back `null` rather
8
+ * than being ordered by some weaker fallback. */
9
+ export interface ParsedModuleVersion {
10
+ major: number;
11
+ minor: number;
12
+ patch: number;
13
+ /** Dot-separated prerelease identifiers, or `null` for a release version. */
14
+ pre: string[] | null;
15
+ }
16
+ /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
17
+ * isn't a plain three-part numeric core — an unparseable version is never
18
+ * silently ordered. */
19
+ export declare function parseModuleVersion(raw: string | undefined): ParsedModuleVersion | null;
20
+ /** SemVer precedence: numeric core, then a release outranks a prerelease, then
21
+ * prerelease identifiers compared field-by-field (numeric < non-numeric per
22
+ * spec, shorter set loses when all shared fields are equal). */
23
+ export declare function compareParsedModuleVersions(a: ParsedModuleVersion, b: ParsedModuleVersion): number;
24
+ /** Negative / zero / positive when both versions parse, `null` when either does
25
+ * not. The string-in convenience over {@link compareParsedModuleVersions}. */
26
+ export declare function compareModuleVersions(a: string, b: string): number | null;
27
+ /** True when `candidate` is strictly newer than `current` — the test for
28
+ * whether an import is behind. False when they are equal, when `current` is
29
+ * ahead (a version index can lag the module's own origin, and "upgrading" to
30
+ * what it knows would be a downgrade), or when either side is unparseable. */
31
+ export declare function isNewerModuleVersion(candidate: string, current: string): boolean;
32
+ /** True when two tags name the same version, tolerating a `v` prefix on either
33
+ * side. Falls back to exact equality for unparseable tags, so a digest still
34
+ * matches itself. */
35
+ export declare function isSameModuleVersion(a: string, b: string): boolean;
36
+ //# sourceMappingURL=module-version-order.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module-version-order.d.ts","sourceRoot":"","sources":["../src/module-version-order.ts"],"names":[],"mappings":"AAAA;;;;;;;kDAOkD;AAElD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CACtB;AAED;;wBAEwB;AACxB,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,mBAAmB,GAAG,IAAI,CAatF;AAED;;iEAEiE;AACjE,wBAAgB,2BAA2B,CACzC,CAAC,EAAE,mBAAmB,EACtB,CAAC,EAAE,mBAAmB,GACrB,MAAM,CAyBR;AAED;+EAC+E;AAC/E,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED;;;+EAG+E;AAC/E,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEhF;AAED;;sBAEsB;AACtB,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEjE"}
@@ -0,0 +1,91 @@
1
+ /** SemVer precedence for module versions — the single ordering rule shared by
2
+ * version reconciliation and by any host deciding whether an import is behind.
3
+ *
4
+ * Pure and dependency-free (no `semver` package), so the analyzer stays
5
+ * browser-safe and the editor can reach the same rule the kernel-side analysis
6
+ * uses. A caller that cannot parse a version must not guess: an OCI digest, a
7
+ * moving tag like `latest`, and a malformed pin all come back `null` rather
8
+ * than being ordered by some weaker fallback. */
9
+ /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
10
+ * isn't a plain three-part numeric core — an unparseable version is never
11
+ * silently ordered. */
12
+ export function parseModuleVersion(raw) {
13
+ if (typeof raw !== "string")
14
+ return null;
15
+ const v = raw.startsWith("v") ? raw.slice(1) : raw;
16
+ const [core, ...preParts] = v.split("-");
17
+ const pre = preParts.length > 0 ? preParts.join("-") : null;
18
+ const segments = core.split(".");
19
+ if (segments.length !== 3)
20
+ return null;
21
+ const [major, minor, patch] = segments.map((s) => {
22
+ if (!/^\d+$/.test(s))
23
+ return NaN;
24
+ return Number(s);
25
+ });
26
+ if ([major, minor, patch].some((n) => Number.isNaN(n)))
27
+ return null;
28
+ return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
29
+ }
30
+ /** SemVer precedence: numeric core, then a release outranks a prerelease, then
31
+ * prerelease identifiers compared field-by-field (numeric < non-numeric per
32
+ * spec, shorter set loses when all shared fields are equal). */
33
+ export function compareParsedModuleVersions(a, b) {
34
+ if (a.major !== b.major)
35
+ return a.major - b.major;
36
+ if (a.minor !== b.minor)
37
+ return a.minor - b.minor;
38
+ if (a.patch !== b.patch)
39
+ return a.patch - b.patch;
40
+ if (a.pre === null && b.pre === null)
41
+ return 0;
42
+ if (a.pre === null)
43
+ return 1;
44
+ if (b.pre === null)
45
+ return -1;
46
+ const len = Math.max(a.pre.length, b.pre.length);
47
+ for (let i = 0; i < len; i++) {
48
+ const ai = a.pre[i];
49
+ const bi = b.pre[i];
50
+ if (ai === undefined)
51
+ return -1;
52
+ if (bi === undefined)
53
+ return 1;
54
+ const an = /^\d+$/.test(ai);
55
+ const bn = /^\d+$/.test(bi);
56
+ if (an && bn) {
57
+ const d = Number(ai) - Number(bi);
58
+ if (d !== 0)
59
+ return d;
60
+ }
61
+ else if (an !== bn) {
62
+ return an ? -1 : 1;
63
+ }
64
+ else if (ai !== bi) {
65
+ return ai < bi ? -1 : 1;
66
+ }
67
+ }
68
+ return 0;
69
+ }
70
+ /** Negative / zero / positive when both versions parse, `null` when either does
71
+ * not. The string-in convenience over {@link compareParsedModuleVersions}. */
72
+ export function compareModuleVersions(a, b) {
73
+ const left = parseModuleVersion(a);
74
+ const right = parseModuleVersion(b);
75
+ if (!left || !right)
76
+ return null;
77
+ return compareParsedModuleVersions(left, right);
78
+ }
79
+ /** True when `candidate` is strictly newer than `current` — the test for
80
+ * whether an import is behind. False when they are equal, when `current` is
81
+ * ahead (a version index can lag the module's own origin, and "upgrading" to
82
+ * what it knows would be a downgrade), or when either side is unparseable. */
83
+ export function isNewerModuleVersion(candidate, current) {
84
+ return (compareModuleVersions(candidate, current) ?? 0) > 0;
85
+ }
86
+ /** True when two tags name the same version, tolerating a `v` prefix on either
87
+ * side. Falls back to exact equality for unparseable tags, so a digest still
88
+ * matches itself. */
89
+ export function isSameModuleVersion(a, b) {
90
+ return a === b || compareModuleVersions(a, b) === 0;
91
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-module-versions.d.ts","sourceRoot":"","sources":["../src/reconcile-module-versions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAElE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;0CAI0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;2EACuE;IACvE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAuMD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAChD,qBAAqB,CAsEvB"}
1
+ {"version":3,"file":"reconcile-module-versions.d.ts","sourceRoot":"","sources":["../src/reconcile-module-versions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAOlE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;0CAI0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B;2EACuE;IACvE,WAAW,EAAE,kBAAkB,EAAE,CAAC;CACnC;AAgJD;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,EAClC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,GAChD,qBAAqB,CAsEvB"}
@@ -1,69 +1,7 @@
1
1
  import { isModuleKind } from "./module-kinds.js";
2
+ import { compareParsedModuleVersions, parseModuleVersion, } from "./module-version-order.js";
2
3
  import { DiagnosticSeverity } from "./types.js";
3
4
  const SOURCE = "telo-analyzer";
4
- /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
5
- * isn't a plain three-part numeric core — an unparseable version forces the
6
- * group onto the conflict path (we never silently hoist across a version we
7
- * can't reason about). Pure: no dependency on the `semver` package, so the
8
- * analyzer stays browser-safe and dependency-free. */
9
- function parseVersion(raw) {
10
- if (typeof raw !== "string")
11
- return null;
12
- const v = raw.startsWith("v") ? raw.slice(1) : raw;
13
- const [core, ...preParts] = v.split("-");
14
- const pre = preParts.length > 0 ? preParts.join("-") : null;
15
- const segments = core.split(".");
16
- if (segments.length !== 3)
17
- return null;
18
- const [major, minor, patch] = segments.map((s) => {
19
- if (!/^\d+$/.test(s))
20
- return NaN;
21
- return Number(s);
22
- });
23
- if ([major, minor, patch].some((n) => Number.isNaN(n)))
24
- return null;
25
- return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
26
- }
27
- /** SemVer precedence: numeric core, then a release outranks a prerelease, then
28
- * prerelease identifiers compared field-by-field (numeric < non-numeric per
29
- * spec, shorter set loses when all shared fields are equal). */
30
- function compareVersions(a, b) {
31
- if (a.major !== b.major)
32
- return a.major - b.major;
33
- if (a.minor !== b.minor)
34
- return a.minor - b.minor;
35
- if (a.patch !== b.patch)
36
- return a.patch - b.patch;
37
- if (a.pre === null && b.pre === null)
38
- return 0;
39
- if (a.pre === null)
40
- return 1;
41
- if (b.pre === null)
42
- return -1;
43
- const len = Math.max(a.pre.length, b.pre.length);
44
- for (let i = 0; i < len; i++) {
45
- const ai = a.pre[i];
46
- const bi = b.pre[i];
47
- if (ai === undefined)
48
- return -1;
49
- if (bi === undefined)
50
- return 1;
51
- const an = /^\d+$/.test(ai);
52
- const bn = /^\d+$/.test(bi);
53
- if (an && bn) {
54
- const d = Number(ai) - Number(bi);
55
- if (d !== 0)
56
- return d;
57
- }
58
- else if (an !== bn) {
59
- return an ? -1 : 1;
60
- }
61
- else if (ai !== bi) {
62
- return ai < bi ? -1 : 1;
63
- }
64
- }
65
- return 0;
66
- }
67
5
  /** The location identity of an import ref: the ref with its version stripped.
68
6
  * Two refs share an identity when they address the same module at different
69
7
  * versions, whatever transport owns them:
@@ -115,7 +53,7 @@ function moduleIdentityOf(mod, identity) {
115
53
  source: mod.owner.source,
116
54
  identity,
117
55
  version,
118
- parsed: parseVersion(version),
56
+ parsed: parseModuleVersion(version),
119
57
  text: mod.owner.text,
120
58
  };
121
59
  }
@@ -135,7 +73,7 @@ function resolveGroup(members) {
135
73
  return best;
136
74
  if (!best.parsed)
137
75
  return cur;
138
- const cmp = compareVersions(cur.parsed, best.parsed);
76
+ const cmp = compareParsedModuleVersions(cur.parsed, best.parsed);
139
77
  if (cmp > 0)
140
78
  return cur;
141
79
  if (cmp === 0 && cur.source < best.source)
@@ -0,0 +1,34 @@
1
+ /** A module ref split into the parts an upgrade needs: the version-independent
2
+ * ref, the version segment it currently names, and any inline pin. */
3
+ export interface ParsedVersionedRef {
4
+ /** The ref with its `@version` segment and integrity fragment removed —
5
+ * `std/run`, `oci://ghcr.io/telorun/timer`. This is the identity a version
6
+ * list is keyed by (the hub registers modules under exactly this form). */
7
+ baseRef: string;
8
+ /** The version segment, raw — a registry `@version`, an OCI tag, or an OCI
9
+ * digest reference (`sha256:…`). The caller applies its own SemVer check. */
10
+ version: string;
11
+ /** Telo's inline `sha256-<base64url>` pin, when the ref carried one. */
12
+ integrity?: string;
13
+ }
14
+ /** Split a versioned module ref. Returns `null` when the ref names no
15
+ * upgradeable version — a local path, a bare `https://` URL, or an OCI ref
16
+ * with no explicit reference (an implicit `latest` is not a pin).
17
+ *
18
+ * Browser-safe and transport-neutral: this is the *grammar* half of an
19
+ * upgrade, shared by the kernel transports (whose `refVersion` / `withVersion`
20
+ * delegate here) and the editor, which cannot use a transport at all — the
21
+ * *network* half (enumerating versions) is scheme-specific and stays behind
22
+ * `Transport.listVersions` on Node and the hub's `/module/versions` in the
23
+ * browser. */
24
+ export declare function parseVersionedRef(ref: string): ParsedVersionedRef | null;
25
+ /** `ref` rewritten to name `version`, dropping any integrity fragment (the
26
+ * caller re-pins the result). Appends the version when the ref carries none —
27
+ * an untagged `oci://host/repo` is still a versionable address.
28
+ *
29
+ * Throws when the ref's grammar has no version segment at all — a relative
30
+ * path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
31
+ * a ref nothing can resolve, so this fails where the transport-specific
32
+ * parsers it replaced (`parseOciRef` / `parseModuleRef`) also failed. */
33
+ export declare function withRefVersion(ref: string, version: string): string;
34
+ //# sourceMappingURL=versioned-ref.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versioned-ref.d.ts","sourceRoot":"","sources":["../../src/sources/versioned-ref.ts"],"names":[],"mappings":"AAIA;uEACuE;AACvE,MAAM,WAAW,kBAAkB;IACjC;;gFAE4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB;kFAC8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;eASe;AACf,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAOxE;AAED;;;;;;;0EAO0E;AAC1E,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAUnE"}
@@ -0,0 +1,60 @@
1
+ import { splitIntegrity } from "./integrity.js";
2
+ import { isRegistryRef } from "./module-ref.js";
3
+ import { OCI_SCHEME } from "./oci-ref.js";
4
+ /** Split a versioned module ref. Returns `null` when the ref names no
5
+ * upgradeable version — a local path, a bare `https://` URL, or an OCI ref
6
+ * with no explicit reference (an implicit `latest` is not a pin).
7
+ *
8
+ * Browser-safe and transport-neutral: this is the *grammar* half of an
9
+ * upgrade, shared by the kernel transports (whose `refVersion` / `withVersion`
10
+ * delegate here) and the editor, which cannot use a transport at all — the
11
+ * *network* half (enumerating versions) is scheme-specific and stays behind
12
+ * `Transport.listVersions` on Node and the hub's `/module/versions` in the
13
+ * browser. */
14
+ export function parseVersionedRef(ref) {
15
+ const { base, integrity } = splitIntegrity(ref);
16
+ const at = versionSeparator(base);
17
+ if (at === null)
18
+ return null;
19
+ const version = base.slice(at + 1);
20
+ if (!version)
21
+ return null;
22
+ return { baseRef: base.slice(0, at), version, integrity };
23
+ }
24
+ /** `ref` rewritten to name `version`, dropping any integrity fragment (the
25
+ * caller re-pins the result). Appends the version when the ref carries none —
26
+ * an untagged `oci://host/repo` is still a versionable address.
27
+ *
28
+ * Throws when the ref's grammar has no version segment at all — a relative
29
+ * path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
30
+ * a ref nothing can resolve, so this fails where the transport-specific
31
+ * parsers it replaced (`parseOciRef` / `parseModuleRef`) also failed. */
32
+ export function withRefVersion(ref, version) {
33
+ const { base } = splitIntegrity(ref);
34
+ if (refGrammar(base) === null) {
35
+ throw new Error(`Cannot set a version on '${ref}' — only registry (namespace/name@version) ` +
36
+ `and oci:// refs carry a version segment.`);
37
+ }
38
+ const at = versionSeparator(base);
39
+ return `${at === null ? base : base.slice(0, at)}@${version}`;
40
+ }
41
+ /** Which versionable ref grammar `base` is written in, or `null` when it is
42
+ * neither — a relative/absolute path, a `file:`/`https://` URL. */
43
+ function refGrammar(base) {
44
+ if (base.startsWith(OCI_SCHEME)) {
45
+ // A host alone is not addressable; the repo path is what carries a version.
46
+ return base.indexOf("/", OCI_SCHEME.length) > OCI_SCHEME.length ? "oci" : null;
47
+ }
48
+ // `isRegistryRef` requires the `@`, so a version-less `std/console` is not a
49
+ // registry ref by this test — matching `parseModuleRef`, which throws on it.
50
+ return isRegistryRef(base) ? "registry" : null;
51
+ }
52
+ /** Index of the `@` that separates the version, or `null` when the ref names
53
+ * none. Split on the LAST `@` so a digest reference (`repo@sha256:…`) keeps
54
+ * everything before it as the ref. */
55
+ function versionSeparator(base) {
56
+ if (refGrammar(base) === null)
57
+ return null;
58
+ const at = base.lastIndexOf("@");
59
+ return at > (base.startsWith(OCI_SCHEME) ? OCI_SCHEME.length : 0) ? at : null;
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
package/src/index.ts CHANGED
@@ -60,6 +60,14 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
60
60
  export type { ParseOptions } from "./parse-loaded-file.js";
61
61
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
62
62
  export type { SyntheticImport } from "./inline-imports.js";
63
+ export {
64
+ compareModuleVersions,
65
+ compareParsedModuleVersions,
66
+ isNewerModuleVersion,
67
+ isSameModuleVersion,
68
+ parseModuleVersion,
69
+ } from "./module-version-order.js";
70
+ export type { ParsedModuleVersion } from "./module-version-order.js";
63
71
  export { reconcileModuleVersions } from "./reconcile-module-versions.js";
64
72
  export type { VersionReconciliation } from "./reconcile-module-versions.js";
65
73
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
@@ -86,6 +94,8 @@ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
86
94
  export type { ParsedModuleRef } from "./sources/module-ref.js";
87
95
  export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
88
96
  export type { ParsedOciRef } from "./sources/oci-ref.js";
97
+ export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
98
+ export type { ParsedVersionedRef } from "./sources/versioned-ref.js";
89
99
  export { isLocalPathSource } from "./sources/local-path-ref.js";
90
100
  export {
91
101
  MANIFEST_CACHE_BASE_URL,
@@ -0,0 +1,91 @@
1
+ /** SemVer precedence for module versions — the single ordering rule shared by
2
+ * version reconciliation and by any host deciding whether an import is behind.
3
+ *
4
+ * Pure and dependency-free (no `semver` package), so the analyzer stays
5
+ * browser-safe and the editor can reach the same rule the kernel-side analysis
6
+ * uses. A caller that cannot parse a version must not guess: an OCI digest, a
7
+ * moving tag like `latest`, and a malformed pin all come back `null` rather
8
+ * than being ordered by some weaker fallback. */
9
+
10
+ export interface ParsedModuleVersion {
11
+ major: number;
12
+ minor: number;
13
+ patch: number;
14
+ /** Dot-separated prerelease identifiers, or `null` for a release version. */
15
+ pre: string[] | null;
16
+ }
17
+
18
+ /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
19
+ * isn't a plain three-part numeric core — an unparseable version is never
20
+ * silently ordered. */
21
+ export function parseModuleVersion(raw: string | undefined): ParsedModuleVersion | null {
22
+ if (typeof raw !== "string") return null;
23
+ const v = raw.startsWith("v") ? raw.slice(1) : raw;
24
+ const [core, ...preParts] = v.split("-");
25
+ const pre = preParts.length > 0 ? preParts.join("-") : null;
26
+ const segments = core.split(".");
27
+ if (segments.length !== 3) return null;
28
+ const [major, minor, patch] = segments.map((s) => {
29
+ if (!/^\d+$/.test(s)) return NaN;
30
+ return Number(s);
31
+ });
32
+ if ([major, minor, patch].some((n) => Number.isNaN(n))) return null;
33
+ return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
34
+ }
35
+
36
+ /** SemVer precedence: numeric core, then a release outranks a prerelease, then
37
+ * prerelease identifiers compared field-by-field (numeric < non-numeric per
38
+ * spec, shorter set loses when all shared fields are equal). */
39
+ export function compareParsedModuleVersions(
40
+ a: ParsedModuleVersion,
41
+ b: ParsedModuleVersion,
42
+ ): number {
43
+ if (a.major !== b.major) return a.major - b.major;
44
+ if (a.minor !== b.minor) return a.minor - b.minor;
45
+ if (a.patch !== b.patch) return a.patch - b.patch;
46
+ if (a.pre === null && b.pre === null) return 0;
47
+ if (a.pre === null) return 1;
48
+ if (b.pre === null) return -1;
49
+ const len = Math.max(a.pre.length, b.pre.length);
50
+ for (let i = 0; i < len; i++) {
51
+ const ai = a.pre[i];
52
+ const bi = b.pre[i];
53
+ if (ai === undefined) return -1;
54
+ if (bi === undefined) return 1;
55
+ const an = /^\d+$/.test(ai);
56
+ const bn = /^\d+$/.test(bi);
57
+ if (an && bn) {
58
+ const d = Number(ai) - Number(bi);
59
+ if (d !== 0) return d;
60
+ } else if (an !== bn) {
61
+ return an ? -1 : 1;
62
+ } else if (ai !== bi) {
63
+ return ai < bi ? -1 : 1;
64
+ }
65
+ }
66
+ return 0;
67
+ }
68
+
69
+ /** Negative / zero / positive when both versions parse, `null` when either does
70
+ * not. The string-in convenience over {@link compareParsedModuleVersions}. */
71
+ export function compareModuleVersions(a: string, b: string): number | null {
72
+ const left = parseModuleVersion(a);
73
+ const right = parseModuleVersion(b);
74
+ if (!left || !right) return null;
75
+ return compareParsedModuleVersions(left, right);
76
+ }
77
+
78
+ /** True when `candidate` is strictly newer than `current` — the test for
79
+ * whether an import is behind. False when they are equal, when `current` is
80
+ * ahead (a version index can lag the module's own origin, and "upgrading" to
81
+ * what it knows would be a downgrade), or when either side is unparseable. */
82
+ export function isNewerModuleVersion(candidate: string, current: string): boolean {
83
+ return (compareModuleVersions(candidate, current) ?? 0) > 0;
84
+ }
85
+
86
+ /** True when two tags name the same version, tolerating a `v` prefix on either
87
+ * side. Falls back to exact equality for unparseable tags, so a digest still
88
+ * matches itself. */
89
+ export function isSameModuleVersion(a: string, b: string): boolean {
90
+ return a === b || compareModuleVersions(a, b) === 0;
91
+ }
@@ -1,5 +1,10 @@
1
1
  import type { ImportEdge, LoadedModule } from "./loaded-types.js";
2
2
  import { isModuleKind } from "./module-kinds.js";
3
+ import {
4
+ compareParsedModuleVersions,
5
+ parseModuleVersion,
6
+ type ParsedModuleVersion,
7
+ } from "./module-version-order.js";
3
8
  import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
4
9
 
5
10
  const SOURCE = "telo-analyzer";
@@ -17,72 +22,17 @@ export interface VersionReconciliation {
17
22
  diagnostics: AnalysisDiagnostic[];
18
23
  }
19
24
 
20
- interface ParsedVersion {
21
- major: number;
22
- minor: number;
23
- patch: number;
24
- /** Dot-separated prerelease identifiers, or `null` for a release version. */
25
- pre: string[] | null;
26
- }
27
-
28
25
  interface ModuleIdentity {
29
26
  source: string;
30
27
  identity: string;
31
28
  version: string;
32
- parsed: ParsedVersion | null;
29
+ /** `null` for an unparseable version, which forces the group onto the
30
+ * conflict path — we never silently hoist across a version we can't reason
31
+ * about. */
32
+ parsed: ParsedModuleVersion | null;
33
33
  text: string;
34
34
  }
35
35
 
36
- /** Parse `X.Y.Z`, `vX.Y.Z`, or `X.Y.Z-pre.1`. Returns `null` for anything that
37
- * isn't a plain three-part numeric core — an unparseable version forces the
38
- * group onto the conflict path (we never silently hoist across a version we
39
- * can't reason about). Pure: no dependency on the `semver` package, so the
40
- * analyzer stays browser-safe and dependency-free. */
41
- function parseVersion(raw: string | undefined): ParsedVersion | null {
42
- if (typeof raw !== "string") return null;
43
- const v = raw.startsWith("v") ? raw.slice(1) : raw;
44
- const [core, ...preParts] = v.split("-");
45
- const pre = preParts.length > 0 ? preParts.join("-") : null;
46
- const segments = core.split(".");
47
- if (segments.length !== 3) return null;
48
- const [major, minor, patch] = segments.map((s) => {
49
- if (!/^\d+$/.test(s)) return NaN;
50
- return Number(s);
51
- });
52
- if ([major, minor, patch].some((n) => Number.isNaN(n))) return null;
53
- return { major, minor, patch, pre: pre === null ? null : pre.split(".") };
54
- }
55
-
56
- /** SemVer precedence: numeric core, then a release outranks a prerelease, then
57
- * prerelease identifiers compared field-by-field (numeric < non-numeric per
58
- * spec, shorter set loses when all shared fields are equal). */
59
- function compareVersions(a: ParsedVersion, b: ParsedVersion): number {
60
- if (a.major !== b.major) return a.major - b.major;
61
- if (a.minor !== b.minor) return a.minor - b.minor;
62
- if (a.patch !== b.patch) return a.patch - b.patch;
63
- if (a.pre === null && b.pre === null) return 0;
64
- if (a.pre === null) return 1;
65
- if (b.pre === null) return -1;
66
- const len = Math.max(a.pre.length, b.pre.length);
67
- for (let i = 0; i < len; i++) {
68
- const ai = a.pre[i];
69
- const bi = b.pre[i];
70
- if (ai === undefined) return -1;
71
- if (bi === undefined) return 1;
72
- const an = /^\d+$/.test(ai);
73
- const bn = /^\d+$/.test(bi);
74
- if (an && bn) {
75
- const d = Number(ai) - Number(bi);
76
- if (d !== 0) return d;
77
- } else if (an !== bn) {
78
- return an ? -1 : 1;
79
- } else if (ai !== bi) {
80
- return ai < bi ? -1 : 1;
81
- }
82
- }
83
- return 0;
84
- }
85
-
86
36
  /** The location identity of an import ref: the ref with its version stripped.
87
37
  * Two refs share an identity when they address the same module at different
88
38
  * versions, whatever transport owns them:
@@ -133,7 +83,7 @@ function moduleIdentityOf(mod: LoadedModule, identity: string): ModuleIdentity |
133
83
  source: mod.owner.source,
134
84
  identity,
135
85
  version,
136
- parsed: parseVersion(version),
86
+ parsed: parseModuleVersion(version),
137
87
  text: mod.owner.text,
138
88
  };
139
89
  }
@@ -158,7 +108,7 @@ function resolveGroup(members: ModuleIdentity[]): GroupResolution {
158
108
  const winner = members.reduce((best, cur) => {
159
109
  if (!cur.parsed) return best;
160
110
  if (!best.parsed) return cur;
161
- const cmp = compareVersions(cur.parsed, best.parsed);
111
+ const cmp = compareParsedModuleVersions(cur.parsed, best.parsed);
162
112
  if (cmp > 0) return cur;
163
113
  if (cmp === 0 && cur.source < best.source) return cur;
164
114
  return best;
@@ -0,0 +1,77 @@
1
+ import { splitIntegrity } from "./integrity.js";
2
+ import { isRegistryRef } from "./module-ref.js";
3
+ import { OCI_SCHEME } from "./oci-ref.js";
4
+
5
+ /** A module ref split into the parts an upgrade needs: the version-independent
6
+ * ref, the version segment it currently names, and any inline pin. */
7
+ export interface ParsedVersionedRef {
8
+ /** The ref with its `@version` segment and integrity fragment removed —
9
+ * `std/run`, `oci://ghcr.io/telorun/timer`. This is the identity a version
10
+ * list is keyed by (the hub registers modules under exactly this form). */
11
+ baseRef: string;
12
+ /** The version segment, raw — a registry `@version`, an OCI tag, or an OCI
13
+ * digest reference (`sha256:…`). The caller applies its own SemVer check. */
14
+ version: string;
15
+ /** Telo's inline `sha256-<base64url>` pin, when the ref carried one. */
16
+ integrity?: string;
17
+ }
18
+
19
+ /** Split a versioned module ref. Returns `null` when the ref names no
20
+ * upgradeable version — a local path, a bare `https://` URL, or an OCI ref
21
+ * with no explicit reference (an implicit `latest` is not a pin).
22
+ *
23
+ * Browser-safe and transport-neutral: this is the *grammar* half of an
24
+ * upgrade, shared by the kernel transports (whose `refVersion` / `withVersion`
25
+ * delegate here) and the editor, which cannot use a transport at all — the
26
+ * *network* half (enumerating versions) is scheme-specific and stays behind
27
+ * `Transport.listVersions` on Node and the hub's `/module/versions` in the
28
+ * browser. */
29
+ export function parseVersionedRef(ref: string): ParsedVersionedRef | null {
30
+ const { base, integrity } = splitIntegrity(ref);
31
+ const at = versionSeparator(base);
32
+ if (at === null) return null;
33
+ const version = base.slice(at + 1);
34
+ if (!version) return null;
35
+ return { baseRef: base.slice(0, at), version, integrity };
36
+ }
37
+
38
+ /** `ref` rewritten to name `version`, dropping any integrity fragment (the
39
+ * caller re-pins the result). Appends the version when the ref carries none —
40
+ * an untagged `oci://host/repo` is still a versionable address.
41
+ *
42
+ * Throws when the ref's grammar has no version segment at all — a relative
43
+ * path, a bare `https://` URL. Producing `../lib@0.4.0` for those would write
44
+ * a ref nothing can resolve, so this fails where the transport-specific
45
+ * parsers it replaced (`parseOciRef` / `parseModuleRef`) also failed. */
46
+ export function withRefVersion(ref: string, version: string): string {
47
+ const { base } = splitIntegrity(ref);
48
+ if (refGrammar(base) === null) {
49
+ throw new Error(
50
+ `Cannot set a version on '${ref}' — only registry (namespace/name@version) ` +
51
+ `and oci:// refs carry a version segment.`,
52
+ );
53
+ }
54
+ const at = versionSeparator(base);
55
+ return `${at === null ? base : base.slice(0, at)}@${version}`;
56
+ }
57
+
58
+ /** Which versionable ref grammar `base` is written in, or `null` when it is
59
+ * neither — a relative/absolute path, a `file:`/`https://` URL. */
60
+ function refGrammar(base: string): "oci" | "registry" | null {
61
+ if (base.startsWith(OCI_SCHEME)) {
62
+ // A host alone is not addressable; the repo path is what carries a version.
63
+ return base.indexOf("/", OCI_SCHEME.length) > OCI_SCHEME.length ? "oci" : null;
64
+ }
65
+ // `isRegistryRef` requires the `@`, so a version-less `std/console` is not a
66
+ // registry ref by this test — matching `parseModuleRef`, which throws on it.
67
+ return isRegistryRef(base) ? "registry" : null;
68
+ }
69
+
70
+ /** Index of the `@` that separates the version, or `null` when the ref names
71
+ * none. Split on the LAST `@` so a digest reference (`repo@sha256:…`) keeps
72
+ * everything before it as the ref. */
73
+ function versionSeparator(base: string): number | null {
74
+ if (refGrammar(base) === null) return null;
75
+ const at = base.lastIndexOf("@");
76
+ return at > (base.startsWith(OCI_SCHEME) ? OCI_SCHEME.length : 0) ? at : null;
77
+ }