@telorun/analyzer 0.73.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 (138) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +26 -7
  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 +73 -16
  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/forwarded-declaration.d.ts +26 -0
  19. package/dist/forwarded-declaration.d.ts.map +1 -0
  20. package/dist/forwarded-declaration.js +31 -0
  21. package/dist/index.d.ts +9 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +4 -1
  24. package/dist/kernel-globals.d.ts.map +1 -1
  25. package/dist/kernel-globals.js +21 -5
  26. package/dist/module-alias-scope.d.ts +11 -0
  27. package/dist/module-alias-scope.d.ts.map +1 -1
  28. package/dist/module-alias-scope.js +10 -0
  29. package/dist/module-file-claims.d.ts +17 -1
  30. package/dist/module-file-claims.d.ts.map +1 -1
  31. package/dist/module-file-claims.js +38 -3
  32. package/dist/module-named-files.d.ts +72 -0
  33. package/dist/module-named-files.d.ts.map +1 -0
  34. package/dist/module-named-files.js +115 -0
  35. package/dist/native-entries.d.ts +83 -0
  36. package/dist/native-entries.d.ts.map +1 -0
  37. package/dist/native-entries.js +169 -0
  38. package/dist/normalize-inline-resources.d.ts +41 -13
  39. package/dist/normalize-inline-resources.d.ts.map +1 -1
  40. package/dist/normalize-inline-resources.js +236 -54
  41. package/dist/reference-field-map.d.ts +59 -1
  42. package/dist/reference-field-map.d.ts.map +1 -1
  43. package/dist/reference-field-map.js +200 -34
  44. package/dist/release/payload-digest.d.ts +2 -1
  45. package/dist/release/payload-digest.d.ts.map +1 -1
  46. package/dist/release/payload-digest.js +2 -1
  47. package/dist/resolve-ref-sentinels.d.ts.map +1 -1
  48. package/dist/resolve-ref-sentinels.js +8 -5
  49. package/dist/resolve-throws-union.d.ts +25 -4
  50. package/dist/resolve-throws-union.d.ts.map +1 -1
  51. package/dist/resolve-throws-union.js +62 -32
  52. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  53. package/dist/resolve-zone-requirements.js +2 -1
  54. package/dist/rewrite-synthetic-origins.d.ts +6 -1
  55. package/dist/rewrite-synthetic-origins.d.ts.map +1 -1
  56. package/dist/rewrite-synthetic-origins.js +107 -13
  57. package/dist/schema-compat.d.ts.map +1 -1
  58. package/dist/schema-compat.js +18 -5
  59. package/dist/schema-walk.d.ts +47 -13
  60. package/dist/schema-walk.d.ts.map +1 -1
  61. package/dist/schema-walk.js +150 -30
  62. package/dist/scope-declarations.d.ts +59 -0
  63. package/dist/scope-declarations.d.ts.map +1 -0
  64. package/dist/scope-declarations.js +45 -0
  65. package/dist/source-entries.d.ts +119 -0
  66. package/dist/source-entries.d.ts.map +1 -0
  67. package/dist/source-entries.js +349 -0
  68. package/dist/step-bodies.d.ts +24 -0
  69. package/dist/step-bodies.d.ts.map +1 -0
  70. package/dist/step-bodies.js +28 -0
  71. package/dist/telo-version.d.ts +1 -1
  72. package/dist/telo-version.js +1 -1
  73. package/dist/validate-cel-context.d.ts.map +1 -1
  74. package/dist/validate-cel-context.js +3 -2
  75. package/dist/validate-module-artifact.d.ts +4 -2
  76. package/dist/validate-module-artifact.d.ts.map +1 -1
  77. package/dist/validate-module-artifact.js +43 -4
  78. package/dist/validate-native-entries.d.ts +15 -0
  79. package/dist/validate-native-entries.d.ts.map +1 -0
  80. package/dist/validate-native-entries.js +103 -0
  81. package/dist/validate-observed-state.d.ts +7 -1
  82. package/dist/validate-observed-state.d.ts.map +1 -1
  83. package/dist/validate-observed-state.js +24 -6
  84. package/dist/validate-reference-forms.d.ts.map +1 -1
  85. package/dist/validate-reference-forms.js +2 -2
  86. package/dist/validate-references.d.ts.map +1 -1
  87. package/dist/validate-references.js +11 -5
  88. package/dist/validate-scope-reach.d.ts +31 -0
  89. package/dist/validate-scope-reach.d.ts.map +1 -0
  90. package/dist/validate-scope-reach.js +180 -0
  91. package/dist/validate-source-entries.d.ts +16 -0
  92. package/dist/validate-source-entries.d.ts.map +1 -0
  93. package/dist/validate-source-entries.js +90 -0
  94. package/dist/validate-throws-coverage.d.ts.map +1 -1
  95. package/dist/validate-throws-coverage.js +21 -39
  96. package/dist/value-type-keyword.d.ts +22 -5
  97. package/dist/value-type-keyword.d.ts.map +1 -1
  98. package/dist/value-type-keyword.js +16 -7
  99. package/package.json +3 -3
  100. package/src/analyzer.ts +33 -9
  101. package/src/artifact-axes.ts +24 -0
  102. package/src/artifact-layer-index.ts +9 -6
  103. package/src/artifact-selector.ts +110 -35
  104. package/src/builtins.ts +75 -16
  105. package/src/call-graph.ts +30 -0
  106. package/src/catch-scope.ts +1 -1
  107. package/src/forwarded-declaration.ts +32 -0
  108. package/src/index.ts +36 -1
  109. package/src/kernel-globals.ts +28 -5
  110. package/src/migrations/CLAUDE.md +35 -0
  111. package/src/module-alias-scope.ts +17 -0
  112. package/src/module-file-claims.ts +63 -4
  113. package/src/module-named-files.ts +162 -0
  114. package/src/native-entries.ts +227 -0
  115. package/src/normalize-inline-resources.ts +326 -70
  116. package/src/reference-field-map.ts +257 -52
  117. package/src/release/CLAUDE.md +32 -0
  118. package/src/release/payload-digest.ts +2 -1
  119. package/src/resolve-ref-sentinels.ts +7 -6
  120. package/src/resolve-throws-union.ts +87 -32
  121. package/src/resolve-zone-requirements.ts +2 -1
  122. package/src/rewrite-synthetic-origins.ts +107 -16
  123. package/src/schema-compat.ts +18 -4
  124. package/src/schema-walk.ts +162 -34
  125. package/src/scope-declarations.ts +98 -0
  126. package/src/source-entries.ts +502 -0
  127. package/src/step-bodies.ts +55 -0
  128. package/src/telo-version.ts +1 -1
  129. package/src/validate-cel-context.ts +3 -2
  130. package/src/validate-module-artifact.ts +46 -3
  131. package/src/validate-native-entries.ts +147 -0
  132. package/src/validate-observed-state.ts +36 -7
  133. package/src/validate-reference-forms.ts +2 -3
  134. package/src/validate-references.ts +10 -6
  135. package/src/validate-scope-reach.ts +213 -0
  136. package/src/validate-source-entries.ts +120 -0
  137. package/src/validate-throws-coverage.ts +24 -31
  138. package/src/value-type-keyword.ts +35 -9
@@ -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
@@ -9,8 +9,9 @@
9
9
  * scope rule is consumed by the IDE, which must not pull the pass in behind it.
10
10
  */
11
11
  import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
12
- import { readRefSlot, type RefSlot } from "./ref-slot.js";
13
- import { readStepSlot, type StepSlot } from "./step-slot.js";
12
+ import type { RefSlot } from "./ref-slot.js";
13
+ import type { StepSlot } from "./step-slot.js";
14
+ import { buildDrivenSlotMap, refSlotOfEntry, type DrivenSlots } from "./reference-field-map.js";
14
15
 
15
16
  /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
16
17
  * Non-refs and unresolved refs pass through unchanged. */
@@ -144,57 +145,184 @@ export function walkStepArray(
144
145
  }
145
146
  });
146
147
  }
147
- /** A slot through which a resource drives another. */
148
+ /** A slot a kind's schema declares through which its resources drive another.
149
+ * `path` is the field-map form (`routes[].handler`, `content.{}.encoder`);
150
+ * `slots` holds every slot any branch declares there. */
151
+ export type DeclaredSlot =
152
+ | { kind: "step"; slots: StepSlot[]; path: string }
153
+ | { kind: "ref"; slots: RefSlot[]; path: string };
154
+
155
+ /** A reference slot at a site, with the declaration it came from — which is
156
+ * what a case-map selector's schema default is read against. */
157
+ export interface DrivenRef {
158
+ slot: RefSlot;
159
+ fieldPath: string;
160
+ }
161
+
162
+ /** The slots at one concrete site of one resource (`routes[0].handler`): every
163
+ * slot any branch or recursion route declares there. A consumer reads them as
164
+ * a union — a branch that does not apply to this resource can only add what
165
+ * counts, never remove it. */
148
166
  export type DrivenSlot =
149
- | { kind: "step"; slot: StepSlot; data: unknown[]; path: string }
150
- | { kind: "ref"; slot: RefSlot; data: unknown; path: string };
167
+ | { kind: "step"; slots: StepSlot[]; data: unknown[]; path: string }
168
+ | { kind: "ref"; slots: DrivenRef[]; data: unknown; path: string };
169
+
170
+ /**
171
+ * Every slot a kind's schema declares through which its resources drive
172
+ * another — the schema-only mode of {@link forEachDrivenSlot}, read off the
173
+ * same {@link buildDrivenSlotMap}.
174
+ */
175
+ export function forEachDeclaredSlot(schema: unknown, visit: (slot: DeclaredSlot) => void): void {
176
+ if (!schema || typeof schema !== "object") return;
177
+ for (const [path, at] of buildDrivenSlotMap(schema as Record<string, any>).paths) {
178
+ if (at.steps.length > 0) visit({ kind: "step", slots: at.steps, path });
179
+ if (at.refs.length > 0) visit({ kind: "ref", slots: at.refs.map(refSlotOfEntry), path });
180
+ }
181
+ }
151
182
 
152
183
  /**
153
184
  * Every slot of one resource through which it drives another, schema and data in
154
185
  * tandem.
155
186
  *
156
- * One traversal with two consumersthe inherited union here, and the catch
157
- * scope enclosure in `validate-throws-coverage.ts` because both ask the same
158
- * structural question and two copies would eventually disagree about where the
159
- * walk stops. It terminates on the manifest's own depth, and it stops AT a step
160
- * slot (that traversal owns everything below it, `try`/`catch` subtraction
161
- * included) and AT a reference slot (a resolved ref is a leaf, `{kind, name}`,
162
- * with nothing beneath it to visit).
187
+ * One traversal for every throws question a kind's `inherit` union, a scope
188
+ * list's denominator, catch-scope enclosure, and whether `inherit` is legal
189
+ * because they ask the same structural question and two copies would eventually
190
+ * disagree about where the walk stops. The slots are the driven-slot map's, so
191
+ * the reach is the reference field map's plus local `$ref`; a step slot is a
192
+ * stop (that traversal owns everything below it, `try`/`catch` subtraction
193
+ * included) and so is a reference slot (a resolved ref is a leaf).
194
+ *
195
+ * Each concrete site is visited once, with every slot that reaches it. A
196
+ * map-value (`additionalProperties`) slot applies only to keys its schema does
197
+ * not declare, and never to the resource envelope at the root, as JSON Schema
198
+ * applies it. A recursive schema's back-edge is followed as deep as the data
199
+ * goes, guarded against data that aliases one of its own ancestors. A schema
200
+ * whose map holds no step or reference slot is not walked at all.
163
201
  */
164
202
  export function forEachDrivenSlot(
165
203
  schema: unknown,
166
204
  data: unknown,
167
205
  visit: (slot: DrivenSlot) => void,
168
- path = "",
169
206
  ): void {
170
207
  if (!schema || typeof schema !== "object" || data === undefined || data === null) return;
171
- const node = schema as Record<string, any>;
172
-
173
- const stepSlot = readStepSlot(node);
174
- if (stepSlot) {
175
- if (Array.isArray(data)) visit({ kind: "step", slot: stepSlot, data, path });
176
- return;
177
- }
208
+ const driven = buildDrivenSlotMap(schema as Record<string, any>);
209
+ if (!driven.drives) return;
210
+ const sites = new Map<
211
+ string,
212
+ { data: unknown; steps: StepSlot[]; refs: DrivenRef[]; from: Set<object> }
213
+ >();
214
+ const onData = new Set<unknown>([data]);
178
215
 
179
- const refSlot = readRefSlot(node);
180
- if (refSlot) {
181
- visit({ kind: "ref", slot: refSlot, data, path });
182
- return;
183
- }
216
+ const walk = (prefix: string, value: unknown, base: string): void => {
217
+ for (const [fieldPath, at] of driven.paths) {
218
+ const rel = relativeFieldPath(fieldPath, prefix);
219
+ if (rel === undefined) continue;
220
+ for (const found of resolveSites(value, rel, prefix, driven)) {
221
+ const path = joinConcrete(base, found.path);
222
+ if (at.steps.length > 0 || at.refs.length > 0) {
223
+ let site = sites.get(path);
224
+ if (!site) {
225
+ site = { data: found.value, steps: [], refs: [], from: new Set() };
226
+ sites.set(path, site);
227
+ }
228
+ for (const step of at.steps) {
229
+ if (site.from.has(step)) continue;
230
+ site.from.add(step);
231
+ site.steps.push(step);
232
+ }
233
+ for (const entry of at.refs) {
234
+ if (site.from.has(entry)) continue;
235
+ site.from.add(entry);
236
+ site.refs.push({ slot: refSlotOfEntry(entry), fieldPath });
237
+ }
238
+ }
239
+ if (at.recurse.length === 0) continue;
240
+ if (!found.value || typeof found.value !== "object" || onData.has(found.value)) continue;
241
+ onData.add(found.value);
242
+ for (const to of at.recurse) walk(to, found.value, path);
243
+ onData.delete(found.value);
244
+ }
245
+ }
246
+ };
247
+ walk("", data, "");
184
248
 
185
- const props = node.properties as Record<string, any> | undefined;
186
- if (props && typeof data === "object" && !Array.isArray(data)) {
187
- const obj = data as Record<string, unknown>;
188
- for (const [key, propSchema] of Object.entries(props)) {
189
- if (obj[key] === undefined) continue;
190
- forEachDrivenSlot(propSchema, obj[key], visit, path ? `${path}.${key}` : key);
249
+ for (const [path, site] of sites) {
250
+ if (site.steps.length > 0 && Array.isArray(site.data)) {
251
+ visit({ kind: "step", slots: site.steps, data: site.data, path });
252
+ } else if (site.refs.length > 0) {
253
+ visit({ kind: "ref", slots: site.refs, data: site.data, path });
191
254
  }
192
255
  }
256
+ }
257
+
258
+ interface Site {
259
+ value: unknown;
260
+ path: string;
261
+ }
193
262
 
194
- if (node.items && Array.isArray(data)) {
195
- for (const [i, item] of data.entries()) {
196
- forEachDrivenSlot(node.items, item, visit, `${path}[${i}]`);
263
+ /** The values at `rel` below `value`, where `rel` is relative to the field path
264
+ * `prefix`. A `{}` segment skips the keys the driven map records as declared
265
+ * for that map path; each `[]` iterates one array level. */
266
+ function resolveSites(value: unknown, rel: string, prefix: string, driven: DrivenSlots): Site[] {
267
+ let sites: Site[] = [{ value, path: "" }];
268
+ let field = prefix;
269
+ for (const part of rel.split(".")) {
270
+ const next: Site[] = [];
271
+ if (part === "{}") {
272
+ field = joinKey(field, "{}");
273
+ const declared = driven.declaredKeys.get(field);
274
+ for (const site of sites) {
275
+ if (!site.value || typeof site.value !== "object" || Array.isArray(site.value)) continue;
276
+ for (const [key, entry] of Object.entries(site.value as Record<string, unknown>)) {
277
+ if (entry == null || declared?.has(key)) continue;
278
+ next.push({ value: entry, path: joinKey(site.path, key) });
279
+ }
280
+ }
281
+ } else {
282
+ const key = part.replace(/(\[\])+$/, "");
283
+ const depth = (part.length - key.length) / 2;
284
+ field = key ? joinKey(field, part) : `${field}${part}`;
285
+ for (const site of sites) {
286
+ let level: Site[];
287
+ if (key) {
288
+ if (!site.value || typeof site.value !== "object") continue;
289
+ const entry = (site.value as Record<string, unknown>)[key];
290
+ if (entry == null) continue;
291
+ level = [{ value: entry, path: joinKey(site.path, key) }];
292
+ } else {
293
+ level = [site];
294
+ }
295
+ for (let d = 0; d < depth; d++) {
296
+ const items: Site[] = [];
297
+ for (const holder of level) {
298
+ if (!Array.isArray(holder.value)) continue;
299
+ holder.value.forEach((item, i) => {
300
+ if (item != null) items.push({ value: item, path: `${holder.path}[${i}]` });
301
+ });
302
+ }
303
+ level = items;
304
+ }
305
+ next.push(...level);
306
+ }
197
307
  }
308
+ sites = next;
198
309
  }
310
+ return sites;
311
+ }
312
+
313
+ const joinKey = (path: string, key: string): string => (path ? `${path}.${key}` : key);
314
+
315
+ /** `fieldPath` relative to `prefix`, or undefined when it is not strictly
316
+ * below it. A result starting `[]` iterates the value at `prefix` itself. */
317
+ function relativeFieldPath(fieldPath: string, prefix: string): string | undefined {
318
+ if (prefix === "") return fieldPath;
319
+ if (fieldPath.startsWith(`${prefix}.`)) return fieldPath.slice(prefix.length + 1);
320
+ if (fieldPath.startsWith(`${prefix}[]`)) return fieldPath.slice(prefix.length);
321
+ return undefined;
322
+ }
323
+
324
+ function joinConcrete(base: string, rel: string): string {
325
+ if (!base) return rel;
326
+ return rel.startsWith("[") ? `${base}${rel}` : `${base}.${rel}`;
199
327
  }
200
328