@telorun/analyzer 0.72.0 → 0.74.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/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +76 -20
- package/dist/artifact-axes.d.ts +14 -0
- package/dist/artifact-axes.d.ts.map +1 -0
- package/dist/artifact-axes.js +12 -0
- package/dist/artifact-layer-index.d.ts +2 -2
- package/dist/artifact-layer-index.d.ts.map +1 -1
- package/dist/artifact-layer-index.js +9 -4
- package/dist/artifact-selector.d.ts +47 -21
- package/dist/artifact-selector.d.ts.map +1 -1
- package/dist/artifact-selector.js +85 -16
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +144 -113
- package/dist/call-graph.d.ts +12 -1
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +22 -0
- package/dist/catch-scope.js +1 -1
- package/dist/cel-scope-query.d.ts.map +1 -1
- package/dist/cel-scope-query.js +32 -11
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +1 -0
- package/dist/eval-paths.d.ts +36 -5
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +47 -6
- package/dist/extends-resolution.d.ts +12 -0
- package/dist/extends-resolution.d.ts.map +1 -1
- package/dist/extends-resolution.js +23 -0
- package/dist/forwarded-declaration.d.ts +26 -0
- package/dist/forwarded-declaration.d.ts.map +1 -0
- package/dist/forwarded-declaration.js +31 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +21 -5
- package/dist/module-alias-scope.d.ts +11 -0
- package/dist/module-alias-scope.d.ts.map +1 -1
- package/dist/module-alias-scope.js +10 -0
- package/dist/module-file-claims.d.ts +17 -1
- package/dist/module-file-claims.d.ts.map +1 -1
- package/dist/module-file-claims.js +38 -3
- package/dist/module-named-files.d.ts +72 -0
- package/dist/module-named-files.d.ts.map +1 -0
- package/dist/module-named-files.js +115 -0
- package/dist/native-entries.d.ts +83 -0
- package/dist/native-entries.d.ts.map +1 -0
- package/dist/native-entries.js +169 -0
- package/dist/nearest-name.d.ts +22 -0
- package/dist/nearest-name.d.ts.map +1 -0
- package/dist/nearest-name.js +43 -0
- package/dist/normalize-inline-resources.d.ts +41 -13
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +236 -54
- package/dist/reference-field-map.d.ts +59 -1
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +200 -34
- package/dist/release/destinations.d.ts +59 -0
- package/dist/release/destinations.d.ts.map +1 -0
- package/dist/release/destinations.js +75 -0
- package/dist/release/index.d.ts +6 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -1
- package/dist/release/ledger.d.ts +20 -11
- package/dist/release/ledger.d.ts.map +1 -1
- package/dist/release/ledger.js +43 -14
- package/dist/release/payload-digest.d.ts +2 -1
- package/dist/release/payload-digest.d.ts.map +1 -1
- package/dist/release/payload-digest.js +2 -1
- package/dist/release/release-plan.d.ts +3 -2
- package/dist/release/release-plan.d.ts.map +1 -1
- package/dist/release/release-plan.js +12 -19
- package/dist/release/workspace-config.d.ts +114 -34
- package/dist/release/workspace-config.d.ts.map +1 -1
- package/dist/release/workspace-config.js +322 -38
- package/dist/release/workspace-schema.d.ts +41 -0
- package/dist/release/workspace-schema.d.ts.map +1 -0
- package/dist/release/workspace-schema.js +77 -0
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +8 -5
- package/dist/resolve-throws-union.d.ts +25 -4
- package/dist/resolve-throws-union.d.ts.map +1 -1
- package/dist/resolve-throws-union.js +62 -32
- package/dist/resolve-zone-requirements.d.ts.map +1 -1
- package/dist/resolve-zone-requirements.js +2 -1
- package/dist/rewrite-synthetic-origins.d.ts +6 -1
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
- package/dist/rewrite-synthetic-origins.js +107 -13
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +18 -5
- package/dist/schema-error-report.d.ts +7 -0
- package/dist/schema-error-report.d.ts.map +1 -1
- package/dist/schema-error-report.js +9 -4
- package/dist/schema-walk.d.ts +47 -13
- package/dist/schema-walk.d.ts.map +1 -1
- package/dist/schema-walk.js +150 -30
- package/dist/scope-declarations.d.ts +59 -0
- package/dist/scope-declarations.d.ts.map +1 -0
- package/dist/scope-declarations.js +45 -0
- package/dist/source-entries.d.ts +119 -0
- package/dist/source-entries.d.ts.map +1 -0
- package/dist/source-entries.js +349 -0
- package/dist/step-bodies.d.ts +24 -0
- package/dist/step-bodies.d.ts.map +1 -0
- package/dist/step-bodies.js +28 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +10 -0
- package/dist/template-body.d.ts.map +1 -1
- package/dist/template-body.js +25 -0
- package/dist/validate-cel-context.d.ts +23 -8
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +51 -18
- package/dist/validate-exports.d.ts +28 -0
- package/dist/validate-exports.d.ts.map +1 -0
- package/dist/validate-exports.js +146 -0
- package/dist/validate-extends.d.ts.map +1 -1
- package/dist/validate-extends.js +37 -1
- package/dist/validate-module-artifact.d.ts +4 -2
- package/dist/validate-module-artifact.d.ts.map +1 -1
- package/dist/validate-module-artifact.js +43 -4
- package/dist/validate-native-entries.d.ts +15 -0
- package/dist/validate-native-entries.d.ts.map +1 -0
- package/dist/validate-native-entries.js +103 -0
- package/dist/validate-observed-state.d.ts +7 -1
- package/dist/validate-observed-state.d.ts.map +1 -1
- package/dist/validate-observed-state.js +24 -6
- package/dist/validate-provider-coherence.d.ts +4 -8
- package/dist/validate-provider-coherence.d.ts.map +1 -1
- package/dist/validate-provider-coherence.js +12 -123
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +2 -2
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +152 -65
- package/dist/validate-scope-reach.d.ts +31 -0
- package/dist/validate-scope-reach.d.ts.map +1 -0
- package/dist/validate-scope-reach.js +180 -0
- package/dist/validate-source-entries.d.ts +16 -0
- package/dist/validate-source-entries.d.ts.map +1 -0
- package/dist/validate-source-entries.js +90 -0
- package/dist/validate-template-body.d.ts +36 -0
- package/dist/validate-template-body.d.ts.map +1 -0
- package/dist/validate-template-body.js +246 -0
- package/dist/validate-throws-coverage.d.ts.map +1 -1
- package/dist/validate-throws-coverage.js +21 -39
- package/dist/value-type-keyword.d.ts +22 -5
- package/dist/value-type-keyword.d.ts.map +1 -1
- package/dist/value-type-keyword.js +16 -7
- package/package.json +4 -3
- package/src/analyzer.ts +97 -20
- package/src/artifact-axes.ts +24 -0
- package/src/artifact-layer-index.ts +9 -6
- package/src/artifact-selector.ts +110 -35
- package/src/builtins.ts +160 -113
- package/src/call-graph.ts +30 -0
- package/src/catch-scope.ts +1 -1
- package/src/cel-scope-query.ts +28 -9
- package/src/cel-scope.ts +1 -0
- package/src/eval-paths.ts +53 -6
- package/src/extends-resolution.ts +24 -0
- package/src/forwarded-declaration.ts +32 -0
- package/src/index.ts +37 -1
- package/src/kernel-globals.ts +28 -5
- package/src/migrations/CLAUDE.md +35 -0
- package/src/module-alias-scope.ts +17 -0
- package/src/module-file-claims.ts +63 -4
- package/src/module-named-files.ts +162 -0
- package/src/native-entries.ts +227 -0
- package/src/nearest-name.ts +47 -0
- package/src/normalize-inline-resources.ts +326 -70
- package/src/reference-field-map.ts +257 -52
- package/src/release/CLAUDE.md +32 -0
- package/src/release/destinations.ts +105 -0
- package/src/release/index.ts +33 -2
- package/src/release/ledger.ts +51 -21
- package/src/release/payload-digest.ts +2 -1
- package/src/release/release-plan.ts +16 -26
- package/src/release/workspace-config.ts +483 -47
- package/src/release/workspace-schema.ts +99 -0
- package/src/resolve-ref-sentinels.ts +7 -6
- package/src/resolve-throws-union.ts +87 -32
- package/src/resolve-zone-requirements.ts +2 -1
- package/src/rewrite-synthetic-origins.ts +107 -16
- package/src/schema-compat.ts +18 -4
- package/src/schema-error-report.ts +16 -4
- package/src/schema-walk.ts +162 -34
- package/src/scope-declarations.ts +98 -0
- package/src/source-entries.ts +502 -0
- package/src/step-bodies.ts +55 -0
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +25 -0
- package/src/validate-cel-context.ts +60 -18
- package/src/validate-exports.ts +185 -0
- package/src/validate-extends.ts +43 -0
- package/src/validate-module-artifact.ts +46 -3
- package/src/validate-native-entries.ts +147 -0
- package/src/validate-observed-state.ts +36 -7
- package/src/validate-provider-coherence.ts +17 -127
- package/src/validate-reference-forms.ts +2 -3
- package/src/validate-references.ts +151 -66
- package/src/validate-scope-reach.ts +213 -0
- package/src/validate-source-entries.ts +120 -0
- package/src/validate-template-body.ts +274 -0
- package/src/validate-throws-coverage.ts +24 -31
- package/src/value-type-keyword.ts +35 -9
- package/dist/validate-template-dispatch.d.ts +0 -27
- package/dist/validate-template-dispatch.d.ts.map +0 -1
- package/dist/validate-template-dispatch.js +0 -95
- package/src/validate-template-dispatch.ts +0 -99
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { OBSERVED_STATE_KEY } from "@telorun/sdk";
|
|
3
3
|
import { effectiveStatusSchema } from "./extends-resolution.js";
|
|
4
|
+
import { isForwardedDeclaration } from "./forwarded-declaration.js";
|
|
4
5
|
import { parseExportEntry } from "./flatten-for-analyzer.js";
|
|
5
6
|
import {
|
|
6
7
|
moduleScopedDefResolver,
|
|
@@ -129,11 +130,20 @@ export function collectRunReachableNames(graph: CallGraph): Set<string> {
|
|
|
129
130
|
|
|
130
131
|
/** What a resource name resolves to for CEL purposes. `status` is present only
|
|
131
132
|
* when the kind declares one; `scoped` marks a resource declared inside an
|
|
132
|
-
* `x-telo-scope` slot, which resolves only within that scope's regions.
|
|
133
|
+
* `x-telo-scope` slot, which resolves only within that scope's regions.
|
|
134
|
+
* `forwardedFrom` marks a dependency's declaration, keyed by
|
|
135
|
+
* `forwardedResourceKey`: it is in no consumer's `resources`, but a read inside
|
|
136
|
+
* that dependency's own manifests still resolves to it. */
|
|
133
137
|
export interface AnalyzedResource {
|
|
134
138
|
kind: string;
|
|
135
139
|
status?: Record<string, any>;
|
|
136
140
|
scoped?: boolean;
|
|
141
|
+
forwardedFrom?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The index key of a name a dependency declares, as read from inside it. */
|
|
145
|
+
export function forwardedResourceKey(module: string, name: string): string {
|
|
146
|
+
return `${module}\0${name}`;
|
|
137
147
|
}
|
|
138
148
|
|
|
139
149
|
/**
|
|
@@ -157,16 +167,35 @@ export function buildObservedStateIndex(
|
|
|
157
167
|
/** `module` is the resource's DECLARING module: an exported instance is
|
|
158
168
|
* written with that library's aliases (`kind: Self.Listener`), which the
|
|
159
169
|
* consumer's table cannot resolve. */
|
|
160
|
-
const record = (
|
|
170
|
+
const record = (
|
|
171
|
+
kind: string,
|
|
172
|
+
key: string,
|
|
173
|
+
scoped: boolean,
|
|
174
|
+
module?: string,
|
|
175
|
+
forwardedFrom?: string,
|
|
176
|
+
): void => {
|
|
161
177
|
const status = effectiveStatusSchema(resolve.in(kind, module), resolve);
|
|
162
|
-
out.set(key, {
|
|
178
|
+
out.set(key, {
|
|
179
|
+
kind,
|
|
180
|
+
...(status ? { status } : {}),
|
|
181
|
+
...(scoped ? { scoped } : {}),
|
|
182
|
+
...(forwardedFrom !== undefined ? { forwardedFrom } : {}),
|
|
183
|
+
});
|
|
163
184
|
};
|
|
164
185
|
|
|
165
186
|
for (const manifest of manifests) {
|
|
166
187
|
const kind = manifest.kind as string | undefined;
|
|
167
188
|
const name = manifest.metadata?.name as string | undefined;
|
|
168
189
|
if (!kind || SYSTEM_KINDS.has(kind)) continue;
|
|
169
|
-
|
|
190
|
+
const module = manifest.metadata?.module as string | undefined;
|
|
191
|
+
// A dependency's declarations publish into ITS module's `resources`, not this
|
|
192
|
+
// one's: they are keyed by that module, and an export is also indexed under
|
|
193
|
+
// `<Alias>.<name>` below.
|
|
194
|
+
const forwardedFrom = isForwardedDeclaration(manifest) ? module : undefined;
|
|
195
|
+
if (forwardedFrom === undefined && isForwardedDeclaration(manifest)) continue;
|
|
196
|
+
const keyOf = (n: string) =>
|
|
197
|
+
forwardedFrom === undefined ? n : forwardedResourceKey(forwardedFrom, n);
|
|
198
|
+
if (name) record(kind, keyOf(name), false, module, forwardedFrom);
|
|
170
199
|
|
|
171
200
|
const schema = resolve(kind)?.schema as Record<string, any> | undefined;
|
|
172
201
|
if (!schema) continue;
|
|
@@ -177,7 +206,7 @@ export function buildObservedStateIndex(
|
|
|
177
206
|
const scopedKind = (scopedEntry as ResourceManifest)?.kind;
|
|
178
207
|
const scopedName = (scopedEntry as ResourceManifest)?.metadata?.name;
|
|
179
208
|
if (typeof scopedKind === "string" && typeof scopedName === "string") {
|
|
180
|
-
record(scopedKind, scopedName, true);
|
|
209
|
+
record(scopedKind, keyOf(scopedName), true, forwardedFrom, forwardedFrom);
|
|
181
210
|
}
|
|
182
211
|
}
|
|
183
212
|
}
|
|
@@ -254,8 +283,8 @@ export function buildObservedStateResourcesSchema(
|
|
|
254
283
|
open: boolean,
|
|
255
284
|
): Record<string, any> {
|
|
256
285
|
const properties: Record<string, any> = {};
|
|
257
|
-
for (const [key, { status }] of index) {
|
|
258
|
-
if (!status) continue;
|
|
286
|
+
for (const [key, { status, forwardedFrom }] of index) {
|
|
287
|
+
if (!status || forwardedFrom !== undefined) continue;
|
|
259
288
|
applyObservedStateNode(properties, key, status);
|
|
260
289
|
}
|
|
261
290
|
return open
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
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
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
controllerBearingAncestor,
|
|
6
|
+
inheritedCapability,
|
|
7
|
+
type DefResolver,
|
|
8
|
+
} from "./extends-resolution.js";
|
|
5
9
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
6
10
|
|
|
7
11
|
const SOURCE = "telo-analyzer";
|
|
@@ -16,20 +20,16 @@ const SOURCE = "telo-analyzer";
|
|
|
16
20
|
* `capability` is not `Telo.Provider`.
|
|
17
21
|
* - PROVIDE_DISPATCHER_CONFLICT: `provide:` co-exists with `invoke:` or `run:`
|
|
18
22
|
* on the same definition.
|
|
19
|
-
* - PROVIDE_TARGET_UNKNOWN: `provide.name` does not resolve to an entry in
|
|
20
|
-
* `resources:`.
|
|
21
|
-
* - PROVIDE_TARGET_NOT_INVOCABLE: `provide.name` resolves to a resource whose
|
|
22
|
-
* kind is registered but not a `Telo.Invocable`.
|
|
23
23
|
* - PROVIDER_MISSING_IMPLEMENTATION: definition with `capability: Telo.Provider`
|
|
24
24
|
* declares neither `controllers:` (TS-backed) nor `provide:` (template-backed).
|
|
25
25
|
* - MOUNT_ON_NON_MOUNT: `mount:` declared on a definition whose `capability` is
|
|
26
26
|
* not `Telo.Mount`.
|
|
27
27
|
* - MOUNT_DISPATCHER_CONFLICT: `mount:` co-exists with another dispatch
|
|
28
28
|
* entry-point (`invoke:` / `run:` / `provide:`).
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
*
|
|
30
|
+
* What a dispatch slot NAMES — that it is a `!ref`, that the entry exists, that
|
|
31
|
+
* its capability carries the method — is `validate-template-body`'s, for every
|
|
32
|
+
* slot alike.
|
|
33
33
|
*/
|
|
34
34
|
export function validateProviderCoherence(
|
|
35
35
|
manifests: ResourceManifest[],
|
|
@@ -57,13 +57,19 @@ export function validateProviderCoherence(
|
|
|
57
57
|
const label = `${m.kind}/${name}`;
|
|
58
58
|
|
|
59
59
|
const md = m as Record<string, unknown>;
|
|
60
|
-
const
|
|
60
|
+
const resolveDef: DefResolver = (k) =>
|
|
61
|
+
registry.resolve(aliases.resolveKind(k) ?? k) ?? registry.resolve(k);
|
|
62
|
+
// The INHERITED capability, not the declared one: an `extends` child writes
|
|
63
|
+
// none of its own, so reading `md.capability` reported a child that inherits
|
|
64
|
+
// `Telo.Provider` and declares `provide:` as PROVIDE_ON_NON_PROVIDER
|
|
65
|
+
// "(found '<unset>')" — the same defect `celRuleApplies` and
|
|
66
|
+
// `validate-template-body` were fixed for, in the file next door.
|
|
67
|
+
const capability = inheritedCapability(m as ResourceDefinition, resolveDef);
|
|
61
68
|
const provide = md.provide;
|
|
62
69
|
const invoke = md.invoke;
|
|
63
70
|
const run = md.run;
|
|
64
71
|
const mount = md.mount;
|
|
65
72
|
const controllers = md.controllers;
|
|
66
|
-
const resources = md.resources;
|
|
67
73
|
|
|
68
74
|
const hasProvide = provide !== undefined && provide !== null;
|
|
69
75
|
const hasInvoke = invoke !== undefined && invoke !== null;
|
|
@@ -96,70 +102,6 @@ export function validateProviderCoherence(
|
|
|
96
102
|
});
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
if (hasProvide && typeof provide === "object" && !Array.isArray(provide)) {
|
|
100
|
-
const provideObj = provide as { kind?: unknown; name?: unknown };
|
|
101
|
-
const providedName = typeof provideObj.name === "string" ? provideObj.name : undefined;
|
|
102
|
-
const providedKind = typeof provideObj.kind === "string" ? provideObj.kind : undefined;
|
|
103
|
-
if (providedName && Array.isArray(resources)) {
|
|
104
|
-
const match = resources.find((r) => {
|
|
105
|
-
const meta = (r as { metadata?: { name?: unknown } })?.metadata;
|
|
106
|
-
return typeof meta?.name === "string" && meta.name === providedName;
|
|
107
|
-
}) as { kind?: unknown } | undefined;
|
|
108
|
-
if (!match) {
|
|
109
|
-
diagnostics.push({
|
|
110
|
-
severity: DiagnosticSeverity.Error,
|
|
111
|
-
code: "PROVIDE_TARGET_UNKNOWN",
|
|
112
|
-
source: SOURCE,
|
|
113
|
-
message:
|
|
114
|
-
`${label}: 'provide.name: ${providedName}' does not match any entry's ` +
|
|
115
|
-
`metadata.name in 'resources:'.`,
|
|
116
|
-
data: { resource, filePath, path: "provide.name" },
|
|
117
|
-
});
|
|
118
|
-
} else if (typeof match.kind === "string") {
|
|
119
|
-
// `provide.kind` is the type contract the analyzer uses to type
|
|
120
|
-
// `result` CEL against the target's `outputType`. The runtime
|
|
121
|
-
// dispatches on `provide.name` and ignores `provide.kind`, so a
|
|
122
|
-
// mismatch silently degrades `result` typing to an open schema
|
|
123
|
-
// (and at runtime quietly invokes the actually-matched resource).
|
|
124
|
-
// Flag the divergence so result-typing never lies.
|
|
125
|
-
if (providedKind) {
|
|
126
|
-
const providedCanonical = aliases.resolveKind(providedKind) ?? providedKind;
|
|
127
|
-
const matchCanonical = aliases.resolveKind(match.kind) ?? match.kind;
|
|
128
|
-
if (providedCanonical !== matchCanonical) {
|
|
129
|
-
diagnostics.push({
|
|
130
|
-
severity: DiagnosticSeverity.Error,
|
|
131
|
-
code: "PROVIDE_KIND_MISMATCH",
|
|
132
|
-
source: SOURCE,
|
|
133
|
-
message:
|
|
134
|
-
`${label}: 'provide.kind: ${providedKind}' disagrees with the matched ` +
|
|
135
|
-
`'resources:' entry's kind '${match.kind}' (matched by metadata.name ` +
|
|
136
|
-
`'${providedName}'). The runtime dispatches by name, so 'provide.kind' ` +
|
|
137
|
-
`is decorative — but the analyzer types 'result:' against it, and a ` +
|
|
138
|
-
`mismatch silently turns off that typing.`,
|
|
139
|
-
data: { resource, filePath, path: "provide.kind" },
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
|
|
144
|
-
const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
|
|
145
|
-
if (targetDef && targetDef.kind === "Telo.Definition") {
|
|
146
|
-
const targetCap = (targetDef as { capability?: unknown }).capability;
|
|
147
|
-
if (typeof targetCap === "string" && targetCap !== "Telo.Invocable") {
|
|
148
|
-
diagnostics.push({
|
|
149
|
-
severity: DiagnosticSeverity.Error,
|
|
150
|
-
code: "PROVIDE_TARGET_NOT_INVOCABLE",
|
|
151
|
-
source: SOURCE,
|
|
152
|
-
message:
|
|
153
|
-
`${label}: 'provide.name: ${providedName}' resolves to a ${match.kind} ` +
|
|
154
|
-
`(capability '${targetCap}'); 'provide:' requires a Telo.Invocable target.`,
|
|
155
|
-
data: { resource, filePath, path: "provide.name" },
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
105
|
if (hasMount && capability !== "Telo.Mount") {
|
|
164
106
|
diagnostics.push({
|
|
165
107
|
severity: DiagnosticSeverity.Error,
|
|
@@ -185,61 +127,9 @@ export function validateProviderCoherence(
|
|
|
185
127
|
});
|
|
186
128
|
}
|
|
187
129
|
|
|
188
|
-
if (hasMount) {
|
|
189
|
-
// Resolve the target's name from either form: the bare string (the
|
|
190
|
-
// primary, documented form — `mount: api`) or the object's `name`. A CEL
|
|
191
|
-
// target (`${{ … }}`) can only be checked at runtime, so skip those.
|
|
192
|
-
let mountedName: string | undefined;
|
|
193
|
-
if (typeof mount === "string") {
|
|
194
|
-
if (!mount.includes("${{")) mountedName = mount;
|
|
195
|
-
} else if (typeof mount === "object" && !Array.isArray(mount)) {
|
|
196
|
-
const mountObj = mount as { name?: unknown };
|
|
197
|
-
if (typeof mountObj.name === "string" && !mountObj.name.includes("${{")) {
|
|
198
|
-
mountedName = mountObj.name;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const mountPath = typeof mount === "string" ? "mount" : "mount.name";
|
|
202
|
-
if (mountedName && Array.isArray(resources)) {
|
|
203
|
-
const match = resources.find((r) => {
|
|
204
|
-
const meta = (r as { metadata?: { name?: unknown } })?.metadata;
|
|
205
|
-
return typeof meta?.name === "string" && meta.name === mountedName;
|
|
206
|
-
}) as { kind?: unknown } | undefined;
|
|
207
|
-
if (!match) {
|
|
208
|
-
diagnostics.push({
|
|
209
|
-
severity: DiagnosticSeverity.Error,
|
|
210
|
-
code: "MOUNT_TARGET_UNKNOWN",
|
|
211
|
-
source: SOURCE,
|
|
212
|
-
message:
|
|
213
|
-
`${label}: '${mountPath}: ${mountedName}' does not match any entry's ` +
|
|
214
|
-
`metadata.name in 'resources:'.`,
|
|
215
|
-
data: { resource, filePath, path: mountPath },
|
|
216
|
-
});
|
|
217
|
-
} else if (typeof match.kind === "string") {
|
|
218
|
-
const resolvedKind = aliases.resolveKind(match.kind) ?? match.kind;
|
|
219
|
-
const targetDef = registry.resolve(resolvedKind) ?? registry.resolve(match.kind);
|
|
220
|
-
if (targetDef && targetDef.kind === "Telo.Definition") {
|
|
221
|
-
const targetCap = (targetDef as { capability?: unknown }).capability;
|
|
222
|
-
if (typeof targetCap === "string" && targetCap !== "Telo.Mount") {
|
|
223
|
-
diagnostics.push({
|
|
224
|
-
severity: DiagnosticSeverity.Error,
|
|
225
|
-
code: "MOUNT_TARGET_NOT_MOUNTABLE",
|
|
226
|
-
source: SOURCE,
|
|
227
|
-
message:
|
|
228
|
-
`${label}: '${mountPath}: ${mountedName}' resolves to a ${match.kind} ` +
|
|
229
|
-
`(capability '${targetCap}'); 'mount:' requires a Telo.Mount target.`,
|
|
230
|
-
data: { resource, filePath, path: mountPath },
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
130
|
// A definition that inherits a controller by delegation (concrete `extends`,
|
|
239
131
|
// no own controller/template) satisfies the implementation requirement
|
|
240
132
|
// through its parent — `base:` supplies the parent's config.
|
|
241
|
-
const resolveDef: DefResolver = (k) =>
|
|
242
|
-
registry.resolve(aliases.resolveKind(k) ?? k) ?? registry.resolve(k);
|
|
243
133
|
const inheritsController =
|
|
244
134
|
typeof md.extends === "string" &&
|
|
245
135
|
controllerBearingAncestor(m as ResourceDefinition, resolveDef) !== undefined;
|
|
@@ -2,6 +2,7 @@ import type { ResourceManifest } from "@telorun/sdk";
|
|
|
2
2
|
import { isTaggedSentinel } from "@telorun/templating";
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
|
+
import { isForwardedDeclaration } from "./forwarded-declaration.js";
|
|
5
6
|
import { visitManifest } from "./manifest-visitor.js";
|
|
6
7
|
import { satisfiesValueBranch } from "./reference-field-map.js";
|
|
7
8
|
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
@@ -50,9 +51,7 @@ export function validateReferenceForms(
|
|
|
50
51
|
if (!aliases) return [];
|
|
51
52
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
52
53
|
|
|
53
|
-
const
|
|
54
|
-
(r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
|
|
55
|
-
const localResources = resources.filter((r) => !isForeign(r));
|
|
54
|
+
const localResources = resources.filter((r) => !isForwardedDeclaration(r));
|
|
56
55
|
|
|
57
56
|
visitManifest(
|
|
58
57
|
localResources,
|
|
@@ -15,10 +15,143 @@ import { DiagnosticSeverity, type AnalysisDiagnostic, type AnalysisContext } fro
|
|
|
15
15
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
16
16
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
17
17
|
import { moduleAliasScope } from "./module-alias-scope.js";
|
|
18
|
+
import { isModuleKind } from "./module-kinds.js";
|
|
18
19
|
import { isInjectedDeclaration } from "./resource-input.js";
|
|
20
|
+
import { outsideScopesOf } from "./scope-declarations.js";
|
|
21
|
+
import { isForwardedDeclaration, isForwardedExport } from "./forwarded-declaration.js";
|
|
19
22
|
|
|
20
23
|
const SOURCE = "telo-analyzer";
|
|
21
24
|
|
|
25
|
+
/** What a name IS on one side of a collision. The three shapes share the
|
|
26
|
+
* kernel's one namespace but read as different things to an author — an alias
|
|
27
|
+
* is not a resource, and a module's own name is not a declaration inside it —
|
|
28
|
+
* so calling all three "resource name" describes two of them wrongly. */
|
|
29
|
+
function describeNamed(m: ResourceManifest, name: string): string {
|
|
30
|
+
if (m.kind === "Telo.Import") return `import alias '${name}'`;
|
|
31
|
+
if (isModuleKind(m.kind)) return `this module's own name (${m.kind} '${name}')`;
|
|
32
|
+
return `resource ${m.kind}/${name}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* WHAT SHARES A NAME AT THE KERNEL.
|
|
37
|
+
*
|
|
38
|
+
* `registerManifest` keys on `metadata.name` alone, per module context, for
|
|
39
|
+
* EVERY kind — so an import alias, a kind definition and an ordinary resource
|
|
40
|
+
* are one namespace. This check used to model a narrower one: `Telo.Import` was
|
|
41
|
+
* excluded outright, on the stated grounds that an alias "lives in a separate
|
|
42
|
+
* namespace from resources", and `Telo.Definition` / `Telo.Abstract` fell out
|
|
43
|
+
* through the ref-validation skip set, which answers a different question. All
|
|
44
|
+
* three passed `telo check` and then died at boot with `ERR_DUPLICATE_RESOURCE`
|
|
45
|
+
* — an application named after one of its own imports (`metadata.name: Schedule`
|
|
46
|
+
* beside `Schedule: oci://…/scheduler`) being the shape that reaches an author,
|
|
47
|
+
* since nothing about the two lines looks like one name written twice.
|
|
48
|
+
*
|
|
49
|
+
* Grouped per DECLARING MODULE, which is what makes including imports sound: a
|
|
50
|
+
* library's own `Telo.Import` / `Telo.Definition` docs are forwarded into a
|
|
51
|
+
* consumer's flat set, and two modules sharing an alias (`Console` in an app and
|
|
52
|
+
* again in a library it imports) is ordinary rather than a collision. A module
|
|
53
|
+
* doc carries no `metadata.module` and IS its own scope — that is what puts an
|
|
54
|
+
* application's own name in the same group as its imports.
|
|
55
|
+
*
|
|
56
|
+
* Kept apart from the resolution lookup it used to share a map with: that one
|
|
57
|
+
* must hold resolution targets alone, so the two disagree about membership by
|
|
58
|
+
* design and merging them re-keyed every bare-name reference.
|
|
59
|
+
*/
|
|
60
|
+
function duplicateNameDiagnostics(
|
|
61
|
+
resources: ResourceManifest[],
|
|
62
|
+
isForeign: (r: ResourceManifest) => boolean,
|
|
63
|
+
moduleOf: (r: ResourceManifest) => string | undefined,
|
|
64
|
+
): AnalysisDiagnostic[] {
|
|
65
|
+
const diagnostics: AnalysisDiagnostic[] = [];
|
|
66
|
+
|
|
67
|
+
const moduleDocNames = new Set<string>();
|
|
68
|
+
for (const r of resources) {
|
|
69
|
+
if (isModuleKind(r.kind) && typeof r.metadata?.name === "string") {
|
|
70
|
+
moduleDocNames.add(r.metadata.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// A module doc belongs in the scope its OWN resources are in. Flattened, they
|
|
74
|
+
// carry `metadata.module` equal to its name, so it scopes to itself — which is
|
|
75
|
+
// what puts an application's name beside its imports. Unflattened (a hand-built
|
|
76
|
+
// fixture, a single-file editor analysis) nothing is stamped, so its resources
|
|
77
|
+
// sit in the unnamed scope and the doc has to join them there or a collision
|
|
78
|
+
// with one of them splits across two groups and goes unreported.
|
|
79
|
+
const stampedModules = new Set<string>();
|
|
80
|
+
for (const r of resources) {
|
|
81
|
+
const m = moduleOf(r);
|
|
82
|
+
if (m) stampedModules.add(m);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// scope → name → declarations, so no separator has to be safe against a name.
|
|
86
|
+
const byScope = new Map<string, Map<string, ResourceManifest[]>>();
|
|
87
|
+
const seen = new Set<string>();
|
|
88
|
+
for (const r of resources) {
|
|
89
|
+
if (!r.metadata?.name || isForeign(r)) continue;
|
|
90
|
+
const name = r.metadata.name as string;
|
|
91
|
+
const scope = isModuleKind(r.kind)
|
|
92
|
+
? (stampedModules.has(name) ? name : "")
|
|
93
|
+
: (moduleOf(r) ?? "");
|
|
94
|
+
// A scope with no module doc in this set is an imported library's, whose
|
|
95
|
+
// doc `selectModuleManifestsForAnalysis` drops — its internals are its own
|
|
96
|
+
// author's to fix, and its own `telo check` reports them.
|
|
97
|
+
if (scope !== "" && moduleDocNames.size > 0 && !moduleDocNames.has(scope)) continue;
|
|
98
|
+
// Dedup pipeline echoes — the same physical document emitted twice through
|
|
99
|
+
// an analyzer host's pipeline (telo studio walks a file reachable as both an
|
|
100
|
+
// entry module and an `include:` partial). Keyed on (kind, name, source,
|
|
101
|
+
// sourceLine), so two textually-distinct docs in one file keep separate
|
|
102
|
+
// fingerprints and still trip the diagnostic.
|
|
103
|
+
const meta = r.metadata as unknown as { source?: string; sourceLine?: number };
|
|
104
|
+
const fingerprint = `${r.kind} ${name} ${meta.source} ${meta.sourceLine}`;
|
|
105
|
+
if (seen.has(fingerprint)) continue;
|
|
106
|
+
seen.add(fingerprint);
|
|
107
|
+
let names = byScope.get(scope);
|
|
108
|
+
if (!names) byScope.set(scope, (names = new Map()));
|
|
109
|
+
const existing = names.get(name);
|
|
110
|
+
if (existing) existing.push(r);
|
|
111
|
+
else names.set(name, [r]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const names of byScope.values()) {
|
|
115
|
+
for (const [name, list] of names) {
|
|
116
|
+
if (list.length <= 1) continue;
|
|
117
|
+
const [first, ...rest] = list;
|
|
118
|
+
for (const dup of rest) {
|
|
119
|
+
// Two imports sharing an alias is ONE defect with its own diagnostic
|
|
120
|
+
// (`DUPLICATE_IMPORT_ALIAS`), which says what to do about it; reporting
|
|
121
|
+
// it again here would describe one mistake as two.
|
|
122
|
+
if (dup.kind === "Telo.Import" && first.kind === "Telo.Import") continue;
|
|
123
|
+
const dupMeta = dup.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
124
|
+
// The precomputed range matters because editor hosts resolve positions
|
|
125
|
+
// via a `${file}::${kind}::${name}` lookup, which collides on duplicates.
|
|
126
|
+
const range =
|
|
127
|
+
typeof dupMeta?.sourceLine === "number"
|
|
128
|
+
? {
|
|
129
|
+
start: { line: dupMeta.sourceLine, character: 0 },
|
|
130
|
+
end: { line: dupMeta.sourceLine, character: Number.MAX_SAFE_INTEGER },
|
|
131
|
+
}
|
|
132
|
+
: undefined;
|
|
133
|
+
diagnostics.push({
|
|
134
|
+
severity: DiagnosticSeverity.Error,
|
|
135
|
+
code: "DUPLICATE_RESOURCE_NAME",
|
|
136
|
+
source: SOURCE,
|
|
137
|
+
message:
|
|
138
|
+
`${dup.kind}/${name}: ${describeNamed(dup, name)} collides with ` +
|
|
139
|
+
`${describeNamed(first, name)} declared earlier — the kernel registers both ` +
|
|
140
|
+
`under '${name}' in one namespace, so boot fails with ERR_DUPLICATE_RESOURCE. ` +
|
|
141
|
+
`Rename one of them.`,
|
|
142
|
+
...(range ? { range } : {}),
|
|
143
|
+
data: {
|
|
144
|
+
resource: { kind: dup.kind, name },
|
|
145
|
+
filePath: dupMeta?.source,
|
|
146
|
+
path: "metadata.name",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return diagnostics;
|
|
153
|
+
}
|
|
154
|
+
|
|
22
155
|
/**
|
|
23
156
|
* Liskov substitutability at a kind constraint: is `resolved` (a canonical
|
|
24
157
|
* `<module>.<Kind>`) accepted where `targetKind` is required?
|
|
@@ -162,50 +295,25 @@ export function validateReferences(
|
|
|
162
295
|
const aliasesByModule = context.aliasesByModule;
|
|
163
296
|
if (!aliases || !registry) return diagnostics;
|
|
164
297
|
|
|
165
|
-
// Build outer resource lookup by name for resolution
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
// kernel refuses to start" surprises.)
|
|
298
|
+
// Build the outer resource lookup by name, for the resolution checks below.
|
|
299
|
+
// WHICH NAMES COLLIDE is a different question over a different namespace and
|
|
300
|
+
// is answered by `duplicateNameDiagnostics`; this map answers only "what does
|
|
301
|
+
// a bare `!ref <name>` resolve to", so it holds resolution TARGETS alone — an
|
|
302
|
+
// import alias and a kind definition are neither, and admitting them here
|
|
303
|
+
// would resolve a reference to something no ref slot can accept.
|
|
172
304
|
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// colliding aliases vs. resource names is benign — the alias is only
|
|
177
|
-
// ever read as a kind prefix).
|
|
178
|
-
// Group manifests by name to detect collisions. Two subtleties:
|
|
179
|
-
//
|
|
180
|
-
// 1. Some analyzer hosts emit the SAME physical document twice through
|
|
181
|
-
// their pipeline — e.g. telo studio's `toAnalysisManifests` walks
|
|
182
|
-
// each workspace module's documents independently, and a file
|
|
183
|
-
// reachable from two angles (entry module + `include:` partial)
|
|
184
|
-
// shows up twice. The fingerprint includes `sourceLine` so identical
|
|
185
|
-
// docs (same kind, name, source, AND source line) collapse to one,
|
|
186
|
-
// while two textually-separate documents in the same file (different
|
|
187
|
-
// source lines) keep separate fingerprints and trip the diagnostic.
|
|
188
|
-
// 2. The diagnostic carries a precomputed `range` pointing at the
|
|
189
|
-
// duplicate's source line — editor hosts that resolve diagnostic
|
|
190
|
-
// positions via a `${file}::${kind}::${name}` lookup would otherwise
|
|
191
|
-
// collide on duplicates (Map.set overwrites) and place the squiggle
|
|
192
|
-
// ambiguously. The explicit `range` short-circuits that lookup.
|
|
193
|
-
// Dedup pipeline echoes — the same physical document emitted twice
|
|
194
|
-
// through an analyzer host's pipeline. Keyed on (kind, name, source,
|
|
195
|
-
// sourceLine), so two textually-distinct docs in the same file (same
|
|
196
|
-
// source, different sourceLine) keep separate fingerprints and still
|
|
197
|
-
// trip the diagnostic. `analyze()` enforces that every non-system
|
|
198
|
-
// manifest carries both positional fields — no defensive guard needed.
|
|
305
|
+
// The list per name survives because resolution falls back to the FIRST
|
|
306
|
+
// occurrence when a collision exists, which keeps the rest of the pass
|
|
307
|
+
// behaving as it did before duplicates were reported at all.
|
|
199
308
|
// Forwarded foreign exports (an imported library's exported instances, carrying a
|
|
200
309
|
// metadata.module that isn't a root module) are resolution TARGETS only: excluded from
|
|
201
310
|
// duplicate detection and local name resolution, and never walked as ref sources.
|
|
202
311
|
const moduleOf = (r: ResourceManifest): string | undefined =>
|
|
203
312
|
(r.metadata as { module?: string } | undefined)?.module;
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
const isForeign =
|
|
208
|
-
(r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
|
|
313
|
+
// A dependency's code — a forwarded export, or what extraction pulled out of one — is
|
|
314
|
+
// excluded from duplicate detection and local name resolution, and never walked as a
|
|
315
|
+
// ref source. Only an EXPORT is a cross-module resolution target.
|
|
316
|
+
const isForeign = isForwardedDeclaration;
|
|
209
317
|
// Forwarded exported instances keyed `${module}\0${name}` — the lookup that resolves
|
|
210
318
|
// whether a cross-module `!ref Alias.name` names a real exported instance.
|
|
211
319
|
const byModuleName = new Map<string, ResourceManifest>();
|
|
@@ -222,7 +330,7 @@ export function validateReferences(
|
|
|
222
330
|
if (typeof m === "string") loadedModules.add(m);
|
|
223
331
|
continue;
|
|
224
332
|
}
|
|
225
|
-
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !
|
|
333
|
+
if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForwardedExport(r)) continue;
|
|
226
334
|
const m = moduleOf(r);
|
|
227
335
|
if (!m) continue;
|
|
228
336
|
byModuleName.set(`${m}\0${r.metadata.name as string}`, r);
|
|
@@ -246,33 +354,7 @@ export function validateReferences(
|
|
|
246
354
|
if (existing) existing.push(r);
|
|
247
355
|
else byNameAll.set(name, [r]);
|
|
248
356
|
}
|
|
249
|
-
|
|
250
|
-
if (list.length <= 1) continue;
|
|
251
|
-
const [first, ...rest] = list;
|
|
252
|
-
const firstLabel = `${first.kind}/${name}`;
|
|
253
|
-
for (const dup of rest) {
|
|
254
|
-
const dupMeta = dup.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
255
|
-
const range =
|
|
256
|
-
typeof dupMeta?.sourceLine === "number"
|
|
257
|
-
? {
|
|
258
|
-
start: { line: dupMeta.sourceLine, character: 0 },
|
|
259
|
-
end: { line: dupMeta.sourceLine, character: Number.MAX_SAFE_INTEGER },
|
|
260
|
-
}
|
|
261
|
-
: undefined;
|
|
262
|
-
diagnostics.push({
|
|
263
|
-
severity: DiagnosticSeverity.Error,
|
|
264
|
-
code: "DUPLICATE_RESOURCE_NAME",
|
|
265
|
-
source: SOURCE,
|
|
266
|
-
message: `${dup.kind}/${name}: resource name collides with ${firstLabel} declared earlier (kernel runtime would fail with ERR_DUPLICATE_RESOURCE)`,
|
|
267
|
-
...(range ? { range } : {}),
|
|
268
|
-
data: {
|
|
269
|
-
resource: { kind: dup.kind, name },
|
|
270
|
-
filePath: dupMeta?.source,
|
|
271
|
-
path: "metadata.name",
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
357
|
+
diagnostics.push(...duplicateNameDiagnostics(resources, isForeign, moduleOf));
|
|
276
358
|
// The dot rule that used to live here is now the strictest special case of
|
|
277
359
|
// the identifier grammar in `validate-identifier-names.ts` — a dot is one of
|
|
278
360
|
// several characters that make a name unreferenceable, and checking one of
|
|
@@ -341,6 +423,9 @@ export function validateReferences(
|
|
|
341
423
|
visibleScopeManifests.find((m) => m.metadata?.name === localName) ??
|
|
342
424
|
byName.get(localName);
|
|
343
425
|
if (!target) {
|
|
426
|
+
// A scoped name out of this declaration's reach has its own
|
|
427
|
+
// diagnostic, which says why; this one would say only "not found".
|
|
428
|
+
if (outsideScopesOf(r).some((scope) => scope.names.includes(localName))) return;
|
|
344
429
|
diagnostics.push({
|
|
345
430
|
severity: DiagnosticSeverity.Error,
|
|
346
431
|
code: "UNRESOLVED_REFERENCE",
|