@telorun/analyzer 0.70.0 → 0.72.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 +92 -10
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +10 -4
- package/dist/catch-scope.d.ts +72 -0
- package/dist/catch-scope.d.ts.map +1 -0
- package/dist/catch-scope.js +102 -0
- 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/deprecation.d.ts +21 -0
- package/dist/deprecation.d.ts.map +1 -0
- package/dist/deprecation.js +26 -0
- 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 +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/manifest-visitor.d.ts +17 -1
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +16 -4
- package/dist/migrations/report.d.ts +1 -1
- package/dist/migrations/report.d.ts.map +1 -1
- package/dist/migrations/report.js +5 -0
- 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/ref-slot.d.ts +15 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +7 -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 +47 -2
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +199 -20
- 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/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +13 -1
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +48 -4
- package/dist/schema-keywords.d.ts.map +1 -1
- package/dist/schema-keywords.js +3 -1
- package/dist/schema-walk.d.ts +27 -0
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +44 -0
- 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 +20 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/validate-identifier-names.d.ts +2 -2
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +22 -7
- 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-ref-slots.d.ts +1 -1
- package/dist/validate-ref-slots.d.ts.map +1 -1
- package/dist/validate-ref-slots.js +34 -0
- package/dist/validate-references.d.ts +16 -5
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +57 -15
- 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 +241 -86
- package/package.json +3 -3
- package/src/analysis-registry.ts +4 -5
- package/src/analyzer.ts +115 -11
- package/src/call-graph.ts +9 -3
- package/src/catch-scope.ts +157 -0
- package/src/cel-scope-query.ts +45 -8
- package/src/definition-registry.ts +3 -6
- package/src/deprecation.ts +36 -0
- package/src/flatten-for-analyzer.ts +3 -3
- package/src/index.ts +10 -3
- package/src/manifest-visitor.ts +30 -5
- package/src/migrations/report.ts +5 -1
- 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/ref-slot.ts +19 -0
- package/src/resolve-schema-type-refs.ts +2 -1
- package/src/resolve-throws-union.ts +253 -20
- package/src/resolve-zone-requirements.ts +14 -14
- package/src/schema-compat.ts +13 -0
- package/src/schema-error-report.ts +50 -6
- package/src/schema-keywords.ts +4 -1
- package/src/schema-walk.ts +56 -0
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +2 -5
- package/src/types.ts +21 -3
- package/src/validate-identifier-names.ts +28 -9
- package/src/validate-invocation-contract.ts +128 -2
- package/src/validate-logging.ts +2 -3
- package/src/validate-ref-slots.ts +41 -1
- package/src/validate-references.ts +74 -14
- package/src/validate-resource-inputs.ts +26 -3
- package/src/validate-schema-type-refs.ts +2 -1
- package/src/validate-throws-coverage.ts +344 -92
- package/dist/zone-module-documents.d.ts.map +0 -1
- /package/dist/{zone-module-documents.js → module-documents.js} +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
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";
|
|
7
|
+
import { possibleUses, readRefSlot, transfersControl, type RefSlot } from "./ref-slot.js";
|
|
5
8
|
import { readStepSlot } from "./step-slot.js";
|
|
9
|
+
import { forEachDrivenSlot } from "./schema-walk.js";
|
|
6
10
|
|
|
7
11
|
export interface ThrowsCodeMeta {
|
|
8
12
|
data?: Record<string, any>;
|
|
@@ -40,6 +44,23 @@ export interface ResolveCtx {
|
|
|
40
44
|
aliasesByModule: Map<string, AliasResolver>;
|
|
41
45
|
/** The consumer/root module names; resources owned by these resolve against `aliases`. */
|
|
42
46
|
rootModules: Set<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Every imported library's FULL manifest list, keyed by module name.
|
|
49
|
+
*
|
|
50
|
+
* A consumer's flat set holds a library's EXPORTED instances and nothing else,
|
|
51
|
+
* so the siblings an exported entry point invokes are not in it. Without this
|
|
52
|
+
* the walk stops at the first such hop, and the difference is not academic: a
|
|
53
|
+
* library whose entry point raises its own code through an internal guard
|
|
54
|
+
* presented an empty union to its consumer, which then had its `catches:`
|
|
55
|
+
* rejected for the very code the entry point documents.
|
|
56
|
+
*
|
|
57
|
+
* Consulted only as a FALLBACK, after the flat set — the flat set is what the
|
|
58
|
+
* consumer's own resources resolve against, and a library-internal name must
|
|
59
|
+
* never shadow one of them.
|
|
60
|
+
*/
|
|
61
|
+
moduleManifests: Map<string, ResourceManifest[]>;
|
|
62
|
+
/** Keyed `<module>\0<name>`: resource names are module-scoped, so two
|
|
63
|
+
* libraries each declaring a `query` are two different unions. */
|
|
43
64
|
memo: Map<string, ThrowsUnion>;
|
|
44
65
|
inProgress: Set<string>;
|
|
45
66
|
}
|
|
@@ -50,6 +71,7 @@ export function createResolveCtx(
|
|
|
50
71
|
aliases: AliasResolver,
|
|
51
72
|
aliasesByModule: Map<string, AliasResolver> = new Map(),
|
|
52
73
|
rootModules: Set<string> = new Set(),
|
|
74
|
+
moduleManifests: Map<string, ResourceManifest[]> = new Map(),
|
|
53
75
|
): ResolveCtx {
|
|
54
76
|
return {
|
|
55
77
|
allManifests,
|
|
@@ -57,11 +79,90 @@ export function createResolveCtx(
|
|
|
57
79
|
aliases,
|
|
58
80
|
aliasesByModule,
|
|
59
81
|
rootModules,
|
|
82
|
+
moduleManifests,
|
|
60
83
|
memo: new Map(),
|
|
61
84
|
inProgress: new Set(),
|
|
62
85
|
};
|
|
63
86
|
}
|
|
64
87
|
|
|
88
|
+
/**
|
|
89
|
+
* A dispatch target named by a resolved `{kind, name}` ref.
|
|
90
|
+
*
|
|
91
|
+
* The flat set is the scope such a ref was resolved in, so it is asked first;
|
|
92
|
+
* the owning library's own documents are the fallback for a name the flattened
|
|
93
|
+
* view dropped. `kindMatches` applies to BOTH — one function, one rule, or the
|
|
94
|
+
* next caller inherits whichever half it happened to hit.
|
|
95
|
+
*/
|
|
96
|
+
function findTarget(
|
|
97
|
+
ctx: ResolveCtx,
|
|
98
|
+
name: string,
|
|
99
|
+
ownerModule: string | undefined,
|
|
100
|
+
kindMatches: (m: ResourceManifest) => boolean,
|
|
101
|
+
): ResourceManifest | undefined {
|
|
102
|
+
const flat = ctx.allManifests.find((m) => m.metadata?.name === name && kindMatches(m));
|
|
103
|
+
if (flat) return flat;
|
|
104
|
+
if (!ownerModule) return undefined;
|
|
105
|
+
return ctx.moduleManifests
|
|
106
|
+
.get(ownerModule)
|
|
107
|
+
?.find((m) => m.metadata?.name === name && kindMatches(m));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The target of a `!ref` that still carries its sentinel — a library-internal
|
|
112
|
+
* reference inside a manifest forwarded into a consumer's flat set, where Phase
|
|
113
|
+
* 2.5 had nothing to resolve it against.
|
|
114
|
+
*
|
|
115
|
+
* **The declaring library is asked FIRST**, and that ordering is the whole rule:
|
|
116
|
+
* a bare name in a library manifest is unambiguously library-internal, so
|
|
117
|
+
* searching the consumer's flat set first let any consumer resource that
|
|
118
|
+
* happened to share the name supply another library's throw union — the same
|
|
119
|
+
* false `{∅}` this branch exists to remove, arrived at from the other side.
|
|
120
|
+
*
|
|
121
|
+
* An ALIAS-qualified source is the cross-module case and is resolved through the
|
|
122
|
+
* declaring module's own alias table, never by dropping the alias and matching
|
|
123
|
+
* the bare name anywhere.
|
|
124
|
+
*
|
|
125
|
+
* Ambiguity resolves to NOTHING rather than to a guess (`resolveScopedName`'s
|
|
126
|
+
* rule), and the caller reads that as unbounded — the safe direction here, since
|
|
127
|
+
* a union that cannot be enumerated must not read as empty.
|
|
128
|
+
*/
|
|
129
|
+
function findSentinelTarget(
|
|
130
|
+
ctx: ResolveCtx,
|
|
131
|
+
target: RefSentinelTarget,
|
|
132
|
+
ownerModule: string | undefined,
|
|
133
|
+
): ResourceManifest | undefined {
|
|
134
|
+
const named = (pool: readonly ResourceManifest[] | undefined): ResourceManifest[] =>
|
|
135
|
+
(pool ?? []).filter((m) => m.metadata?.name === target.name);
|
|
136
|
+
|
|
137
|
+
if (target.alias !== undefined && target.alias !== "Self") {
|
|
138
|
+
// A forwarded export: its module is whatever the DECLARING module aliases
|
|
139
|
+
// that prefix to, and it keeps its export name in the flat set.
|
|
140
|
+
// A root-owned manifest resolves aliases against the global table, which is
|
|
141
|
+
// what `scopeResolverForModule` returns undefined for — so fall back to it
|
|
142
|
+
// rather than reading a root's own alias as unresolvable.
|
|
143
|
+
const resolver = scopeResolverFor(ctx, ownerModule) ?? ctx.aliases;
|
|
144
|
+
const module = resolver.moduleForAlias(target.alias);
|
|
145
|
+
if (!module) return undefined;
|
|
146
|
+
return named(ctx.allManifests).find((m) => declaringModuleOf(m) === module);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const own = named(ctx.moduleManifests.get(ownerModule ?? ""));
|
|
150
|
+
if (own.length === 1) return own[0];
|
|
151
|
+
if (own.length > 1) return undefined;
|
|
152
|
+
return resolveScopedName(named(ctx.allManifests), declaringModuleOf, ownerModule);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const declaringModuleOf = (m: ResourceManifest): string | undefined =>
|
|
156
|
+
(m.metadata as { module?: string } | undefined)?.module;
|
|
157
|
+
|
|
158
|
+
/** Memo key. Module-scoped, because resource names are. */
|
|
159
|
+
function memoKey(manifest: ResourceManifest): string | undefined {
|
|
160
|
+
const name = manifest.metadata?.name as string | undefined;
|
|
161
|
+
if (!name) return undefined;
|
|
162
|
+
const mod = (manifest.metadata as { module?: string } | undefined)?.module ?? "";
|
|
163
|
+
return `${mod}\0${name}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
65
166
|
function emptyUnion(): ThrowsUnion {
|
|
66
167
|
return { codes: new Map(), unbounded: false };
|
|
67
168
|
}
|
|
@@ -114,7 +215,7 @@ export function resolveThrowsUnion(
|
|
|
114
215
|
manifest: ResourceManifest,
|
|
115
216
|
ctx: ResolveCtx,
|
|
116
217
|
): ThrowsUnion {
|
|
117
|
-
const name = manifest
|
|
218
|
+
const name = memoKey(manifest);
|
|
118
219
|
|
|
119
220
|
if (name) {
|
|
120
221
|
const cached = ctx.memo.get(name);
|
|
@@ -164,6 +265,16 @@ export function resolveThrowsUnion(
|
|
|
164
265
|
}
|
|
165
266
|
}
|
|
166
267
|
|
|
268
|
+
/**
|
|
269
|
+
* `throws.inherit: true` — the union a composer's own STEP BODIES reach.
|
|
270
|
+
*
|
|
271
|
+
* Deliberately steps only, and not every slot the resource drives: `inherit` is
|
|
272
|
+
* a DECLARATION that a kind's union is the union of what it dispatches, and a
|
|
273
|
+
* kind that does not make that claim must not have it inferred — a kind holding
|
|
274
|
+
* a `call` ref it catches internally would silently gain codes it never lets
|
|
275
|
+
* escape. What a CATCH SCOPE needs is a different question with a different
|
|
276
|
+
* answer, and it has its own resolver below.
|
|
277
|
+
*/
|
|
167
278
|
function resolveInherited(
|
|
168
279
|
manifest: ResourceManifest,
|
|
169
280
|
definition: ResourceDefinition,
|
|
@@ -185,6 +296,66 @@ function resolveInherited(
|
|
|
185
296
|
return result;
|
|
186
297
|
}
|
|
187
298
|
|
|
299
|
+
/**
|
|
300
|
+
* The union a SCOPE-LEVEL `catches:` list can be asked to render — everything
|
|
301
|
+
* the resource it is written on drives, transitively.
|
|
302
|
+
*
|
|
303
|
+
* `x-telo-catches-for: ""` is itself the claim that this is the denominator, so
|
|
304
|
+
* nothing is inferred from a kind that did not opt in, and no definition has to
|
|
305
|
+
* declare a `throws:` block to carry a catch scope (the kernel forbids one on a
|
|
306
|
+
* `Telo.Service` and a `Telo.Mount`, and rightly: what a router renders is not
|
|
307
|
+
* what a router THROWS).
|
|
308
|
+
*
|
|
309
|
+
* Three edges, three answers. A **step body** contributes its own traversal,
|
|
310
|
+
* subtraction included. A **control-transferring ref** contributes the target's
|
|
311
|
+
* own declared union — a route handler is a leaf here, and asking what IT drives
|
|
312
|
+
* would credit this scope with codes the handler catches internally. A
|
|
313
|
+
* **`throwsThrough` ref** recurses, because the target is another scope on the
|
|
314
|
+
* same ladder: a server renders what its mounts' routes throw, not what the
|
|
315
|
+
* mounts themselves declare.
|
|
316
|
+
*/
|
|
317
|
+
export function resolveScopeUnion(
|
|
318
|
+
manifest: ResourceManifest,
|
|
319
|
+
definition: ResourceDefinition,
|
|
320
|
+
ctx: ResolveCtx,
|
|
321
|
+
seen: Set<ResourceManifest> = new Set(),
|
|
322
|
+
): ThrowsUnion {
|
|
323
|
+
const result: ThrowsUnion = { codes: new Map(), unbounded: false };
|
|
324
|
+
if (seen.has(manifest)) return result;
|
|
325
|
+
seen.add(manifest);
|
|
326
|
+
const ownerModule = (manifest.metadata as { module?: string } | undefined)?.module;
|
|
327
|
+
|
|
328
|
+
forEachDrivenSlot(definition.schema, manifest, (driven) => {
|
|
329
|
+
if (driven.kind === "step") {
|
|
330
|
+
unionInto(
|
|
331
|
+
result,
|
|
332
|
+
collectStepArrayThrows(driven.data, driven.slot.invoke, undefined, ctx, ownerModule),
|
|
333
|
+
);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (driven.slot.throwsThrough) {
|
|
337
|
+
const target = resolveRefManifest(driven.data, ctx, ownerModule);
|
|
338
|
+
const targetDef = target
|
|
339
|
+
? definitionFor(
|
|
340
|
+
target.kind,
|
|
341
|
+
ctx.defs,
|
|
342
|
+
ctx.aliases,
|
|
343
|
+
scopeResolverFor(ctx, (target.metadata as { module?: string } | undefined)?.module),
|
|
344
|
+
)
|
|
345
|
+
: undefined;
|
|
346
|
+
if (target && targetDef) unionInto(result, resolveScopeUnion(target, targetDef, ctx, seen));
|
|
347
|
+
// A target that cannot be resolved says nothing about what it throws, so
|
|
348
|
+
// the scope's union is no longer enumerable.
|
|
349
|
+
else result.unbounded = true;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (!possibleUses(driven.slot).some(transfersControl)) return;
|
|
353
|
+
unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
188
359
|
function collectStepArrayThrows(
|
|
189
360
|
steps: unknown[],
|
|
190
361
|
invokeField: string,
|
|
@@ -311,36 +482,62 @@ function resolveStepInvokeThrows(
|
|
|
311
482
|
ctx: ResolveCtx,
|
|
312
483
|
ownerModule: string | undefined,
|
|
313
484
|
): ThrowsUnion {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
485
|
+
return resolveRefTargetThrows(step[invokeField], ctx, ownerModule, () =>
|
|
486
|
+
resolvePassthroughAtCallSite(step, enclosingTryCodes),
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* The effective throw union behind a resolved reference value.
|
|
492
|
+
*
|
|
493
|
+
* Shared by both ways a resource drives another: a step's `invoke:` and a
|
|
494
|
+
* reference slot that carries throws. The two used to differ only in where the
|
|
495
|
+
* ref value was read from, and keeping one copy is what stops a router's
|
|
496
|
+
* denominator and a sequence's from disagreeing about what a name resolves to.
|
|
497
|
+
*
|
|
498
|
+
* `onPassthrough` is the one genuine difference: a passthrough kind's union is a
|
|
499
|
+
* property of the CALL SITE (`inputs.code`), which only a step has. A reference
|
|
500
|
+
* slot has no such site, so the union is unbounded there rather than guessed.
|
|
501
|
+
*/
|
|
502
|
+
function resolveRefTargetThrows(
|
|
503
|
+
refValue: unknown,
|
|
504
|
+
ctx: ResolveCtx,
|
|
505
|
+
ownerModule: string | undefined,
|
|
506
|
+
onPassthrough?: () => ThrowsUnion,
|
|
507
|
+
): ThrowsUnion {
|
|
508
|
+
if (!refValue || typeof refValue !== "object" || Array.isArray(refValue)) return emptyUnion();
|
|
509
|
+
const ref = refValue as Record<string, any>;
|
|
510
|
+
const invokedKind = ref.kind as string | undefined;
|
|
511
|
+
// A reference that still carries its parse-time sentinel — a library-internal
|
|
512
|
+
// `!ref` inside a manifest forwarded into a consumer's flat set, where Phase
|
|
513
|
+
// 2.5 had nothing to resolve it against. The target is in the declaring
|
|
514
|
+
// library's own documents, so it is looked up there; only a name that is not
|
|
515
|
+
// there either is UNKNOWN, which is unbounded rather than empty. Reading it as
|
|
516
|
+
// empty is what made a library's exported entry point present a `{∅}` union to
|
|
517
|
+
// its consumer and get the consumer's `catches:` rejected for the code the
|
|
518
|
+
// entry point documents.
|
|
519
|
+
if (!invokedKind) {
|
|
520
|
+
const sentinel = refSentinelTarget(ref);
|
|
521
|
+
if (!sentinel) return emptyUnion();
|
|
522
|
+
const target = findSentinelTarget(ctx, sentinel, ownerModule);
|
|
523
|
+
if (target) return resolveThrowsUnion(target, ctx);
|
|
524
|
+
return { codes: new Map(), unbounded: true };
|
|
525
|
+
}
|
|
318
526
|
|
|
319
527
|
// The invoked kind's alias resolves in the OWNER manifest's lexical scope (the
|
|
320
|
-
//
|
|
528
|
+
// resource that declares the slot), so a library's step referencing its own
|
|
321
529
|
// import resolves against that library, not the consumer.
|
|
322
530
|
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
323
531
|
const definition = definitionFor(invokedKind, ctx.defs, ctx.aliases, scopeResolver);
|
|
324
532
|
if (!definition) return { codes: new Map(), unbounded: true };
|
|
325
533
|
|
|
326
534
|
if (definition.throws?.passthrough) {
|
|
327
|
-
return
|
|
535
|
+
return onPassthrough ? onPassthrough() : { codes: new Map(), unbounded: true };
|
|
328
536
|
}
|
|
329
537
|
|
|
330
538
|
// Named manifest: resolve the full chain (covers transitive inherit).
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
const scopedInvokedKind = scopeResolver?.resolveKind(invokedKind);
|
|
334
|
-
const target = ctx.allManifests.find(
|
|
335
|
-
(m) =>
|
|
336
|
-
m.metadata?.name === invokeName &&
|
|
337
|
-
(m.kind === invokedKind ||
|
|
338
|
-
ctx.aliases.resolveKind(m.kind) === invokedKind ||
|
|
339
|
-
m.kind === ctx.aliases.resolveKind(invokedKind) ||
|
|
340
|
-
(scopedInvokedKind !== undefined && m.kind === scopedInvokedKind)),
|
|
341
|
-
);
|
|
342
|
-
if (target) return resolveThrowsUnion(target, ctx);
|
|
343
|
-
}
|
|
539
|
+
const target = resolveRefManifest(ref, ctx, ownerModule);
|
|
540
|
+
if (target) return resolveThrowsUnion(target, ctx);
|
|
344
541
|
|
|
345
542
|
// Fall back to the definition's own explicit codes. Mark unbounded when the
|
|
346
543
|
// definition depends on call-site or transitive resolution we couldn't
|
|
@@ -351,6 +548,42 @@ function resolveStepInvokeThrows(
|
|
|
351
548
|
return { codes, unbounded };
|
|
352
549
|
}
|
|
353
550
|
|
|
551
|
+
/**
|
|
552
|
+
* The manifest a resolved reference value names, in either shape it arrives in —
|
|
553
|
+
* a `{kind, name}` pair, or a `!ref` still carrying its parse-time sentinel.
|
|
554
|
+
*
|
|
555
|
+
* Exported because the catch-scope enclosure walk asks the same question about
|
|
556
|
+
* the same values; resolving a name twice by two rules is how two passes end up
|
|
557
|
+
* disagreeing about which resource a slot points at.
|
|
558
|
+
*/
|
|
559
|
+
export function resolveRefManifest(
|
|
560
|
+
refValue: unknown,
|
|
561
|
+
ctx: ResolveCtx,
|
|
562
|
+
ownerModule: string | undefined,
|
|
563
|
+
): ResourceManifest | undefined {
|
|
564
|
+
if (!refValue || typeof refValue !== "object" || Array.isArray(refValue)) return undefined;
|
|
565
|
+
const ref = refValue as Record<string, any>;
|
|
566
|
+
const kind = ref.kind as string | undefined;
|
|
567
|
+
if (!kind) {
|
|
568
|
+
const sentinel = refSentinelTarget(ref);
|
|
569
|
+
return sentinel ? findSentinelTarget(ctx, sentinel, ownerModule) : undefined;
|
|
570
|
+
}
|
|
571
|
+
const name = ref.name as string | undefined;
|
|
572
|
+
if (!name) return undefined;
|
|
573
|
+
const scopeResolver = scopeResolverFor(ctx, ownerModule);
|
|
574
|
+
const scopedKind = scopeResolver?.resolveKind(kind);
|
|
575
|
+
return findTarget(
|
|
576
|
+
ctx,
|
|
577
|
+
name,
|
|
578
|
+
ownerModule,
|
|
579
|
+
(m) =>
|
|
580
|
+
m.kind === kind ||
|
|
581
|
+
ctx.aliases.resolveKind(m.kind) === kind ||
|
|
582
|
+
m.kind === ctx.aliases.resolveKind(kind) ||
|
|
583
|
+
(scopedKind !== undefined && m.kind === scopedKind),
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
354
587
|
/** Resolve a passthrough-style invocable at a specific call site. Recognised forms
|
|
355
588
|
* (see "passthrough: true" in the plan):
|
|
356
589
|
* - constant literal (no template) → `{ <literal> }`
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Browser-safe: no Node built-ins.
|
|
15
15
|
*/
|
|
16
16
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
17
|
-
import {
|
|
17
|
+
import { refSentinelTarget } from "./ref-sentinel-target.js";
|
|
18
18
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
19
19
|
import {
|
|
20
20
|
buildCallGraph,
|
|
@@ -28,9 +28,10 @@ import {
|
|
|
28
28
|
propertySchemas,
|
|
29
29
|
resolveLocalRef,
|
|
30
30
|
} from "./manifest-navigation.js";
|
|
31
|
-
import type {
|
|
31
|
+
import type { ModuleDocuments } from "./module-documents.js";
|
|
32
32
|
import { readProvidesZone, readRequiresZone } from "./zone-slot.js";
|
|
33
33
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
34
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
34
35
|
|
|
35
36
|
const SOURCE = "telo-analyzer";
|
|
36
37
|
|
|
@@ -125,7 +126,7 @@ function definitionResolver(
|
|
|
125
126
|
aliasesByModule: Map<string, AliasResolver>,
|
|
126
127
|
) {
|
|
127
128
|
return (kind: string, module?: string): ResourceDefinition | undefined => {
|
|
128
|
-
const scope = (module
|
|
129
|
+
const scope = moduleAliasScope({ module }, aliases, aliasesByModule);
|
|
129
130
|
const canonical = scope.resolveKind(kind);
|
|
130
131
|
return defs.resolve(kind) ?? (canonical ? defs.resolve(canonical) : undefined);
|
|
131
132
|
};
|
|
@@ -193,17 +194,16 @@ function schemaNodeAt(
|
|
|
193
194
|
* under-approximating direction the whole pass leans on. `Self.` is a local
|
|
194
195
|
* name written the long way and does resolve.
|
|
195
196
|
*
|
|
196
|
-
* This
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
197
|
+
* This is this pass's REDUCTION over the shared `refSentinelTarget` parse, not a
|
|
198
|
+
* second reading of the tag: `call-graph`'s answers a different question (what
|
|
199
|
+
* an EDGE points at, cross-module included, for a graph whose consumers tolerate
|
|
200
|
+
* an unresolved target) and states its own reduction the same way.
|
|
200
201
|
*/
|
|
201
202
|
function refName(value: unknown): string | undefined {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return source.slice(0, dot) === "Self" ? source.slice(dot + 1) : undefined;
|
|
203
|
+
const sentinel = refSentinelTarget(value);
|
|
204
|
+
if (sentinel) {
|
|
205
|
+
if (sentinel.alias === undefined) return sentinel.name;
|
|
206
|
+
return sentinel.alias === "Self" ? sentinel.name : undefined;
|
|
207
207
|
}
|
|
208
208
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
209
209
|
const v = value as Record<string, unknown>;
|
|
@@ -712,7 +712,7 @@ export function zoneDocumentsSignature(manifests: readonly ResourceManifest[]):
|
|
|
712
712
|
* the projection derive-only, and keep what reaches an exported instance.
|
|
713
713
|
*/
|
|
714
714
|
export function deriveLibraryExportRequirements(
|
|
715
|
-
docs:
|
|
715
|
+
docs: ModuleDocuments,
|
|
716
716
|
defs: DefinitionRegistry,
|
|
717
717
|
aliases: AliasResolver,
|
|
718
718
|
aliasesByModule: Map<string, AliasResolver>,
|
|
@@ -745,7 +745,7 @@ export interface ZoneAnalysisArgs {
|
|
|
745
745
|
aliases: AliasResolver;
|
|
746
746
|
aliasesByModule: Map<string, AliasResolver>;
|
|
747
747
|
rootModules: ReadonlySet<string>;
|
|
748
|
-
moduleDocuments?: readonly
|
|
748
|
+
moduleDocuments?: readonly ModuleDocuments[];
|
|
749
749
|
cache?: ZoneExportCache;
|
|
750
750
|
}
|
|
751
751
|
|
package/src/schema-compat.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
celBaseOfValueType,
|
|
10
10
|
celTypeOfValueType,
|
|
11
|
+
isCompiledValue,
|
|
11
12
|
readValueTypeSlot,
|
|
12
13
|
valueBrandBases,
|
|
13
14
|
valueTypeOf,
|
|
@@ -799,6 +800,18 @@ export function substituteCelFields(
|
|
|
799
800
|
mark();
|
|
800
801
|
return celPlaceholderForSchema(resolved);
|
|
801
802
|
}
|
|
803
|
+
// The same fact in its third spelling. An expression reaches this walk as a
|
|
804
|
+
// `${{ … }}` string or a `!cel` sentinel BEFORE `precompileDoc`, and as a
|
|
805
|
+
// CompiledValue after — so a caller running under `compile: true` (every
|
|
806
|
+
// `telo run`, unlike `telo check`) handed one to AJV as a plain object, and a
|
|
807
|
+
// slot typed `boolean` rejected a `when:` the author wrote correctly. The
|
|
808
|
+
// kernel's `stripCompiledValues` has always substituted here; missing it on
|
|
809
|
+
// this side made one manifest mean two things depending on which command read
|
|
810
|
+
// it.
|
|
811
|
+
if (isCompiledValue(data)) {
|
|
812
|
+
mark();
|
|
813
|
+
return celPlaceholderForSchema(resolved);
|
|
814
|
+
}
|
|
802
815
|
if (Array.isArray(data)) {
|
|
803
816
|
const item = resolveRefIn((resolved.items ?? {}) as Record<string, any>, root, external);
|
|
804
817
|
return data.map((element, i) =>
|
|
@@ -53,10 +53,42 @@ const UNION_KEYWORDS = new Set(["anyOf", "oneOf"]);
|
|
|
53
53
|
* further in. These are what make a branch implausible. */
|
|
54
54
|
const SHAPE_KEYWORDS = new Set(["required", "type", "additionalProperties", "enum", "const"]);
|
|
55
55
|
|
|
56
|
+
/** Keywords that exist to DISCRIMINATE, so a mismatch is positive evidence that
|
|
57
|
+
* the value is not this branch — at any depth, not only at the union node.
|
|
58
|
+
*
|
|
59
|
+
* Depth is otherwise the tiebreak, and it inverts exactly here: a branch that
|
|
60
|
+
* agreed on the discriminator and failed one constraint reported at the union
|
|
61
|
+
* node loses to every branch that disagreed about the discriminator one level
|
|
62
|
+
* in. That is how a `capability: Telo.Service` document declaring a forbidden
|
|
63
|
+
* key was reported as `/capability must be equal to constant` — naming neither
|
|
64
|
+
* the key at fault nor a branch the value could ever have been. */
|
|
65
|
+
const DISCRIMINATOR_KEYWORDS = new Set(["const", "enum"]);
|
|
66
|
+
|
|
67
|
+
/** RFC 6901 escapes: `~1` is a literal `/` in the key, `~0` a literal `~`.
|
|
68
|
+
* Decoded wherever a segment is shown or matched, because encoded it is not the
|
|
69
|
+
* key the manifest holds — a content map (`application/json`) anchors nowhere
|
|
70
|
+
* and reads wrong in the sentence. `~0` is expanded LAST, or `~01` would decode
|
|
71
|
+
* to `/` instead of the literal `~1` it encodes. */
|
|
72
|
+
function unescapeSegment(part: string): string {
|
|
73
|
+
return part.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The whole pointer, segment by segment, for prose that quotes it verbatim. */
|
|
77
|
+
function unescapePointer(pointer: string): string {
|
|
78
|
+
return pointer
|
|
79
|
+
.split("/")
|
|
80
|
+
.map((segment, i) => (i === 0 ? segment : unescapeSegment(segment)))
|
|
81
|
+
.join("/");
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
/* ------------------------------------------------------------------ prose */
|
|
57
85
|
|
|
58
86
|
export function formatSingleError(err: AjvErrorLike): string {
|
|
59
|
-
|
|
87
|
+
// Unescaped for the same reason the path is: a reader acting on this sentence
|
|
88
|
+
// needs the key the manifest holds (`application/json`), not its RFC 6901
|
|
89
|
+
// encoding. Reporting one form in `path` and the other in `message` made one
|
|
90
|
+
// diagnostic disagree with itself.
|
|
91
|
+
const p = unescapePointer(err.instancePath || "") || "/";
|
|
60
92
|
const params = err.params ?? {};
|
|
61
93
|
switch (err.keyword) {
|
|
62
94
|
case "additionalProperties":
|
|
@@ -67,6 +99,16 @@ export function formatSingleError(err: AjvErrorLike): string {
|
|
|
67
99
|
return `${p} ${err.message ?? "is invalid"} (${(params.allowedValues as unknown[])?.join(" | ")})`;
|
|
68
100
|
case "type":
|
|
69
101
|
return `${p} must be ${params.type}${describeActual(err)}`;
|
|
102
|
+
// A `false` schema at a property is how a branch forbids a key it otherwise
|
|
103
|
+
// declares. AJV's own text ("boolean schema is false") describes the schema
|
|
104
|
+
// rather than the value, and the path is the only part a reader can act on.
|
|
105
|
+
case "false schema":
|
|
106
|
+
return `${p} is not allowed here`;
|
|
107
|
+
// `not:` says a forbidden shape matched, and AJV reports nothing about the
|
|
108
|
+
// inner schema — so the honest message says where, and no more. A branch
|
|
109
|
+
// that wants to name the key writes `properties: { <key>: false }` instead.
|
|
110
|
+
case "not":
|
|
111
|
+
return `${p} matches a shape that is not allowed here`;
|
|
70
112
|
default:
|
|
71
113
|
return `${p} ${err.message ?? "is invalid"}`;
|
|
72
114
|
}
|
|
@@ -130,11 +172,13 @@ function describeAlternatives(errors: AjvErrorLike[], unionInstancePath: string)
|
|
|
130
172
|
}
|
|
131
173
|
|
|
132
174
|
/** Is this branch a plausible reading of the value — does it accept the value's
|
|
133
|
-
* shape at the union node itself, and
|
|
175
|
+
* shape at the union node itself, and agree with every discriminator it pins? */
|
|
134
176
|
function isPlausible(errors: AjvErrorLike[], unionInstancePath: string): boolean {
|
|
135
|
-
return !errors.some(
|
|
136
|
-
|
|
137
|
-
|
|
177
|
+
return !errors.some((e) => {
|
|
178
|
+
const keyword = e.keyword ?? "";
|
|
179
|
+
if (DISCRIMINATOR_KEYWORDS.has(keyword)) return true;
|
|
180
|
+
return (e.instancePath || "") === unionInstancePath && SHAPE_KEYWORDS.has(keyword);
|
|
181
|
+
});
|
|
138
182
|
}
|
|
139
183
|
|
|
140
184
|
/**
|
|
@@ -392,7 +436,7 @@ export function ajvErrorToPath(err: AjvErrorLike): string {
|
|
|
392
436
|
let result = "";
|
|
393
437
|
for (const part of parts) {
|
|
394
438
|
if (/^\d+$/.test(part)) result += `[${part}]`;
|
|
395
|
-
else result += result ? `.${part}` : part;
|
|
439
|
+
else result += result ? `.${unescapeSegment(part)}` : unescapeSegment(part);
|
|
396
440
|
}
|
|
397
441
|
if (err.keyword === "required" && err.params?.missingProperty) {
|
|
398
442
|
const missing = err.params.missingProperty as string;
|
package/src/schema-keywords.ts
CHANGED
|
@@ -386,7 +386,10 @@ export const TELO_SCHEMA_ANNOTATIONS: Record<
|
|
|
386
386
|
},
|
|
387
387
|
"x-telo-catches-for": {
|
|
388
388
|
title: "Catches for",
|
|
389
|
-
description:
|
|
389
|
+
description:
|
|
390
|
+
"Whose failures this catch list renders: a sibling field naming the handler, " +
|
|
391
|
+
"or the EMPTY string for everything this resource drives — a scope-level list, " +
|
|
392
|
+
"which owes coverage of nothing itself and answers for every site it encloses.",
|
|
390
393
|
type: "string",
|
|
391
394
|
},
|
|
392
395
|
};
|
package/src/schema-walk.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* scope rule is consumed by the IDE, which must not pull the pass in behind it.
|
|
10
10
|
*/
|
|
11
11
|
import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
|
|
12
|
+
import { readRefSlot, type RefSlot } from "./ref-slot.js";
|
|
13
|
+
import { readStepSlot, type StepSlot } from "./step-slot.js";
|
|
12
14
|
|
|
13
15
|
/** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
|
|
14
16
|
* Non-refs and unresolved refs pass through unchanged. */
|
|
@@ -142,3 +144,57 @@ export function walkStepArray(
|
|
|
142
144
|
}
|
|
143
145
|
});
|
|
144
146
|
}
|
|
147
|
+
/** A slot through which a resource drives another. */
|
|
148
|
+
export type DrivenSlot =
|
|
149
|
+
| { kind: "step"; slot: StepSlot; data: unknown[]; path: string }
|
|
150
|
+
| { kind: "ref"; slot: RefSlot; data: unknown; path: string };
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Every slot of one resource through which it drives another, schema and data in
|
|
154
|
+
* tandem.
|
|
155
|
+
*
|
|
156
|
+
* One traversal with two consumers — the inherited union here, and the catch
|
|
157
|
+
* scope enclosure in `validate-throws-coverage.ts` — because both ask the same
|
|
158
|
+
* structural question and two copies would eventually disagree about where the
|
|
159
|
+
* walk stops. It terminates on the manifest's own depth, and it stops AT a step
|
|
160
|
+
* slot (that traversal owns everything below it, `try`/`catch` subtraction
|
|
161
|
+
* included) and AT a reference slot (a resolved ref is a leaf, `{kind, name}`,
|
|
162
|
+
* with nothing beneath it to visit).
|
|
163
|
+
*/
|
|
164
|
+
export function forEachDrivenSlot(
|
|
165
|
+
schema: unknown,
|
|
166
|
+
data: unknown,
|
|
167
|
+
visit: (slot: DrivenSlot) => void,
|
|
168
|
+
path = "",
|
|
169
|
+
): void {
|
|
170
|
+
if (!schema || typeof schema !== "object" || data === undefined || data === null) return;
|
|
171
|
+
const node = schema as Record<string, any>;
|
|
172
|
+
|
|
173
|
+
const stepSlot = readStepSlot(node);
|
|
174
|
+
if (stepSlot) {
|
|
175
|
+
if (Array.isArray(data)) visit({ kind: "step", slot: stepSlot, data, path });
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const refSlot = readRefSlot(node);
|
|
180
|
+
if (refSlot) {
|
|
181
|
+
visit({ kind: "ref", slot: refSlot, data, path });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const props = node.properties as Record<string, any> | undefined;
|
|
186
|
+
if (props && typeof data === "object" && !Array.isArray(data)) {
|
|
187
|
+
const obj = data as Record<string, unknown>;
|
|
188
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
189
|
+
if (obj[key] === undefined) continue;
|
|
190
|
+
forEachDrivenSlot(propSchema, obj[key], visit, path ? `${path}.${key}` : key);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (node.items && Array.isArray(data)) {
|
|
195
|
+
for (const [i, item] of data.entries()) {
|
|
196
|
+
forEachDrivenSlot(node.items, item, visit, `${path}[${i}]`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
package/src/telo-version.ts
CHANGED
package/src/template-body.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
import type { ModuleScopes } from "./alias-resolver.js";
|
|
5
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* A `Telo.Definition`'s `resources:` entries — the bodies a kind writes for
|
|
@@ -47,11 +48,7 @@ export function templateBodies(
|
|
|
47
48
|
// A nested kind is written through the alias scope of the module that DECLARED
|
|
48
49
|
// the definition, never the consumer's — the same rule every other kind
|
|
49
50
|
// resolution in a forwarded manifest follows.
|
|
50
|
-
const
|
|
51
|
-
const scope =
|
|
52
|
-
(ownModule && scopes && !scopes.rootModules.has(ownModule)
|
|
53
|
-
? scopes.aliasesByModule.get(ownModule)
|
|
54
|
-
: undefined) ?? aliases;
|
|
51
|
+
const scope = moduleAliasScope(m.metadata, aliases, scopes?.aliasesByModule);
|
|
55
52
|
|
|
56
53
|
const out: TemplateBody[] = [];
|
|
57
54
|
for (let i = 0; i < bodies.length; i++) {
|