@telorun/analyzer 0.53.0 → 0.55.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/analysis-registry.d.ts +20 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +36 -3
- package/dist/analyzer.d.ts +3 -2
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +188 -26
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +32 -12
- package/dist/call-graph.d.ts +189 -0
- package/dist/call-graph.d.ts.map +1 -0
- package/dist/call-graph.js +617 -0
- package/dist/dependency-graph.d.ts +17 -7
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +36 -65
- package/dist/flatten-for-analyzer.d.ts +8 -0
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +32 -0
- package/dist/index.d.ts +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/manifest-navigation.d.ts +32 -0
- package/dist/manifest-navigation.d.ts.map +1 -0
- package/dist/manifest-navigation.js +91 -0
- package/dist/manifest-visitor.js +1 -1
- package/dist/ref-slot.d.ts +125 -0
- package/dist/ref-slot.d.ts.map +1 -0
- package/dist/ref-slot.js +226 -0
- package/dist/reference-field-map.d.ts +15 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +29 -35
- package/dist/resolve-schema-ref-kinds.d.ts +4 -0
- package/dist/resolve-schema-ref-kinds.d.ts.map +1 -1
- package/dist/resolve-schema-ref-kinds.js +31 -8
- package/dist/resolve-zone-requirements.d.ts +110 -0
- package/dist/resolve-zone-requirements.d.ts.map +1 -0
- package/dist/resolve-zone-requirements.js +541 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-observed-state.d.ts +14 -13
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +21 -88
- package/dist/validate-ref-slots.d.ts +48 -0
- package/dist/validate-ref-slots.d.ts.map +1 -0
- package/dist/validate-ref-slots.js +219 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +8 -1
- package/dist/validate-zone-slots.d.ts +39 -0
- package/dist/validate-zone-slots.d.ts.map +1 -0
- package/dist/validate-zone-slots.js +114 -0
- package/dist/zone-module-documents.d.ts +27 -0
- package/dist/zone-module-documents.d.ts.map +1 -0
- package/dist/zone-module-documents.js +1 -0
- package/dist/zone-slot.d.ts +61 -0
- package/dist/zone-slot.d.ts.map +1 -0
- package/dist/zone-slot.js +91 -0
- package/package.json +3 -3
- package/src/analysis-registry.ts +36 -2
- package/src/analyzer.ts +206 -24
- package/src/builtins.ts +32 -12
- package/src/call-graph.ts +827 -0
- package/src/dependency-graph.ts +34 -68
- package/src/flatten-for-analyzer.ts +32 -0
- package/src/index.ts +47 -0
- package/src/manifest-navigation.ts +91 -0
- package/src/manifest-visitor.ts +1 -1
- package/src/ref-slot.ts +273 -0
- package/src/reference-field-map.ts +39 -36
- package/src/resolve-schema-ref-kinds.ts +34 -7
- package/src/resolve-zone-requirements.ts +781 -0
- package/src/types.ts +8 -0
- package/src/validate-observed-state.ts +26 -92
- package/src/validate-ref-slots.ts +293 -0
- package/src/validate-references.ts +8 -1
- package/src/validate-zone-slots.ts +175 -0
- package/src/zone-module-documents.ts +27 -0
- package/src/zone-slot.ts +116 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The zone projection — a CONSUMER of the call-graph service, with no traversal
|
|
3
|
+
* of its own (see `plans/execution-zones.md`): it filters the graph's edges by
|
|
4
|
+
* `use`, propagates zone requirements callee→caller along `call` edges,
|
|
5
|
+
* discharges them at providing slots under the correlation rule, and fires at
|
|
6
|
+
* terminating edges and at boot.
|
|
7
|
+
*
|
|
8
|
+
* Polarity: zones UNDER-approximate. A requirement is asserted only where the
|
|
9
|
+
* manifest states it, a correlation only where a key pointer resolves, and an
|
|
10
|
+
* edge whose `use` is unknown neither propagates nor terminates — it warns.
|
|
11
|
+
* The runtime (`requireZone`) stays the enforcement; a path this pass cannot
|
|
12
|
+
* see degrades to the runtime error at the right place, never to silence.
|
|
13
|
+
*
|
|
14
|
+
* Browser-safe: no Node built-ins.
|
|
15
|
+
*/
|
|
16
|
+
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
17
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
18
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
19
|
+
import {
|
|
20
|
+
buildCallGraph,
|
|
21
|
+
type CallGraph,
|
|
22
|
+
type CallGraphEdge,
|
|
23
|
+
type ResourceGraphNode,
|
|
24
|
+
} from "./call-graph.js";
|
|
25
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
26
|
+
import {
|
|
27
|
+
enclosingOf,
|
|
28
|
+
propertySchemas,
|
|
29
|
+
resolveLocalRef,
|
|
30
|
+
} from "./manifest-navigation.js";
|
|
31
|
+
import type { ZoneModuleDocuments } from "./zone-module-documents.js";
|
|
32
|
+
import { readProvidesZone, readRequiresZone } from "./zone-slot.js";
|
|
33
|
+
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
34
|
+
|
|
35
|
+
const SOURCE = "telo-analyzer";
|
|
36
|
+
|
|
37
|
+
/** One open requirement on a library's exported resource — the contract an
|
|
38
|
+
* importer must satisfy. Plain data, so it caches and crosses the
|
|
39
|
+
* per-library derivation boundary. */
|
|
40
|
+
export interface ZoneRequirementSpec {
|
|
41
|
+
/** Canonical `<module>.<Kind>` of the required zone. */
|
|
42
|
+
zone: string;
|
|
43
|
+
/** Correlation identity — the resolved declaration site of the instance the
|
|
44
|
+
* requirement correlates on (see {@link correlationIdOf}). Absent =
|
|
45
|
+
* uncorrelated. */
|
|
46
|
+
correlation?: string;
|
|
47
|
+
/** Human label of the correlation target (`sqlite.Connection 'billingDb'`). */
|
|
48
|
+
correlationLabel?: string;
|
|
49
|
+
/** Bare name of the correlation target — what the export-satisfiability
|
|
50
|
+
* check compares against `exports.resources`. */
|
|
51
|
+
correlationName?: string;
|
|
52
|
+
reason?: string;
|
|
53
|
+
/** Label of the requiring resource (`sql.Command 'charge'`). */
|
|
54
|
+
origin: string;
|
|
55
|
+
/** Resource names on the propagation path so far, origin first. */
|
|
56
|
+
via: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Per-library derived export contracts: export name → open requirements. */
|
|
60
|
+
export type ZoneExportRequirements = Map<string, ZoneRequirementSpec[]>;
|
|
61
|
+
|
|
62
|
+
export interface ZoneExportCacheEntry {
|
|
63
|
+
signature: string;
|
|
64
|
+
exports: ZoneExportRequirements;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Host-lifetime cache for per-library export derivation, keyed by the
|
|
69
|
+
* library's source identity with a content signature guarding staleness. The
|
|
70
|
+
* HOST owns it (the editor re-analyzes on every keystroke and must not rebuild
|
|
71
|
+
* every dependency's graph each time); the CLI passes none. It deliberately
|
|
72
|
+
* does not live in `AnalysisRegistry`, which the editor constructs fresh per
|
|
73
|
+
* closure per run — a cache there dies at exactly the boundary it must cross.
|
|
74
|
+
*/
|
|
75
|
+
export type ZoneExportCache = Map<string, ZoneExportCacheEntry>;
|
|
76
|
+
|
|
77
|
+
interface Requirement {
|
|
78
|
+
zone: string;
|
|
79
|
+
/** Kinds that discharge it: the zone kind plus everything extending it. */
|
|
80
|
+
accepted: ReadonlySet<string>;
|
|
81
|
+
correlation?: string;
|
|
82
|
+
correlationLabel?: string;
|
|
83
|
+
correlationName?: string;
|
|
84
|
+
reason?: string;
|
|
85
|
+
origin: string;
|
|
86
|
+
/** Identity for memoization / dedup: zone + correlation. */
|
|
87
|
+
key: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface ProjectionArgs {
|
|
91
|
+
graph: CallGraph;
|
|
92
|
+
defs: DefinitionRegistry;
|
|
93
|
+
aliases: AliasResolver;
|
|
94
|
+
aliasesByModule: Map<string, AliasResolver>;
|
|
95
|
+
/** Modules whose files diagnostics may be reported against (the analysis
|
|
96
|
+
* entry's own modules). Empty = derive-only, report nothing. */
|
|
97
|
+
reportModules: ReadonlySet<string>;
|
|
98
|
+
/** Extra requirements seeded at forwarded export nodes, keyed
|
|
99
|
+
* `${module}\0${name}` — an imported library's derived contracts. */
|
|
100
|
+
seeds?: Map<string, ZoneRequirementSpec[]>;
|
|
101
|
+
/** When set, record every requirement that reaches a module-level resource
|
|
102
|
+
* this returns an export name for. */
|
|
103
|
+
exportOf?: (node: ResourceGraphNode) => string | undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ProjectionResult {
|
|
107
|
+
diagnostics: AnalysisDiagnostic[];
|
|
108
|
+
openExports: ZoneExportRequirements;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Resolve a possibly alias-form kind to its definition in the DECLARING
|
|
112
|
+
* module's scope — the same layering `AnalysisRegistry.resolveDefinitionIn`
|
|
113
|
+
* uses. */
|
|
114
|
+
function definitionResolver(
|
|
115
|
+
defs: DefinitionRegistry,
|
|
116
|
+
aliases: AliasResolver,
|
|
117
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
118
|
+
) {
|
|
119
|
+
return (kind: string, module?: string): ResourceDefinition | undefined => {
|
|
120
|
+
const scope = (module ? aliasesByModule.get(module) : undefined) ?? aliases;
|
|
121
|
+
const canonical = scope.resolveKind(kind);
|
|
122
|
+
return defs.resolve(kind) ?? (canonical ? defs.resolve(canonical) : undefined);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const canonicalOf = (def: ResourceDefinition): string =>
|
|
127
|
+
`${def.metadata.module}.${def.metadata.name}`;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Correlation identity — the resolved DECLARATION SITE, mirroring runtime
|
|
131
|
+
* instance identity exactly. A named module-level resource is
|
|
132
|
+
* `(declaring file, name)` — the file, not the owning module name, because a
|
|
133
|
+
* re-exported instance is forwarded once per re-exporting module under that
|
|
134
|
+
* module's name while its declaration site survives the copy. A
|
|
135
|
+
* `with:`-scoped resource is its scope site plus name (one instance per scope
|
|
136
|
+
* run); an inline declaration is its own generated node.
|
|
137
|
+
*/
|
|
138
|
+
function correlationIdOf(node: ResourceGraphNode): string {
|
|
139
|
+
if (node.scoped) return `${node.scopeOwner}\0${node.scopeSite}\0${node.name}`;
|
|
140
|
+
const source = (node.manifest.metadata as { source?: string } | undefined)?.source ?? "";
|
|
141
|
+
return `${source}\0${node.name}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const labelOf = (node: ResourceGraphNode): string => `${node.kind} '${node.name}'`;
|
|
145
|
+
|
|
146
|
+
/** The schema node at a field-map path (`steps`, `routes[].handler`), following
|
|
147
|
+
* `[]` into `items`, `{}` into `additionalProperties` and local `$defs` refs —
|
|
148
|
+
* where a slot's zone annotations live. */
|
|
149
|
+
function schemaNodeAt(
|
|
150
|
+
rootSchema: Record<string, any> | undefined,
|
|
151
|
+
slotPath: string,
|
|
152
|
+
): Record<string, any> | undefined {
|
|
153
|
+
if (!rootSchema) return undefined;
|
|
154
|
+
let current: Record<string, any> | undefined = rootSchema;
|
|
155
|
+
for (const segment of slotPath.split(".")) {
|
|
156
|
+
if (!current) return undefined;
|
|
157
|
+
const bare = segment.replace(/(\[\]|\{\})+$/g, "");
|
|
158
|
+
let next: Record<string, any> | undefined = propertySchemas(current).find(
|
|
159
|
+
([k]) => k === bare,
|
|
160
|
+
)?.[1];
|
|
161
|
+
for (const marker of segment.slice(bare.length).match(/\[\]|\{\}/g) ?? []) {
|
|
162
|
+
next = resolveLocalRef(
|
|
163
|
+
marker === "[]"
|
|
164
|
+
? (next?.items as Record<string, any> | undefined)
|
|
165
|
+
: (next?.additionalProperties as Record<string, any> | undefined),
|
|
166
|
+
rootSchema,
|
|
167
|
+
);
|
|
168
|
+
if (!next || typeof next !== "object") return undefined;
|
|
169
|
+
}
|
|
170
|
+
current = resolveLocalRef(next, rootSchema);
|
|
171
|
+
}
|
|
172
|
+
return current;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The LOCAL name a correlation-key hop resolves to, or undefined when the value
|
|
177
|
+
* is not a reference or does not name something local.
|
|
178
|
+
*
|
|
179
|
+
* **A cross-module reference is undefined, deliberately**, matching the kernel's
|
|
180
|
+
* `referencedName` exactly: `!ref Alias.name` names an instance in another
|
|
181
|
+
* module's scope, and the only index available here is flat and
|
|
182
|
+
* module-unscoped, so taking the bare name would bind to whatever local
|
|
183
|
+
* resource happens to share it. Correlation is an identity comparison — binding
|
|
184
|
+
* it to the wrong resource is worse than leaving it uncorrelated, which is the
|
|
185
|
+
* under-approximating direction the whole pass leans on. `Self.` is a local
|
|
186
|
+
* name written the long way and does resolve.
|
|
187
|
+
*
|
|
188
|
+
* This deliberately differs from `call-graph`'s `refTargetName`, which answers a
|
|
189
|
+
* different question (what an EDGE points at, cross-module included, for a graph
|
|
190
|
+
* whose consumers tolerate an unresolved target) — hence two functions rather
|
|
191
|
+
* than one shared helper.
|
|
192
|
+
*/
|
|
193
|
+
function refName(value: unknown): string | undefined {
|
|
194
|
+
if (isRefSentinel(value)) {
|
|
195
|
+
const source = value.source;
|
|
196
|
+
const dot = source.indexOf(".");
|
|
197
|
+
if (dot <= 0) return source;
|
|
198
|
+
return source.slice(0, dot) === "Self" ? source.slice(dot + 1) : undefined;
|
|
199
|
+
}
|
|
200
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
201
|
+
const v = value as Record<string, unknown>;
|
|
202
|
+
const pure =
|
|
203
|
+
typeof v.kind === "string" &&
|
|
204
|
+
typeof v.name === "string" &&
|
|
205
|
+
Object.keys(v).every((k) => k === "kind" || k === "name" || k === "alias" || k === "__ref");
|
|
206
|
+
if (!pure) return undefined;
|
|
207
|
+
const alias = v.alias;
|
|
208
|
+
if (typeof alias === "string" && alias !== "Self") return undefined;
|
|
209
|
+
return v.name as string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Resolve an ordered correlation-key pointer list against a resource's
|
|
214
|
+
* manifest, first hit winning — the static counterpart of the kernel's walk.
|
|
215
|
+
* A pointer may traverse a `!ref` into the referenced resource's own manifest
|
|
216
|
+
* (read field → resolve reference → read field); traversal is mechanical, so
|
|
217
|
+
* no kind is named here. Returns undefined when nothing resolves — the
|
|
218
|
+
* requirement then discharges uncorrelated, the under-approximating side.
|
|
219
|
+
*/
|
|
220
|
+
function resolveStaticKey(
|
|
221
|
+
start: ResourceGraphNode,
|
|
222
|
+
pointers: readonly string[],
|
|
223
|
+
resolveName: (name: string, from: ResourceGraphNode) => ResourceGraphNode | undefined,
|
|
224
|
+
): ResourceGraphNode | undefined {
|
|
225
|
+
for (const pointer of pointers) {
|
|
226
|
+
if (!pointer.startsWith("/")) continue;
|
|
227
|
+
let manifest: Record<string, unknown> | undefined = start.manifest as Record<string, unknown>;
|
|
228
|
+
let context = start;
|
|
229
|
+
let value: unknown = undefined;
|
|
230
|
+
let failed = false;
|
|
231
|
+
const segments = pointer.slice(1).split("/");
|
|
232
|
+
for (let i = 0; i < segments.length; i++) {
|
|
233
|
+
const segment = segments[i]!.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
234
|
+
if (i > 0) {
|
|
235
|
+
// Traverse the previous hop's reference into its declaration.
|
|
236
|
+
const name = refName(value);
|
|
237
|
+
const target = name ? resolveName(name, context) : undefined;
|
|
238
|
+
if (!target) {
|
|
239
|
+
failed = true;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
manifest = target.manifest as Record<string, unknown>;
|
|
243
|
+
context = target;
|
|
244
|
+
}
|
|
245
|
+
value = manifest?.[segment];
|
|
246
|
+
if (value === undefined || value === null) {
|
|
247
|
+
failed = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (failed) continue;
|
|
252
|
+
const terminalName = refName(value);
|
|
253
|
+
const target = terminalName ? resolveName(terminalName, context) : undefined;
|
|
254
|
+
if (target) return target;
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Anchor a provider-side single pointer at the object enclosing the slot,
|
|
260
|
+
* then resolve exactly like a requirer key. */
|
|
261
|
+
function resolveProviderKey(
|
|
262
|
+
provider: ResourceGraphNode,
|
|
263
|
+
edge: CallGraphEdge,
|
|
264
|
+
pointer: string,
|
|
265
|
+
resolveName: (name: string, from: ResourceGraphNode) => ResourceGraphNode | undefined,
|
|
266
|
+
): ResourceGraphNode | undefined {
|
|
267
|
+
const enclosing = enclosingOf(provider.manifest, edge.path);
|
|
268
|
+
if (enclosing === provider.manifest || enclosing === undefined) {
|
|
269
|
+
return resolveStaticKey(provider, [pointer], resolveName);
|
|
270
|
+
}
|
|
271
|
+
// Slot inside an array item: resolve the first hop off the item, further
|
|
272
|
+
// hops through references as usual.
|
|
273
|
+
const synthetic: ResourceGraphNode = { ...provider, manifest: enclosing as ResourceManifest };
|
|
274
|
+
return resolveStaticKey(synthetic, [pointer], resolveName);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Run the projection over one graph. */
|
|
278
|
+
export function projectZoneRequirements(args: ProjectionArgs): ProjectionResult {
|
|
279
|
+
const { graph, defs, aliases, aliasesByModule, reportModules, seeds, exportOf } = args;
|
|
280
|
+
const resolveDef = definitionResolver(defs, aliases, aliasesByModule);
|
|
281
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
282
|
+
const openExports: ZoneExportRequirements = new Map();
|
|
283
|
+
const reported = new Set<string>();
|
|
284
|
+
|
|
285
|
+
// Scope-local name index, so a scoped resource's pointers resolve against its
|
|
286
|
+
// scope siblings first — the order `ScopeContext` and `!ref` agree on.
|
|
287
|
+
const scopeLocal = new Map<string, Map<string, ResourceGraphNode>>();
|
|
288
|
+
for (const node of graph.nodes.values()) {
|
|
289
|
+
if (node.type !== "resource" || !node.scoped) continue;
|
|
290
|
+
const key = `${node.scopeOwner}\0${node.scopeSite}`;
|
|
291
|
+
let bucket = scopeLocal.get(key);
|
|
292
|
+
if (!bucket) scopeLocal.set(key, (bucket = new Map()));
|
|
293
|
+
bucket.set(node.name, node);
|
|
294
|
+
}
|
|
295
|
+
const resolveName = (
|
|
296
|
+
name: string,
|
|
297
|
+
from: ResourceGraphNode,
|
|
298
|
+
): ResourceGraphNode | undefined => {
|
|
299
|
+
if (from.scoped) {
|
|
300
|
+
const local = scopeLocal.get(`${from.scopeOwner}\0${from.scopeSite}`)?.get(name);
|
|
301
|
+
if (local) return local;
|
|
302
|
+
}
|
|
303
|
+
return graph.resourceByName(name);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const acceptedFor = (zone: string): ReadonlySet<string> => {
|
|
307
|
+
const out = new Set<string>([zone]);
|
|
308
|
+
for (const def of defs.getByExtends(zone)) {
|
|
309
|
+
const module = (def.metadata as { module?: string } | undefined)?.module;
|
|
310
|
+
if (module && def.metadata?.name) out.add(`${module}.${def.metadata.name as string}`);
|
|
311
|
+
}
|
|
312
|
+
return out;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const moduleOf = (node: ResourceGraphNode): string | undefined =>
|
|
316
|
+
(node.manifest.metadata as { module?: string } | undefined)?.module;
|
|
317
|
+
|
|
318
|
+
const reportable = (node: ResourceGraphNode): boolean => {
|
|
319
|
+
const module = moduleOf(node);
|
|
320
|
+
return module === undefined || reportModules.has(module);
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const emit = (
|
|
324
|
+
severity: DiagnosticSeverity,
|
|
325
|
+
code: string,
|
|
326
|
+
edge: CallGraphEdge,
|
|
327
|
+
caller: ResourceGraphNode,
|
|
328
|
+
req: Requirement,
|
|
329
|
+
via: string[],
|
|
330
|
+
why: string,
|
|
331
|
+
): void => {
|
|
332
|
+
if (!reportable(caller)) return;
|
|
333
|
+
const dedupe = `${code}\0${edge.from}\0${edge.path}\0${req.key}`;
|
|
334
|
+
if (reported.has(dedupe)) return;
|
|
335
|
+
reported.add(dedupe);
|
|
336
|
+
const wanted = req.correlationLabel
|
|
337
|
+
? `a ${req.zone} zone on ${req.correlationLabel}`
|
|
338
|
+
: `a ${req.zone} zone`;
|
|
339
|
+
const path = [...via, `${caller.name}.${edge.path}`].join(" → ");
|
|
340
|
+
const reason = req.reason ? ` ${req.reason}.` : "";
|
|
341
|
+
diagnostics.push({
|
|
342
|
+
severity,
|
|
343
|
+
code,
|
|
344
|
+
source: SOURCE,
|
|
345
|
+
message: `${req.origin} requires ${wanted}, and the path ${path} ${why}.${reason}`,
|
|
346
|
+
data: {
|
|
347
|
+
resource: { kind: caller.kind, name: caller.name },
|
|
348
|
+
filePath: (caller.manifest.metadata as { source?: string } | undefined)?.source,
|
|
349
|
+
path: edge.path,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const visited = new Set<string>();
|
|
355
|
+
|
|
356
|
+
const propagate = (node: ResourceGraphNode, req: Requirement, via: string[]): void => {
|
|
357
|
+
const visitKey = `${node.id}\0${req.key}`;
|
|
358
|
+
if (visited.has(visitKey)) return;
|
|
359
|
+
visited.add(visitKey);
|
|
360
|
+
|
|
361
|
+
// A requirement reaching an exported instance is part of that export's
|
|
362
|
+
// contract — recorded, and propagation continues (the export may also be
|
|
363
|
+
// reached internally, where an enclosing provider can discharge it).
|
|
364
|
+
const exportName = exportOf?.(node);
|
|
365
|
+
if (exportName !== undefined) {
|
|
366
|
+
let bucket = openExports.get(exportName);
|
|
367
|
+
if (!bucket) openExports.set(exportName, (bucket = []));
|
|
368
|
+
if (!bucket.some((r) => `${r.zone}\0${r.correlation ?? ""}` === req.key)) {
|
|
369
|
+
bucket.push({
|
|
370
|
+
zone: req.zone,
|
|
371
|
+
correlation: req.correlation,
|
|
372
|
+
correlationLabel: req.correlationLabel,
|
|
373
|
+
correlationName: req.correlationName,
|
|
374
|
+
reason: req.reason,
|
|
375
|
+
origin: req.origin,
|
|
376
|
+
via: [...via],
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
for (const edge of graph.edgesTo(node.id)) {
|
|
382
|
+
const from = graph.nodes.get(edge.from);
|
|
383
|
+
if (!from) continue;
|
|
384
|
+
const caller =
|
|
385
|
+
from.type === "step"
|
|
386
|
+
? (graph.nodes.get(from.owner) as ResourceGraphNode | undefined)
|
|
387
|
+
: from;
|
|
388
|
+
if (!caller || caller.type !== "resource") continue;
|
|
389
|
+
|
|
390
|
+
// Discharge: the slot provides a zone whose kind satisfies the
|
|
391
|
+
// requirement and whose correlation payload is the same declaration
|
|
392
|
+
// site. Checked BEFORE termination, so a terminating provider slot
|
|
393
|
+
// (a detached durable body) still discharges its own zone.
|
|
394
|
+
const callerDef = resolveDef(caller.kind, moduleOf(caller));
|
|
395
|
+
const slotSchema = schemaNodeAt(
|
|
396
|
+
callerDef?.schema as Record<string, any> | undefined,
|
|
397
|
+
edge.slot,
|
|
398
|
+
);
|
|
399
|
+
const provides = readProvidesZone(slotSchema);
|
|
400
|
+
if (provides && callerDef && req.accepted.has(canonicalOf(callerDef))) {
|
|
401
|
+
const providerKey = provides.key
|
|
402
|
+
? resolveProviderKey(caller, edge, provides.key, resolveName)
|
|
403
|
+
: undefined;
|
|
404
|
+
const discharged =
|
|
405
|
+
req.correlation === undefined ||
|
|
406
|
+
(providerKey !== undefined && correlationIdOf(providerKey) === req.correlation);
|
|
407
|
+
if (discharged) continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// A dynamic selector is already a hard diagnostic (validate-ref-slots);
|
|
411
|
+
// zones do not re-report it — and must not propagate through a use they
|
|
412
|
+
// cannot read.
|
|
413
|
+
if (edge.unresolvedReason === "dynamic") continue;
|
|
414
|
+
|
|
415
|
+
if (edge.use.length === 0) {
|
|
416
|
+
emit(
|
|
417
|
+
DiagnosticSeverity.Warning,
|
|
418
|
+
"ZONE_REQUIREMENT_DEFERRED",
|
|
419
|
+
edge,
|
|
420
|
+
caller,
|
|
421
|
+
req,
|
|
422
|
+
via,
|
|
423
|
+
"reaches a slot that declares no use, so whether the zone survives it cannot be decided statically; the runtime check remains the enforcement",
|
|
424
|
+
);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (edge.unresolved) {
|
|
428
|
+
emit(
|
|
429
|
+
DiagnosticSeverity.Warning,
|
|
430
|
+
"ZONE_REQUIREMENT_DEFERRED",
|
|
431
|
+
edge,
|
|
432
|
+
caller,
|
|
433
|
+
req,
|
|
434
|
+
via,
|
|
435
|
+
`reaches a slot whose use selector could not be resolved (${edge.unresolvedReason}), so whether the zone survives it cannot be decided statically; the runtime check remains the enforcement`,
|
|
436
|
+
);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (edge.use.every((u) => u === "dependency" || u === "schema")) continue;
|
|
440
|
+
|
|
441
|
+
if (edge.use.length > 1) {
|
|
442
|
+
// The zone's lifetime extends through the edge only if EVERY member is
|
|
443
|
+
// `call` — a set says several relations hold at once, not that one of
|
|
444
|
+
// them might.
|
|
445
|
+
if (edge.use.every((u) => u === "call")) {
|
|
446
|
+
continueUp(caller, edge, req, via);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// A member the runtime guarantees is cleared makes the set decidably
|
|
450
|
+
// wrong, not undecidable: the detached dispatch is NEVER inside the
|
|
451
|
+
// caller's zone, so the requirement — a universal claim — is violated
|
|
452
|
+
// on that path. What stays unknown is only how often that path is
|
|
453
|
+
// taken, and "sometimes throws" is not a working manifest.
|
|
454
|
+
//
|
|
455
|
+
// This is the one place the plan's original rule was inverted, and
|
|
456
|
+
// deliberately. That rule existed to stop `Cache.View`'s UNCONDITIONAL
|
|
457
|
+
// `[call, detached]` from hard-erroring every cached transactional call
|
|
458
|
+
// under the default `revalidate: sync`, where the controller never
|
|
459
|
+
// detaches — but the same change that added this pass re-annotated that
|
|
460
|
+
// slot as a case map, so a set now appears only where its detach really
|
|
461
|
+
// happens. The justification went with the annotation.
|
|
462
|
+
const guaranteedCleared = edge.use.filter(
|
|
463
|
+
(u) => u === "detached" || u === "trigger.inbound",
|
|
464
|
+
);
|
|
465
|
+
if (guaranteedCleared.length > 0) {
|
|
466
|
+
emit(
|
|
467
|
+
DiagnosticSeverity.Error,
|
|
468
|
+
"ZONE_REQUIREMENT_UNSATISFIED",
|
|
469
|
+
edge,
|
|
470
|
+
caller,
|
|
471
|
+
req,
|
|
472
|
+
via,
|
|
473
|
+
`reaches a slot that dispatches its target several ways in one invocation ([${edge.use.join(", ")}]), and the ${guaranteedCleared.join(" / ")} dispatch is guaranteed a fresh context — so on that path the zone is gone`,
|
|
474
|
+
);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
// Everything else in a set is undecidable rather than wrong — a
|
|
478
|
+
// `trigger.consumer` member means a drain site MIGHT be inside the
|
|
479
|
+
// zone, which no static reading can settle.
|
|
480
|
+
emit(
|
|
481
|
+
DiagnosticSeverity.Warning,
|
|
482
|
+
"ZONE_REQUIREMENT_DEFERRED",
|
|
483
|
+
edge,
|
|
484
|
+
caller,
|
|
485
|
+
req,
|
|
486
|
+
via,
|
|
487
|
+
`reaches a slot whose declared use is the set [${edge.use.join(", ")}]; whether the zone survives depends on where the consumer drains it, so it cannot be decided statically and the runtime check remains the enforcement`,
|
|
488
|
+
);
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
switch (edge.use[0]) {
|
|
493
|
+
case "call":
|
|
494
|
+
continueUp(caller, edge, req, via);
|
|
495
|
+
break;
|
|
496
|
+
case "detached":
|
|
497
|
+
emit(
|
|
498
|
+
DiagnosticSeverity.Error,
|
|
499
|
+
"ZONE_REQUIREMENT_UNSATISFIED",
|
|
500
|
+
edge,
|
|
501
|
+
caller,
|
|
502
|
+
req,
|
|
503
|
+
via,
|
|
504
|
+
"detaches there — the runtime guarantees the detached work a fresh context, outside every zone its caller was in",
|
|
505
|
+
);
|
|
506
|
+
break;
|
|
507
|
+
case "trigger.inbound":
|
|
508
|
+
emit(
|
|
509
|
+
DiagnosticSeverity.Error,
|
|
510
|
+
"ZONE_REQUIREMENT_UNSATISFIED",
|
|
511
|
+
edge,
|
|
512
|
+
caller,
|
|
513
|
+
req,
|
|
514
|
+
via,
|
|
515
|
+
"is an inbound trigger registration — the handler runs on a fresh context driven by a request or timer, outside every zone",
|
|
516
|
+
);
|
|
517
|
+
break;
|
|
518
|
+
case "trigger.consumer":
|
|
519
|
+
emit(
|
|
520
|
+
DiagnosticSeverity.Warning,
|
|
521
|
+
"ZONE_REQUIREMENT_DEFERRED",
|
|
522
|
+
edge,
|
|
523
|
+
caller,
|
|
524
|
+
req,
|
|
525
|
+
via,
|
|
526
|
+
"is dispatched when a returned value is drained, so where it runs is the drain site's choice; the runtime check remains the enforcement",
|
|
527
|
+
);
|
|
528
|
+
break;
|
|
529
|
+
// dependency / schema singletons were filtered above.
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const continueUp = (
|
|
535
|
+
caller: ResourceGraphNode,
|
|
536
|
+
edge: CallGraphEdge,
|
|
537
|
+
req: Requirement,
|
|
538
|
+
via: string[],
|
|
539
|
+
): void => {
|
|
540
|
+
// Nothing encloses an Application's boot targets: an open requirement
|
|
541
|
+
// arriving there surfaces at boot.
|
|
542
|
+
if (caller.kind === "Telo.Application") {
|
|
543
|
+
emit(
|
|
544
|
+
DiagnosticSeverity.Error,
|
|
545
|
+
"ZONE_REQUIREMENT_UNSATISFIED",
|
|
546
|
+
edge,
|
|
547
|
+
caller,
|
|
548
|
+
req,
|
|
549
|
+
via,
|
|
550
|
+
"reaches the application's boot targets, which nothing encloses",
|
|
551
|
+
);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
propagate(caller, req, [...via, caller.name]);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// ── Origins: instances of the entry graph whose annotated field is present.
|
|
558
|
+
for (const node of graph.nodes.values()) {
|
|
559
|
+
if (node.type !== "resource") continue;
|
|
560
|
+
const meta = node.manifest.metadata as
|
|
561
|
+
| { module?: string; forwardedExport?: boolean }
|
|
562
|
+
| undefined;
|
|
563
|
+
// A forwarded export's requirements are derived by ITS library's own stage
|
|
564
|
+
// (with the internal graph in hand) and seeded below — deriving them here
|
|
565
|
+
// against the flattened view would resolve correlation against a graph
|
|
566
|
+
// that no longer holds the library's internals.
|
|
567
|
+
if (meta?.forwardedExport) continue;
|
|
568
|
+
const def = resolveDef(node.kind, meta?.module);
|
|
569
|
+
const schema = def?.schema as Record<string, any> | undefined;
|
|
570
|
+
if (!def || !schema?.properties) continue;
|
|
571
|
+
for (const [field, propSchema] of Object.entries(
|
|
572
|
+
schema.properties as Record<string, Record<string, any>>,
|
|
573
|
+
)) {
|
|
574
|
+
const requires = readRequiresZone(propSchema);
|
|
575
|
+
if (!requires) continue;
|
|
576
|
+
if ((node.manifest as Record<string, unknown>)[field] === undefined) continue;
|
|
577
|
+
const zoneDef = resolveDef(requires.zone, def.metadata.module);
|
|
578
|
+
if (!zoneDef) continue; // ZONE_PROVIDER_UNRESOLVED is reported at registration
|
|
579
|
+
const zone = canonicalOf(zoneDef);
|
|
580
|
+
const target = resolveStaticKey(node, requires.key, resolveName);
|
|
581
|
+
const req: Requirement = {
|
|
582
|
+
zone,
|
|
583
|
+
accepted: acceptedFor(zone),
|
|
584
|
+
correlation: target ? correlationIdOf(target) : undefined,
|
|
585
|
+
correlationLabel: target ? labelOf(target) : undefined,
|
|
586
|
+
correlationName: target?.name,
|
|
587
|
+
reason: requires.reason,
|
|
588
|
+
origin: labelOf(node),
|
|
589
|
+
key: "",
|
|
590
|
+
};
|
|
591
|
+
req.key = `${req.zone}\0${req.correlation ?? ""}`;
|
|
592
|
+
propagate(node, req, [node.name]);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ── Seeds: imported libraries' derived export contracts, attached at the
|
|
597
|
+
// forwarded nodes the importer's graph actually holds.
|
|
598
|
+
if (seeds && seeds.size > 0) {
|
|
599
|
+
for (const node of graph.nodes.values()) {
|
|
600
|
+
if (node.type !== "resource") continue;
|
|
601
|
+
const meta = node.manifest.metadata as
|
|
602
|
+
| { module?: string; forwardedExport?: boolean }
|
|
603
|
+
| undefined;
|
|
604
|
+
if (!meta?.forwardedExport || !meta.module) continue;
|
|
605
|
+
const specs = seeds.get(`${meta.module}\0${node.name}`);
|
|
606
|
+
if (!specs) continue;
|
|
607
|
+
for (const spec of specs) {
|
|
608
|
+
const zoneDef = defs.resolve(spec.zone);
|
|
609
|
+
const req: Requirement = {
|
|
610
|
+
zone: spec.zone,
|
|
611
|
+
accepted: zoneDef ? acceptedFor(spec.zone) : new Set([spec.zone]),
|
|
612
|
+
correlation: spec.correlation,
|
|
613
|
+
correlationLabel: spec.correlationLabel,
|
|
614
|
+
correlationName: spec.correlationName,
|
|
615
|
+
reason: spec.reason,
|
|
616
|
+
origin: spec.origin,
|
|
617
|
+
key: `${spec.zone}\0${spec.correlation ?? ""}`,
|
|
618
|
+
};
|
|
619
|
+
propagate(node, req, [...spec.via, node.name]);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return { diagnostics, openExports };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** Content signature over a library's loaded documents — what makes a
|
|
628
|
+
* workspace library invalidate on the keystroke that changed it while a
|
|
629
|
+
* published library (immutable bytes) hits every time. FNV-1a over the JSON
|
|
630
|
+
* projection; collisions only stale a warning-level derivation, and the
|
|
631
|
+
* projection is cheap relative to the graph build it guards. */
|
|
632
|
+
export function zoneDocumentsSignature(manifests: readonly ResourceManifest[]): string {
|
|
633
|
+
let hash = 0x811c9dc5;
|
|
634
|
+
const mix = (text: string): void => {
|
|
635
|
+
for (let i = 0; i < text.length; i++) {
|
|
636
|
+
hash ^= text.charCodeAt(i);
|
|
637
|
+
hash = Math.imul(hash, 0x01000193);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
for (const m of manifests) mix(JSON.stringify(m) ?? "");
|
|
641
|
+
return (hash >>> 0).toString(16);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Derive one library's export contracts: build the library-scoped graph from
|
|
646
|
+
* its full documents (which the flattened analysis view no longer holds), run
|
|
647
|
+
* the projection derive-only, and keep what reaches an exported instance.
|
|
648
|
+
*/
|
|
649
|
+
export function deriveLibraryExportRequirements(
|
|
650
|
+
docs: ZoneModuleDocuments,
|
|
651
|
+
defs: DefinitionRegistry,
|
|
652
|
+
aliases: AliasResolver,
|
|
653
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
654
|
+
cache?: ZoneExportCache,
|
|
655
|
+
): ZoneExportRequirements {
|
|
656
|
+
const signature = docs.signature ?? zoneDocumentsSignature(docs.manifests);
|
|
657
|
+
const cached = cache?.get(docs.sourceId);
|
|
658
|
+
if (cached && cached.signature === signature) return cached.exports;
|
|
659
|
+
|
|
660
|
+
const exported = new Set(docs.exportedNames);
|
|
661
|
+
const graph = buildCallGraph(docs.manifests, defs, { aliases, aliasesByModule });
|
|
662
|
+
const { openExports } = projectZoneRequirements({
|
|
663
|
+
graph,
|
|
664
|
+
defs,
|
|
665
|
+
aliases,
|
|
666
|
+
aliasesByModule,
|
|
667
|
+
reportModules: new Set(),
|
|
668
|
+
exportOf: (node) =>
|
|
669
|
+
!node.scoped && exported.has(node.name) ? node.name : undefined,
|
|
670
|
+
});
|
|
671
|
+
cache?.set(docs.sourceId, { signature, exports: openExports });
|
|
672
|
+
return openExports;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export interface ZoneAnalysisArgs {
|
|
676
|
+
/** The entry analysis set (post inline-normalization + sentinel resolution). */
|
|
677
|
+
manifests: ResourceManifest[];
|
|
678
|
+
graph: CallGraph;
|
|
679
|
+
defs: DefinitionRegistry;
|
|
680
|
+
aliases: AliasResolver;
|
|
681
|
+
aliasesByModule: Map<string, AliasResolver>;
|
|
682
|
+
rootModules: ReadonlySet<string>;
|
|
683
|
+
moduleDocuments?: readonly ZoneModuleDocuments[];
|
|
684
|
+
cache?: ZoneExportCache;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The zone stage of `analyze()`: per-library export derivation (cached), the
|
|
689
|
+
* entry projection with those contracts seeded, and — when the entry is a
|
|
690
|
+
* library — the export-satisfiability check, at the one desk where it is
|
|
691
|
+
* fixable.
|
|
692
|
+
*/
|
|
693
|
+
export function runZoneAnalysis(args: ZoneAnalysisArgs): AnalysisDiagnostic[] {
|
|
694
|
+
const { manifests, graph, defs, aliases, aliasesByModule, rootModules } = args;
|
|
695
|
+
|
|
696
|
+
// Per-library export contracts, derived over each library's own documents.
|
|
697
|
+
const seeds = new Map<string, ZoneRequirementSpec[]>();
|
|
698
|
+
for (const docs of args.moduleDocuments ?? []) {
|
|
699
|
+
const contracts = deriveLibraryExportRequirements(
|
|
700
|
+
docs,
|
|
701
|
+
defs,
|
|
702
|
+
aliases,
|
|
703
|
+
aliasesByModule,
|
|
704
|
+
args.cache,
|
|
705
|
+
);
|
|
706
|
+
for (const [exportName, specs] of contracts) {
|
|
707
|
+
if (specs.length > 0) seeds.set(`${docs.module}\0${exportName}`, specs);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// The entry library's own export surface, for the satisfiability check.
|
|
712
|
+
const rootLibraries: Array<{ module: string; exportedNames: Set<string>; doc: ResourceManifest }> =
|
|
713
|
+
[];
|
|
714
|
+
for (const m of manifests) {
|
|
715
|
+
if (m.kind !== "Telo.Library") continue;
|
|
716
|
+
const name = m.metadata?.name as string | undefined;
|
|
717
|
+
if (!name || !rootModules.has(name)) continue;
|
|
718
|
+
const exportedNames = new Set<string>();
|
|
719
|
+
for (const entry of (m as { exports?: { resources?: unknown[] } }).exports?.resources ?? []) {
|
|
720
|
+
if (typeof entry !== "string") continue;
|
|
721
|
+
const dot = entry.indexOf(".");
|
|
722
|
+
exportedNames.add(dot > 0 ? entry.slice(dot + 1) : entry);
|
|
723
|
+
}
|
|
724
|
+
if (exportedNames.size > 0) rootLibraries.push({ module: name, exportedNames, doc: m });
|
|
725
|
+
}
|
|
726
|
+
const exportOf =
|
|
727
|
+
rootLibraries.length > 0
|
|
728
|
+
? (node: ResourceGraphNode): string | undefined => {
|
|
729
|
+
if (node.scoped) return undefined;
|
|
730
|
+
const module = (node.manifest.metadata as { module?: string } | undefined)?.module;
|
|
731
|
+
const lib = rootLibraries.find((l) => l.module === module);
|
|
732
|
+
return lib?.exportedNames.has(node.name) ? node.name : undefined;
|
|
733
|
+
}
|
|
734
|
+
: undefined;
|
|
735
|
+
|
|
736
|
+
const { diagnostics, openExports } = projectZoneRequirements({
|
|
737
|
+
graph,
|
|
738
|
+
defs,
|
|
739
|
+
aliases,
|
|
740
|
+
aliasesByModule,
|
|
741
|
+
reportModules: rootModules,
|
|
742
|
+
seeds: seeds.size > 0 ? seeds : undefined,
|
|
743
|
+
exportOf,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// An export whose requirement correlates on a resource importers cannot
|
|
747
|
+
// reach is unsatisfiable by construction — raised HERE, at the exporting
|
|
748
|
+
// library, never by an importer against a file it does not own.
|
|
749
|
+
for (const lib of rootLibraries) {
|
|
750
|
+
for (const [exportName, specs] of openExports) {
|
|
751
|
+
if (!lib.exportedNames.has(exportName)) continue;
|
|
752
|
+
const node = graph.resourceByName(exportName);
|
|
753
|
+
if (!node || (node.manifest.metadata as { module?: string } | undefined)?.module !== lib.module) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
for (const spec of specs) {
|
|
757
|
+
if (spec.correlationName === undefined) continue; // uncorrelated — satisfiable by any zone
|
|
758
|
+
if (lib.exportedNames.has(spec.correlationName)) continue; // importers can reach it
|
|
759
|
+
diagnostics.push({
|
|
760
|
+
severity: DiagnosticSeverity.Error,
|
|
761
|
+
code: "ZONE_EXPORT_UNSATISFIABLE",
|
|
762
|
+
source: SOURCE,
|
|
763
|
+
message:
|
|
764
|
+
`exported resource '${exportName}' carries an open requirement: ${spec.origin} requires ` +
|
|
765
|
+
`a ${spec.zone} zone on ${spec.correlationLabel}, which this library does not export — ` +
|
|
766
|
+
`no importer can satisfy it. Export '${spec.correlationName}' too (importers wrap ` +
|
|
767
|
+
`'${exportName}' in their own zone on it), or export a resource that goes through the ` +
|
|
768
|
+
`provider instead of '${exportName}' directly.` +
|
|
769
|
+
(spec.reason ? ` ${spec.reason}.` : ""),
|
|
770
|
+
data: {
|
|
771
|
+
resource: { kind: node.kind, name: node.name },
|
|
772
|
+
filePath: (node.manifest.metadata as { source?: string } | undefined)?.source,
|
|
773
|
+
path: "",
|
|
774
|
+
},
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return diagnostics;
|
|
781
|
+
}
|