@telorun/analyzer 0.63.0 → 0.64.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 (52) hide show
  1. package/dist/analysis-registry.d.ts +24 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +35 -0
  4. package/dist/analyzer.d.ts +3 -37
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +57 -467
  7. package/dist/cel-scope-query.d.ts +109 -0
  8. package/dist/cel-scope-query.d.ts.map +1 -0
  9. package/dist/cel-scope-query.js +270 -0
  10. package/dist/cel-scope.d.ts +166 -0
  11. package/dist/cel-scope.d.ts.map +1 -0
  12. package/dist/cel-scope.js +377 -0
  13. package/dist/definition-registry.d.ts +15 -0
  14. package/dist/definition-registry.d.ts.map +1 -1
  15. package/dist/definition-registry.js +25 -9
  16. package/dist/find-manifest.d.ts +10 -0
  17. package/dist/find-manifest.d.ts.map +1 -0
  18. package/dist/find-manifest.js +12 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -0
  22. package/dist/invocation-contract.d.ts +11 -0
  23. package/dist/invocation-contract.d.ts.map +1 -1
  24. package/dist/invocation-contract.js +15 -0
  25. package/dist/manifest-analysis.d.ts +73 -0
  26. package/dist/manifest-analysis.d.ts.map +1 -0
  27. package/dist/manifest-analysis.js +78 -0
  28. package/dist/manifest-path.d.ts +18 -0
  29. package/dist/manifest-path.d.ts.map +1 -0
  30. package/dist/manifest-path.js +37 -0
  31. package/dist/schema-walk.d.ts +25 -0
  32. package/dist/schema-walk.d.ts.map +1 -0
  33. package/dist/schema-walk.js +126 -0
  34. package/dist/telo-version.d.ts +1 -1
  35. package/dist/telo-version.js +1 -1
  36. package/dist/validate-step-inputs.d.ts +17 -0
  37. package/dist/validate-step-inputs.d.ts.map +1 -1
  38. package/dist/validate-step-inputs.js +97 -6
  39. package/package.json +2 -2
  40. package/src/analysis-registry.ts +37 -0
  41. package/src/analyzer.ts +61 -585
  42. package/src/cel-scope-query.ts +337 -0
  43. package/src/cel-scope.ts +570 -0
  44. package/src/definition-registry.ts +31 -13
  45. package/src/find-manifest.ts +19 -0
  46. package/src/index.ts +13 -0
  47. package/src/invocation-contract.ts +22 -0
  48. package/src/manifest-analysis.ts +132 -0
  49. package/src/manifest-path.ts +34 -0
  50. package/src/schema-walk.ts +144 -0
  51. package/src/telo-version.ts +1 -1
  52. package/src/validate-step-inputs.ts +143 -7
@@ -1,4 +1,6 @@
1
1
  import { isLiveSlot, type ResourceDefinition, valueTypeOf } from "@telorun/sdk";
2
+ import { AliasResolver, moduleScopedDefResolver, type ModuleScopes } from "./alias-resolver.js";
3
+ import { DefinitionRegistry } from "./definition-registry.js";
2
4
  import {
3
5
  type ContractDirection,
4
6
  contractDeclarer,
@@ -14,6 +16,26 @@ import {
14
16
 
15
17
  export type { ContractDirection };
16
18
 
19
+ /** The {@link ContractScope} the analyzer resolves invocation contracts in: kinds
20
+ * resolve in the module that declared the definition they were read off (so an
21
+ * `extends` chain crossing module boundaries re-scopes at every hop), and named
22
+ * `telo#Type` references resolve against the flattened manifest list. `resolveIn`
23
+ * is the top-level entry point, where the kind was written by the READING
24
+ * module and there is no declaring definition yet. */
25
+ export function analyzerContractScope(
26
+ defs: DefinitionRegistry,
27
+ aliases: AliasResolver,
28
+ scopes: ModuleScopes,
29
+ allManifests: Record<string, any>[],
30
+ ): ContractScope & { resolveIn(kind: string, module?: string): ResourceDefinition | undefined } {
31
+ const resolve = moduleScopedDefResolver<ResourceDefinition>(defs, aliases, scopes);
32
+ return {
33
+ resolveDefinition: resolve,
34
+ resolveIn: resolve.in,
35
+ typeManifestsFor: () => allManifests,
36
+ };
37
+ }
38
+
17
39
  /**
18
40
  * The one answer to "what is this target's input / output schema".
19
41
  *
@@ -0,0 +1,132 @@
1
+ /**
2
+ * **One analyzed manifest set, and the questions asked of it.**
3
+ *
4
+ * Several answers a host needs require the SAME two things: the registry's
5
+ * definitions and aliases, and the manifest set they were resolved against. The
6
+ * registry deliberately holds no manifests — it is populated per analysis and
7
+ * reused across them — so each such answer would otherwise become another
8
+ * factory on the registry and another optional parameter on every IDE entry
9
+ * point. Four of those arrived in short order (CEL scope, step declarations,
10
+ * context-binding declarations, invocation contracts) and the next one is not
11
+ * hypothetical.
12
+ *
13
+ * So the pairing is named once and the questions hang off it. A host threads ONE
14
+ * object and gains later questions for free; each facet keeps its own honest
15
+ * name rather than accreting onto whichever one happened to exist first.
16
+ *
17
+ * Nothing here re-implements an answer. `contractFor` is the shared
18
+ * {@link resolveContract} — the one `telo check` runs and the kernel binds at
19
+ * dispatch — given the scope to run in; `celScope` is the same
20
+ * {@link CelScopeQuery} the analysis pass's rule is built from. That is the
21
+ * whole point: a completion list is a claim about what the checker accepts, and
22
+ * a second implementation of any of these could not be held to it.
23
+ */
24
+ import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
25
+ import { AliasResolver, type ModuleScopes } from "./alias-resolver.js";
26
+ import { CelScopeQuery, type CelScopeQueryContext } from "./cel-scope-query.js";
27
+ import { DefinitionRegistry } from "./definition-registry.js";
28
+ import type { ContractDirection } from "./extends-resolution.js";
29
+ import { analyzerContractScope, resolveContract } from "./invocation-contract.js";
30
+ import { findManifest } from "./find-manifest.js";
31
+ import { isModuleKind } from "./module-kinds.js";
32
+
33
+ /**
34
+ * A reference as the loader leaves it — the internal `{kind, name, alias?}`
35
+ * shape `resolveRefSentinels` rewrites `!ref` into.
36
+ */
37
+ export interface ManifestRef {
38
+ kind?: string;
39
+ name?: string;
40
+ alias?: string;
41
+ }
42
+
43
+ export class ManifestAnalysis {
44
+ private readonly scopes: ModuleScopes;
45
+ private celScopeQuery: CelScopeQuery | undefined;
46
+
47
+ constructor(
48
+ readonly manifests: ResourceManifest[],
49
+ private readonly ctx: CelScopeQueryContext,
50
+ ) {
51
+ const rootModules = new Set<string>();
52
+ for (const m of manifests) {
53
+ if (isModuleKind(m.kind) && m.metadata?.name) rootModules.add(m.metadata.name as string);
54
+ }
55
+ this.scopes = { aliasesByModule: ctx.aliasesByModule, rootModules };
56
+ }
57
+
58
+ /** What CEL sees, per site. Built on first use — its indices are a function of
59
+ * the whole set, and a host that never opens a CEL body should not pay for
60
+ * them. */
61
+ get celScope(): CelScopeQuery {
62
+ return (this.celScopeQuery ??= new CelScopeQuery(this.manifests, this.ctx));
63
+ }
64
+
65
+ /** The manifest a `(kind, name)` pair addresses. */
66
+ resourceFor(kind: string | undefined, name: string | undefined): ResourceManifest | undefined {
67
+ return findManifest(this.manifests, kind, name);
68
+ }
69
+
70
+ /**
71
+ * The invocation contract of the resource a reference names.
72
+ *
73
+ * The shared resolver, so an editor offering a target's input keys is offering
74
+ * exactly what `telo check` validates that call site against and what the
75
+ * kernel binds at dispatch. Layered instance-first: a resource declaring its
76
+ * own `inputType:` narrows the kind's, which is the common case for a
77
+ * `Run.Sequence` used as a handler.
78
+ */
79
+ contractFor(ref: ManifestRef, direction: ContractDirection): Record<string, any> | undefined {
80
+ const target = this.resolveRef(ref);
81
+ const definition = ref.kind ? this.definitionFor(ref.kind) : undefined;
82
+ if (!target && !definition) return undefined;
83
+ return resolveContract(
84
+ direction,
85
+ target as Record<string, any> | undefined,
86
+ definition,
87
+ analyzerContractScope(
88
+ this.ctx.defs,
89
+ this.ctx.aliases,
90
+ this.scopes,
91
+ this.manifests as Record<string, any>[],
92
+ ),
93
+ )?.schema;
94
+ }
95
+
96
+ /**
97
+ * The manifest a reference names.
98
+ *
99
+ * An ALIAS narrows before the name does: a flattened set carries every
100
+ * imported library's exported instances, so two libraries exporting a `store`
101
+ * are two manifests with one name. Matching the alias to its target module
102
+ * picks the right one; where the alias resolves to nothing the name alone is
103
+ * used, which is what a local reference needs anyway.
104
+ */
105
+ private resolveRef(ref: ManifestRef): ResourceManifest | undefined {
106
+ if (!ref.name) return undefined;
107
+ const byName = this.manifests.filter(
108
+ (m) => (m.metadata as { name?: string } | undefined)?.name === ref.name,
109
+ );
110
+ if (byName.length === 0) return undefined;
111
+ if (byName.length === 1) return byName[0];
112
+
113
+ const targetModule = ref.alias ? this.ctx.aliases.moduleForAlias?.(ref.alias) : undefined;
114
+ if (targetModule) {
115
+ const scoped = byName.find(
116
+ (m) => (m.metadata as { module?: string } | undefined)?.module === targetModule,
117
+ );
118
+ if (scoped) return scoped;
119
+ }
120
+ // Several candidates and nothing to choose between them: refusing is the
121
+ // honest answer, since typing a call site against the wrong resource's
122
+ // contract is worse than typing it against none.
123
+ return ref.kind ? byName.find((m) => m.kind === ref.kind) : undefined;
124
+ }
125
+
126
+ private definitionFor(kind: string): ResourceDefinition | undefined {
127
+ const canonical = this.ctx.aliases.resolveKind(kind);
128
+ return this.ctx.defs.resolve(kind) ?? (canonical ? this.ctx.defs.resolve(canonical) : undefined);
129
+ }
130
+ }
131
+
132
+ export type { CelScopeQueryContext, AliasResolver, DefinitionRegistry };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Navigating a MANIFEST by a concrete path (`routes[0].request.schema.query`).
3
+ *
4
+ * The counterpart to `schema-walk.ts`, which navigates a schema: this addresses
5
+ * the author's own document, indices and all. Its own module because three
6
+ * places needed it independently — the CEL scope query resolving a context
7
+ * binding's declaration, the call-site checker resolving an argument map, and
8
+ * the IDE resolving the same map for completion — and three copies of one
9
+ * traversal is exactly what the shared-answer rule exists to prevent.
10
+ */
11
+
12
+ /**
13
+ * The value at `path`, or `undefined` when any segment is absent.
14
+ *
15
+ * Absence is what makes a candidate path a CHECK rather than a guess: a caller
16
+ * offering several possible shapes can try each and know a hit is a real node.
17
+ */
18
+ export function navigateConcretePath(root: Record<string, any>, path: string): unknown {
19
+ let current: unknown = root;
20
+ for (const segment of path.split(".")) {
21
+ if (!segment) continue;
22
+ const match = segment.match(/^([^[]*)((?:\[\d+\])*)$/);
23
+ if (!match) return undefined;
24
+ if (match[1]) {
25
+ if (current === null || typeof current !== "object") return undefined;
26
+ current = (current as Record<string, unknown>)[match[1]];
27
+ }
28
+ for (const index of match[2].matchAll(/\[(\d+)\]/g)) {
29
+ if (!Array.isArray(current)) return undefined;
30
+ current = current[Number(index[1])];
31
+ }
32
+ }
33
+ return current;
34
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Structural traversal over a kind's JSON Schema and the step arrays it
3
+ * declares. Nothing here analyzes: these answer "what does this schema node
4
+ * point at" and "how do steps nest", the two questions every analyzer pass
5
+ * asks before it can say anything.
6
+ *
7
+ * Its own file so the CEL scope rule (`cel-scope.ts`) and the analysis pass
8
+ * (`analyzer.ts`) can both reach it without either importing the other — the
9
+ * scope rule is consumed by the IDE, which must not pull the pass in behind it.
10
+ */
11
+ import { MANIFEST_SCHEMA_URI, ManifestRootSchema } from "./manifest-schemas.js";
12
+
13
+ /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
14
+ * Non-refs and unresolved refs pass through unchanged. */
15
+ export function resolveLocalRef(
16
+ schema: Record<string, any> | undefined,
17
+ root: Record<string, any>,
18
+ ): Record<string, any> | undefined {
19
+ if (!schema) return undefined;
20
+ const ref = schema.$ref;
21
+ if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
22
+ const defName = ref.slice("#/$defs/".length);
23
+ const resolved = root.$defs?.[defName];
24
+ if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
25
+ }
26
+ // A kernel-owned structural fragment (`telo://manifest#/$defs/InvokeStep`).
27
+ // Resolved HERE rather than by each walker: this is the one chokepoint every
28
+ // structural walk already goes through — the step-array walks, the call graph,
29
+ // the zone projection, the eval-path collector — so a composer that points at a
30
+ // shared shape stays legible to all of them at once. Nothing is inlined into
31
+ // the stored schema, which keeps validator-cache identity stable and matches
32
+ // what `resolveSchemaTypeRefs` does for a named user type.
33
+ if (typeof ref === "string" && ref.startsWith(BUILTIN_FRAGMENT_PREFIX)) {
34
+ const defName = ref.slice(BUILTIN_FRAGMENT_PREFIX.length);
35
+ const resolved = (ManifestRootSchema.$defs as Record<string, unknown>)[defName];
36
+ if (resolved && typeof resolved === "object") return resolved as Record<string, any>;
37
+ }
38
+ return schema;
39
+ }
40
+
41
+ const BUILTIN_FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
42
+
43
+ /** Gather property schemas from a (possibly variant-bearing) object schema:
44
+ * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
45
+ *
46
+ * Each branch is resolved through {@link resolveLocalRef} first, so a branch
47
+ * that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
48
+ * site — contributes its properties like an inline one. Without that, pointing a
49
+ * composer at a shared shape would silently empty every role-driven lookup that
50
+ * reads this (the inputs slot, the retry policy, the eval paths), which is a
51
+ * failure with no diagnostic attached to it. */
52
+ export function gatherPropertySchemas(
53
+ schema: Record<string, any>,
54
+ root?: Record<string, any>,
55
+ ): Array<[string, Record<string, any>]> {
56
+ const out: Array<[string, Record<string, any>]> = [];
57
+ const base = resolveLocalRef(schema, root ?? schema) ?? schema;
58
+ if (base.properties && typeof base.properties === "object") {
59
+ for (const [k, v] of Object.entries(base.properties as Record<string, any>)) {
60
+ out.push([k, v as Record<string, any>]);
61
+ }
62
+ }
63
+ for (const variantKey of ["oneOf", "anyOf", "allOf"] as const) {
64
+ const arr = base[variantKey];
65
+ if (!Array.isArray(arr)) continue;
66
+ for (const raw of arr) {
67
+ if (!raw || typeof raw !== "object") continue;
68
+ const variant = resolveLocalRef(raw as Record<string, any>, root ?? schema) ?? raw;
69
+ if (variant.properties) {
70
+ for (const [k, v] of Object.entries(variant.properties as Record<string, any>)) {
71
+ out.push([k, v as Record<string, any>]);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * Generic, role-driven walk over a step array. Calls
81
+ * `visit(step, stepPath)` for every step — top-level and nested through the
82
+ * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
83
+ * the single definition of how steps nest, shared by `buildStepContextSchema`
84
+ * (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
85
+ * checks invoke refs), so the topology contract lives in one place — adding a
86
+ * role or nesting form updates both consumers at once. No resource kind is
87
+ * hardcoded; recursion is driven entirely by the schema annotations.
88
+ */
89
+ export function walkStepArray(
90
+ steps: unknown[],
91
+ stepItemSchema: Record<string, any> | undefined,
92
+ rootSchema: Record<string, any>,
93
+ basePath: string,
94
+ visit: (step: Record<string, any>, stepPath: string) => void,
95
+ ): void {
96
+ const dispatchRole = (
97
+ data: unknown,
98
+ role: string,
99
+ itemsSchema: Record<string, any> | undefined,
100
+ path: string,
101
+ ): void => {
102
+ if (role === "branch" && Array.isArray(data)) {
103
+ walkStepArray(data, stepItemSchema, rootSchema, path, visit);
104
+ } else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
105
+ for (const [caseKey, arr] of Object.entries(data as Record<string, unknown>)) {
106
+ if (Array.isArray(arr)) walkStepArray(arr, stepItemSchema, rootSchema, `${path}.${caseKey}`, visit);
107
+ }
108
+ } else if (role === "branch-list" && Array.isArray(data)) {
109
+ const entrySchema = resolveLocalRef(itemsSchema, rootSchema);
110
+ if (!entrySchema) return;
111
+ data.forEach((entry, i) => {
112
+ if (!entry || typeof entry !== "object") return;
113
+ for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
114
+ const subRole = subSchema["x-telo-topology-role"];
115
+ if (typeof subRole !== "string") continue;
116
+ dispatchRole(
117
+ (entry as Record<string, any>)[subKey],
118
+ subRole,
119
+ subSchema.items as Record<string, any> | undefined,
120
+ `${path}[${i}].${subKey}`,
121
+ );
122
+ }
123
+ });
124
+ }
125
+ };
126
+
127
+ steps.forEach((step, i) => {
128
+ if (!step || typeof step !== "object") return;
129
+ const s = step as Record<string, any>;
130
+ const stepPath = `${basePath}[${i}]`;
131
+ visit(s, stepPath);
132
+ if (!stepItemSchema) return;
133
+ for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
134
+ const role = propSchema["x-telo-topology-role"];
135
+ if (typeof role !== "string") continue;
136
+ dispatchRole(
137
+ s[propKey],
138
+ role,
139
+ propSchema.items as Record<string, any> | undefined,
140
+ `${stepPath}.${propKey}`,
141
+ );
142
+ }
143
+ });
144
+ }
@@ -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.79.0";
9
+ export const TELO_SURFACE_VERSION = "0.80.0";
@@ -9,7 +9,7 @@ import {
9
9
  validateAgainstSchema,
10
10
  } from "./schema-compat.js";
11
11
  import { plainChainOf } from "@telorun/templating";
12
- import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
12
+ import { isLiveSlot, valueTypeOf, type ResourceDefinition } from "@telorun/sdk";
13
13
  import { manifestFragmentOf } from "./manifest-schemas.js";
14
14
  import {
15
15
  analyzerContractScope,
@@ -20,6 +20,12 @@ import {
20
20
  walkStepArray,
21
21
  } from "./analyzer.js";
22
22
  import { readStepSlot } from "./step-slot.js";
23
+ import { navigateConcretePath } from "./manifest-path.js";
24
+ import {
25
+ isRefEntry,
26
+ resolveFieldEntries,
27
+ type ReferenceFieldMap,
28
+ } from "./reference-field-map.js";
23
29
 
24
30
  export interface StepInputIssue {
25
31
  path: string;
@@ -66,6 +72,15 @@ export function collectStepInputIssues(
66
72
  const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
67
73
  const readingModule = (manifest.metadata as { module?: string } | undefined)?.module;
68
74
 
75
+ const ctx: CallCheckContext = {
76
+ manifest,
77
+ allManifests,
78
+ defs,
79
+ contractScope,
80
+ readingModule,
81
+ stepContext,
82
+ };
83
+
69
84
  for (const [fieldName, fieldSchema] of Object.entries(props)) {
70
85
  const stepCtx = readStepSlot(fieldSchema);
71
86
  if (!stepCtx) continue;
@@ -92,6 +107,127 @@ export function collectStepInputIssues(
92
107
  if (!invoke || typeof invoke !== "object") return;
93
108
  if (!values || typeof values !== "object" || Array.isArray(values)) return;
94
109
 
110
+ out.push(
111
+ ...checkCallSite(
112
+ {
113
+ inputsPath: `${stepPath}.${inputsField}`,
114
+ values: values as Record<string, any>,
115
+ invoke,
116
+ // Only a step declares a re-attempt policy, so only a step can carry
117
+ // the live-value-retried finding.
118
+ declaredRetryFor: (invokedManifest, invokedDef) =>
119
+ declaredRetry(step, stepItemSchema, invokedManifest, invokedDef),
120
+ },
121
+ ctx,
122
+ ),
123
+ );
124
+ });
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Validate the argument map of every call this resource makes through a
131
+ * REFERENCE SLOT, as opposed to a step.
132
+ *
133
+ * A slot that transfers control names its argument slot on its own `x-telo-ref`
134
+ * (`inputs:`, a JSON Pointer relative to the object enclosing the slot). That
135
+ * annotation is the only thing tying an otherwise-open `inputs:` map to the
136
+ * resource it holds arguments for — an HTTP route's `handler:` + `inputs:` pair
137
+ * is exactly this shape, and nothing about it is a step.
138
+ *
139
+ * Discovery is driven by the annotation rather than by any kind's topology, so
140
+ * a composer that names its argument slot gets its call sites checked without
141
+ * the analyzer learning what a route is. It is the same check the step driver
142
+ * runs, because it is the same question.
143
+ */
144
+ export function collectRefInputIssues(
145
+ manifest: Record<string, any>,
146
+ fieldMap: ReferenceFieldMap | undefined,
147
+ allManifests: Record<string, any>[],
148
+ defs: DefinitionRegistry,
149
+ aliases: AliasResolver,
150
+ scopes: ModuleScopes,
151
+ ): StepInputIssue[] {
152
+ const out: StepInputIssue[] = [];
153
+ if (!fieldMap) return out;
154
+
155
+ const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
156
+ const ctx: CallCheckContext = {
157
+ manifest,
158
+ allManifests,
159
+ defs,
160
+ contractScope,
161
+ readingModule: (manifest.metadata as { module?: string } | undefined)?.module,
162
+ };
163
+
164
+ for (const [fieldPath, entry] of fieldMap) {
165
+ if (!isRefEntry(entry) || !entry.inputs) continue;
166
+ const pointer = pointerSegments(entry.inputs);
167
+ if (!pointer) continue;
168
+
169
+ for (const { value: invoke, path: slotPath } of resolveFieldEntries(manifest, fieldPath)) {
170
+ if (!invoke || typeof invoke !== "object" || Array.isArray(invoke)) continue;
171
+ // Relative to the object ENCLOSING the slot, which is the annotation's
172
+ // documented anchor.
173
+ const enclosing = slotPath.slice(0, Math.max(0, slotPath.lastIndexOf(".")));
174
+ const inputsPath = [enclosing, ...pointer].filter(Boolean).join(".");
175
+ const values = navigateConcretePath(manifest, inputsPath);
176
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
177
+
178
+ out.push(
179
+ ...checkCallSite(
180
+ { inputsPath, values: values as Record<string, any>, invoke: invoke as Record<string, any> },
181
+ ctx,
182
+ ),
183
+ );
184
+ }
185
+ }
186
+ return out;
187
+ }
188
+
189
+ /** A JSON Pointer naming a sibling FIELD path. An array index is not a field,
190
+ * so a pointer carrying one names nothing this can resolve. */
191
+ function pointerSegments(pointer: string): string[] | undefined {
192
+ if (!pointer.startsWith("/")) return undefined;
193
+ const segments = pointer
194
+ .slice(1)
195
+ .split("/")
196
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
197
+ return segments.every((s) => s.length > 0 && !/^\d+$/.test(s)) ? segments : undefined;
198
+ }
199
+
200
+ /** One call site: the arguments written, and the reference they are for. */
201
+ interface CallSite {
202
+ /** Concrete path of the argument map, for anchoring a diagnostic. */
203
+ inputsPath: string;
204
+ values: Record<string, any>;
205
+ invoke: Record<string, any>;
206
+ declaredRetryFor?(
207
+ invokedManifest: Record<string, any> | undefined,
208
+ invokedDef: ResourceDefinition | undefined,
209
+ ): string | undefined;
210
+ }
211
+
212
+ interface CallCheckContext {
213
+ manifest: Record<string, any>;
214
+ allManifests: Record<string, any>[];
215
+ defs: DefinitionRegistry;
216
+ contractScope: ReturnType<typeof analyzerContractScope>;
217
+ readingModule: string | undefined;
218
+ stepContext?: Record<string, any>;
219
+ }
220
+
221
+ /**
222
+ * The check itself, shared by both drivers: the arguments written at a call site
223
+ * against the contract the target declares.
224
+ */
225
+ function checkCallSite(site: CallSite, ctx: CallCheckContext): StepInputIssue[] {
226
+ const out: StepInputIssue[] = [];
227
+ const { manifest, allManifests, defs, contractScope, readingModule, stepContext } = ctx;
228
+ const { invoke, values } = site;
229
+ {
230
+ {
95
231
  const invokedKind = invoke.kind as string | undefined;
96
232
  const invokedName = invoke.name as string | undefined;
97
233
  const invokedManifest = invokedName
@@ -104,7 +240,7 @@ export function collectStepInputIssues(
104
240
  ? contractScope.resolveIn(invokedKind, readingModule)
105
241
  : undefined;
106
242
  const contract = resolveContract("inputType", invokedManifest, invokedDef, contractScope);
107
- if (!contract) return;
243
+ if (!contract) return out;
108
244
 
109
245
  // Findings AT a substituted path are about a placeholder, not about
110
246
  // anything the author wrote — a `pattern`-constrained string or a `oneOf`
@@ -163,10 +299,10 @@ export function collectStepInputIssues(
163
299
  // already declared: the value's liveness by its value type, and the
164
300
  // re-attempt by the retry policy. No kind is named.
165
301
  if (isLiveSlot(produced)) {
166
- const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
302
+ const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
167
303
  if (retry !== undefined) {
168
304
  out.push({
169
- path: `${stepPath}.${inputsField}.${inputName}`,
305
+ path: `${site.inputsPath}.${inputName}`,
170
306
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
171
307
  message:
172
308
  `'${inputName}' is a live value, which is consumed by reading and so exists ` +
@@ -184,7 +320,7 @@ export function collectStepInputIssues(
184
320
  );
185
321
  if (compatible) continue;
186
322
  out.push({
187
- path: `${stepPath}.${inputsField}.${inputName}`,
323
+ path: `${site.inputsPath}.${inputName}`,
188
324
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
189
325
  message: issues.join("; "),
190
326
  code: "CEL_TYPE_ARGUMENT_MISMATCH",
@@ -200,12 +336,12 @@ export function collectStepInputIssues(
200
336
  // container that should have held it, which does exist.
201
337
  const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
202
338
  out.push({
203
- path: anchor ? `${stepPath}.${inputsField}.${anchor}` : `${stepPath}.${inputsField}`,
339
+ path: anchor ? `${site.inputsPath}.${anchor}` : site.inputsPath,
204
340
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
205
341
  message: issue.message,
206
342
  });
207
343
  }
208
- });
344
+ }
209
345
  }
210
346
  return out;
211
347
  }