@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
|
@@ -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/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++) {
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HostVersions } from "./requires-block.js";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ModuleDocuments } from "./module-documents.js";
|
|
4
4
|
/** Matches LSP DiagnosticSeverity values exactly.
|
|
5
5
|
* https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnosticSeverity */
|
|
6
6
|
export const DiagnosticSeverity = {
|
|
@@ -142,10 +142,10 @@ export interface AnalysisOptions {
|
|
|
142
142
|
/** Imported libraries' FULL document sets, for the zone stage's per-library
|
|
143
143
|
* export derivation — the flattened analysis view forwards only each
|
|
144
144
|
* library's export surface, never its internal dispatch chain. Collected
|
|
145
|
-
* from a LoadedGraph via `
|
|
145
|
+
* from a LoadedGraph via `collectModuleDocuments`. Omitting it skips
|
|
146
146
|
* the derivation (the under-approximating direction — the runtime check
|
|
147
147
|
* remains the enforcement). */
|
|
148
|
-
moduleDocuments?:
|
|
148
|
+
moduleDocuments?: ModuleDocuments[];
|
|
149
149
|
/** When true, `analyze()` runs the state-mutating setup (module identity /
|
|
150
150
|
* alias / definition registration plus `normalizeInlineResources`) but
|
|
151
151
|
* skips every diagnostic-producing pass — per-resource validation, the
|
|
@@ -2,13 +2,17 @@ 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 {
|
|
5
|
+
ancestorChain,
|
|
5
6
|
type ContractDirection,
|
|
6
7
|
type DefResolver,
|
|
7
8
|
effectiveContractField,
|
|
8
9
|
mappingFieldFor,
|
|
9
10
|
needsContractMapping,
|
|
10
11
|
} from "./extends-resolution.js";
|
|
12
|
+
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
13
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
11
14
|
import { buildReferenceFieldMap, isRefEntry } from "./reference-field-map.js";
|
|
15
|
+
import { checkSchemaCompatibility } from "./schema-compat.js";
|
|
12
16
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
13
17
|
|
|
14
18
|
const SOURCE = "telo-analyzer";
|
|
@@ -29,6 +33,11 @@ const SOURCE = "telo-analyzer";
|
|
|
29
33
|
* whose input contract is now `inputType:`.
|
|
30
34
|
* - CONTRACT_TYPE_NOT_FOUND: a contract names a type that is not declared in
|
|
31
35
|
* scope, so every call through it would fail at dispatch.
|
|
36
|
+
* - CONTRACT_NOT_SUBSTITUTABLE: a definition replaces an ancestor's contract
|
|
37
|
+
* with a shape that cannot stand in for it. The one check nothing else makes:
|
|
38
|
+
* contracts resolve to the nearest declaration in BOTH halves, so a child that
|
|
39
|
+
* declares its own is compared against its abstract by neither the analyzer
|
|
40
|
+
* nor dispatch.
|
|
32
41
|
*
|
|
33
42
|
* Deliberately NOT diagnosed: an input that is neither `required:` nor
|
|
34
43
|
* defaulted. It is indistinguishable from a genuinely optional one — `Ai.Text`
|
|
@@ -45,8 +54,7 @@ export function validateInvocationContract(
|
|
|
45
54
|
): AnalysisDiagnostic[] {
|
|
46
55
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
47
56
|
const resolveDef: DefResolver = (kind, from) => {
|
|
48
|
-
const
|
|
49
|
-
const scope = (module ? aliasesByModule.get(module) : undefined) ?? aliases;
|
|
57
|
+
const scope = moduleAliasScope(from?.metadata, aliases, aliasesByModule);
|
|
50
58
|
return registry.resolve(kind) ?? registry.resolve(scope.resolveKind(kind) ?? kind);
|
|
51
59
|
};
|
|
52
60
|
|
|
@@ -73,6 +81,7 @@ export function validateInvocationContract(
|
|
|
73
81
|
if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") {
|
|
74
82
|
checkMappingRequired(m, resource, filePath, resolveDef, diagnostics);
|
|
75
83
|
checkContractResolves(m, md, manifests, resource, filePath, diagnostics);
|
|
84
|
+
checkAncestorSubstitutability(m, md, manifests, resource, filePath, resolveDef, diagnostics);
|
|
76
85
|
continue;
|
|
77
86
|
}
|
|
78
87
|
|
|
@@ -360,6 +369,123 @@ function namedTypeReference(value: unknown): string | undefined {
|
|
|
360
369
|
return typeof ref.name === "string" ? ref.name : undefined;
|
|
361
370
|
}
|
|
362
371
|
|
|
372
|
+
/**
|
|
373
|
+
* A REPLACING CONTRACT MUST STILL STAND IN FOR THE ONE IT REPLACES.
|
|
374
|
+
*
|
|
375
|
+
* Contracts resolve to the NEAREST declaration and never merge, which is right —
|
|
376
|
+
* a call signature is not additive, and a backend narrowing `status` to the
|
|
377
|
+
* states it actually has could not express that through a merge. But the
|
|
378
|
+
* consequence is that an abstract's contract binds only the children that
|
|
379
|
+
* declare none of their own: a child that declares one is checked against
|
|
380
|
+
* nothing, statically or at dispatch, since `resolveBoundContract` stops at the
|
|
381
|
+
* same nearest declaration. So an abstract could state a floor every
|
|
382
|
+
* implementation must answer with (`Durable.Run`'s `runId` / `status`, the
|
|
383
|
+
* identity every later question about a run is asked by) and have that floor hold
|
|
384
|
+
* for exactly the implementations that declared nothing — while the real ones,
|
|
385
|
+
* which all declare their own, escaped it. A slot typed by the abstract would
|
|
386
|
+
* then be typed by a promise nothing kept.
|
|
387
|
+
*
|
|
388
|
+
* The rule is substitutability, and its direction differs per contract because
|
|
389
|
+
* one is produced and the other consumed:
|
|
390
|
+
*
|
|
391
|
+
* - `outputType` is COVARIANT — a consumer written against the ancestor reads
|
|
392
|
+
* the ancestor's shape, so the child's output must be acceptable where the
|
|
393
|
+
* ancestor's is declared. Dropping a required property is what breaks it.
|
|
394
|
+
* - `inputType` is CONTRAVARIANT — a caller written against the ancestor sends
|
|
395
|
+
* the ancestor's shape, so the ancestor's input must be acceptable where the
|
|
396
|
+
* child's is declared. Requiring an input the ancestor never mentions is what
|
|
397
|
+
* breaks it: no such caller could satisfy it.
|
|
398
|
+
*
|
|
399
|
+
* **Only an ABSTRACT ancestor's contract is a contract**, and that narrowing is a
|
|
400
|
+
* decision rather than an oversight. Extending a CONCRETE kind is how a friendlier
|
|
401
|
+
* schema is put over an existing controller — `base:` reshapes its config and
|
|
402
|
+
* `inputs:` / `result:` translate its call signature — so a child there is
|
|
403
|
+
* SUPPOSED to present different inputs, and demanding substitutability would
|
|
404
|
+
* reject the pattern the standard library and every custom-kind example are built
|
|
405
|
+
* on. An abstract has no implementation to reshape: extending one is a claim to
|
|
406
|
+
* BE the thing, its contract is written for implementors, and a slot typed by it
|
|
407
|
+
* is polymorphic by construction — which is exactly where an unkept promise has
|
|
408
|
+
* nowhere to be caught.
|
|
409
|
+
*
|
|
410
|
+
* A direction the child BRIDGES (`inputs:` for inputs, `result:` for outputs) is
|
|
411
|
+
* skipped for the same reason one hop down: the mapping is the author saying the
|
|
412
|
+
* shapes differ deliberately and are translated.
|
|
413
|
+
*
|
|
414
|
+
* The comparison is {@link checkSchemaCompatibility}, which reports only DEFINITE
|
|
415
|
+
* mismatches — a union, an absent `type`, an undeclared argument all read as
|
|
416
|
+
* compatible — so narrowing a value's type, adding an optional property or
|
|
417
|
+
* restricting an enum stays legal, which is the whole point of letting a child
|
|
418
|
+
* replace the contract at all.
|
|
419
|
+
*/
|
|
420
|
+
function checkAncestorSubstitutability(
|
|
421
|
+
m: ResourceManifest,
|
|
422
|
+
md: Record<string, unknown>,
|
|
423
|
+
manifests: ResourceManifest[],
|
|
424
|
+
resource: { kind: string; name: string },
|
|
425
|
+
filePath: string | undefined,
|
|
426
|
+
resolveDef: DefResolver,
|
|
427
|
+
diagnostics: AnalysisDiagnostic[],
|
|
428
|
+
): void {
|
|
429
|
+
const def = m as unknown as ResourceDefinition;
|
|
430
|
+
|
|
431
|
+
for (const direction of ["inputType", "outputType"] as ContractDirection[]) {
|
|
432
|
+
const own = md[direction];
|
|
433
|
+
if (own === undefined || own === null) continue;
|
|
434
|
+
// A declared bridge says the shapes differ on purpose and are translated.
|
|
435
|
+
if (md[mappingFieldFor(direction)] != null) continue;
|
|
436
|
+
|
|
437
|
+
// The nearest ANCESTOR that declares it — the contract this one replaces.
|
|
438
|
+
// Walking past self is the whole question: `contractDeclarer` would answer
|
|
439
|
+
// "this one", which is what nothing checks.
|
|
440
|
+
const ancestor = ancestorChain(def, resolveDef).find((a) => {
|
|
441
|
+
const inherited = (a as unknown as Record<string, unknown>)[direction];
|
|
442
|
+
return inherited !== undefined && inherited !== null;
|
|
443
|
+
});
|
|
444
|
+
if (!ancestor || ancestor.kind !== "Telo.Abstract") continue;
|
|
445
|
+
const inherited = (ancestor as unknown as Record<string, unknown>)[direction];
|
|
446
|
+
|
|
447
|
+
// The WHOLE manifest set, which is what `analyzerContractScope`'s
|
|
448
|
+
// `typeManifestsFor` hands `resolveContract` — so this resolves a named type
|
|
449
|
+
// exactly as the resolver the kernel binds with does. Filtering to the
|
|
450
|
+
// declaring module looked more careful and was strictly worse: a contract
|
|
451
|
+
// naming a shape from a shared module resolved to nothing, and an
|
|
452
|
+
// unresolvable side is skipped, so the check silently switched itself off for
|
|
453
|
+
// precisely the libraries that factor their shapes out. Nothing else caught
|
|
454
|
+
// it either — `CONTRACT_TYPE_NOT_FOUND` finds the type through its own global
|
|
455
|
+
// fallback, so such a kind passed `telo check` with no diagnostic at all.
|
|
456
|
+
const ownSchema = resolveTypeFieldToSchema(own, manifests);
|
|
457
|
+
const ancestorSchema = resolveTypeFieldToSchema(inherited, manifests);
|
|
458
|
+
// An unresolvable side says nothing about compatibility; CONTRACT_TYPE_NOT_FOUND
|
|
459
|
+
// is what reports a contract that names a type nothing declares.
|
|
460
|
+
if (!ownSchema || !ancestorSchema) continue;
|
|
461
|
+
|
|
462
|
+
const { compatible, issues } =
|
|
463
|
+
direction === "outputType"
|
|
464
|
+
? checkSchemaCompatibility(ownSchema, ancestorSchema)
|
|
465
|
+
: checkSchemaCompatibility(ancestorSchema, ownSchema);
|
|
466
|
+
if (compatible) continue;
|
|
467
|
+
|
|
468
|
+
const ancestorName = `${(ancestor.metadata as { module?: string } | undefined)?.module ?? ""}.${
|
|
469
|
+
ancestor.metadata?.name ?? "?"
|
|
470
|
+
}`.replace(/^\./, "");
|
|
471
|
+
const why =
|
|
472
|
+
direction === "outputType"
|
|
473
|
+
? `every caller that holds this through '${ancestorName}' reads the shape that kind declares`
|
|
474
|
+
: `a caller that holds this through '${ancestorName}' sends the shape that kind declares`;
|
|
475
|
+
diagnostics.push({
|
|
476
|
+
severity: DiagnosticSeverity.Error,
|
|
477
|
+
code: "CONTRACT_NOT_SUBSTITUTABLE",
|
|
478
|
+
source: SOURCE,
|
|
479
|
+
message:
|
|
480
|
+
`${m.kind}/${resource.name}: \`${direction}\` replaces the one declared by '${ancestorName}' ` +
|
|
481
|
+
`with a shape that cannot stand in for it — ${issues.join("; ")}. Contracts replace rather ` +
|
|
482
|
+
`than merge, so nothing re-checks this at dispatch: ${why}. Restate the fields ` +
|
|
483
|
+
`'${ancestorName}' declares, or drop \`${direction}\` to inherit the contract unchanged.`,
|
|
484
|
+
data: { resource, filePath, path: direction },
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
363
489
|
/** A child that inherits its controller and REPLACES a contract must bridge it:
|
|
364
490
|
* contracts resolve to the nearest declaration and never merge, so the
|
|
365
491
|
* inherited controller only understands its own shape. Without the mapping the
|
package/src/validate-logging.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { AliasResolver } from "./alias-resolver.js";
|
|
|
3
3
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
4
4
|
import { parseRedactionPath, RedactionPathError } from "./redaction-path.js";
|
|
5
5
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
6
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
6
7
|
|
|
7
8
|
const SOURCE = "telo-analyzer";
|
|
8
9
|
|
|
@@ -124,9 +125,7 @@ function isSinkKind(
|
|
|
124
125
|
): boolean {
|
|
125
126
|
if (typeof manifest.kind !== "string") return false;
|
|
126
127
|
if (manifest.kind === "Telo.ConsoleSink" || manifest.kind === "Telo.FileSink") return true;
|
|
127
|
-
const
|
|
128
|
-
const resolver =
|
|
129
|
-
(ownModule ? aliasesByModule?.get(ownModule) : undefined) ?? aliases;
|
|
128
|
+
const resolver = moduleAliasScope(manifest.metadata, aliases, aliasesByModule);
|
|
130
129
|
const canonical = resolver.resolveKind(manifest.kind) ?? manifest.kind;
|
|
131
130
|
return registry.resolve(canonical)?.capability === "Telo.Sink";
|
|
132
131
|
}
|
|
@@ -14,6 +14,8 @@ import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
|
14
14
|
import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisContext } from "./types.js";
|
|
15
15
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
16
16
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
17
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
18
|
+
import { isInjectedDeclaration } from "./resource-input.js";
|
|
17
19
|
|
|
18
20
|
const SOURCE = "telo-analyzer";
|
|
19
21
|
|
|
@@ -27,20 +29,31 @@ const SOURCE = "telo-analyzer";
|
|
|
27
29
|
*
|
|
28
30
|
* A value satisfies the slot when it transitively extends the target kind, or —
|
|
29
31
|
* for a CONCRETE target — IS that kind; `getByExtends` is the same transitive
|
|
30
|
-
* subtype index for both
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* subtype index for both. Accepts a constraint that resolves to nothing, and an
|
|
33
|
+
* abstract with no loaded implementations: partial context, where a rejection
|
|
34
|
+
* would be a guess.
|
|
35
|
+
*
|
|
36
|
+
* An abstract is satisfied only by an implementer, never by the abstract itself,
|
|
37
|
+
* because a resource declared `kind: <some abstract>` is refused at `create()`
|
|
38
|
+
* and reported as `ABSTRACT_KIND_INSTANTIATED` at its declaration — so accepting
|
|
39
|
+
* it here would leave the reference sites silent about a manifest that cannot
|
|
40
|
+
* run. `isDeclaration` is the ONE exception and it is not an instance: a
|
|
41
|
+
* library's `resources:` entry is constrained by kind alone, so its kind-only
|
|
42
|
+
* stand-in routinely IS an abstract (`connection: {kind: Sql.Connection}`), and
|
|
43
|
+
* every use of that name inside the library is a reference to it.
|
|
34
44
|
*/
|
|
35
45
|
export function kindSatisfies(
|
|
36
46
|
resolved: string,
|
|
37
47
|
targetKind: string,
|
|
38
48
|
registry: DefinitionRegistry,
|
|
49
|
+
/** The referent is a kind-only stand-in for a `resources:` entry rather than a
|
|
50
|
+
* resource anything instantiates, so identity satisfies an abstract slot. */
|
|
51
|
+
isDeclaration = false,
|
|
39
52
|
): boolean {
|
|
40
53
|
const canonical = registry.resolveRef(targetKind) ?? targetKind;
|
|
41
54
|
const targetDef = registry.resolve(canonical);
|
|
42
55
|
if (!targetDef) return true;
|
|
43
|
-
if (targetDef.kind !== "Telo.Abstract" && resolved === canonical) return true;
|
|
56
|
+
if ((targetDef.kind !== "Telo.Abstract" || isDeclaration) && resolved === canonical) return true;
|
|
44
57
|
const subtypes = registry.getByExtends(canonical);
|
|
45
58
|
if (subtypes.some((d) => `${d.metadata.module}.${d.metadata.name}` === resolved)) return true;
|
|
46
59
|
// Leniency is about the CANDIDATE, not about the population, and it is the
|
|
@@ -70,6 +83,8 @@ function checkKind(
|
|
|
70
83
|
entry: RefFieldEntry,
|
|
71
84
|
registry: DefinitionRegistry,
|
|
72
85
|
aliases: AliasResolver,
|
|
86
|
+
/** See {@link kindSatisfies}: the referent is a `resources:` stand-in. */
|
|
87
|
+
isDeclaration = false,
|
|
73
88
|
): string[] {
|
|
74
89
|
const resolved = aliases.resolveKind(kind) ?? kind;
|
|
75
90
|
// A qualified kind whose prefix names no import in this scope is a bad NAME
|
|
@@ -92,7 +107,7 @@ function checkKind(
|
|
|
92
107
|
if (!targetKind) return [];
|
|
93
108
|
const targetDef = registry.resolve(targetKind);
|
|
94
109
|
if (!targetDef) return [];
|
|
95
|
-
if (!unknownAlias && kindSatisfies(resolved, targetKind, registry)) return [];
|
|
110
|
+
if (!unknownAlias && kindSatisfies(resolved, targetKind, registry, isDeclaration)) return [];
|
|
96
111
|
const subtypes = registry.getByExtends(targetKind);
|
|
97
112
|
const subtypeKinds = new Set(subtypes.map((d) => `${d.metadata.module}.${d.metadata.name}`));
|
|
98
113
|
if (targetDef.kind === "Telo.Abstract") {
|
|
@@ -335,7 +350,13 @@ export function validateReferences(
|
|
|
335
350
|
});
|
|
336
351
|
return;
|
|
337
352
|
}
|
|
338
|
-
const kindErrors = checkKind(
|
|
353
|
+
const kindErrors = checkKind(
|
|
354
|
+
target.kind as string,
|
|
355
|
+
entry,
|
|
356
|
+
registry,
|
|
357
|
+
aliases,
|
|
358
|
+
isInjectedDeclaration(target),
|
|
359
|
+
);
|
|
339
360
|
if (kindErrors.length > 0) {
|
|
340
361
|
diagnostics.push({
|
|
341
362
|
severity: DiagnosticSeverity.Error,
|
|
@@ -385,7 +406,18 @@ export function validateReferences(
|
|
|
385
406
|
}
|
|
386
407
|
|
|
387
408
|
// 2. Kind check
|
|
388
|
-
const
|
|
409
|
+
const objectTarget =
|
|
410
|
+
typeof refVal.name === "string"
|
|
411
|
+
? (visibleScopeManifests.find((m) => m.metadata?.name === refVal.name) ??
|
|
412
|
+
byName.get(refVal.name))
|
|
413
|
+
: undefined;
|
|
414
|
+
const kindErrors = checkKind(
|
|
415
|
+
refVal.kind,
|
|
416
|
+
entry,
|
|
417
|
+
registry,
|
|
418
|
+
aliases,
|
|
419
|
+
isInjectedDeclaration(objectTarget),
|
|
420
|
+
);
|
|
389
421
|
if (kindErrors.length > 0) {
|
|
390
422
|
diagnostics.push({
|
|
391
423
|
severity: DiagnosticSeverity.Error,
|
|
@@ -484,9 +516,7 @@ export function validateReferences(
|
|
|
484
516
|
const resolvedResourceKind = aliases.resolveKind(r.kind) ?? r.kind;
|
|
485
517
|
const resourceDef =
|
|
486
518
|
registry.resolve(r.kind) ?? registry.resolve(resolvedResourceKind);
|
|
487
|
-
const
|
|
488
|
-
const ownerScope =
|
|
489
|
-
(owningModule ? aliasesByModule?.get(owningModule) : undefined) ?? aliases;
|
|
519
|
+
const ownerScope = moduleAliasScope(resourceDef?.metadata, aliases, aliasesByModule);
|
|
490
520
|
|
|
491
521
|
const targetKind = ownerScope.resolveKind(anchorName);
|
|
492
522
|
if (!targetKind) {
|
|
@@ -3,7 +3,11 @@ import { isRefSentinel } from "@telorun/templating";
|
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { findDynamicLeaf } from "./resource-rule.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
isInjectedDeclaration,
|
|
8
|
+
readResourceInputs,
|
|
9
|
+
readSuppliedResources,
|
|
10
|
+
} from "./resource-input.js";
|
|
7
11
|
import { type AnalysisDiagnostic, DiagnosticSeverity } from "./types.js";
|
|
8
12
|
|
|
9
13
|
const SOURCE = "telo-analyzer";
|
|
@@ -38,6 +42,9 @@ type RequiredResources = Record<string, string>;
|
|
|
38
42
|
*
|
|
39
43
|
* Browser-safe.
|
|
40
44
|
*/
|
|
45
|
+
const declaringModule = (m: ResourceManifest): string | undefined =>
|
|
46
|
+
(m.metadata as { module?: string } | undefined)?.module;
|
|
47
|
+
|
|
41
48
|
export function validateResourceInputs(
|
|
42
49
|
manifests: ResourceManifest[],
|
|
43
50
|
registry: DefinitionRegistry,
|
|
@@ -46,7 +53,11 @@ export function validateResourceInputs(
|
|
|
46
53
|
/** Kind acceptance, transitively — the `checkKind` rule `validate-references`
|
|
47
54
|
* applies at an ordinary ref slot, passed in rather than re-derived so the
|
|
48
55
|
* two cannot disagree about what satisfies a constraint. */
|
|
49
|
-
acceptsKind: (
|
|
56
|
+
acceptsKind: (
|
|
57
|
+
suppliedKind: string,
|
|
58
|
+
requiredKind: string,
|
|
59
|
+
suppliedIsDeclaration: boolean,
|
|
60
|
+
) => boolean,
|
|
50
61
|
): AnalysisDiagnostic[] {
|
|
51
62
|
const out: AnalysisDiagnostic[] = [];
|
|
52
63
|
|
|
@@ -220,7 +231,19 @@ export function validateResourceInputs(
|
|
|
220
231
|
const suppliedKind = (value as { kind?: unknown }).kind;
|
|
221
232
|
if (typeof suppliedKind !== "string") continue;
|
|
222
233
|
const canonical = aliases.resolveKind(suppliedKind) ?? suppliedKind;
|
|
223
|
-
|
|
234
|
+
// A library forwarding an input it was itself handed supplies a kind-only
|
|
235
|
+
// stand-in, whose kind is routinely the abstract the target declares. That
|
|
236
|
+
// is identity against an abstract, which an ordinary reference is refused
|
|
237
|
+
// — the referent there would be a resource nothing can instantiate, while
|
|
238
|
+
// here it is another declaration.
|
|
239
|
+
const suppliedName = (value as { name?: unknown }).name;
|
|
240
|
+
const supplied =
|
|
241
|
+
typeof suppliedName === "string"
|
|
242
|
+
? manifests.find(
|
|
243
|
+
(c) => c.metadata?.name === suppliedName && isOwn(declaringModule(c)),
|
|
244
|
+
)
|
|
245
|
+
: undefined;
|
|
246
|
+
if (acceptsKind(canonical, entry, isInjectedDeclaration(supplied))) continue;
|
|
224
247
|
out.push({
|
|
225
248
|
severity: DiagnosticSeverity.Error,
|
|
226
249
|
code: "RESOURCE_INPUT_KIND_MISMATCH",
|
|
@@ -3,6 +3,7 @@ import { canonicalTypeSchemaId, parseTeloTypeRef } from "@telorun/sdk";
|
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
6
|
+
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
6
7
|
|
|
7
8
|
const SOURCE = "telo-analyzer";
|
|
8
9
|
const SCHEMA_FIELDS = ["schema", "inputType", "outputType"] as const;
|
|
@@ -38,7 +39,7 @@ export function validateSchemaTypeRefs(
|
|
|
38
39
|
// Only validate refs authored in a root module's scope; imported defs are
|
|
39
40
|
// validated against their own library when it's analyzed as a root.
|
|
40
41
|
if (ownModule && !rootModules.has(ownModule)) continue;
|
|
41
|
-
const resolver = (
|
|
42
|
+
const resolver = moduleAliasScope(m.metadata, aliases, aliasesByModule);
|
|
42
43
|
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
43
44
|
const label = `${m.kind}/${name}`;
|
|
44
45
|
|
|
@@ -523,11 +523,22 @@ export function validateThrowsCoverage(
|
|
|
523
523
|
env: Environment,
|
|
524
524
|
aliasesByModule: Map<string, AliasResolver> = new Map(),
|
|
525
525
|
rootModules: Set<string> = new Set(),
|
|
526
|
+
/** Each imported library's full document set, so the walk can follow an
|
|
527
|
+
* exported entry point into the siblings it invokes — which a consumer's
|
|
528
|
+
* flat set does not carry. */
|
|
529
|
+
moduleManifests: Map<string, ResourceManifest[]> = new Map(),
|
|
526
530
|
): AnalysisDiagnostic[] {
|
|
527
531
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
528
532
|
diagnostics.push(...validateThrowsDeclarations(manifests));
|
|
529
533
|
|
|
530
|
-
const resolveCtx = createResolveCtx(
|
|
534
|
+
const resolveCtx = createResolveCtx(
|
|
535
|
+
manifests,
|
|
536
|
+
defs,
|
|
537
|
+
aliases,
|
|
538
|
+
aliasesByModule,
|
|
539
|
+
rootModules,
|
|
540
|
+
moduleManifests,
|
|
541
|
+
);
|
|
531
542
|
|
|
532
543
|
// The alias resolver for a manifest's own lexical scope — an imported library's
|
|
533
544
|
// resolver when it owns the manifest, else undefined (fall back to root aliases).
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"zone-module-documents.d.ts","sourceRoot":"","sources":["../src/zone-module-documents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC"}
|
|
File without changes
|