@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
@@ -3,7 +3,8 @@
3
3
  * controller candidate is chosen by, and the key a controller layer of a module
4
4
  * artifact is stored under.
5
5
  *
6
- * A selector is `format` plus the optional platform axes `os` / `arch` / `libc`.
6
+ * A selector is `format` plus the optional platform axes, whose vocabulary is
7
+ * data (`analyzer/artifact-axes/axes.json`, generated into `PLATFORM_AXES`).
7
8
  * Matching is one rule, applied per axis: an axis the selector omits accepts
8
9
  * anything, an axis it states must be equal. That is what lets a `js` controller
9
10
  * be platform-neutral and a `napi` controller be pinned to one triple, with no
@@ -25,11 +26,20 @@
25
26
  * published into OCI descriptors.
26
27
  */
27
28
 
28
- /** The role a layer plays in a module artifact. `controller` and `library` layers
29
- * carry a selector; `assets` and `common` are singletons and carry none. */
30
- export type LayerRole = "controller" | "library" | "assets" | "common";
29
+ import { AXIS_VALUE_FORMS, PLATFORM_AXES, type PlatformAxis } from "./artifact-axes.js";
31
30
 
32
- export const LAYER_ROLES: readonly LayerRole[] = ["controller", "library", "assets", "common"];
31
+ /** The role a layer plays in a module artifact. `controller`, `library` and
32
+ * `native` layers carry a selector; `assets` and `common` are singletons and
33
+ * carry none. */
34
+ export type LayerRole = "controller" | "library" | "native" | "assets" | "common";
35
+
36
+ export const LAYER_ROLES: readonly LayerRole[] = [
37
+ "controller",
38
+ "library",
39
+ "native",
40
+ "assets",
41
+ "common",
42
+ ];
33
43
 
34
44
  export function isLayerRole(value: unknown): value is LayerRole {
35
45
  return typeof value === "string" && (LAYER_ROLES as readonly string[]).includes(value);
@@ -42,33 +52,28 @@ export function isLayerRole(value: unknown): value is LayerRole {
42
52
  * A singleton would be wrong the moment a second runtime ships. */
43
53
  export const CODE_LAYER_ROLES: readonly LayerRole[] = ["controller", "library"];
44
54
 
55
+ /** Every role keyed by a selector, one layer per selector: the code roles, plus
56
+ * `native` — a platform-specific file the runtime does not import as code,
57
+ * declared in the module doc's `native:` block. */
58
+ const SELECTOR_LAYER_ROLES: readonly LayerRole[] = [...CODE_LAYER_ROLES, "native"];
59
+
45
60
  export function roleCarriesSelector(role: LayerRole): boolean {
46
- return (CODE_LAYER_ROLES as readonly string[]).includes(role);
61
+ return (SELECTOR_LAYER_ROLES as readonly string[]).includes(role);
47
62
  }
48
63
 
49
- /** The platform axes, in canonical order. Not a closed vocabulary of *values* —
50
- * new architectures appear without a Telo release — only of axis names. */
51
- export const PLATFORM_AXES = ["os", "arch", "libc"] as const;
64
+ export { PLATFORM_AXES, type PlatformAxis };
52
65
 
53
- export type PlatformAxis = (typeof PLATFORM_AXES)[number];
54
-
55
- export interface ArtifactSelector {
66
+ export interface ArtifactSelector extends Partial<Record<PlatformAxis, string>> {
56
67
  /** Bundled controller format: the PURL name segment (`js`, `napi`, `wasm`, …). */
57
68
  format: string;
58
- os?: string;
59
- arch?: string;
60
- libc?: string;
61
69
  }
62
70
 
63
71
  /** What a selector is matched against: the host the kernel runs on, or the
64
- * target `telo install --platform` is warming a cache for. An axis left
65
- * undetermined (a host whose libc cannot be detected) matches no selector that
66
- * constrains it — refusing to load is the safe direction for a native binary. */
67
- export interface PlatformTarget {
72
+ * target `telo install` is warming a cache for. An axis left undetermined (a
73
+ * host whose libc cannot be detected) matches no selector that constrains it —
74
+ * refusing to load is the safe direction for a native binary. */
75
+ export interface PlatformTarget extends Partial<Record<PlatformAxis, string>> {
68
76
  format?: string;
69
- os?: string;
70
- arch?: string;
71
- libc?: string;
72
77
  }
73
78
 
74
79
  export class ArtifactSelectorError extends Error {
@@ -84,7 +89,9 @@ export class ArtifactSelectorError extends Error {
84
89
  * platform written two ways is one layer rather than two. */
85
90
  const TOKEN = /^[a-z0-9][a-z0-9_.-]*$/;
86
91
 
87
- function normalizeToken(axis: string, raw: unknown, describe: string): string {
92
+ /** Validate and normalize one selector value: the shared token grammar, plus the
93
+ * axis's own value form where the vocabulary declares one. */
94
+ export function normalizeAxisValue(axis: string, raw: unknown, describe: string): string {
88
95
  if (typeof raw !== "string") {
89
96
  throw new ArtifactSelectorError(
90
97
  `${describe}: ${axis} must be a string, got ${raw === null ? "null" : typeof raw}.`,
@@ -97,6 +104,13 @@ function normalizeToken(axis: string, raw: unknown, describe: string): string {
97
104
  `Use lowercase letters, digits, '.', '-' or '_', starting with a letter or digit.`,
98
105
  );
99
106
  }
107
+ const valueForm = AXIS_VALUE_FORMS[axis as PlatformAxis];
108
+ if (valueForm && !valueForm.pattern.test(value)) {
109
+ throw new ArtifactSelectorError(
110
+ `${describe}: ${axis} value '${raw}' must have the form ${valueForm.form}, ` +
111
+ `e.g. ${valueForm.examples.map((e) => `'${e}'`).join(" or ")}.`,
112
+ );
113
+ }
100
114
  return value;
101
115
  }
102
116
 
@@ -111,35 +125,38 @@ export function selectorFromQualifiers(
111
125
  describe = "controller selector",
112
126
  ): ArtifactSelector {
113
127
  const selector: ArtifactSelector = {
114
- format: normalizeToken("format", format, describe),
128
+ format: normalizeAxisValue("format", format, describe),
115
129
  };
116
130
  for (const axis of PLATFORM_AXES) {
117
131
  const raw = qualifiers?.[axis];
118
132
  if (raw === undefined || raw === "") continue;
119
- selector[axis] = normalizeToken(axis, raw, describe);
133
+ selector[axis] = normalizeAxisValue(axis, raw, describe);
120
134
  }
121
135
  return selector;
122
136
  }
123
137
 
124
- /** Validate and normalize a selector read off a published layer index. */
138
+ /**
139
+ * Validate and normalize a selector read off a published layer index.
140
+ *
141
+ * Returns undefined when the selector carries an axis this runtime does not
142
+ * know: the layer is for a newer runtime, and the caller skips it whole. The
143
+ * unknown axis is never dropped — two layers differing only in it would then
144
+ * claim one address. The known axes are still validated, since their grammar
145
+ * does not change with the axis set.
146
+ */
125
147
  export function normalizeSelector(
126
148
  value: unknown,
127
149
  describe = "layer selector",
128
- ): ArtifactSelector {
150
+ ): ArtifactSelector | undefined {
129
151
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
130
152
  throw new ArtifactSelectorError(`${describe}: expected an object of selector axes.`);
131
153
  }
132
154
  const record = value as Record<string, unknown>;
133
- const unknown = Object.keys(record).filter(
155
+ const selector = selectorFromQualifiers(record.format, record, describe);
156
+ const carriesUnknownAxis = Object.keys(record).some(
134
157
  (k) => k !== "format" && !(PLATFORM_AXES as readonly string[]).includes(k),
135
158
  );
136
- if (unknown.length > 0) {
137
- throw new ArtifactSelectorError(
138
- `${describe}: unknown selector ${unknown.length === 1 ? "axis" : "axes"} ` +
139
- `${unknown.map((k) => `'${k}'`).join(", ")}. Known axes: format, ${PLATFORM_AXES.join(", ")}.`,
140
- );
141
- }
142
- return selectorFromQualifiers(record.format, record, describe);
159
+ return carriesUnknownAxis ? undefined : selector;
143
160
  }
144
161
 
145
162
  /**
@@ -180,3 +197,61 @@ export function selectorMatches(selector: ArtifactSelector, target: PlatformTarg
180
197
  }
181
198
  return true;
182
199
  }
200
+
201
+ /**
202
+ * The axes `selector` constrains and `target` leaves undetermined, when those
203
+ * alone keep the two from matching — what a warm skips for want of a value rather
204
+ * than because the selector is for another platform. `undefined` when the
205
+ * selector matches, or differs on a determined axis.
206
+ */
207
+ export function undeterminedAxesBlockingMatch(
208
+ selector: ArtifactSelector,
209
+ target: PlatformTarget,
210
+ ): PlatformAxis[] | undefined {
211
+ if (target.format !== undefined && selector.format !== target.format) return undefined;
212
+ const axes: PlatformAxis[] = [];
213
+ for (const axis of PLATFORM_AXES) {
214
+ const constraint = selector[axis];
215
+ if (constraint === undefined) continue;
216
+ if (target[axis] === undefined) axes.push(axis);
217
+ else if (target[axis] !== constraint) return undefined;
218
+ }
219
+ return axes.length > 0 ? axes : undefined;
220
+ }
221
+
222
+ /** A selector that no host can match as its author meant it. */
223
+ export interface SelectorContradiction {
224
+ /** Suffix of the diagnostic code; each surface prefixes its own. */
225
+ readonly rule: "NAPI_ABI_FORBIDDEN" | "LIBC_OFF_LINUX";
226
+ readonly axis: PlatformAxis;
227
+ readonly detail: string;
228
+ }
229
+
230
+ /**
231
+ * The combinations of axes the grammar accepts and no host can mean: an N-API
232
+ * addon stating a runtime ABI, and a libc on an os that has none. Shared by every
233
+ * surface that authors a selector — `native:` entries, controller candidates and
234
+ * `exports.code:` entries — so each reports the same rule under its own prefix.
235
+ */
236
+ export function selectorContradictions(selector: ArtifactSelector): SelectorContradiction[] {
237
+ const out: SelectorContradiction[] = [];
238
+ if (selector.format === "napi" && selector.abi !== undefined) {
239
+ out.push({
240
+ rule: "NAPI_ABI_FORBIDDEN",
241
+ axis: "abi",
242
+ detail:
243
+ `an N-API addon is ABI-stable across runtime releases and states no abi — remove ` +
244
+ `abi ${selector.abi}, which would keep it from loading anywhere else.`,
245
+ });
246
+ }
247
+ if (selector.libc !== undefined && selector.os !== undefined && selector.os !== "linux") {
248
+ out.push({
249
+ rule: "LIBC_OFF_LINUX",
250
+ axis: "libc",
251
+ detail:
252
+ `libc is only determined on linux, so a selector for os '${selector.os}' that states ` +
253
+ `libc ${selector.libc} could never match a host. Remove libc.`,
254
+ });
255
+ }
256
+ return out;
257
+ }
package/src/builtins.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { PLATFORM_AXES } from "./artifact-axes.js";
1
2
  import { manifestFragment, manifestFragmentRef, withSchemaFragments } from "./manifest-schemas.js";
2
3
 
3
4
  /** A slot holding author-written JSON Schema. Localized and hoisted by
@@ -23,6 +24,62 @@ const observedStateSlot = {
23
24
  "What a resource of this kind reports while running, published at `resources.<name>.status.<field>`.",
24
25
  $ref: manifestFragmentRef("JsonSchema7"),
25
26
  };
27
+
28
+ /** The `self`-only CEL scope a dispatch slot's name template is written in. */
29
+ const dispatchSelfContext = {
30
+ type: "object",
31
+ additionalProperties: false,
32
+ properties: { self: { "x-telo-context-from-root": "schema" } },
33
+ };
34
+
35
+ /**
36
+ * A `Telo.Definition` dispatch slot: which `resources:` entry receives the call.
37
+ *
38
+ * Three shapes, and the union is what keeps the slot CONSTRAINED for manifests
39
+ * no pass walks — `validate-template-body` resolves the target and its
40
+ * capability, but it is entry-scoped, so a dependency's definition would
41
+ * otherwise have nothing checking this slot at all. An array or a number fails
42
+ * every branch here, which is the floor AJV used to provide.
43
+ *
44
+ * `anyOf`, never `oneOf`: a `!ref` sentinel is an object, so it would match both
45
+ * the sentinel branch and any permissive object branch and then fail for
46
+ * matching twice. That is also how the sentinel used to pass — the legacy object
47
+ * branch required nothing, so it accepted one by accident; here it is a branch
48
+ * of its own that says so.
49
+ */
50
+ function dispatchSlot(title: string, description: string): Record<string, unknown> {
51
+ return {
52
+ title,
53
+ description,
54
+ anyOf: [
55
+ // `!ref <entry>` — the spelling to write.
56
+ {
57
+ type: "object",
58
+ required: ["__tagged", "engine", "source"],
59
+ properties: {
60
+ __tagged: { const: true },
61
+ engine: { const: "ref" },
62
+ source: { type: "string", minLength: 1 },
63
+ },
64
+ additionalProperties: false,
65
+ },
66
+ // Legacy: a name template expanded against `self`. Read forever, because
67
+ // published artifacts carry it; `DEPRECATED_TEMPLATE_DISPATCH_FORM` is
68
+ // what moves an author off it.
69
+ { type: "string", "x-telo-context": dispatchSelfContext },
70
+ // Legacy: `{ kind?, name }`, where `name` is that same template.
71
+ {
72
+ type: "object",
73
+ required: ["name"],
74
+ properties: {
75
+ kind: { type: "string" },
76
+ name: { type: "string", "x-telo-context": dispatchSelfContext },
77
+ },
78
+ additionalProperties: true,
79
+ },
80
+ ],
81
+ };
82
+ }
26
83
  import type { ResourceDefinition } from "@telorun/sdk";
27
84
 
28
85
  /** Descriptive provenance a module declares about itself, shared by
@@ -101,9 +158,70 @@ const LIBRARY_CANDIDATES_SCHEMA = {
101
158
  format: { type: "string" },
102
159
  path: { type: "string" },
103
160
  source: { type: "string" },
104
- os: { type: "string" },
105
- arch: { type: "string" },
106
- libc: { type: "string" },
161
+ ...Object.fromEntries(PLATFORM_AXES.map((axis) => [axis, { type: "string" }])),
162
+ },
163
+ additionalProperties: false,
164
+ },
165
+ };
166
+
167
+ /** `native:` — the module's platform-specific files, one entry per logical name
168
+ * per platform tuple. Closed, so a mistyped axis is a schema violation rather
169
+ * than a platform-neutral entry. Grammar and rules live in
170
+ * `analyzer/nodejs/src/native-entries.ts` and `validate-native-entries.ts`. */
171
+ const NATIVE_ENTRIES_SCHEMA = {
172
+ type: "array",
173
+ items: {
174
+ type: "object",
175
+ required: ["name", "format", "os", "arch", "path"],
176
+ properties: {
177
+ name: { type: "string" },
178
+ format: { type: "string" },
179
+ path: { type: "string" },
180
+ ...Object.fromEntries(PLATFORM_AXES.map((axis) => [axis, { type: "string" }])),
181
+ },
182
+ additionalProperties: false,
183
+ },
184
+ };
185
+
186
+ /** `sources:` — where every staged file comes from, keyed by source name, each
187
+ * entry keyed by the module-relative path it produces. Closed at every level.
188
+ * An entry is a file (`upstream` + `member`, pinned with `sha256` +
189
+ * `executable`) or a link (`target`); a mix of the two, and every other rule,
190
+ * is reported by `analyzer/nodejs/src/validate-source-entries.ts`. `archive`
191
+ * names the upstream's format. A source built in the repo names its build under
192
+ * `build`, keyed by build system (`cargo: <crate dir>`), with the digest of its
193
+ * build inputs (`inputs`). */
194
+ const SOURCES_SCHEMA = {
195
+ type: "object",
196
+ additionalProperties: {
197
+ type: "object",
198
+ required: ["version", "url", "archive", "notices", "entries"],
199
+ properties: {
200
+ version: { type: "string" },
201
+ url: { type: "string" },
202
+ archive: { enum: ["tar.gz"] },
203
+ notices: { type: "array", minItems: 1, items: { type: "string" } },
204
+ build: {
205
+ type: "object",
206
+ required: ["cargo"],
207
+ properties: { cargo: { type: "string" }, inputs: { type: "string" } },
208
+ additionalProperties: false,
209
+ },
210
+ entries: {
211
+ type: "object",
212
+ additionalProperties: {
213
+ type: "object",
214
+ anyOf: [{ required: ["upstream", "member"] }, { required: ["target"] }],
215
+ properties: {
216
+ upstream: { type: "string" },
217
+ member: { type: "string" },
218
+ sha256: { type: "string" },
219
+ executable: { type: "boolean" },
220
+ target: { type: "string" },
221
+ },
222
+ additionalProperties: false,
223
+ },
224
+ },
107
225
  },
108
226
  additionalProperties: false,
109
227
  },
@@ -113,25 +231,18 @@ const LIBRARY_CANDIDATES_SCHEMA = {
113
231
  * One entry per layer except the manifest layer, which cannot list its own hash
114
232
  * inside itself and is pinned by the importer's `#sha256-...` instead. Shape and
115
233
  * matching rules are normative in kernel/specs/module-artifact.md; the parser
116
- * that enforces them is `artifact-layer-index.ts`. */
234
+ * that enforces them is `artifact-layer-index.ts`. `role` stays open and
235
+ * `selector` unconstrained: an entry for a newer runtime is skipped, not
236
+ * rejected, and its selector is not examined (spec §3.1). A known role's
237
+ * selector is validated by the parser, which `telo check` runs. */
117
238
  const LAYER_INDEX_SCHEMA = {
118
239
  type: "array",
119
240
  items: {
120
241
  type: "object",
121
242
  required: ["role", "blob", "integrity"],
122
243
  properties: {
123
- role: { type: "string", enum: ["controller", "library", "assets", "common"] },
124
- selector: {
125
- type: "object",
126
- required: ["format"],
127
- properties: {
128
- format: { type: "string" },
129
- os: { type: "string" },
130
- arch: { type: "string" },
131
- libc: { type: "string" },
132
- },
133
- additionalProperties: false,
134
- },
244
+ role: { type: "string", minLength: 1 },
245
+ selector: {},
135
246
  blob: { type: "string", pattern: "^sha256:[0-9a-f]{64}$" },
136
247
  integrity: { type: "string", pattern: "^sha256-[A-Za-z0-9_-]{43}$" },
137
248
  },
@@ -480,99 +591,31 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
480
591
  },
481
592
  },
482
593
  },
483
- invoke: {
484
- oneOf: [
485
- {
486
- type: "string",
487
- "x-telo-context": {
488
- type: "object",
489
- additionalProperties: false,
490
- properties: {
491
- self: { "x-telo-context-from-root": "schema" },
492
- },
493
- },
494
- },
495
- {
496
- type: "object",
497
- additionalProperties: true,
498
- properties: {
499
- kind: { type: "string" },
500
- name: {
501
- type: "string",
502
- "x-telo-context": {
503
- type: "object",
504
- additionalProperties: false,
505
- properties: {
506
- self: { "x-telo-context-from-root": "schema" },
507
- },
508
- },
509
- },
510
- },
511
- },
512
- ],
513
- },
514
- provide: {
515
- type: "object",
516
- additionalProperties: true,
517
- properties: {
518
- kind: { type: "string" },
519
- name: {
520
- type: "string",
521
- "x-telo-context": {
522
- type: "object",
523
- additionalProperties: false,
524
- properties: {
525
- self: { "x-telo-context-from-root": "schema" },
526
- },
527
- },
528
- },
529
- },
530
- },
531
- run: {
532
- type: "string",
533
- "x-telo-context": {
534
- type: "object",
535
- additionalProperties: false,
536
- properties: {
537
- self: { "x-telo-context-from-root": "schema" },
538
- },
539
- },
540
- },
541
- // Mount dispatch: names the `resources:` entry (a Telo.Mount, e.g. an
542
- // Http.Api) whose `register()` this definition delegates to. Same
543
- // string / { kind, name } grammar as `invoke:`. The named child stays
544
- // persistent so the produced mount's routes can `!ref` its siblings.
545
- mount: {
546
- oneOf: [
547
- {
548
- type: "string",
549
- "x-telo-context": {
550
- type: "object",
551
- additionalProperties: false,
552
- properties: {
553
- self: { "x-telo-context-from-root": "schema" },
554
- },
555
- },
556
- },
557
- {
558
- type: "object",
559
- additionalProperties: true,
560
- properties: {
561
- kind: { type: "string" },
562
- name: {
563
- type: "string",
564
- "x-telo-context": {
565
- type: "object",
566
- additionalProperties: false,
567
- properties: {
568
- self: { "x-telo-context-from-root": "schema" },
569
- },
570
- },
571
- },
572
- },
573
- },
574
- ],
575
- },
594
+ // A dispatch slot names the `resources:` entry that receives the call,
595
+ // as `!ref <entry>` — the one spelling every reference in Telo has. The
596
+ // string and `{ kind, name }` forms it used to admit (a CEL name
597
+ // template matched against CEL-named entries) were the removed
598
+ // reference object surviving in the one place nothing resolved it;
599
+ // `validate-template-body` is what resolves this slot, so the schema
600
+ // stays open here rather than reporting the same defect twice.
601
+ invoke: dispatchSlot(
602
+ "Invoke target",
603
+ "The `resources:` entry whose `invoke()` this kind dispatches to, as `!ref <entry>`.",
604
+ ),
605
+ provide: dispatchSlot(
606
+ "Provide target",
607
+ "The `resources:` entry whose `invoke()` produces this provider's value, as `!ref <entry>`.",
608
+ ),
609
+ run: dispatchSlot(
610
+ "Run target",
611
+ "The `resources:` entry whose `run()` this kind dispatches to, as `!ref <entry>`.",
612
+ ),
613
+ // The named child stays persistent so the produced mount's routes can
614
+ // `!ref` its siblings.
615
+ mount: dispatchSlot(
616
+ "Mount target",
617
+ "The `resources:` entry (a Telo.Mount, e.g. an Http.Api) whose `register()` this kind delegates to, as `!ref <entry>`.",
618
+ ),
576
619
  inputs: {
577
620
  type: "object",
578
621
  additionalProperties: true,
@@ -593,11 +636,11 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
593
636
  additionalProperties: false,
594
637
  properties: {
595
638
  self: { "x-telo-context-from-root": "schema" },
639
+ // Typed from the dispatch target's declared output: the slot
640
+ // holds a `!ref` to a `resources:` entry, resolved to that
641
+ // entry's kind (or its own `outputType`, where it narrows one).
596
642
  result: {
597
- "x-telo-context-from-ref-kind": [
598
- "provide/kind#outputType",
599
- "invoke/kind#outputType",
600
- ],
643
+ "x-telo-context-from-ref-kind": ["provide#outputType", "invoke#outputType"],
601
644
  },
602
645
  },
603
646
  },
@@ -763,6 +806,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
763
806
  items: { type: "string" },
764
807
  },
765
808
  assets: ASSETS_FILES_SCHEMA,
809
+ native: NATIVE_ENTRIES_SCHEMA,
810
+ sources: SOURCES_SCHEMA,
766
811
  layers: LAYER_INDEX_SCHEMA,
767
812
  filesIntegrity: LEGACY_FILES_INTEGRITY_SCHEMA,
768
813
  // Inline imports — name-keyed map sugar for separate `Telo.Import`
@@ -936,6 +981,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
936
981
  items: { type: "string" },
937
982
  },
938
983
  assets: ASSETS_FILES_SCHEMA,
984
+ native: NATIVE_ENTRIES_SCHEMA,
985
+ sources: SOURCES_SCHEMA,
939
986
  layers: LAYER_INDEX_SCHEMA,
940
987
  filesIntegrity: LEGACY_FILES_INTEGRITY_SCHEMA,
941
988
  // Inline imports — same name-keyed map sugar as Telo.Application; the
package/src/call-graph.ts CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  possibleUses,
54
54
  readRefSlot,
55
55
  transfersControl,
56
+ type RefSlot,
56
57
  type RefUse,
57
58
  type RefUseCases,
58
59
  } from "./ref-slot.js";
@@ -399,6 +400,35 @@ function resolveUseAtSite(
399
400
  return { use: possibleUses(slot), unresolved: entry.useCases, unresolvedReason };
400
401
  }
401
402
 
403
+ /**
404
+ * A slot's use at one concrete site of one resource, for a consumer that walks
405
+ * the kind's schema itself rather than reading the graph's edges.
406
+ *
407
+ * The same rule the graph applies to a resource-level edge — the same enclosing
408
+ * object, the same schema default — so a consumer and an edge cannot disagree
409
+ * about which case of a case map holds. `concretePath` is the site
410
+ * (`routes[0].handler`); `fieldPath` is the declaration it resolved from
411
+ * (`routes[].handler`), which is what the schema default is read against.
412
+ */
413
+ export function resolveSlotUseAt(
414
+ slot: RefSlot,
415
+ resource: unknown,
416
+ rootSchema: Record<string, any> | undefined,
417
+ concretePath: string,
418
+ fieldPath: string,
419
+ ): Pick<CallGraphEdge, "use" | "unresolved" | "unresolvedReason"> {
420
+ const entry: RefFieldEntry = {
421
+ refs: slot.kinds,
422
+ uses: slot.uses,
423
+ isArray: false,
424
+ ...(slot.useCases ? { useCases: slot.useCases } : {}),
425
+ };
426
+ const schemaDefault = rootSchema
427
+ ? schemaDefaultOf(enclosingSchemaOf(rootSchema, fieldPath))
428
+ : NO_DEFAULT;
429
+ return resolveUseAtSite(entry, resource, concretePath, schemaDefault);
430
+ }
431
+
402
432
  /** A resolved plain reference value (`{kind, name}`, optionally `alias`) — the
403
433
  * shape `resolveRefSentinels` leaves at a ref site. NOT a step: a bare boot
404
434
  * target written `!ref X` must not mint a step node. */
@@ -86,7 +86,7 @@ export function buildEnclosers(
86
86
  const definition = definitionOf(manifest);
87
87
  if (!definition?.schema) continue;
88
88
  forEachDrivenSlot(definition.schema, manifest, (driven) => {
89
- if (driven.kind !== "ref" || !driven.slot.throwsThrough) return;
89
+ if (driven.kind !== "ref" || !driven.slots.some(({ slot }) => slot.throwsThrough)) return;
90
90
  const target = resolveRefManifest(driven.data, ctx, moduleOf(manifest));
91
91
  if (!target || target === manifest) return;
92
92
  const list = enclosers.get(target);
@@ -21,6 +21,7 @@ import { buildKernelGlobalsIndex } from "./kernel-globals.js";
21
21
  import { moduleAliasScope } from "./module-alias-scope.js";
22
22
  import { isModuleKind } from "./module-kinds.js";
23
23
  import { navigateConcretePath } from "./manifest-path.js";
24
+ import { kindAtPath } from "./validate-cel-context.js";
24
25
  import { findManifest } from "./find-manifest.js";
25
26
  import { resolveLocalRef, walkStepArray } from "./schema-walk.js";
26
27
  import { readStepSlot } from "./step-slot.js";
@@ -269,14 +270,27 @@ export class CelScopeQuery {
269
270
  }
270
271
 
271
272
  // A kind's own declaration: the target is the `Telo.Definition` document,
272
- // which is an ordinary manifest in the set.
273
+ // which is an ordinary manifest in the set. The slot holds a `!ref` to a
274
+ // `resources:` entry; the entry's own field wins where it narrows one, else
275
+ // the declaration is the entry's KIND. The first slot that resolves to an
276
+ // entry is the one the type resolver reads, so it is the one navigated.
273
277
  const fromRefKind = annotated["x-telo-context-from-ref-kind"];
274
- const first = Array.isArray(fromRefKind) ? fromRefKind[0] : fromRefKind;
275
- if (typeof first === "string") {
276
- const hash = first.indexOf("#");
277
- if (hash <= 0) return undefined;
278
- const kindValue = navigateConcretePath(root, first.slice(0, hash).split("/").join("."));
279
- if (typeof kindValue !== "string") return undefined;
278
+ const slots = Array.isArray(fromRefKind) ? fromRefKind : [fromRefKind];
279
+ for (const slotSpec of slots) {
280
+ if (typeof slotSpec !== "string") continue;
281
+ const hash = slotSpec.indexOf("#");
282
+ // `continue`, never `return`: a malformed spec is one entry of a list the
283
+ // type resolver walks to the end, and aborting here made the two answer
284
+ // differently for the same annotation.
285
+ if (hash <= 0) continue;
286
+ const field = slotSpec.slice(hash + 1);
287
+ const namedKind = kindAtPath(root, slotSpec.slice(0, hash));
288
+ if (!namedKind) continue;
289
+ if (namedKind.entry?.[field] !== undefined) {
290
+ const index = (root.resources as unknown[]).indexOf(namedKind.entry);
291
+ return { manifest: resource, path: `resources[${index}].${field}`, propertyMap: false };
292
+ }
293
+ const kindValue = namedKind.kind;
280
294
  // The kind was read off THIS resource, so it is spelled in the alias scope
281
295
  // of the module that declared it — not the entry's.
282
296
  const scope = moduleAliasScope(resource.metadata, this.ctx.aliases, this.ctx.aliasesByModule);
@@ -300,9 +314,14 @@ export class CelScopeQuery {
300
314
  (owningModule
301
315
  ? named.find((m) => (m.metadata as { module?: string } | undefined)?.module === owningModule)
302
316
  : undefined) ?? (owningModule && named.length > 1 ? undefined : named[0]);
303
- if (!target) return undefined;
304
- return { manifest: target, path: first.slice(hash + 1), propertyMap: false };
317
+ // Fall through to the next spec when this one names no kind, or names one
318
+ // that does not declare the field the same order the TYPE resolver
319
+ // takes, so go-to-declaration lands where the type came from instead of on
320
+ // a field the target never had.
321
+ if (!target || target[field] === undefined) continue;
322
+ return { manifest: target, path: field, propertyMap: false };
305
323
  }
324
+ if (slots.some((s) => typeof s === "string")) return undefined;
306
325
 
307
326
  // `x-telo-context-element-from` / `-collection-from` type a binding from an
308
327
  // EXPRESSION, so there is no declaration to navigate to.
package/src/cel-scope.ts CHANGED
@@ -651,6 +651,7 @@ export class CelScopeResolver {
651
651
  manifestRoot: rootForResolver,
652
652
  defs,
653
653
  aliases,
654
+ aliasesByModule: scopes?.aliasesByModule,
654
655
  allManifests: allManifests as Record<string, any>[],
655
656
  });
656
657
  return mergeKernelGlobalsIntoContext(