@telorun/analyzer 0.44.0 → 0.46.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 (38) hide show
  1. package/dist/alias-resolver.d.ts +45 -0
  2. package/dist/alias-resolver.d.ts.map +1 -1
  3. package/dist/alias-resolver.js +33 -0
  4. package/dist/analyzer.d.ts.map +1 -1
  5. package/dist/analyzer.js +104 -3
  6. package/dist/builtins.d.ts.map +1 -1
  7. package/dist/builtins.js +3 -0
  8. package/dist/extends-resolution.d.ts +19 -2
  9. package/dist/extends-resolution.d.ts.map +1 -1
  10. package/dist/extends-resolution.js +25 -1
  11. package/dist/index.d.ts +9 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +5 -1
  14. package/dist/kernel-globals.d.ts +9 -1
  15. package/dist/kernel-globals.d.ts.map +1 -1
  16. package/dist/kernel-globals.js +24 -1
  17. package/dist/module-version-order.d.ts +36 -0
  18. package/dist/module-version-order.d.ts.map +1 -0
  19. package/dist/module-version-order.js +91 -0
  20. package/dist/reconcile-module-versions.d.ts.map +1 -1
  21. package/dist/reconcile-module-versions.js +3 -65
  22. package/dist/sources/versioned-ref.d.ts +34 -0
  23. package/dist/sources/versioned-ref.d.ts.map +1 -0
  24. package/dist/sources/versioned-ref.js +60 -0
  25. package/dist/validate-observed-state.d.ts +98 -0
  26. package/dist/validate-observed-state.d.ts.map +1 -0
  27. package/dist/validate-observed-state.js +304 -0
  28. package/package.json +2 -2
  29. package/src/alias-resolver.ts +58 -0
  30. package/src/analyzer.ts +118 -2
  31. package/src/builtins.ts +3 -0
  32. package/src/extends-resolution.ts +37 -3
  33. package/src/index.ts +23 -0
  34. package/src/kernel-globals.ts +20 -0
  35. package/src/module-version-order.ts +91 -0
  36. package/src/reconcile-module-versions.ts +11 -61
  37. package/src/sources/versioned-ref.ts +77 -0
  38. package/src/validate-observed-state.ts +354 -0
package/src/index.ts CHANGED
@@ -24,10 +24,23 @@ export {
24
24
  type ReExportSpec,
25
25
  } from "./flatten-for-analyzer.js";
26
26
  export { buildEvalPaths, evalPathCovers } from "./eval-paths.js";
27
+ export {
28
+ applyObservedStateNode,
29
+ buildObservedStateIndex,
30
+ buildObservedStateResourcesSchema,
31
+ collectRunReachableNames,
32
+ observedStateRead,
33
+ validateObservedStateDeclarations,
34
+ OBSERVED_STATE_SCHEMA,
35
+ } from "./validate-observed-state.js";
36
+ export type { AnalyzedResource, ObservedStateRead } from "./validate-observed-state.js";
37
+ export { moduleScopedDefResolver, scopeResolverForModule } from "./alias-resolver.js";
38
+ export type { ModuleScopes } from "./alias-resolver.js";
27
39
  export {
28
40
  ancestorChain,
29
41
  controllerBearingAncestor,
30
42
  effectiveAuthorSchema,
43
+ effectiveStatusSchema,
31
44
  hasOwnControllerOrTemplate,
32
45
  inheritedCapability,
33
46
  isInheritedDelegation,
@@ -60,6 +73,14 @@ export { parseLoadedFile } from "./parse-loaded-file.js";
60
73
  export type { ParseOptions } from "./parse-loaded-file.js";
61
74
  export { desugarLoadedFile, inlineImportManifests } from "./inline-imports.js";
62
75
  export type { SyntheticImport } from "./inline-imports.js";
76
+ export {
77
+ compareModuleVersions,
78
+ compareParsedModuleVersions,
79
+ isNewerModuleVersion,
80
+ isSameModuleVersion,
81
+ parseModuleVersion,
82
+ } from "./module-version-order.js";
83
+ export type { ParsedModuleVersion } from "./module-version-order.js";
63
84
  export { reconcileModuleVersions } from "./reconcile-module-versions.js";
64
85
  export type { VersionReconciliation } from "./reconcile-module-versions.js";
65
86
  export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
@@ -86,6 +107,8 @@ export { parseModuleRef, isRegistryRef } from "./sources/module-ref.js";
86
107
  export type { ParsedModuleRef } from "./sources/module-ref.js";
87
108
  export { OCI_SCHEME, isOciRef, parseOciRef } from "./sources/oci-ref.js";
88
109
  export type { ParsedOciRef } from "./sources/oci-ref.js";
110
+ export { parseVersionedRef, withRefVersion } from "./sources/versioned-ref.js";
111
+ export type { ParsedVersionedRef } from "./sources/versioned-ref.js";
89
112
  export { isLocalPathSource } from "./sources/local-path-ref.js";
90
113
  export {
91
114
  MANIFEST_CACHE_BASE_URL,
@@ -1,5 +1,6 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
2
  import { residualEntrySchemaMap } from "./residual-schema.js";
3
+ import { applyObservedStateNode } from "./validate-observed-state.js";
3
4
 
4
5
  /**
5
6
  * Kernel global names available in every CEL evaluation context at runtime.
@@ -36,6 +37,11 @@ const SYSTEM_KINDS = new Set([
36
37
  */
37
38
  export function buildKernelGlobalsSchema(
38
39
  manifests: ResourceManifest[],
40
+ /** Every resource a CEL read can name, including scope-declared ones (see
41
+ * `buildObservedStateIndex`). Kinds that declare a `status:` get a typed,
42
+ * closed `status` node; every other resource node stays open, so no flat read
43
+ * that passes today can start failing. */
44
+ resources?: ReadonlyMap<string, { kind: string; status?: Record<string, any> }>,
39
45
  ): Record<string, any> {
40
46
  const moduleManifest =
41
47
  (manifests.find((m) => m.kind === "Telo.Application") as
@@ -55,6 +61,20 @@ export function buildKernelGlobalsSchema(
55
61
  resourceProps[name] = { type: "object", additionalProperties: true };
56
62
  }
57
63
  }
64
+ // Scope-declared resources (a `Run.Sequence`'s `with:`) publish like any other
65
+ // now, so their names resolve too — inside the scope's regions, which is where
66
+ // the only expressions that can name them live.
67
+ for (const [key, entry] of resources ?? []) {
68
+ if (key.includes(".")) continue;
69
+ resourceProps[key] ??= { type: "object", additionalProperties: true };
70
+ if (entry.status) applyObservedStateNode(resourceProps, key, entry.status);
71
+ }
72
+ // Imports' exported instances publish two levels deep (`resources.<Alias>.<name>`);
73
+ // the alias node stays open so its other keys keep resolving.
74
+ for (const [key, entry] of resources ?? []) {
75
+ if (!key.includes(".") || !entry.status) continue;
76
+ applyObservedStateNode(resourceProps, key, entry.status);
77
+ }
58
78
 
59
79
  return {
60
80
  type: "object",
@@ -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
+ }