@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.
- package/dist/analysis-registry.d.ts +24 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +35 -0
- package/dist/analyzer.d.ts +3 -37
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +76 -470
- package/dist/cel-scope-query.d.ts +109 -0
- package/dist/cel-scope-query.d.ts.map +1 -0
- package/dist/cel-scope-query.js +270 -0
- package/dist/cel-scope.d.ts +166 -0
- package/dist/cel-scope.d.ts.map +1 -0
- package/dist/cel-scope.js +377 -0
- package/dist/definition-registry.d.ts +38 -6
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +66 -22
- package/dist/find-manifest.d.ts +10 -0
- package/dist/find-manifest.d.ts.map +1 -0
- package/dist/find-manifest.js +12 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/invocation-contract.d.ts +11 -0
- package/dist/invocation-contract.d.ts.map +1 -1
- package/dist/invocation-contract.js +15 -0
- package/dist/manifest-analysis.d.ts +73 -0
- package/dist/manifest-analysis.d.ts.map +1 -0
- package/dist/manifest-analysis.js +78 -0
- package/dist/manifest-path.d.ts +18 -0
- package/dist/manifest-path.d.ts.map +1 -0
- package/dist/manifest-path.js +37 -0
- package/dist/schema-compat.d.ts +59 -22
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +60 -75
- package/dist/schema-error-report.d.ts +68 -0
- package/dist/schema-error-report.d.ts.map +1 -0
- package/dist/schema-error-report.js +356 -0
- package/dist/schema-walk.d.ts +25 -0
- package/dist/schema-walk.d.ts.map +1 -0
- package/dist/schema-walk.js +126 -0
- package/dist/telo-version.d.ts +1 -1
- package/dist/telo-version.js +1 -1
- package/dist/validate-nested-inline.d.ts +22 -1
- package/dist/validate-nested-inline.d.ts.map +1 -1
- package/dist/validate-nested-inline.js +17 -9
- package/dist/validate-step-inputs.d.ts +17 -0
- package/dist/validate-step-inputs.d.ts.map +1 -1
- package/dist/validate-step-inputs.js +108 -9
- package/package.json +2 -2
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +83 -587
- package/src/cel-scope-query.ts +337 -0
- package/src/cel-scope.ts +570 -0
- package/src/definition-registry.ts +79 -24
- package/src/find-manifest.ts +19 -0
- package/src/index.ts +23 -2
- package/src/invocation-contract.ts +22 -0
- package/src/manifest-analysis.ts +132 -0
- package/src/manifest-path.ts +34 -0
- package/src/schema-compat.ts +92 -79
- package/src/schema-error-report.ts +417 -0
- package/src/schema-walk.ts +144 -0
- package/src/telo-version.ts +1 -1
- package/src/validate-nested-inline.ts +35 -14
- 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
|
+
}
|
package/dist/telo-version.d.ts
CHANGED
package/dist/telo-version.js
CHANGED
|
@@ -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.
|
|
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
|
|
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;
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
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
|
-
...
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
222
|
+
const retry = site.declaredRetryFor?.(invokedManifest, invokedDef);
|
|
124
223
|
if (retry !== undefined) {
|
|
125
224
|
out.push({
|
|
126
|
-
path: `${
|
|
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: `${
|
|
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
|
|
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 ? `${
|
|
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.
|
|
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.
|
|
52
|
+
"@telorun/sdk": "0.82.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@telorun/sdk": "*"
|
package/src/analysis-registry.ts
CHANGED
|
@@ -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
|