@telorun/analyzer 0.31.0 → 0.33.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +263 -71
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +6 -1
- package/dist/sources/http-source.d.ts.map +1 -1
- package/dist/sources/http-source.js +5 -6
- package/dist/sources/integrity.d.ts +42 -0
- package/dist/sources/integrity.d.ts.map +1 -0
- package/dist/sources/integrity.js +92 -0
- package/dist/sources/module-ref.d.ts +21 -0
- package/dist/sources/module-ref.d.ts.map +1 -0
- package/dist/sources/module-ref.js +36 -0
- package/dist/sources/registry-source.d.ts +0 -1
- package/dist/sources/registry-source.d.ts.map +1 -1
- package/dist/sources/registry-source.js +8 -30
- package/dist/validate-kind-descriptions.d.ts +20 -0
- package/dist/validate-kind-descriptions.d.ts.map +1 -0
- package/dist/validate-kind-descriptions.js +65 -0
- package/package.json +3 -3
- package/src/analyzer.ts +301 -82
- package/src/builtins.ts +13 -0
- package/src/index.ts +10 -0
- package/src/inline-imports.ts +7 -1
- package/src/sources/http-source.ts +5 -8
- package/src/sources/integrity.ts +112 -0
- package/src/sources/module-ref.ts +49 -0
- package/src/sources/registry-source.ts +8 -38
- package/src/validate-kind-descriptions.ts +65 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { splitIntegrity } from "./integrity.js";
|
|
2
|
+
|
|
3
|
+
/** A parsed registry module reference. `modulePath` is `namespace/name`,
|
|
4
|
+
* `version` has any leading `v` stripped, and `integrity` carries the inline
|
|
5
|
+
* `sha256-<base64url>` hash when the ref was pinned. */
|
|
6
|
+
export interface ParsedModuleRef {
|
|
7
|
+
modulePath: string;
|
|
8
|
+
version: string;
|
|
9
|
+
integrity?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** True when `url` has the bare registry-ref shape `namespace/name@version`
|
|
13
|
+
* (no scheme of any kind, no leading `/` or `.`, contains both `@` and `/`).
|
|
14
|
+
* The integrity fragment, if any, does not affect the classification.
|
|
15
|
+
*
|
|
16
|
+
* A registry ref never carries a `scheme://` — that guard is what keeps an
|
|
17
|
+
* `oci://…@ver` (or future `s3://…`) ref from being misrouted here, so a
|
|
18
|
+
* scheme-owning transport claims it instead. */
|
|
19
|
+
export function isRegistryRef(url: string): boolean {
|
|
20
|
+
const { base } = splitIntegrity(url);
|
|
21
|
+
return (
|
|
22
|
+
!base.includes("://") &&
|
|
23
|
+
!base.startsWith("/") &&
|
|
24
|
+
!base.startsWith(".") &&
|
|
25
|
+
base.includes("@") &&
|
|
26
|
+
base.includes("/")
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Canonical parser for `namespace/name@version[#sha256-...]` refs. The single
|
|
31
|
+
* source of truth shared by the registry source, the kernel manifest cache,
|
|
32
|
+
* and the CLI (install / upgrade / bundle). Throws on a malformed ref. */
|
|
33
|
+
export function parseModuleRef(ref: string): ParsedModuleRef {
|
|
34
|
+
const { base, integrity } = splitIntegrity(ref);
|
|
35
|
+
const atIdx = base.lastIndexOf("@");
|
|
36
|
+
if (atIdx <= 0 || atIdx === base.length - 1) {
|
|
37
|
+
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
38
|
+
}
|
|
39
|
+
const modulePath = base.slice(0, atIdx);
|
|
40
|
+
if (!modulePath.includes("/")) {
|
|
41
|
+
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
42
|
+
}
|
|
43
|
+
const rawVersion = base.slice(atIdx + 1);
|
|
44
|
+
const version = rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion;
|
|
45
|
+
if (!version) {
|
|
46
|
+
throw new Error(`Invalid module reference '${ref}', expected namespace/name@version`);
|
|
47
|
+
}
|
|
48
|
+
return { modulePath, version, integrity };
|
|
49
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_MANIFEST_FILENAME, type ManifestSource } from "../types.js";
|
|
2
|
+
import { splitIntegrity, verifiedFetch } from "./integrity.js";
|
|
3
|
+
import { isRegistryRef, parseModuleRef } from "./module-ref.js";
|
|
2
4
|
|
|
3
5
|
const DEFAULT_REGISTRY_URL = "https://registry.telo.run";
|
|
4
6
|
|
|
@@ -6,25 +8,13 @@ export class RegistrySource implements ManifestSource {
|
|
|
6
8
|
constructor(private registryUrl = DEFAULT_REGISTRY_URL) {}
|
|
7
9
|
|
|
8
10
|
supports(url: string): boolean {
|
|
9
|
-
return (
|
|
10
|
-
!url.startsWith("http://") &&
|
|
11
|
-
!url.startsWith("https://") &&
|
|
12
|
-
!url.startsWith("/") &&
|
|
13
|
-
!url.startsWith(".") &&
|
|
14
|
-
url.includes("@") &&
|
|
15
|
-
url.includes("/")
|
|
16
|
-
);
|
|
11
|
+
return isRegistryRef(url);
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
async read(moduleRef: string): Promise<{ text: string; source: string }> {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
throw new Error(
|
|
24
|
-
`Failed to fetch manifest ${moduleRef}: ${response.status} ${response.statusText}`,
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
const text = await response.text();
|
|
15
|
+
const { base, integrity } = splitIntegrity(moduleRef);
|
|
16
|
+
const fetchUrl = this.toRegistryUrl(base);
|
|
17
|
+
const { text } = await verifiedFetch(fetchUrl, integrity, base);
|
|
28
18
|
// Some object-storage backends (e.g. Cloudflare R2 / S3) surface auth or
|
|
29
19
|
// permission failures by returning a 200 status with an XML error body.
|
|
30
20
|
// Catch this here so the loader produces a precise error rather than
|
|
@@ -37,7 +27,7 @@ export class RegistrySource implements ManifestSource {
|
|
|
37
27
|
? `${codeMatch[1]}: ${messageMatch[1]}`
|
|
38
28
|
: text.slice(0, 200);
|
|
39
29
|
throw new Error(
|
|
40
|
-
`Registry returned a non-manifest response for ${
|
|
30
|
+
`Registry returned a non-manifest response for ${base} ` +
|
|
41
31
|
`(URL: ${fetchUrl}): ${detail}`,
|
|
42
32
|
);
|
|
43
33
|
}
|
|
@@ -51,7 +41,7 @@ export class RegistrySource implements ManifestSource {
|
|
|
51
41
|
}
|
|
52
42
|
|
|
53
43
|
private toRegistryModuleBase(moduleRef: string): string {
|
|
54
|
-
const parsed =
|
|
44
|
+
const parsed = parseModuleRef(moduleRef);
|
|
55
45
|
const normalizedBase = this.registryUrl.replace(/\/+$/, "");
|
|
56
46
|
return `${normalizedBase}/${parsed.modulePath}/${parsed.version}`;
|
|
57
47
|
}
|
|
@@ -59,24 +49,4 @@ export class RegistrySource implements ManifestSource {
|
|
|
59
49
|
private toRegistryUrl(moduleRef: string): string {
|
|
60
50
|
return `${this.toRegistryModuleBase(moduleRef)}/${DEFAULT_MANIFEST_FILENAME}`;
|
|
61
51
|
}
|
|
62
|
-
|
|
63
|
-
private parseModuleRef(moduleRef: string): { modulePath: string; version: string } {
|
|
64
|
-
const atIdx = moduleRef.lastIndexOf("@");
|
|
65
|
-
if (atIdx <= 0 || atIdx === moduleRef.length - 1) {
|
|
66
|
-
throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const modulePath = moduleRef.slice(0, atIdx);
|
|
70
|
-
if (!modulePath.includes("/")) {
|
|
71
|
-
throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const rawVersion = moduleRef.slice(atIdx + 1);
|
|
75
|
-
const version = rawVersion.startsWith("v") ? rawVersion.substring(1) : rawVersion;
|
|
76
|
-
if (!version) {
|
|
77
|
-
throw new Error(`Invalid module reference '${moduleRef}', expected namespace/name@version`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { modulePath, version };
|
|
81
|
-
}
|
|
82
52
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const SOURCE = "telo-analyzer";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Warns when a library exports a kind (via `exports.kinds`) whose local
|
|
8
|
+
* `Telo.Definition` carries no `metadata.description`.
|
|
9
|
+
*
|
|
10
|
+
* The description is the primary human text the federated-discovery hub embeds
|
|
11
|
+
* for semantic search (`search_resources`), so an exported kind without one is
|
|
12
|
+
* undiscoverable by meaning. A warning (not an error) so the stdlib backfill is
|
|
13
|
+
* incremental and CI isn't blocked mid-migration.
|
|
14
|
+
*
|
|
15
|
+
* Scope: only kinds a library *exports* and *defines locally* are checked.
|
|
16
|
+
* Re-exported kinds (`exports.kinds: [Alias.Kind]`) belong to their owning
|
|
17
|
+
* module and are skipped; an exported name that isn't a local Telo.Definition
|
|
18
|
+
* (e.g. a Telo.Abstract) is skipped too. The check keys off the root library's
|
|
19
|
+
* own module doc, which is only present when that library is analyzed directly
|
|
20
|
+
* — so importing an under-described library never leaks warnings to its consumer.
|
|
21
|
+
*/
|
|
22
|
+
export function validateKindDescriptions(manifests: ResourceManifest[]): AnalysisDiagnostic[] {
|
|
23
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
24
|
+
|
|
25
|
+
// Local Telo.Definition docs keyed by `<module>/<name>`.
|
|
26
|
+
const definitions = new Map<string, ResourceManifest>();
|
|
27
|
+
for (const m of manifests) {
|
|
28
|
+
if (m.kind !== "Telo.Definition") continue;
|
|
29
|
+
const name = m.metadata?.name as string | undefined;
|
|
30
|
+
const mod = (m.metadata as { module?: string } | undefined)?.module;
|
|
31
|
+
if (name && mod) definitions.set(`${mod}/${name}`, m);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const m of manifests) {
|
|
35
|
+
if (m.kind !== "Telo.Library") continue;
|
|
36
|
+
const moduleName = m.metadata?.name as string | undefined;
|
|
37
|
+
if (!moduleName) continue;
|
|
38
|
+
const exportedKinds = (m as { exports?: { kinds?: unknown } }).exports?.kinds;
|
|
39
|
+
if (!Array.isArray(exportedKinds)) continue;
|
|
40
|
+
|
|
41
|
+
for (const entry of exportedKinds) {
|
|
42
|
+
// Re-export (`Alias.Kind`) — the owning module owns its description.
|
|
43
|
+
if (typeof entry !== "string" || entry.includes(".")) continue;
|
|
44
|
+
const def = definitions.get(`${moduleName}/${entry}`);
|
|
45
|
+
if (!def) continue; // not a local Telo.Definition (e.g. an abstract)
|
|
46
|
+
const description = (def.metadata as { description?: unknown } | undefined)?.description;
|
|
47
|
+
if (typeof description === "string" && description.trim() !== "") continue;
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
severity: DiagnosticSeverity.Warning,
|
|
50
|
+
code: "KIND_MISSING_DESCRIPTION",
|
|
51
|
+
source: SOURCE,
|
|
52
|
+
message:
|
|
53
|
+
`${moduleName}.${entry}: exported kind has no 'metadata.description'. Add a one-line ` +
|
|
54
|
+
`description — it is the primary text indexed for semantic discovery (search_resources).`,
|
|
55
|
+
data: {
|
|
56
|
+
resource: { kind: "Telo.Definition", name: entry },
|
|
57
|
+
filePath: (def.metadata as { source?: string } | undefined)?.source,
|
|
58
|
+
path: "metadata.description",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return diagnostics;
|
|
65
|
+
}
|