@telorun/analyzer 0.43.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/analysis-registry.d.ts +4 -4
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +3 -3
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +84 -1
- package/dist/builtins.js +7 -7
- package/dist/definition-registry.d.ts +49 -22
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +67 -61
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/loaded-types.d.ts +13 -6
- package/dist/loaded-types.d.ts.map +1 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +1 -0
- package/dist/module-version-order.d.ts +36 -0
- package/dist/module-version-order.d.ts.map +1 -0
- package/dist/module-version-order.js +91 -0
- package/dist/reconcile-module-versions.d.ts +2 -2
- package/dist/reconcile-module-versions.d.ts.map +1 -1
- package/dist/reconcile-module-versions.js +69 -79
- package/dist/reference-field-map.d.ts +2 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.d.ts +62 -0
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -0
- package/dist/resolve-schema-ref-kinds.js +81 -0
- package/dist/sources/manifest-cache.d.ts +14 -4
- package/dist/sources/manifest-cache.d.ts.map +1 -1
- package/dist/sources/manifest-cache.js +15 -6
- package/dist/sources/versioned-ref.d.ts +34 -0
- package/dist/sources/versioned-ref.d.ts.map +1 -0
- package/dist/sources/versioned-ref.js +60 -0
- package/package.json +2 -2
- package/src/analysis-registry.ts +4 -4
- package/src/analyzer.ts +85 -2
- package/src/builtins.ts +7 -7
- package/src/definition-registry.ts +67 -66
- package/src/index.ts +10 -0
- package/src/loaded-types.ts +13 -6
- package/src/manifest-loader.ts +1 -0
- package/src/module-version-order.ts +91 -0
- package/src/reconcile-module-versions.ts +77 -75
- package/src/reference-field-map.ts +2 -1
- package/src/resolve-schema-ref-kinds.ts +118 -0
- package/src/sources/manifest-cache.ts +26 -8
- package/src/sources/versioned-ref.ts +77 -0
|
@@ -10,15 +10,24 @@ function invalidSegment(segment) {
|
|
|
10
10
|
return segment === "" || segment === "." || segment === ".." || segment.includes("\\");
|
|
11
11
|
}
|
|
12
12
|
/** Deterministic cache key for one module version:
|
|
13
|
-
* `<transport>/<host>/<path…>/<version
|
|
14
|
-
*
|
|
13
|
+
* `<transport>/<host>/<path…>/<version>/<file>`, where `version` is omitted
|
|
14
|
+
* when the coordinates carry none and `file` defaults to the module manifest.
|
|
15
|
+
* Returns `null` when any coordinate is empty or would traverse out of the key
|
|
16
|
+
* space. */
|
|
15
17
|
export function manifestCacheKey(coords) {
|
|
16
|
-
const { transport, host, path, version } = coords;
|
|
17
|
-
const segments = [transport, host, ...path.split("/")
|
|
18
|
-
if (
|
|
18
|
+
const { transport, host, path, version, file } = coords;
|
|
19
|
+
const segments = [transport, host, ...(path ? path.split("/") : [])];
|
|
20
|
+
if (version !== undefined)
|
|
21
|
+
segments.push(version);
|
|
22
|
+
segments.push(file ?? DEFAULT_MANIFEST_FILENAME);
|
|
23
|
+
if (segments.some(invalidSegment) ||
|
|
24
|
+
transport.includes("/") ||
|
|
25
|
+
host.includes("/") ||
|
|
26
|
+
(version !== undefined && version.includes("/")) ||
|
|
27
|
+
(file !== undefined && file.includes("/"))) {
|
|
19
28
|
return null;
|
|
20
29
|
}
|
|
21
|
-
return
|
|
30
|
+
return segments.join("/");
|
|
22
31
|
}
|
|
23
32
|
/** Cache coordinates for an `oci://host/repo@tag` ref. Returns `null` when the
|
|
24
33
|
* ref carries no explicit tag (a defaulted `latest` or a `sha256:` digest is
|
|
@@ -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.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8",
|
|
51
|
-
"@telorun/sdk": "0.
|
|
51
|
+
"@telorun/sdk": "0.56.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@telorun/sdk": "*"
|
package/src/analysis-registry.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface RefFieldInfo {
|
|
|
17
17
|
path: string;
|
|
18
18
|
/** True when the path traverses at least one array. */
|
|
19
19
|
isArray: boolean;
|
|
20
|
-
/** Accepted `x-telo-ref` constraint strings (e.g. `
|
|
20
|
+
/** Accepted `x-telo-ref` constraint strings, canonicalized (e.g. `Telo.Runnable`). */
|
|
21
21
|
refs: string[];
|
|
22
22
|
/** Distinct capabilities the slot may target (`Telo.Runnable`,
|
|
23
23
|
* `Telo.Service`, `Telo.Provider`, …) — one per resolvable constraint. The
|
|
@@ -119,9 +119,9 @@ export class AnalysisRegistry {
|
|
|
119
119
|
|
|
120
120
|
/** Base capability an `x-telo-ref` constraint targets. A definition's declared
|
|
121
121
|
* `capability` is always one of the base capabilities, so it wins — this
|
|
122
|
-
* resolves user-defined abstracts (e.g. `
|
|
122
|
+
* resolves user-defined abstracts (e.g. `ai.Model`, declared
|
|
123
123
|
* `capability: Telo.Invocable`) to the capability instances satisfy, not the
|
|
124
|
-
* abstract kind. Builtin abstracts (`
|
|
124
|
+
* abstract kind. Builtin abstracts (`Telo.Runnable`) carry no `capability`
|
|
125
125
|
* field — there the kind itself *is* the capability. Undefined when
|
|
126
126
|
* unresolvable. */
|
|
127
127
|
capabilityForRef(xTeloRef: string): string | undefined {
|
|
@@ -306,7 +306,7 @@ export class AnalysisRegistry {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
/** Returns every user-facing (alias-form) kind that satisfies the given
|
|
309
|
-
* `x-telo-ref` constraint string (e.g. `"
|
|
309
|
+
* `x-telo-ref` constraint string, canonicalized (e.g. `"Telo.Invocable"`, `"sql.Connection"`).
|
|
310
310
|
* Resolution mirrors `validateReferences.checkKind`: abstract targets expand to
|
|
311
311
|
* the set of definitions extending them; concrete targets yield just themselves.
|
|
312
312
|
* Returns `undefined` when the ref can't be resolved (e.g. unregistered identity),
|
package/src/analyzer.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { isModuleKind } from "./module-kinds.js";
|
|
|
20
20
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
21
21
|
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
22
22
|
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
23
|
+
import { resolveSchemaRefKinds, type RefConstraintIssue } from "./resolve-schema-ref-kinds.js";
|
|
23
24
|
import { resolveSchemaTypeRefs } from "./resolve-schema-type-refs.js";
|
|
24
25
|
import { validateSchemaTypeRefs } from "./validate-schema-type-refs.js";
|
|
25
26
|
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
@@ -1035,6 +1036,7 @@ export class StaticAnalyzer {
|
|
|
1035
1036
|
// declaring scope's resolver, so `extendedBy` is keyed by canonical kind regardless
|
|
1036
1037
|
// of alias choices. `capability` covers the legacy implements-this-abstract overload;
|
|
1037
1038
|
// `extends` is the canonical first-class form.
|
|
1039
|
+
const refConstraintIssues: RefConstraintIssue[] = [];
|
|
1038
1040
|
for (const m of manifests) {
|
|
1039
1041
|
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
1040
1042
|
const def = m as unknown as ResourceDefinition;
|
|
@@ -1043,6 +1045,15 @@ export class StaticAnalyzer {
|
|
|
1043
1045
|
ownModule && !rootModules.has(ownModule)
|
|
1044
1046
|
? (aliasesByModule.get(ownModule) ?? new AliasResolver())
|
|
1045
1047
|
: aliases;
|
|
1048
|
+
// Canonicalize alias-form `x-telo-ref` constraints in the DECLARING module's
|
|
1049
|
+
// scope, before the schema reaches `register()` and the lazily-built field
|
|
1050
|
+
// maps. Same pre-resolution `capability` / `extends` get below.
|
|
1051
|
+
const issues = resolveSchemaRefKinds(m, scopeResolver);
|
|
1052
|
+
// Report only for definitions the author can edit. A published dependency
|
|
1053
|
+
// still on the deprecated form — or with a constraint that no longer
|
|
1054
|
+
// resolves — is not the consumer's to fix, and every import would
|
|
1055
|
+
// otherwise flood `telo check` with unactionable noise.
|
|
1056
|
+
if (!ownModule || rootModules.has(ownModule)) refConstraintIssues.push(...issues);
|
|
1046
1057
|
const resolvedCapability = def.capability
|
|
1047
1058
|
? (scopeResolver.resolveKind(def.capability) ?? def.capability)
|
|
1048
1059
|
: def.capability;
|
|
@@ -1063,6 +1074,57 @@ export class StaticAnalyzer {
|
|
|
1063
1074
|
// distinguishable from the resolver's own substitution (after Phase 2/2.5
|
|
1064
1075
|
// they are the same object).
|
|
1065
1076
|
if (!options?.skipValidation) {
|
|
1077
|
+
for (const issue of refConstraintIssues) {
|
|
1078
|
+
// An `unknown` prefix is also what an ALREADY-CANONICAL value looks like
|
|
1079
|
+
// (`kv-store.Store` names a module, not an alias). Now that every kind is
|
|
1080
|
+
// registered, the registry separates the two — anything it resolves was
|
|
1081
|
+
// canonical, anything it doesn't names nothing at all.
|
|
1082
|
+
if (issue.reason === "unknown" && defs.resolve(issue.ref)) continue;
|
|
1083
|
+
const resource = {
|
|
1084
|
+
kind: issue.manifest.kind,
|
|
1085
|
+
name: issue.manifest.metadata?.name as string,
|
|
1086
|
+
};
|
|
1087
|
+
const filePath = (issue.manifest.metadata as { source?: string } | undefined)?.source;
|
|
1088
|
+
const data = { resource, filePath, path: issue.path };
|
|
1089
|
+
if (issue.reason === "legacy") {
|
|
1090
|
+
diagnostics.push({
|
|
1091
|
+
severity: DiagnosticSeverity.Warning,
|
|
1092
|
+
code: "X_TELO_REF_LEGACY_IDENTITY",
|
|
1093
|
+
source: SOURCE,
|
|
1094
|
+
message:
|
|
1095
|
+
`x-telo-ref '${issue.ref}' at '${issue.path}' uses the deprecated ` +
|
|
1096
|
+
`'<namespace>/<module>#<Kind>' form. Write the target as an alias-qualified kind ` +
|
|
1097
|
+
`instead — '<Alias>.<Kind>' for a module declared in this file's 'imports:' map, ` +
|
|
1098
|
+
`'Self.<Kind>' for a kind in this library, or 'Telo.<Kind>' for a built-in capability.`,
|
|
1099
|
+
data,
|
|
1100
|
+
});
|
|
1101
|
+
} else if (issue.reason === "gated") {
|
|
1102
|
+
diagnostics.push({
|
|
1103
|
+
severity: DiagnosticSeverity.Error,
|
|
1104
|
+
code: "KIND_NOT_EXPORTED",
|
|
1105
|
+
source: SOURCE,
|
|
1106
|
+
message:
|
|
1107
|
+
`x-telo-ref '${issue.ref}' at '${issue.path}' targets a kind module ` +
|
|
1108
|
+
`'${issue.gate?.module}' does not export. Add ` +
|
|
1109
|
+
`'${issue.ref.slice(issue.ref.indexOf(".") + 1)}' to that module's exports.kinds. ` +
|
|
1110
|
+
`Exported kinds: ${issue.gate?.exported.join(", ") || "(none)"}.`,
|
|
1111
|
+
data,
|
|
1112
|
+
});
|
|
1113
|
+
} else {
|
|
1114
|
+
diagnostics.push({
|
|
1115
|
+
severity: DiagnosticSeverity.Error,
|
|
1116
|
+
code: "X_TELO_REF_UNRESOLVED",
|
|
1117
|
+
source: SOURCE,
|
|
1118
|
+
message:
|
|
1119
|
+
`x-telo-ref '${issue.ref}' at '${issue.path}' names no kind. The prefix must be an ` +
|
|
1120
|
+
`import alias declared in this file's 'imports:' map, 'Self' for a kind in this ` +
|
|
1121
|
+
`library, or 'Telo' for a built-in capability. An unresolvable constraint would ` +
|
|
1122
|
+
`leave the slot accepting any resource. Known aliases: ` +
|
|
1123
|
+
`${issue.knownAliases?.join(", ") || "(none)"}.`,
|
|
1124
|
+
data,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1066
1128
|
diagnostics.push(...validateReferenceForms(manifests, defs, aliases, aliasesByModule));
|
|
1067
1129
|
}
|
|
1068
1130
|
|
|
@@ -1089,10 +1151,31 @@ export class StaticAnalyzer {
|
|
|
1089
1151
|
rootModules.has(ownModule) ? aliases : (aliasesByModule.get(ownModule) ?? new AliasResolver());
|
|
1090
1152
|
const canonicalKind = scopeResolver.resolveKind(m.kind as string) ?? (m.kind as string);
|
|
1091
1153
|
if (defs.resolve(canonicalKind)?.capability !== "Telo.Type") continue;
|
|
1092
|
-
|
|
1093
|
-
|
|
1154
|
+
const typeName = m.metadata.name as string;
|
|
1155
|
+
const registered = defs.registerNamedTypeSchema(
|
|
1156
|
+
canonicalTypeSchemaId(ownModule, typeName),
|
|
1094
1157
|
m.schema as Record<string, any>,
|
|
1095
1158
|
);
|
|
1159
|
+
// Kinds and named types share one `telo://<module>/<Name>` id space. A
|
|
1160
|
+
// collision would leave every `$ref` to that id resolving to the kind's
|
|
1161
|
+
// schema — validating the wrong shape, silently — so it is an error, not
|
|
1162
|
+
// a last-writer-wins.
|
|
1163
|
+
if (!registered && !options?.skipValidation) {
|
|
1164
|
+
diagnostics.push({
|
|
1165
|
+
severity: DiagnosticSeverity.Error,
|
|
1166
|
+
code: "DUPLICATE_SCHEMA_ID",
|
|
1167
|
+
source: SOURCE,
|
|
1168
|
+
message:
|
|
1169
|
+
`Type '${typeName}' collides with the kind '${ownModule}.${typeName}': both claim the ` +
|
|
1170
|
+
`schema id '${canonicalTypeSchemaId(ownModule, typeName)}'. A '$ref' to it would ` +
|
|
1171
|
+
`resolve to the kind's schema. Rename one of them.`,
|
|
1172
|
+
data: {
|
|
1173
|
+
resource: { kind: m.kind, name: typeName },
|
|
1174
|
+
filePath: (m.metadata as { source?: string } | undefined)?.source,
|
|
1175
|
+
path: "metadata.name",
|
|
1176
|
+
},
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1096
1179
|
}
|
|
1097
1180
|
if (!options?.skipValidation) {
|
|
1098
1181
|
diagnostics.push(
|
package/src/builtins.ts
CHANGED
|
@@ -97,7 +97,7 @@ const ROOT_LOGGING_SCHEMA = {
|
|
|
97
97
|
type: "array",
|
|
98
98
|
items: {
|
|
99
99
|
type: "object",
|
|
100
|
-
"x-telo-ref": "
|
|
100
|
+
"x-telo-ref": "Telo.LogSink",
|
|
101
101
|
"x-telo-inline": true,
|
|
102
102
|
},
|
|
103
103
|
},
|
|
@@ -444,8 +444,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
444
444
|
type: "array",
|
|
445
445
|
items: {
|
|
446
446
|
anyOf: [
|
|
447
|
-
{ type: "string", "x-telo-ref": "
|
|
448
|
-
{ type: "string", "x-telo-ref": "
|
|
447
|
+
{ type: "string", "x-telo-ref": "Telo.Runnable" },
|
|
448
|
+
{ type: "string", "x-telo-ref": "Telo.Service" },
|
|
449
449
|
// Post-resolution shape that `resolveRefSentinels`
|
|
450
450
|
// substitutes a `!ref <name>` sentinel into. The
|
|
451
451
|
// adjacent `x-telo-ref` constraints govern the kind
|
|
@@ -469,8 +469,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
469
469
|
properties: {
|
|
470
470
|
ref: {
|
|
471
471
|
anyOf: [
|
|
472
|
-
{ type: "string", "x-telo-ref": "
|
|
473
|
-
{ type: "string", "x-telo-ref": "
|
|
472
|
+
{ type: "string", "x-telo-ref": "Telo.Runnable" },
|
|
473
|
+
{ type: "string", "x-telo-ref": "Telo.Service" },
|
|
474
474
|
{
|
|
475
475
|
type: "object",
|
|
476
476
|
required: ["kind", "name"],
|
|
@@ -510,8 +510,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
510
510
|
},
|
|
511
511
|
additionalProperties: true,
|
|
512
512
|
anyOf: [
|
|
513
|
-
{ "x-telo-ref": "
|
|
514
|
-
{ "x-telo-ref": "
|
|
513
|
+
{ "x-telo-ref": "Telo.Invocable" },
|
|
514
|
+
{ "x-telo-ref": "Telo.Runnable" },
|
|
515
515
|
],
|
|
516
516
|
},
|
|
517
517
|
inputs: { type: "object", additionalProperties: true },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { canonicalTypeSchemaId } from "@telorun/sdk";
|
|
2
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
4
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
4
5
|
import {
|
|
@@ -21,18 +22,21 @@ export class DefinitionRegistry {
|
|
|
21
22
|
* across analyze() calls and no unbounded growth across the process lifetime. */
|
|
22
23
|
private readonly ajv = createAjv();
|
|
23
24
|
private readonly registeredSchemaIds = new Set<string>();
|
|
25
|
+
/** The subset of `registeredSchemaIds` claimed by a kind's schema. Kinds and
|
|
26
|
+
* named `Telo.Type`s share one `telo://<module>/<Name>` id space, so this is
|
|
27
|
+
* what lets a colliding type name be reported instead of silently dropped. */
|
|
28
|
+
private readonly definitionSchemaIds = new Set<string>();
|
|
24
29
|
|
|
25
30
|
private readonly defs = new Map<string, ResourceDefinition>();
|
|
26
31
|
private readonly fieldMaps = new Map<string, ReferenceFieldMap>();
|
|
27
32
|
/** Reverse inheritance index: parent kind → direct child kinds. */
|
|
28
33
|
private readonly extendedBy = new Map<string, string[]>();
|
|
29
|
-
/**
|
|
30
|
-
* "
|
|
34
|
+
/** DEPRECATED module identity table: identity string → canonical module name
|
|
35
|
+
* ("std/pipeline" → "pipeline"). Serves only the legacy
|
|
36
|
+
* `<namespace>/<module>#<Kind>` form of `x-telo-ref`, kept resolvable for
|
|
37
|
+
* module versions published before constraints named their target by import
|
|
38
|
+
* alias. Fed by `metadata.namespace`, which nothing else reads. */
|
|
31
39
|
private readonly identityMap = new Map<string, string>();
|
|
32
|
-
/** Reverse identity table: canonical module name → full identity string.
|
|
33
|
-
* "Telo" → "telo", "pipeline" → "std/pipeline", etc.
|
|
34
|
-
* Used to compute definition $id values for the AJV schema store. */
|
|
35
|
-
private readonly reverseIdentityMap = new Map<string, string>();
|
|
36
40
|
|
|
37
41
|
register(definition: ResourceDefinition): void {
|
|
38
42
|
const { name, module: mod } = definition.metadata;
|
|
@@ -60,12 +64,11 @@ export class DefinitionRegistry {
|
|
|
60
64
|
if (definition.extends) {
|
|
61
65
|
this.addExtendedBy(definition.extends, key);
|
|
62
66
|
}
|
|
63
|
-
// Auto-register the telo identity when any Telo built-in is registered
|
|
67
|
+
// Auto-register the legacy telo identity when any Telo built-in is registered,
|
|
68
|
+
// so an already-published `x-telo-ref: "telo#Invocable"` still resolves.
|
|
64
69
|
if (definition.kind === "Telo.Abstract" && mod === "Telo") {
|
|
65
70
|
this.identityMap.set("telo", "Telo");
|
|
66
|
-
this.reverseIdentityMap.set("Telo", "telo");
|
|
67
71
|
}
|
|
68
|
-
// If identity is already known, register the schema in AJV immediately.
|
|
69
72
|
if (mod && definition.schema) {
|
|
70
73
|
this.tryRegisterSchema(mod, name as string, definition.schema as Record<string, any>);
|
|
71
74
|
}
|
|
@@ -80,50 +83,42 @@ export class DefinitionRegistry {
|
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
/** Register a module identity
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
86
|
+
/** DEPRECATED. Register a module identity so the legacy
|
|
87
|
+
* `<namespace>/<module>#<Kind>` form of `x-telo-ref` still resolves for module
|
|
88
|
+
* versions published before constraints named their target by import alias.
|
|
89
|
+
* New manifests declare no namespace and need no identity — their constraints
|
|
90
|
+
* are canonicalized to `<module>.<Kind>` before registration.
|
|
91
|
+
*
|
|
92
|
+
* The "telo" identity is reserved for the built-in module and is populated
|
|
93
|
+
* automatically when a `Telo.Abstract` registers. A namespace-less module must
|
|
94
|
+
* not claim it: overwriting the entry would repoint every legacy `telo#…`
|
|
95
|
+
* constraint at a module that declares no such kind, and the resulting
|
|
96
|
+
* unresolvable ref reads as partial context rather than an error.
|
|
97
|
+
*
|
|
98
|
+
* @param namespace The module's `metadata.namespace`, or null when it declares none.
|
|
99
|
+
* @param moduleName The module's `metadata.name` (e.g. "pipeline", "http-server"). */
|
|
87
100
|
registerModuleIdentity(namespace: string | null, moduleName: string): void {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// `register` below). A user app / library without a namespace must NOT
|
|
91
|
-
// claim it — silently overwriting the built-in entry breaks every
|
|
92
|
-
// x-telo-ref that resolves through "telo#…". Concretely, the
|
|
93
|
-
// `Http.Api.routes[].handler` slot in the http-server schema carries
|
|
94
|
-
// `x-telo-ref: "telo#Invocable"`. If the entry application is, say,
|
|
95
|
-
// `Telo.Application/HelloApi` (no namespace), this method previously
|
|
96
|
-
// overwrote `"telo" → "Telo"` with `"telo" → "HelloApi"`. The handler's
|
|
97
|
-
// ref then resolved to a nonexistent `HelloApi.Invocable`, the
|
|
98
|
-
// kind-mismatch check inside `validate-references.ts` short-circuited
|
|
99
|
-
// on partial context, and the analyzer reported zero issues for a
|
|
100
|
-
// manifest that explodes at runtime. Skip non-Telo no-namespace modules;
|
|
101
|
-
// they have no x-telo-ref identity to declare anyway.
|
|
102
|
-
if (!namespace && moduleName !== "Telo") return;
|
|
103
|
-
const identity = namespace ? `${namespace}/${moduleName}` : "telo";
|
|
104
|
-
this.identityMap.set(identity, moduleName);
|
|
105
|
-
this.reverseIdentityMap.set(moduleName, identity);
|
|
106
|
-
// Retroactively register AJV schemas for definitions of this module already in the registry.
|
|
107
|
-
for (const def of this.defs.values()) {
|
|
108
|
-
if (def.metadata.module === moduleName && def.schema) {
|
|
109
|
-
this.tryRegisterSchema(
|
|
110
|
-
moduleName,
|
|
111
|
-
def.metadata.name as string,
|
|
112
|
-
def.schema as Record<string, any>,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
101
|
+
if (!namespace || moduleName === "Telo") return;
|
|
102
|
+
this.identityMap.set(`${namespace}/${moduleName}`, moduleName);
|
|
116
103
|
}
|
|
117
104
|
|
|
118
105
|
/** Registers a named `Telo.Type` resource's schema under its canonical
|
|
119
106
|
* module-scoped URI `$id` (`telo://<module>/<name>`), so a sibling schema's
|
|
120
107
|
* `$ref: "telo://Self/<name>"` (rewritten to the canonical form by
|
|
121
108
|
* `resolveSchemaTypeRefs`) resolves during AJV compilation. Mirrors the
|
|
122
|
-
* kernel type controller's `registerSchema(canonicalTypeSchemaId(...))`.
|
|
123
|
-
|
|
124
|
-
|
|
109
|
+
* kernel type controller's `registerSchema(canonicalTypeSchemaId(...))`.
|
|
110
|
+
*
|
|
111
|
+
* Returns `false` when a kind schema in the same module already owns the id —
|
|
112
|
+
* a name collision between a kind and a named type. Definitions register
|
|
113
|
+
* first, so the type is the one that would be dropped, and every
|
|
114
|
+
* `$ref: "telo://<module>/<Name>"` would then silently validate against the
|
|
115
|
+
* kind's schema instead. The caller reports it; nothing is overwritten. */
|
|
116
|
+
registerNamedTypeSchema(id: string, schema: Record<string, any>): boolean {
|
|
117
|
+
if (this.definitionSchemaIds.has(id)) return false;
|
|
118
|
+
if (this.registeredSchemaIds.has(id) || this.ajv.getSchema(id)) return true;
|
|
125
119
|
this.ajv.addSchema(schema, id);
|
|
126
120
|
this.registeredSchemaIds.add(id);
|
|
121
|
+
return true;
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
/** True when a schema is registered under `id` (a canonical `telo://` type id
|
|
@@ -132,14 +127,6 @@ export class DefinitionRegistry {
|
|
|
132
127
|
return this.registeredSchemaIds.has(id) || this.ajv.getSchema(id) !== undefined;
|
|
133
128
|
}
|
|
134
129
|
|
|
135
|
-
/** Computes the $id for a definition schema: "<identity>/<TypeName>".
|
|
136
|
-
* Returns undefined when the module identity is not yet registered. */
|
|
137
|
-
computeId(moduleName: string, typeName: string): string | undefined {
|
|
138
|
-
const identity = this.reverseIdentityMap.get(moduleName);
|
|
139
|
-
if (!identity) return undefined;
|
|
140
|
-
return `${identity}/${typeName}`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
130
|
/** Validates data against a schema using this registry's AJV instance, which has all
|
|
144
131
|
* registered definition schemas loaded — enabling cross-module $ref resolution.
|
|
145
132
|
* A compile failure returns `[]` here; it is surfaced loudly (once, on the
|
|
@@ -172,37 +159,51 @@ export class DefinitionRegistry {
|
|
|
172
159
|
}
|
|
173
160
|
}
|
|
174
161
|
|
|
162
|
+
/** Registers a definition schema under the same module-scoped `telo://` id a
|
|
163
|
+
* named `Telo.Type` uses, so a kind schema and a type schema are addressable
|
|
164
|
+
* the same way and a `$ref` between them resolves at AJV compile time. One id
|
|
165
|
+
* space per module: a kind and a named type may not share a name, which
|
|
166
|
+
* `registerNamedTypeSchema` reports rather than resolving silently. */
|
|
175
167
|
private tryRegisterSchema(
|
|
176
168
|
moduleName: string,
|
|
177
169
|
typeName: string,
|
|
178
170
|
schema: Record<string, any>,
|
|
179
171
|
): void {
|
|
180
|
-
const id =
|
|
181
|
-
if (
|
|
172
|
+
const id = canonicalTypeSchemaId(moduleName, typeName);
|
|
173
|
+
if (this.registeredSchemaIds.has(id)) {
|
|
174
|
+
this.definitionSchemaIds.add(id);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
182
177
|
if (this.ajv.getSchema(id)) {
|
|
183
178
|
throw new Error(`Duplicate definition schema $id: "${id}" is already registered`);
|
|
184
179
|
}
|
|
185
180
|
this.ajv.addSchema(schema, id);
|
|
186
181
|
this.registeredSchemaIds.add(id);
|
|
182
|
+
this.definitionSchemaIds.add(id);
|
|
187
183
|
}
|
|
188
184
|
|
|
189
|
-
/** Resolves an x-telo-ref
|
|
190
|
-
*
|
|
191
|
-
*
|
|
185
|
+
/** Resolves an `x-telo-ref` constraint to a canonical registry kind key.
|
|
186
|
+
*
|
|
187
|
+
* The constraint is already canonical `<module>.<Kind>`: alias-form values
|
|
188
|
+
* (`KvStore.Store`, `Self.Store`, `Telo.Invocable`) are rewritten in the
|
|
189
|
+
* declaring module's scope by `resolveSchemaRefKinds` before registration, so
|
|
190
|
+
* no module context is needed here.
|
|
191
|
+
*
|
|
192
|
+
* The legacy `<namespace>/<module>#<Kind>` form still resolves through the
|
|
193
|
+
* identity table for module versions published before the alias form existed:
|
|
192
194
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* "std/http-server#Server" → "http-server.Server"
|
|
195
|
+
* "telo#Invocable" → "Telo.Invocable"
|
|
196
|
+
* "std/http-server#Server" → "http-server.Server"
|
|
196
197
|
*
|
|
197
|
-
* Returns undefined when
|
|
198
|
+
* Returns undefined when a legacy string is malformed or its identity was
|
|
199
|
+
* never registered. */
|
|
198
200
|
resolveRef(xTeloRef: string): string | undefined {
|
|
199
201
|
const hash = xTeloRef.indexOf("#");
|
|
200
|
-
if (hash === -1
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
const moduleName = this.identityMap.get(identity);
|
|
202
|
+
if (hash === -1) return xTeloRef;
|
|
203
|
+
if (hash === xTeloRef.length - 1) return undefined;
|
|
204
|
+
const moduleName = this.identityMap.get(xTeloRef.slice(0, hash));
|
|
204
205
|
if (!moduleName) return undefined;
|
|
205
|
-
return `${moduleName}.${
|
|
206
|
+
return `${moduleName}.${xTeloRef.slice(hash + 1)}`;
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
resolve(kind: string): ResourceDefinition | undefined {
|
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,
|
package/src/loaded-types.ts
CHANGED
|
@@ -50,18 +50,25 @@ export interface LoadedModule {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/** Resolved Telo.Import edge: where the import points and what library
|
|
53
|
-
* identity it resolves to. Carrying name
|
|
54
|
-
* `flattenForAnalyzer` can stamp `metadata.resolvedModuleName`
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* identity it resolves to. Carrying the name on the edge means
|
|
54
|
+
* `flattenForAnalyzer` can stamp `metadata.resolvedModuleName` from this
|
|
55
|
+
* single source rather than re-deriving the target from manifest metadata,
|
|
56
|
+
* which would silently miss whenever a future projection forgets to stamp
|
|
57
|
+
* `metadata.source` consistently. */
|
|
58
58
|
export interface ImportEdge {
|
|
59
59
|
/** Canonical resolved URL of the target — a key into `modules`. */
|
|
60
60
|
targetSource: string;
|
|
61
|
+
/** The import's `source` exactly as authored — a registry ref, an `oci://`
|
|
62
|
+
* or `https://` ref, or a relative path. Version reconciliation keys on
|
|
63
|
+
* this (minus its version), since it names the module's location
|
|
64
|
+
* independently of what the module declares about itself. */
|
|
65
|
+
targetRef: string;
|
|
61
66
|
/** Target library's `metadata.name`, or `null` when the target had no
|
|
62
67
|
* Telo.Library doc (an error case captured in `LoadedGraph.errors`). */
|
|
63
68
|
targetModuleName: string | null;
|
|
64
|
-
/** Target library's `metadata.namespace
|
|
69
|
+
/** DEPRECATED. Target library's `metadata.namespace`, or `null` when it
|
|
70
|
+
* declares none. Feeds only the legacy `<namespace>/<module>#<Kind>` form
|
|
71
|
+
* of `x-telo-ref`; nothing else reads it. */
|
|
65
72
|
targetNamespace: string | null;
|
|
66
73
|
}
|
|
67
74
|
|