@telorun/analyzer 0.52.0 → 0.53.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.
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"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"}
package/dist/analyzer.js CHANGED
@@ -27,6 +27,7 @@ import { BINDINGS_ANNOTATION, bindingContextProperties, bindingPathChain, CEL_RE
27
27
  import { validateExtends } from "./validate-extends.js";
28
28
  import { validateLogging } from "./validate-logging.js";
29
29
  import { validateModuleArtifact } from "./validate-module-artifact.js";
30
+ import { validateModuleMetadata } from "./validate-module-metadata.js";
30
31
  import { validateBaseMapping } from "./validate-base-mapping.js";
31
32
  import { validateInvocationContract } from "./validate-invocation-contract.js";
32
33
  import { collectStepInputIssues } from "./validate-step-inputs.js";
@@ -1084,6 +1085,10 @@ export class StaticAnalyzer {
1084
1085
  // would otherwise fail on a consumer's machine — or, for a mistyped platform
1085
1086
  // axis, silently offer one platform's binary to every host.
1086
1087
  diagnostics.push(...validateModuleArtifact(allManifests));
1088
+ // The descriptive `metadata:` surface. Nothing in the kernel branches on
1089
+ // these fields, which is precisely why they need a check: a mistyped one
1090
+ // has no runtime failure mode that would ever surface it.
1091
+ diagnostics.push(...validateModuleMetadata(allManifests, defs, aliases));
1087
1092
  }
1088
1093
  resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
1089
1094
  // Trusted-input fast path: when the caller has already attested that
package/dist/index.d.ts CHANGED
@@ -53,6 +53,7 @@ export type { ArtifactSelector, LayerRole, PlatformAxis, PlatformTarget, } from
53
53
  export { LayerIndexError, matchControllerLayers, parseLayerIndex, singletonLayer, } from "./artifact-layer-index.js";
54
54
  export type { ArtifactLayer } from "./artifact-layer-index.js";
55
55
  export { validateModuleArtifact } from "./validate-module-artifact.js";
56
+ export { PUBLISH_BLOCKING_CODES } from "./validate-module-metadata.js";
56
57
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
57
58
  export { documentToAst, parseToAst } from "./yaml-ast.js";
58
59
  export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.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
@@ -31,6 +31,10 @@ export { MANIFEST_CACHE_BASE_URL, ManifestCacheSource, isHttpsModuleRef, manifes
31
31
  export { LAYER_ROLES, PLATFORM_AXES, ArtifactSelectorError, describeSelector, isLayerRole, normalizeSelector, selectorFromQualifiers, selectorKey, selectorMatches, } from "./artifact-selector.js";
32
32
  export { LayerIndexError, matchControllerLayers, parseLayerIndex, singletonLayer, } from "./artifact-layer-index.js";
33
33
  export { validateModuleArtifact } from "./validate-module-artifact.js";
34
+ // Warnings everywhere, fatal at `telo publish` — descriptive metadata has no
35
+ // runtime failure mode, so it must not stop a manifest running, but it is the
36
+ // module's public face the moment it is published.
37
+ export { PUBLISH_BLOCKING_CODES } from "./validate-module-metadata.js";
34
38
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
35
39
  export { documentToAst, parseToAst } from "./yaml-ast.js";
36
40
  export { CelParseError, buildCelSegments, wrapCelAst } from "./cel-ast.js";
@@ -0,0 +1,38 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ import type { DefinitionRegistry } from "./definition-registry.js";
4
+ import { type AnalysisDiagnostic } from "./types.js";
5
+ /**
6
+ * Static validation of the `metadata:` block on module docs (`Telo.Application` /
7
+ * `Telo.Library`) and of `metadata.deprecated` wherever it appears.
8
+ *
9
+ * These fields are descriptive — nothing in the kernel branches on them — but they
10
+ * are the module's public face: a hub indexes them, and a consumer reads them
11
+ * before deciding to import. That is exactly why they need checking. A field the
12
+ * runtime ignores has no failure mode that would ever surface it, so a mistyped
13
+ * `licence:` or `deprecatd:` is invisible forever, and the module ships claiming
14
+ * nothing while its author believes otherwise.
15
+ *
16
+ * The vocabulary stays **open** — `metadata` accepts any key, because a publisher
17
+ * may carry their own — so an unknown key is only reported when it is a near-miss
18
+ * of a known one. That catches the typo without closing the set.
19
+ *
20
+ * **Everything here is a WARNING, and fatal only at `telo publish`** (see
21
+ * {@link PUBLISH_BLOCKING_CODES}). Refusing to *run* a manifest over a field no
22
+ * runtime reads gets the cost backwards: `version: 1.0` is a YAML float rather
23
+ * than a string, which is a real mistake worth reporting, but stopping the app
24
+ * from starting over it is worse than the mistake. Publication is the moment
25
+ * these fields become consequential — they are projected onto the artifact's
26
+ * annotations and indexed by the hub — so that is where they block.
27
+ */
28
+ /**
29
+ * Codes that must not block running a manifest but MUST block publishing one.
30
+ *
31
+ * Kept as a set rather than a severity because the two audiences differ: a
32
+ * developer running a manifest wants to know, a publisher must be stopped. If a
33
+ * later check earns the same treatment, add its code here rather than inventing
34
+ * a third severity level.
35
+ */
36
+ export declare const PUBLISH_BLOCKING_CODES: ReadonlySet<string>;
37
+ export declare function validateModuleMetadata(manifests: ResourceManifest[], registry: DefinitionRegistry, aliases: AliasResolver): AnalysisDiagnostic[];
38
+ //# sourceMappingURL=validate-module-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate-module-metadata.d.ts","sourceRoot":"","sources":["../src/validate-module-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAIzE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAKrD,CAAC;AAiDH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,EAAE,aAAa,GACrB,kBAAkB,EAAE,CA8CtB"}
@@ -0,0 +1,256 @@
1
+ import { distance } from "./levenshtein.js";
2
+ import { DiagnosticSeverity } from "./types.js";
3
+ const SOURCE = "telo-analyzer";
4
+ /**
5
+ * Static validation of the `metadata:` block on module docs (`Telo.Application` /
6
+ * `Telo.Library`) and of `metadata.deprecated` wherever it appears.
7
+ *
8
+ * These fields are descriptive — nothing in the kernel branches on them — but they
9
+ * are the module's public face: a hub indexes them, and a consumer reads them
10
+ * before deciding to import. That is exactly why they need checking. A field the
11
+ * runtime ignores has no failure mode that would ever surface it, so a mistyped
12
+ * `licence:` or `deprecatd:` is invisible forever, and the module ships claiming
13
+ * nothing while its author believes otherwise.
14
+ *
15
+ * The vocabulary stays **open** — `metadata` accepts any key, because a publisher
16
+ * may carry their own — so an unknown key is only reported when it is a near-miss
17
+ * of a known one. That catches the typo without closing the set.
18
+ *
19
+ * **Everything here is a WARNING, and fatal only at `telo publish`** (see
20
+ * {@link PUBLISH_BLOCKING_CODES}). Refusing to *run* a manifest over a field no
21
+ * runtime reads gets the cost backwards: `version: 1.0` is a YAML float rather
22
+ * than a string, which is a real mistake worth reporting, but stopping the app
23
+ * from starting over it is worse than the mistake. Publication is the moment
24
+ * these fields become consequential — they are projected onto the artifact's
25
+ * annotations and indexed by the hub — so that is where they block.
26
+ */
27
+ /**
28
+ * Codes that must not block running a manifest but MUST block publishing one.
29
+ *
30
+ * Kept as a set rather than a severity because the two audiences differ: a
31
+ * developer running a manifest wants to know, a publisher must be stopped. If a
32
+ * later check earns the same treatment, add its code here rather than inventing
33
+ * a third severity level.
34
+ */
35
+ export const PUBLISH_BLOCKING_CODES = new Set([
36
+ "METADATA_INVALID_TYPE",
37
+ "METADATA_UNKNOWN_FIELD",
38
+ "INVALID_DEPRECATION",
39
+ "DEPRECATION_REPLACEMENT_UNRESOLVED",
40
+ ]);
41
+ /** Conventional module-doc metadata, and the type each carries. Descriptive only;
42
+ * `name` is the sole field anything resolves against. */
43
+ const MODULE_METADATA_TYPES = {
44
+ name: "string",
45
+ module: "string",
46
+ version: "string",
47
+ description: "string",
48
+ repository: "string",
49
+ homepage: "string",
50
+ documentation: "string",
51
+ license: "string",
52
+ namespace: "string",
53
+ categories: "string[]",
54
+ deprecated: "object",
55
+ };
56
+ /** What a kind doc's `metadata:` may carry.
57
+ *
58
+ * Deliberately narrower than a module's: `version`, `license` and the rest
59
+ * belong to the module, and a kind restating them means nothing. `categories`
60
+ * is legal and *replaces* the module's for that kind; `description` is hub
61
+ * search text. Both have exactly the failure mode this file exists for — a
62
+ * `descriptoin:` on a kind doc is read by nothing and reported by nothing, so
63
+ * it ships silently — which is why kind docs are checked rather than exempt. */
64
+ const KIND_METADATA_TYPES = {
65
+ name: "string",
66
+ module: "string",
67
+ description: "string",
68
+ categories: "string[]",
69
+ deprecated: "object",
70
+ };
71
+ /** Alias-qualified kind — `Self.Migrations`, `Cache.Store`, `Telo.JsonSchema`. */
72
+ const ALIAS_KIND_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
73
+ /** The built-in namespace, resolvable without an import — mirrors `validate-extends`. */
74
+ const TELO_BUILTIN_ALIAS = "Telo";
75
+ function typeOf(value) {
76
+ if (typeof value === "string")
77
+ return "string";
78
+ if (Array.isArray(value))
79
+ return value.every((v) => typeof v === "string") ? "string[]" : "other";
80
+ if (value !== null && typeof value === "object")
81
+ return "object";
82
+ return "other";
83
+ }
84
+ export function validateModuleMetadata(manifests, registry, aliases) {
85
+ const out = [];
86
+ // Docs forwarded from imported libraries carry `metadata.module` set to that
87
+ // library's name. Their `replacedBy` aliases — `Self`, or any alias private to
88
+ // that library — belong to the library's OWN scope, which the consumer's
89
+ // resolver knows nothing about, so re-checking them here reports a false
90
+ // DEPRECATION_REPLACEMENT_UNRESOLVED against a manifest the consumer does not
91
+ // own. They are validated when that library is analyzed as a root, which is
92
+ // its author's concern. Same rule, and the same reason, as `validate-extends`.
93
+ const importedModules = new Set();
94
+ for (const m of manifests) {
95
+ if (m.kind !== "Telo.Import")
96
+ continue;
97
+ const resolved = m.metadata
98
+ ?.resolvedModuleName;
99
+ if (resolved)
100
+ importedModules.add(resolved);
101
+ }
102
+ for (const manifest of manifests) {
103
+ const isModuleDoc = manifest.kind === "Telo.Application" || manifest.kind === "Telo.Library";
104
+ const isKindDoc = manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract";
105
+ if (!isModuleDoc && !isKindDoc)
106
+ continue;
107
+ const metadata = manifest.metadata;
108
+ if (!metadata)
109
+ continue;
110
+ const ownModule = metadata.module;
111
+ if (ownModule && importedModules.has(ownModule))
112
+ continue;
113
+ const name = typeof metadata.name === "string" ? metadata.name : undefined;
114
+ const filePath = typeof metadata.source === "string" ? metadata.source : undefined;
115
+ const ctx = {
116
+ label: `${manifest.kind}/${name ?? "(unnamed)"}`,
117
+ resource: { kind: manifest.kind, name },
118
+ filePath,
119
+ };
120
+ validateFieldTypes(metadata, isModuleDoc ? MODULE_METADATA_TYPES : KIND_METADATA_TYPES, ctx, out);
121
+ validateDeprecation(metadata, isModuleDoc, ctx, registry, aliases, out);
122
+ }
123
+ return out;
124
+ }
125
+ /** How far a key may be from a known one and still be called a typo.
126
+ *
127
+ * Scaled, not absolute: at a flat 2, `date:` (a perfectly ordinary key an
128
+ * author might carry) is two edits from `name` and gets told it is a
129
+ * misspelling of it. The vocabulary is open, so a false accusation on a short
130
+ * key is worse than missing a typo on one. */
131
+ function typoThreshold(key, known) {
132
+ return Math.max(1, Math.floor(Math.min(key.length, known.length) / 3));
133
+ }
134
+ function validateFieldTypes(metadata, allowed, ctx, out) {
135
+ const known = Object.keys(allowed);
136
+ for (const [key, value] of Object.entries(metadata)) {
137
+ // Stamped by the loader, not authored — never a typo to report on.
138
+ if (key === "source")
139
+ continue;
140
+ // Listed as known so a typo still gets suggested against it, but its shape
141
+ // belongs to `validateDeprecation`, which can say what is actually wrong.
142
+ // Type-checking it here too would report one mistake twice.
143
+ if (key === "deprecated")
144
+ continue;
145
+ const expected = allowed[key];
146
+ if (expected === undefined) {
147
+ const near = known.find((k) => distance(key, k) <= typoThreshold(key, k));
148
+ if (near) {
149
+ out.push({
150
+ severity: DiagnosticSeverity.Warning,
151
+ code: "METADATA_UNKNOWN_FIELD",
152
+ source: SOURCE,
153
+ message: `${ctx.label}: 'metadata.${key}' is not a known field — did you mean '${near}'? ` +
154
+ `Nothing reads an unrecognized key, so this declares nothing.`,
155
+ data: { resource: ctx.resource, filePath: ctx.filePath, path: `metadata.${key}` },
156
+ });
157
+ }
158
+ continue;
159
+ }
160
+ if (typeOf(value) !== expected) {
161
+ out.push({
162
+ severity: DiagnosticSeverity.Warning,
163
+ code: "METADATA_INVALID_TYPE",
164
+ source: SOURCE,
165
+ message: `${ctx.label}: 'metadata.${key}' must be ${describeType(expected)}.`,
166
+ data: { resource: ctx.resource, filePath: ctx.filePath, path: `metadata.${key}` },
167
+ });
168
+ }
169
+ }
170
+ }
171
+ function describeType(t) {
172
+ if (t === "string[]")
173
+ return "an array of strings";
174
+ if (t === "object")
175
+ return "an object";
176
+ return "a string";
177
+ }
178
+ /**
179
+ * `metadata.deprecated: { reason, replacedBy? }`.
180
+ *
181
+ * `replacedBy` is deliberately resolvable rather than free text, and its form
182
+ * follows the level: a module doc names another **module ref** (the `imports:`
183
+ * source grammar), a kind doc names an **alias-qualified kind** resolved through
184
+ * this file's own imports — the same grammar `kind:` / `extends:` use, so the
185
+ * replacement is a link a consumer can follow rather than a sentence they have to
186
+ * interpret.
187
+ *
188
+ * A kind whose replacement lives in a module this one does not import cannot be
189
+ * named; that case deprecates at module level with a module ref instead. Accepted
190
+ * over inventing a second grammar for it.
191
+ */
192
+ function validateDeprecation(metadata, isModuleDoc, ctx, registry, aliases, out) {
193
+ const deprecated = metadata.deprecated;
194
+ if (deprecated === undefined)
195
+ return;
196
+ const at = "metadata.deprecated";
197
+ const push = (code, message, path = at, severity = DiagnosticSeverity.Warning) => {
198
+ out.push({
199
+ severity,
200
+ code,
201
+ source: SOURCE,
202
+ message: `${ctx.label}: ${message}`,
203
+ data: { resource: ctx.resource, filePath: ctx.filePath, path },
204
+ });
205
+ };
206
+ if (typeOf(deprecated) !== "object") {
207
+ push("INVALID_DEPRECATION", `'${at}' must be an object with a 'reason' (and an optional 'replacedBy'). ` +
208
+ `A bare 'true' says a thing is deprecated without saying what to do instead.`);
209
+ return;
210
+ }
211
+ const block = deprecated;
212
+ const allowed = new Set(["reason", "replacedBy"]);
213
+ for (const key of Object.keys(block)) {
214
+ if (!allowed.has(key)) {
215
+ push("INVALID_DEPRECATION", `'${at}.${key}' is not a recognized key (reason, replacedBy).`, `${at}.${key}`);
216
+ }
217
+ }
218
+ if (typeof block.reason !== "string" || block.reason.trim() === "") {
219
+ push("INVALID_DEPRECATION", `'${at}.reason' is required and must be a non-empty string — it is what a consumer reads to know what to do instead.`, `${at}.reason`);
220
+ }
221
+ const replacedBy = block.replacedBy;
222
+ if (replacedBy === undefined)
223
+ return;
224
+ const path = `${at}.replacedBy`;
225
+ if (typeof replacedBy !== "string" || replacedBy.trim() === "") {
226
+ push("INVALID_DEPRECATION", `'${path}' must be a non-empty string.`, path);
227
+ return;
228
+ }
229
+ if (isModuleDoc) {
230
+ // A module is replaced by another module, addressed the way an import is.
231
+ // Catching alias form here is worth a dedicated message: it is the natural
232
+ // mistake, and it would otherwise be stored as an unresolvable ref.
233
+ if (ALIAS_KIND_RE.test(replacedBy)) {
234
+ push("INVALID_DEPRECATION", `'${path}: ${replacedBy}' looks like a kind reference, but a module doc's replacement is a ` +
235
+ `module ref (e.g. 'oci://ghcr.io/acme/thing'). Deprecate the kind itself to point at another kind.`, path);
236
+ }
237
+ return;
238
+ }
239
+ // Kind level: resolve through this file's imports, exactly as `extends` does.
240
+ if (!ALIAS_KIND_RE.test(replacedBy)) {
241
+ push("INVALID_DEPRECATION", `'${path}: ${replacedBy}' must be an alias-qualified kind ("<Alias>.<Kind>", ` +
242
+ `e.g. 'Self.Migrations'), resolved via this file's imports.`, path);
243
+ return;
244
+ }
245
+ const prefix = replacedBy.slice(0, replacedBy.indexOf("."));
246
+ if (prefix !== TELO_BUILTIN_ALIAS && !aliases.hasAlias(prefix)) {
247
+ push("DEPRECATION_REPLACEMENT_UNRESOLVED", `'${path}: ${replacedBy}' — alias '${prefix}' is not an import in this file's scope. ` +
248
+ `Declare the import or correct the alias.`, path);
249
+ return;
250
+ }
251
+ const canonical = aliases.resolveKind(replacedBy);
252
+ if (!canonical || !registry.resolve(canonical)) {
253
+ push("DEPRECATION_REPLACEMENT_UNRESOLVED", `'${path}: ${replacedBy}' does not resolve to a known kind. A replacement a consumer ` +
254
+ `cannot follow is no better than none.`, path);
255
+ }
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
package/src/analyzer.ts CHANGED
@@ -76,6 +76,7 @@ import {
76
76
  import { validateExtends } from "./validate-extends.js";
77
77
  import { validateLogging } from "./validate-logging.js";
78
78
  import { validateModuleArtifact } from "./validate-module-artifact.js";
79
+ import { validateModuleMetadata } from "./validate-module-metadata.js";
79
80
  import { validateBaseMapping } from "./validate-base-mapping.js";
80
81
  import { validateInvocationContract } from "./validate-invocation-contract.js";
81
82
  import { collectStepInputIssues } from "./validate-step-inputs.js";
@@ -1315,6 +1316,10 @@ export class StaticAnalyzer {
1315
1316
  // would otherwise fail on a consumer's machine — or, for a mistyped platform
1316
1317
  // axis, silently offer one platform's binary to every host.
1317
1318
  diagnostics.push(...validateModuleArtifact(allManifests));
1319
+ // The descriptive `metadata:` surface. Nothing in the kernel branches on
1320
+ // these fields, which is precisely why they need a check: a mistyped one
1321
+ // has no runtime failure mode that would ever surface it.
1322
+ diagnostics.push(...validateModuleMetadata(allManifests, defs, aliases));
1318
1323
  }
1319
1324
  resolveSchemaTypeRefs(allManifests, aliases, aliasesByModule);
1320
1325
 
package/src/index.ts CHANGED
@@ -167,6 +167,10 @@ export {
167
167
  } from "./artifact-layer-index.js";
168
168
  export type { ArtifactLayer } from "./artifact-layer-index.js";
169
169
  export { validateModuleArtifact } from "./validate-module-artifact.js";
170
+ // Warnings everywhere, fatal at `telo publish` — descriptive metadata has no
171
+ // runtime failure mode, so it must not stop a manifest running, but it is the
172
+ // module's public face the moment it is published.
173
+ export { PUBLISH_BLOCKING_CODES } from "./validate-module-metadata.js";
170
174
  export { withSyntheticPositions } from "./with-synthetic-positions.js";
171
175
  export { documentToAst, parseToAst } from "./yaml-ast.js";
172
176
  export type { AstDocument, AstMap, AstNode, AstPair, AstScalar, AstSeq } from "./yaml-ast.js";
@@ -0,0 +1,335 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+
3
+ import type { AliasResolver } from "./alias-resolver.js";
4
+ import type { DefinitionRegistry } from "./definition-registry.js";
5
+ import { distance } from "./levenshtein.js";
6
+ import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
7
+
8
+ const SOURCE = "telo-analyzer";
9
+
10
+ /**
11
+ * Static validation of the `metadata:` block on module docs (`Telo.Application` /
12
+ * `Telo.Library`) and of `metadata.deprecated` wherever it appears.
13
+ *
14
+ * These fields are descriptive — nothing in the kernel branches on them — but they
15
+ * are the module's public face: a hub indexes them, and a consumer reads them
16
+ * before deciding to import. That is exactly why they need checking. A field the
17
+ * runtime ignores has no failure mode that would ever surface it, so a mistyped
18
+ * `licence:` or `deprecatd:` is invisible forever, and the module ships claiming
19
+ * nothing while its author believes otherwise.
20
+ *
21
+ * The vocabulary stays **open** — `metadata` accepts any key, because a publisher
22
+ * may carry their own — so an unknown key is only reported when it is a near-miss
23
+ * of a known one. That catches the typo without closing the set.
24
+ *
25
+ * **Everything here is a WARNING, and fatal only at `telo publish`** (see
26
+ * {@link PUBLISH_BLOCKING_CODES}). Refusing to *run* a manifest over a field no
27
+ * runtime reads gets the cost backwards: `version: 1.0` is a YAML float rather
28
+ * than a string, which is a real mistake worth reporting, but stopping the app
29
+ * from starting over it is worse than the mistake. Publication is the moment
30
+ * these fields become consequential — they are projected onto the artifact's
31
+ * annotations and indexed by the hub — so that is where they block.
32
+ */
33
+
34
+ /**
35
+ * Codes that must not block running a manifest but MUST block publishing one.
36
+ *
37
+ * Kept as a set rather than a severity because the two audiences differ: a
38
+ * developer running a manifest wants to know, a publisher must be stopped. If a
39
+ * later check earns the same treatment, add its code here rather than inventing
40
+ * a third severity level.
41
+ */
42
+ export const PUBLISH_BLOCKING_CODES: ReadonlySet<string> = new Set([
43
+ "METADATA_INVALID_TYPE",
44
+ "METADATA_UNKNOWN_FIELD",
45
+ "INVALID_DEPRECATION",
46
+ "DEPRECATION_REPLACEMENT_UNRESOLVED",
47
+ ]);
48
+
49
+ type FieldType = "string" | "string[]" | "object";
50
+
51
+ /** Conventional module-doc metadata, and the type each carries. Descriptive only;
52
+ * `name` is the sole field anything resolves against. */
53
+ const MODULE_METADATA_TYPES: Record<string, FieldType> = {
54
+ name: "string",
55
+ module: "string",
56
+ version: "string",
57
+ description: "string",
58
+ repository: "string",
59
+ homepage: "string",
60
+ documentation: "string",
61
+ license: "string",
62
+ namespace: "string",
63
+ categories: "string[]",
64
+ deprecated: "object",
65
+ };
66
+
67
+ /** What a kind doc's `metadata:` may carry.
68
+ *
69
+ * Deliberately narrower than a module's: `version`, `license` and the rest
70
+ * belong to the module, and a kind restating them means nothing. `categories`
71
+ * is legal and *replaces* the module's for that kind; `description` is hub
72
+ * search text. Both have exactly the failure mode this file exists for — a
73
+ * `descriptoin:` on a kind doc is read by nothing and reported by nothing, so
74
+ * it ships silently — which is why kind docs are checked rather than exempt. */
75
+ const KIND_METADATA_TYPES: Record<string, FieldType> = {
76
+ name: "string",
77
+ module: "string",
78
+ description: "string",
79
+ categories: "string[]",
80
+ deprecated: "object",
81
+ };
82
+
83
+ /** Alias-qualified kind — `Self.Migrations`, `Cache.Store`, `Telo.JsonSchema`. */
84
+ const ALIAS_KIND_RE = /^[A-Z][A-Za-z0-9_]*\.[A-Z][A-Za-z0-9_]*$/;
85
+
86
+ /** The built-in namespace, resolvable without an import — mirrors `validate-extends`. */
87
+ const TELO_BUILTIN_ALIAS = "Telo";
88
+
89
+ function typeOf(value: unknown): "string" | "string[]" | "object" | "other" {
90
+ if (typeof value === "string") return "string";
91
+ if (Array.isArray(value)) return value.every((v) => typeof v === "string") ? "string[]" : "other";
92
+ if (value !== null && typeof value === "object") return "object";
93
+ return "other";
94
+ }
95
+
96
+ export function validateModuleMetadata(
97
+ manifests: ResourceManifest[],
98
+ registry: DefinitionRegistry,
99
+ aliases: AliasResolver,
100
+ ): AnalysisDiagnostic[] {
101
+ const out: AnalysisDiagnostic[] = [];
102
+
103
+ // Docs forwarded from imported libraries carry `metadata.module` set to that
104
+ // library's name. Their `replacedBy` aliases — `Self`, or any alias private to
105
+ // that library — belong to the library's OWN scope, which the consumer's
106
+ // resolver knows nothing about, so re-checking them here reports a false
107
+ // DEPRECATION_REPLACEMENT_UNRESOLVED against a manifest the consumer does not
108
+ // own. They are validated when that library is analyzed as a root, which is
109
+ // its author's concern. Same rule, and the same reason, as `validate-extends`.
110
+ const importedModules = new Set<string>();
111
+ for (const m of manifests) {
112
+ if (m.kind !== "Telo.Import") continue;
113
+ const resolved = (m.metadata as { resolvedModuleName?: string } | undefined)
114
+ ?.resolvedModuleName;
115
+ if (resolved) importedModules.add(resolved);
116
+ }
117
+
118
+ for (const manifest of manifests) {
119
+ const isModuleDoc = manifest.kind === "Telo.Application" || manifest.kind === "Telo.Library";
120
+ const isKindDoc = manifest.kind === "Telo.Definition" || manifest.kind === "Telo.Abstract";
121
+ if (!isModuleDoc && !isKindDoc) continue;
122
+
123
+ const metadata = manifest.metadata as Record<string, unknown> | undefined;
124
+ if (!metadata) continue;
125
+
126
+ const ownModule = (metadata as { module?: string }).module;
127
+ if (ownModule && importedModules.has(ownModule)) continue;
128
+
129
+ const name = typeof metadata.name === "string" ? metadata.name : undefined;
130
+ const filePath = typeof metadata.source === "string" ? metadata.source : undefined;
131
+ const ctx = {
132
+ label: `${manifest.kind}/${name ?? "(unnamed)"}`,
133
+ resource: { kind: manifest.kind, name },
134
+ filePath,
135
+ };
136
+
137
+ validateFieldTypes(
138
+ metadata,
139
+ isModuleDoc ? MODULE_METADATA_TYPES : KIND_METADATA_TYPES,
140
+ ctx,
141
+ out,
142
+ );
143
+ validateDeprecation(metadata, isModuleDoc, ctx, registry, aliases, out);
144
+ }
145
+ return out;
146
+ }
147
+
148
+ interface DocContext {
149
+ label: string;
150
+ resource: { kind: string; name: string | undefined };
151
+ filePath: string | undefined;
152
+ }
153
+
154
+ /** How far a key may be from a known one and still be called a typo.
155
+ *
156
+ * Scaled, not absolute: at a flat 2, `date:` (a perfectly ordinary key an
157
+ * author might carry) is two edits from `name` and gets told it is a
158
+ * misspelling of it. The vocabulary is open, so a false accusation on a short
159
+ * key is worse than missing a typo on one. */
160
+ function typoThreshold(key: string, known: string): number {
161
+ return Math.max(1, Math.floor(Math.min(key.length, known.length) / 3));
162
+ }
163
+
164
+ function validateFieldTypes(
165
+ metadata: Record<string, unknown>,
166
+ allowed: Record<string, FieldType>,
167
+ ctx: DocContext,
168
+ out: AnalysisDiagnostic[],
169
+ ): void {
170
+ const known = Object.keys(allowed);
171
+ for (const [key, value] of Object.entries(metadata)) {
172
+ // Stamped by the loader, not authored — never a typo to report on.
173
+ if (key === "source") continue;
174
+
175
+ // Listed as known so a typo still gets suggested against it, but its shape
176
+ // belongs to `validateDeprecation`, which can say what is actually wrong.
177
+ // Type-checking it here too would report one mistake twice.
178
+ if (key === "deprecated") continue;
179
+
180
+ const expected = allowed[key];
181
+ if (expected === undefined) {
182
+ const near = known.find((k) => distance(key, k) <= typoThreshold(key, k));
183
+ if (near) {
184
+ out.push({
185
+ severity: DiagnosticSeverity.Warning,
186
+ code: "METADATA_UNKNOWN_FIELD",
187
+ source: SOURCE,
188
+ message:
189
+ `${ctx.label}: 'metadata.${key}' is not a known field — did you mean '${near}'? ` +
190
+ `Nothing reads an unrecognized key, so this declares nothing.`,
191
+ data: { resource: ctx.resource, filePath: ctx.filePath, path: `metadata.${key}` },
192
+ });
193
+ }
194
+ continue;
195
+ }
196
+
197
+ if (typeOf(value) !== expected) {
198
+ out.push({
199
+ severity: DiagnosticSeverity.Warning,
200
+ code: "METADATA_INVALID_TYPE",
201
+ source: SOURCE,
202
+ message: `${ctx.label}: 'metadata.${key}' must be ${describeType(expected)}.`,
203
+ data: { resource: ctx.resource, filePath: ctx.filePath, path: `metadata.${key}` },
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ function describeType(t: "string" | "string[]" | "object"): string {
210
+ if (t === "string[]") return "an array of strings";
211
+ if (t === "object") return "an object";
212
+ return "a string";
213
+ }
214
+
215
+ /**
216
+ * `metadata.deprecated: { reason, replacedBy? }`.
217
+ *
218
+ * `replacedBy` is deliberately resolvable rather than free text, and its form
219
+ * follows the level: a module doc names another **module ref** (the `imports:`
220
+ * source grammar), a kind doc names an **alias-qualified kind** resolved through
221
+ * this file's own imports — the same grammar `kind:` / `extends:` use, so the
222
+ * replacement is a link a consumer can follow rather than a sentence they have to
223
+ * interpret.
224
+ *
225
+ * A kind whose replacement lives in a module this one does not import cannot be
226
+ * named; that case deprecates at module level with a module ref instead. Accepted
227
+ * over inventing a second grammar for it.
228
+ */
229
+ function validateDeprecation(
230
+ metadata: Record<string, unknown>,
231
+ isModuleDoc: boolean,
232
+ ctx: DocContext,
233
+ registry: DefinitionRegistry,
234
+ aliases: AliasResolver,
235
+ out: AnalysisDiagnostic[],
236
+ ): void {
237
+ const deprecated = metadata.deprecated;
238
+ if (deprecated === undefined) return;
239
+
240
+ const at = "metadata.deprecated";
241
+ const push = (
242
+ code: string,
243
+ message: string,
244
+ path = at,
245
+ severity = DiagnosticSeverity.Warning,
246
+ ): void => {
247
+ out.push({
248
+ severity,
249
+ code,
250
+ source: SOURCE,
251
+ message: `${ctx.label}: ${message}`,
252
+ data: { resource: ctx.resource, filePath: ctx.filePath, path },
253
+ });
254
+ };
255
+
256
+ if (typeOf(deprecated) !== "object") {
257
+ push(
258
+ "INVALID_DEPRECATION",
259
+ `'${at}' must be an object with a 'reason' (and an optional 'replacedBy'). ` +
260
+ `A bare 'true' says a thing is deprecated without saying what to do instead.`,
261
+ );
262
+ return;
263
+ }
264
+
265
+ const block = deprecated as Record<string, unknown>;
266
+ const allowed = new Set(["reason", "replacedBy"]);
267
+ for (const key of Object.keys(block)) {
268
+ if (!allowed.has(key)) {
269
+ push("INVALID_DEPRECATION", `'${at}.${key}' is not a recognized key (reason, replacedBy).`, `${at}.${key}`);
270
+ }
271
+ }
272
+
273
+ if (typeof block.reason !== "string" || block.reason.trim() === "") {
274
+ push(
275
+ "INVALID_DEPRECATION",
276
+ `'${at}.reason' is required and must be a non-empty string — it is what a consumer reads to know what to do instead.`,
277
+ `${at}.reason`,
278
+ );
279
+ }
280
+
281
+ const replacedBy = block.replacedBy;
282
+ if (replacedBy === undefined) return;
283
+ const path = `${at}.replacedBy`;
284
+ if (typeof replacedBy !== "string" || replacedBy.trim() === "") {
285
+ push("INVALID_DEPRECATION", `'${path}' must be a non-empty string.`, path);
286
+ return;
287
+ }
288
+
289
+ if (isModuleDoc) {
290
+ // A module is replaced by another module, addressed the way an import is.
291
+ // Catching alias form here is worth a dedicated message: it is the natural
292
+ // mistake, and it would otherwise be stored as an unresolvable ref.
293
+ if (ALIAS_KIND_RE.test(replacedBy)) {
294
+ push(
295
+ "INVALID_DEPRECATION",
296
+ `'${path}: ${replacedBy}' looks like a kind reference, but a module doc's replacement is a ` +
297
+ `module ref (e.g. 'oci://ghcr.io/acme/thing'). Deprecate the kind itself to point at another kind.`,
298
+ path,
299
+ );
300
+ }
301
+ return;
302
+ }
303
+
304
+ // Kind level: resolve through this file's imports, exactly as `extends` does.
305
+ if (!ALIAS_KIND_RE.test(replacedBy)) {
306
+ push(
307
+ "INVALID_DEPRECATION",
308
+ `'${path}: ${replacedBy}' must be an alias-qualified kind ("<Alias>.<Kind>", ` +
309
+ `e.g. 'Self.Migrations'), resolved via this file's imports.`,
310
+ path,
311
+ );
312
+ return;
313
+ }
314
+
315
+ const prefix = replacedBy.slice(0, replacedBy.indexOf("."));
316
+ if (prefix !== TELO_BUILTIN_ALIAS && !aliases.hasAlias(prefix)) {
317
+ push(
318
+ "DEPRECATION_REPLACEMENT_UNRESOLVED",
319
+ `'${path}: ${replacedBy}' — alias '${prefix}' is not an import in this file's scope. ` +
320
+ `Declare the import or correct the alias.`,
321
+ path,
322
+ );
323
+ return;
324
+ }
325
+
326
+ const canonical = aliases.resolveKind(replacedBy);
327
+ if (!canonical || !registry.resolve(canonical)) {
328
+ push(
329
+ "DEPRECATION_REPLACEMENT_UNRESOLVED",
330
+ `'${path}: ${replacedBy}' does not resolve to a known kind. A replacement a consumer ` +
331
+ `cannot follow is no better than none.`,
332
+ path,
333
+ );
334
+ }
335
+ }