@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
@@ -0,0 +1,73 @@
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 { ResourceManifest } from "@telorun/sdk";
25
+ import { AliasResolver } 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
+ /**
30
+ * A reference as the loader leaves it — the internal `{kind, name, alias?}`
31
+ * shape `resolveRefSentinels` rewrites `!ref` into.
32
+ */
33
+ export interface ManifestRef {
34
+ kind?: string;
35
+ name?: string;
36
+ alias?: string;
37
+ }
38
+ export declare class ManifestAnalysis {
39
+ readonly manifests: ResourceManifest[];
40
+ private readonly ctx;
41
+ private readonly scopes;
42
+ private celScopeQuery;
43
+ constructor(manifests: ResourceManifest[], ctx: CelScopeQueryContext);
44
+ /** What CEL sees, per site. Built on first use — its indices are a function of
45
+ * the whole set, and a host that never opens a CEL body should not pay for
46
+ * them. */
47
+ get celScope(): CelScopeQuery;
48
+ /** The manifest a `(kind, name)` pair addresses. */
49
+ resourceFor(kind: string | undefined, name: string | undefined): ResourceManifest | undefined;
50
+ /**
51
+ * The invocation contract of the resource a reference names.
52
+ *
53
+ * The shared resolver, so an editor offering a target's input keys is offering
54
+ * exactly what `telo check` validates that call site against and what the
55
+ * kernel binds at dispatch. Layered instance-first: a resource declaring its
56
+ * own `inputType:` narrows the kind's, which is the common case for a
57
+ * `Run.Sequence` used as a handler.
58
+ */
59
+ contractFor(ref: ManifestRef, direction: ContractDirection): Record<string, any> | undefined;
60
+ /**
61
+ * The manifest a reference names.
62
+ *
63
+ * An ALIAS narrows before the name does: a flattened set carries every
64
+ * imported library's exported instances, so two libraries exporting a `store`
65
+ * are two manifests with one name. Matching the alias to its target module
66
+ * picks the right one; where the alias resolves to nothing the name alone is
67
+ * used, which is what a local reference needs anyway.
68
+ */
69
+ private resolveRef;
70
+ private definitionFor;
71
+ }
72
+ export type { CelScopeQueryContext, AliasResolver, DefinitionRegistry };
73
+ //# sourceMappingURL=manifest-analysis.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-analysis.d.ts","sourceRoot":"","sources":["../src/manifest-analysis.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAsB,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,aAAa,EAAqB,MAAM,qBAAqB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAKjE;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,gBAAgB;IAKzB,QAAQ,CAAC,SAAS,EAAE,gBAAgB,EAAE;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG;IALtB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,aAAa,CAA4B;gBAGtC,SAAS,EAAE,gBAAgB,EAAE,EACrB,GAAG,EAAE,oBAAoB;IAS5C;;gBAEY;IACZ,IAAI,QAAQ,IAAI,aAAa,CAE5B;IAED,oDAAoD;IACpD,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,gBAAgB,GAAG,SAAS;IAI7F;;;;;;;;OAQG;IACH,WAAW,CAAC,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS;IAiB5F;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAqBlB,OAAO,CAAC,aAAa;CAItB;AAED,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,kBAAkB,EAAE,CAAC"}
@@ -0,0 +1,78 @@
1
+ import { CelScopeQuery } from "./cel-scope-query.js";
2
+ import { analyzerContractScope, resolveContract } from "./invocation-contract.js";
3
+ import { findManifest } from "./find-manifest.js";
4
+ import { isModuleKind } from "./module-kinds.js";
5
+ export class ManifestAnalysis {
6
+ manifests;
7
+ ctx;
8
+ scopes;
9
+ celScopeQuery;
10
+ constructor(manifests, ctx) {
11
+ this.manifests = manifests;
12
+ this.ctx = ctx;
13
+ const rootModules = new Set();
14
+ for (const m of manifests) {
15
+ if (isModuleKind(m.kind) && m.metadata?.name)
16
+ rootModules.add(m.metadata.name);
17
+ }
18
+ this.scopes = { aliasesByModule: ctx.aliasesByModule, rootModules };
19
+ }
20
+ /** What CEL sees, per site. Built on first use — its indices are a function of
21
+ * the whole set, and a host that never opens a CEL body should not pay for
22
+ * them. */
23
+ get celScope() {
24
+ return (this.celScopeQuery ??= new CelScopeQuery(this.manifests, this.ctx));
25
+ }
26
+ /** The manifest a `(kind, name)` pair addresses. */
27
+ resourceFor(kind, name) {
28
+ return findManifest(this.manifests, kind, name);
29
+ }
30
+ /**
31
+ * The invocation contract of the resource a reference names.
32
+ *
33
+ * The shared resolver, so an editor offering a target's input keys is offering
34
+ * exactly what `telo check` validates that call site against and what the
35
+ * kernel binds at dispatch. Layered instance-first: a resource declaring its
36
+ * own `inputType:` narrows the kind's, which is the common case for a
37
+ * `Run.Sequence` used as a handler.
38
+ */
39
+ contractFor(ref, direction) {
40
+ const target = this.resolveRef(ref);
41
+ const definition = ref.kind ? this.definitionFor(ref.kind) : undefined;
42
+ if (!target && !definition)
43
+ return undefined;
44
+ return resolveContract(direction, target, definition, analyzerContractScope(this.ctx.defs, this.ctx.aliases, this.scopes, this.manifests))?.schema;
45
+ }
46
+ /**
47
+ * The manifest a reference names.
48
+ *
49
+ * An ALIAS narrows before the name does: a flattened set carries every
50
+ * imported library's exported instances, so two libraries exporting a `store`
51
+ * are two manifests with one name. Matching the alias to its target module
52
+ * picks the right one; where the alias resolves to nothing the name alone is
53
+ * used, which is what a local reference needs anyway.
54
+ */
55
+ resolveRef(ref) {
56
+ if (!ref.name)
57
+ return undefined;
58
+ const byName = this.manifests.filter((m) => m.metadata?.name === ref.name);
59
+ if (byName.length === 0)
60
+ return undefined;
61
+ if (byName.length === 1)
62
+ return byName[0];
63
+ const targetModule = ref.alias ? this.ctx.aliases.moduleForAlias?.(ref.alias) : undefined;
64
+ if (targetModule) {
65
+ const scoped = byName.find((m) => m.metadata?.module === targetModule);
66
+ if (scoped)
67
+ return scoped;
68
+ }
69
+ // Several candidates and nothing to choose between them: refusing is the
70
+ // honest answer, since typing a call site against the wrong resource's
71
+ // contract is worse than typing it against none.
72
+ return ref.kind ? byName.find((m) => m.kind === ref.kind) : undefined;
73
+ }
74
+ definitionFor(kind) {
75
+ const canonical = this.ctx.aliases.resolveKind(kind);
76
+ return this.ctx.defs.resolve(kind) ?? (canonical ? this.ctx.defs.resolve(canonical) : undefined);
77
+ }
78
+ }
@@ -0,0 +1,18 @@
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
+ * The value at `path`, or `undefined` when any segment is absent.
13
+ *
14
+ * Absence is what makes a candidate path a CHECK rather than a guess: a caller
15
+ * offering several possible shapes can try each and know a hit is a real node.
16
+ */
17
+ export declare function navigateConcretePath(root: Record<string, any>, path: string): unknown;
18
+ //# sourceMappingURL=manifest-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-path.d.ts","sourceRoot":"","sources":["../src/manifest-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAgBrF"}
@@ -0,0 +1,37 @@
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
+ * The value at `path`, or `undefined` when any segment is absent.
13
+ *
14
+ * Absence is what makes a candidate path a CHECK rather than a guess: a caller
15
+ * offering several possible shapes can try each and know a hit is a real node.
16
+ */
17
+ export function navigateConcretePath(root, path) {
18
+ let current = root;
19
+ for (const segment of path.split(".")) {
20
+ if (!segment)
21
+ continue;
22
+ const match = segment.match(/^([^[]*)((?:\[\d+\])*)$/);
23
+ if (!match)
24
+ return undefined;
25
+ if (match[1]) {
26
+ if (current === null || typeof current !== "object")
27
+ return undefined;
28
+ current = current[match[1]];
29
+ }
30
+ for (const index of match[2].matchAll(/\[(\d+)\]/g)) {
31
+ if (!Array.isArray(current))
32
+ return undefined;
33
+ current = current[Number(index[1])];
34
+ }
35
+ }
36
+ return current;
37
+ }
@@ -0,0 +1,25 @@
1
+ /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
2
+ * Non-refs and unresolved refs pass through unchanged. */
3
+ export declare function resolveLocalRef(schema: Record<string, any> | undefined, root: Record<string, any>): Record<string, any> | undefined;
4
+ /** Gather property schemas from a (possibly variant-bearing) object schema:
5
+ * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
6
+ *
7
+ * Each branch is resolved through {@link resolveLocalRef} first, so a branch
8
+ * that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
9
+ * site — contributes its properties like an inline one. Without that, pointing a
10
+ * composer at a shared shape would silently empty every role-driven lookup that
11
+ * reads this (the inputs slot, the retry policy, the eval paths), which is a
12
+ * failure with no diagnostic attached to it. */
13
+ export declare function gatherPropertySchemas(schema: Record<string, any>, root?: Record<string, any>): Array<[string, Record<string, any>]>;
14
+ /**
15
+ * Generic, role-driven walk over a step array. Calls
16
+ * `visit(step, stepPath)` for every step — top-level and nested through the
17
+ * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
18
+ * the single definition of how steps nest, shared by `buildStepContextSchema`
19
+ * (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
20
+ * checks invoke refs), so the topology contract lives in one place — adding a
21
+ * role or nesting form updates both consumers at once. No resource kind is
22
+ * hardcoded; recursion is driven entirely by the schema annotations.
23
+ */
24
+ export declare function walkStepArray(steps: unknown[], stepItemSchema: Record<string, any> | undefined, rootSchema: Record<string, any>, basePath: string, visit: (step: Record<string, any>, stepPath: string) => void): void;
25
+ //# sourceMappingURL=schema-walk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-walk.d.ts","sourceRoot":"","sources":["../src/schema-walk.ts"],"names":[],"mappings":"AAYA;2DAC2D;AAC3D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EACvC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACxB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAqBjC;AAID;;;;;;;;iDAQiD;AACjD,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACzB,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAsBtC;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EAAE,EAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAC3D,IAAI,CAiDN"}
@@ -0,0 +1,126 @@
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
+ /** Resolve a local `$ref` (only `#/$defs/<name>` form) against the root schema.
13
+ * Non-refs and unresolved refs pass through unchanged. */
14
+ export function resolveLocalRef(schema, root) {
15
+ if (!schema)
16
+ return undefined;
17
+ const ref = schema.$ref;
18
+ if (typeof ref === "string" && ref.startsWith("#/$defs/")) {
19
+ const defName = ref.slice("#/$defs/".length);
20
+ const resolved = root.$defs?.[defName];
21
+ if (resolved && typeof resolved === "object")
22
+ return resolved;
23
+ }
24
+ // A kernel-owned structural fragment (`telo://manifest#/$defs/InvokeStep`).
25
+ // Resolved HERE rather than by each walker: this is the one chokepoint every
26
+ // structural walk already goes through — the step-array walks, the call graph,
27
+ // the zone projection, the eval-path collector — so a composer that points at a
28
+ // shared shape stays legible to all of them at once. Nothing is inlined into
29
+ // the stored schema, which keeps validator-cache identity stable and matches
30
+ // what `resolveSchemaTypeRefs` does for a named user type.
31
+ if (typeof ref === "string" && ref.startsWith(BUILTIN_FRAGMENT_PREFIX)) {
32
+ const defName = ref.slice(BUILTIN_FRAGMENT_PREFIX.length);
33
+ const resolved = ManifestRootSchema.$defs[defName];
34
+ if (resolved && typeof resolved === "object")
35
+ return resolved;
36
+ }
37
+ return schema;
38
+ }
39
+ const BUILTIN_FRAGMENT_PREFIX = `${MANIFEST_SCHEMA_URI}#/$defs/`;
40
+ /** Gather property schemas from a (possibly variant-bearing) object schema:
41
+ * top-level `properties` plus every `oneOf` / `anyOf` / `allOf` branch.
42
+ *
43
+ * Each branch is resolved through {@link resolveLocalRef} first, so a branch
44
+ * that points at a shared shape — a `oneOf` arm that IS the kernel's dispatch
45
+ * site — contributes its properties like an inline one. Without that, pointing a
46
+ * composer at a shared shape would silently empty every role-driven lookup that
47
+ * reads this (the inputs slot, the retry policy, the eval paths), which is a
48
+ * failure with no diagnostic attached to it. */
49
+ export function gatherPropertySchemas(schema, root) {
50
+ const out = [];
51
+ const base = resolveLocalRef(schema, root ?? schema) ?? schema;
52
+ if (base.properties && typeof base.properties === "object") {
53
+ for (const [k, v] of Object.entries(base.properties)) {
54
+ out.push([k, v]);
55
+ }
56
+ }
57
+ for (const variantKey of ["oneOf", "anyOf", "allOf"]) {
58
+ const arr = base[variantKey];
59
+ if (!Array.isArray(arr))
60
+ continue;
61
+ for (const raw of arr) {
62
+ if (!raw || typeof raw !== "object")
63
+ continue;
64
+ const variant = resolveLocalRef(raw, root ?? schema) ?? raw;
65
+ if (variant.properties) {
66
+ for (const [k, v] of Object.entries(variant.properties)) {
67
+ out.push([k, v]);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ return out;
73
+ }
74
+ /**
75
+ * Generic, role-driven walk over a step array. Calls
76
+ * `visit(step, stepPath)` for every step — top-level and nested through the
77
+ * `x-telo-topology-role` forms (`branch`, `branch-list`, `case-map`). This is
78
+ * the single definition of how steps nest, shared by `buildStepContextSchema`
79
+ * (which types `steps.<name>.result`) and `validateStepInvokeReferences` (which
80
+ * checks invoke refs), so the topology contract lives in one place — adding a
81
+ * role or nesting form updates both consumers at once. No resource kind is
82
+ * hardcoded; recursion is driven entirely by the schema annotations.
83
+ */
84
+ export function walkStepArray(steps, stepItemSchema, rootSchema, basePath, visit) {
85
+ const dispatchRole = (data, role, itemsSchema, path) => {
86
+ if (role === "branch" && Array.isArray(data)) {
87
+ walkStepArray(data, stepItemSchema, rootSchema, path, visit);
88
+ }
89
+ else if (role === "case-map" && data && typeof data === "object" && !Array.isArray(data)) {
90
+ for (const [caseKey, arr] of Object.entries(data)) {
91
+ if (Array.isArray(arr))
92
+ walkStepArray(arr, stepItemSchema, rootSchema, `${path}.${caseKey}`, visit);
93
+ }
94
+ }
95
+ else if (role === "branch-list" && Array.isArray(data)) {
96
+ const entrySchema = resolveLocalRef(itemsSchema, rootSchema);
97
+ if (!entrySchema)
98
+ return;
99
+ data.forEach((entry, i) => {
100
+ if (!entry || typeof entry !== "object")
101
+ return;
102
+ for (const [subKey, subSchema] of gatherPropertySchemas(entrySchema)) {
103
+ const subRole = subSchema["x-telo-topology-role"];
104
+ if (typeof subRole !== "string")
105
+ continue;
106
+ dispatchRole(entry[subKey], subRole, subSchema.items, `${path}[${i}].${subKey}`);
107
+ }
108
+ });
109
+ }
110
+ };
111
+ steps.forEach((step, i) => {
112
+ if (!step || typeof step !== "object")
113
+ return;
114
+ const s = step;
115
+ const stepPath = `${basePath}[${i}]`;
116
+ visit(s, stepPath);
117
+ if (!stepItemSchema)
118
+ return;
119
+ for (const [propKey, propSchema] of gatherPropertySchemas(stepItemSchema)) {
120
+ const role = propSchema["x-telo-topology-role"];
121
+ if (typeof role !== "string")
122
+ continue;
123
+ dispatchRole(s[propKey], role, propSchema.items, `${stepPath}.${propKey}`);
124
+ }
125
+ });
126
+ }
@@ -1,3 +1,3 @@
1
1
  /** The surface generation this analyzer implements. */
2
- export declare const TELO_SURFACE_VERSION = "0.79.0";
2
+ export declare const TELO_SURFACE_VERSION = "0.80.0";
3
3
  //# sourceMappingURL=telo-version.d.ts.map
@@ -5,4 +5,4 @@
5
5
  // its release identity, this is the scale a module's `requires.telo` range is
6
6
  // written against, and every kernel in every language reports the same scale.
7
7
  /** The surface generation this analyzer implements. */
8
- export const TELO_SURFACE_VERSION = "0.79.0";
8
+ export const TELO_SURFACE_VERSION = "0.80.0";
@@ -1,5 +1,6 @@
1
1
  import type { AliasResolver, ModuleScopes } from "./alias-resolver.js";
2
2
  import type { DefinitionRegistry } from "./definition-registry.js";
3
+ import { type ReferenceFieldMap } from "./reference-field-map.js";
3
4
  export interface StepInputIssue {
4
5
  path: string;
5
6
  targetLabel: string;
@@ -29,4 +30,20 @@ export declare function collectStepInputIssues(manifest: Record<string, any>, de
29
30
  * caller because building it is analyzer state; without it the contract check
30
31
  * still runs and only the type-argument comparison is skipped. */
31
32
  stepContext?: Record<string, any>): StepInputIssue[];
33
+ /**
34
+ * Validate the argument map of every call this resource makes through a
35
+ * REFERENCE SLOT, as opposed to a step.
36
+ *
37
+ * A slot that transfers control names its argument slot on its own `x-telo-ref`
38
+ * (`inputs:`, a JSON Pointer relative to the object enclosing the slot). That
39
+ * annotation is the only thing tying an otherwise-open `inputs:` map to the
40
+ * resource it holds arguments for — an HTTP route's `handler:` + `inputs:` pair
41
+ * is exactly this shape, and nothing about it is a step.
42
+ *
43
+ * Discovery is driven by the annotation rather than by any kind's topology, so
44
+ * a composer that names its argument slot gets its call sites checked without
45
+ * the analyzer learning what a route is. It is the same check the step driver
46
+ * runs, because it is the same question.
47
+ */
48
+ export declare function collectRefInputIssues(manifest: Record<string, any>, fieldMap: ReferenceFieldMap | undefined, allManifests: Record<string, any>[], defs: DefinitionRegistry, aliases: AliasResolver, scopes: ModuleScopes): StepInputIssue[];
32
49
  //# sourceMappingURL=validate-step-inputs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-step-inputs.d.ts","sourceRoot":"","sources":["../src/validate-step-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAsBnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,4BAA4B,GAAG,oBAAoB,CAAC;CAC5D;AAGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY;AACpB;;mEAEmE;AACnE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,cAAc,EAAE,CAsJlB"}
1
+ {"version":3,"file":"validate-step-inputs.d.ts","sourceRoot":"","sources":["../src/validate-step-inputs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAsBnE,OAAO,EAGL,KAAK,iBAAiB,EACvB,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB;iFAC6E;IAC7E,IAAI,CAAC,EAAE,4BAA4B,GAAG,oBAAoB,CAAC;CAC5D;AAGD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY;AACpB;;mEAEmE;AACnE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAChC,cAAc,EAAE,CA4DlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC7B,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,EACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY,GACnB,cAAc,EAAE,CAoClB"}
@@ -5,6 +5,8 @@ import { isLiveSlot, valueTypeOf } from "@telorun/sdk";
5
5
  import { manifestFragmentOf } from "./manifest-schemas.js";
6
6
  import { analyzerContractScope, containerOf, gatherPropertySchemas, missingRequired, resolveLocalRef, walkStepArray, } from "./analyzer.js";
7
7
  import { readStepSlot } from "./step-slot.js";
8
+ import { navigateConcretePath } from "./manifest-path.js";
9
+ import { isRefEntry, resolveFieldEntries, } from "./reference-field-map.js";
8
10
  /**
9
11
  * Validate every step's `inputs:` against the invoked target's declared input
10
12
  * contract — the static half of what the kernel enforces at dispatch.
@@ -32,6 +34,14 @@ stepContext) {
32
34
  return out;
33
35
  const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
34
36
  const readingModule = manifest.metadata?.module;
37
+ const ctx = {
38
+ manifest,
39
+ allManifests,
40
+ defs,
41
+ contractScope,
42
+ readingModule,
43
+ stepContext,
44
+ };
35
45
  for (const [fieldName, fieldSchema] of Object.entries(props)) {
36
46
  const stepCtx = readStepSlot(fieldSchema);
37
47
  if (!stepCtx)
@@ -58,6 +68,87 @@ stepContext) {
58
68
  return;
59
69
  if (!values || typeof values !== "object" || Array.isArray(values))
60
70
  return;
71
+ out.push(...checkCallSite({
72
+ inputsPath: `${stepPath}.${inputsField}`,
73
+ values: values,
74
+ invoke,
75
+ // Only a step declares a re-attempt policy, so only a step can carry
76
+ // the live-value-retried finding.
77
+ declaredRetryFor: (invokedManifest, invokedDef) => declaredRetry(step, stepItemSchema, invokedManifest, invokedDef),
78
+ }, ctx));
79
+ });
80
+ }
81
+ return out;
82
+ }
83
+ /**
84
+ * Validate the argument map of every call this resource makes through a
85
+ * REFERENCE SLOT, as opposed to a step.
86
+ *
87
+ * A slot that transfers control names its argument slot on its own `x-telo-ref`
88
+ * (`inputs:`, a JSON Pointer relative to the object enclosing the slot). That
89
+ * annotation is the only thing tying an otherwise-open `inputs:` map to the
90
+ * resource it holds arguments for — an HTTP route's `handler:` + `inputs:` pair
91
+ * is exactly this shape, and nothing about it is a step.
92
+ *
93
+ * Discovery is driven by the annotation rather than by any kind's topology, so
94
+ * a composer that names its argument slot gets its call sites checked without
95
+ * the analyzer learning what a route is. It is the same check the step driver
96
+ * runs, because it is the same question.
97
+ */
98
+ export function collectRefInputIssues(manifest, fieldMap, allManifests, defs, aliases, scopes) {
99
+ const out = [];
100
+ if (!fieldMap)
101
+ return out;
102
+ const contractScope = analyzerContractScope(defs, aliases, scopes, allManifests);
103
+ const ctx = {
104
+ manifest,
105
+ allManifests,
106
+ defs,
107
+ contractScope,
108
+ readingModule: manifest.metadata?.module,
109
+ };
110
+ for (const [fieldPath, entry] of fieldMap) {
111
+ if (!isRefEntry(entry) || !entry.inputs)
112
+ continue;
113
+ const pointer = pointerSegments(entry.inputs);
114
+ if (!pointer)
115
+ continue;
116
+ for (const { value: invoke, path: slotPath } of resolveFieldEntries(manifest, fieldPath)) {
117
+ if (!invoke || typeof invoke !== "object" || Array.isArray(invoke))
118
+ continue;
119
+ // Relative to the object ENCLOSING the slot, which is the annotation's
120
+ // documented anchor.
121
+ const enclosing = slotPath.slice(0, Math.max(0, slotPath.lastIndexOf(".")));
122
+ const inputsPath = [enclosing, ...pointer].filter(Boolean).join(".");
123
+ const values = navigateConcretePath(manifest, inputsPath);
124
+ if (!values || typeof values !== "object" || Array.isArray(values))
125
+ continue;
126
+ out.push(...checkCallSite({ inputsPath, values: values, invoke: invoke }, ctx));
127
+ }
128
+ }
129
+ return out;
130
+ }
131
+ /** A JSON Pointer naming a sibling FIELD path. An array index is not a field,
132
+ * so a pointer carrying one names nothing this can resolve. */
133
+ function pointerSegments(pointer) {
134
+ if (!pointer.startsWith("/"))
135
+ return undefined;
136
+ const segments = pointer
137
+ .slice(1)
138
+ .split("/")
139
+ .map((s) => s.replace(/~1/g, "/").replace(/~0/g, "~"));
140
+ return segments.every((s) => s.length > 0 && !/^\d+$/.test(s)) ? segments : undefined;
141
+ }
142
+ /**
143
+ * The check itself, shared by both drivers: the arguments written at a call site
144
+ * against the contract the target declares.
145
+ */
146
+ function checkCallSite(site, ctx) {
147
+ const out = [];
148
+ const { manifest, allManifests, defs, contractScope, readingModule, stepContext } = ctx;
149
+ const { invoke, values } = site;
150
+ {
151
+ {
61
152
  const invokedKind = invoke.kind;
62
153
  const invokedName = invoke.name;
63
154
  const invokedManifest = invokedName
@@ -68,7 +159,7 @@ stepContext) {
68
159
  : undefined;
69
160
  const contract = resolveContract("inputType", invokedManifest, invokedDef, contractScope);
70
161
  if (!contract)
71
- return;
162
+ return out;
72
163
  // Findings AT a substituted path are about a placeholder, not about
73
164
  // anything the author wrote — a `pattern`-constrained string or a `oneOf`
74
165
  // of unrelated shapes cannot be satisfied by any stand-in. Structural
@@ -120,10 +211,10 @@ stepContext) {
120
211
  // already declared: the value's liveness by its value type, and the
121
212
  // re-attempt by the retry policy. No kind is named.
122
213
  if (isLiveSlot(produced)) {
123
- const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
214
+ const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
124
215
  if (retry !== undefined) {
125
216
  out.push({
126
- path: `${stepPath}.${inputsField}.${inputName}`,
217
+ path: `${site.inputsPath}.${inputName}`,
127
218
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
128
219
  message: `'${inputName}' is a live value, which is consumed by reading and so exists ` +
129
220
  `once — but ${retry} re-attempts the dispatch, and a re-attempt would pass ` +
@@ -140,7 +231,7 @@ stepContext) {
140
231
  if (compatible)
141
232
  continue;
142
233
  out.push({
143
- path: `${stepPath}.${inputsField}.${inputName}`,
234
+ path: `${site.inputsPath}.${inputName}`,
144
235
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
145
236
  message: issues.join("; "),
146
237
  code: "CEL_TYPE_ARGUMENT_MISMATCH",
@@ -156,12 +247,12 @@ stepContext) {
156
247
  // container that should have held it, which does exist.
157
248
  const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
158
249
  out.push({
159
- path: anchor ? `${stepPath}.${inputsField}.${anchor}` : `${stepPath}.${inputsField}`,
250
+ path: anchor ? `${site.inputsPath}.${anchor}` : site.inputsPath,
160
251
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
161
252
  message: issue.message,
162
253
  });
163
254
  }
164
- });
255
+ }
165
256
  }
166
257
  return out;
167
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.63.0",
3
+ "version": "0.64.0",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -49,7 +49,7 @@
49
49
  "@types/node": "^20.0.0",
50
50
  "typescript": "^5.0.0",
51
51
  "vitest": "^2.1.8",
52
- "@telorun/sdk": "0.79.0"
52
+ "@telorun/sdk": "0.80.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@telorun/sdk": "*"
@@ -1,6 +1,7 @@
1
1
  import type { ResourceDefinition, ResourceManifest } from "@telorun/sdk";
2
2
  import { AliasResolver } from "./alias-resolver.js";
3
3
  import { KERNEL_BUILTINS } from "./builtins.js";
4
+ import { ManifestAnalysis } from "./manifest-analysis.js";
4
5
  import { DefinitionRegistry } from "./definition-registry.js";
5
6
  import { computeSuggestKind, computeValidUserFacingKinds } from "./kind-suggest.js";
6
7
  import { visitManifest as runVisitManifest, type ManifestVisitor } from "./manifest-visitor.js";
@@ -242,6 +243,42 @@ export class AnalysisRegistry {
242
243
  });
243
244
  }
244
245
 
246
+ /**
247
+ * Resolves an `x-telo-schema-from` annotation to the schema node it derives,
248
+ * in the scope of the kind that DECLARED it — anchors are alias-qualified, and
249
+ * the declaring definition's module is where those aliases mean something.
250
+ *
251
+ * The seam an IDE walks a schema through: a slot shaped entirely by this
252
+ * annotation (an `Http.Api` route's `request:`) carries no `properties` of its
253
+ * own, so a walker that only reads `properties` finds nothing there and
254
+ * silently offers no keys and no hover — a whole field of the standard library
255
+ * looking like an unknown one.
256
+ */
257
+ resolveSchemaFrom(schemaFrom: string, declaringKind: string): Record<string, any> | undefined {
258
+ const def = this.resolveDefinition(declaringKind);
259
+ const ownerModule = (def?.metadata as { module?: string } | undefined)?.module;
260
+ const scope = (ownerModule ? this.aliasesByModule.get(ownerModule) : undefined) ?? this.aliases;
261
+ return this.defs.resolveSchemaFromNode(schemaFrom, scope);
262
+ }
263
+
264
+ /**
265
+ * The queries that need this registry AND a manifest set: what CEL sees at a
266
+ * site, where a binding was declared, what a reference's contract is.
267
+ *
268
+ * The seam an IDE reaches all of them through, and one object rather than a
269
+ * factory per question — each answer has to be the one the analysis pass
270
+ * computed, or a completion list stops being a claim about what `telo check`
271
+ * accepts. Built per analysis, not per keystroke: the indices behind it are a
272
+ * function of the whole manifest set.
273
+ */
274
+ analysisOf(manifests: ResourceManifest[]): ManifestAnalysis {
275
+ return new ManifestAnalysis(manifests, {
276
+ defs: this.defs,
277
+ aliases: this.aliases,
278
+ aliasesByModule: this.aliasesByModule,
279
+ });
280
+ }
281
+
245
282
  /**
246
283
  * Returns the built-in kernel definitions. The underlying DefinitionRegistry already
247
284
  * seeds these on construction; this method exposes them so callers (e.g. the kernel's