@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
@@ -35,6 +35,10 @@
35
35
  * derivation can reach.
36
36
  */
37
37
 
38
+ import type { ResourceManifest } from "@telorun/sdk";
39
+ import { isRefSentinel } from "@telorun/templating";
40
+ import { isInjectedDeclaration } from "./resource-input.js";
41
+
38
42
  /** How a kind's entry collection projects to an object schema. */
39
43
  export interface SchemaProjection {
40
44
  /** JSON Pointer, from the resource root, to the entries. */
@@ -48,6 +52,33 @@ export interface SchemaProjection {
48
52
  readonly nullable?: string;
49
53
  /** Entry field that wraps the mapped node in an array. */
50
54
  readonly array?: string;
55
+ /**
56
+ * How an entry whose keyed field holds a REFERENCE projects.
57
+ *
58
+ * The map is keyed on the field's VALUE, and a reference is not a key, so a
59
+ * `type:` holding one falls through to this path. It is declared as data by the
60
+ * backend, which is what keeps the analyzer from learning that an enum exists:
61
+ * `from` names the field of the target declaration to read, `keyword` the
62
+ * schema keyword its values become, and `base` / `baseFrom` where the node's
63
+ * own type comes from — a literal for an engine whose named type IS its own
64
+ * base, a field of the target for one that declares a storage class.
65
+ *
66
+ * A backend that declares none projects exactly as it did before.
67
+ */
68
+ readonly reference?: ProjectionReference;
69
+ }
70
+
71
+ /** The reference path of a projection — see {@link SchemaProjection.reference}. */
72
+ export interface ProjectionReference {
73
+ /** Field of the TARGET declaration whose value the keyword takes. */
74
+ readonly from: string;
75
+ /** The JSON Schema keyword those values become (`enum`). */
76
+ readonly keyword: string;
77
+ /** The node the keyword is added to, written literally. */
78
+ readonly base?: Record<string, unknown>;
79
+ /** Field of the target declaration naming a value in the kind's own
80
+ * `x-telo-schema-map`, whose mapped node is the base. */
81
+ readonly baseFrom?: string;
51
82
  }
52
83
 
53
84
  export type SchemaMap = Readonly<Record<string, Record<string, unknown>>>;
@@ -71,6 +102,19 @@ export function readSchemaProjection(definition: unknown): SchemaProjection | un
71
102
  nameField: typeof raw.name === "string" ? raw.name : undefined,
72
103
  nullable: typeof raw.nullable === "string" ? raw.nullable : undefined,
73
104
  array: typeof raw.array === "string" ? raw.array : undefined,
105
+ reference: readProjectionReference(raw.reference),
106
+ };
107
+ }
108
+
109
+ function readProjectionReference(raw: unknown): ProjectionReference | undefined {
110
+ if (!isObject(raw)) return undefined;
111
+ const { from, keyword, base, baseFrom } = raw;
112
+ if (typeof from !== "string" || typeof keyword !== "string") return undefined;
113
+ return {
114
+ from,
115
+ keyword,
116
+ base: isObject(base) ? (base as Record<string, unknown>) : undefined,
117
+ baseFrom: typeof baseFrom === "string" ? baseFrom : undefined,
74
118
  };
75
119
  }
76
120
 
@@ -94,7 +138,35 @@ export function schemaProjectionIsMisplaced(definition: unknown): boolean {
94
138
  return isObject(schema) && schema["x-telo-schema-projection"] !== undefined;
95
139
  }
96
140
 
141
+ /**
142
+ * The schema node that CARRIES the value vocabulary — the node itself, or the
143
+ * branch of a union that declares the map.
144
+ *
145
+ * A slot unioning a closed value vocabulary with a reference keeps its map on the
146
+ * value branch, exactly as the ref-slot reader peels the same union for its
147
+ * constraint. Exported because the strict half checks the map against the same
148
+ * branch's `enum`, and two implementations of "which branch is the value one"
149
+ * would eventually disagree — silently, since the failure of missing one is a
150
+ * completeness check that quietly stops running.
151
+ */
152
+ export function schemaMapBranch(node: unknown): Record<string, unknown> | undefined {
153
+ if (!isObject(node)) return undefined;
154
+ if (node["x-telo-schema-map"] !== undefined) return node;
155
+ for (const key of ["oneOf", "anyOf"] as const) {
156
+ const branches = node[key];
157
+ if (!Array.isArray(branches)) continue;
158
+ for (const branch of branches) {
159
+ if (isObject(branch) && branch["x-telo-schema-map"] !== undefined) return branch;
160
+ }
161
+ }
162
+ return undefined;
163
+ }
164
+
97
165
  export function readSchemaMap(node: unknown): SchemaMap | undefined {
166
+ return ownSchemaMap(schemaMapBranch(node));
167
+ }
168
+
169
+ function ownSchemaMap(node: unknown): SchemaMap | undefined {
98
170
  if (!isObject(node)) return undefined;
99
171
  const raw = node["x-telo-schema-map"];
100
172
  if (!isObject(raw)) return undefined;
@@ -146,6 +218,76 @@ export function projectionKeyMap(
146
218
  return readSchemaMap(entry.properties[projection.key]);
147
219
  }
148
220
 
221
+ /**
222
+ * The node an entry whose keyed field holds a REFERENCE projects to.
223
+ *
224
+ * This is the one place a projection crosses to another declaration, and it is
225
+ * a deliberate exception to the projection's lossiness: length, precision and
226
+ * collation stop at the boundary because the database enforces them, while a
227
+ * domain crosses because it IS the type at the granularity a consumer acts on —
228
+ * the enum in a CRUD model's OpenAPI operation, a completion list in the editor,
229
+ * a filter a repository can reject before the query.
230
+ *
231
+ * **A reference that cannot be read projects OPEN, never to nothing**, and that
232
+ * is the opposite of the rule an unmapped VALUE follows. The two failures are
233
+ * not the same failure: an unmapped value is a gap in the kind's own vocabulary,
234
+ * so there is no entry to speak of, while an unreadable reference names an entry
235
+ * the declaration plainly HAS and only leaves its type unknown. Dropping it made
236
+ * the projection deny the entry exists — a table whose enum reference had a typo
237
+ * reported `'status' is not allowed` against a column declared three lines up,
238
+ * blaming the seed row for the reference's mistake. Open is the honest
239
+ * under-approximation, and the reason is reported alongside.
240
+ */
241
+ function referencedNode(
242
+ value: unknown,
243
+ entryName: string,
244
+ projection: SchemaProjection,
245
+ map: SchemaMap,
246
+ options?: {
247
+ readonly scope?: ProjectionScope;
248
+ readonly pointer?: string;
249
+ readonly failures?: ProjectionFailure[];
250
+ },
251
+ ): Record<string, unknown> | undefined {
252
+ const reference = projection.reference;
253
+ if (!reference || !isObject(value)) return undefined;
254
+ // Through the single reader, so the name in the diagnostic is the one the
255
+ // author wrote whichever shape the slot holds — reading `value.name` here
256
+ // reported `<unnamed>` for an unresolved `!ref`, which is precisely the case
257
+ // that produces the diagnostic.
258
+ const name = readProjectionRef(value)?.name ?? "<unnamed>";
259
+ const report = (): Record<string, unknown> => {
260
+ options?.failures?.push({
261
+ reason: "entry-reference",
262
+ // The EMPTY pointer means the projected declaration is the one carrying
263
+ // the diagnostic, so the entry's own path is a real anchor in that file —
264
+ // the column, not the document root. Any other pointer names a slot
265
+ // holding a reference to a DIFFERENT manifest, whose entry paths mean
266
+ // nothing here, so the slot stays the anchor.
267
+ pointer:
268
+ options?.pointer === ""
269
+ ? `${projection.entries}/${entryName}`
270
+ : (options?.pointer ?? projection.entries),
271
+ entry: entryName,
272
+ name,
273
+ });
274
+ return {};
275
+ };
276
+ const found = options?.scope?.resolveManifest(value);
277
+ if (!found || "ambiguous" in found) return report();
278
+
279
+ const values = (found.manifest as Record<string, unknown>)[reference.from];
280
+ if (!Array.isArray(values) || values.length === 0) return report();
281
+
282
+ let base: Record<string, unknown> | undefined = reference.base;
283
+ if (reference.baseFrom !== undefined) {
284
+ const declared = (found.manifest as Record<string, unknown>)[reference.baseFrom];
285
+ base = typeof declared === "string" ? map[declared] : undefined;
286
+ }
287
+ if (!base) return report();
288
+ return { ...base, [reference.keyword]: values };
289
+ }
290
+
149
291
  /**
150
292
  * Project one declaration to an object schema.
151
293
  *
@@ -164,6 +306,15 @@ export function projectEntries(
164
306
  manifest: unknown,
165
307
  projection: SchemaProjection,
166
308
  map: SchemaMap,
309
+ /** What a REFERENCE at the keyed field is resolved through, and where a
310
+ * failure to resolve one is reported. A caller with no scope cannot resolve
311
+ * one, so such an entry projects OPEN — present, untyped — rather than
312
+ * vanishing from the row. */
313
+ options?: {
314
+ readonly scope?: ProjectionScope;
315
+ readonly pointer?: string;
316
+ readonly failures?: ProjectionFailure[];
317
+ },
167
318
  ): Record<string, unknown> | undefined {
168
319
  const entries = navigate(manifest, projection.entries);
169
320
  if (entries === undefined) return undefined;
@@ -172,7 +323,10 @@ export function projectEntries(
172
323
  const consider = (name: string | undefined, entry: unknown): void => {
173
324
  if (!isObject(entry) || name === undefined) return;
174
325
  const key = entry[projection.key];
175
- const mapped = typeof key === "string" ? map[key] : undefined;
326
+ const mapped =
327
+ typeof key === "string"
328
+ ? map[key]
329
+ : referencedNode(key, name, projection, map, options);
176
330
  // A value with no map entry projects to nothing rather than to `any`: the
177
331
  // vocabulary is the kind's own enum, so an unmapped value is a gap in the
178
332
  // kind's declaration, not a shape to guess at.
@@ -215,10 +369,25 @@ export interface ProjectionRef {
215
369
  readonly alias?: string;
216
370
  }
217
371
 
218
- /** The `{kind, name, alias?}` reference a value holds, or undefined. Exported so
219
- * a host whose slot may hold EITHER shape can fall back to this reading. */
372
+ /**
373
+ * The `{kind, name, alias?}` reference a value holds, or undefined. Exported so
374
+ * a host whose slot may hold EITHER shape can fall back to this reading.
375
+ *
376
+ * The unresolved `!ref` SENTINEL is read too. `resolveRefSentinels` normally
377
+ * rewrites one before this pass, but not when the reference names nothing — and
378
+ * that is exactly when a projection failure is reported, so reading only the
379
+ * resolved shape made the diagnostic name the target `<unnamed>`, which is the
380
+ * one fact the author needed from it. A round-trip host (`compile` off) carries
381
+ * the sentinel for every reference, resolved or not.
382
+ */
220
383
  export function readProjectionRef(value: unknown): ProjectionRef | undefined {
221
384
  if (!isObject(value)) return undefined;
385
+ if (isRefSentinel(value)) {
386
+ const dot = value.source.indexOf(".");
387
+ return dot > 0
388
+ ? { name: value.source.slice(dot + 1), alias: value.source.slice(0, dot) }
389
+ : { name: value.source };
390
+ }
222
391
  const name = value.name;
223
392
  if (typeof name !== "string") return undefined;
224
393
  return {
@@ -306,11 +475,51 @@ export function manifestListScope(
306
475
  };
307
476
  }
308
477
 
478
+ /**
479
+ * Why a slot could not be typed from a projection.
480
+ *
481
+ * Each reason is a DIFFERENT repair, which is why the three ways a target can
482
+ * carry no usable projection are kept apart rather than collapsed into
483
+ * `no-projection`: that one message ("declares no 'x-telo-schema-projection'")
484
+ * was printed for a kind that declares one whose key field carries no map, and
485
+ * for a declaration whose entry collection is simply absent — accusing the wrong
486
+ * author of the wrong omission in both.
487
+ */
309
488
  export type ProjectionFailure =
310
489
  | { readonly reason: "no-ref"; readonly pointer: string }
311
490
  | { readonly reason: "unresolved"; readonly pointer: string; readonly name: string }
312
491
  | { readonly reason: "ambiguous"; readonly pointer: string; readonly name: string }
313
- | { readonly reason: "no-projection"; readonly pointer: string; readonly kind: string };
492
+ /** The target's KIND declares no `x-telo-schema-projection` at all. */
493
+ | { readonly reason: "no-projection"; readonly pointer: string; readonly kind: string }
494
+ /** It declares one, but the field it keys on carries no `x-telo-schema-map`. */
495
+ | { readonly reason: "no-projection-map"; readonly pointer: string; readonly kind: string }
496
+ /** Both are declared and the DECLARATION holds no entry collection to project
497
+ * — an absent `columns:`, or a value that is not a collection. */
498
+ | {
499
+ readonly reason: "no-entries";
500
+ readonly pointer: string;
501
+ readonly kind: string;
502
+ readonly entries: string;
503
+ }
504
+ /** The slot names a resource the module does not DECLARE — a library's
505
+ * `resources:` input, standing in for an instance its importer supplies. A
506
+ * projection is DECLARATION-derived, so it cannot be answered here at all;
507
+ * the stand-in has no entries, and reporting that would tell the library
508
+ * author their block is wrong when it is correct. Resolution moves to the
509
+ * injection site, where the real declaration is. Carried as its own reason
510
+ * rather than as `no-entries` so a consumer can tell "unanswerable here"
511
+ * from "answered, and empty". */
512
+ | { readonly reason: "injected"; readonly pointer: string; readonly name: string }
513
+ /** An ENTRY of the projected declaration references a shape that could not be
514
+ * read. Reported rather than dropped: the entry would silently vanish from
515
+ * the projected row, so a consumer naming it would be told the property does
516
+ * not exist. */
517
+ | {
518
+ readonly reason: "entry-reference";
519
+ readonly pointer: string;
520
+ readonly entry: string;
521
+ readonly name: string;
522
+ };
314
523
 
315
524
  function refTarget(
316
525
  value: unknown,
@@ -325,18 +534,41 @@ function refTarget(
325
534
  if (!found) return { reason: "unresolved", pointer, name };
326
535
  if ("ambiguous" in found) return { reason: "ambiguous", pointer, name };
327
536
  const manifest = found.manifest;
537
+ if (isInjectedDeclaration(manifest as ResourceManifest)) {
538
+ return { reason: "injected", pointer, name };
539
+ }
328
540
  if (typeof manifest.kind !== "string") return { reason: "unresolved", pointer, name };
329
541
  const definition = scope.resolveDefinition(manifest.kind);
330
542
  if (!definition) return { reason: "no-projection", pointer, kind: manifest.kind };
331
543
  return { manifest, definition };
332
544
  }
333
545
 
546
+ /** The declaration the annotation is written on, as a projection target. */
547
+ function ownTarget(
548
+ manifest: Record<string, any>,
549
+ scope: ProjectionScope,
550
+ ): { manifest: Record<string, any>; definition: Record<string, any> } | ProjectionFailure {
551
+ if (typeof manifest.kind !== "string") {
552
+ return { reason: "no-ref", pointer: "" };
553
+ }
554
+ const definition = scope.resolveDefinition(manifest.kind);
555
+ if (!definition) return { reason: "no-projection", pointer: "", kind: manifest.kind };
556
+ return { manifest, definition };
557
+ }
558
+
334
559
  export function describeProjectionFailure(failure: ProjectionFailure): string {
335
560
  switch (failure.reason) {
336
561
  case "no-ref":
337
- return `'${failure.pointer}' does not hold a reference, so there is no declaration to project.`;
562
+ return failure.pointer === ""
563
+ ? "this resource declares no 'kind:', so there is no definition to project it through."
564
+ : `'${failure.pointer}' does not hold a reference, so there is no declaration to project.`;
338
565
  case "unresolved":
339
566
  return `'${failure.pointer}' references '${failure.name}', which resolves to no resource.`;
567
+ case "injected":
568
+ return (
569
+ `'${failure.pointer}' references '${failure.name}', a resource input this module does ` +
570
+ `not declare — its entries belong to whoever supplies it.`
571
+ );
340
572
  case "ambiguous":
341
573
  return (
342
574
  `'${failure.pointer}' references '${failure.name}', which matches more than one resource ` +
@@ -347,6 +579,23 @@ export function describeProjectionFailure(failure: ProjectionFailure): string {
347
579
  `'${failure.pointer}' references a resource of kind '${failure.kind}', which declares no ` +
348
580
  `'x-telo-schema-projection' — so there is nothing for this slot to be typed from.`
349
581
  );
582
+ case "no-projection-map":
583
+ return (
584
+ `kind '${failure.kind}' declares an 'x-telo-schema-projection' whose key field carries ` +
585
+ `no 'x-telo-schema-map', so there is no vocabulary to project its entries through and ` +
586
+ `'${failure.pointer || "this declaration"}' cannot be typed from it.`
587
+ );
588
+ case "no-entries":
589
+ return (
590
+ `'${failure.entries}' holds no entry collection on this ${failure.kind}, so the ` +
591
+ `projection has nothing to type '${failure.pointer || "this declaration"}' from.`
592
+ );
593
+ case "entry-reference":
594
+ return (
595
+ `entry '${failure.entry}' at '${failure.pointer}' references '${failure.name}', which ` +
596
+ `resolves to no declaration this analysis can read — so that entry is projected as an ` +
597
+ `open value and nothing typed from it is checked against the shape it was meant to have.`
598
+ );
350
599
  }
351
600
  }
352
601
 
@@ -360,6 +609,16 @@ export function describeProjectionFailure(failure: ProjectionFailure): string {
360
609
  * `failures`, because degrading SILENTLY is the failure this whole mechanism
361
610
  * exists to move earlier: the consumer's contract quietly reopens and a
362
611
  * misspelled field passes `telo check` exactly as it did before.
612
+ *
613
+ * **A node that projected NOTHING is returned by IDENTITY**, and that is a
614
+ * correctness property of the caller rather than a micro-optimization:
615
+ * `DefinitionRegistry` memoizes a compiled AJV validator per schema OBJECT,
616
+ * because every resource of a kind is checked against the same one at keystroke
617
+ * time. Rebuilding each node unconditionally — which this did — misses that memo
618
+ * for every resource in the analysis, so AJV recompiled the whole kind schema
619
+ * once per resource: on `apps/hub` that was 197 compiles instead of 54, and 723
620
+ * ms instead of 97. Returning the input where nothing changed restores it for
621
+ * every kind that declares no projection at all, which is nearly all of them.
363
622
  */
364
623
  export function resolveSchemaProjections(
365
624
  schema: unknown,
@@ -368,36 +627,59 @@ export function resolveSchemaProjections(
368
627
  failures?: ProjectionFailure[],
369
628
  ): unknown {
370
629
  if (Array.isArray(schema)) {
371
- return schema.map((item) => resolveSchemaProjections(item, manifest, scope, failures));
630
+ let moved = false;
631
+ const items = schema.map((item) => {
632
+ const next = resolveSchemaProjections(item, manifest, scope, failures);
633
+ if (next !== item) moved = true;
634
+ return next;
635
+ });
636
+ return moved ? items : schema;
372
637
  }
373
638
  if (!isObject(schema)) return schema;
374
639
 
375
640
  const pointer = readProjectionFrom(schema);
376
- if (pointer && manifest) {
377
- const target = refTarget(navigate(manifest, pointer), scope, pointer);
641
+ if (pointer !== undefined && manifest) {
642
+ // The EMPTY pointer names the declaration this annotation is written on,
643
+ // rather than a slot holding a reference to another. Resolution is skipped
644
+ // because the declaration is already in hand — which is what lets a kind
645
+ // type its own data against its own entries (a table's seed rows against its
646
+ // columns), something no reference could reach.
647
+ const target =
648
+ pointer === ""
649
+ ? ownTarget(manifest, scope)
650
+ : refTarget(navigate(manifest, pointer), scope, pointer);
378
651
  if ("reason" in target) {
379
652
  failures?.push(target);
380
653
  } else {
654
+ const kind = String(target.manifest.kind ?? "<unknown>");
381
655
  const projection = readSchemaProjection(target.definition);
382
656
  const map = projection && projectionKeyMap(target.definition.schema, projection);
383
657
  const projected =
384
- projection && map ? projectEntries(target.manifest, projection, map) : undefined;
658
+ projection && map
659
+ ? projectEntries(target.manifest, projection, map, { scope, pointer, failures })
660
+ : undefined;
385
661
  if (projected) {
386
662
  const { ["x-telo-schema-projection-from"]: _dropped, ...rest } = schema;
387
663
  return { ...rest, ...projected };
388
664
  }
389
- failures?.push({
390
- reason: "no-projection",
391
- pointer,
392
- kind: String(target.manifest.kind ?? "<unknown>"),
393
- });
665
+ // Three distinct omissions, three repairs by three different authors: the
666
+ // kind declares no projection, the kind declares one the key field has no
667
+ // vocabulary for, or this DECLARATION simply lists no entries.
668
+ if (!projection) failures?.push({ reason: "no-projection", pointer, kind });
669
+ else if (!map) failures?.push({ reason: "no-projection-map", pointer, kind });
670
+ else {
671
+ failures?.push({ reason: "no-entries", pointer, kind, entries: projection.entries });
672
+ }
394
673
  }
395
674
  }
396
675
 
397
- return Object.fromEntries(
398
- Object.entries(schema).map(([key, value]) => [
399
- key,
400
- key.startsWith("x-telo-") ? value : resolveSchemaProjections(value, manifest, scope, failures),
401
- ]),
402
- );
676
+ let moved = false;
677
+ const entries = Object.entries(schema).map(([key, value]) => {
678
+ const next = key.startsWith("x-telo-")
679
+ ? value
680
+ : resolveSchemaProjections(value, manifest, scope, failures);
681
+ if (next !== value) moved = true;
682
+ return [key, next] as const;
683
+ });
684
+ return moved ? Object.fromEntries(entries) : schema;
403
685
  }
@@ -28,8 +28,13 @@ export const DEPENDENCY_GRAPH_SKIP_KINDS: ReadonlySet<string> = new Set([
28
28
  /** Skipped by `!ref` sentinel resolution: kinds whose bodies are
29
29
  * blueprints or import-time metadata, not resource instances with
30
30
  * user-referenced ref slots. Mirrors `REF_VALIDATION_SKIP_KINDS` but
31
- * also drops Telo.Import (its `source` isn't a ref slot, and walking
32
- * the field map on it is pointless since there's no registered kind). */
31
+ * also lists Telo.Import, because walking its field map is pointless
32
+ * there is no registered kind behind it.
33
+ *
34
+ * `resolveRefSentinels` reaches Telo.Import's `resources:` block AHEAD of this
35
+ * set, deliberately: those are the references an importer supplies for a
36
+ * library's declared inputs, and they resolve exactly like any other. Nothing
37
+ * else on the document is a reference slot, so only that subtree is walked. */
33
38
  export const REF_RESOLUTION_SKIP_KINDS: ReadonlySet<string> = new Set([
34
39
  "Telo.Definition",
35
40
  "Telo.Abstract",
@@ -6,4 +6,4 @@
6
6
  // written against, and every kernel in every language reports the same scale.
7
7
 
8
8
  /** The surface generation this analyzer implements. */
9
- export const TELO_SURFACE_VERSION = "0.82.0";
9
+ export const TELO_SURFACE_VERSION = "0.83.0";
@@ -0,0 +1,104 @@
1
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
+ import type { AliasResolver } from "./alias-resolver.js";
3
+ import type { DefinitionRegistry } from "./definition-registry.js";
4
+ import type { ModuleScopes } from "./alias-resolver.js";
5
+
6
+ /**
7
+ * A `Telo.Definition`'s `resources:` entries — the bodies a kind writes for
8
+ * ANOTHER kind — read as what they are: declarations of that other kind.
9
+ *
10
+ * The consequence is the whole point. CEL inside such a body used to be typed in
11
+ * the ENCLOSING definition's scope, against one fixed permissive context
12
+ * (`self`, plus open `request` / `result` / `steps` / `error`). So `inputs` and
13
+ * `item` were undefined wherever the nested kind declares them, while `error`
14
+ * was offered everywhere regardless of whether a `catch:` was in scope — a body
15
+ * of more than one dispatch was unwritable, which is why no standard-library
16
+ * template has one.
17
+ *
18
+ * Resolving through the nested kind's OWN annotations — its `x-telo-context`
19
+ * regions, its step body, its error branches — makes a nested declaration answer
20
+ * exactly as the same declaration written at the top level does, with `self`
21
+ * merged in from the enclosing definition's `schema:`.
22
+ *
23
+ * Browser-safe.
24
+ */
25
+ export interface TemplateBody {
26
+ /** Concrete path prefix of this entry (`resources[0]`), as CEL sites are
27
+ * addressed. */
28
+ prefix: string;
29
+ /** JSONPath prefix for context scopes (`$.resources[0]`). */
30
+ scopePrefix: string;
31
+ manifest: ResourceManifest;
32
+ definition: ResourceDefinition | undefined;
33
+ }
34
+
35
+ /** The template bodies a manifest declares — empty for anything that is not a
36
+ * `Telo.Definition` with a `resources:` array. */
37
+ export function templateBodies(
38
+ m: ResourceManifest,
39
+ registry: DefinitionRegistry,
40
+ aliases: AliasResolver | undefined,
41
+ scopes: ModuleScopes | undefined,
42
+ ): TemplateBody[] {
43
+ if (m.kind !== "Telo.Definition") return [];
44
+ const bodies = (m as Record<string, unknown>).resources;
45
+ if (!Array.isArray(bodies)) return [];
46
+
47
+ // A nested kind is written through the alias scope of the module that DECLARED
48
+ // the definition, never the consumer's — the same rule every other kind
49
+ // resolution in a forwarded manifest follows.
50
+ const ownModule = (m.metadata as { module?: string } | undefined)?.module;
51
+ const scope =
52
+ (ownModule && scopes && !scopes.rootModules.has(ownModule)
53
+ ? scopes.aliasesByModule.get(ownModule)
54
+ : undefined) ?? aliases;
55
+
56
+ const out: TemplateBody[] = [];
57
+ for (let i = 0; i < bodies.length; i++) {
58
+ const body = bodies[i];
59
+ if (!body || typeof body !== "object" || Array.isArray(body)) continue;
60
+ const kind = (body as { kind?: unknown }).kind;
61
+ if (typeof kind !== "string") continue;
62
+ const canonical = scope?.resolveKind(kind);
63
+ const definition = registry.resolve(kind) ?? (canonical ? registry.resolve(canonical) : undefined);
64
+ out.push({
65
+ prefix: `resources[${i}]`,
66
+ scopePrefix: `$.resources[${i}]`,
67
+ manifest: body as ResourceManifest,
68
+ definition,
69
+ });
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** True when a CEL path lies at or inside a body's own subtree. */
75
+ export function pathInBody(path: string, prefix: string): boolean {
76
+ return (
77
+ path === prefix || path.startsWith(`${prefix}.`) || path.startsWith(`${prefix}[`)
78
+ );
79
+ }
80
+
81
+ /** The body a CEL path belongs to, or undefined for a path in the definition's
82
+ * own fields. Generic over what the caller keyed to each body — the resolver
83
+ * carries a step context and an error map, the visitor carries the schema. */
84
+ export function bodyForPath<T extends { prefix: string }>(
85
+ bodies: readonly T[],
86
+ path: string,
87
+ ): T | undefined {
88
+ return bodies.find((b) => pathInBody(path, b.prefix));
89
+ }
90
+
91
+ /** `self` is in scope throughout a template body — it is how the body reaches
92
+ * the configuration its enclosing kind was given — so it is merged into every
93
+ * context the nested kind declares, which knows nothing about it. Merged UNDER
94
+ * the nested kind's own properties: a name the nested kind declares wins, since
95
+ * that is what its controller will bind. */
96
+ export function withTemplateSelf(contextSchema: Record<string, any>): Record<string, any> {
97
+ return {
98
+ ...contextSchema,
99
+ properties: {
100
+ self: { "x-telo-context-from-root": "schema" },
101
+ ...(contextSchema.properties ?? {}),
102
+ },
103
+ };
104
+ }