@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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +26 -7
- package/dist/artifact-axes.d.ts +14 -0
- package/dist/artifact-axes.d.ts.map +1 -0
- package/dist/artifact-axes.js +12 -0
- package/dist/artifact-layer-index.d.ts +2 -2
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +9 -4
- package/dist/artifact-selector.d.ts +47 -21
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +85 -16
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +73 -16
- package/dist/call-graph.d.ts +12 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +22 -0
- package/dist/catch-scope.js +1 -1
- package/dist/forwarded-declaration.d.ts +26 -0
- package/dist/forwarded-declaration.d.ts.map +1 -0
- package/dist/forwarded-declaration.js +31 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +21 -5
- package/dist/module-alias-scope.d.ts +11 -0
- package/dist/module-alias-scope.d.ts.map +1 -1
- package/dist/module-alias-scope.js +10 -0
- package/dist/module-file-claims.d.ts +17 -1
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +38 -3
- package/dist/module-named-files.d.ts +72 -0
- package/dist/module-named-files.d.ts.map +1 -0
- package/dist/module-named-files.js +115 -0
- package/dist/native-entries.d.ts +83 -0
- package/dist/native-entries.d.ts.map +1 -0
- package/dist/native-entries.js +169 -0
- package/dist/normalize-inline-resources.d.ts +41 -13
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +236 -54
- package/dist/reference-field-map.d.ts +59 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +200 -34
- package/dist/release/payload-digest.d.ts +2 -1
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +2 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +8 -5
- package/dist/resolve-throws-union.d.ts +25 -4
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +62 -32
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +2 -1
- package/dist/rewrite-synthetic-origins.d.ts +6 -1
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
- package/dist/rewrite-synthetic-origins.js +107 -13
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +18 -5
- package/dist/schema-walk.d.ts +47 -13
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +150 -30
- package/dist/scope-declarations.d.ts +59 -0
- package/dist/scope-declarations.d.ts.map +1 -0
- package/dist/scope-declarations.js +45 -0
- package/dist/source-entries.d.ts +119 -0
- package/dist/source-entries.d.ts.map +1 -0
- package/dist/source-entries.js +349 -0
- package/dist/step-bodies.d.ts +24 -0
- package/dist/step-bodies.d.ts.map +1 -0
- package/dist/step-bodies.js +28 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +3 -2
- package/dist/validate-module-artifact.d.ts +4 -2
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +43 -4
- package/dist/validate-native-entries.d.ts +15 -0
- package/dist/validate-native-entries.d.ts.map +1 -0
- package/dist/validate-native-entries.js +103 -0
- package/dist/validate-observed-state.d.ts +7 -1
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +24 -6
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +2 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +11 -5
- package/dist/validate-scope-reach.d.ts +31 -0
- package/dist/validate-scope-reach.d.ts.map +1 -0
- package/dist/validate-scope-reach.js +180 -0
- package/dist/validate-source-entries.d.ts +16 -0
- package/dist/validate-source-entries.d.ts.map +1 -0
- package/dist/validate-source-entries.js +90 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +21 -39
- package/dist/value-type-keyword.d.ts +22 -5
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +16 -7
- package/package.json +3 -3
- package/src/analyzer.ts +33 -9
- package/src/artifact-axes.ts +24 -0
- package/src/artifact-layer-index.ts +9 -6
- package/src/artifact-selector.ts +110 -35
- package/src/builtins.ts +75 -16
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +36 -1
- package/src/kernel-globals.ts +28 -5
- package/src/migrations/CLAUDE.md +35 -0
- package/src/module-alias-scope.ts +17 -0
- package/src/module-file-claims.ts +63 -4
- package/src/module-named-files.ts +162 -0
- package/src/native-entries.ts +227 -0
- package/src/normalize-inline-resources.ts +326 -70
- package/src/reference-field-map.ts +257 -52
- package/src/release/CLAUDE.md +32 -0
- package/src/release/payload-digest.ts +2 -1
- package/src/resolve-ref-sentinels.ts +7 -6
- package/src/resolve-throws-union.ts +87 -32
- package/src/resolve-zone-requirements.ts +2 -1
- package/src/rewrite-synthetic-origins.ts +107 -16
- package/src/schema-compat.ts +18 -4
- package/src/schema-walk.ts +162 -34
- package/src/scope-declarations.ts +98 -0
- package/src/source-entries.ts +502 -0
- package/src/step-bodies.ts +55 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-cel-context.ts +3 -2
- package/src/validate-module-artifact.ts +46 -3
- package/src/validate-native-entries.ts +147 -0
- package/src/validate-observed-state.ts +36 -7
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +10 -6
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
package/src/artifact-selector.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
61
|
+
return (SELECTOR_LAYER_ROLES as readonly string[]).includes(role);
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
|
|
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
|
|
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
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
|
|
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:
|
|
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] =
|
|
133
|
+
selector[axis] = normalizeAxisValue(axis, raw, describe);
|
|
120
134
|
}
|
|
121
135
|
return selector;
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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",
|
|
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. */
|
package/src/catch-scope.ts
CHANGED
|
@@ -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.
|
package/src/kernel-globals.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|