@telorun/analyzer 0.73.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 (138) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +26 -7
  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 +73 -16
  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/forwarded-declaration.d.ts +26 -0
  19. package/dist/forwarded-declaration.d.ts.map +1 -0
  20. package/dist/forwarded-declaration.js +31 -0
  21. package/dist/index.d.ts +9 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/kernel-globals.d.ts.map +1 -1
  25. package/dist/kernel-globals.js +21 -5
  26. package/dist/module-alias-scope.d.ts +11 -0
  27. package/dist/module-alias-scope.d.ts.map +1 -1
  28. package/dist/module-alias-scope.js +10 -0
  29. package/dist/module-file-claims.d.ts +17 -1
  30. package/dist/module-file-claims.d.ts.map +1 -1
  31. package/dist/module-file-claims.js +38 -3
  32. package/dist/module-named-files.d.ts +72 -0
  33. package/dist/module-named-files.d.ts.map +1 -0
  34. package/dist/module-named-files.js +115 -0
  35. package/dist/native-entries.d.ts +83 -0
  36. package/dist/native-entries.d.ts.map +1 -0
  37. package/dist/native-entries.js +169 -0
  38. package/dist/normalize-inline-resources.d.ts +41 -13
  39. package/dist/normalize-inline-resources.d.ts.map +1 -1
  40. package/dist/normalize-inline-resources.js +236 -54
  41. package/dist/reference-field-map.d.ts +59 -1
  42. package/dist/reference-field-map.d.ts.map +1 -1
  43. package/dist/reference-field-map.js +200 -34
  44. package/dist/release/payload-digest.d.ts +2 -1
  45. package/dist/release/payload-digest.d.ts.map +1 -1
  46. package/dist/release/payload-digest.js +2 -1
  47. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  48. package/dist/resolve-ref-sentinels.js +8 -5
  49. package/dist/resolve-throws-union.d.ts +25 -4
  50. package/dist/resolve-throws-union.d.ts.map +1 -1
  51. package/dist/resolve-throws-union.js +62 -32
  52. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  53. package/dist/resolve-zone-requirements.js +2 -1
  54. package/dist/rewrite-synthetic-origins.d.ts +6 -1
  55. package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
  56. package/dist/rewrite-synthetic-origins.js +107 -13
  57. package/dist/schema-compat.d.ts.map +1 -1
  58. package/dist/schema-compat.js +18 -5
  59. package/dist/schema-walk.d.ts +47 -13
  60. package/dist/schema-walk.d.ts.map +1 -1
  61. package/dist/schema-walk.js +150 -30
  62. package/dist/scope-declarations.d.ts +59 -0
  63. package/dist/scope-declarations.d.ts.map +1 -0
  64. package/dist/scope-declarations.js +45 -0
  65. package/dist/source-entries.d.ts +119 -0
  66. package/dist/source-entries.d.ts.map +1 -0
  67. package/dist/source-entries.js +349 -0
  68. package/dist/step-bodies.d.ts +24 -0
  69. package/dist/step-bodies.d.ts.map +1 -0
  70. package/dist/step-bodies.js +28 -0
  71. package/dist/telo-version.d.ts +1 -1
  72. package/dist/telo-version.js +1 -1
  73. package/dist/validate-cel-context.d.ts.map +1 -1
  74. package/dist/validate-cel-context.js +3 -2
  75. package/dist/validate-module-artifact.d.ts +4 -2
  76. package/dist/validate-module-artifact.d.ts.map +1 -1
  77. package/dist/validate-module-artifact.js +43 -4
  78. package/dist/validate-native-entries.d.ts +15 -0
  79. package/dist/validate-native-entries.d.ts.map +1 -0
  80. package/dist/validate-native-entries.js +103 -0
  81. package/dist/validate-observed-state.d.ts +7 -1
  82. package/dist/validate-observed-state.d.ts.map +1 -1
  83. package/dist/validate-observed-state.js +24 -6
  84. package/dist/validate-reference-forms.d.ts.map +1 -1
  85. package/dist/validate-reference-forms.js +2 -2
  86. package/dist/validate-references.d.ts.map +1 -1
  87. package/dist/validate-references.js +11 -5
  88. package/dist/validate-scope-reach.d.ts +31 -0
  89. package/dist/validate-scope-reach.d.ts.map +1 -0
  90. package/dist/validate-scope-reach.js +180 -0
  91. package/dist/validate-source-entries.d.ts +16 -0
  92. package/dist/validate-source-entries.d.ts.map +1 -0
  93. package/dist/validate-source-entries.js +90 -0
  94. package/dist/validate-throws-coverage.d.ts.map +1 -1
  95. package/dist/validate-throws-coverage.js +21 -39
  96. package/dist/value-type-keyword.d.ts +22 -5
  97. package/dist/value-type-keyword.d.ts.map +1 -1
  98. package/dist/value-type-keyword.js +16 -7
  99. package/package.json +3 -3
  100. package/src/analyzer.ts +33 -9
  101. package/src/artifact-axes.ts +24 -0
  102. package/src/artifact-layer-index.ts +9 -6
  103. package/src/artifact-selector.ts +110 -35
  104. package/src/builtins.ts +75 -16
  105. package/src/call-graph.ts +30 -0
  106. package/src/catch-scope.ts +1 -1
  107. package/src/forwarded-declaration.ts +32 -0
  108. package/src/index.ts +36 -1
  109. package/src/kernel-globals.ts +28 -5
  110. package/src/migrations/CLAUDE.md +35 -0
  111. package/src/module-alias-scope.ts +17 -0
  112. package/src/module-file-claims.ts +63 -4
  113. package/src/module-named-files.ts +162 -0
  114. package/src/native-entries.ts +227 -0
  115. package/src/normalize-inline-resources.ts +326 -70
  116. package/src/reference-field-map.ts +257 -52
  117. package/src/release/CLAUDE.md +32 -0
  118. package/src/release/payload-digest.ts +2 -1
  119. package/src/resolve-ref-sentinels.ts +7 -6
  120. package/src/resolve-throws-union.ts +87 -32
  121. package/src/resolve-zone-requirements.ts +2 -1
  122. package/src/rewrite-synthetic-origins.ts +107 -16
  123. package/src/schema-compat.ts +18 -4
  124. package/src/schema-walk.ts +162 -34
  125. package/src/scope-declarations.ts +98 -0
  126. package/src/source-entries.ts +502 -0
  127. package/src/step-bodies.ts +55 -0
  128. package/src/telo-version.ts +1 -1
  129. package/src/validate-cel-context.ts +3 -2
  130. package/src/validate-module-artifact.ts +46 -3
  131. package/src/validate-native-entries.ts +147 -0
  132. package/src/validate-observed-state.ts +36 -7
  133. package/src/validate-reference-forms.ts +2 -3
  134. package/src/validate-references.ts +10 -6
  135. package/src/validate-scope-reach.ts +213 -0
  136. package/src/validate-source-entries.ts +120 -0
  137. package/src/validate-throws-coverage.ts +24 -31
  138. package/src/value-type-keyword.ts +35 -9
@@ -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
@@ -157,9 +158,70 @@ const LIBRARY_CANDIDATES_SCHEMA = {
157
158
  format: { type: "string" },
158
159
  path: { type: "string" },
159
160
  source: { type: "string" },
160
- os: { type: "string" },
161
- arch: { type: "string" },
162
- 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
+ },
163
225
  },
164
226
  additionalProperties: false,
165
227
  },
@@ -169,25 +231,18 @@ const LIBRARY_CANDIDATES_SCHEMA = {
169
231
  * One entry per layer except the manifest layer, which cannot list its own hash
170
232
  * inside itself and is pinned by the importer's `#sha256-...` instead. Shape and
171
233
  * matching rules are normative in kernel/specs/module-artifact.md; the parser
172
- * 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. */
173
238
  const LAYER_INDEX_SCHEMA = {
174
239
  type: "array",
175
240
  items: {
176
241
  type: "object",
177
242
  required: ["role", "blob", "integrity"],
178
243
  properties: {
179
- role: { type: "string", enum: ["controller", "library", "assets", "common"] },
180
- selector: {
181
- type: "object",
182
- required: ["format"],
183
- properties: {
184
- format: { type: "string" },
185
- os: { type: "string" },
186
- arch: { type: "string" },
187
- libc: { type: "string" },
188
- },
189
- additionalProperties: false,
190
- },
244
+ role: { type: "string", minLength: 1 },
245
+ selector: {},
191
246
  blob: { type: "string", pattern: "^sha256:[0-9a-f]{64}$" },
192
247
  integrity: { type: "string", pattern: "^sha256-[A-Za-z0-9_-]{43}$" },
193
248
  },
@@ -751,6 +806,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
751
806
  items: { type: "string" },
752
807
  },
753
808
  assets: ASSETS_FILES_SCHEMA,
809
+ native: NATIVE_ENTRIES_SCHEMA,
810
+ sources: SOURCES_SCHEMA,
754
811
  layers: LAYER_INDEX_SCHEMA,
755
812
  filesIntegrity: LEGACY_FILES_INTEGRITY_SCHEMA,
756
813
  // Inline imports — name-keyed map sugar for separate `Telo.Import`
@@ -924,6 +981,8 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
924
981
  items: { type: "string" },
925
982
  },
926
983
  assets: ASSETS_FILES_SCHEMA,
984
+ native: NATIVE_ENTRIES_SCHEMA,
985
+ sources: SOURCES_SCHEMA,
927
986
  layers: LAYER_INDEX_SCHEMA,
928
987
  filesIntegrity: LEGACY_FILES_INTEGRITY_SCHEMA,
929
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);
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Is this manifest, in a consumer's flattened analysis, a DEPENDENCY's code?
3
+ *
4
+ * Two stamps say so, and they are kept apart on purpose:
5
+ *
6
+ * - `forwardedExport` — an imported library's exported instance, stamped by
7
+ * flatten. It is what the consumer can reach, so the passes that count
8
+ * EXPORTS (`validate-exports`, the `resources.<Alias>.<name>` index, zone
9
+ * export seeding) read it and nothing else.
10
+ * - `forwardedInternal` — a declaration inline extraction pulled OUT of a
11
+ * forwarded manifest (a step target, an inline handler), stamped by
12
+ * `normalizeInlineResources`. It is the library's code, written in the
13
+ * library's names, and not something the library exports; counting it as an
14
+ * export would let a consumer's `!ref Alias.<generated name>` pass a check the
15
+ * kernel refuses.
16
+ *
17
+ * Every pass that asks "is this the consumer's to check, or to resolve its own
18
+ * names against?" asks THIS, so a dependency's extraction is treated exactly as
19
+ * the manifest it came out of.
20
+ *
21
+ * Browser-safe.
22
+ */
23
+ export function isForwardedDeclaration(manifest: unknown): boolean {
24
+ const meta = (manifest as { metadata?: Record<string, unknown> } | undefined)?.metadata;
25
+ return meta?.forwardedExport === true || meta?.forwardedInternal === true;
26
+ }
27
+
28
+ /** Only an exported instance — for the passes that count what a library exports. */
29
+ export function isForwardedExport(manifest: unknown): boolean {
30
+ const meta = (manifest as { metadata?: Record<string, unknown> } | undefined)?.metadata;
31
+ return meta?.forwardedExport === true;
32
+ }
package/src/index.ts CHANGED
@@ -441,20 +441,37 @@ export {
441
441
  ArtifactSelectorError,
442
442
  describeSelector,
443
443
  isLayerRole,
444
+ normalizeAxisValue,
444
445
  normalizeSelector,
445
446
  roleCarriesSelector,
447
+ selectorContradictions,
446
448
  selectorFromQualifiers,
447
449
  selectorKey,
448
450
  selectorMatches,
451
+ undeterminedAxesBlockingMatch,
449
452
  } from "./artifact-selector.js";
450
453
  export type {
451
454
  ArtifactSelector,
452
455
  LayerRole,
453
456
  PlatformAxis,
454
457
  PlatformTarget,
458
+ SelectorContradiction,
455
459
  } from "./artifact-selector.js";
456
460
  export { collectModuleFileClaims } from "./module-file-claims.js";
457
- export type { ModuleFileClaim } from "./module-file-claims.js";
461
+ export type { LocatedClaim, ModuleFileClaim } from "./module-file-claims.js";
462
+ export {
463
+ crossLayerSourceLinks,
464
+ describeClaim,
465
+ nativeClaimConflicts,
466
+ stageableFiles,
467
+ unclaimedSourceEntries,
468
+ } from "./module-named-files.js";
469
+ export type {
470
+ CrossLayerLink,
471
+ LocatedSourceEntry,
472
+ NativeClaimConflict,
473
+ StageableFile,
474
+ } from "./module-named-files.js";
458
475
  export { readLibraryCandidates } from "./module-library.js";
459
476
  export type {
460
477
  LibraryCandidate,
@@ -470,6 +487,24 @@ export {
470
487
  } from "./artifact-layer-index.js";
471
488
  export type { ArtifactLayer } from "./artifact-layer-index.js";
472
489
  export { validateModuleArtifact } from "./validate-module-artifact.js";
490
+ export { normalizeNativePath, readNativeEntries } from "./native-entries.js";
491
+ export type { NativeEntries, NativeEntry, NativeEntryProblem } from "./native-entries.js";
492
+ export {
493
+ SOURCE_ARCHIVE_FORMATS,
494
+ readModuleSources,
495
+ resolveSourceUrl,
496
+ sourceUrlProblem,
497
+ } from "./source-entries.js";
498
+ export type {
499
+ ModuleSource,
500
+ ModuleSources,
501
+ SourceArchiveFormat,
502
+ SourceBuild,
503
+ SourceEntry,
504
+ SourcePin,
505
+ SourceProblem,
506
+ SourceProblemCode,
507
+ } from "./source-entries.js";
473
508
  // Warnings everywhere, fatal at `telo publish` — descriptive metadata has no
474
509
  // runtime failure mode, so it must not stop a manifest running, but it is the
475
510
  // module's public face the moment it is published.
@@ -1,4 +1,5 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
+ import { isForwardedDeclaration } from "./forwarded-declaration.js";
2
3
  import { moduleMetadataSchema } from "./module-metadata-scope.js";
3
4
  import { residualEntrySchemaMap } from "./residual-schema.js";
4
5
  import { applyObservedStateNode } from "./validate-observed-state.js";
@@ -73,7 +74,10 @@ export function buildKernelGlobalsIndex(
73
74
  | Record<string, any>
74
75
  | undefined);
75
76
 
76
- const entrySchema = globalsSchema(entryDoc, buildResourcesSchema(manifests, resources));
77
+ const entrySchema = globalsSchema(
78
+ entryDoc,
79
+ buildResourcesSchema(manifests, resources, entryDoc?.metadata?.name as string | undefined),
80
+ );
77
81
  const openResources = { type: "object", additionalProperties: true };
78
82
  const byModule = new Map<string, Record<string, any>>();
79
83
 
@@ -129,15 +133,34 @@ function buildModuleSchema(doc: ModuleGlobals | Record<string, any> | undefined)
129
133
  );
130
134
  }
131
135
 
132
- /** Every non-system resource name in the set, plus the scope-declared ones. */
136
+ /** Every non-system resource name the entry module declares, plus the
137
+ * scope-declared ones. */
133
138
  function buildResourcesSchema(
134
139
  manifests: ResourceManifest[],
135
- resources?: ReadonlyMap<string, { kind: string; status?: Record<string, any> }>,
140
+ resources:
141
+ | ReadonlyMap<string, { kind: string; status?: Record<string, any>; forwardedFrom?: string }>
142
+ | undefined,
143
+ entryModule: string | undefined,
136
144
  ): Record<string, any> {
137
145
  const resourceProps: Record<string, any> = {};
138
146
  for (const m of manifests) {
139
147
  const name = m.metadata?.name as string | undefined;
140
148
  if (!name || !m.kind) continue;
149
+ // A dependency's code is not in the consumer's `resources`: an export is read
150
+ // as `resources.<Alias>.<name>`, and what extraction pulled out of one is
151
+ // reachable under no name at all.
152
+ if (isForwardedDeclaration(m)) continue;
153
+ // Nor are the imports a dependency declares for itself: its aliases publish
154
+ // into its own module's `resources`.
155
+ const module = m.metadata?.module;
156
+ if (
157
+ m.kind === "Telo.Import" &&
158
+ entryModule !== undefined &&
159
+ typeof module === "string" &&
160
+ module !== entryModule
161
+ ) {
162
+ continue;
163
+ }
141
164
  // Telo.Import snapshots are stored under resources.<alias> at runtime,
142
165
  // so they appear here alongside regular resources.
143
166
  if (!SYSTEM_KINDS.has(m.kind)) {
@@ -148,14 +171,14 @@ function buildResourcesSchema(
148
171
  // now, so their names resolve too — inside the scope's regions, which is where
149
172
  // the only expressions that can name them live.
150
173
  for (const [key, entry] of resources ?? []) {
151
- if (key.includes(".")) continue;
174
+ if (key.includes(".") || entry.forwardedFrom !== undefined) continue;
152
175
  resourceProps[key] ??= { type: "object", additionalProperties: true };
153
176
  if (entry.status) applyObservedStateNode(resourceProps, key, entry.status);
154
177
  }
155
178
  // Imports' exported instances publish two levels deep (`resources.<Alias>.<name>`);
156
179
  // the alias node stays open so its other keys keep resolving.
157
180
  for (const [key, entry] of resources ?? []) {
158
- if (!key.includes(".") || !entry.status) continue;
181
+ if (!key.includes(".") || !entry.status || entry.forwardedFrom !== undefined) continue;
159
182
  applyObservedStateNode(resourceProps, key, entry.status);
160
183
  }
161
184
 
@@ -0,0 +1,35 @@
1
+ # Manifest migrations — guide
2
+
3
+ Loaded when working under `analyzer/nodejs/src/migrations/`. Author guide: `docs/extend/manifest-migrations.md`. The entries themselves are data under `analyzer/migrations/`.
4
+
5
+ **A migration is a legacy spelling rewritten to the current one, because published artifacts carry the old and cannot be edited.** Distinct from a **normalization** (`desugarImports`, `normalizeInlineResources`, `resolveRefSentinels`, `resolveSchemaRefKinds`, `rewriteSyntheticOrigins`), which folds authoring sugar into the internal form and is correctly invisible. One "rewrite pass" abstraction covering both was rejected: it would carry a "reported / written back" flag on every entry, putting desugaring one boolean away from editing a user's manifest. Registry + driver in `analyzer/nodejs/src/migrations/`; author guide in `docs/extend/manifest-migrations.md`.
6
+
7
+ **An entry is DATA in both halves and contains no code** — one JSON file per entry under `analyzer/migrations/`, read as one lexically ordered set. A predicate expressed in one language would be readable by one kernel only, which is the divergence the design exists to prevent. JSON rather than YAML because it is the only format all three runtimes embed with no generation step: Rust has `include_str!`, Go has `//go:embed`, TypeScript has neither and only `resolveJsonModule`. The files sit beside the language implementations rather than inside either; `scripts/copy-migration-entries.mjs` (the analyzer's `prepare`) copies them in and emits the barrel from the same directory listing, so a file that exists always runs — a hand-maintained list was the one place the mechanism could fail silently.
8
+
9
+ **The patch names what it TARGETS** — `rename-key`, `set-value`, `set-tag`, `insert-item`, `remove-entry`, each with one fixed parameter shape and no path of its own. **Every operation has a known YAML edit form**: that is what makes a migration applicable to a *file* at all, and what lets the driver **derive** whether a quick fix exists — read straight off the verb, never declared, so a missing repair is stated rather than silent. A written `value` must be a **scalar**, refused at entry-READ time: the file applier re-quotes a value in the author's style at the node's own span, which has no meaning for a mapping, so accepting one would make the limitation invisible until a user ran `telo migrate` and was told, permanently, to fix it by hand. The model is JSON Patch's; the *names* deliberately are not, because these are narrower and a lookalike wearing a standard's names invites expectations it does not meet (`move` relocates across parents and replaces an occupied destination; `rename-key` renames within one mapping and *refuses* one).
10
+
11
+ **A SCHEMA REGION is the one containment a kind list cannot express.** An annotation keyword occurs in author-written JSON Schema, and schema fragments are not confined to kind documents: an inline `inputType:` / `outputType:` on any kind that declares one, an API route's `request.schema.body`, a `Telo.JsonSchema`'s `schema`. That set of kinds is open, and enumerating the standard library's would put resource-kind knowledge into the analyzer. So `inSchema: true` bounds a rule to nodes reached through the **kernel's own** schema-valued keys (`schema`, `status`, `inputType`, `outputType`, `itemType`), which no kind owns — and only with it may `inKind` / `under` be `["*"]`, and only for a rule whose `key` begins with `x-telo-`, both refused at entry-read. That pairing is the containment: the region bounds where the walk may go, the reserved key bounds what it may touch. The wildcard drops the bounded walk, which is not worth pricing at Telo's scale; what it genuinely costs is one wrong-rewrite residue — a schema literal inside an assertion's expected value, rewritten into its own synonym — which cannot be closed in a data-only matcher without naming kinds, and is accepted because the sites it reaches are exactly the ones no enumeration covers.
12
+
13
+ **The matcher's containment is POSITIVE and required** (`analyzer/nodejs/src/migrations/match.ts`): `inKind` names the document kinds a rule may touch, `under` the region within them it may reach, and nothing outside is reachable; `value`/`valueOneOf`, `withSibling` and `notUnder` narrow within it. **`under` is ANCHORED at the document root** — it names top-level keys and the match must be at or below one of them, which is what makes the containment claim true rather than decorative: a `Telo.Definition`'s `resources:` template body carries other kinds' configuration, so a rule matching "any path segment spelled `schema`" would reach the very user JSON blob the positive form exists to keep out, and delete from it silently. Both gates bound the WALK rather than filter its output (`applicableRules` → `buildMatchIndex`), so a document no rule targets is never walked and a region no rule names is never descended into — this runs on the kernel's boot path for every file in the graph, and `type:` alone occurs a couple of hundred times in a standard-library manifest. Walk-everything-and-subtract cannot be made sound — the set to subtract is unbounded, since any kind whose config carries a user JSON blob can hold something shaped like the node a rule looks for — and it cannot express the guarantee the module surface is promised to carry (*a dependency renames its own field and provably nothing else*), which is a statement about what a rule may REACH and so has to be said positively. Both halves are closed vocabularies and an unknown token is refused, never ignored. A migration that does not fit is a signal to extend the vocabulary, never to hand-write a rewrite.
14
+
15
+ **One phase, in the loader (`parseLoadedFile`), after parse and before both `precompileDoc` and `desugarLoadedFile`.** The driver invariant is that **a migration only ever matches author-written nodes**, and the position is what makes that structural rather than a convention: a synthetic `Telo.Import` has no YAML document to edit, would record a path the file never had, and shares `variables`/`secrets` *by reference* with the module doc, so a match inside one would apply twice. Before precompile for the same reason in miniature — a matcher must see what the author wrote, not a `CompiledValue`. **No post-resolution phase**: it would sit inside the analyze pass, which the Rust side does not have, so every entry in it would be structurally Node-only — reintroducing the divergence the design exists to prevent.
16
+
17
+ **Composition is the driver's guarantee, not each entry's proof obligation.** One pass, match set frozen against the pre-migration tree (so no rule matches a node another rule produced), rules ordered within an entry, entries independent — which is cross-party determinism once module entries are aggregated. Idempotency then follows from the driver: a rule matches only the legacy spelling, so re-running finds nothing. **A migration that cannot rewrite leaves the node untouched** (a malformed value, an occupied `rename-key` destination) for the ordinary validator to reject — never guessing, never dropping. Freezing is not enough for a **sequence index**, which is not an identity: a stale KEY path resolves to nothing and refuses itself, while a stale index silently names a different element, so the driver tracks which arrays a patch resized and refuses any later indexed match into one. The file applier is stricter in one further way — two patches whose byte spans overlap cannot both splice, so the later is dropped — and, unlike the tree, it **reports** what it could not write (`migrateFileText`'s `unwritable`), because the diagnostic that sent the author to `telo migrate` would otherwise leave a warning with no way to act on it. **A removal of a mapping entry that OPENS a sequence item splices to the following key** rather than deleting the line (which would take the `- ` with it and fold the item into its predecessor): a legacy ref slot is overwhelmingly an `anyOf` branch written `- type: string`, so refusing it would have made the diagnostic's own advice dead for the case it is most often given in. **The scalar re-quoting rule and the splice loop are ONE primitive** (`analyzer/nodejs/src/yaml-source-edit.ts`, browser-safe), read by the migration applier, `ide-support`'s quick fix and `telo upgrade`'s pin rewrite alike — three surfaces write repairs into the same files, and two copies of a subtle quoting rule would eventually quote one value two ways with nothing to catch it.
18
+
19
+ **Rewrite always, report locally.** The runtime must read artifacts published years ago, so every file in the graph is rewritten; only the ENTRY module's own files (owner + `include:` partials) report, because a published dependency is not the consumer's to fix. Enforced at `loadGraph` (`LoadedGraph.migrationDiagnostics`), while the per-file record lives on `LoadedFile.migrations`. **Path provenance is part of the contract**: each rewrite records `legacyPath` beside `migratedPath`, `remapMigratedPaths` maps every downstream diagnostic back before position resolution (`assembleGraphDiagnostics`, the editor's closure, and the SDK's `check` seam — all three, since a caller acts on `path` and one surface reporting the migrated spelling would make a manifest mean two things depending on who asked), and `telo migrate` reads its edit target from the same record — the generalization of `rewriteSyntheticOrigins`. Without it a key rename silently downgrades every squiggle on that node to a parent, and a fix among them writes across a parent's span. **The general index is by FILE and every rewrite is in it** — a diagnostic carries at most two routing facts and routinely only one, so indexing by resource identity alone left every diagnostic without `data.resource` and every rewrite in a doc without `metadata.name` (all of `Telo.Import`) simply unreachable. `(kind, name)` is the NARROWING index, never the general one: resource names are module-scoped, so two libraries declaring a `Store` would share one bucket and move each other's squiggles. Where narrowing is impossible — an identity declared in several files with no `filePath`, or two records in one file covering a path with different legacy spellings — nothing is remapped rather than guessed.
20
+
21
+ **`LoadOptions.migrate` is a third cache axis** beside `compile` and `desugarImports` (variant tag `m`/`x`), opt-in like both. Every resolved consumer passes it; a **round-trip** view must not, since the editor pairs manifests to YAML nodes by index and writes the pair back on save, and `telo migrate` has to see the legacy spelling to find it. Forgetting it fails loudly (the old spelling reaches the validator), never silently.
22
+
23
+ **A diagnostic composes in three parts and the entry writes only one.** The driver generates *what changed* and *how to apply it* identically for every entry; the entry supplies `reason` — a sentence or two of rationale, never a clause of the generated sentence, and entry-level because one entry may carry several rules that share one story. An entry also declares its stable `id`, `code` and `severity`. **No version stamp**: "can this be deleted?" turns on whether any published artifact still carries the spelling, which the hub can answer and a stamp cannot.
24
+
25
+ **File application reuses byte-splice editing, not `Document.toString()`** (`migrations/yaml-edit.ts`) — the `cli/nodejs/src/commands/upgrade.ts` precedent: re-serializing re-folds block scalars, drops quote-style hints and reflows strings, so a one-key repair would land as a whole-file diff. The document is never mutated, so every effect of a patch resolves at the location the patch matched. **`telo migrate` is the REFERENCE application, not the only one** — applying pending migrations is an operation other commands compose over a subset of entries, and `telo upgrade` is the case that matters (it should repair what its pin move broke, over just the modules it moved). That composition waits on the module surface, since core-only entries have no per-module subset.
26
+
27
+ **Two declaration surfaces, one mechanism** — a **core** entry ships with the analyzer and may name any `inKind`; a **module** entry ships in a module's artifact and may name only kinds that module owns. Scope of match and provenance differ, not shape; the closed vocabulary is the trust boundary. The module surface is planned separately, as is the Rust reader and `kernel/specs/manifest-migrations.md`.
28
+
29
+ A second committed entry is **`normalize-value-types`**, which rewrites `x-telo-stream: true` / `x-telo-binary: true` to `x-telo-type: Telo.Stream` / `Telo.Bytes` and qualifies a bare `TcpPort` / `UdpPort` — the entry the schema region exists for, and the first to use the wildcard forms.
30
+
31
+ The committed entry is **`ref-slot-scalar-type`**, which drops the scalar `type:` a reference slot pinned back when references were plain strings. It replaced `normalizeRefSlots`, which did the same rewrite at every schema-compile site regardless of `LoadOptions.migrate` — a duplicate with a *different* traversal, and one that falsified the design's own safety property, since a consumer who forgot the flag behaved identically apart from the missing warning. Deleting it is what makes the entry able to prove the mechanism it demonstrates.
32
+
33
+ ## Where to look
34
+
35
+ - "an old spelling has to keep loading" → `analyzer/nodejs/src/migrations/` (`types.ts` the entry vocabulary, `patch.ts` the plan/apply split, `yaml-edit.ts` the file repair, `match.ts` the selector, `driver.ts` the guarantees, `report.ts` the diagnostic, `provenance.ts` the path remap, `registry.ts` the set), `cli/nodejs/src/commands/migrate.ts`
@@ -60,6 +60,23 @@ export function moduleAliasScope<A extends KindResolver, M extends KindResolver>
60
60
  return aliasesByModule?.get(declaringModule) ?? aliases;
61
61
  }
62
62
 
63
+ /**
64
+ * A manifest's kind, resolved to its definition in the alias table of the
65
+ * module that wrote it — the canonical spelling first, as every other lookup in
66
+ * the analyzer does (`resolve(resolveKind(k) ?? k)`), so an alias that happens to
67
+ * spell another module's canonical kind cannot win over the one it names.
68
+ */
69
+ export function definitionInScope<D>(
70
+ registry: { resolve(kind: string): D | undefined },
71
+ kind: string,
72
+ metadata: { module?: unknown } | undefined,
73
+ aliases: KindResolver | undefined,
74
+ aliasesByModule: ReadonlyMap<string, KindResolver> | undefined,
75
+ ): D | undefined {
76
+ const canonical = moduleAliasScope(metadata, aliases, aliasesByModule)?.resolveKind(kind);
77
+ return (canonical ? registry.resolve(canonical) : undefined) ?? registry.resolve(kind);
78
+ }
79
+
63
80
  /** All this rule needs of a resolver, and deliberately all it asks for:
64
81
  * `ModuleScopes` already types its map this way so a caller can hand over a
65
82
  * lighter table, and requiring the full `AliasResolver` here would have made