@telorun/analyzer 0.72.0 → 0.74.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 (208) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +76 -20
  3. package/dist/artifact-axes.d.ts +14 -0
  4. package/dist/artifact-axes.d.ts.map +1 -0
  5. package/dist/artifact-axes.js +12 -0
  6. package/dist/artifact-layer-index.d.ts +2 -2
  7. package/dist/artifact-layer-index.d.ts.map +1 -1
  8. package/dist/artifact-layer-index.js +9 -4
  9. package/dist/artifact-selector.d.ts +47 -21
  10. package/dist/artifact-selector.d.ts.map +1 -1
  11. package/dist/artifact-selector.js +85 -16
  12. package/dist/builtins.d.ts.map +1 -1
  13. package/dist/builtins.js +144 -113
  14. package/dist/call-graph.d.ts +12 -1
  15. package/dist/call-graph.d.ts.map +1 -1
  16. package/dist/call-graph.js +22 -0
  17. package/dist/catch-scope.js +1 -1
  18. package/dist/cel-scope-query.d.ts.map +1 -1
  19. package/dist/cel-scope-query.js +32 -11
  20. package/dist/cel-scope.d.ts.map +1 -1
  21. package/dist/cel-scope.js +1 -0
  22. package/dist/eval-paths.d.ts +36 -5
  23. package/dist/eval-paths.d.ts.map +1 -1
  24. package/dist/eval-paths.js +47 -6
  25. package/dist/extends-resolution.d.ts +12 -0
  26. package/dist/extends-resolution.d.ts.map +1 -1
  27. package/dist/extends-resolution.js +23 -0
  28. package/dist/forwarded-declaration.d.ts +26 -0
  29. package/dist/forwarded-declaration.d.ts.map +1 -0
  30. package/dist/forwarded-declaration.js +31 -0
  31. package/dist/index.d.ts +10 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5 -2
  34. package/dist/kernel-globals.d.ts.map +1 -1
  35. package/dist/kernel-globals.js +21 -5
  36. package/dist/module-alias-scope.d.ts +11 -0
  37. package/dist/module-alias-scope.d.ts.map +1 -1
  38. package/dist/module-alias-scope.js +10 -0
  39. package/dist/module-file-claims.d.ts +17 -1
  40. package/dist/module-file-claims.d.ts.map +1 -1
  41. package/dist/module-file-claims.js +38 -3
  42. package/dist/module-named-files.d.ts +72 -0
  43. package/dist/module-named-files.d.ts.map +1 -0
  44. package/dist/module-named-files.js +115 -0
  45. package/dist/native-entries.d.ts +83 -0
  46. package/dist/native-entries.d.ts.map +1 -0
  47. package/dist/native-entries.js +169 -0
  48. package/dist/nearest-name.d.ts +22 -0
  49. package/dist/nearest-name.d.ts.map +1 -0
  50. package/dist/nearest-name.js +43 -0
  51. package/dist/normalize-inline-resources.d.ts +41 -13
  52. package/dist/normalize-inline-resources.d.ts.map +1 -1
  53. package/dist/normalize-inline-resources.js +236 -54
  54. package/dist/reference-field-map.d.ts +59 -1
  55. package/dist/reference-field-map.d.ts.map +1 -1
  56. package/dist/reference-field-map.js +200 -34
  57. package/dist/release/destinations.d.ts +59 -0
  58. package/dist/release/destinations.d.ts.map +1 -0
  59. package/dist/release/destinations.js +75 -0
  60. package/dist/release/index.d.ts +6 -2
  61. package/dist/release/index.d.ts.map +1 -1
  62. package/dist/release/index.js +3 -1
  63. package/dist/release/ledger.d.ts +20 -11
  64. package/dist/release/ledger.d.ts.map +1 -1
  65. package/dist/release/ledger.js +43 -14
  66. package/dist/release/payload-digest.d.ts +2 -1
  67. package/dist/release/payload-digest.d.ts.map +1 -1
  68. package/dist/release/payload-digest.js +2 -1
  69. package/dist/release/release-plan.d.ts +3 -2
  70. package/dist/release/release-plan.d.ts.map +1 -1
  71. package/dist/release/release-plan.js +12 -19
  72. package/dist/release/workspace-config.d.ts +114 -34
  73. package/dist/release/workspace-config.d.ts.map +1 -1
  74. package/dist/release/workspace-config.js +322 -38
  75. package/dist/release/workspace-schema.d.ts +41 -0
  76. package/dist/release/workspace-schema.d.ts.map +1 -0
  77. package/dist/release/workspace-schema.js +77 -0
  78. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  79. package/dist/resolve-ref-sentinels.js +8 -5
  80. package/dist/resolve-throws-union.d.ts +25 -4
  81. package/dist/resolve-throws-union.d.ts.map +1 -1
  82. package/dist/resolve-throws-union.js +62 -32
  83. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  84. package/dist/resolve-zone-requirements.js +2 -1
  85. package/dist/rewrite-synthetic-origins.d.ts +6 -1
  86. package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
  87. package/dist/rewrite-synthetic-origins.js +107 -13
  88. package/dist/schema-compat.d.ts.map +1 -1
  89. package/dist/schema-compat.js +18 -5
  90. package/dist/schema-error-report.d.ts +7 -0
  91. package/dist/schema-error-report.d.ts.map +1 -1
  92. package/dist/schema-error-report.js +9 -4
  93. package/dist/schema-walk.d.ts +47 -13
  94. package/dist/schema-walk.d.ts.map +1 -1
  95. package/dist/schema-walk.js +150 -30
  96. package/dist/scope-declarations.d.ts +59 -0
  97. package/dist/scope-declarations.d.ts.map +1 -0
  98. package/dist/scope-declarations.js +45 -0
  99. package/dist/source-entries.d.ts +119 -0
  100. package/dist/source-entries.d.ts.map +1 -0
  101. package/dist/source-entries.js +349 -0
  102. package/dist/step-bodies.d.ts +24 -0
  103. package/dist/step-bodies.d.ts.map +1 -0
  104. package/dist/step-bodies.js +28 -0
  105. package/dist/telo-version.d.ts +1 -1
  106. package/dist/telo-version.js +1 -1
  107. package/dist/template-body.d.ts +10 -0
  108. package/dist/template-body.d.ts.map +1 -1
  109. package/dist/template-body.js +25 -0
  110. package/dist/validate-cel-context.d.ts +23 -8
  111. package/dist/validate-cel-context.d.ts.map +1 -1
  112. package/dist/validate-cel-context.js +51 -18
  113. package/dist/validate-exports.d.ts +28 -0
  114. package/dist/validate-exports.d.ts.map +1 -0
  115. package/dist/validate-exports.js +146 -0
  116. package/dist/validate-extends.d.ts.map +1 -1
  117. package/dist/validate-extends.js +37 -1
  118. package/dist/validate-module-artifact.d.ts +4 -2
  119. package/dist/validate-module-artifact.d.ts.map +1 -1
  120. package/dist/validate-module-artifact.js +43 -4
  121. package/dist/validate-native-entries.d.ts +15 -0
  122. package/dist/validate-native-entries.d.ts.map +1 -0
  123. package/dist/validate-native-entries.js +103 -0
  124. package/dist/validate-observed-state.d.ts +7 -1
  125. package/dist/validate-observed-state.d.ts.map +1 -1
  126. package/dist/validate-observed-state.js +24 -6
  127. package/dist/validate-provider-coherence.d.ts +4 -8
  128. package/dist/validate-provider-coherence.d.ts.map +1 -1
  129. package/dist/validate-provider-coherence.js +12 -123
  130. package/dist/validate-reference-forms.d.ts.map +1 -1
  131. package/dist/validate-reference-forms.js +2 -2
  132. package/dist/validate-references.d.ts.map +1 -1
  133. package/dist/validate-references.js +152 -65
  134. package/dist/validate-scope-reach.d.ts +31 -0
  135. package/dist/validate-scope-reach.d.ts.map +1 -0
  136. package/dist/validate-scope-reach.js +180 -0
  137. package/dist/validate-source-entries.d.ts +16 -0
  138. package/dist/validate-source-entries.d.ts.map +1 -0
  139. package/dist/validate-source-entries.js +90 -0
  140. package/dist/validate-template-body.d.ts +36 -0
  141. package/dist/validate-template-body.d.ts.map +1 -0
  142. package/dist/validate-template-body.js +246 -0
  143. package/dist/validate-throws-coverage.d.ts.map +1 -1
  144. package/dist/validate-throws-coverage.js +21 -39
  145. package/dist/value-type-keyword.d.ts +22 -5
  146. package/dist/value-type-keyword.d.ts.map +1 -1
  147. package/dist/value-type-keyword.js +16 -7
  148. package/package.json +4 -3
  149. package/src/analyzer.ts +97 -20
  150. package/src/artifact-axes.ts +24 -0
  151. package/src/artifact-layer-index.ts +9 -6
  152. package/src/artifact-selector.ts +110 -35
  153. package/src/builtins.ts +160 -113
  154. package/src/call-graph.ts +30 -0
  155. package/src/catch-scope.ts +1 -1
  156. package/src/cel-scope-query.ts +28 -9
  157. package/src/cel-scope.ts +1 -0
  158. package/src/eval-paths.ts +53 -6
  159. package/src/extends-resolution.ts +24 -0
  160. package/src/forwarded-declaration.ts +32 -0
  161. package/src/index.ts +37 -1
  162. package/src/kernel-globals.ts +28 -5
  163. package/src/migrations/CLAUDE.md +35 -0
  164. package/src/module-alias-scope.ts +17 -0
  165. package/src/module-file-claims.ts +63 -4
  166. package/src/module-named-files.ts +162 -0
  167. package/src/native-entries.ts +227 -0
  168. package/src/nearest-name.ts +47 -0
  169. package/src/normalize-inline-resources.ts +326 -70
  170. package/src/reference-field-map.ts +257 -52
  171. package/src/release/CLAUDE.md +32 -0
  172. package/src/release/destinations.ts +105 -0
  173. package/src/release/index.ts +33 -2
  174. package/src/release/ledger.ts +51 -21
  175. package/src/release/payload-digest.ts +2 -1
  176. package/src/release/release-plan.ts +16 -26
  177. package/src/release/workspace-config.ts +483 -47
  178. package/src/release/workspace-schema.ts +99 -0
  179. package/src/resolve-ref-sentinels.ts +7 -6
  180. package/src/resolve-throws-union.ts +87 -32
  181. package/src/resolve-zone-requirements.ts +2 -1
  182. package/src/rewrite-synthetic-origins.ts +107 -16
  183. package/src/schema-compat.ts +18 -4
  184. package/src/schema-error-report.ts +16 -4
  185. package/src/schema-walk.ts +162 -34
  186. package/src/scope-declarations.ts +98 -0
  187. package/src/source-entries.ts +502 -0
  188. package/src/step-bodies.ts +55 -0
  189. package/src/telo-version.ts +1 -1
  190. package/src/template-body.ts +25 -0
  191. package/src/validate-cel-context.ts +60 -18
  192. package/src/validate-exports.ts +185 -0
  193. package/src/validate-extends.ts +43 -0
  194. package/src/validate-module-artifact.ts +46 -3
  195. package/src/validate-native-entries.ts +147 -0
  196. package/src/validate-observed-state.ts +36 -7
  197. package/src/validate-provider-coherence.ts +17 -127
  198. package/src/validate-reference-forms.ts +2 -3
  199. package/src/validate-references.ts +151 -66
  200. package/src/validate-scope-reach.ts +213 -0
  201. package/src/validate-source-entries.ts +120 -0
  202. package/src/validate-template-body.ts +274 -0
  203. package/src/validate-throws-coverage.ts +24 -31
  204. package/src/value-type-keyword.ts +35 -9
  205. package/dist/validate-template-dispatch.d.ts +0 -27
  206. package/dist/validate-template-dispatch.d.ts.map +0 -1
  207. package/dist/validate-template-dispatch.js +0 -95
  208. package/src/validate-template-dispatch.ts +0 -99
@@ -6,7 +6,9 @@ import {
6
6
  parseCanonicalTypeSchemaId,
7
7
  } from "@telorun/sdk";
8
8
  import { KERNEL_BUILTINS } from "./builtins.js";
9
+ import { moduleAliasScope } from "./module-alias-scope.js";
9
10
  import { withRefSlotsAsReadings } from "./ref-slot-reading.js";
11
+ import { dispatchTargetOf } from "./template-body.js";
10
12
  // Where CEL is evaluated is one reader (`eval-paths.ts`), and the region half of
11
13
  // it moved there so the scope walk and the `x-telo-eval` walk answer the same
12
14
  // question in one place. Re-exported: this module is where every existing
@@ -51,6 +53,9 @@ export interface ContextResolveOpts {
51
53
  aliases?: {
52
54
  resolveKind(kind: string): string | undefined;
53
55
  };
56
+ /** Per-declaring-module alias tables, so a kind read off a forwarded
57
+ * definition resolves in the module that wrote it. */
58
+ aliasesByModule?: ReadonlyMap<string, { resolveKind(kind: string): string | undefined }>;
54
59
  allManifests?: Record<string, any>[];
55
60
  }
56
61
 
@@ -334,18 +339,20 @@ function collectionBindingWithheld(
334
339
  * Example: `properties.self.x-telo-context-from-root: "schema"` reads
335
340
  * `manifestRoot.schema` and uses it as the schema of the `self` CEL variable.
336
341
  *
337
- * - `x-telo-context-from-ref-kind`: reads a kind name from `manifestRoot.<refPath>`,
338
- * resolves it via the definition registry, and returns that kind's `<field>` schema
339
- * (e.g. `outputType`/`inputType`). Used to type `result` against the dispatch
340
- * target's declared output shape.
342
+ * - `x-telo-context-from-ref-kind`: reads a KIND from `manifestRoot.<refPath>` —
343
+ * a field holding a kind name, or a definition's dispatch slot holding a
344
+ * `!ref` to a `resources:` entry (whose kind it is) — and returns the
345
+ * `<field>` schema (e.g. `outputType`/`inputType`) the entry itself declares,
346
+ * else the one the kind's definition declares. Used to type `result` against
347
+ * the dispatch target's declared output shape.
341
348
  *
342
349
  * Syntax: `<refPath>#<field>` — slashes traverse the manifest tree.
343
350
  *
344
- * Example: `x-telo-context-from-ref-kind: "provide/kind#outputType"` reads
345
- * `manifestRoot.provide.kind` as a kind name, looks up the kind's Telo.Definition,
346
- * and returns the `outputType` schema.
351
+ * Example: `x-telo-context-from-ref-kind: "provide#outputType"` resolves
352
+ * `manifestRoot.provide` (`!ref source`) to the entry named `source`, and
353
+ * returns its `outputType` schema, or its kind's.
347
354
  *
348
- * Accepts either a single string or an array of strings. With an array, paths
355
+ * Accepts either a single string or an array of strings. With an array, slots
349
356
  * are tried in order and the first one that resolves to a usable schema wins —
350
357
  * used by `result:` to find its dispatch target under whichever entry-point
351
358
  * field (`provide:` or `invoke:`) the definition declares.
@@ -370,7 +377,8 @@ export function resolveContextAnnotations(
370
377
  const normalizedOpts: ContextResolveOpts = Array.isArray(opts)
371
378
  ? { allManifests: opts }
372
379
  : (opts ?? {});
373
- const { manifestRoot = manifestItem, defs, aliases, allManifests } = normalizedOpts;
380
+ const { manifestRoot = manifestItem, defs, aliases, aliasesByModule, allManifests } =
381
+ normalizedOpts;
374
382
 
375
383
  const from = schema["x-telo-context-from"] as string | undefined;
376
384
  if (from) {
@@ -459,26 +467,38 @@ export function resolveContextAnnotations(
459
467
  }
460
468
  }
461
469
  if (defs) {
470
+ // The kind is read off THIS document, so it is spelled in the alias scope
471
+ // of the module that declared it — a library's dispatch target is written
472
+ // through an import the consumer has no reason to have.
473
+ const scope = moduleAliasScope(
474
+ (manifestRoot as { metadata?: { module?: unknown } }).metadata,
475
+ aliases,
476
+ aliasesByModule,
477
+ );
462
478
  for (const fromRefKind of fromRefKinds) {
463
479
  const hashIdx = fromRefKind.indexOf("#");
464
480
  if (hashIdx <= 0) continue;
465
481
  const refPath = fromRefKind.slice(0, hashIdx);
466
482
  const field = fromRefKind.slice(hashIdx + 1);
467
- const kindValue = navigatePath(manifestRoot, refPath.split("/"));
468
- if (typeof kindValue !== "string" || kindValue.length === 0) continue;
469
- const canonical = aliases?.resolveKind(kindValue) ?? kindValue;
470
- const def = defs.resolve(canonical);
471
- const typeField = def
472
- ? (def as Record<string, unknown>)[field]
473
- : undefined;
483
+ const target = kindAtPath(manifestRoot, refPath);
484
+ if (!target) continue;
485
+ // The entry's own field first — a per-instance narrowing (`outputType:`
486
+ // on the entry) is what the kernel binds — then the kind's.
487
+ const own = target.entry?.[field];
488
+ const ownResolved = own !== undefined ? resolveTypeFieldToSchema(own, allManifests ?? []) : undefined;
489
+ if (ownResolved && typeof ownResolved === "object") return ownResolved;
490
+ const canonical = scope?.resolveKind(target.kind) ?? target.kind;
491
+ const def = defs.resolve(canonical) ?? defs.resolve(target.kind);
492
+ const typeField = def ? (def as Record<string, unknown>)[field] : undefined;
474
493
  const resolved = resolveTypeFieldToSchema(typeField, allManifests ?? []);
475
494
  if (resolved && typeof resolved === "object") {
476
495
  return resolved;
477
496
  }
478
497
  }
479
498
  }
480
- // Open fallback so unresolved types never produce false-positive CEL diagnostics.
481
- return { type: "object", additionalProperties: true };
499
+ // Untyped (`dyn`) so an unresolved type never produces a false-positive CEL
500
+ // diagnostic; an open object would still type as `map` and refuse `acc + x`.
501
+ return {};
482
502
  }
483
503
 
484
504
  const refFrom = schema["x-telo-context-ref-from"] as string | undefined;
@@ -597,6 +617,28 @@ function navigatePath(obj: unknown, segments: string[]): unknown {
597
617
  return cur;
598
618
  }
599
619
 
620
+ /** The kind an `x-telo-context-from-ref-kind` path names: a field holding a
621
+ * kind NAME (`targetKind`), or a definition's dispatch slot holding a `!ref`
622
+ * to a `resources:` entry, which carries the kind — and, where it narrows
623
+ * one, the contract field itself. */
624
+ export function kindAtPath(
625
+ manifestRoot: Record<string, any>,
626
+ refPath: string,
627
+ ): { kind: string; entry?: Record<string, any> } | undefined {
628
+ const segments = refPath.split("/");
629
+ const value = navigatePath(manifestRoot, segments);
630
+ if (typeof value === "string") return value.length > 0 ? { kind: value } : undefined;
631
+ // The dispatch-slot reading is a fact about ONE built-in document, not about
632
+ // the annotation: `resources:` is a `Telo.Definition`'s entry list. The
633
+ // annotation itself is generic vocabulary any kind may declare, and without
634
+ // this gate a third-party kind whose own `resources:` array means something
635
+ // else would have a slot silently resolved against it — a wrong answer with
636
+ // nothing reporting it, which is worse than no answer.
637
+ if (segments.length !== 1 || manifestRoot.kind !== "Telo.Definition") return undefined;
638
+ const dispatch = dispatchTargetOf(manifestRoot, segments[0]!);
639
+ return dispatch ? { kind: dispatch.kind, entry: dispatch.entry } : undefined;
640
+ }
641
+
600
642
  /**
601
643
  * Walk a JSON Schema tree and collect all `x-telo-context` annotations,
602
644
  * returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
@@ -0,0 +1,185 @@
1
+ import { nearestName } from "./nearest-name.js";
2
+ import type { ResourceManifest } from "@telorun/sdk";
3
+ import type { AliasResolver } from "./alias-resolver.js";
4
+ import type { DefinitionRegistry } from "./definition-registry.js";
5
+ import { parseExportEntry } from "./flatten-for-analyzer.js";
6
+ import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
7
+
8
+ const SOURCE = "telo-analyzer";
9
+
10
+ /**
11
+ * A LIBRARY'S EXPORT LIST IS CHECKED WHERE IT IS WRITTEN.
12
+ *
13
+ * `exports.kinds` and `exports.resources` are a library's public contract, and
14
+ * they were read as a GATE only: a listed name resolved when a consumer asked
15
+ * for it, and one that named nothing failed in the consumer's file
16
+ * (`UNDEFINED_KIND` on `kind: L.Client`, with nothing wrong in that file) while
17
+ * the author who could fix it saw a clean check. Both entries are resolved here,
18
+ * against what the library declares:
19
+ *
20
+ * - a bare kind names a `Telo.Definition` / `Telo.Abstract` of this module, and
21
+ * `Alias.Kind` re-exports through an import declared in this file
22
+ * (`EXPORT_KIND_UNKNOWN`). The one mistake that reads as intended — a bare
23
+ * name that IS a kind, of an imported module — gets the re-export spelling
24
+ * in its message, because a clean check was read as confirmation it worked;
25
+ * - a bare instance names a resource declared in this module, and
26
+ * `Alias.name` an instance the aliased import exports
27
+ * (`EXPORT_RESOURCE_UNKNOWN`).
28
+ *
29
+ * Runs on the library as an ENTRY — a consumer's flattened analysis drops the
30
+ * library doc, and the list is not the consumer's to fix. Browser-safe.
31
+ */
32
+ export function validateExports(
33
+ manifests: ResourceManifest[],
34
+ registry: DefinitionRegistry,
35
+ aliases: AliasResolver,
36
+ rootModules: ReadonlySet<string>,
37
+ ): AnalysisDiagnostic[] {
38
+ const out: AnalysisDiagnostic[] = [];
39
+
40
+ for (const lib of manifests) {
41
+ if (lib.kind !== "Telo.Library") continue;
42
+ const moduleName = lib.metadata?.name as string | undefined;
43
+ if (!moduleName || !rootModules.has(moduleName)) continue;
44
+ const exports = (lib as { exports?: { kinds?: unknown; resources?: unknown } }).exports;
45
+ if (!exports || typeof exports !== "object") continue;
46
+ const filePath = (lib.metadata as { source?: string } | undefined)?.source;
47
+ const resource = { kind: lib.kind, name: moduleName };
48
+
49
+ const own = (m: ResourceManifest): boolean => {
50
+ const mod = (m.metadata as { module?: string } | undefined)?.module;
51
+ return mod === undefined || mod === moduleName;
52
+ };
53
+ const declaredKinds = new Set<string>();
54
+ const instances = new Set<string>();
55
+ const importAliases = new Map<string, string | undefined>();
56
+ const forwardedByModule = new Map<string, Set<string>>();
57
+ for (const m of manifests) {
58
+ const name = m.metadata?.name;
59
+ if (typeof name !== "string") continue;
60
+ const meta = m.metadata as {
61
+ module?: string;
62
+ forwardedExport?: boolean;
63
+ resolvedModuleName?: string;
64
+ };
65
+ if (meta.forwardedExport && meta.module) {
66
+ let set = forwardedByModule.get(meta.module);
67
+ if (!set) forwardedByModule.set(meta.module, (set = new Set()));
68
+ set.add(name);
69
+ continue;
70
+ }
71
+ if (!own(m)) continue;
72
+ if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") declaredKinds.add(name);
73
+ else if (m.kind === "Telo.Import") importAliases.set(name, meta.resolvedModuleName);
74
+ else if (m.kind !== "Telo.Library" && m.kind !== "Telo.Application") instances.add(name);
75
+ }
76
+
77
+ const report = (code: string, path: string, message: string, fix?: string) =>
78
+ out.push({
79
+ severity: DiagnosticSeverity.Error,
80
+ code,
81
+ source: SOURCE,
82
+ message: `Telo.Library/${moduleName}: ${message}`,
83
+ data: { resource, filePath, path, ...(fix ? { fix: { replacement: fix } } : {}) },
84
+ });
85
+
86
+ const kinds = exports.kinds;
87
+ if (Array.isArray(kinds)) {
88
+ kinds.forEach((entry, i) => {
89
+ if (typeof entry !== "string") return;
90
+ const path = `exports.kinds[${i}]`;
91
+ const { alias, name } = parseExportEntry(entry);
92
+ if (alias) {
93
+ if (!importAliases.has(alias)) {
94
+ report(
95
+ "EXPORT_KIND_UNKNOWN",
96
+ path,
97
+ `'exports.kinds: ${entry}' re-exports through '${alias}', which is not an import ` +
98
+ `of this library. Imports: ${[...importAliases.keys()].join(", ") || "(none)"}.`,
99
+ );
100
+ return;
101
+ }
102
+ const canonical = aliases.resolveKind(entry);
103
+ if (canonical && !registry.resolve(canonical)) {
104
+ report(
105
+ "EXPORT_KIND_UNKNOWN",
106
+ path,
107
+ `'exports.kinds: ${entry}' names no kind '${name}' exported by '${alias}'.`,
108
+ );
109
+ }
110
+ return;
111
+ }
112
+ if (declaredKinds.has(name)) return;
113
+ // The natural first attempt at a re-export: the imported kind's bare
114
+ // suffix. It resolves as a kind — just not as one this library owns.
115
+ const viaImport = [...importAliases.keys()].find((a) => {
116
+ const canonical = aliases.resolveKind(`${a}.${name}`);
117
+ return canonical !== undefined && registry.resolve(canonical) !== undefined;
118
+ });
119
+ if (viaImport) {
120
+ report(
121
+ "EXPORT_KIND_UNKNOWN",
122
+ path,
123
+ `'exports.kinds: ${name}' is not a kind this library declares — it is ` +
124
+ `'${viaImport}.${name}', an imported kind. Re-export it as '${viaImport}.${name}', ` +
125
+ `or declare a local kind that extends it.`,
126
+ `${viaImport}.${name}`,
127
+ );
128
+ return;
129
+ }
130
+ const suggestion = nearestName(name, [...declaredKinds]);
131
+ report(
132
+ "EXPORT_KIND_UNKNOWN",
133
+ path,
134
+ `'exports.kinds: ${name}' names no Telo.Definition or Telo.Abstract of this library. ` +
135
+ `Declared: ${[...declaredKinds].join(", ") || "(none)"}.` +
136
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""),
137
+ suggestion,
138
+ );
139
+ });
140
+ }
141
+
142
+ const resources = exports.resources;
143
+ if (Array.isArray(resources)) {
144
+ resources.forEach((entry, i) => {
145
+ if (typeof entry !== "string") return;
146
+ const path = `exports.resources[${i}]`;
147
+ const { alias, name } = parseExportEntry(entry);
148
+ if (alias) {
149
+ if (!importAliases.has(alias)) {
150
+ report(
151
+ "EXPORT_RESOURCE_UNKNOWN",
152
+ path,
153
+ `'exports.resources: ${entry}' re-exports through '${alias}', which is not an ` +
154
+ `import of this library. Imports: ${[...importAliases.keys()].join(", ") || "(none)"}.`,
155
+ );
156
+ return;
157
+ }
158
+ const targetModule = importAliases.get(alias);
159
+ const exported = targetModule ? forwardedByModule.get(targetModule) : undefined;
160
+ if (exported && !exported.has(name)) {
161
+ report(
162
+ "EXPORT_RESOURCE_UNKNOWN",
163
+ path,
164
+ `'exports.resources: ${entry}' names no instance '${name}' exported by '${alias}'. ` +
165
+ `Exported: ${[...exported].join(", ") || "(none)"}.`,
166
+ );
167
+ }
168
+ return;
169
+ }
170
+ if (instances.has(name)) return;
171
+ const suggestion = nearestName(name, [...instances]);
172
+ report(
173
+ "EXPORT_RESOURCE_UNKNOWN",
174
+ path,
175
+ `'exports.resources: ${name}' names no resource declared in this library. ` +
176
+ `Declared: ${[...instances].join(", ") || "(none)"}.` +
177
+ (suggestion ? ` Did you mean '${suggestion}'?` : ""),
178
+ suggestion,
179
+ );
180
+ });
181
+ }
182
+ }
183
+
184
+ return out;
185
+ }
@@ -4,6 +4,7 @@ import type { DefinitionRegistry } from "./definition-registry.js";
4
4
  import {
5
5
  controllerBearingAncestor,
6
6
  effectiveAuthorSchema,
7
+ hasOwnControllerOrTemplate,
7
8
  inheritedCapability,
8
9
  isInheritedDelegation,
9
10
  type DefResolver,
@@ -74,6 +75,44 @@ export function validateExtends(
74
75
  const resource = { kind: m.kind, name };
75
76
  const label = `${m.kind}/${name}`;
76
77
 
78
+ // `base:` is read on exactly one path — the inherited-controller one, which
79
+ // the kernel takes only when the definition has NO body of its own. A
80
+ // `resources:` block or a dispatch slot selects the template path, and
81
+ // `base:` is then never evaluated: the kind silently became an empty
82
+ // template, and the failure landed in the CONSUMER's resource as a `{}`
83
+ // where a parent instance should have been. Refused here, where the two
84
+ // blocks sit side by side, and at registration in the kernel.
85
+ //
86
+ // WHETHER there is a body is `hasOwnControllerOrTemplate` — the same
87
+ // predicate the kernel branches on, so the two halves cannot disagree about
88
+ // what a body is. Deciding it here with a key scan did disagree: an empty
89
+ // `resources: []` is falsy to a length test and truthy to the kernel's, so
90
+ // it passed `telo check` and threw `ERR_BASE_WITH_TEMPLATE_BODY` at boot —
91
+ // a brand-new guard shipping with the very gap it was written to close. The
92
+ // scan survives for one job only: naming the offending key in the message.
93
+ if ((m as { base?: unknown }).base != null) {
94
+ const hasBody = hasOwnControllerOrTemplate(m as ResourceDefinition);
95
+ const bodyKey =
96
+ (["resources", "controllers", "invoke", "run", "provide", "mount"] as const).find(
97
+ (key) => (m as Record<string, unknown>)[key] !== undefined,
98
+ ) ?? "resources";
99
+ if (hasBody) {
100
+ diagnostics.push({
101
+ severity: DiagnosticSeverity.Error,
102
+ code: "BASE_WITH_TEMPLATE_BODY",
103
+ source: SOURCE,
104
+ message:
105
+ `${label}: 'base:' maps this kind's config onto the inherited controller of ` +
106
+ `'${typeof extendsOf(m) === "string" ? extendsOf(m) : "<no extends>"}', so the ` +
107
+ `definition may not also declare '${bodyKey}:'. With '${bodyKey}:' the kind is a ` +
108
+ `template and 'base:' is never evaluated. Either drop 'base:' and dispatch to a ` +
109
+ `'resources:' entry with '!ref', or drop '${bodyKey}:' and build the parent's config ` +
110
+ `in 'base:' alone.`,
111
+ data: { resource, filePath, path: bodyKey },
112
+ });
113
+ }
114
+ }
115
+
77
116
  // --- extends validation ---
78
117
  // At this point `m.extends` is whatever the manifest declared (alias form, e.g.
79
118
  // "Ai.Model"). Resolve through the AliasResolver to the canonical form before
@@ -243,3 +282,7 @@ export function validateExtends(
243
282
 
244
283
  return diagnostics;
245
284
  }
285
+
286
+ function extendsOf(m: ResourceManifest): unknown {
287
+ return (m as { extends?: unknown }).extends;
288
+ }
@@ -4,6 +4,7 @@ import { parseLayerIndex, LayerIndexError } from "./artifact-layer-index.js";
4
4
  import {
5
5
  ArtifactSelectorError,
6
6
  PLATFORM_AXES,
7
+ selectorContradictions,
7
8
  selectorFromQualifiers,
8
9
  selectorKey,
9
10
  } from "./artifact-selector.js";
@@ -26,10 +27,12 @@ const SOURCE = "telo-analyzer";
26
27
  * selector (spec §1), which is what every module with two `js` controllers relies
27
28
  * on.
28
29
  *
29
- * 1. **Controller selector qualifiers.** `os` / `arch` / `libc` / `siblings` are
30
+ * 1. **Controller selector qualifiers.** The platform axes and `siblings` are
30
31
  * authored surface. An unknown qualifier is reported rather than ignored, since
31
32
  * ignoring is what makes a typo invisible; an invalid value is reported here
32
- * instead of throwing from the loader later.
33
+ * instead of throwing from the loader later, and so is a combination no host
34
+ * can match (`selectorContradictions`, shared with `native:` and
35
+ * `exports.code:`).
33
36
  * 2. **The published layer index.** The owner doc's JSON Schema covers shape; the
34
37
  * semantic rules — controller-requires-selector, singletons carry none, no
35
38
  * duplicate selector, the token grammar (`os: Linux` passes the schema and
@@ -79,6 +82,16 @@ function validateLibraryCandidates(manifest: ResourceManifest, out: AnalysisDiag
79
82
  // mean a consumer's import resolves by whichever candidate is read first.
80
83
  const seen = new Map<string, LibraryCandidate>();
81
84
  for (const candidate of candidates) {
85
+ for (const contradiction of selectorContradictions(candidate.selector)) {
86
+ out.push({
87
+ severity: DiagnosticSeverity.Error,
88
+ code: `LIBRARY_${contradiction.rule}`,
89
+ source: SOURCE,
90
+ message:
91
+ `Telo.Library/${metadata?.name ?? "(unnamed)"}: ${candidate.origin}: ${contradiction.detail}`,
92
+ data: { resource, filePath: metadata?.source, path: "exports/code" },
93
+ });
94
+ }
82
95
  const key = selectorKey(candidate.selector);
83
96
  const first = seen.get(key);
84
97
  if (first) {
@@ -162,7 +175,37 @@ function validateControllerSelectors(
162
175
  // Validate the selector; the value is not otherwise needed here, since
163
176
  // candidates sharing a selector legitimately share a layer.
164
177
  try {
165
- selectorFromQualifiers(parsed.format, parsed.qualifiers, candidate);
178
+ const selector = selectorFromQualifiers(parsed.format, parsed.qualifiers, candidate);
179
+ for (const contradiction of selectorContradictions(selector)) {
180
+ out.push({
181
+ severity: DiagnosticSeverity.Error,
182
+ code: `CONTROLLER_${contradiction.rule}`,
183
+ source: SOURCE,
184
+ message:
185
+ `${manifest.kind}/${name ?? "(unnamed)"}: controller candidate "${candidate}": ` +
186
+ contradiction.detail,
187
+ data: { resource, filePath, path: `${at}?${contradiction.axis}` },
188
+ });
189
+ }
190
+ // A `dylib` opens over the Rust controller ABI, and a library built
191
+ // against another version of it must never match — so the candidate states
192
+ // which, in the `telo` family.
193
+ if (parsed.format === "dylib" && !selector.abi?.startsWith("telo-")) {
194
+ out.push({
195
+ severity: DiagnosticSeverity.Error,
196
+ code: "CONTROLLER_DYLIB_ABI_MISSING",
197
+ source: SOURCE,
198
+ message:
199
+ `${manifest.kind}/${name ?? "(unnamed)"}: dylib candidate "${candidate}" ` +
200
+ (selector.abi === undefined
201
+ ? `states no abi. `
202
+ : `states abi=${selector.abi}, which is not a Rust controller ABI. `) +
203
+ `A dylib is built against one version of the Rust controller ABI and must say which — ` +
204
+ `add abi=telo-<version>, e.g. abi=telo-2. Without it the Rust kernel downloads the ` +
205
+ `library on every host and refuses it only when it opens it.`,
206
+ data: { resource, filePath, path: at },
207
+ });
208
+ }
166
209
  } catch (err) {
167
210
  if (!(err instanceof ArtifactSelectorError)) throw err;
168
211
  out.push({
@@ -0,0 +1,147 @@
1
+ import type { ResourceManifest } from "@telorun/sdk";
2
+
3
+ import { describeSelector, selectorContradictions, selectorKey } from "./artifact-selector.js";
4
+ import { moduleDocumentClaims } from "./module-file-claims.js";
5
+ import { describeClaim, nativeClaimConflicts } from "./module-named-files.js";
6
+ import { readNativeEntries, type NativeEntry } from "./native-entries.js";
7
+ import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
8
+
9
+ const SOURCE = "telo-analyzer";
10
+
11
+ /**
12
+ * The strict half of `native-entries.ts`: what `telo check` reports about a
13
+ * module doc's `native:` block.
14
+ *
15
+ * Every rule is decidable from the manifest and would otherwise surface only on
16
+ * a consumer's host, as a native file that never matches or a layer that
17
+ * overwrites another's file. Shape errors (a missing key, an unknown one) are
18
+ * left to the owner doc's JSON Schema, which reports them in the same pass.
19
+ *
20
+ * Entry-module-scoped: a dependency's block is not the consumer's to fix.
21
+ */
22
+ export function validateNativeEntries(
23
+ manifests: ResourceManifest[],
24
+ entryModules?: ReadonlySet<string>,
25
+ ): AnalysisDiagnostic[] {
26
+ const out: AnalysisDiagnostic[] = [];
27
+ for (const manifest of manifests) {
28
+ if (manifest.kind !== "Telo.Application" && manifest.kind !== "Telo.Library") continue;
29
+ const metadata = (manifest.metadata ?? {}) as Record<string, unknown>;
30
+ const name = typeof metadata.name === "string" ? metadata.name : undefined;
31
+ const owned =
32
+ entryModules === undefined ||
33
+ (typeof metadata.module === "string"
34
+ ? entryModules.has(metadata.module)
35
+ : name === undefined || entryModules.has(name));
36
+ if (!owned) continue;
37
+
38
+ const label = `${manifest.kind}/${name ?? "(unnamed)"}`;
39
+ const report = (code: string, path: string, message: string) =>
40
+ out.push({
41
+ severity: DiagnosticSeverity.Error,
42
+ code,
43
+ source: SOURCE,
44
+ message: `${label}: ${message}`,
45
+ data: {
46
+ resource: { kind: manifest.kind, name },
47
+ filePath: typeof metadata.source === "string" ? metadata.source : undefined,
48
+ path,
49
+ },
50
+ });
51
+
52
+ const { entries, problems } = readNativeEntries(manifest);
53
+ for (const problem of problems) {
54
+ if (problem.kind === "shape") continue;
55
+ report(
56
+ problem.kind === "escape" ? "NATIVE_PATH_ESCAPES_MODULE" : "NATIVE_ENTRY_INVALID",
57
+ problem.path,
58
+ problem.message,
59
+ );
60
+ }
61
+ if (entries.length === 0) continue;
62
+
63
+ const moduleName = typeof metadata.module === "string" ? metadata.module : name;
64
+ for (const { entry, claim } of nativeClaimConflicts(
65
+ entries,
66
+ moduleDocumentClaims(manifests, moduleName),
67
+ )) {
68
+ report(
69
+ "NATIVE_PATH_CLAIMED",
70
+ `native[${entry.index}].path`,
71
+ `${entry.origin}: path '${entry.path}' is also named by ${describeClaim(claim)}. A native ` +
72
+ `file ships only in its platform's native layer and a file extracts from exactly one ` +
73
+ `layer, so publish refuses it — give the native entry its own file, or drop the other ` +
74
+ `declaration.`,
75
+ );
76
+ }
77
+
78
+ const bySelector = new Map<string, NativeEntry>();
79
+ const byPath = new Map<string, NativeEntry>();
80
+ for (const entry of entries) {
81
+ const at = `native[${entry.index}]`;
82
+ if (entry.selector.format === "node" && entry.selector.abi === undefined) {
83
+ report(
84
+ "NATIVE_NODE_ABI_MISSING",
85
+ at,
86
+ `${entry.origin}: a 'node' addon is built against one Node ABI and must state it — ` +
87
+ `add abi: node-<NODE_MODULE_VERSION>, e.g. 'node-137'. Without it the file matches ` +
88
+ `every Node release and fails to load on all but one.`,
89
+ );
90
+ }
91
+ for (const contradiction of selectorContradictions(entry.selector)) {
92
+ report(
93
+ `NATIVE_${contradiction.rule}`,
94
+ `${at}.${contradiction.axis}`,
95
+ `${entry.origin}: ${contradiction.detail}`,
96
+ );
97
+ }
98
+
99
+ const key = `${entry.name}\0${selectorKey(entry.selector)}`;
100
+ const twin = bySelector.get(key);
101
+ if (twin) {
102
+ report(
103
+ "NATIVE_ENTRY_DUPLICATE",
104
+ at,
105
+ `${entry.origin}: ${twin.origin} already declares '${entry.name}' for ` +
106
+ `${describeSelector(entry.selector)}. A name has one entry per platform tuple, or ` +
107
+ `a lookup by name could resolve to either file.`,
108
+ );
109
+ } else {
110
+ bySelector.set(key, entry);
111
+ }
112
+
113
+ const sharer = byPath.get(entry.path);
114
+ if (sharer && selectorKey(sharer.selector) !== selectorKey(entry.selector)) {
115
+ report(
116
+ "NATIVE_PATH_SHARED",
117
+ `${at}.path`,
118
+ `${entry.origin}: path '${entry.path}' is also declared by ${sharer.origin} for ` +
119
+ `${describeSelector(sharer.selector)}. Every layer of a module extracts into one ` +
120
+ `directory, so each platform tuple needs its own path — e.g. ` +
121
+ `'native/<os>-<arch>/…'.`,
122
+ );
123
+ } else if (!sharer) {
124
+ byPath.set(entry.path, entry);
125
+ }
126
+
127
+ const through = [...byPath.values()].find(
128
+ (other) =>
129
+ other !== entry &&
130
+ (entry.path.startsWith(`${other.path}/`) || other.path.startsWith(`${entry.path}/`)),
131
+ );
132
+ if (through) {
133
+ const relation = entry.path.startsWith(`${through.path}/`)
134
+ ? `runs through '${through.path}' as a directory`
135
+ : `is a directory that '${through.path}' runs through`;
136
+ report(
137
+ "NATIVE_PATH_NESTED",
138
+ `${at}.path`,
139
+ `${entry.origin}: path '${entry.path}' ${relation}, and ${through.origin} declares ` +
140
+ `'${through.path}'. Every layer of a module extracts into one directory, so a path ` +
141
+ `cannot be both a file and a directory — give each entry its own path.`,
142
+ );
143
+ }
144
+ }
145
+ }
146
+ return out;
147
+ }