@telorun/analyzer 0.63.0 → 0.65.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 (64) 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 +76 -470
  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 +38 -6
  14. package/dist/definition-registry.d.ts.map +1 -1
  15. package/dist/definition-registry.js +66 -22
  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 +11 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +11 -1
  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-compat.d.ts +59 -22
  32. package/dist/schema-compat.d.ts.map +1 -1
  33. package/dist/schema-compat.js +60 -75
  34. package/dist/schema-error-report.d.ts +68 -0
  35. package/dist/schema-error-report.d.ts.map +1 -0
  36. package/dist/schema-error-report.js +356 -0
  37. package/dist/schema-walk.d.ts +25 -0
  38. package/dist/schema-walk.d.ts.map +1 -0
  39. package/dist/schema-walk.js +126 -0
  40. package/dist/telo-version.d.ts +1 -1
  41. package/dist/telo-version.js +1 -1
  42. package/dist/validate-nested-inline.d.ts +22 -1
  43. package/dist/validate-nested-inline.d.ts.map +1 -1
  44. package/dist/validate-nested-inline.js +17 -9
  45. package/dist/validate-step-inputs.d.ts +17 -0
  46. package/dist/validate-step-inputs.d.ts.map +1 -1
  47. package/dist/validate-step-inputs.js +108 -9
  48. package/package.json +2 -2
  49. package/src/analysis-registry.ts +37 -0
  50. package/src/analyzer.ts +83 -587
  51. package/src/cel-scope-query.ts +337 -0
  52. package/src/cel-scope.ts +570 -0
  53. package/src/definition-registry.ts +79 -24
  54. package/src/find-manifest.ts +19 -0
  55. package/src/index.ts +23 -2
  56. package/src/invocation-contract.ts +22 -0
  57. package/src/manifest-analysis.ts +132 -0
  58. package/src/manifest-path.ts +34 -0
  59. package/src/schema-compat.ts +92 -79
  60. package/src/schema-error-report.ts +417 -0
  61. package/src/schema-walk.ts +144 -0
  62. package/src/telo-version.ts +1 -1
  63. package/src/validate-nested-inline.ts +35 -14
  64. package/src/validate-step-inputs.ts +153 -11
@@ -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.82.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.82.0";
@@ -1,4 +1,6 @@
1
1
  import type { ResourceManifest } from "@telorun/sdk";
2
+ import type { ExternalSchemaResolver } from "./schema-compat.js";
3
+ import type { SchemaIssue } from "./schema-error-report.js";
2
4
  import { type AnalysisDiagnostic } from "./types.js";
3
5
  /** Minimal view of a definition needed to validate an inline resource's config. */
4
6
  export interface InlineDefinitionLookup {
@@ -6,6 +8,21 @@ export interface InlineDefinitionLookup {
6
8
  schema?: Record<string, any>;
7
9
  } | undefined;
8
10
  }
11
+ /**
12
+ * The validator this pass checks an inline resource's config with, and the
13
+ * resolver that lets both it and the stand-in walk see through a named shape.
14
+ *
15
+ * Passed in rather than reached for: the module-level AJV this used has no
16
+ * registered shapes, so a kind whose `schema:` references one compiled nowhere
17
+ * and every inline declaration of it was silently unchecked — while the
18
+ * identical resource written standalone was checked, and the kernel rejected
19
+ * both at boot. Two validators answering one question is what allowed that, so
20
+ * the caller supplies the one that holds the shapes.
21
+ */
22
+ export interface InlineConfigValidator {
23
+ validate(data: unknown, schema: Record<string, any>): SchemaIssue[];
24
+ external: ExternalSchemaResolver;
25
+ }
9
26
  /**
10
27
  * Validates inline resources nested inside a resource body against their kind's
11
28
  * config schema. The per-resource walk in `analyze()` validates a resource's
@@ -28,5 +45,9 @@ export interface InlineDefinitionLookup {
28
45
  */
29
46
  export declare function validateNestedInlineResources(manifest: ResourceManifest, rootSchema: Record<string, any>, lookupDefinition: InlineDefinitionLookup,
30
47
  /** Needed to resolve a `telo#Type` field a value slot is validated against. */
31
- allManifests?: Record<string, any>[]): AnalysisDiagnostic[];
48
+ allManifests: Record<string, any>[],
49
+ /** REQUIRED, and deliberately not defaulted: a default would be a second
50
+ * validator answering the same question, and omitting it would silently stop
51
+ * checking rather than fail. */
52
+ validator: InlineConfigValidator): AnalysisDiagnostic[];
32
53
  //# sourceMappingURL=validate-nested-inline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate-nested-inline.d.ts","sourceRoot":"","sources":["../src/validate-nested-inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAQrD,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKzE,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,gBAAgB,EAAE,sBAAsB;AACxC,+EAA+E;AAC/E,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAO,GACvC,kBAAkB,EAAE,CA6HtB"}
1
+ {"version":3,"file":"validate-nested-inline.d.ts","sourceRoot":"","sources":["../src/validate-nested-inline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAsB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAKzE,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;CAC9D;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,WAAW,EAAE,CAAC;IACpE,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,6BAA6B,CAC3C,QAAQ,EAAE,gBAAgB,EAC1B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,gBAAgB,EAAE,sBAAsB;AACxC,+EAA+E;AAC/E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE;AACnC;;iCAEiC;AACjC,SAAS,EAAE,qBAAqB,GAC/B,kBAAkB,EAAE,CAiItB"}
@@ -1,5 +1,5 @@
1
1
  import { collectRefs, isInlineResource } from "./reference-field-map.js";
2
- import { collectProperties, resolveRef, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
2
+ import { collectProperties, resolveRef, substituteCelFields } from "./schema-compat.js";
3
3
  import { DiagnosticSeverity } from "./types.js";
4
4
  import { collectValueSchemaIssues } from "./validate-value-schema.js";
5
5
  const SOURCE = "telo-analyzer";
@@ -25,7 +25,11 @@ const SOURCE = "telo-analyzer";
25
25
  */
26
26
  export function validateNestedInlineResources(manifest, rootSchema, lookupDefinition,
27
27
  /** Needed to resolve a `telo#Type` field a value slot is validated against. */
28
- allManifests = []) {
28
+ allManifests,
29
+ /** REQUIRED, and deliberately not defaulted: a default would be a second
30
+ * validator answering the same question, and omitting it would silently stop
31
+ * checking rather than fail. */
32
+ validator) {
29
33
  const diagnostics = [];
30
34
  const resource = { kind: manifest.kind, name: manifest.metadata?.name };
31
35
  const filePath = manifest.metadata?.source;
@@ -70,14 +74,18 @@ allManifests = []) {
70
74
  ? inline.metadata
71
75
  : {};
72
76
  const data = { ...inline, metadata: { name: "__inline__", ...existingMeta } };
73
- const substituted = substituteCelFields(data, effectiveSchema, effectiveSchema);
74
- // The same two passes the top-level resource loop runs, so a kind's
75
- // guarantees don't depend on whether the author wrote it standalone or
76
- // inline (under a step's `invoke:`, or in a `with:` scope). `data` carries
77
- // the synthesized metadata; `x-telo-value-schema-from` reads sibling fields
78
- // off the resource, which are present either way.
77
+ const substituted = substituteCelFields(data, effectiveSchema, effectiveSchema, {
78
+ external: validator.external,
79
+ });
80
+ // The same two passes the top-level resource loop runs, on the same
81
+ // validator, so a kind's guarantees don't depend on whether the author wrote
82
+ // it standalone or inline (under a step's `invoke:`, or in a `with:` scope)
83
+ // — and CLAUDE.md mandates inline for a single-use resource, so inline is
84
+ // the common shape rather than the exception. `data` carries the synthesized
85
+ // metadata; `x-telo-value-schema-from` reads sibling fields off the
86
+ // resource, which are present either way.
79
87
  const inlineIssues = [
80
- ...validateAgainstSchema(substituted, effectiveSchema),
88
+ ...validator.validate(substituted, effectiveSchema),
81
89
  ...collectValueSchemaIssues(data, schema, allManifests),
82
90
  ];
83
91
  for (const issue of inlineIssues) {
@@ -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"}
@@ -1,10 +1,12 @@
1
1
  import { resolveContract } from "./invocation-contract.js";
2
- import { checkSchemaCompatibility, navigateSchemaToExprPath, substituteCelFields, validateAgainstSchema, } from "./schema-compat.js";
2
+ import { checkSchemaCompatibility, navigateSchemaToExprPath, substituteCelFields, } from "./schema-compat.js";
3
3
  import { plainChainOf } from "@telorun/templating";
4
4
  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,14 +159,22 @@ 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
75
166
  // findings (missing required, unknown property) are located at the
76
167
  // container and survive the filter.
77
168
  const celPaths = new Set();
78
- const substituted = substituteCelFields(values, contract.schema, undefined, (p) => celPaths.add(p));
169
+ const substituted = substituteCelFields(values, contract.schema, undefined, {
170
+ onSubstitute: (p) => celPaths.add(p),
171
+ // A contract may name a shape declared elsewhere. Both halves need the
172
+ // resolver or they disagree about the same slot: the stand-in walk hands
173
+ // its expressions a typeless value, and the check below compiles nothing
174
+ // at all — so a step's arguments went unchecked against exactly the
175
+ // contracts that describe them most precisely.
176
+ external: (ref) => defs.schemaForId(ref),
177
+ });
79
178
  // The type-argument check, at the one site where a produced value's schema
80
179
  // meets a consuming slot's. A CEL leaf's placeholder says nothing about
81
180
  // what the expression yields, so AJV above is silent here by design — and
@@ -120,10 +219,10 @@ stepContext) {
120
219
  // already declared: the value's liveness by its value type, and the
121
220
  // re-attempt by the retry policy. No kind is named.
122
221
  if (isLiveSlot(produced)) {
123
- const retry = declaredRetry(step, stepItemSchema, invokedManifest, invokedDef);
222
+ const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
124
223
  if (retry !== undefined) {
125
224
  out.push({
126
- path: `${stepPath}.${inputsField}.${inputName}`,
225
+ path: `${site.inputsPath}.${inputName}`,
127
226
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
128
227
  message: `'${inputName}' is a live value, which is consumed by reading and so exists ` +
129
228
  `once — but ${retry} re-attempts the dispatch, and a re-attempt would pass ` +
@@ -140,14 +239,14 @@ stepContext) {
140
239
  if (compatible)
141
240
  continue;
142
241
  out.push({
143
- path: `${stepPath}.${inputsField}.${inputName}`,
242
+ path: `${site.inputsPath}.${inputName}`,
144
243
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
145
244
  message: issues.join("; "),
146
245
  code: "CEL_TYPE_ARGUMENT_MISMATCH",
147
246
  });
148
247
  }
149
248
  }
150
- for (const issue of validateAgainstSchema(substituted, contract.schema)) {
249
+ for (const issue of defs.validateResourceConfig(substituted, contract.schema)) {
151
250
  if (celPaths.has(issue.path))
152
251
  continue;
153
252
  // A missing-required issue names the property that ISN'T there, so
@@ -156,12 +255,12 @@ stepContext) {
156
255
  // container that should have held it, which does exist.
157
256
  const anchor = missingRequired(issue) ? containerOf(issue.path) : issue.path;
158
257
  out.push({
159
- path: anchor ? `${stepPath}.${inputsField}.${anchor}` : `${stepPath}.${inputsField}`,
258
+ path: anchor ? `${site.inputsPath}.${anchor}` : site.inputsPath,
160
259
  targetLabel: invokedName ?? invokedKind ?? "the invoked resource",
161
260
  message: issue.message,
162
261
  });
163
262
  }
164
- });
263
+ }
165
264
  }
166
265
  return out;
167
266
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.63.0",
3
+ "version": "0.65.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.82.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