@telorun/analyzer 0.65.0 → 0.67.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 +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +194 -65
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +67 -16
- package/dist/call-graph.d.ts.map +1 -1
- package/dist/call-graph.js +7 -1
- package/dist/cel-environment.d.ts +12 -0
- package/dist/cel-environment.d.ts.map +1 -1
- package/dist/cel-environment.js +35 -19
- package/dist/cel-scope.d.ts +8 -0
- package/dist/cel-scope.d.ts.map +1 -1
- package/dist/cel-scope.js +89 -11
- package/dist/definition-registry.d.ts +41 -0
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +76 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +65 -0
- package/dist/eval-paths.d.ts +68 -0
- package/dist/eval-paths.d.ts.map +1 -1
- package/dist/eval-paths.js +138 -0
- package/dist/flatten-for-analyzer.d.ts +37 -1
- package/dist/flatten-for-analyzer.d.ts.map +1 -1
- package/dist/flatten-for-analyzer.js +104 -5
- package/dist/import-resolution-diagnostics.d.ts +1 -1
- package/dist/import-resolution-diagnostics.js +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/inline-imports.d.ts.map +1 -1
- package/dist/inline-imports.js +1 -0
- package/dist/manifest-visitor.d.ts +4 -0
- package/dist/manifest-visitor.d.ts.map +1 -1
- package/dist/manifest-visitor.js +36 -1
- package/dist/migrations/entries/index.d.ts.map +1 -1
- package/dist/migrations/entries/index.js +2 -0
- package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
- package/dist/peer-binding.d.ts +232 -0
- package/dist/peer-binding.d.ts.map +1 -0
- package/dist/peer-binding.js +418 -0
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +8 -0
- package/dist/ref-slot.d.ts +13 -0
- package/dist/ref-slot.d.ts.map +1 -1
- package/dist/ref-slot.js +26 -0
- package/dist/reference-field-map.d.ts +28 -0
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +25 -0
- package/dist/referrer-rule.d.ts +17 -0
- package/dist/referrer-rule.d.ts.map +1 -1
- package/dist/referrer-rule.js +4 -1
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.js +1 -1
- package/dist/resolve-ref-sentinels.d.ts.map +1 -1
- package/dist/resolve-ref-sentinels.js +14 -1
- package/dist/resource-input.d.ts +75 -0
- package/dist/resource-input.d.ts.map +1 -0
- package/dist/resource-input.js +90 -0
- package/dist/resource-rule.d.ts +40 -27
- package/dist/resource-rule.d.ts.map +1 -1
- package/dist/resource-rule.js +61 -7
- package/dist/rule-condition.d.ts +11 -0
- package/dist/rule-condition.d.ts.map +1 -1
- package/dist/rule-condition.js +13 -0
- package/dist/schema-projection.d.ts +119 -4
- package/dist/schema-projection.d.ts.map +1 -1
- package/dist/schema-projection.js +213 -18
- package/dist/system-kinds.d.ts +7 -2
- package/dist/system-kinds.d.ts.map +1 -1
- package/dist/system-kinds.js +7 -2
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/template-body.d.ts +50 -0
- package/dist/template-body.d.ts.map +1 -0
- package/dist/template-body.js +58 -0
- package/dist/validate-cel-context.d.ts +1 -17
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +73 -78
- package/dist/validate-identifier-names.d.ts.map +1 -1
- package/dist/validate-identifier-names.js +17 -2
- package/dist/validate-reference-forms.d.ts +10 -0
- package/dist/validate-reference-forms.d.ts.map +1 -1
- package/dist/validate-reference-forms.js +23 -0
- package/dist/validate-references.d.ts +17 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +77 -18
- package/dist/validate-referrer-rules.d.ts +38 -4
- package/dist/validate-referrer-rules.d.ts.map +1 -1
- package/dist/validate-referrer-rules.js +139 -31
- package/dist/validate-resource-inputs.d.ts +35 -0
- package/dist/validate-resource-inputs.d.ts.map +1 -0
- package/dist/validate-resource-inputs.js +319 -0
- package/dist/validate-resource-rules.d.ts +11 -2
- package/dist/validate-resource-rules.d.ts.map +1 -1
- package/dist/validate-resource-rules.js +18 -12
- package/dist/validate-schema-projection.d.ts.map +1 -1
- package/dist/validate-schema-projection.js +33 -2
- package/dist/validate-template-dispatch.d.ts +27 -0
- package/dist/validate-template-dispatch.d.ts.map +1 -0
- package/dist/validate-template-dispatch.js +95 -0
- package/dist/with-synthetic-positions.d.ts +1 -1
- package/dist/with-synthetic-positions.js +1 -1
- package/package.json +3 -3
- package/src/analyzer.ts +236 -70
- package/src/builtins.ts +69 -16
- package/src/call-graph.ts +7 -1
- package/src/cel-environment.ts +48 -19
- package/src/cel-scope.ts +113 -19
- package/src/definition-registry.ts +78 -0
- package/src/dependency-graph.ts +66 -0
- package/src/eval-paths.ts +152 -0
- package/src/flatten-for-analyzer.ts +117 -4
- package/src/import-resolution-diagnostics.ts +1 -1
- package/src/index.ts +57 -4
- package/src/inline-imports.ts +1 -0
- package/src/manifest-visitor.ts +41 -1
- package/src/migrations/entries/index.ts +2 -0
- package/src/migrations/entries/schema-prepare-bucket.json +27 -0
- package/src/peer-binding.ts +641 -0
- package/src/precompile.ts +8 -0
- package/src/ref-slot.ts +36 -0
- package/src/reference-field-map.ts +42 -0
- package/src/referrer-rule.ts +20 -1
- package/src/release/index.ts +1 -1
- package/src/resolve-ref-sentinels.ts +12 -1
- package/src/resource-input.ts +132 -0
- package/src/resource-rule.ts +69 -7
- package/src/rule-condition.ts +15 -0
- package/src/schema-projection.ts +302 -20
- package/src/system-kinds.ts +7 -2
- package/src/telo-version.ts +1 -1
- package/src/template-body.ts +104 -0
- package/src/validate-cel-context.ts +73 -78
- package/src/validate-identifier-names.ts +18 -3
- package/src/validate-reference-forms.ts +22 -0
- package/src/validate-references.ts +85 -16
- package/src/validate-referrer-rules.ts +215 -28
- package/src/validate-resource-inputs.ts +367 -0
- package/src/validate-resource-rules.ts +26 -12
- package/src/validate-schema-projection.ts +50 -1
- package/src/validate-template-dispatch.ts +99 -0
- package/src/with-synthetic-positions.ts +1 -1
|
@@ -6,8 +6,37 @@ import {
|
|
|
6
6
|
parseCanonicalTypeSchemaId,
|
|
7
7
|
} from "@telorun/sdk";
|
|
8
8
|
import { KERNEL_BUILTINS } from "./builtins.js";
|
|
9
|
-
import { isStepSlot } from "./step-slot.js";
|
|
10
9
|
import { withRefSlotsAsReadings } from "./ref-slot-reading.js";
|
|
10
|
+
// Where CEL is evaluated is one reader (`eval-paths.ts`), and the region half of
|
|
11
|
+
// it moved there so the scope walk and the `x-telo-eval` walk answer the same
|
|
12
|
+
// question in one place. Re-exported: this module is where every existing
|
|
13
|
+
// consumer imports them from.
|
|
14
|
+
export { extractCelRegionScopes, pathMatchesScope } from "./eval-paths.js";
|
|
15
|
+
import { pathMatchesScope } from "./eval-paths.js";
|
|
16
|
+
|
|
17
|
+
/** True when a node's value is decided at load or dispatch rather than written
|
|
18
|
+
* — a tagged sentinel (`!cel`, `!sql`, an embed) or an already-compiled value.
|
|
19
|
+
* A schema position holding one declares a shape nothing static can read. */
|
|
20
|
+
function isDynamicNode(value: unknown): boolean {
|
|
21
|
+
if (!value || typeof value !== "object") return false;
|
|
22
|
+
const node = value as Record<string, unknown>;
|
|
23
|
+
return node.__tagged === true || node.__compiled !== undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Replace every dynamically-valued property with an OPEN schema, so the name is
|
|
27
|
+
* in scope with its members unconstrained. */
|
|
28
|
+
function openDynamicProperties(
|
|
29
|
+
props: Record<string, any> | undefined,
|
|
30
|
+
): Record<string, any> | undefined {
|
|
31
|
+
if (!props || typeof props !== "object") return props;
|
|
32
|
+
let out: Record<string, any> | undefined;
|
|
33
|
+
for (const [key, value] of Object.entries(props)) {
|
|
34
|
+
if (!isDynamicNode(value)) continue;
|
|
35
|
+
out ??= { ...props };
|
|
36
|
+
out[key] = {};
|
|
37
|
+
}
|
|
38
|
+
return out ?? props;
|
|
39
|
+
}
|
|
11
40
|
|
|
12
41
|
export interface ContextResolveOpts {
|
|
13
42
|
/** When provided, used to resolve `x-telo-context-from-root` annotations against the
|
|
@@ -282,35 +311,6 @@ function collectionBindingWithheld(
|
|
|
282
311
|
return isLiveSlot(resolveCollectionSchema(manifestRoot, from, allManifests));
|
|
283
312
|
}
|
|
284
313
|
|
|
285
|
-
/**
|
|
286
|
-
* Returns true when a CEL expression path (from walkCelExpressions, e.g. "routes[0].inputs.q")
|
|
287
|
-
* falls within the scope of a context (e.g. "$.routes[*].inputs").
|
|
288
|
-
*
|
|
289
|
-
* The scope is matched directly (no sibling sharing): a context at "$.routes[*].inputs" only
|
|
290
|
-
* applies to expressions whose path starts with "routes[N].inputs", not to other sibling fields.
|
|
291
|
-
*/
|
|
292
|
-
export function pathMatchesScope(exprPath: string, scope: string): boolean {
|
|
293
|
-
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
294
|
-
if (!stripped) return false;
|
|
295
|
-
|
|
296
|
-
// Split on wildcard array segments; each [*] must match a concrete [N] in exprPath
|
|
297
|
-
const parts = stripped.split("[*]");
|
|
298
|
-
let remaining = exprPath;
|
|
299
|
-
for (let i = 0; i < parts.length; i++) {
|
|
300
|
-
const part = parts[i]!;
|
|
301
|
-
if (!remaining.startsWith(part)) return false;
|
|
302
|
-
remaining = remaining.slice(part.length);
|
|
303
|
-
if (i < parts.length - 1) {
|
|
304
|
-
// Expect a concrete array index like [0], [12], ...
|
|
305
|
-
const m = remaining.match(/^\[\d+\]/);
|
|
306
|
-
if (!m) return false;
|
|
307
|
-
remaining = remaining.slice(m[0].length);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
// Expression must end here or continue into a child path
|
|
311
|
-
return remaining === "" || remaining[0] === "." || remaining[0] === "[";
|
|
312
|
-
}
|
|
313
|
-
|
|
314
314
|
/**
|
|
315
315
|
* Resolves `x-telo-context-*` annotations in a context schema using the concrete
|
|
316
316
|
* manifest item (per-scope) and the manifest root.
|
|
@@ -381,8 +381,19 @@ export function resolveContextAnnotations(
|
|
|
381
381
|
// to be resolved first: the standard library writes it as the inline
|
|
382
382
|
// `{ kind: Type.JsonSchema, schema: … }` wrapper, so merging it verbatim
|
|
383
383
|
// would type the variable as `{ kind, schema }` instead of its properties.
|
|
384
|
+
// The navigated node — or one of the properties in it — may itself be an
|
|
385
|
+
// expression: a route whose `request.schema.body` is `!cel "self.model.schema"`
|
|
386
|
+
// declares a body whose SHAPE is only known once the template is
|
|
387
|
+
// instantiated. Such a name types as OPEN rather than as nothing, and a
|
|
388
|
+
// wholly dynamic map leaves the whole node open. Resolving it to nothing
|
|
389
|
+
// reports `request.body` as undefined, which blames the reader for the
|
|
390
|
+
// writer's dynamism — the same posture a rule takes when a value it reads
|
|
391
|
+
// is dynamic: skip the judgement, never invert it.
|
|
392
|
+
if (isDynamicNode(navigated)) {
|
|
393
|
+
return { ...schema, properties: { ...(schema.properties ?? {}) }, additionalProperties: true };
|
|
394
|
+
}
|
|
384
395
|
const asType = resolveTypeFieldToSchema(navigated, allManifests ?? []);
|
|
385
|
-
const resolved = asType?.properties ?? navigated;
|
|
396
|
+
const resolved = openDynamicProperties(asType?.properties ?? navigated);
|
|
386
397
|
const required = Array.isArray(asType?.required) ? asType.required : undefined;
|
|
387
398
|
return {
|
|
388
399
|
...schema,
|
|
@@ -544,12 +555,37 @@ export function getManifestItem(
|
|
|
544
555
|
manifest: Record<string, any>,
|
|
545
556
|
): Record<string, any> {
|
|
546
557
|
const stripped = scope.startsWith("$.") ? scope.slice(2) : scope;
|
|
547
|
-
const
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
558
|
+
const parts = stripped.split("[*]");
|
|
559
|
+
if (parts.length === 1) return manifest;
|
|
560
|
+
// Resolve each `[*]` against the concrete index the expression path carries at
|
|
561
|
+
// the same position, accumulating up to the LAST wildcard: that node is the
|
|
562
|
+
// per-scope item (`routes[2]`), which is what `x-telo-context-from` navigates
|
|
563
|
+
// from. Built by walking the pattern rather than by a regex over the scope —
|
|
564
|
+
// a scope is a path, not a pattern, and its own `[4]` segments are regex
|
|
565
|
+
// character classes, which is what silently made every nested scope resolve
|
|
566
|
+
// to the whole document instead of the item.
|
|
567
|
+
let remaining = exprPath;
|
|
568
|
+
let concrete = "";
|
|
569
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
570
|
+
const part = parts[i]!;
|
|
571
|
+
if (!remaining.startsWith(part)) return manifest;
|
|
572
|
+
remaining = remaining.slice(part.length);
|
|
573
|
+
const index = remaining.match(/^\[(\d+)\]/);
|
|
574
|
+
if (!index) return manifest;
|
|
575
|
+
remaining = remaining.slice(index[0].length);
|
|
576
|
+
concrete += `${part}${index[0]}`;
|
|
577
|
+
}
|
|
578
|
+
return (navigateConcretePath(manifest, concrete) as Record<string, any> | undefined) ?? manifest;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/** Walk a concrete dotted path with `[N]` indices (`resources[4].routes[2]`). */
|
|
582
|
+
function navigateConcretePath(root: unknown, path: string): unknown {
|
|
583
|
+
let cur: unknown = root;
|
|
584
|
+
for (const segment of path.match(/[^.[\]]+/g) ?? []) {
|
|
585
|
+
if (cur === null || typeof cur !== "object") return undefined;
|
|
586
|
+
cur = (cur as Record<string, unknown>)[segment];
|
|
587
|
+
}
|
|
588
|
+
return cur;
|
|
553
589
|
}
|
|
554
590
|
|
|
555
591
|
function navigatePath(obj: unknown, segments: string[]): unknown {
|
|
@@ -579,47 +615,6 @@ export function extractContextsFromSchema(
|
|
|
579
615
|
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
580
616
|
}
|
|
581
617
|
|
|
582
|
-
/** Schema keys that declare a CEL-bearing region: a field carrying any of these
|
|
583
|
-
* is evaluated at runtime, so a `!cel` inside it (or a descendant) is live. A
|
|
584
|
-
* STEP BODY is one too, and says so through the grammar its items point at
|
|
585
|
-
* rather than through a key — {@link isStepSlot} reads either spelling. */
|
|
586
|
-
const CEL_REGION_KEYS = [
|
|
587
|
-
"x-telo-context",
|
|
588
|
-
"x-telo-step-context",
|
|
589
|
-
"x-telo-error-context",
|
|
590
|
-
] as const;
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Walk a JSON Schema tree and collect the JSONPath scopes of every field that
|
|
594
|
-
* declares a CEL-bearing region (`x-telo-context` / `x-telo-step-context` /
|
|
595
|
-
* `x-telo-error-context`). Used — alongside `x-telo-eval` paths — to decide
|
|
596
|
-
* whether a `!cel` expression sits in a slot the runtime actually evaluates.
|
|
597
|
-
* Scopes use the same `$.a.b[*]` form as `extractContextsFromSchema`, matched
|
|
598
|
-
* against expression paths with `pathMatchesScope`.
|
|
599
|
-
*/
|
|
600
|
-
export function extractCelRegionScopes(schema: Record<string, any>, path = "$"): string[] {
|
|
601
|
-
if (!schema || typeof schema !== "object") return [];
|
|
602
|
-
const out: string[] = [];
|
|
603
|
-
|
|
604
|
-
if (CEL_REGION_KEYS.some((k) => schema[k]) || isStepSlot(schema)) out.push(path);
|
|
605
|
-
|
|
606
|
-
if (schema.properties) {
|
|
607
|
-
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
608
|
-
out.push(...extractCelRegionScopes(value, `${path}.${key}`));
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (schema.items && typeof schema.items === "object") {
|
|
612
|
-
out.push(...extractCelRegionScopes(schema.items, `${path}[*]`));
|
|
613
|
-
}
|
|
614
|
-
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
615
|
-
if (Array.isArray(schema[key])) {
|
|
616
|
-
for (const subschema of schema[key]) out.push(...extractCelRegionScopes(subschema, path));
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return out;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
618
|
function collectContexts(
|
|
624
619
|
schema: Record<string, any>,
|
|
625
620
|
path: string,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type NameLevel,
|
|
10
10
|
type NameViolation,
|
|
11
11
|
} from "./identifier-name.js";
|
|
12
|
+
import { isInjectedDeclaration } from "./resource-input.js";
|
|
12
13
|
import { type AnalysisDiagnostic } from "./types.js";
|
|
13
14
|
|
|
14
15
|
const SOURCE = "telo-analyzer";
|
|
@@ -56,6 +57,11 @@ export function validateIdentifierNames(
|
|
|
56
57
|
// would blame them for a spelling this pass's own pipeline chose.
|
|
57
58
|
if (metadata?.xTeloOrigin) continue;
|
|
58
59
|
|
|
60
|
+
// A kind-only stand-in for a `resources:` entry carries the name the author
|
|
61
|
+
// wrote in that block, and the block's own keys are checked below — on the
|
|
62
|
+
// module doc, where the name is written and a squiggle can land.
|
|
63
|
+
if (isInjectedDeclaration(manifest)) continue;
|
|
64
|
+
|
|
59
65
|
const ownModule = metadata?.module as string | undefined;
|
|
60
66
|
if (ownModule && !rootModules.has(ownModule)) continue;
|
|
61
67
|
|
|
@@ -74,7 +80,10 @@ export function validateIdentifierNames(
|
|
|
74
80
|
// keys are the imported library's declarations, so a violation there is
|
|
75
81
|
// the library author's, reported when their module is analyzed as a root.
|
|
76
82
|
if (manifest.kind === "Telo.Application" || manifest.kind === "Telo.Library") {
|
|
77
|
-
|
|
83
|
+
// `resources:` joins them: a library's resource inputs are named in the
|
|
84
|
+
// same identifier space (`!ref connection`, `resources.connection`) and
|
|
85
|
+
// break the same way.
|
|
86
|
+
for (const field of ["variables", "secrets", "ports", "resources"] as const) {
|
|
78
87
|
const block = (manifest as Record<string, unknown>)[field];
|
|
79
88
|
if (!block || typeof block !== "object" || Array.isArray(block)) continue;
|
|
80
89
|
for (const key of Object.keys(block as Record<string, unknown>)) {
|
|
@@ -149,8 +158,14 @@ function surfaceFor(kind: string): string {
|
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
function singular(field: "variables" | "secrets" | "ports"): string {
|
|
153
|
-
return field === "variables"
|
|
161
|
+
function singular(field: "variables" | "secrets" | "ports" | "resources"): string {
|
|
162
|
+
return field === "variables"
|
|
163
|
+
? "variable"
|
|
164
|
+
: field === "secrets"
|
|
165
|
+
? "secret"
|
|
166
|
+
: field === "ports"
|
|
167
|
+
? "port"
|
|
168
|
+
: "resource input";
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
function push(
|
|
@@ -3,6 +3,7 @@ import { isTaggedSentinel } from "@telorun/templating";
|
|
|
3
3
|
import type { AliasResolver } from "./alias-resolver.js";
|
|
4
4
|
import type { DefinitionRegistry } from "./definition-registry.js";
|
|
5
5
|
import { visitManifest } from "./manifest-visitor.js";
|
|
6
|
+
import { satisfiesValueBranch } from "./reference-field-map.js";
|
|
6
7
|
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
7
8
|
import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
|
|
8
9
|
|
|
@@ -29,6 +30,16 @@ const SOURCE = "telo-analyzer";
|
|
|
29
30
|
* Rejected, each with an actionable diagnostic pointing at `!ref`:
|
|
30
31
|
* - the object form `{ kind, name }` (the old reference object), and
|
|
31
32
|
* - a bare string (the old name / dotted-FQN reference).
|
|
33
|
+
*
|
|
34
|
+
* **A value-or-reference union slot is the one exception**, and it is decided by
|
|
35
|
+
* the value rather than by a new spelling: where the reference constraint is a
|
|
36
|
+
* *branch* of a union (`type:` on a column, holding either a storage class or a
|
|
37
|
+
* `!ref` to a declared enum), a SCALAR is a value and is left to that branch,
|
|
38
|
+
* while an object is still checked for the removed `{kind, name}` form unless a
|
|
39
|
+
* branch describes it. The rule guards a REMOVED spelling — a bare string is
|
|
40
|
+
* simply not a reference in Telo — so admitting one takes nothing away: what it
|
|
41
|
+
* costs is the answer when an author meant a reference and wrote a bare name,
|
|
42
|
+
* which against a closed branch AJV reports as an unknown value.
|
|
32
43
|
*/
|
|
33
44
|
export function validateReferenceForms(
|
|
34
45
|
resources: ResourceManifest[],
|
|
@@ -52,6 +63,17 @@ export function validateReferenceForms(
|
|
|
52
63
|
// `!ref` and `!cel`/`${{ }}` sentinels are the supported shapes.
|
|
53
64
|
if (isTaggedSentinel(value)) return;
|
|
54
65
|
|
|
66
|
+
// A SCALAR at a slot whose union has a value branch is a value, and the
|
|
67
|
+
// value branch is what judges it. Reporting a mistyped storage class as
|
|
68
|
+
// a malformed reference — "write it as '!ref txt'" — instructs the author
|
|
69
|
+
// to convert a typo into a reference, and AJV has already said what the
|
|
70
|
+
// value must be one of. The object form below is not ambiguous the same
|
|
71
|
+
// way: `{kind, name}` is the removed reference object, so it is still
|
|
72
|
+
// reported unless a branch genuinely describes that shape.
|
|
73
|
+
const hasValueBranch = (e.entry.valueBranches?.length ?? 0) > 0;
|
|
74
|
+
if (hasValueBranch && typeof value !== "object") return;
|
|
75
|
+
if (satisfiesValueBranch(value, e.entry.valueBranches, registry)) return;
|
|
76
|
+
|
|
55
77
|
const r = e.source;
|
|
56
78
|
const resourceLabel = `${r.kind}/${r.metadata!.name as string}`;
|
|
57
79
|
const resourceData = { kind: r.kind, name: r.metadata!.name as string };
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
2
|
import { isRefSentinel } from "@telorun/templating";
|
|
3
3
|
import { visitManifest } from "./manifest-visitor.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isInlineResource,
|
|
6
|
+
resolveFieldEntries,
|
|
7
|
+
resolveFieldValues,
|
|
8
|
+
satisfiesValueBranch,
|
|
9
|
+
type RefFieldEntry,
|
|
10
|
+
} from "./reference-field-map.js";
|
|
5
11
|
import { navigateJsonPointer, substituteCelFields } from "./schema-compat.js";
|
|
6
12
|
import { REF_VALIDATION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
|
|
7
13
|
import { resolveTypeFieldToSchema } from "./validate-cel-context.js";
|
|
@@ -12,11 +18,53 @@ import type { DefinitionRegistry } from "./definition-registry.js";
|
|
|
12
18
|
const SOURCE = "telo-analyzer";
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
21
|
+
* Liskov substitutability at a kind constraint: is `resolved` (a canonical
|
|
22
|
+
* `<module>.<Kind>`) accepted where `targetKind` is required?
|
|
23
|
+
*
|
|
24
|
+
* THE one implementation — `checkKind` below builds a MESSAGE from it and adds
|
|
25
|
+
* nothing to the rule, so a resource input's injection site and an ordinary ref
|
|
26
|
+
* slot cannot come to disagree about what satisfies a constraint.
|
|
27
|
+
*
|
|
28
|
+
* A value satisfies the slot when it transitively extends the target kind, or —
|
|
29
|
+
* for a CONCRETE target — IS that kind; `getByExtends` is the same transitive
|
|
30
|
+
* subtype index for both, and an abstract is satisfied only by an implementer,
|
|
31
|
+
* never by the abstract itself (which is non-instantiable). Accepts a constraint
|
|
32
|
+
* that resolves to nothing, and an abstract with no loaded implementations:
|
|
33
|
+
* partial context, where a rejection would be a guess.
|
|
19
34
|
*/
|
|
35
|
+
export function kindSatisfies(
|
|
36
|
+
resolved: string,
|
|
37
|
+
targetKind: string,
|
|
38
|
+
registry: DefinitionRegistry,
|
|
39
|
+
): boolean {
|
|
40
|
+
const canonical = registry.resolveRef(targetKind) ?? targetKind;
|
|
41
|
+
const targetDef = registry.resolve(canonical);
|
|
42
|
+
if (!targetDef) return true;
|
|
43
|
+
if (targetDef.kind !== "Telo.Abstract" && resolved === canonical) return true;
|
|
44
|
+
const subtypes = registry.getByExtends(canonical);
|
|
45
|
+
if (subtypes.some((d) => `${d.metadata.module}.${d.metadata.name}` === resolved)) return true;
|
|
46
|
+
// Leniency is about the CANDIDATE, not about the population, and it is the
|
|
47
|
+
// same question for an abstract target and a concrete one. Accepting whenever
|
|
48
|
+
// no subtype happened to be loaded silenced the check exactly where it was
|
|
49
|
+
// needed: an app whose imports declare no `Telo.Runnable` is an app whose boot
|
|
50
|
+
// targets are all wrong, and every one of them passed.
|
|
51
|
+
//
|
|
52
|
+
// An UNREGISTERED candidate is not partial context, and the distinction is
|
|
53
|
+
// load-bearing: a kind nobody declared is an unknown kind or an unimported
|
|
54
|
+
// alias prefix (`NotAnAlias.Script`), which is precisely what this check
|
|
55
|
+
// exists to report. Only a kind that IS declared, whose ancestry reaches
|
|
56
|
+
// something this analysis never saw, is undecidable — there the missing hop is
|
|
57
|
+
// where the target it appears not to reach could have been declared.
|
|
58
|
+
// A candidate the registry never saw cannot be judged on what it implements —
|
|
59
|
+
// the analysis simply does not hold it. Whether its NAME is resolvable is a
|
|
60
|
+
// different question, asked of the alias scope by the caller.
|
|
61
|
+
if (!registry.resolve(resolved)) return true;
|
|
62
|
+
return !registry.ancestryResolved(resolved);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** {@link kindSatisfies} at every kind a slot accepts, rendered as messages.
|
|
66
|
+
* The acceptance rule is not restated here — only what to say when it fails,
|
|
67
|
+
* which needs the slot's alternatives and the target's flavour. */
|
|
20
68
|
function checkKind(
|
|
21
69
|
kind: string,
|
|
22
70
|
entry: RefFieldEntry,
|
|
@@ -24,23 +72,30 @@ function checkKind(
|
|
|
24
72
|
aliases: AliasResolver,
|
|
25
73
|
): string[] {
|
|
26
74
|
const resolved = aliases.resolveKind(kind) ?? kind;
|
|
75
|
+
// A qualified kind whose prefix names no import in this scope is a bad NAME
|
|
76
|
+
// rather than a bad type — `{kind: NotAnAlias.Script}` where only `Lib` is
|
|
77
|
+
// imported — and this check is the only place it surfaces, so it is never
|
|
78
|
+
// waved through as partial context. Asked of the resolver, which reports
|
|
79
|
+
// `unknown` for exactly that case: a kind reached through a DECLARED alias
|
|
80
|
+
// resolves (`ok`) even when the target's definitions are not loaded, and a
|
|
81
|
+
// gated one says so separately.
|
|
82
|
+
const unknownAlias =
|
|
83
|
+
kind.includes(".") &&
|
|
84
|
+
aliases.resolveKindResult(kind).status === "unknown" &&
|
|
85
|
+
// …and the registry does not hold it under the name as written either. A
|
|
86
|
+
// manifest may carry a canonical `<module>.<Kind>`, which no alias resolves
|
|
87
|
+
// and which is nonetheless perfectly identified.
|
|
88
|
+
registry.resolve(resolved) === undefined;
|
|
27
89
|
const errors: string[] = [];
|
|
28
90
|
for (const refStr of entry.refs) {
|
|
29
91
|
const targetKind = registry.resolveRef(refStr);
|
|
30
92
|
if (!targetKind) return [];
|
|
31
93
|
const targetDef = registry.resolve(targetKind);
|
|
32
94
|
if (!targetDef) return [];
|
|
33
|
-
|
|
34
|
-
// extends the target kind, or — for a CONCRETE target — IS that kind.
|
|
35
|
-
// `getByExtends` is the same transitive subtype index for abstract and
|
|
36
|
-
// concrete targets alike; an abstract is satisfied only by an implementer,
|
|
37
|
-
// never by the abstract kind itself (which is non-instantiable).
|
|
38
|
-
if (targetDef.kind !== "Telo.Abstract" && resolved === targetKind) return [];
|
|
95
|
+
if (!unknownAlias && kindSatisfies(resolved, targetKind, registry)) return [];
|
|
39
96
|
const subtypes = registry.getByExtends(targetKind);
|
|
40
97
|
const subtypeKinds = new Set(subtypes.map((d) => `${d.metadata.module}.${d.metadata.name}`));
|
|
41
|
-
if (subtypeKinds.has(resolved)) return [];
|
|
42
98
|
if (targetDef.kind === "Telo.Abstract") {
|
|
43
|
-
if (subtypes.length === 0) return []; // partial context — no implementations loaded yet
|
|
44
99
|
// Suggest only what an author can actually wire: with abstract-extends-
|
|
45
100
|
// abstract real (Telo.Executable over Invocable/Runnable), the transitive
|
|
46
101
|
// subtype list contains abstracts, which are non-instantiable and would
|
|
@@ -48,9 +103,16 @@ function checkKind(
|
|
|
48
103
|
const concrete = subtypes
|
|
49
104
|
.filter((d) => d.kind !== "Telo.Abstract")
|
|
50
105
|
.map((d) => `${d.metadata.module}.${d.metadata.name}`);
|
|
51
|
-
const options =
|
|
106
|
+
const options = concrete.length > 0 ? concrete : [...subtypeKinds];
|
|
107
|
+
// With nothing loaded that implements the target there is no list to
|
|
108
|
+
// offer, so the message says what this kind IS instead — which is the
|
|
109
|
+
// half the author can act on ("it declares 'Telo.Invocable'" points
|
|
110
|
+
// straight at a boot target that should have been an invoke step).
|
|
111
|
+
const declared = registry.resolve(resolved)?.capability;
|
|
52
112
|
errors.push(
|
|
53
|
-
|
|
113
|
+
options.length > 0
|
|
114
|
+
? `'${kind}' does not implement '${targetKind}' (known implementations: ${options.join(", ")})`
|
|
115
|
+
: `'${kind}' does not implement '${targetKind}'${declared ? ` — it declares '${declared}'` : ""}`,
|
|
54
116
|
);
|
|
55
117
|
} else {
|
|
56
118
|
const options = subtypeKinds.size > 0 ? ` or a subtype (${[...subtypeKinds].join(", ")})` : "";
|
|
@@ -101,7 +163,7 @@ export function validateReferences(
|
|
|
101
163
|
// Group manifests by name to detect collisions. Two subtleties:
|
|
102
164
|
//
|
|
103
165
|
// 1. Some analyzer hosts emit the SAME physical document twice through
|
|
104
|
-
// their pipeline — e.g.
|
|
166
|
+
// their pipeline — e.g. telo studio's `toAnalysisManifests` walks
|
|
105
167
|
// each workspace module's documents independently, and a file
|
|
106
168
|
// reachable from two angles (entry module + `include:` partial)
|
|
107
169
|
// shows up twice. The fingerprint includes `sourceLine` so identical
|
|
@@ -293,6 +355,13 @@ export function validateReferences(
|
|
|
293
355
|
if (typeof val !== "object") return;
|
|
294
356
|
const refVal = val as Record<string, unknown>;
|
|
295
357
|
|
|
358
|
+
// A value the slot's own union describes is a value, not a reference —
|
|
359
|
+
// the same narrowing `validateReferenceForms` applies, through the same
|
|
360
|
+
// function, because it has to be applied here too: an OBJECT-shaped value
|
|
361
|
+
// branch would otherwise reach the structural check below and be reported
|
|
362
|
+
// as a reference missing 'kind' and 'name'.
|
|
363
|
+
if (satisfiesValueBranch(val, entry.valueBranches, registry)) return;
|
|
364
|
+
|
|
296
365
|
// Skip inline resources — Phase 2 normalization hasn't run yet.
|
|
297
366
|
if (isInlineResource(refVal)) return;
|
|
298
367
|
|