@telorun/analyzer 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +76 -20
- 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 +144 -113
- 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/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +32 -11
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +1 -0
- package/dist/eval-paths.d.ts +36 -5
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +47 -6
- package/dist/extends-resolution.d.ts +12 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +23 -0
- 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 +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- 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/nearest-name.d.ts +22 -0
- package/dist/nearest-name.d.ts.map +1 -0
- package/dist/nearest-name.js +43 -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/destinations.d.ts +59 -0
- package/dist/release/destinations.d.ts.map +1 -0
- package/dist/release/destinations.js +75 -0
- package/dist/release/index.d.ts +6 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -1
- package/dist/release/ledger.d.ts +20 -11
- package/dist/release/ledger.d.ts.map +1 -1
- package/dist/release/ledger.js +43 -14
- 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/release/release-plan.d.ts +3 -2
- package/dist/release/release-plan.d.ts.map +1 -1
- package/dist/release/release-plan.js +12 -19
- package/dist/release/workspace-config.d.ts +114 -34
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +322 -38
- package/dist/release/workspace-schema.d.ts +41 -0
- package/dist/release/workspace-schema.d.ts.map +1 -0
- package/dist/release/workspace-schema.js +77 -0
- 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-error-report.d.ts +7 -0
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +9 -4
- 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/template-body.d.ts +10 -0
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +25 -0
- package/dist/validate-cel-context.d.ts +23 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +51 -18
- package/dist/validate-exports.d.ts +28 -0
- package/dist/validate-exports.d.ts.map +1 -0
- package/dist/validate-exports.js +146 -0
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +37 -1
- 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-provider-coherence.d.ts +4 -8
- package/dist/validate-provider-coherence.d.ts.map +1 -1
- package/dist/validate-provider-coherence.js +12 -123
- 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 +152 -65
- 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-template-body.d.ts +36 -0
- package/dist/validate-template-body.d.ts.map +1 -0
- package/dist/validate-template-body.js +246 -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 +4 -3
- package/src/analyzer.ts +97 -20
- 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 +160 -113
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/cel-scope-query.ts +28 -9
- package/src/cel-scope.ts +1 -0
- package/src/eval-paths.ts +53 -6
- package/src/extends-resolution.ts +24 -0
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +37 -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/nearest-name.ts +47 -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/destinations.ts +105 -0
- package/src/release/index.ts +33 -2
- package/src/release/ledger.ts +51 -21
- package/src/release/payload-digest.ts +2 -1
- package/src/release/release-plan.ts +16 -26
- package/src/release/workspace-config.ts +483 -47
- package/src/release/workspace-schema.ts +99 -0
- 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-error-report.ts +16 -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/template-body.ts +25 -0
- package/src/validate-cel-context.ts +60 -18
- package/src/validate-exports.ts +185 -0
- package/src/validate-extends.ts +43 -0
- 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-provider-coherence.ts +17 -127
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +151 -66
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-template-body.ts +274 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
- package/dist/validate-template-dispatch.d.ts +0 -27
- package/dist/validate-template-dispatch.d.ts.map +0 -1
- package/dist/validate-template-dispatch.js +0 -95
- package/src/validate-template-dispatch.ts +0 -99
package/src/eval-paths.ts
CHANGED
|
@@ -186,6 +186,36 @@ export function celEvalSites(schema: Record<string, any> | undefined): CelEvalSi
|
|
|
186
186
|
return { compile, runtime, regions: extractCelRegionScopes(schema) };
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/**
|
|
190
|
+
* A BASE-FORM CHILD'S OWN FIELDS ARE COMPILE-EVAL WITHOUT ANNOTATION.
|
|
191
|
+
*
|
|
192
|
+
* A definition with `base:` has no controller of its own: the kernel evaluates
|
|
193
|
+
* the mapping once, at `create()`, against `self` — the instance's config — and
|
|
194
|
+
* hands the result to the inherited controller as the parent's config. The
|
|
195
|
+
* child's own schema fields never reach a controller; they exist to be read by
|
|
196
|
+
* `base:`, and the mapping expands whatever compiled value it reads. So every
|
|
197
|
+
* own field is evaluated exactly once at creation against the startup scope,
|
|
198
|
+
* which is what compile-eval IS. Declared here as the rule rather than left as a
|
|
199
|
+
* property of the mapping walk — the `Telo.Provider` posture, where a
|
|
200
|
+
* construction-time-only surface declares compile-eval once for all its fields
|
|
201
|
+
* rather than per field. Without it the rule was off for every inheritance kind
|
|
202
|
+
* (its capability is inherited, so the gate read `undefined`) and the
|
|
203
|
+
* expressions were never typed either: a `!cel "variables.whoo"` passed `telo
|
|
204
|
+
* check` and failed at boot.
|
|
205
|
+
*
|
|
206
|
+
* Read by the kernel's instance production and the analyzer's coverage decision,
|
|
207
|
+
* so the two cannot disagree about which fields a base child evaluates. An
|
|
208
|
+
* explicitly `runtime` own field still wins, through the same overlap rule a
|
|
209
|
+
* root `x-telo-eval: compile` follows.
|
|
210
|
+
*/
|
|
211
|
+
export const IMPLICIT_COMPILE_SITES: CelEvalSites = { compile: ["**"], runtime: [], regions: [] };
|
|
212
|
+
|
|
213
|
+
export function implicitEvalSites(
|
|
214
|
+
definition: { base?: unknown } | undefined,
|
|
215
|
+
): CelEvalSites {
|
|
216
|
+
return definition?.base != null ? IMPLICIT_COMPILE_SITES : NO_CEL_EVAL_SITES;
|
|
217
|
+
}
|
|
218
|
+
|
|
189
219
|
/** The union of several schemas' sites — a kind's own and its capability
|
|
190
220
|
* abstract's, which is how a `Telo.Provider`'s implicit compile-eval reaches
|
|
191
221
|
* fields the provider never annotated. */
|
|
@@ -201,11 +231,16 @@ export function mergeCelEvalSites(...sites: CelEvalSites[]): CelEvalSites {
|
|
|
201
231
|
* Whether the value at `path` is evaluated, and when — null for a field whose
|
|
202
232
|
* value is read as a literal.
|
|
203
233
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
234
|
+
* An annotated field wins over the region it sits in — a field's own annotation
|
|
235
|
+
* is more specific than an enclosing one, and a region resolves to `runtime`
|
|
236
|
+
* because that is what a region IS, a per-invocation scope naming what its
|
|
237
|
+
* expressions can read. Between the two annotations, `runtime` wins wherever
|
|
238
|
+
* they OVERLAP and `compile` answers everywhere else.
|
|
239
|
+
*
|
|
240
|
+
* That overlap rule is the kernel's, read back: its compile expansion skips any
|
|
241
|
+
* compile path a runtime path contains or is contained by, and under a root
|
|
242
|
+
* `**` it skips per top-level key — so a runtime-annotated field under an
|
|
243
|
+
* implicit compile root stays runtime, here as at dispatch.
|
|
209
244
|
*
|
|
210
245
|
* `path` is the `walkCelExpressions` spelling (`routes[0].returns[1].when`).
|
|
211
246
|
*/
|
|
@@ -213,8 +248,20 @@ export function celEvalModeAt(
|
|
|
213
248
|
sites: CelEvalSites,
|
|
214
249
|
path: string,
|
|
215
250
|
): "compile" | "runtime" | null {
|
|
216
|
-
|
|
251
|
+
const compiled = sites.compile.some((p) => {
|
|
252
|
+
if (!evalPathCovers(p, path)) return false;
|
|
253
|
+
const effective = p === "**" ? topLevelKey(path) : p;
|
|
254
|
+
return !sites.runtime.some(
|
|
255
|
+
(rp) => evalPathCovers(rp, effective) || evalPathCovers(effective, rp),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
if (compiled) return "compile";
|
|
217
259
|
if (evalPathsCover(sites.runtime, path)) return "runtime";
|
|
218
260
|
if (sites.regions.some((scope) => pathMatchesScope(path, scope))) return "runtime";
|
|
219
261
|
return null;
|
|
220
262
|
}
|
|
263
|
+
|
|
264
|
+
function topLevelKey(path: string): string {
|
|
265
|
+
const end = path.search(/[.[]/);
|
|
266
|
+
return end === -1 ? path : path.slice(0, end);
|
|
267
|
+
}
|
|
@@ -137,6 +137,30 @@ export function effectiveAuthorSchema(
|
|
|
137
137
|
return mergeTypeSchemas([parentSchema, own]) as Record<string, any>;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The parent's required fields that a merge-form child leaves on its OWN author
|
|
142
|
+
* surface — inherited by the rule above, and not redeclared by the child.
|
|
143
|
+
*
|
|
144
|
+
* The one fact behind a confusing diagnostic: a kind written to wire a field
|
|
145
|
+
* internally still demands it from its consumer, because without `base:` the
|
|
146
|
+
* child is authored against merge(parent, own) and the parent's `required` comes
|
|
147
|
+
* along. Empty for a child with `base:` (its surface is its own schema) and for
|
|
148
|
+
* a kind that extends nothing, so a caller can use a non-empty result as the
|
|
149
|
+
* condition itself.
|
|
150
|
+
*/
|
|
151
|
+
export function inheritedRequiredFields(
|
|
152
|
+
def: ResourceDefinition | undefined,
|
|
153
|
+
resolve: DefResolver,
|
|
154
|
+
): string[] {
|
|
155
|
+
if (!body(def).extends || body(def).base) return [];
|
|
156
|
+
const parent = resolveParent(def, resolve);
|
|
157
|
+
if (!parent) return [];
|
|
158
|
+
const required = effectiveAuthorSchema(parent, resolve).required;
|
|
159
|
+
if (!Array.isArray(required)) return [];
|
|
160
|
+
const own = ((body(def).schema ?? {}).properties ?? {}) as Record<string, unknown>;
|
|
161
|
+
return required.filter((f): f is string => typeof f === "string" && !(f in own));
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
/**
|
|
141
165
|
* The fields a merge-form inheriting child publishes over its parent's reading.
|
|
142
166
|
*
|
|
@@ -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
|
@@ -33,6 +33,7 @@ export {
|
|
|
33
33
|
celEvalSites,
|
|
34
34
|
declaresCelRegion,
|
|
35
35
|
evalPathCovers,
|
|
36
|
+
implicitEvalSites,
|
|
36
37
|
mergeCelEvalSites,
|
|
37
38
|
pathMatchesScope,
|
|
38
39
|
NO_CEL_EVAL_SITES,
|
|
@@ -440,20 +441,37 @@ export {
|
|
|
440
441
|
ArtifactSelectorError,
|
|
441
442
|
describeSelector,
|
|
442
443
|
isLayerRole,
|
|
444
|
+
normalizeAxisValue,
|
|
443
445
|
normalizeSelector,
|
|
444
446
|
roleCarriesSelector,
|
|
447
|
+
selectorContradictions,
|
|
445
448
|
selectorFromQualifiers,
|
|
446
449
|
selectorKey,
|
|
447
450
|
selectorMatches,
|
|
451
|
+
undeterminedAxesBlockingMatch,
|
|
448
452
|
} from "./artifact-selector.js";
|
|
449
453
|
export type {
|
|
450
454
|
ArtifactSelector,
|
|
451
455
|
LayerRole,
|
|
452
456
|
PlatformAxis,
|
|
453
457
|
PlatformTarget,
|
|
458
|
+
SelectorContradiction,
|
|
454
459
|
} from "./artifact-selector.js";
|
|
455
460
|
export { collectModuleFileClaims } from "./module-file-claims.js";
|
|
456
|
-
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";
|
|
457
475
|
export { readLibraryCandidates } from "./module-library.js";
|
|
458
476
|
export type {
|
|
459
477
|
LibraryCandidate,
|
|
@@ -469,6 +487,24 @@ export {
|
|
|
469
487
|
} from "./artifact-layer-index.js";
|
|
470
488
|
export type { ArtifactLayer } from "./artifact-layer-index.js";
|
|
471
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";
|
|
472
508
|
// Warnings everywhere, fatal at `telo publish` — descriptive metadata has no
|
|
473
509
|
// runtime failure mode, so it must not stop a manifest running, but it is the
|
|
474
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
|
|
@@ -4,16 +4,22 @@ import {
|
|
|
4
4
|
walkCelExpressions,
|
|
5
5
|
type TemplatingEngineRegistry,
|
|
6
6
|
} from "@telorun/templating";
|
|
7
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
7
8
|
import { PackageURL } from "packageurl-js";
|
|
8
9
|
import { parseAllDocuments } from "yaml";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ArtifactSelectorError,
|
|
12
|
+
selectorFromQualifiers,
|
|
13
|
+
selectorKey,
|
|
14
|
+
type ArtifactSelector,
|
|
15
|
+
} from "./artifact-selector.js";
|
|
10
16
|
import { readLibraryCandidates } from "./module-library.js";
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* One module-relative file a manifest names, and the artifact layer it belongs
|
|
14
20
|
* to.
|
|
15
21
|
*
|
|
16
|
-
* The
|
|
22
|
+
* The answer to "why is this file in the payload" for everything but `native:` entries (read by `native-entries.ts`), replacing two
|
|
17
23
|
* derivations that happened to agree: publish used to re-parse the manifest with
|
|
18
24
|
* PURL knowledge hardcoded into the CLI, and any second vocabulary — a tag that
|
|
19
25
|
* embeds a file, say — would have had to be added there by hand. Here the
|
|
@@ -188,11 +194,64 @@ function claimKey(claim: ModuleFileClaim): string {
|
|
|
188
194
|
export function collectModuleFileClaims(
|
|
189
195
|
manifestText: string,
|
|
190
196
|
registry: TemplatingEngineRegistry = defaultRegistry(),
|
|
197
|
+
): ModuleFileClaim[] {
|
|
198
|
+
return collectDocumentFileClaims(
|
|
199
|
+
parseAllDocuments(manifestText, { customTags: defaultCustomTags() }).map(
|
|
200
|
+
(doc) => doc.toJSON() as unknown,
|
|
201
|
+
),
|
|
202
|
+
registry,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A claim, with the document that made it when the caller knows which. */
|
|
207
|
+
export interface LocatedClaim {
|
|
208
|
+
readonly claim: ModuleFileClaim;
|
|
209
|
+
/** `<kind>/<name>` of the declaring document. */
|
|
210
|
+
readonly where?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The claims one module's own documents make, out of an analysis set that also
|
|
215
|
+
* holds its dependencies' documents. A document whose candidate selector cannot
|
|
216
|
+
* be read contributes none — `validateModuleArtifact` reports it.
|
|
217
|
+
*/
|
|
218
|
+
export function moduleDocumentClaims(
|
|
219
|
+
manifests: readonly ResourceManifest[],
|
|
220
|
+
moduleName: string | undefined,
|
|
221
|
+
registry: TemplatingEngineRegistry = defaultRegistry(),
|
|
222
|
+
): LocatedClaim[] {
|
|
223
|
+
const out: LocatedClaim[] = [];
|
|
224
|
+
for (const doc of manifests) {
|
|
225
|
+
const metadata = (doc.metadata ?? {}) as Record<string, unknown>;
|
|
226
|
+
const docModule =
|
|
227
|
+
typeof metadata.module === "string"
|
|
228
|
+
? metadata.module
|
|
229
|
+
: doc.kind === "Telo.Application" || doc.kind === "Telo.Library"
|
|
230
|
+
? metadata.name
|
|
231
|
+
: undefined;
|
|
232
|
+
if (docModule !== moduleName) continue;
|
|
233
|
+
let claims: ModuleFileClaim[];
|
|
234
|
+
try {
|
|
235
|
+
claims = collectDocumentFileClaims([doc], registry);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (err instanceof ArtifactSelectorError) continue;
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
const where = `${doc.kind}/${typeof metadata.name === "string" ? metadata.name : "(unnamed)"}`;
|
|
241
|
+
for (const claim of claims) out.push({ claim, where });
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The same claims, from one module's already-parsed documents — the form the
|
|
247
|
+
* analysis pass holds. */
|
|
248
|
+
export function collectDocumentFileClaims(
|
|
249
|
+
docs: readonly unknown[],
|
|
250
|
+
registry: TemplatingEngineRegistry = defaultRegistry(),
|
|
191
251
|
): ModuleFileClaim[] {
|
|
192
252
|
const seen = new Set<string>();
|
|
193
253
|
const claims: ModuleFileClaim[] = [];
|
|
194
|
-
for (const
|
|
195
|
-
const json = doc.toJSON() as unknown;
|
|
254
|
+
for (const json of docs) {
|
|
196
255
|
for (const claim of [
|
|
197
256
|
...libraryClaims(json),
|
|
198
257
|
...controllerClaims(json),
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules that relate a module's `native:` entries, its file claims and its
|
|
3
|
+
* `sources:` block — which files a source may stage, which declarations collide
|
|
4
|
+
* on one path, and which staged entries nothing names.
|
|
5
|
+
*
|
|
6
|
+
* One home, because `telo check` (`validate-native-entries.ts`,
|
|
7
|
+
* `validate-source-entries.ts`) and `telo publish` apply the same rules to the
|
|
8
|
+
* same manifest; two copies would each drift toward whatever its own tests pin.
|
|
9
|
+
* Callers read the three blocks with their own readers and hand the results in.
|
|
10
|
+
* Browser-safe: no filesystem.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { PLATFORM_AXES, describeSelector } from "./artifact-selector.js";
|
|
14
|
+
import type { LocatedClaim, ModuleFileClaim } from "./module-file-claims.js";
|
|
15
|
+
import type { NativeEntry } from "./native-entries.js";
|
|
16
|
+
import type { ModuleSource, SourceEntry } from "./source-entries.js";
|
|
17
|
+
|
|
18
|
+
const CLAIM_LABEL: Record<ModuleFileClaim["role"], string> = {
|
|
19
|
+
controller: "the controller candidate",
|
|
20
|
+
library: "the exports.code entry",
|
|
21
|
+
assets: "the embed",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** The declaration behind a claim, for messages: `the controller candidate pkg:… on Telo.Definition/Reader`. */
|
|
25
|
+
export function describeClaim(located: LocatedClaim): string {
|
|
26
|
+
const { claim, where } = located;
|
|
27
|
+
return `${CLAIM_LABEL[claim.role]} ${claim.origin}${where === undefined ? "" : ` on ${where}`}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A `native:` entry whose path another declaration also names. */
|
|
31
|
+
export interface NativeClaimConflict {
|
|
32
|
+
readonly entry: NativeEntry;
|
|
33
|
+
readonly claim: LocatedClaim;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Every `native:` entry whose path the module also names as code or as an embed.
|
|
38
|
+
* Each declaration decides a different layer, and a file extracts from exactly
|
|
39
|
+
* one — a runtime materializing one would find the file missing through the other.
|
|
40
|
+
*/
|
|
41
|
+
export function nativeClaimConflicts(
|
|
42
|
+
native: readonly NativeEntry[],
|
|
43
|
+
claims: readonly LocatedClaim[],
|
|
44
|
+
): NativeClaimConflict[] {
|
|
45
|
+
const out: NativeClaimConflict[] = [];
|
|
46
|
+
for (const entry of native) {
|
|
47
|
+
for (const claim of claims) {
|
|
48
|
+
if (claim.claim.path === entry.path) out.push({ entry, claim });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A file a `sources:` entry may stage. */
|
|
55
|
+
export interface StageableFile {
|
|
56
|
+
readonly path: string;
|
|
57
|
+
/** The declaration naming it, for messages. */
|
|
58
|
+
readonly label: string;
|
|
59
|
+
/** The layer it ships in, for messages. */
|
|
60
|
+
readonly layer: string;
|
|
61
|
+
/** Produced by a controller build (`local_path=`) rather than staged or committed. */
|
|
62
|
+
readonly built: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The files the module names that a `sources:` entry may stage: every `native:`
|
|
67
|
+
* entry's path, and every bundled controller candidate's `path=` that carries a
|
|
68
|
+
* platform qualifier. A platform-neutral candidate is one bundle for every host,
|
|
69
|
+
* which nothing stages.
|
|
70
|
+
*/
|
|
71
|
+
export function stageableFiles(
|
|
72
|
+
native: readonly NativeEntry[],
|
|
73
|
+
claims: readonly LocatedClaim[],
|
|
74
|
+
): Map<string, StageableFile> {
|
|
75
|
+
const out = new Map<string, StageableFile>();
|
|
76
|
+
for (const entry of native) {
|
|
77
|
+
if (out.has(entry.path)) continue;
|
|
78
|
+
out.set(entry.path, {
|
|
79
|
+
path: entry.path,
|
|
80
|
+
label: `${entry.origin} for ${describeSelector(entry.selector)}`,
|
|
81
|
+
layer: `native ${describeSelector(entry.selector)}`,
|
|
82
|
+
built: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (const { claim } of claims) {
|
|
86
|
+
if (claim.role !== "controller") continue;
|
|
87
|
+
if (!PLATFORM_AXES.some((axis) => claim.selector[axis] !== undefined)) continue;
|
|
88
|
+
const existing = out.get(claim.path);
|
|
89
|
+
if (existing) {
|
|
90
|
+
if (claim.localPath && !existing.built) out.set(claim.path, { ...existing, built: true });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out.set(claim.path, {
|
|
94
|
+
path: claim.path,
|
|
95
|
+
label: `controller candidate ${claim.origin}`,
|
|
96
|
+
layer: `controller ${describeSelector(claim.selector)}`,
|
|
97
|
+
built: claim.localPath !== undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A `sources:` entry, with the source declaring it. */
|
|
104
|
+
export interface LocatedSourceEntry {
|
|
105
|
+
readonly source: ModuleSource;
|
|
106
|
+
readonly entry: SourceEntry;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The staged entries nothing in the module names: not a stageable file, and not
|
|
111
|
+
* one of the entry's own source's notices. `alsoNamed` holds paths the caller
|
|
112
|
+
* knows are named though they did not read — a `native:` entry with an unrelated
|
|
113
|
+
* problem — so one defect is not reported twice.
|
|
114
|
+
*/
|
|
115
|
+
export function unclaimedSourceEntries(
|
|
116
|
+
sources: readonly ModuleSource[],
|
|
117
|
+
stageable: ReadonlyMap<string, StageableFile>,
|
|
118
|
+
alsoNamed: ReadonlySet<string> = new Set(),
|
|
119
|
+
): LocatedSourceEntry[] {
|
|
120
|
+
const out: LocatedSourceEntry[] = [];
|
|
121
|
+
for (const source of sources) {
|
|
122
|
+
for (const entry of source.entries) {
|
|
123
|
+
if (stageable.has(entry.path) || source.notices.includes(entry.path)) continue;
|
|
124
|
+
if (alsoNamed.has(entry.path)) continue;
|
|
125
|
+
out.push({ source, entry });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A link whose target ships in a different layer than the link. */
|
|
132
|
+
export interface CrossLayerLink {
|
|
133
|
+
readonly source: ModuleSource;
|
|
134
|
+
readonly entry: Extract<SourceEntry, { kind: "link" }>;
|
|
135
|
+
readonly layer: string;
|
|
136
|
+
readonly targetLayer: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The link entries whose target ships in another layer. A runtime extracts only
|
|
141
|
+
* the layers it needs, so such a link dangles wherever the other layer is not
|
|
142
|
+
* materialized. A notice ships in `common`.
|
|
143
|
+
*/
|
|
144
|
+
export function crossLayerSourceLinks(
|
|
145
|
+
sources: readonly ModuleSource[],
|
|
146
|
+
stageable: ReadonlyMap<string, StageableFile>,
|
|
147
|
+
): CrossLayerLink[] {
|
|
148
|
+
const layerOf = (source: ModuleSource, path: string): string | undefined =>
|
|
149
|
+
stageable.get(path)?.layer ?? (source.notices.includes(path) ? "common" : undefined);
|
|
150
|
+
const out: CrossLayerLink[] = [];
|
|
151
|
+
for (const source of sources) {
|
|
152
|
+
for (const entry of source.entries) {
|
|
153
|
+
if (entry.kind !== "link") continue;
|
|
154
|
+
const layer = layerOf(source, entry.path);
|
|
155
|
+
const targetLayer = layerOf(source, entry.resolved);
|
|
156
|
+
if (layer !== undefined && targetLayer !== undefined && layer !== targetLayer) {
|
|
157
|
+
out.push({ source, entry, layer, targetLayer });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|