@telorun/analyzer 0.70.0 → 0.71.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.map +1 -1
- package/dist/analysis-registry.js +4 -5
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +50 -8
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +10 -4
- package/dist/cel-scope-query.d.ts +14 -0
- package/dist/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +36 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +3 -4
- package/dist/flatten-for-analyzer.d.ts +2 -2
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +11 -3
- package/dist/module-alias-scope.d.ts +65 -0
- package/dist/module-alias-scope.d.ts.map +1 -0
- package/dist/module-alias-scope.js +25 -0
- package/dist/{zone-module-documents.d.ts → module-documents.d.ts} +11 -7
- package/dist/module-documents.d.ts.map +1 -0
- package/dist/ref-sentinel-target.d.ts +38 -0
- package/dist/ref-sentinel-target.d.ts.map +1 -0
- package/dist/ref-sentinel-target.js +13 -0
- package/dist/resolve-schema-type-refs.d.ts.map +1 -1
- package/dist/resolve-schema-type-refs.js +2 -1
- package/dist/resolve-throws-union.d.ts +18 -1
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +93 -9
- package/dist/resolve-zone-requirements.d.ts +3 -3
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +12 -12
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +2 -4
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-invocation-contract.d.ts +5 -0
- package/dist/validate-invocation-contract.d.ts.map +1 -1
- package/dist/validate-invocation-contract.js +114 -3
- package/dist/validate-logging.d.ts.map +1 -1
- package/dist/validate-logging.js +2 -2
- package/dist/validate-references.d.ts +16 -5
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +30 -12
- package/dist/validate-resource-inputs.d.ts +1 -26
- package/dist/validate-resource-inputs.d.ts.map +1 -1
- package/dist/validate-resource-inputs.js +12 -2
- package/dist/validate-schema-type-refs.d.ts.map +1 -1
- package/dist/validate-schema-type-refs.js +2 -1
- package/dist/validate-throws-coverage.d.ts +5 -1
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +6 -2
- package/package.json +2 -2
- package/src/analysis-registry.ts +4 -5
- package/src/analyzer.ts +61 -10
- package/src/call-graph.ts +9 -3
- package/src/cel-scope-query.ts +45 -8
- package/src/definition-registry.ts +3 -6
- package/src/flatten-for-analyzer.ts +3 -3
- package/src/index.ts +2 -2
- package/src/manifest-visitor.ts +11 -3
- package/src/module-alias-scope.ts +94 -0
- package/src/{zone-module-documents.ts → module-documents.ts} +10 -6
- package/src/ref-sentinel-target.ts +46 -0
- package/src/resolve-schema-type-refs.ts +2 -1
- package/src/resolve-throws-union.ts +123 -8
- package/src/resolve-zone-requirements.ts +14 -14
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +2 -5
- package/src/types.ts +3 -3
- package/src/validate-invocation-contract.ts +128 -2
- package/src/validate-logging.ts +2 -3
- package/src/validate-references.ts +41 -11
- package/src/validate-resource-inputs.ts +26 -3
- package/src/validate-schema-type-refs.ts +2 -1
- package/src/validate-throws-coverage.ts +12 -1
- package/dist/zone-module-documents.d.ts.map +0 -1
- /package/dist/{zone-module-documents.js → module-documents.js} +0 -0
package/src/call-graph.ts
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
42
42
|
import { isRefSentinel, isTaggedSentinel } from "@telorun/templating";
|
|
43
|
+
import { refSentinelTarget } from "./ref-sentinel-target.js";
|
|
43
44
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
44
45
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
45
46
|
import {
|
|
@@ -63,6 +64,7 @@ import {
|
|
|
63
64
|
type RefFieldEntry,
|
|
64
65
|
} from "./reference-field-map.js";
|
|
65
66
|
import { DEPENDENCY_GRAPH_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
67
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
66
68
|
|
|
67
69
|
export interface ResourceGraphNode {
|
|
68
70
|
type: "resource";
|
|
@@ -693,8 +695,7 @@ export function buildCallGraph(
|
|
|
693
695
|
const definitionFor = (manifest: ResourceManifest): ResourceDefinition | undefined => {
|
|
694
696
|
const direct = registry.resolve(manifest.kind as string);
|
|
695
697
|
if (direct) return direct;
|
|
696
|
-
const
|
|
697
|
-
const scope = (module ? options.aliasesByModule?.get(module) : undefined) ?? options.aliases;
|
|
698
|
+
const scope = moduleAliasScope(manifest.metadata, options.aliases, options.aliasesByModule);
|
|
698
699
|
const canonical = scope?.resolveKind(manifest.kind as string);
|
|
699
700
|
return canonical ? registry.resolve(canonical) : undefined;
|
|
700
701
|
};
|
|
@@ -962,7 +963,12 @@ export function buildCallGraph(
|
|
|
962
963
|
* here: an unresolved `!ref <name>` sentinel and the `{kind, name}` object
|
|
963
964
|
* `resolveRefSentinels` rewrites it into. */
|
|
964
965
|
function refTargetName(value: unknown): string | undefined {
|
|
965
|
-
|
|
966
|
+
// An edge's target is taken VERBATIM — `resolveScopedName` and the node index
|
|
967
|
+
// are what resolve it, and a cross-module `Alias.name` is a target this graph
|
|
968
|
+
// legitimately carries unresolved. That is this pass's reduction over the
|
|
969
|
+
// shared parse, not a second reading of the tag.
|
|
970
|
+
const sentinel = refSentinelTarget(value);
|
|
971
|
+
if (sentinel) return sentinel.source;
|
|
966
972
|
if (!value || typeof value !== "object") return undefined;
|
|
967
973
|
const name = (value as Record<string, unknown>).name;
|
|
968
974
|
return typeof name === "string" ? name : undefined;
|
package/src/cel-scope-query.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { buildCelEnvironment } from "./cel-environment.js";
|
|
|
18
18
|
import { CelScopeResolver, type CelScope } from "./cel-scope.js";
|
|
19
19
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
20
20
|
import { buildKernelGlobalsIndex } from "./kernel-globals.js";
|
|
21
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
21
22
|
import { isModuleKind } from "./module-kinds.js";
|
|
22
23
|
import { navigateConcretePath } from "./manifest-path.js";
|
|
23
24
|
import { findManifest } from "./find-manifest.js";
|
|
@@ -41,6 +42,12 @@ export interface ContextDeclarationSite {
|
|
|
41
42
|
kind: string;
|
|
42
43
|
name: string;
|
|
43
44
|
path: string;
|
|
45
|
+
/** The declaring module, when the manifest carries one. Part of the identity,
|
|
46
|
+
* not decoration: a resource name is unique inside its module and not across a
|
|
47
|
+
* flattened set, so `(kind, name)` alone lets a host locating this site land on
|
|
48
|
+
* another library's same-named document — which for go-to-declaration means
|
|
49
|
+
* jumping to the wrong file with nothing to indicate it. */
|
|
50
|
+
module?: string;
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
/** Join a concrete path segment, tolerating an empty base (the manifest root). */
|
|
@@ -210,9 +217,14 @@ export class CelScopeQuery {
|
|
|
210
217
|
base = hit;
|
|
211
218
|
}
|
|
212
219
|
|
|
213
|
-
const metadata = origin.manifest.metadata as { name?: string } | undefined;
|
|
220
|
+
const metadata = origin.manifest.metadata as { name?: string; module?: string } | undefined;
|
|
214
221
|
if (!origin.manifest.kind || !metadata?.name) return undefined;
|
|
215
|
-
return {
|
|
222
|
+
return {
|
|
223
|
+
kind: origin.manifest.kind,
|
|
224
|
+
name: metadata.name,
|
|
225
|
+
path: base,
|
|
226
|
+
...(metadata.module === undefined ? {} : { module: metadata.module }),
|
|
227
|
+
};
|
|
216
228
|
}
|
|
217
229
|
|
|
218
230
|
/** The manifest and path an annotated context property is derived from, and
|
|
@@ -265,13 +277,29 @@ export class CelScopeQuery {
|
|
|
265
277
|
if (hash <= 0) return undefined;
|
|
266
278
|
const kindValue = navigateConcretePath(root, first.slice(0, hash).split("/").join("."));
|
|
267
279
|
if (typeof kindValue !== "string") return undefined;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
280
|
+
// The kind was read off THIS resource, so it is spelled in the alias scope
|
|
281
|
+
// of the module that declared it — not the entry's.
|
|
282
|
+
const scope = moduleAliasScope(resource.metadata, this.ctx.aliases, this.ctx.aliasesByModule);
|
|
283
|
+
const canonical = scope.resolveKind(kindValue) ?? kindValue;
|
|
284
|
+
const dot = canonical.indexOf(".");
|
|
285
|
+
const owningModule = dot === -1 ? undefined : canonical.slice(0, dot);
|
|
286
|
+
const suffix = canonical.slice(dot + 1);
|
|
287
|
+
// Matched by MODULE AND NAME once the canonical form carries a module: a
|
|
288
|
+
// definition name is unique inside its module and not across a flattened
|
|
289
|
+
// set, so two libraries each declaring an `Api` would otherwise resolve to
|
|
290
|
+
// whichever came first — and the editor would type a context region off the
|
|
291
|
+
// wrong kind's annotations, silently. Falls back to name alone only when the
|
|
292
|
+
// kind did not resolve to a canonical form, where there is nothing to narrow
|
|
293
|
+
// by and a best-effort match is still better than none.
|
|
294
|
+
const named = this.manifests.filter(
|
|
271
295
|
(m) =>
|
|
272
296
|
(m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") &&
|
|
273
297
|
(m.metadata as { name?: string } | undefined)?.name === suffix,
|
|
274
|
-
) as Record<string, any>
|
|
298
|
+
) as Record<string, any>[];
|
|
299
|
+
const target =
|
|
300
|
+
(owningModule
|
|
301
|
+
? named.find((m) => (m.metadata as { module?: string } | undefined)?.module === owningModule)
|
|
302
|
+
: undefined) ?? (owningModule && named.length > 1 ? undefined : named[0]);
|
|
275
303
|
if (!target) return undefined;
|
|
276
304
|
return { manifest: target, path: first.slice(hash + 1), propertyMap: false };
|
|
277
305
|
}
|
|
@@ -313,9 +341,18 @@ export class CelScopeQuery {
|
|
|
313
341
|
return undefined;
|
|
314
342
|
}
|
|
315
343
|
|
|
344
|
+
/** The kind's declaration, resolved in the scope of the module that DECLARED
|
|
345
|
+
* this resource — the same rule the manifest visitor applies, and the reason
|
|
346
|
+
* it has to be the same one: everything this query offers is read off the
|
|
347
|
+
* definition, so resolving a forwarded library resource's `kind: Http.Api`
|
|
348
|
+
* through the entry's aliases alone found nothing and the editor silently
|
|
349
|
+
* offered no `request` / `result` at a site `telo check` accepts. A
|
|
350
|
+
* completion list is a claim that the name it offers will pass the checker,
|
|
351
|
+
* so the two must resolve a kind identically. */
|
|
316
352
|
private definitionFor(resource: ResourceManifest): ResourceDefinition | undefined {
|
|
317
|
-
const { defs, aliases } = this.ctx;
|
|
318
|
-
const
|
|
353
|
+
const { defs, aliases, aliasesByModule } = this.ctx;
|
|
354
|
+
const scope = moduleAliasScope(resource.metadata, aliases, aliasesByModule);
|
|
355
|
+
const canonical = scope.resolveKind(resource.kind);
|
|
319
356
|
return defs.resolve(resource.kind) ?? (canonical ? defs.resolve(canonical) : undefined);
|
|
320
357
|
}
|
|
321
358
|
|
|
@@ -2,6 +2,7 @@ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import { canonicalTypeSchemaId } from "@telorun/sdk";
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
5
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
5
6
|
import {
|
|
6
7
|
buildFieldMapAtPath,
|
|
7
8
|
buildReferenceFieldMap,
|
|
@@ -372,9 +373,7 @@ export class DefinitionRegistry {
|
|
|
372
373
|
// (e.g. `Ai.AgentStream` in a library that imports `Ai`), which the root/global
|
|
373
374
|
// resolver doesn't know — using the global scope here left the base field map
|
|
374
375
|
// unresolved, so Phase-5 injection saw no ref fields and skipped injection.
|
|
375
|
-
const
|
|
376
|
-
const moduleScope =
|
|
377
|
-
(ownModule ? aliasesByModule.get(ownModule) : undefined) ?? aliases;
|
|
376
|
+
const moduleScope = moduleAliasScope(resource.metadata, aliases, aliasesByModule);
|
|
378
377
|
|
|
379
378
|
const baseMap = this.getFieldMapForKind(resource.kind, moduleScope);
|
|
380
379
|
if (!baseMap) return undefined;
|
|
@@ -383,9 +382,7 @@ export class DefinitionRegistry {
|
|
|
383
382
|
const def = this.resolve(resource.kind) ?? this.resolve(resolvedKind);
|
|
384
383
|
// schema-from anchors resolve in the DEFINITION's module scope (where the anchor
|
|
385
384
|
// kind is declared), which may differ from the resource's own module.
|
|
386
|
-
const
|
|
387
|
-
const ownerScope =
|
|
388
|
-
(ownerModule ? aliasesByModule.get(ownerModule) : undefined) ?? aliases;
|
|
385
|
+
const ownerScope = moduleAliasScope(def?.metadata, aliases, aliasesByModule);
|
|
389
386
|
|
|
390
387
|
const expanded: ReferenceFieldMap = new Map();
|
|
391
388
|
for (const [path, entry] of baseMap) {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
readResourceInputs,
|
|
9
9
|
type ResourceInput,
|
|
10
10
|
} from "./resource-input.js";
|
|
11
|
-
import type {
|
|
11
|
+
import type { ModuleDocuments } from "./module-documents.js";
|
|
12
12
|
|
|
13
13
|
/** One parsed `exports.resources` / `exports.kinds` entry. `name` is the exported
|
|
14
14
|
* instance name or kind suffix (the part after the dot, or the whole entry); `alias`
|
|
@@ -557,8 +557,8 @@ function canonicalizeInputKind(
|
|
|
557
557
|
* projection needs the internal dispatch chain to derive an export's open
|
|
558
558
|
* requirements. Only libraries that actually export instances are included:
|
|
559
559
|
* a library exporting nothing has no export contract to derive. */
|
|
560
|
-
export function
|
|
561
|
-
const out:
|
|
560
|
+
export function collectModuleDocuments(graph: LoadedGraph): ModuleDocuments[] {
|
|
561
|
+
const out: ModuleDocuments[] = [];
|
|
562
562
|
for (const [source, mod] of graph.modules) {
|
|
563
563
|
if (source === graph.rootSource) continue;
|
|
564
564
|
const libDoc = mod.owner.manifests.find((m) => m && isModuleKind(m.kind)) as
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export type {
|
|
|
11
11
|
ParseError,
|
|
12
12
|
} from "./loaded-types.js";
|
|
13
13
|
export {
|
|
14
|
-
|
|
14
|
+
collectModuleDocuments,
|
|
15
15
|
flattenForAnalyzer,
|
|
16
16
|
flattenLoadedModule,
|
|
17
17
|
forwardReExportManifests,
|
|
@@ -246,7 +246,7 @@ export type {
|
|
|
246
246
|
ZoneExportRequirements,
|
|
247
247
|
ZoneRequirementSpec,
|
|
248
248
|
} from "./resolve-zone-requirements.js";
|
|
249
|
-
export type {
|
|
249
|
+
export type { ModuleDocuments } from "./module-documents.js";
|
|
250
250
|
export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
251
251
|
export {
|
|
252
252
|
RESOURCE_RULES_ANNOTATION,
|
package/src/manifest-visitor.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type SchemaFromFieldEntry,
|
|
18
18
|
} from "./reference-field-map.js";
|
|
19
19
|
import type { ModuleScopes } from "./alias-resolver.js";
|
|
20
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
20
21
|
import { templateBodies, withTemplateSelf } from "./template-body.js";
|
|
21
22
|
import { extractContextsFromSchema, pathMatchesScope } from "./validate-cel-context.js";
|
|
22
23
|
|
|
@@ -237,7 +238,14 @@ export function visitManifest(
|
|
|
237
238
|
if (!r.metadata?.name || !r.kind) continue;
|
|
238
239
|
if (skipKinds?.has(r.kind)) continue;
|
|
239
240
|
|
|
240
|
-
|
|
241
|
+
// A resource's own kind is written in the alias scope of the module that
|
|
242
|
+
// DECLARED it — see `moduleAliasScope`, which is the one place that rule
|
|
243
|
+
// lives. Resolving it through the entry's aliases alone left `definition`
|
|
244
|
+
// undefined for every forwarded resource, so its `x-telo-context` regions
|
|
245
|
+
// contributed nothing and `request` / `result` read as unknown identifiers
|
|
246
|
+
// inside the library's own routes, on a file the consumer cannot fix.
|
|
247
|
+
const moduleScope = moduleAliasScope(r.metadata, aliases, aliasesByModule);
|
|
248
|
+
const resolvedKind = moduleScope?.resolveKind(r.kind);
|
|
241
249
|
const definition =
|
|
242
250
|
registry.resolve(r.kind) ??
|
|
243
251
|
(resolvedKind ? registry.resolve(resolvedKind) : undefined);
|
|
@@ -249,8 +257,8 @@ export function visitManifest(
|
|
|
249
257
|
const emittedRefPaths = wantsNested ? new Set<string>() : null;
|
|
250
258
|
|
|
251
259
|
if (wantsRefs || wantsScope || wantsSchemaFrom) {
|
|
252
|
-
const baseMap =
|
|
253
|
-
? registry.getFieldMapForKind(r.kind,
|
|
260
|
+
const baseMap = moduleScope
|
|
261
|
+
? registry.getFieldMapForKind(r.kind, moduleScope)
|
|
254
262
|
: registry.getFieldMap(r.kind);
|
|
255
263
|
|
|
256
264
|
// Expanded map drives ref/scope sites when requested; schema-from sites
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AliasResolver, scopeResolverForModule, type ModuleScopes } from "./alias-resolver.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WHICH ALIAS TABLE A MANIFEST'S OWN NAMES ARE WRITTEN IN.
|
|
5
|
+
*
|
|
6
|
+
* An application analysis is flattened, so an imported library's definitions and
|
|
7
|
+
* exported instances are checked in the CONSUMER's pass. But every alias-qualified
|
|
8
|
+
* name on such a manifest — its `kind:`, its `extends:`, an `x-telo-schema-from`
|
|
9
|
+
* anchor — was written against the alias map of the module that DECLARED it. A
|
|
10
|
+
* library writes `kind: Http.Api` through an import the consumer has no reason to
|
|
11
|
+
* have, so resolving it through the entry's table finds nothing, and everything
|
|
12
|
+
* derived from the definition silently goes missing: the field map (Phase-5
|
|
13
|
+
* injection blind to a forwarded resource's ref slots) and the `x-telo-context`
|
|
14
|
+
* regions (`request` / `result` reported as unknown identifiers on a file the
|
|
15
|
+
* consumer cannot edit).
|
|
16
|
+
*
|
|
17
|
+
* **There are TWO fallbacks, not one, and this file is where they are told
|
|
18
|
+
* apart** — that is the whole reason they live together. They agree on every
|
|
19
|
+
* module that has a table, and differ on a non-root module that has none:
|
|
20
|
+
*
|
|
21
|
+
* - {@link moduleAliasScope} falls back to the GLOBAL table. For a question
|
|
22
|
+
* asked about an arbitrary manifest, where a missing entry means "this is the
|
|
23
|
+
* entry's own", which is what the global table answers correctly.
|
|
24
|
+
* - {@link declaringModuleScope} falls back to an EMPTY table for a non-root
|
|
25
|
+
* module. For canonicalizing what a library DECLARED, where resolving its
|
|
26
|
+
* `Http.Api` through the consumer's imports would silently bind a library's
|
|
27
|
+
* kind to whatever the app happens to import under that alias — a wrong
|
|
28
|
+
* answer, which is worse than none.
|
|
29
|
+
*
|
|
30
|
+
* Both rest on one invariant neither states inline: **a root module is never a
|
|
31
|
+
* key in `aliasesByModule`** — a root's imports are registered into the global
|
|
32
|
+
* table instead (see the `rootModules` guards in `analyze()`) — so for a
|
|
33
|
+
* consumer-owned manifest the two rules coincide and neither fallback is a
|
|
34
|
+
* degradation.
|
|
35
|
+
*
|
|
36
|
+
* Every READ of the rule goes through one of these two. What legitimately does
|
|
37
|
+
* not, and why, so the next reader does not have to re-derive it: the three
|
|
38
|
+
* sites in `analyze()` that POPULATE `aliasesByModule` (a write, not a lookup),
|
|
39
|
+
* and `resolve-ref-sentinels`, whose module is known non-root by construction and
|
|
40
|
+
* whose fallback is the raw kind rather than another table — a different rule
|
|
41
|
+
* that happens to read the same map.
|
|
42
|
+
*/
|
|
43
|
+
export function moduleAliasScope<A extends KindResolver, M extends KindResolver>(
|
|
44
|
+
metadata: { module?: unknown } | undefined,
|
|
45
|
+
aliases: A,
|
|
46
|
+
aliasesByModule: ReadonlyMap<string, M> | undefined,
|
|
47
|
+
): A | M;
|
|
48
|
+
export function moduleAliasScope<A extends KindResolver, M extends KindResolver>(
|
|
49
|
+
metadata: { module?: unknown } | undefined,
|
|
50
|
+
aliases: A | undefined,
|
|
51
|
+
aliasesByModule: ReadonlyMap<string, M> | undefined,
|
|
52
|
+
): A | M | undefined;
|
|
53
|
+
export function moduleAliasScope<A extends KindResolver, M extends KindResolver>(
|
|
54
|
+
metadata: { module?: unknown } | undefined,
|
|
55
|
+
aliases: A | undefined,
|
|
56
|
+
aliasesByModule: ReadonlyMap<string, M> | undefined,
|
|
57
|
+
): A | M | undefined {
|
|
58
|
+
const declaringModule = metadata?.module;
|
|
59
|
+
if (typeof declaringModule !== "string") return aliases;
|
|
60
|
+
return aliasesByModule?.get(declaringModule) ?? aliases;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** All this rule needs of a resolver, and deliberately all it asks for:
|
|
64
|
+
* `ModuleScopes` already types its map this way so a caller can hand over a
|
|
65
|
+
* lighter table, and requiring the full `AliasResolver` here would have made
|
|
66
|
+
* the one site that does (a template body) reach for a cast. */
|
|
67
|
+
interface KindResolver {
|
|
68
|
+
resolveKind(kind: string): string | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** An empty table, shared: a non-root module with no aliases of its own resolves
|
|
72
|
+
* nothing rather than resolving through the consumer's. One instance because it
|
|
73
|
+
* is immutable in use and allocating one per call put a resolver on a per-schema
|
|
74
|
+
* loop. */
|
|
75
|
+
const NO_ALIASES = new AliasResolver();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The scope a DECLARING module's own alias-form names are canonicalized in — the
|
|
79
|
+
* root-aware half of the rule above, and the one that must never fall through to
|
|
80
|
+
* the consumer's table. See this file's header for why the two differ.
|
|
81
|
+
*/
|
|
82
|
+
export function declaringModuleScope(
|
|
83
|
+
ownModule: string | undefined,
|
|
84
|
+
aliases: AliasResolver,
|
|
85
|
+
scopes: { aliasesByModule: Map<string, AliasResolver>; rootModules: Set<string> },
|
|
86
|
+
): AliasResolver {
|
|
87
|
+
const own = scopeResolverForModule(ownModule, scopes.rootModules, scopes.aliasesByModule);
|
|
88
|
+
if (own) return own;
|
|
89
|
+
// Root (or unknown-and-therefore-treated-as-root): the global table IS its
|
|
90
|
+
// import map. A non-root module with no table of its own resolves nothing.
|
|
91
|
+
return ownModule && !scopes.rootModules.has(ownModule) ? NO_ALIASES : aliases;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type { ModuleScopes };
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* One imported library's FULL document set
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* One imported library's FULL document set — what the flattened analysis view
|
|
5
|
+
* no longer holds, since it forwards only each library's export surface and
|
|
6
|
+
* never its internal dispatch chain.
|
|
7
|
+
*
|
|
8
|
+
* Named for what it CARRIES rather than for who asked first: the zone stage's
|
|
9
|
+
* per-library export derivation was the original consumer, and the throws walk
|
|
10
|
+
* is the second — a third should not have to import "zone" to ask about
|
|
11
|
+
* something else again.
|
|
8
12
|
*
|
|
9
13
|
* Plain data in a module of its own, deliberately. It is produced by the
|
|
10
|
-
* loading side (`
|
|
14
|
+
* loading side (`collectModuleDocuments`), named in `AnalysisOptions`, and
|
|
11
15
|
* consumed by the projection; putting it in any of the three would make the
|
|
12
16
|
* other two import that one, and `types.ts` ↔ the projection is a genuine
|
|
13
17
|
* cycle. A leaf module with no imports of its own breaks it without an inline
|
|
14
18
|
* `import(...)` type expression standing in for the dependency nobody wanted.
|
|
15
19
|
*/
|
|
16
|
-
export interface
|
|
20
|
+
export interface ModuleDocuments {
|
|
17
21
|
/** The library's module name (its `Telo.Library` doc's `metadata.name`). */
|
|
18
22
|
module: string;
|
|
19
23
|
/** Stable source identity of the library's owner file — the cache key. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isRefSentinel } from "@telorun/templating";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a `!ref` names, parsed once.
|
|
5
|
+
*
|
|
6
|
+
* THE single reader of the tag's grammar, on the `ref-slot.ts` / `zone-slot.ts`
|
|
7
|
+
* precedent. Three passes had grown their own parse of the same scalar and they
|
|
8
|
+
* disagreed: one took the source verbatim, one split on the first dot and
|
|
9
|
+
* dropped every alias but `Self`, one split on the LAST dot and dropped the
|
|
10
|
+
* alias entirely — so the same `!ref Alias.name` named three different things
|
|
11
|
+
* depending on which pass was asking, and the loosest of the three resolved a
|
|
12
|
+
* bare name against whatever manifest happened to share it.
|
|
13
|
+
*
|
|
14
|
+
* The grammar itself is one line and is not in dispute: `!ref <name>` or
|
|
15
|
+
* `!ref <Alias>.<name>`, split on the FIRST dot, because that is what
|
|
16
|
+
* `resolveRefSentinels` does and a name may not contain one
|
|
17
|
+
* (`INVALID_NAME` rejects it at every declaration site).
|
|
18
|
+
*
|
|
19
|
+
* What this deliberately does NOT do is decide what to look up. A reduction is
|
|
20
|
+
* the caller's, and the three genuinely differ: an edge in the call graph points
|
|
21
|
+
* at a cross-module target and tolerates not resolving it; a zone correlation
|
|
22
|
+
* refuses to bind anything it cannot resolve exactly; a throws walk resolves in
|
|
23
|
+
* the DECLARING library first. Each states its own, over one parse.
|
|
24
|
+
*
|
|
25
|
+
* Browser-safe.
|
|
26
|
+
*/
|
|
27
|
+
export interface RefSentinelTarget {
|
|
28
|
+
/** The scalar as written, for a caller that wants the author's spelling. */
|
|
29
|
+
source: string;
|
|
30
|
+
/** The prefix before the first dot, `Self` included and not normalized away —
|
|
31
|
+
* a caller that treats `Self` as "no alias" says so itself. */
|
|
32
|
+
alias?: string;
|
|
33
|
+
/** The segment after the first dot, or the whole scalar when there is none. */
|
|
34
|
+
name: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parse a `!ref` sentinel. Returns `undefined` for anything that is not one —
|
|
38
|
+
* including the `{kind, name}` object `resolveRefSentinels` rewrites it into,
|
|
39
|
+
* which is a different shape with a different reader. */
|
|
40
|
+
export function refSentinelTarget(value: unknown): RefSentinelTarget | undefined {
|
|
41
|
+
if (!isRefSentinel(value)) return undefined;
|
|
42
|
+
const source = value.source;
|
|
43
|
+
const dot = source.indexOf(".");
|
|
44
|
+
if (dot <= 0) return { source, name: source };
|
|
45
|
+
return { source, alias: source.slice(0, dot), name: source.slice(dot + 1) };
|
|
46
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
4
5
|
|
|
5
6
|
/** Schema-bearing fields on a Telo.Definition / Telo.Type resource. */
|
|
6
7
|
const SCHEMA_FIELDS = ["schema", "inputType", "outputType"];
|
|
@@ -96,7 +97,7 @@ export function resolveSchemaTypeRefs(
|
|
|
96
97
|
|
|
97
98
|
for (const r of resources) {
|
|
98
99
|
const ownModule = (r.metadata as { module?: string } | undefined)?.module;
|
|
99
|
-
const resolver = (
|
|
100
|
+
const resolver = moduleAliasScope(r.metadata, aliases, aliasesByModule);
|
|
100
101
|
const resolveAuthority = (authority: string): string | undefined =>
|
|
101
102
|
authority === "Self" ? ownModule : resolver?.moduleForAlias(authority);
|
|
102
103
|
for (const field of SCHEMA_FIELDS) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
|
|
4
|
+
import { resolveScopedName } from "./call-graph.js";
|
|
5
|
+
import { refSentinelTarget, type RefSentinelTarget } from "./ref-sentinel-target.js";
|
|
4
6
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
7
|
import { readStepSlot } from "./step-slot.js";
|
|
6
8
|
|
|
@@ -40,6 +42,23 @@ export interface ResolveCtx {
|
|
|
40
42
|
aliasesByModule: Map<string, AliasResolver>;
|
|
41
43
|
/** The consumer/root module names; resources owned by these resolve against `aliases`. */
|
|
42
44
|
rootModules: Set<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Every imported library's FULL manifest list, keyed by module name.
|
|
47
|
+
*
|
|
48
|
+
* A consumer's flat set holds a library's EXPORTED instances and nothing else,
|
|
49
|
+
* so the siblings an exported entry point invokes are not in it. Without this
|
|
50
|
+
* the walk stops at the first such hop, and the difference is not academic: a
|
|
51
|
+
* library whose entry point raises its own code through an internal guard
|
|
52
|
+
* presented an empty union to its consumer, which then had its `catches:`
|
|
53
|
+
* rejected for the very code the entry point documents.
|
|
54
|
+
*
|
|
55
|
+
* Consulted only as a FALLBACK, after the flat set — the flat set is what the
|
|
56
|
+
* consumer's own resources resolve against, and a library-internal name must
|
|
57
|
+
* never shadow one of them.
|
|
58
|
+
*/
|
|
59
|
+
moduleManifests: Map<string, ResourceManifest[]>;
|
|
60
|
+
/** Keyed `<module>\0<name>`: resource names are module-scoped, so two
|
|
61
|
+
* libraries each declaring a `query` are two different unions. */
|
|
43
62
|
memo: Map<string, ThrowsUnion>;
|
|
44
63
|
inProgress: Set<string>;
|
|
45
64
|
}
|
|
@@ -50,6 +69,7 @@ export function createResolveCtx(
|
|
|
50
69
|
aliases: AliasResolver,
|
|
51
70
|
aliasesByModule: Map<string, AliasResolver> = new Map(),
|
|
52
71
|
rootModules: Set<string> = new Set(),
|
|
72
|
+
moduleManifests: Map<string, ResourceManifest[]> = new Map(),
|
|
53
73
|
): ResolveCtx {
|
|
54
74
|
return {
|
|
55
75
|
allManifests,
|
|
@@ -57,11 +77,90 @@ export function createResolveCtx(
|
|
|
57
77
|
aliases,
|
|
58
78
|
aliasesByModule,
|
|
59
79
|
rootModules,
|
|
80
|
+
moduleManifests,
|
|
60
81
|
memo: new Map(),
|
|
61
82
|
inProgress: new Set(),
|
|
62
83
|
};
|
|
63
84
|
}
|
|
64
85
|
|
|
86
|
+
/**
|
|
87
|
+
* A dispatch target named by a resolved `{kind, name}` ref.
|
|
88
|
+
*
|
|
89
|
+
* The flat set is the scope such a ref was resolved in, so it is asked first;
|
|
90
|
+
* the owning library's own documents are the fallback for a name the flattened
|
|
91
|
+
* view dropped. `kindMatches` applies to BOTH — one function, one rule, or the
|
|
92
|
+
* next caller inherits whichever half it happened to hit.
|
|
93
|
+
*/
|
|
94
|
+
function findTarget(
|
|
95
|
+
ctx: ResolveCtx,
|
|
96
|
+
name: string,
|
|
97
|
+
ownerModule: string | undefined,
|
|
98
|
+
kindMatches: (m: ResourceManifest) => boolean,
|
|
99
|
+
): ResourceManifest | undefined {
|
|
100
|
+
const flat = ctx.allManifests.find((m) => m.metadata?.name === name && kindMatches(m));
|
|
101
|
+
if (flat) return flat;
|
|
102
|
+
if (!ownerModule) return undefined;
|
|
103
|
+
return ctx.moduleManifests
|
|
104
|
+
.get(ownerModule)
|
|
105
|
+
?.find((m) => m.metadata?.name === name && kindMatches(m));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The target of a `!ref` that still carries its sentinel — a library-internal
|
|
110
|
+
* reference inside a manifest forwarded into a consumer's flat set, where Phase
|
|
111
|
+
* 2.5 had nothing to resolve it against.
|
|
112
|
+
*
|
|
113
|
+
* **The declaring library is asked FIRST**, and that ordering is the whole rule:
|
|
114
|
+
* a bare name in a library manifest is unambiguously library-internal, so
|
|
115
|
+
* searching the consumer's flat set first let any consumer resource that
|
|
116
|
+
* happened to share the name supply another library's throw union — the same
|
|
117
|
+
* false `{∅}` this branch exists to remove, arrived at from the other side.
|
|
118
|
+
*
|
|
119
|
+
* An ALIAS-qualified source is the cross-module case and is resolved through the
|
|
120
|
+
* declaring module's own alias table, never by dropping the alias and matching
|
|
121
|
+
* the bare name anywhere.
|
|
122
|
+
*
|
|
123
|
+
* Ambiguity resolves to NOTHING rather than to a guess (`resolveScopedName`'s
|
|
124
|
+
* rule), and the caller reads that as unbounded — the safe direction here, since
|
|
125
|
+
* a union that cannot be enumerated must not read as empty.
|
|
126
|
+
*/
|
|
127
|
+
function findSentinelTarget(
|
|
128
|
+
ctx: ResolveCtx,
|
|
129
|
+
target: RefSentinelTarget,
|
|
130
|
+
ownerModule: string | undefined,
|
|
131
|
+
): ResourceManifest | undefined {
|
|
132
|
+
const named = (pool: readonly ResourceManifest[] | undefined): ResourceManifest[] =>
|
|
133
|
+
(pool ?? []).filter((m) => m.metadata?.name === target.name);
|
|
134
|
+
|
|
135
|
+
if (target.alias !== undefined && target.alias !== "Self") {
|
|
136
|
+
// A forwarded export: its module is whatever the DECLARING module aliases
|
|
137
|
+
// that prefix to, and it keeps its export name in the flat set.
|
|
138
|
+
// A root-owned manifest resolves aliases against the global table, which is
|
|
139
|
+
// what `scopeResolverForModule` returns undefined for — so fall back to it
|
|
140
|
+
// rather than reading a root's own alias as unresolvable.
|
|
141
|
+
const resolver = scopeResolverFor(ctx, ownerModule) ?? ctx.aliases;
|
|
142
|
+
const module = resolver.moduleForAlias(target.alias);
|
|
143
|
+
if (!module) return undefined;
|
|
144
|
+
return named(ctx.allManifests).find((m) => declaringModuleOf(m) === module);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const own = named(ctx.moduleManifests.get(ownerModule ?? ""));
|
|
148
|
+
if (own.length === 1) return own[0];
|
|
149
|
+
if (own.length > 1) return undefined;
|
|
150
|
+
return resolveScopedName(named(ctx.allManifests), declaringModuleOf, ownerModule);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const declaringModuleOf = (m: ResourceManifest): string | undefined =>
|
|
154
|
+
(m.metadata as { module?: string } | undefined)?.module;
|
|
155
|
+
|
|
156
|
+
/** Memo key. Module-scoped, because resource names are. */
|
|
157
|
+
function memoKey(manifest: ResourceManifest): string | undefined {
|
|
158
|
+
const name = manifest.metadata?.name as string | undefined;
|
|
159
|
+
if (!name) return undefined;
|
|
160
|
+
const mod = (manifest.metadata as { module?: string } | undefined)?.module ?? "";
|
|
161
|
+
return `${mod}\0${name}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
65
164
|
function emptyUnion(): ThrowsUnion {
|
|
66
165
|
return { codes: new Map(), unbounded: false };
|
|
67
166
|
}
|
|
@@ -114,7 +213,7 @@ export function resolveThrowsUnion(
|
|
|
114
213
|
manifest: ResourceManifest,
|
|
115
214
|
ctx: ResolveCtx,
|
|
116
215
|
): ThrowsUnion {
|
|
117
|
-
const name = manifest
|
|
216
|
+
const name = memoKey(manifest);
|
|
118
217
|
|
|
119
218
|
if (name) {
|
|
120
219
|
const cached = ctx.memo.get(name);
|
|
@@ -314,7 +413,21 @@ function resolveStepInvokeThrows(
|
|
|
314
413
|
const invokeRef = step[invokeField];
|
|
315
414
|
if (!invokeRef || typeof invokeRef !== "object") return emptyUnion();
|
|
316
415
|
const invokedKind = invokeRef.kind as string | undefined;
|
|
317
|
-
|
|
416
|
+
// A reference that still carries its parse-time sentinel — a library-internal
|
|
417
|
+
// `!ref` inside a manifest forwarded into a consumer's flat set, where Phase
|
|
418
|
+
// 2.5 had nothing to resolve it against. The target is in the declaring
|
|
419
|
+
// library's own documents, so it is looked up there; only a name that is not
|
|
420
|
+
// there either is UNKNOWN, which is unbounded rather than empty. Reading it as
|
|
421
|
+
// empty is what made a library's exported entry point present a `{∅}` union to
|
|
422
|
+
// its consumer and get the consumer's `catches:` rejected for the code the
|
|
423
|
+
// entry point documents.
|
|
424
|
+
if (!invokedKind) {
|
|
425
|
+
const sentinel = refSentinelTarget(invokeRef);
|
|
426
|
+
if (!sentinel) return emptyUnion();
|
|
427
|
+
const target = findSentinelTarget(ctx, sentinel, ownerModule);
|
|
428
|
+
if (target) return resolveThrowsUnion(target, ctx);
|
|
429
|
+
return { codes: new Map(), unbounded: true };
|
|
430
|
+
}
|
|
318
431
|
|
|
319
432
|
// The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
|
|
320
433
|
// composer that declares the step), so a library's step referencing its own
|
|
@@ -331,13 +444,15 @@ function resolveStepInvokeThrows(
|
|
|
331
444
|
const invokeName = invokeRef.name as string | undefined;
|
|
332
445
|
if (invokeName) {
|
|
333
446
|
const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
|
|
334
|
-
const target =
|
|
447
|
+
const target = findTarget(
|
|
448
|
+
ctx,
|
|
449
|
+
invokeName,
|
|
450
|
+
ownerModule,
|
|
335
451
|
(m) =>
|
|
336
|
-
m.
|
|
337
|
-
(m.kind === invokedKind ||
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind)),
|
|
452
|
+
m.kind === invokedKind ||
|
|
453
|
+
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
454
|
+
m.kind === ctx.aliases.resolveKind(invokedKind) ||
|
|
455
|
+
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind),
|
|
341
456
|
);
|
|
342
457
|
if (target) return resolveThrowsUnion(target, ctx);
|
|
343
458
|
}
|