@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
package/src/analyzer.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
type CelHandlers,
|
|
25
25
|
} from "./cel-environment.js";
|
|
26
26
|
import { DefinitionRegistry } from "./definition-registry.js";
|
|
27
|
+
import { readDeprecation } from "./deprecation.js";
|
|
27
28
|
import { type ContractDirection, effectiveAuthorSchema } from "./extends-resolution.js";
|
|
28
29
|
import {
|
|
29
30
|
analyzerContractScope,
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
} from "./validate-observed-state.js";
|
|
48
49
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
49
50
|
import { visitManifest } from "./manifest-visitor.js";
|
|
51
|
+
import { declaringModuleScope, moduleAliasScope } from "./module-alias-scope.js";
|
|
50
52
|
import { isModuleKind } from "./module-kinds.js";
|
|
51
53
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
52
54
|
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
@@ -124,7 +126,12 @@ import {
|
|
|
124
126
|
type SchemaIssue,
|
|
125
127
|
} from "./schema-compat.js";
|
|
126
128
|
import { collectValueSchemaIssues } from "./validate-value-schema.js";
|
|
127
|
-
import {
|
|
129
|
+
import {
|
|
130
|
+
DiagnosticSeverity,
|
|
131
|
+
DiagnosticTag,
|
|
132
|
+
type AnalysisDiagnostic,
|
|
133
|
+
type AnalysisOptions,
|
|
134
|
+
} from "./types.js";
|
|
128
135
|
import {
|
|
129
136
|
extractAccessChains,
|
|
130
137
|
extractContextsFromSchema,
|
|
@@ -999,10 +1006,10 @@ export class StaticAnalyzer {
|
|
|
999
1006
|
if (m.kind !== "Telo.Definition" && m.kind !== "Telo.Abstract") continue;
|
|
1000
1007
|
const def = m as unknown as ResourceDefinition;
|
|
1001
1008
|
const ownModule = (def.metadata as { module?: string } | undefined)?.module;
|
|
1002
|
-
const scopeResolver =
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1009
|
+
const scopeResolver = declaringModuleScope(ownModule, aliases, {
|
|
1010
|
+
aliasesByModule,
|
|
1011
|
+
rootModules,
|
|
1012
|
+
});
|
|
1006
1013
|
// Canonicalize alias-form `x-telo-ref` constraints in the DECLARING module's
|
|
1007
1014
|
// scope, before the schema reaches `register()` and the lazily-built field
|
|
1008
1015
|
// maps. Same pre-resolution `capability` / `extends` get below.
|
|
@@ -1425,7 +1432,7 @@ export class StaticAnalyzer {
|
|
|
1425
1432
|
// analyzer knowing it exists: going native costs a module, not a change
|
|
1426
1433
|
// here.
|
|
1427
1434
|
const resolveRegionDef = (kind: string, module?: string) => {
|
|
1428
|
-
const scope = (module
|
|
1435
|
+
const scope = moduleAliasScope({ module }, aliases, aliasesByModule);
|
|
1429
1436
|
const canonical = scope.resolveKind(kind);
|
|
1430
1437
|
return defs.resolve(kind) ?? (canonical ? defs.resolve(canonical) : undefined);
|
|
1431
1438
|
};
|
|
@@ -1456,8 +1463,10 @@ export class StaticAnalyzer {
|
|
|
1456
1463
|
if (!ownModule || !m.metadata?.name || typeof m.schema !== "object" || m.schema === null) {
|
|
1457
1464
|
continue;
|
|
1458
1465
|
}
|
|
1459
|
-
const scopeResolver =
|
|
1460
|
-
|
|
1466
|
+
const scopeResolver = declaringModuleScope(ownModule, aliases, {
|
|
1467
|
+
aliasesByModule,
|
|
1468
|
+
rootModules,
|
|
1469
|
+
});
|
|
1461
1470
|
const canonicalKind = scopeResolver.resolveKind(m.kind as string) ?? (m.kind as string);
|
|
1462
1471
|
if (defs.resolve(canonicalKind)?.capability !== "Telo.Type") continue;
|
|
1463
1472
|
const typeName = m.metadata.name as string;
|
|
@@ -1532,6 +1541,7 @@ export class StaticAnalyzer {
|
|
|
1532
1541
|
aliases,
|
|
1533
1542
|
rootModules,
|
|
1534
1543
|
getCallGraph(),
|
|
1544
|
+
{ aliasesByModule, rootModules },
|
|
1535
1545
|
),
|
|
1536
1546
|
);
|
|
1537
1547
|
// A file embed resolves at resource creation, so one written on a doc that
|
|
@@ -1877,6 +1887,83 @@ export class StaticAnalyzer {
|
|
|
1877
1887
|
continue;
|
|
1878
1888
|
}
|
|
1879
1889
|
|
|
1890
|
+
// An abstract names a CONTRACT for reference slots, not something to
|
|
1891
|
+
// instantiate — the kernel refuses it outright at `create()`. Reported
|
|
1892
|
+
// here, at the declaration, because that is the line the author has to
|
|
1893
|
+
// change and because a runtime-only refusal is exactly the shape static
|
|
1894
|
+
// analysis exists to remove. The hint mirrors the kernel's: the concrete
|
|
1895
|
+
// implementations this analysis holds, or the reason there are none.
|
|
1896
|
+
//
|
|
1897
|
+
// Injected declarations never reach this loop (skipped above), which is
|
|
1898
|
+
// what keeps a `resources:` entry — kind-only and routinely abstract by
|
|
1899
|
+
// design — out of it.
|
|
1900
|
+
if (definition.kind === "Telo.Abstract") {
|
|
1901
|
+
const canonical = resolvedKind ?? m.kind;
|
|
1902
|
+
const impls = defs
|
|
1903
|
+
.getByExtends(canonical)
|
|
1904
|
+
.filter((d) => d.kind !== "Telo.Abstract")
|
|
1905
|
+
.map((d) => `${d.metadata.module}.${d.metadata.name}`);
|
|
1906
|
+
const kindInfo =
|
|
1907
|
+
canonical !== m.kind ? `'${m.kind}' (resolved to '${canonical}')` : `'${m.kind}'`;
|
|
1908
|
+
const hint = impls.length
|
|
1909
|
+
? `instantiate a concrete implementation: ${impls.join(", ")}`
|
|
1910
|
+
: "no concrete implementations are registered — import a module that provides one";
|
|
1911
|
+
diagnostics.push({
|
|
1912
|
+
severity: DiagnosticSeverity.Error,
|
|
1913
|
+
code: "ABSTRACT_KIND_INSTANTIATED",
|
|
1914
|
+
source: SOURCE,
|
|
1915
|
+
message: `Kind ${kindInfo} is abstract and cannot be instantiated directly; ${hint}.`,
|
|
1916
|
+
data: { resource, filePath, path: "kind" },
|
|
1917
|
+
});
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
// A kind its own author marked `metadata.deprecated` still works exactly as
|
|
1922
|
+
// it did — the manifest is valid and keeps running — so this is a WARNING,
|
|
1923
|
+
// reported at the declaration because `kind:` is the line that has to
|
|
1924
|
+
// change. Emitted here rather than in a pass of its own so it reads the
|
|
1925
|
+
// kind THIS walk resolved: the resolution is alias- and gate-aware and
|
|
1926
|
+
// scope-dependent, and a second implementation of it would eventually
|
|
1927
|
+
// disagree about which definition a name means.
|
|
1928
|
+
//
|
|
1929
|
+
// Entry-module-scoped, like every other "not the consumer's to fix" check:
|
|
1930
|
+
// a library's internal use of a kind its own author deprecated is that
|
|
1931
|
+
// author's concern, and reporting it floods a consumer with lines they
|
|
1932
|
+
// cannot act on.
|
|
1933
|
+
//
|
|
1934
|
+
// No `DiagnosticFix`. `replacedBy` names a kind through the DECLARING
|
|
1935
|
+
// module's aliases; writing it into the consumer's file would produce a
|
|
1936
|
+
// prefix that resolves to nothing there — and a kind swap is not a
|
|
1937
|
+
// whole-value replacement anyway, since the successor needs its own import
|
|
1938
|
+
// and usually a different configuration.
|
|
1939
|
+
const deprecation = readDeprecation(definition.metadata);
|
|
1940
|
+
if (deprecation && (!ownModule || rootModules.has(ownModule))) {
|
|
1941
|
+
// Quoted CANONICALLY, not verbatim: `Self.Thing` is how the declaring
|
|
1942
|
+
// library names its own kind and means nothing where the warning lands.
|
|
1943
|
+
const declaringScope =
|
|
1944
|
+
scopeResolverForModule(
|
|
1945
|
+
(definition.metadata as { module?: string } | undefined)?.module,
|
|
1946
|
+
rootModules,
|
|
1947
|
+
aliasesByModule,
|
|
1948
|
+
) ?? aliases;
|
|
1949
|
+
const replacement = deprecation.replacedBy
|
|
1950
|
+
? (declaringScope.resolveKind(deprecation.replacedBy) ?? deprecation.replacedBy)
|
|
1951
|
+
: undefined;
|
|
1952
|
+
diagnostics.push({
|
|
1953
|
+
severity: DiagnosticSeverity.Warning,
|
|
1954
|
+
code: "DEPRECATED_KIND",
|
|
1955
|
+
source: SOURCE,
|
|
1956
|
+
// Warning-grade AND a deprecation: the severity says it must
|
|
1957
|
+
// eventually be dealt with, the tag says what it is, and an editor
|
|
1958
|
+
// strikes the kind through on the strength of the second.
|
|
1959
|
+
tags: [DiagnosticTag.Deprecated],
|
|
1960
|
+
message:
|
|
1961
|
+
`Kind '${m.kind}' is deprecated: ${deprecation.reason}` +
|
|
1962
|
+
(replacement ? ` Use '${replacement}' instead.` : ""),
|
|
1963
|
+
data: { resource, filePath, path: "kind" },
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1880
1967
|
// Validate resource config against the definition's AUTHOR-FACING schema —
|
|
1881
1968
|
// inheritance-resolved, with `kind` / `metadata` injected. See
|
|
1882
1969
|
// `validationSchemaFor`, which is where both derivations and the reason
|
|
@@ -2690,9 +2777,25 @@ export class StaticAnalyzer {
|
|
|
2690
2777
|
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
2691
2778
|
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
2692
2779
|
|
|
2693
|
-
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
2780
|
+
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7).
|
|
2781
|
+
// The library document sets collected for the zone stage serve here too: a
|
|
2782
|
+
// library's exported entry point is forwarded into the flat set while the
|
|
2783
|
+
// siblings it invokes are not, so without them the walk stops at the
|
|
2784
|
+
// boundary and reports an empty union for a body that plainly throws.
|
|
2785
|
+
const libraryManifests = new Map<string, ResourceManifest[]>();
|
|
2786
|
+
for (const doc of options?.moduleDocuments ?? []) {
|
|
2787
|
+
libraryManifests.set(doc.module, doc.manifests);
|
|
2788
|
+
}
|
|
2694
2789
|
diagnostics.push(
|
|
2695
|
-
...validateThrowsCoverage(
|
|
2790
|
+
...validateThrowsCoverage(
|
|
2791
|
+
allManifests,
|
|
2792
|
+
defs,
|
|
2793
|
+
aliases,
|
|
2794
|
+
this.celEnv,
|
|
2795
|
+
aliasesByModule,
|
|
2796
|
+
rootModules,
|
|
2797
|
+
libraryManifests,
|
|
2798
|
+
),
|
|
2696
2799
|
);
|
|
2697
2800
|
|
|
2698
2801
|
// Warn about declared variables / secrets / ports that no CEL references.
|
|
@@ -2710,7 +2813,8 @@ export class StaticAnalyzer {
|
|
|
2710
2813
|
defs,
|
|
2711
2814
|
aliases,
|
|
2712
2815
|
rootModules,
|
|
2713
|
-
(supplied, required) =>
|
|
2816
|
+
(supplied, required, isDeclaration) =>
|
|
2817
|
+
kindSatisfies(supplied, required, defs, isDeclaration),
|
|
2714
2818
|
),
|
|
2715
2819
|
);
|
|
2716
2820
|
|
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;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `catches:` lists answer for one dispatch site.
|
|
3
|
+
*
|
|
4
|
+
* A catch list is one rung of a SCOPE LADDER — a route's, its router's, its
|
|
5
|
+
* server's — and every rule over those lists needs the same two answers: what
|
|
6
|
+
* a scope list is checked against, and which scopes enclose a given site. This
|
|
7
|
+
* module owns that model; the rule checks in `validate-throws-coverage.ts` read
|
|
8
|
+
* one answer instead of rebuilding it beside four unrelated jobs.
|
|
9
|
+
*
|
|
10
|
+
* Browser-safe: no Node built-ins.
|
|
11
|
+
*/
|
|
12
|
+
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
13
|
+
import type { AliasResolver } from "./alias-resolver.js";
|
|
14
|
+
import type { ResourceManifest } from "@telorun/sdk";
|
|
15
|
+
import { visitManifest } from "./manifest-visitor.js";
|
|
16
|
+
import { forEachDrivenSlot } from "./schema-walk.js";
|
|
17
|
+
import { resolveRefManifest, type ResolveCtx } from "./resolve-throws-union.js";
|
|
18
|
+
|
|
19
|
+
/** What a list proves it renders: the codes its coverage-proving `when:` clauses
|
|
20
|
+
* name, and whether it ends in a catch-all.
|
|
21
|
+
*
|
|
22
|
+
* Unfiltered by any denominator, because this is also what an enclosing scope
|
|
23
|
+
* contributes DOWNWARD — a server entry naming a code the server's own closure
|
|
24
|
+
* could not enumerate still renders it for the route that throws it. Filtering
|
|
25
|
+
* the local answer against the declared union happens where that answer is
|
|
26
|
+
* used, and subtracting from the declared set makes the two equivalent there. */
|
|
27
|
+
export interface ProvenCoverage {
|
|
28
|
+
codes: Set<string>;
|
|
29
|
+
hasCatchAll: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const NO_COVERAGE: ProvenCoverage = { codes: new Set(), hasCatchAll: false };
|
|
33
|
+
|
|
34
|
+
/** A declaration inside another resource's `x-telo-scope` array, kept with the
|
|
35
|
+
* resource that encloses it and the path it is written at in that resource's
|
|
36
|
+
* document. Both are needed: the owner is the only place its kind's alias scope
|
|
37
|
+
* can be found, and the only document position lookup can reach. */
|
|
38
|
+
export interface ScopedManifest {
|
|
39
|
+
manifest: ResourceManifest;
|
|
40
|
+
owner: ResourceManifest;
|
|
41
|
+
/** Concrete owner-relative path of the declaration (`with[0]`). */
|
|
42
|
+
path: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every `with:`-scoped declaration in the set, once each.
|
|
46
|
+
*
|
|
47
|
+
* Scoped resources are absent from the flat manifest list, so every check that
|
|
48
|
+
* iterates it skips them — and standing a server up around a test is exactly
|
|
49
|
+
* that shape, so the sanctioned pattern was the one the pass could not see.
|
|
50
|
+
* Discovered through the shared visitor rather than a second scope walk. */
|
|
51
|
+
export function collectScopedManifests(
|
|
52
|
+
manifests: ResourceManifest[],
|
|
53
|
+
defs: DefinitionRegistry,
|
|
54
|
+
aliases: AliasResolver,
|
|
55
|
+
aliasesByModule: Map<string, AliasResolver>,
|
|
56
|
+
rootModules: Set<string>,
|
|
57
|
+
): ScopedManifest[] {
|
|
58
|
+
const scoped: ScopedManifest[] = [];
|
|
59
|
+
visitManifest(
|
|
60
|
+
manifests,
|
|
61
|
+
defs,
|
|
62
|
+
{
|
|
63
|
+
onScope: (event) => {
|
|
64
|
+
for (const { manifest, path } of event.declarations) {
|
|
65
|
+
if (!manifest?.kind || !manifest.metadata?.name) continue;
|
|
66
|
+
scoped.push({ manifest, owner: event.source, path });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{ aliases, aliasesByModule, rootModules },
|
|
71
|
+
);
|
|
72
|
+
return scoped;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Which resources each resource's scope list encloses, along the slots that
|
|
76
|
+
* declare `throwsThrough` — one fact stated once, since the edge a throws
|
|
77
|
+
* closure crosses is the edge a catch scope encloses through. */
|
|
78
|
+
export function buildEnclosers(
|
|
79
|
+
manifests: ResourceManifest[],
|
|
80
|
+
definitionOf: (m: ResourceManifest) => { schema?: Record<string, any> } | undefined,
|
|
81
|
+
moduleOf: (m: ResourceManifest) => string | undefined,
|
|
82
|
+
ctx: ResolveCtx,
|
|
83
|
+
): Map<ResourceManifest, ResourceManifest[]> {
|
|
84
|
+
const enclosers = new Map<ResourceManifest, ResourceManifest[]>();
|
|
85
|
+
for (const manifest of manifests) {
|
|
86
|
+
const definition = definitionOf(manifest);
|
|
87
|
+
if (!definition?.schema) continue;
|
|
88
|
+
forEachDrivenSlot(definition.schema, manifest, (driven) => {
|
|
89
|
+
if (driven.kind !== "ref" || !driven.slot.throwsThrough) return;
|
|
90
|
+
const target = resolveRefManifest(driven.data, ctx, moduleOf(manifest));
|
|
91
|
+
if (!target || target === manifest) return;
|
|
92
|
+
const list = enclosers.get(target);
|
|
93
|
+
if (list) list.push(manifest);
|
|
94
|
+
else enclosers.set(target, [manifest]);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return enclosers;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* What every scope enclosing a resource is guaranteed to render for it.
|
|
102
|
+
*
|
|
103
|
+
* **Across enclosers this INTERSECTS, and that is the whole correctness of the
|
|
104
|
+
* reduction.** Coverage claims that a throw cannot escape unrendered, so it holds
|
|
105
|
+
* only when EVERY path to the site renders it — a router mounted on a public
|
|
106
|
+
* server with a catch-all and an internal one without is covered on one path and
|
|
107
|
+
* bare on the other, and unioning the two reported it as fully covered while the
|
|
108
|
+
* internal server answered with the built-in envelope. A resource with no
|
|
109
|
+
* encloser contributes nothing rather than everything: the empty set is the
|
|
110
|
+
* identity for the site's own coverage, and treating "no paths" as "all paths
|
|
111
|
+
* agree" would assert coverage no list provides.
|
|
112
|
+
*
|
|
113
|
+
* A resource's OWN scope list is unioned in, because it applies on every path.
|
|
114
|
+
*
|
|
115
|
+
* Memoized, and a cycle among `throwsThrough` edges resolves to what has been
|
|
116
|
+
* accumulated so far rather than raising: this answers what a scope renders, and
|
|
117
|
+
* no cycle makes that answer larger.
|
|
118
|
+
*/
|
|
119
|
+
export function enclosingCoverage(
|
|
120
|
+
manifest: ResourceManifest,
|
|
121
|
+
ownScope: Map<ResourceManifest, ProvenCoverage>,
|
|
122
|
+
enclosers: Map<ResourceManifest, ResourceManifest[]>,
|
|
123
|
+
memo: Map<ResourceManifest, ProvenCoverage> = new Map(),
|
|
124
|
+
walking: Set<ResourceManifest> = new Set(),
|
|
125
|
+
): ProvenCoverage {
|
|
126
|
+
const cached = memo.get(manifest);
|
|
127
|
+
if (cached) return cached;
|
|
128
|
+
if (walking.has(manifest)) return NO_COVERAGE;
|
|
129
|
+
walking.add(manifest);
|
|
130
|
+
try {
|
|
131
|
+
const own = ownScope.get(manifest);
|
|
132
|
+
const result: ProvenCoverage = {
|
|
133
|
+
codes: new Set(own?.codes ?? []),
|
|
134
|
+
hasCatchAll: own?.hasCatchAll ?? false,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const outer = (enclosers.get(manifest) ?? []).map((e) =>
|
|
138
|
+
enclosingCoverage(e, ownScope, enclosers, memo, walking),
|
|
139
|
+
);
|
|
140
|
+
if (outer.length > 0) {
|
|
141
|
+
const [first, ...rest] = outer;
|
|
142
|
+
const shared = new Set(first.codes);
|
|
143
|
+
let everyCatchAll = first.hasCatchAll;
|
|
144
|
+
for (const other of rest) {
|
|
145
|
+
for (const code of [...shared]) if (!other.codes.has(code)) shared.delete(code);
|
|
146
|
+
everyCatchAll &&= other.hasCatchAll;
|
|
147
|
+
}
|
|
148
|
+
for (const code of shared) result.codes.add(code);
|
|
149
|
+
if (everyCatchAll) result.hasCatchAll = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
memo.set(manifest, result);
|
|
153
|
+
return result;
|
|
154
|
+
} finally {
|
|
155
|
+
walking.delete(manifest);
|
|
156
|
+
}
|
|
157
|
+
}
|
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) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `metadata.deprecated` — the annotation's single reader.
|
|
3
|
+
*
|
|
4
|
+
* Structural validity belongs to `validate-module-metadata.ts` (the strict half,
|
|
5
|
+
* the `ref-slot.ts` split). This side is deliberately lenient: it reads a
|
|
6
|
+
* well-formed block and treats everything else as absent, so a malformed
|
|
7
|
+
* declaration in a published dependency never becomes a warning at a consumer's
|
|
8
|
+
* use site. The consumer can fix neither one, and reporting the second blames
|
|
9
|
+
* the wrong author.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A deprecation as declared: why, and optionally what to use instead. */
|
|
13
|
+
export interface Deprecation {
|
|
14
|
+
/** What a consumer reads to know what to do instead. Always non-empty. */
|
|
15
|
+
reason: string;
|
|
16
|
+
/** Alias-qualified kind (kind docs) or module ref (module docs), **as written
|
|
17
|
+
* in the declaring file's own scope** — `Self.Thing` means nothing to a
|
|
18
|
+
* consumer, so a use site resolves it before quoting it. */
|
|
19
|
+
replacedBy?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readDeprecation(metadata: unknown): Deprecation | undefined {
|
|
23
|
+
if (metadata === null || typeof metadata !== "object" || Array.isArray(metadata)) return undefined;
|
|
24
|
+
const block = (metadata as Record<string, unknown>).deprecated;
|
|
25
|
+
if (block === null || typeof block !== "object" || Array.isArray(block)) return undefined;
|
|
26
|
+
|
|
27
|
+
const { reason, replacedBy } = block as Record<string, unknown>;
|
|
28
|
+
if (typeof reason !== "string" || reason.trim() === "") return undefined;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
reason: reason.trim(),
|
|
32
|
+
...(typeof replacedBy === "string" && replacedBy.trim() !== ""
|
|
33
|
+
? { replacedBy: replacedBy.trim() }
|
|
34
|
+
: {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -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,
|
|
@@ -185,6 +185,8 @@ export {
|
|
|
185
185
|
export type { RefSlot, RefUse, RefUseCases } from "./ref-slot.js";
|
|
186
186
|
export { isStepSlot, readStepSlot, STEP_FRAGMENT } from "./step-slot.js";
|
|
187
187
|
export type { StepSlot } from "./step-slot.js";
|
|
188
|
+
export { readDeprecation } from "./deprecation.js";
|
|
189
|
+
export type { Deprecation } from "./deprecation.js";
|
|
188
190
|
export {
|
|
189
191
|
ANNOTATION_KEYWORDS,
|
|
190
192
|
registerTeloKeywords,
|
|
@@ -246,7 +248,7 @@ export type {
|
|
|
246
248
|
ZoneExportRequirements,
|
|
247
249
|
ZoneRequirementSpec,
|
|
248
250
|
} from "./resolve-zone-requirements.js";
|
|
249
|
-
export type {
|
|
251
|
+
export type { ModuleDocuments } from "./module-documents.js";
|
|
250
252
|
export { validateZoneSlotDeclarations } from "./validate-zone-slots.js";
|
|
251
253
|
export {
|
|
252
254
|
RESOURCE_RULES_ANNOTATION,
|
|
@@ -489,7 +491,12 @@ export type { CelScopeQueryContext, ContextDeclarationSite } from "./cel-scope-q
|
|
|
489
491
|
export { navigateConcretePath } from "./manifest-path.js";
|
|
490
492
|
export { ManifestAnalysis } from "./manifest-analysis.js";
|
|
491
493
|
export type { ManifestRef } from "./manifest-analysis.js";
|
|
492
|
-
export {
|
|
494
|
+
export {
|
|
495
|
+
DEFAULT_MANIFEST_FILENAME,
|
|
496
|
+
DiagnosticSeverity,
|
|
497
|
+
DiagnosticTag,
|
|
498
|
+
diagnosticFix,
|
|
499
|
+
} from "./types.js";
|
|
493
500
|
export type {
|
|
494
501
|
AnalysisDiagnostic,
|
|
495
502
|
AnalysisOptions,
|