@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.
Files changed (142) hide show
  1. package/dist/analyzer.d.ts +1 -1
  2. package/dist/analyzer.d.ts.map +1 -1
  3. package/dist/analyzer.js +194 -65
  4. package/dist/builtins.d.ts.map +1 -1
  5. package/dist/builtins.js +67 -16
  6. package/dist/call-graph.d.ts.map +1 -1
  7. package/dist/call-graph.js +7 -1
  8. package/dist/cel-environment.d.ts +12 -0
  9. package/dist/cel-environment.d.ts.map +1 -1
  10. package/dist/cel-environment.js +35 -19
  11. package/dist/cel-scope.d.ts +8 -0
  12. package/dist/cel-scope.d.ts.map +1 -1
  13. package/dist/cel-scope.js +89 -11
  14. package/dist/definition-registry.d.ts +41 -0
  15. package/dist/definition-registry.d.ts.map +1 -1
  16. package/dist/definition-registry.js +76 -0
  17. package/dist/dependency-graph.d.ts.map +1 -1
  18. package/dist/dependency-graph.js +65 -0
  19. package/dist/eval-paths.d.ts +68 -0
  20. package/dist/eval-paths.d.ts.map +1 -1
  21. package/dist/eval-paths.js +138 -0
  22. package/dist/flatten-for-analyzer.d.ts +37 -1
  23. package/dist/flatten-for-analyzer.d.ts.map +1 -1
  24. package/dist/flatten-for-analyzer.js +104 -5
  25. package/dist/import-resolution-diagnostics.d.ts +1 -1
  26. package/dist/import-resolution-diagnostics.js +1 -1
  27. package/dist/index.d.ts +12 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -6
  30. package/dist/inline-imports.d.ts.map +1 -1
  31. package/dist/inline-imports.js +1 -0
  32. package/dist/manifest-visitor.d.ts +4 -0
  33. package/dist/manifest-visitor.d.ts.map +1 -1
  34. package/dist/manifest-visitor.js +36 -1
  35. package/dist/migrations/entries/index.d.ts.map +1 -1
  36. package/dist/migrations/entries/index.js +2 -0
  37. package/dist/migrations/entries/schema-prepare-bucket.json +27 -0
  38. package/dist/peer-binding.d.ts +232 -0
  39. package/dist/peer-binding.d.ts.map +1 -0
  40. package/dist/peer-binding.js +418 -0
  41. package/dist/precompile.d.ts.map +1 -1
  42. package/dist/precompile.js +8 -0
  43. package/dist/ref-slot.d.ts +13 -0
  44. package/dist/ref-slot.d.ts.map +1 -1
  45. package/dist/ref-slot.js +26 -0
  46. package/dist/reference-field-map.d.ts +28 -0
  47. package/dist/reference-field-map.d.ts.map +1 -1
  48. package/dist/reference-field-map.js +25 -0
  49. package/dist/referrer-rule.d.ts +17 -0
  50. package/dist/referrer-rule.d.ts.map +1 -1
  51. package/dist/referrer-rule.js +4 -1
  52. package/dist/release/index.d.ts +1 -1
  53. package/dist/release/index.js +1 -1
  54. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  55. package/dist/resolve-ref-sentinels.js +14 -1
  56. package/dist/resource-input.d.ts +75 -0
  57. package/dist/resource-input.d.ts.map +1 -0
  58. package/dist/resource-input.js +90 -0
  59. package/dist/resource-rule.d.ts +40 -27
  60. package/dist/resource-rule.d.ts.map +1 -1
  61. package/dist/resource-rule.js +61 -7
  62. package/dist/rule-condition.d.ts +11 -0
  63. package/dist/rule-condition.d.ts.map +1 -1
  64. package/dist/rule-condition.js +13 -0
  65. package/dist/schema-projection.d.ts +119 -4
  66. package/dist/schema-projection.d.ts.map +1 -1
  67. package/dist/schema-projection.js +213 -18
  68. package/dist/system-kinds.d.ts +7 -2
  69. package/dist/system-kinds.d.ts.map +1 -1
  70. package/dist/system-kinds.js +7 -2
  71. package/dist/telo-version.d.ts +1 -1
  72. package/dist/telo-version.js +1 -1
  73. package/dist/template-body.d.ts +50 -0
  74. package/dist/template-body.d.ts.map +1 -0
  75. package/dist/template-body.js +58 -0
  76. package/dist/validate-cel-context.d.ts +1 -17
  77. package/dist/validate-cel-context.d.ts.map +1 -1
  78. package/dist/validate-cel-context.js +73 -78
  79. package/dist/validate-identifier-names.d.ts.map +1 -1
  80. package/dist/validate-identifier-names.js +17 -2
  81. package/dist/validate-reference-forms.d.ts +10 -0
  82. package/dist/validate-reference-forms.d.ts.map +1 -1
  83. package/dist/validate-reference-forms.js +23 -0
  84. package/dist/validate-references.d.ts +17 -0
  85. package/dist/validate-references.d.ts.map +1 -1
  86. package/dist/validate-references.js +77 -18
  87. package/dist/validate-referrer-rules.d.ts +38 -4
  88. package/dist/validate-referrer-rules.d.ts.map +1 -1
  89. package/dist/validate-referrer-rules.js +139 -31
  90. package/dist/validate-resource-inputs.d.ts +35 -0
  91. package/dist/validate-resource-inputs.d.ts.map +1 -0
  92. package/dist/validate-resource-inputs.js +319 -0
  93. package/dist/validate-resource-rules.d.ts +11 -2
  94. package/dist/validate-resource-rules.d.ts.map +1 -1
  95. package/dist/validate-resource-rules.js +18 -12
  96. package/dist/validate-schema-projection.d.ts.map +1 -1
  97. package/dist/validate-schema-projection.js +33 -2
  98. package/dist/validate-template-dispatch.d.ts +27 -0
  99. package/dist/validate-template-dispatch.d.ts.map +1 -0
  100. package/dist/validate-template-dispatch.js +95 -0
  101. package/dist/with-synthetic-positions.d.ts +1 -1
  102. package/dist/with-synthetic-positions.js +1 -1
  103. package/package.json +3 -3
  104. package/src/analyzer.ts +236 -70
  105. package/src/builtins.ts +69 -16
  106. package/src/call-graph.ts +7 -1
  107. package/src/cel-environment.ts +48 -19
  108. package/src/cel-scope.ts +113 -19
  109. package/src/definition-registry.ts +78 -0
  110. package/src/dependency-graph.ts +66 -0
  111. package/src/eval-paths.ts +152 -0
  112. package/src/flatten-for-analyzer.ts +117 -4
  113. package/src/import-resolution-diagnostics.ts +1 -1
  114. package/src/index.ts +57 -4
  115. package/src/inline-imports.ts +1 -0
  116. package/src/manifest-visitor.ts +41 -1
  117. package/src/migrations/entries/index.ts +2 -0
  118. package/src/migrations/entries/schema-prepare-bucket.json +27 -0
  119. package/src/peer-binding.ts +641 -0
  120. package/src/precompile.ts +8 -0
  121. package/src/ref-slot.ts +36 -0
  122. package/src/reference-field-map.ts +42 -0
  123. package/src/referrer-rule.ts +20 -1
  124. package/src/release/index.ts +1 -1
  125. package/src/resolve-ref-sentinels.ts +12 -1
  126. package/src/resource-input.ts +132 -0
  127. package/src/resource-rule.ts +69 -7
  128. package/src/rule-condition.ts +15 -0
  129. package/src/schema-projection.ts +302 -20
  130. package/src/system-kinds.ts +7 -2
  131. package/src/telo-version.ts +1 -1
  132. package/src/template-body.ts +104 -0
  133. package/src/validate-cel-context.ts +73 -78
  134. package/src/validate-identifier-names.ts +18 -3
  135. package/src/validate-reference-forms.ts +22 -0
  136. package/src/validate-references.ts +85 -16
  137. package/src/validate-referrer-rules.ts +215 -28
  138. package/src/validate-resource-inputs.ts +367 -0
  139. package/src/validate-resource-rules.ts +26 -12
  140. package/src/validate-schema-projection.ts +50 -1
  141. package/src/validate-template-dispatch.ts +99 -0
  142. 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 wildcardIdx = stripped.indexOf("[*]");
548
- if (wildcardIdx === -1) return manifest;
549
- const arrayProp = stripped.slice(0, wildcardIdx); // e.g. "routes"
550
- const m = exprPath.match(new RegExp(`^${arrayProp}\\[(\\d+)\\]`));
551
- if (!m) return manifest;
552
- return (manifest as any)[arrayProp]?.[Number(m[1])] ?? manifest;
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
- for (const field of ["variables", "secrets", "ports"] as const) {
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" ? "variable" : field === "secrets" ? "secret" : "port";
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 { isInlineResource, resolveFieldEntries, resolveFieldValues, type RefFieldEntry } from "./reference-field-map.js";
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
- * Checks whether `kind` satisfies the ref constraint in `entry`.
16
- * Returns an empty array when valid, or mismatch error strings when not.
17
- * Returns an empty array immediately when the ref identity is not registered
18
- * (partial contextskip check rather than false-positive).
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
- // Liskov substitutability: a value satisfies the slot when it transitively
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 = (concrete.length > 0 ? concrete : [...subtypeKinds]).join(", ");
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
- `'${kind}' does not implement '${targetKind}' (known implementations: ${options})`,
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. the telo-editor's `toAnalysisManifests` walks
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