@telorun/analyzer 0.72.0 → 0.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +76 -20
  3. package/dist/artifact-axes.d.ts +14 -0
  4. package/dist/artifact-axes.d.ts.map +1 -0
  5. package/dist/artifact-axes.js +12 -0
  6. package/dist/artifact-layer-index.d.ts +2 -2
  7. package/dist/artifact-layer-index.d.ts.map +1 -1
  8. package/dist/artifact-layer-index.js +9 -4
  9. package/dist/artifact-selector.d.ts +47 -21
  10. package/dist/artifact-selector.d.ts.map +1 -1
  11. package/dist/artifact-selector.js +85 -16
  12. package/dist/builtins.d.ts.map +1 -1
  13. package/dist/builtins.js +144 -113
  14. package/dist/call-graph.d.ts +12 -1
  15. package/dist/call-graph.d.ts.map +1 -1
  16. package/dist/call-graph.js +22 -0
  17. package/dist/catch-scope.js +1 -1
  18. package/dist/cel-scope-query.d.ts.map +1 -1
  19. package/dist/cel-scope-query.js +32 -11
  20. package/dist/cel-scope.d.ts.map +1 -1
  21. package/dist/cel-scope.js +1 -0
  22. package/dist/eval-paths.d.ts +36 -5
  23. package/dist/eval-paths.d.ts.map +1 -1
  24. package/dist/eval-paths.js +47 -6
  25. package/dist/extends-resolution.d.ts +12 -0
  26. package/dist/extends-resolution.d.ts.map +1 -1
  27. package/dist/extends-resolution.js +23 -0
  28. package/dist/forwarded-declaration.d.ts +26 -0
  29. package/dist/forwarded-declaration.d.ts.map +1 -0
  30. package/dist/forwarded-declaration.js +31 -0
  31. package/dist/index.d.ts +10 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5 -2
  34. package/dist/kernel-globals.d.ts.map +1 -1
  35. package/dist/kernel-globals.js +21 -5
  36. package/dist/module-alias-scope.d.ts +11 -0
  37. package/dist/module-alias-scope.d.ts.map +1 -1
  38. package/dist/module-alias-scope.js +10 -0
  39. package/dist/module-file-claims.d.ts +17 -1
  40. package/dist/module-file-claims.d.ts.map +1 -1
  41. package/dist/module-file-claims.js +38 -3
  42. package/dist/module-named-files.d.ts +72 -0
  43. package/dist/module-named-files.d.ts.map +1 -0
  44. package/dist/module-named-files.js +115 -0
  45. package/dist/native-entries.d.ts +83 -0
  46. package/dist/native-entries.d.ts.map +1 -0
  47. package/dist/native-entries.js +169 -0
  48. package/dist/nearest-name.d.ts +22 -0
  49. package/dist/nearest-name.d.ts.map +1 -0
  50. package/dist/nearest-name.js +43 -0
  51. package/dist/normalize-inline-resources.d.ts +41 -13
  52. package/dist/normalize-inline-resources.d.ts.map +1 -1
  53. package/dist/normalize-inline-resources.js +236 -54
  54. package/dist/reference-field-map.d.ts +59 -1
  55. package/dist/reference-field-map.d.ts.map +1 -1
  56. package/dist/reference-field-map.js +200 -34
  57. package/dist/release/destinations.d.ts +59 -0
  58. package/dist/release/destinations.d.ts.map +1 -0
  59. package/dist/release/destinations.js +75 -0
  60. package/dist/release/index.d.ts +6 -2
  61. package/dist/release/index.d.ts.map +1 -1
  62. package/dist/release/index.js +3 -1
  63. package/dist/release/ledger.d.ts +20 -11
  64. package/dist/release/ledger.d.ts.map +1 -1
  65. package/dist/release/ledger.js +43 -14
  66. package/dist/release/payload-digest.d.ts +2 -1
  67. package/dist/release/payload-digest.d.ts.map +1 -1
  68. package/dist/release/payload-digest.js +2 -1
  69. package/dist/release/release-plan.d.ts +3 -2
  70. package/dist/release/release-plan.d.ts.map +1 -1
  71. package/dist/release/release-plan.js +12 -19
  72. package/dist/release/workspace-config.d.ts +114 -34
  73. package/dist/release/workspace-config.d.ts.map +1 -1
  74. package/dist/release/workspace-config.js +322 -38
  75. package/dist/release/workspace-schema.d.ts +41 -0
  76. package/dist/release/workspace-schema.d.ts.map +1 -0
  77. package/dist/release/workspace-schema.js +77 -0
  78. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  79. package/dist/resolve-ref-sentinels.js +8 -5
  80. package/dist/resolve-throws-union.d.ts +25 -4
  81. package/dist/resolve-throws-union.d.ts.map +1 -1
  82. package/dist/resolve-throws-union.js +62 -32
  83. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  84. package/dist/resolve-zone-requirements.js +2 -1
  85. package/dist/rewrite-synthetic-origins.d.ts +6 -1
  86. package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
  87. package/dist/rewrite-synthetic-origins.js +107 -13
  88. package/dist/schema-compat.d.ts.map +1 -1
  89. package/dist/schema-compat.js +18 -5
  90. package/dist/schema-error-report.d.ts +7 -0
  91. package/dist/schema-error-report.d.ts.map +1 -1
  92. package/dist/schema-error-report.js +9 -4
  93. package/dist/schema-walk.d.ts +47 -13
  94. package/dist/schema-walk.d.ts.map +1 -1
  95. package/dist/schema-walk.js +150 -30
  96. package/dist/scope-declarations.d.ts +59 -0
  97. package/dist/scope-declarations.d.ts.map +1 -0
  98. package/dist/scope-declarations.js +45 -0
  99. package/dist/source-entries.d.ts +119 -0
  100. package/dist/source-entries.d.ts.map +1 -0
  101. package/dist/source-entries.js +349 -0
  102. package/dist/step-bodies.d.ts +24 -0
  103. package/dist/step-bodies.d.ts.map +1 -0
  104. package/dist/step-bodies.js +28 -0
  105. package/dist/telo-version.d.ts +1 -1
  106. package/dist/telo-version.js +1 -1
  107. package/dist/template-body.d.ts +10 -0
  108. package/dist/template-body.d.ts.map +1 -1
  109. package/dist/template-body.js +25 -0
  110. package/dist/validate-cel-context.d.ts +23 -8
  111. package/dist/validate-cel-context.d.ts.map +1 -1
  112. package/dist/validate-cel-context.js +51 -18
  113. package/dist/validate-exports.d.ts +28 -0
  114. package/dist/validate-exports.d.ts.map +1 -0
  115. package/dist/validate-exports.js +146 -0
  116. package/dist/validate-extends.d.ts.map +1 -1
  117. package/dist/validate-extends.js +37 -1
  118. package/dist/validate-module-artifact.d.ts +4 -2
  119. package/dist/validate-module-artifact.d.ts.map +1 -1
  120. package/dist/validate-module-artifact.js +43 -4
  121. package/dist/validate-native-entries.d.ts +15 -0
  122. package/dist/validate-native-entries.d.ts.map +1 -0
  123. package/dist/validate-native-entries.js +103 -0
  124. package/dist/validate-observed-state.d.ts +7 -1
  125. package/dist/validate-observed-state.d.ts.map +1 -1
  126. package/dist/validate-observed-state.js +24 -6
  127. package/dist/validate-provider-coherence.d.ts +4 -8
  128. package/dist/validate-provider-coherence.d.ts.map +1 -1
  129. package/dist/validate-provider-coherence.js +12 -123
  130. package/dist/validate-reference-forms.d.ts.map +1 -1
  131. package/dist/validate-reference-forms.js +2 -2
  132. package/dist/validate-references.d.ts.map +1 -1
  133. package/dist/validate-references.js +152 -65
  134. package/dist/validate-scope-reach.d.ts +31 -0
  135. package/dist/validate-scope-reach.d.ts.map +1 -0
  136. package/dist/validate-scope-reach.js +180 -0
  137. package/dist/validate-source-entries.d.ts +16 -0
  138. package/dist/validate-source-entries.d.ts.map +1 -0
  139. package/dist/validate-source-entries.js +90 -0
  140. package/dist/validate-template-body.d.ts +36 -0
  141. package/dist/validate-template-body.d.ts.map +1 -0
  142. package/dist/validate-template-body.js +246 -0
  143. package/dist/validate-throws-coverage.d.ts.map +1 -1
  144. package/dist/validate-throws-coverage.js +21 -39
  145. package/dist/value-type-keyword.d.ts +22 -5
  146. package/dist/value-type-keyword.d.ts.map +1 -1
  147. package/dist/value-type-keyword.js +16 -7
  148. package/package.json +4 -3
  149. package/src/analyzer.ts +97 -20
  150. package/src/artifact-axes.ts +24 -0
  151. package/src/artifact-layer-index.ts +9 -6
  152. package/src/artifact-selector.ts +110 -35
  153. package/src/builtins.ts +160 -113
  154. package/src/call-graph.ts +30 -0
  155. package/src/catch-scope.ts +1 -1
  156. package/src/cel-scope-query.ts +28 -9
  157. package/src/cel-scope.ts +1 -0
  158. package/src/eval-paths.ts +53 -6
  159. package/src/extends-resolution.ts +24 -0
  160. package/src/forwarded-declaration.ts +32 -0
  161. package/src/index.ts +37 -1
  162. package/src/kernel-globals.ts +28 -5
  163. package/src/migrations/CLAUDE.md +35 -0
  164. package/src/module-alias-scope.ts +17 -0
  165. package/src/module-file-claims.ts +63 -4
  166. package/src/module-named-files.ts +162 -0
  167. package/src/native-entries.ts +227 -0
  168. package/src/nearest-name.ts +47 -0
  169. package/src/normalize-inline-resources.ts +326 -70
  170. package/src/reference-field-map.ts +257 -52
  171. package/src/release/CLAUDE.md +32 -0
  172. package/src/release/destinations.ts +105 -0
  173. package/src/release/index.ts +33 -2
  174. package/src/release/ledger.ts +51 -21
  175. package/src/release/payload-digest.ts +2 -1
  176. package/src/release/release-plan.ts +16 -26
  177. package/src/release/workspace-config.ts +483 -47
  178. package/src/release/workspace-schema.ts +99 -0
  179. package/src/resolve-ref-sentinels.ts +7 -6
  180. package/src/resolve-throws-union.ts +87 -32
  181. package/src/resolve-zone-requirements.ts +2 -1
  182. package/src/rewrite-synthetic-origins.ts +107 -16
  183. package/src/schema-compat.ts +18 -4
  184. package/src/schema-error-report.ts +16 -4
  185. package/src/schema-walk.ts +162 -34
  186. package/src/scope-declarations.ts +98 -0
  187. package/src/source-entries.ts +502 -0
  188. package/src/step-bodies.ts +55 -0
  189. package/src/telo-version.ts +1 -1
  190. package/src/template-body.ts +25 -0
  191. package/src/validate-cel-context.ts +60 -18
  192. package/src/validate-exports.ts +185 -0
  193. package/src/validate-extends.ts +43 -0
  194. package/src/validate-module-artifact.ts +46 -3
  195. package/src/validate-native-entries.ts +147 -0
  196. package/src/validate-observed-state.ts +36 -7
  197. package/src/validate-provider-coherence.ts +17 -127
  198. package/src/validate-reference-forms.ts +2 -3
  199. package/src/validate-references.ts +151 -66
  200. package/src/validate-scope-reach.ts +213 -0
  201. package/src/validate-source-entries.ts +120 -0
  202. package/src/validate-template-body.ts +274 -0
  203. package/src/validate-throws-coverage.ts +24 -31
  204. package/src/value-type-keyword.ts +35 -9
  205. package/dist/validate-template-dispatch.d.ts +0 -27
  206. package/dist/validate-template-dispatch.d.ts.map +0 -1
  207. package/dist/validate-template-dispatch.js +0 -95
  208. package/src/validate-template-dispatch.ts +0 -99
@@ -7,6 +7,7 @@ import {
7
7
  type ReferenceFieldMap,
8
8
  } from "./reference-field-map.js";
9
9
  import { REF_RESOLUTION_SKIP_KINDS as SYSTEM_KINDS } from "./system-kinds.js";
10
+ import { isForwardedDeclaration, isForwardedExport } from "./forwarded-declaration.js";
10
11
 
11
12
  /** The slice of the definition registry this pass needs: a kind's field map, from
12
13
  * which the `x-telo-scope` slots are read. */
@@ -73,10 +74,10 @@ export function resolveRefSentinels(
73
74
  ): void {
74
75
  const moduleOf = (r: ResourceManifest): string | undefined =>
75
76
  (r.metadata as { module?: string } | undefined)?.module;
76
- // Forwarded exports are flagged by flattenForAnalyzer (`metadata.forwardedExport`); they're
77
- // cross-module resolution targets only never walked as local ref sources here.
78
- const isForeign = (r: ResourceManifest): boolean =>
79
- (r.metadata as { forwardedExport?: boolean } | undefined)?.forwardedExport === true;
77
+ // A dependency's code a forwarded export, or what extraction pulled out of one — is
78
+ // never walked as a local ref source here and never a local name; only an EXPORT is a
79
+ // cross-module resolution target.
80
+ const isForeign = isForwardedDeclaration;
80
81
 
81
82
  // Local resources resolve a bare / `Self.`-qualified name; forwarded foreign exports
82
83
  // resolve an `Alias.`-qualified name keyed by (module, name).
@@ -86,13 +87,13 @@ export function resolveRefSentinels(
86
87
  if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind)) continue;
87
88
  const name = r.metadata.name as string;
88
89
  if (isForeign(r)) {
89
- byModuleName.set(`${moduleOf(r)}\0${name}`, r);
90
+ if (isForwardedExport(r)) byModuleName.set(`${moduleOf(r)}\0${name}`, r);
90
91
  } else {
91
92
  byName.set(name, r);
92
93
  }
93
94
  }
94
95
  for (const r of crossModuleTargets) {
95
- if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForeign(r)) continue;
96
+ if (!r.metadata?.name || SYSTEM_KINDS.has(r.kind) || !isForwardedExport(r)) continue;
96
97
  byModuleName.set(`${moduleOf(r)}\0${r.metadata.name as string}`, r);
97
98
  }
98
99
 
@@ -1,12 +1,18 @@
1
1
  import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
2
  import { isTaggedSentinel } from "@telorun/templating";
3
3
  import { scopeResolverForModule, type AliasResolver } from "./alias-resolver.js";
4
- import { resolveScopedName } from "./call-graph.js";
4
+ import { resolveScopedName, resolveSlotUseAt } from "./call-graph.js";
5
5
  import { refSentinelTarget, type RefSentinelTarget } from "./ref-sentinel-target.js";
6
6
  import type { DefinitionRegistry } from "./definition-registry.js";
7
- import { possibleUses, readRefSlot, transfersControl, type RefSlot } from "./ref-slot.js";
8
- import { readStepSlot } from "./step-slot.js";
7
+ import {
8
+ hasDeclaredUse,
9
+ possibleUses,
10
+ transfersControl,
11
+ type RefSlot,
12
+ type RefUse,
13
+ } from "./ref-slot.js";
9
14
  import { forEachDrivenSlot } from "./schema-walk.js";
15
+ import type { StepSlot } from "./step-slot.js";
10
16
 
11
17
  export interface ThrowsCodeMeta {
12
18
  data?: Record<string, any>;
@@ -207,10 +213,10 @@ function codesFromDefinition(definition: ResourceDefinition): Map<string, Throws
207
213
  }
208
214
 
209
215
  /** Resolve the effective throw union for a named manifest. The result combines
210
- * explicit `throws.codes`, `throws.inherit: true` dataflow (step-context
211
- * traversal with try/catch subtraction), and unbounded markers for
212
- * unresolvable passthrough call sites. Cycles short-circuit to an empty
213
- * result so resolution always terminates. */
216
+ * explicit `throws.codes`, `throws.inherit: true` dataflow (step bodies with
217
+ * try/catch subtraction, plus reference slots that hand a failure back), and
218
+ * unbounded markers for unresolvable passthrough call sites. Cycles
219
+ * short-circuit to an empty result so resolution always terminates. */
214
220
  export function resolveThrowsUnion(
215
221
  manifest: ResourceManifest,
216
222
  ctx: ResolveCtx,
@@ -266,14 +272,42 @@ export function resolveThrowsUnion(
266
272
  }
267
273
 
268
274
  /**
269
- * `throws.inherit: true` the union a composer's own STEP BODIES reach.
275
+ * True for a use through which a target's failure reaches the declaring
276
+ * resource's CALLER: `call` returns into my invocation, and `trigger.consumer`
277
+ * rejects the value my caller drains. `detached` and `trigger.inbound` run where
278
+ * no caller of mine awaits them; `dependency` and `schema` dispatch nothing.
270
279
  *
271
- * Deliberately steps only, and not every slot the resource drives: `inherit` is
272
- * a DECLARATION that a kind's union is the union of what it dispatches, and a
273
- * kind that does not make that claim must not have it inferred — a kind holding
274
- * a `call` ref it catches internally would silently gain codes it never lets
275
- * escape. What a CATCH SCOPE needs is a different question with a different
276
- * answer, and it has its own resolver below.
280
+ * The reduction `inherit` is read through by the resolver and by the check
281
+ * that a definition declaring it has something to inherit from.
282
+ */
283
+ export function handsFailureBack(use: RefUse): boolean {
284
+ return use === "call" || use === "trigger.consumer";
285
+ }
286
+
287
+ /**
288
+ * The uses a throws consumer reads off a slot. A slot that declares no use —
289
+ * the legacy bare-string form — reads as `call`: the throws union must assume
290
+ * `call` to keep an error path, the direction the call graph takes for the same
291
+ * slot. The zone projection reads it the opposite way, deliberately.
292
+ *
293
+ * `uses` defaults to every use the slot can take; a consumer that resolved the
294
+ * slot's case map for one instance passes that answer instead.
295
+ */
296
+ export function throwsUses(
297
+ slot: RefSlot,
298
+ uses: readonly RefUse[] = possibleUses(slot),
299
+ ): readonly RefUse[] {
300
+ return hasDeclaredUse(slot) ? uses : ["call"];
301
+ }
302
+
303
+ /**
304
+ * `throws.inherit: true` — the union of what the resource dispatches: its step
305
+ * bodies (try/catch subtraction included) and every reference slot whose use,
306
+ * resolved for THIS instance, hands a failure back ({@link handsFailureBack}).
307
+ *
308
+ * Only on a kind that DECLARES `inherit`: it is a claim that the kind lets what
309
+ * it dispatches escape, and a kind catching a slot internally does not make it.
310
+ * What a CATCH SCOPE encloses is a different question, answered below.
277
311
  */
278
312
  function resolveInherited(
279
313
  manifest: ResourceManifest,
@@ -282,16 +316,23 @@ function resolveInherited(
282
316
  ownerModule: string | undefined,
283
317
  ): ThrowsUnion {
284
318
  const result: ThrowsUnion = { codes: new Map(), unbounded: false };
285
- const props = definition.schema?.properties as Record<string, any> | undefined;
286
- if (!props) return result;
287
-
288
- for (const [fieldName, fieldSchema] of Object.entries(props)) {
289
- const stepCtx = readStepSlot(fieldSchema);
290
- if (!stepCtx) continue;
291
- const steps = (manifest as Record<string, any>)[fieldName];
292
- if (!Array.isArray(steps)) continue;
293
- unionInto(result, collectStepArrayThrows(steps, stepCtx.invoke, undefined, ctx, ownerModule));
294
- }
319
+ const schema = definition.schema as Record<string, any> | undefined;
320
+
321
+ forEachDrivenSlot(schema, manifest, (driven) => {
322
+ if (driven.kind === "step") {
323
+ unionInto(result, stepSiteThrows(driven.data, driven.slots, ctx, ownerModule));
324
+ return;
325
+ }
326
+ // Several slots at one site are the branches declaring it; any one that
327
+ // hands a failure back counts, since a branch not applying can only add codes.
328
+ const handsBack = driven.slots.some(({ slot, fieldPath }) =>
329
+ throwsUses(
330
+ slot,
331
+ resolveSlotUseAt(slot, manifest, schema, driven.path, fieldPath).use,
332
+ ).some(handsFailureBack),
333
+ );
334
+ if (handsBack) unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
335
+ });
295
336
 
296
337
  return result;
297
338
  }
@@ -327,13 +368,15 @@ export function resolveScopeUnion(
327
368
 
328
369
  forEachDrivenSlot(definition.schema, manifest, (driven) => {
329
370
  if (driven.kind === "step") {
330
- unionInto(
331
- result,
332
- collectStepArrayThrows(driven.data, driven.slot.invoke, undefined, ctx, ownerModule),
333
- );
371
+ unionInto(result, stepSiteThrows(driven.data, driven.slots, ctx, ownerModule));
334
372
  return;
335
373
  }
336
- if (driven.slot.throwsThrough) {
374
+ if (
375
+ driven.slots.some(({ slot }) => !slot.throwsThrough && throwsUses(slot).some(transfersControl))
376
+ ) {
377
+ unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
378
+ }
379
+ if (driven.slots.some(({ slot }) => slot.throwsThrough)) {
337
380
  const target = resolveRefManifest(driven.data, ctx, ownerModule);
338
381
  const targetDef = target
339
382
  ? definitionFor(
@@ -347,15 +390,27 @@ export function resolveScopeUnion(
347
390
  // A target that cannot be resolved says nothing about what it throws, so
348
391
  // the scope's union is no longer enumerable.
349
392
  else result.unbounded = true;
350
- return;
351
393
  }
352
- if (!possibleUses(driven.slot).some(transfersControl)) return;
353
- unionInto(result, resolveRefTargetThrows(driven.data, ctx, ownerModule));
354
394
  });
355
395
 
356
396
  return result;
357
397
  }
358
398
 
399
+ /** The union a step-body site reaches — once per distinct invoke field, since
400
+ * several branches may declare a body at one site. */
401
+ function stepSiteThrows(
402
+ steps: unknown[],
403
+ slots: StepSlot[],
404
+ ctx: ResolveCtx,
405
+ ownerModule: string | undefined,
406
+ ): ThrowsUnion {
407
+ const result = emptyUnion();
408
+ for (const invoke of new Set(slots.map((slot) => slot.invoke))) {
409
+ unionInto(result, collectStepArrayThrows(steps, invoke, undefined, ctx, ownerModule));
410
+ }
411
+ return result;
412
+ }
413
+
359
414
  function collectStepArrayThrows(
360
415
  steps: unknown[],
361
416
  invokeField: string,
@@ -32,6 +32,7 @@ import type { ModuleDocuments } from "./module-documents.js";
32
32
  import { readProvidesZone, readRequiresZone } from "./zone-slot.js";
33
33
  import { DiagnosticSeverity, type AnalysisDiagnostic } from "./types.js";
34
34
  import { moduleAliasScope } from "./module-alias-scope.js";
35
+ import { isForwardedDeclaration } from "./forwarded-declaration.js";
35
36
 
36
37
  const SOURCE = "telo-analyzer";
37
38
 
@@ -616,7 +617,7 @@ export function projectZoneRequirements(args: ProjectionArgs): ProjectionResult
616
617
  // (with the internal graph in hand) and seeded below — deriving them here
617
618
  // against the flattened view would resolve correlation against a graph
618
619
  // that no longer holds the library's internals.
619
- if (meta?.forwardedExport) continue;
620
+ if (isForwardedDeclaration(node.manifest)) continue;
620
621
  const def = resolveDef(node.kind, meta?.module);
621
622
  const schema = def?.schema as Record<string, any> | undefined;
622
623
  if (!def || !schema) continue;
@@ -7,9 +7,9 @@ interface XTeloOrigin {
7
7
  pathFromParent: string;
8
8
  }
9
9
 
10
- function readOrigin(manifest: ResourceManifest | undefined): XTeloOrigin | undefined {
11
- if (!manifest) return undefined;
12
- const origin = (manifest.metadata as { xTeloOrigin?: XTeloOrigin } | undefined)?.xTeloOrigin;
10
+ function readOrigin(manifest: unknown): XTeloOrigin | undefined {
11
+ if (!manifest || typeof manifest !== "object") return undefined;
12
+ const origin = (manifest as { metadata?: { xTeloOrigin?: XTeloOrigin } }).metadata?.xTeloOrigin;
13
13
  if (
14
14
  !origin ||
15
15
  typeof origin.parentKind !== "string" ||
@@ -26,7 +26,12 @@ function readOrigin(manifest: ResourceManifest | undefined): XTeloOrigin | undef
26
26
  * `data.resource`, which has no YAML source. Rewrite each such diagnostic
27
27
  * back to the chain root: walk up `metadata.xTeloOrigin` until a manifest
28
28
  * with no origin is reached, and prepend each hop's `pathFromParent` to
29
- * `data.path` so position-index lookups against the root doc resolve. */
29
+ * `data.path` so position-index lookups against the root doc resolve.
30
+ *
31
+ * An extraction from a scope member lives INSIDE the scope array rather than at
32
+ * the top level, so a diagnostic about it names the top-level owner and a path
33
+ * through an array index the author never wrote (`with[3]`). That prefix is
34
+ * unfolded the same way, into the position the declaration was written at. */
30
35
  export function rewriteSyntheticOrigins(
31
36
  diagnostics: AnalysisDiagnostic[],
32
37
  manifests: ResourceManifest[],
@@ -45,31 +50,117 @@ export function rewriteSyntheticOrigins(
45
50
 
46
51
  let current = byName.get(data.resource.name);
47
52
  let origin = readOrigin(current);
48
- if (!origin) return d;
49
-
50
- let accumPath = typeof data.path === "string" ? data.path : "";
51
- let rootKind: string = origin.parentKind;
52
- let rootName: string = origin.parentName;
53
+ const path = typeof data.path === "string" ? data.path : "";
54
+ let accumPath = path;
55
+ let root: { kind?: string; name?: string } = data.resource;
53
56
 
54
57
  while (origin) {
55
58
  accumPath = accumPath ? `${origin.pathFromParent}.${accumPath}` : origin.pathFromParent;
56
- rootKind = origin.parentKind;
57
- rootName = origin.parentName;
59
+ root = { kind: origin.parentKind, name: origin.parentName };
58
60
  current = byName.get(origin.parentName);
59
61
  origin = readOrigin(current);
60
62
  }
61
63
 
62
- const rootFilePath =
63
- (current?.metadata as { source?: string } | undefined)?.source ?? data.filePath;
64
+ const rerouted = root !== data.resource;
65
+ const unfolded = current ? unfoldScopedOrigins(current, accumPath) : accumPath;
66
+ if (!rerouted && unfolded === path) return d;
64
67
 
65
68
  return {
66
69
  ...d,
67
70
  data: {
68
71
  ...data,
69
- resource: { kind: rootKind, name: rootName },
70
- filePath: rootFilePath,
71
- path: accumPath,
72
+ resource: root,
73
+ filePath: rerouted
74
+ ? ((current?.metadata as { source?: string } | undefined)?.source ?? data.filePath)
75
+ : data.filePath,
76
+ path: unfolded,
72
77
  },
73
78
  };
74
79
  });
75
80
  }
81
+
82
+ type Segment = string | number;
83
+
84
+ /** Replace every scope-array index that holds an extraction with the position
85
+ * the extraction was written at, until none is left. */
86
+ function unfoldScopedOrigins(root: ResourceManifest, path: string): string {
87
+ let segments = parseSegments(path);
88
+ let unfoldedAny = false;
89
+ // Each unfolding replaces one extraction with its parent, which is strictly
90
+ // closer to what the author wrote, so the chain is as long as the nesting.
91
+ for (let hops = 0; hops <= segments.length + 16; hops++) {
92
+ const next = unfoldOnce(root, segments);
93
+ if (!next) break;
94
+ segments = next;
95
+ unfoldedAny = true;
96
+ }
97
+ // Re-formatting is lossy (an empty segment in `entries../x` is dropped), so a
98
+ // path nothing unfolded is returned as written.
99
+ return unfoldedAny ? formatSegments(segments) : path;
100
+ }
101
+
102
+ function unfoldOnce(root: ResourceManifest, segments: Segment[]): Segment[] | undefined {
103
+ let node: unknown = root;
104
+ for (let i = 0; i < segments.length; i++) {
105
+ const segment = segments[i];
106
+ const container = node;
107
+ node =
108
+ typeof segment === "number"
109
+ ? Array.isArray(container)
110
+ ? container[segment]
111
+ : undefined
112
+ : container && typeof container === "object" && !Array.isArray(container)
113
+ ? (container as Record<string, unknown>)[segment]
114
+ : undefined;
115
+ if (node === undefined || node === null) return undefined;
116
+ if (typeof segment !== "number" || i === 0) continue;
117
+ const origin = readOrigin(node);
118
+ if (!origin) continue;
119
+
120
+ // Its parent is either the resource holding this scope array (a slot of
121
+ // the resource that declares the scope) or a sibling member of the array.
122
+ const holderPath = segments.slice(0, i - 1);
123
+ let parentPath: Segment[] | undefined;
124
+ if (nameOf(at(root, holderPath)) === origin.parentName) {
125
+ parentPath = holderPath;
126
+ } else {
127
+ const sibling = (container as unknown[]).findIndex((m) => nameOf(m) === origin.parentName);
128
+ if (sibling >= 0) parentPath = [...segments.slice(0, i), sibling];
129
+ }
130
+ if (!parentPath) return undefined;
131
+ return [...parentPath, ...parseSegments(origin.pathFromParent), ...segments.slice(i + 1)];
132
+ }
133
+ return undefined;
134
+ }
135
+
136
+ function at(root: unknown, segments: Segment[]): unknown {
137
+ let node = root;
138
+ for (const segment of segments) {
139
+ if (!node || typeof node !== "object") return undefined;
140
+ node = (node as Record<string | number, unknown>)[segment];
141
+ }
142
+ return node;
143
+ }
144
+
145
+ function nameOf(value: unknown): string | undefined {
146
+ const name = (value as { metadata?: { name?: unknown } } | undefined)?.metadata?.name;
147
+ return typeof name === "string" ? name : undefined;
148
+ }
149
+
150
+ /** `with[3].steps[0].invoke` → `["with", 3, "steps", 0, "invoke"]`. */
151
+ function parseSegments(path: string): Segment[] {
152
+ const out: Segment[] = [];
153
+ for (const match of path.matchAll(/\[(\d+)\]|([^.[\]]+)/g)) {
154
+ out.push(match[1] !== undefined ? Number(match[1]) : match[2]!);
155
+ }
156
+ return out;
157
+ }
158
+
159
+ function formatSegments(segments: Segment[]): string {
160
+ let out = "";
161
+ for (const segment of segments) {
162
+ if (typeof segment === "number") out += `[${segment}]`;
163
+ else out += out ? `.${segment}` : segment;
164
+ }
165
+ return out;
166
+ }
@@ -10,6 +10,7 @@ import {
10
10
  celTypeOfValueType,
11
11
  isCompiledValue,
12
12
  readValueTypeSlot,
13
+ VALUE_TYPE_BINDINGS,
13
14
  valueBrandBases,
14
15
  valueTypeOf,
15
16
  valueTypePlaceholder,
@@ -36,8 +37,9 @@ export function createAjv(): InstanceType<typeof Ajv> {
36
37
  // One registration site for every Telo keyword — the annotations as no-ops and
37
38
  // `x-telo-type` as the one that checks. Registered here and in the kernel's
38
39
  // validators from one definition, so a literal at an instance-typed slot is
39
- // rejected statically and at dispatch by the identical rule.
40
- registerTeloKeywords(instance);
40
+ // rejected statically and at dispatch by the identical rule. This instance is
41
+ // static analysis alone, so it also asserts a `live` type: no literal can be one.
42
+ registerTeloKeywords(instance, { assertLive: true });
41
43
  instance.addSchema(ManifestRootSchema);
42
44
  return instance;
43
45
  }
@@ -508,6 +510,18 @@ function foldedConstraints(schema: Record<string, any>): Record<string, any> {
508
510
  return out;
509
511
  }
510
512
 
513
+ /** The stand-in for a CEL leaf at a `live` slot. Static analysis asserts a live
514
+ * type like any instance type (a literal can never be one), so the leaf's
515
+ * stand-in has to BE one. A live binding declares no factory, and building an
516
+ * instance would mean knowing its constructor's signature; an object on the
517
+ * constructor's prototype satisfies `instanceof` and is never used as one. */
518
+ function liveValuePlaceholder(schema: Record<string, any>): unknown | undefined {
519
+ const entry = readValueTypeSlot(schema)?.entry;
520
+ if (!entry?.live || entry.representation !== "instance") return undefined;
521
+ const binding = VALUE_TYPE_BINDINGS[entry.binding!];
522
+ return binding ? Object.create(binding.constructor.prototype) : undefined;
523
+ }
524
+
511
525
  export function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown {
512
526
  const schema = foldedConstraints(rawSchema);
513
527
  // An instance-typed slot's placeholder must BE an instance: the same keyword
@@ -516,8 +530,8 @@ export function celPlaceholderForSchema(rawSchema: Record<string, any>): unknown
516
530
  // rejected because no YAML literal is a byte buffer, while a value arriving by
517
531
  // reference passes. The stand-in comes from the binding table, so a new
518
532
  // instance type brings its own rather than adding a branch here; a `live` type
519
- // declares none, because nothing validates it.
520
- const placeholder = valueTypePlaceholder(schema);
533
+ // declares none, because nothing validates it at dispatch.
534
+ const placeholder = valueTypePlaceholder(schema) ?? liveValuePlaceholder(schema);
521
535
  if (placeholder !== undefined) return placeholder;
522
536
  if (schema.default !== undefined) return schema.default;
523
537
  // An enum-constrained field needs a placeholder drawn from the enum: the
@@ -44,6 +44,13 @@ export interface SchemaIssue {
44
44
  message: string;
45
45
  /** Dotted path to the field (e.g. "config.handler"). Empty string means root. */
46
46
  path: string;
47
+ /** The AJV keyword that produced it, so a consumer can key on WHAT failed
48
+ * rather than on how the sentence reads. Prose is the renderer's to change;
49
+ * a caller matching on it breaks silently when it does, and mis-fires on any
50
+ * other issue whose text happens to quote the same name. */
51
+ keyword?: string;
52
+ /** For `required`, the property that is missing. */
53
+ missingProperty?: string;
47
54
  }
48
55
 
49
56
  const UNION_KEYWORDS = new Set(["anyOf", "oneOf"]);
@@ -447,10 +454,15 @@ export function ajvErrorToPath(err: AjvErrorLike): string {
447
454
 
448
455
  /** Reduced, path-anchored issues — what a diagnostic list is built from. */
449
456
  export function schemaIssues(errors: AjvErrorLike[] | null | undefined): SchemaIssue[] {
450
- return reduceSchemaErrors(errors).map((err) => ({
451
- message: formatSingleError(err),
452
- path: ajvErrorToPath(err),
453
- }));
457
+ return reduceSchemaErrors(errors).map((err) => {
458
+ const missing = (err.params ?? {}).missingProperty;
459
+ return {
460
+ message: formatSingleError(err),
461
+ path: ajvErrorToPath(err),
462
+ ...(err.keyword ? { keyword: err.keyword } : {}),
463
+ ...(typeof missing === "string" ? { missingProperty: missing } : {}),
464
+ };
465
+ });
454
466
  }
455
467
 
456
468
  /** Reduced, rendered as one sentence — what a thrown runtime error carries. */