@telorun/analyzer 0.10.1 → 0.12.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/LICENSE +2 -2
- package/README.md +3 -3
- package/dist/adapters/http-adapter.d.ts +10 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +18 -0
- package/dist/adapters/node-adapter.d.ts +17 -0
- package/dist/adapters/node-adapter.d.ts.map +1 -0
- package/dist/adapters/node-adapter.js +71 -0
- package/dist/adapters/registry-adapter.d.ts +15 -0
- package/dist/adapters/registry-adapter.d.ts.map +1 -0
- package/dist/adapters/registry-adapter.js +53 -0
- package/dist/analysis-registry.d.ts +7 -0
- package/dist/analysis-registry.d.ts.map +1 -1
- package/dist/analysis-registry.js +38 -0
- package/dist/analyzer.d.ts +15 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +268 -7
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +172 -1
- package/dist/definition-registry.d.ts.map +1 -1
- package/dist/definition-registry.js +16 -0
- package/dist/dependency-graph.d.ts.map +1 -1
- package/dist/dependency-graph.js +27 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/kernel-globals.d.ts.map +1 -1
- package/dist/kernel-globals.js +9 -11
- package/dist/manifest-loader.d.ts +23 -1
- package/dist/manifest-loader.d.ts.map +1 -1
- package/dist/manifest-loader.js +66 -3
- package/dist/normalize-inline-resources.d.ts.map +1 -1
- package/dist/normalize-inline-resources.js +26 -14
- package/dist/position-metadata.d.ts +11 -2
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +18 -3
- package/dist/precompile.d.ts.map +1 -1
- package/dist/precompile.js +9 -1
- package/dist/reference-field-map.d.ts +21 -4
- package/dist/reference-field-map.d.ts.map +1 -1
- package/dist/reference-field-map.js +93 -25
- package/dist/residual-schema.d.ts +23 -0
- package/dist/residual-schema.d.ts.map +1 -0
- package/dist/residual-schema.js +45 -0
- package/dist/resolve-ref-sentinels.d.ts +27 -0
- package/dist/resolve-ref-sentinels.d.ts.map +1 -0
- package/dist/resolve-ref-sentinels.js +114 -0
- package/dist/rewrite-synthetic-origins.d.ts +10 -0
- package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
- package/dist/rewrite-synthetic-origins.js +55 -0
- package/dist/schema-compat.d.ts +7 -1
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +19 -2
- package/dist/system-kinds.d.ts +25 -0
- package/dist/system-kinds.d.ts.map +1 -0
- package/dist/system-kinds.js +34 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts +61 -7
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +90 -8
- package/dist/validate-provider-coherence.d.ts +23 -0
- package/dist/validate-provider-coherence.d.ts.map +1 -0
- package/dist/validate-provider-coherence.js +148 -0
- package/dist/validate-references.d.ts.map +1 -1
- package/dist/validate-references.js +141 -36
- package/dist/with-synthetic-positions.d.ts +28 -0
- package/dist/with-synthetic-positions.d.ts.map +1 -0
- package/dist/with-synthetic-positions.js +45 -0
- package/package.json +7 -4
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +313 -9
- package/src/builtins.ts +172 -1
- package/src/definition-registry.ts +15 -0
- package/src/dependency-graph.ts +27 -14
- package/src/index.ts +2 -0
- package/src/kernel-globals.ts +9 -11
- package/src/manifest-loader.ts +69 -4
- package/src/normalize-inline-resources.ts +48 -13
- package/src/position-metadata.ts +18 -3
- package/src/precompile.ts +8 -1
- package/src/reference-field-map.ts +129 -24
- package/src/residual-schema.ts +49 -0
- package/src/resolve-ref-sentinels.ts +127 -0
- package/src/rewrite-synthetic-origins.ts +75 -0
- package/src/schema-compat.ts +19 -2
- package/src/system-kinds.ts +37 -0
- package/src/types.ts +12 -0
- package/src/validate-cel-context.ts +111 -8
- package/src/validate-provider-coherence.ts +166 -0
- package/src/validate-references.ts +138 -35
- package/src/with-synthetic-positions.ts +48 -0
package/src/analyzer.ts
CHANGED
|
@@ -14,6 +14,9 @@ import { buildKernelGlobalsSchema, mergeKernelGlobalsIntoContext } from "./kerne
|
|
|
14
14
|
import { computeSuggestKind } from "./kind-suggest.js";
|
|
15
15
|
import { isModuleKind } from "./module-kinds.js";
|
|
16
16
|
import { normalizeInlineResources } from "./normalize-inline-resources.js";
|
|
17
|
+
import { REF_VALIDATION_SKIP_KINDS } from "./system-kinds.js";
|
|
18
|
+
import { resolveRefSentinels } from "./resolve-ref-sentinels.js";
|
|
19
|
+
import { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
17
20
|
import {
|
|
18
21
|
celTypeSatisfiesJsonSchema,
|
|
19
22
|
substituteCelFields,
|
|
@@ -28,11 +31,45 @@ import {
|
|
|
28
31
|
resolveTypeFieldToSchema,
|
|
29
32
|
} from "./validate-cel-context.js";
|
|
30
33
|
import { validateExtends } from "./validate-extends.js";
|
|
34
|
+
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
31
35
|
import { validateReferences } from "./validate-references.js";
|
|
32
36
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
33
37
|
|
|
34
38
|
const SELF_PREFIX = "Self.";
|
|
35
39
|
|
|
40
|
+
/**
|
|
41
|
+
* `StaticAnalyzer.analyze()` requires `metadata.source` (non-empty) and
|
|
42
|
+
* `metadata.sourceLine` (number) on every non-system manifest — see the
|
|
43
|
+
* JSDoc on `analyze()`. Production callers stamp these via the `Loader` /
|
|
44
|
+
* `flattenForAnalyzer` / `emitDocsFor` paths; programmatic callers (tests,
|
|
45
|
+
* scripts) should pre-process inputs with `withSyntheticPositions(...)`.
|
|
46
|
+
* Surfacing the violation here turns silent dedup misbehaviour into a
|
|
47
|
+
* loud, actionable error.
|
|
48
|
+
*/
|
|
49
|
+
function assertManifestPositions(manifests: ResourceManifest[]): void {
|
|
50
|
+
for (let i = 0; i < manifests.length; i++) {
|
|
51
|
+
const m = manifests[i];
|
|
52
|
+
if (REF_VALIDATION_SKIP_KINDS.has(m.kind)) continue;
|
|
53
|
+
const meta = m.metadata as { source?: string; sourceLine?: number } | undefined;
|
|
54
|
+
const okSource = typeof meta?.source === "string" && meta.source.length > 0;
|
|
55
|
+
const okLine = typeof meta?.sourceLine === "number";
|
|
56
|
+
if (okSource && okLine) continue;
|
|
57
|
+
const label = `${m.kind}/${m.metadata?.name ?? "(unnamed)"}`;
|
|
58
|
+
const missing = [
|
|
59
|
+
!okSource ? "metadata.source" : null,
|
|
60
|
+
!okLine ? "metadata.sourceLine" : null,
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join(" and ");
|
|
64
|
+
throw new Error(
|
|
65
|
+
`StaticAnalyzer.analyze(): manifest #${i} (${label}) is missing ${missing}. ` +
|
|
66
|
+
`Real callers stamp positions automatically; programmatic callers ` +
|
|
67
|
+
`(tests, ad-hoc scripts) should pass inputs through ` +
|
|
68
|
+
`\`withSyntheticPositions(manifests)\` before calling analyze().`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
36
73
|
/** Resolve an alias-prefixed kind value (e.g. `Self.Encoder` or `Ai.Model`)
|
|
37
74
|
* to its canonical form. `Self.<Name>` resolves to `<ownModule>.<Name>` —
|
|
38
75
|
* the magic alias for "this library's own module" — and other prefixes
|
|
@@ -68,14 +105,108 @@ function lookupDefinitionTypeField(
|
|
|
68
105
|
|
|
69
106
|
const SOURCE = "telo-analyzer";
|
|
70
107
|
|
|
108
|
+
/** Build a closed JSON Schema for the `self` CEL variable available inside a
|
|
109
|
+
* `Telo.Definition` template body. Mirrors the runtime template controller's
|
|
110
|
+
* `const self = { ...resource, name: resource.metadata.name };` — every
|
|
111
|
+
* property the user declared in `schema:` plus synthetic `name` / `kind` and
|
|
112
|
+
* the metadata sub-object (kept open since metadata legitimately carries
|
|
113
|
+
* arbitrary user-added fields). */
|
|
114
|
+
function buildSelfSchema(definition: Record<string, any>): Record<string, any> {
|
|
115
|
+
const userSchema = (definition.schema ?? {}) as Record<string, any>;
|
|
116
|
+
const userProps = (userSchema.properties ?? {}) as Record<string, any>;
|
|
117
|
+
const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
|
|
118
|
+
return {
|
|
119
|
+
type: "object",
|
|
120
|
+
additionalProperties: false,
|
|
121
|
+
properties: {
|
|
122
|
+
...userProps,
|
|
123
|
+
name: { type: "string" },
|
|
124
|
+
kind: { type: "string" },
|
|
125
|
+
metadata: {
|
|
126
|
+
type: "object",
|
|
127
|
+
additionalProperties: true,
|
|
128
|
+
properties: { name: { type: "string" } },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
required: [...userRequired, "name", "kind"],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Build the JSON Schema for the `inputs` CEL variable available inside an
|
|
136
|
+
* invocable template body. Three-layer fallback mirroring the runtime's
|
|
137
|
+
* caller-supplied inputs:
|
|
138
|
+
* 1. The definition's own `inputType:` field (preferred).
|
|
139
|
+
* 2. The `extends:`-declared abstract's `inputType:` (so a concrete
|
|
140
|
+
* definition inheriting a contract gets typed inputs without
|
|
141
|
+
* redeclaring them).
|
|
142
|
+
* 3. Undefined — caller signals opaque `map<string, dyn>` upstream. */
|
|
143
|
+
function lookupTemplateInputsSchema(
|
|
144
|
+
definition: Record<string, any>,
|
|
145
|
+
defs: DefinitionRegistry,
|
|
146
|
+
aliases: AliasResolver,
|
|
147
|
+
allManifests: Record<string, any>[],
|
|
148
|
+
): Record<string, any> | undefined {
|
|
149
|
+
const own = resolveTypeFieldToSchema(definition.inputType, allManifests);
|
|
150
|
+
if (own) return own;
|
|
151
|
+
const ext = definition.extends as string | undefined;
|
|
152
|
+
if (typeof ext === "string" && ext.length > 0) {
|
|
153
|
+
const canonical = aliases.resolveKind(ext) ?? ext;
|
|
154
|
+
const abstractDef = defs.resolve(canonical);
|
|
155
|
+
if (abstractDef) {
|
|
156
|
+
const inherited = resolveTypeFieldToSchema(
|
|
157
|
+
(abstractDef as unknown as Record<string, unknown>).inputType,
|
|
158
|
+
allManifests,
|
|
159
|
+
);
|
|
160
|
+
if (inherited) return inherited;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Returns a "resolver-facing" view of the manifest where the fields used as
|
|
167
|
+
* navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
|
|
168
|
+
* have been pre-augmented:
|
|
169
|
+
* - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
|
|
170
|
+
* - `inputType` → resolved with extends fallback when the field isn't
|
|
171
|
+
* declared directly on the definition.
|
|
172
|
+
*
|
|
173
|
+
* For non-definition manifests the original object is returned. */
|
|
174
|
+
function manifestRootForResolver(
|
|
175
|
+
m: Record<string, any>,
|
|
176
|
+
defs: DefinitionRegistry,
|
|
177
|
+
aliases: AliasResolver,
|
|
178
|
+
allManifests: Record<string, any>[],
|
|
179
|
+
): Record<string, any> {
|
|
180
|
+
if (m.kind !== "Telo.Definition") return m;
|
|
181
|
+
const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests);
|
|
182
|
+
return {
|
|
183
|
+
...m,
|
|
184
|
+
schema: buildSelfSchema(m),
|
|
185
|
+
...(inputs ? { inputType: inputs } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
71
189
|
/**
|
|
72
190
|
* Walk a JSON Schema tree and collect all `x-telo-context` annotations,
|
|
73
191
|
* returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
|
|
74
192
|
* the same format the analyzer uses for CEL context validation.
|
|
193
|
+
*
|
|
194
|
+
* Result is sorted by scope specificity (longer scope first) so that the
|
|
195
|
+
* per-expression resolver's first-match-wins logic picks the most-specific
|
|
196
|
+
* context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
|
|
197
|
+
* could shadow a narrower descendant scope whose activation differs.
|
|
75
198
|
*/
|
|
76
199
|
function extractContextsFromSchema(
|
|
77
200
|
schema: Record<string, any>,
|
|
78
201
|
path = "$",
|
|
202
|
+
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
203
|
+
const all = collectContexts(schema, path);
|
|
204
|
+
return all.sort((a, b) => b.scope.length - a.scope.length);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function collectContexts(
|
|
208
|
+
schema: Record<string, any>,
|
|
209
|
+
path: string,
|
|
79
210
|
): Array<{ scope: string; schema: Record<string, any> }> {
|
|
80
211
|
if (!schema || typeof schema !== "object") return [];
|
|
81
212
|
const results: Array<{ scope: string; schema: Record<string, any> }> = [];
|
|
@@ -86,18 +217,18 @@ function extractContextsFromSchema(
|
|
|
86
217
|
|
|
87
218
|
if (schema.properties) {
|
|
88
219
|
for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
|
|
89
|
-
results.push(...
|
|
220
|
+
results.push(...collectContexts(value, `${path}.${key}`));
|
|
90
221
|
}
|
|
91
222
|
}
|
|
92
223
|
|
|
93
224
|
if (schema.items && typeof schema.items === "object") {
|
|
94
|
-
results.push(...
|
|
225
|
+
results.push(...collectContexts(schema.items, `${path}[*]`));
|
|
95
226
|
}
|
|
96
227
|
|
|
97
228
|
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
|
|
98
229
|
if (Array.isArray(schema[key])) {
|
|
99
230
|
for (const subschema of schema[key]) {
|
|
100
|
-
results.push(...
|
|
231
|
+
results.push(...collectContexts(subschema, path));
|
|
101
232
|
}
|
|
102
233
|
}
|
|
103
234
|
}
|
|
@@ -399,11 +530,27 @@ export class StaticAnalyzer {
|
|
|
399
530
|
this.celEnv = buildCelEnvironment(options.celHandlers);
|
|
400
531
|
}
|
|
401
532
|
|
|
533
|
+
/**
|
|
534
|
+
* Run static analysis over a flattened manifest list.
|
|
535
|
+
*
|
|
536
|
+
* **Contract**: every non-system manifest (anything outside `Telo.Definition`,
|
|
537
|
+
* `Telo.Abstract`) must carry `metadata.source` (non-empty string) and
|
|
538
|
+
* `metadata.sourceLine` (number). The dedup that backs
|
|
539
|
+
* `DUPLICATE_RESOURCE_NAME` reads those fields to tell a pipeline echo
|
|
540
|
+
* apart from a genuine collision, and downstream diagnostic positioning
|
|
541
|
+
* depends on them too. Real callers stamp positions already (the `Loader`,
|
|
542
|
+
* `flattenForAnalyzer`, the telo-editor's `emitDocsFor`, the VSCode
|
|
543
|
+
* extension). Programmatic callers — tests, ad-hoc scripts — should pass
|
|
544
|
+
* their inputs through `withSyntheticPositions(...)` before calling
|
|
545
|
+
* `analyze()`. A missing position throws a clear error rather than
|
|
546
|
+
* silently producing wrong diagnostics.
|
|
547
|
+
*/
|
|
402
548
|
analyze(
|
|
403
549
|
manifests: ResourceManifest[],
|
|
404
550
|
options?: AnalysisOptions,
|
|
405
551
|
registry?: AnalysisRegistry,
|
|
406
552
|
): AnalysisDiagnostic[] {
|
|
553
|
+
assertManifestPositions(manifests);
|
|
407
554
|
const diagnostics: AnalysisDiagnostic[] = [];
|
|
408
555
|
|
|
409
556
|
// Use pre-seeded registries from the provided AnalysisRegistry, or create fresh ones.
|
|
@@ -527,6 +674,22 @@ export class StaticAnalyzer {
|
|
|
527
674
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
528
675
|
const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
|
|
529
676
|
|
|
677
|
+
// Phase 2.5: resolve `!ref <name>` sentinels at every ref slot to canonical
|
|
678
|
+
// {kind, name} objects so downstream phases (validation, dependency graph,
|
|
679
|
+
// kernel controllers) see a uniform shape. Runs after normalize so both
|
|
680
|
+
// original and inline-extracted manifests have their sentinels resolved.
|
|
681
|
+
resolveRefSentinels(allManifests, defs, aliases, aliasesByModule);
|
|
682
|
+
|
|
683
|
+
// Trusted-input fast path: when the caller has already attested that
|
|
684
|
+
// this exact manifest set passes analysis (e.g. via the kernel's
|
|
685
|
+
// hash-stamped `.validated.json` cache), skip the validation walk.
|
|
686
|
+
// Registration of identities / aliases / definitions and inline-resource
|
|
687
|
+
// normalisation have already run above; that's all downstream
|
|
688
|
+
// consumers (prepare, init loop) require.
|
|
689
|
+
if (options?.skipValidation) {
|
|
690
|
+
return diagnostics;
|
|
691
|
+
}
|
|
692
|
+
|
|
530
693
|
// Build a name→manifest map for looking up referenced resources
|
|
531
694
|
const byName = new Map<string, ResourceManifest>();
|
|
532
695
|
for (const m of allManifests) {
|
|
@@ -535,6 +698,35 @@ export class StaticAnalyzer {
|
|
|
535
698
|
}
|
|
536
699
|
}
|
|
537
700
|
|
|
701
|
+
// Library env: rejection — `env:` on a Library `variables` / `secrets`
|
|
702
|
+
// entry is forbidden. The Library entry schema is otherwise open so that
|
|
703
|
+
// any JSON Schema property schema is valid; this targeted check produces
|
|
704
|
+
// a clear diagnostic instead of a generic "additional property" error.
|
|
705
|
+
for (const m of allManifests) {
|
|
706
|
+
if (m.kind !== "Telo.Library") continue;
|
|
707
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
708
|
+
const moduleName = m.metadata?.name as string | undefined;
|
|
709
|
+
const resource = moduleName ? { kind: m.kind, name: moduleName } : undefined;
|
|
710
|
+
for (const block of ["variables", "secrets"] as const) {
|
|
711
|
+
const entries = (m as Record<string, any>)[block];
|
|
712
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
|
|
713
|
+
for (const [entryName, entry] of Object.entries(entries)) {
|
|
714
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
715
|
+
if ("env" in (entry as Record<string, unknown>)) {
|
|
716
|
+
diagnostics.push({
|
|
717
|
+
severity: DiagnosticSeverity.Error,
|
|
718
|
+
code: "LIBRARY_ENV_KEY_REJECTED",
|
|
719
|
+
source: SOURCE,
|
|
720
|
+
message:
|
|
721
|
+
`Telo.Library ${block}/${entryName}: 'env:' is only permitted on Telo.Application entries. ` +
|
|
722
|
+
`Libraries must receive values from importers via the parent manifest's variables / secrets block.`,
|
|
723
|
+
data: { resource, filePath, path: `${block}.${entryName}.env` },
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
538
730
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
539
731
|
// recognises variables, secrets, resources, env automatically
|
|
540
732
|
const kernelGlobals = buildKernelGlobalsSchema(allManifests);
|
|
@@ -552,7 +744,11 @@ export class StaticAnalyzer {
|
|
|
552
744
|
});
|
|
553
745
|
continue;
|
|
554
746
|
}
|
|
555
|
-
|
|
747
|
+
// Abstracts carry only inputType / outputType schema fields and no template
|
|
748
|
+
// body — nothing for the per-resource walk to validate. Definitions are now
|
|
749
|
+
// walked: their template bodies (`resources` / `invoke` / `run` / `provide`)
|
|
750
|
+
// contain CEL that must be checked against `self` / `inputs` / `result`.
|
|
751
|
+
if (m.kind === "Telo.Abstract") {
|
|
556
752
|
continue;
|
|
557
753
|
}
|
|
558
754
|
|
|
@@ -612,6 +808,97 @@ export class StaticAnalyzer {
|
|
|
612
808
|
// (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
|
|
613
809
|
}
|
|
614
810
|
|
|
811
|
+
// Template-body structural validations: check that template entry-points produce
|
|
812
|
+
// values matching the contract of their dispatch target and (for `provide:`)
|
|
813
|
+
// the abstract this definition `extends`. CEL fields inside the templated
|
|
814
|
+
// values are replaced with type-appropriate placeholders before AJV runs —
|
|
815
|
+
// same pattern as the per-resource schema validation above.
|
|
816
|
+
for (const m of allManifests) {
|
|
817
|
+
if (m.kind !== "Telo.Definition") continue;
|
|
818
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
819
|
+
const name = (m.metadata as any)?.name as string | undefined;
|
|
820
|
+
if (!name) continue;
|
|
821
|
+
const resource = { kind: m.kind, name };
|
|
822
|
+
const md = m as Record<string, any>;
|
|
823
|
+
|
|
824
|
+
const emitTargetMismatch = (
|
|
825
|
+
targetKind: string,
|
|
826
|
+
valueSchema: Record<string, any>,
|
|
827
|
+
value: unknown,
|
|
828
|
+
path: string,
|
|
829
|
+
) => {
|
|
830
|
+
const substituted = substituteCelFields(value, valueSchema);
|
|
831
|
+
const issues = validateAgainstSchema(substituted, valueSchema);
|
|
832
|
+
for (const issue of issues) {
|
|
833
|
+
diagnostics.push({
|
|
834
|
+
severity: DiagnosticSeverity.Error,
|
|
835
|
+
code: "TEMPLATE_TARGET_MISMATCH",
|
|
836
|
+
source: SOURCE,
|
|
837
|
+
message: `${m.kind}/${name}: ${path} does not satisfy ${targetKind}'s contract: ${issue.message}`,
|
|
838
|
+
data: { resource, filePath, path: issue.path ? `${path}.${issue.path}` : path },
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// Resolve the dispatch target's kind, if statically known. Object-form
|
|
844
|
+
// `invoke: { kind, name }` and `provide: { kind, name }` carry it; the
|
|
845
|
+
// string-form `invoke: "name"` does not (the matching resource entry would
|
|
846
|
+
// need to be located by expanded name — out of scope here).
|
|
847
|
+
const invoke = md.invoke;
|
|
848
|
+
const provide = md.provide;
|
|
849
|
+
let dispatchKind: string | undefined;
|
|
850
|
+
if (invoke && typeof invoke === "object" && !Array.isArray(invoke) && typeof invoke.kind === "string") {
|
|
851
|
+
dispatchKind = invoke.kind;
|
|
852
|
+
} else if (
|
|
853
|
+
provide &&
|
|
854
|
+
typeof provide === "object" &&
|
|
855
|
+
!Array.isArray(provide) &&
|
|
856
|
+
typeof provide.kind === "string"
|
|
857
|
+
) {
|
|
858
|
+
dispatchKind = provide.kind;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Top-level `inputs:` (sibling of `invoke:` / `provide:`) carries the
|
|
862
|
+
// values passed to the dispatch target's invoke(). Validate against the
|
|
863
|
+
// target's declared `inputType` when both sides have one.
|
|
864
|
+
if (dispatchKind && md.inputs && typeof md.inputs === "object") {
|
|
865
|
+
const targetSchema = lookupDefinitionTypeField(
|
|
866
|
+
dispatchKind,
|
|
867
|
+
"inputType",
|
|
868
|
+
defs,
|
|
869
|
+
aliases,
|
|
870
|
+
allManifests as Record<string, any>[],
|
|
871
|
+
);
|
|
872
|
+
if (targetSchema) {
|
|
873
|
+
emitTargetMismatch(dispatchKind, targetSchema, md.inputs, "inputs");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Top-level `result:` is a post-call mapping that must satisfy the abstract
|
|
878
|
+
// this definition `extends` (`outputType`). It's a sibling of whichever
|
|
879
|
+
// dispatch entry-point declared a kind-typed target (`provide:` or
|
|
880
|
+
// `invoke:`). The target's outputType lives on the dispatcher's `kind`
|
|
881
|
+
// and is what `result` is typed against *inside* CEL — separate role.
|
|
882
|
+
const hasDispatchObject =
|
|
883
|
+
(provide && typeof provide === "object" && !Array.isArray(provide)) ||
|
|
884
|
+
(invoke && typeof invoke === "object" && !Array.isArray(invoke));
|
|
885
|
+
if (hasDispatchObject && md.result && typeof md.result === "object") {
|
|
886
|
+
const extendsValue = md.extends as string | undefined;
|
|
887
|
+
if (typeof extendsValue === "string" && extendsValue.length > 0) {
|
|
888
|
+
const abstractSchema = lookupDefinitionTypeField(
|
|
889
|
+
extendsValue,
|
|
890
|
+
"outputType",
|
|
891
|
+
defs,
|
|
892
|
+
aliases,
|
|
893
|
+
allManifests as Record<string, any>[],
|
|
894
|
+
);
|
|
895
|
+
if (abstractSchema) {
|
|
896
|
+
emitTargetMismatch(extendsValue, abstractSchema, md.result, "result");
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
615
902
|
// Validate CEL syntax and context variable access in all manifests
|
|
616
903
|
for (const m of allManifests) {
|
|
617
904
|
const resource = { kind: m.kind, name: m.metadata?.name as string };
|
|
@@ -670,11 +957,18 @@ export class StaticAnalyzer {
|
|
|
670
957
|
const manifestItem = matchedScope
|
|
671
958
|
? getManifestItem(path, matchedScope, m as Record<string, any>)
|
|
672
959
|
: (m as Record<string, any>);
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
960
|
+
const rootForResolver = manifestRootForResolver(
|
|
961
|
+
m as Record<string, any>,
|
|
962
|
+
defs,
|
|
963
|
+
aliases,
|
|
676
964
|
allManifests as Record<string, any>[],
|
|
677
965
|
);
|
|
966
|
+
const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
|
|
967
|
+
manifestRoot: rootForResolver,
|
|
968
|
+
defs,
|
|
969
|
+
aliases,
|
|
970
|
+
allManifests: allManifests as Record<string, any>[],
|
|
971
|
+
});
|
|
678
972
|
effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
|
|
679
973
|
}
|
|
680
974
|
|
|
@@ -722,10 +1016,15 @@ export class StaticAnalyzer {
|
|
|
722
1016
|
// Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
|
|
723
1017
|
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
724
1018
|
|
|
1019
|
+
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
1020
|
+
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
1021
|
+
|
|
725
1022
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
726
1023
|
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
727
1024
|
|
|
728
|
-
|
|
1025
|
+
// Reroute diagnostics on synthetic (inline-extracted) resources back to
|
|
1026
|
+
// the chain root so position-index lookups land on the parent doc.
|
|
1027
|
+
return rewriteSyntheticOrigins(diagnostics, allManifests);
|
|
729
1028
|
}
|
|
730
1029
|
|
|
731
1030
|
analyzeErrors(
|
|
@@ -740,12 +1039,17 @@ export class StaticAnalyzer {
|
|
|
740
1039
|
|
|
741
1040
|
normalize(manifests: ResourceManifest[], registry: AnalysisRegistry): ResourceManifest[] {
|
|
742
1041
|
const ctx = registry._context();
|
|
743
|
-
|
|
1042
|
+
const normalized = normalizeInlineResources(
|
|
744
1043
|
manifests,
|
|
745
1044
|
ctx.definitions!,
|
|
746
1045
|
ctx.aliases,
|
|
747
1046
|
ctx.aliasesByModule,
|
|
748
1047
|
);
|
|
1048
|
+
// Resolve !ref sentinels after normalize so both the original and
|
|
1049
|
+
// inline-extracted manifests get their refs canonicalized to
|
|
1050
|
+
// {kind, name} for the kernel that consumes this output.
|
|
1051
|
+
resolveRefSentinels(normalized, ctx.definitions!, ctx.aliases, ctx.aliasesByModule);
|
|
1052
|
+
return normalized;
|
|
749
1053
|
}
|
|
750
1054
|
|
|
751
1055
|
prepare(
|
package/src/builtins.ts
CHANGED
|
@@ -40,7 +40,126 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
40
40
|
kind: "Telo.Definition",
|
|
41
41
|
metadata: { name: "Definition", module: "Telo" },
|
|
42
42
|
capability: "Telo.Template",
|
|
43
|
-
|
|
43
|
+
// Top-level shape stays open (`additionalProperties: true`) so this change
|
|
44
|
+
// attaches x-telo-context annotations to known template-body fields without
|
|
45
|
+
// tightening the Telo.Definition shape itself. The annotations drive
|
|
46
|
+
// static CEL validation of expressions inside `resources:` / `invoke:` /
|
|
47
|
+
// `run:` / `provide:` / top-level `inputs:` / top-level `result:` against
|
|
48
|
+
// `self` (typed from `schema:`) and `inputs` (typed from `inputType:`,
|
|
49
|
+
// falling back to the extends-declared abstract).
|
|
50
|
+
//
|
|
51
|
+
// `inputs:` and `result:` live as top-level siblings of `invoke:` / `provide:`,
|
|
52
|
+
// matching how Run.Sequence steps factor dispatch from data. The dispatch
|
|
53
|
+
// entry-point (`invoke` / `provide` / `run`) determines how `inputs`/`result`
|
|
54
|
+
// are interpreted at runtime. See analyzer/nodejs/plans/template-internal-cel-validation.md.
|
|
55
|
+
schema: {
|
|
56
|
+
type: "object",
|
|
57
|
+
additionalProperties: true,
|
|
58
|
+
properties: {
|
|
59
|
+
resources: {
|
|
60
|
+
type: "array",
|
|
61
|
+
items: {
|
|
62
|
+
type: "object",
|
|
63
|
+
additionalProperties: true,
|
|
64
|
+
"x-telo-context": {
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
69
|
+
inputs: { "x-telo-context-from-root": "inputType" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
invoke: {
|
|
75
|
+
oneOf: [
|
|
76
|
+
{
|
|
77
|
+
type: "string",
|
|
78
|
+
"x-telo-context": {
|
|
79
|
+
type: "object",
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
properties: {
|
|
82
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: "object",
|
|
88
|
+
additionalProperties: true,
|
|
89
|
+
properties: {
|
|
90
|
+
kind: { type: "string" },
|
|
91
|
+
name: {
|
|
92
|
+
type: "string",
|
|
93
|
+
"x-telo-context": {
|
|
94
|
+
type: "object",
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
properties: {
|
|
97
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
provide: {
|
|
106
|
+
type: "object",
|
|
107
|
+
additionalProperties: true,
|
|
108
|
+
properties: {
|
|
109
|
+
kind: { type: "string" },
|
|
110
|
+
name: {
|
|
111
|
+
type: "string",
|
|
112
|
+
"x-telo-context": {
|
|
113
|
+
type: "object",
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
properties: {
|
|
116
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
run: {
|
|
123
|
+
type: "string",
|
|
124
|
+
"x-telo-context": {
|
|
125
|
+
type: "object",
|
|
126
|
+
additionalProperties: false,
|
|
127
|
+
properties: {
|
|
128
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
inputs: {
|
|
133
|
+
type: "object",
|
|
134
|
+
additionalProperties: true,
|
|
135
|
+
"x-telo-context": {
|
|
136
|
+
type: "object",
|
|
137
|
+
additionalProperties: false,
|
|
138
|
+
properties: {
|
|
139
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
140
|
+
inputs: { "x-telo-context-from-root": "inputType" },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
result: {
|
|
145
|
+
type: "object",
|
|
146
|
+
additionalProperties: true,
|
|
147
|
+
"x-telo-context": {
|
|
148
|
+
type: "object",
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
properties: {
|
|
151
|
+
self: { "x-telo-context-from-root": "schema" },
|
|
152
|
+
result: {
|
|
153
|
+
"x-telo-context-from-ref-kind": [
|
|
154
|
+
"provide/kind#outputType",
|
|
155
|
+
"invoke/kind#outputType",
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
44
163
|
},
|
|
45
164
|
{
|
|
46
165
|
kind: "Telo.Definition",
|
|
@@ -101,6 +220,20 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
101
220
|
anyOf: [
|
|
102
221
|
{ type: "string", "x-telo-ref": "telo#Runnable" },
|
|
103
222
|
{ type: "string", "x-telo-ref": "telo#Service" },
|
|
223
|
+
// Post-resolution shape that `resolveRefSentinels`
|
|
224
|
+
// substitutes a `!ref <name>` sentinel into. The
|
|
225
|
+
// adjacent `x-telo-ref` constraints govern the kind
|
|
226
|
+
// check; this branch only admits the structural form so
|
|
227
|
+
// AJV doesn't reject a resolved ref.
|
|
228
|
+
{
|
|
229
|
+
type: "object",
|
|
230
|
+
required: ["kind", "name"],
|
|
231
|
+
properties: {
|
|
232
|
+
kind: { type: "string" },
|
|
233
|
+
name: { type: "string" },
|
|
234
|
+
},
|
|
235
|
+
additionalProperties: true,
|
|
236
|
+
},
|
|
104
237
|
],
|
|
105
238
|
},
|
|
106
239
|
},
|
|
@@ -108,6 +241,44 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
108
241
|
type: "array",
|
|
109
242
|
items: { type: "string" },
|
|
110
243
|
},
|
|
244
|
+
// Application-level environment contract. Each entry layers `env:`
|
|
245
|
+
// (required, names the source env var) and `default:` (optional, used
|
|
246
|
+
// when the env var is unset) on top of an open JSON Schema property
|
|
247
|
+
// schema. `type:` constrains the coercion rule applied to the raw env
|
|
248
|
+
// string (scalars per-type; `object` / `array` via JSON.parse with the
|
|
249
|
+
// matching top-level type). All other JSON Schema keywords are passed
|
|
250
|
+
// through unchanged and applied to the coerced value via the standard
|
|
251
|
+
// schema validator. See kernel/nodejs/src/application-env.ts.
|
|
252
|
+
variables: {
|
|
253
|
+
type: "object",
|
|
254
|
+
additionalProperties: {
|
|
255
|
+
type: "object",
|
|
256
|
+
required: ["env", "type"],
|
|
257
|
+
properties: {
|
|
258
|
+
env: { type: "string" },
|
|
259
|
+
type: {
|
|
260
|
+
type: "string",
|
|
261
|
+
enum: ["string", "integer", "number", "boolean", "object", "array"],
|
|
262
|
+
},
|
|
263
|
+
default: {},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
secrets: {
|
|
268
|
+
type: "object",
|
|
269
|
+
additionalProperties: {
|
|
270
|
+
type: "object",
|
|
271
|
+
required: ["env", "type"],
|
|
272
|
+
properties: {
|
|
273
|
+
env: { type: "string" },
|
|
274
|
+
type: {
|
|
275
|
+
type: "string",
|
|
276
|
+
enum: ["string", "integer", "number", "boolean", "object", "array"],
|
|
277
|
+
},
|
|
278
|
+
default: {},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
111
282
|
},
|
|
112
283
|
required: ["metadata"],
|
|
113
284
|
additionalProperties: false,
|
|
@@ -80,6 +80,21 @@ export class DefinitionRegistry {
|
|
|
80
80
|
* @param namespace The module's metadata.namespace (e.g. "std"), or null for telo built-ins.
|
|
81
81
|
* @param moduleName The module's metadata.name (e.g. "pipeline", "http-server"). */
|
|
82
82
|
registerModuleIdentity(namespace: string | null, moduleName: string): void {
|
|
83
|
+
// The "telo" identity is reserved for the Telo built-in module and gets
|
|
84
|
+
// populated automatically when a Telo.Abstract definition registers (see
|
|
85
|
+
// `register` below). A user app / library without a namespace must NOT
|
|
86
|
+
// claim it — silently overwriting the built-in entry breaks every
|
|
87
|
+
// x-telo-ref that resolves through "telo#…". Concretely, the
|
|
88
|
+
// `Http.Api.routes[].handler` slot in the http-server schema carries
|
|
89
|
+
// `x-telo-ref: "telo#Invocable"`. If the entry application is, say,
|
|
90
|
+
// `Telo.Application/HelloApi` (no namespace), this method previously
|
|
91
|
+
// overwrote `"telo" → "Telo"` with `"telo" → "HelloApi"`. The handler's
|
|
92
|
+
// ref then resolved to a nonexistent `HelloApi.Invocable`, the
|
|
93
|
+
// kind-mismatch check inside `validate-references.ts` short-circuited
|
|
94
|
+
// on partial context, and the analyzer reported zero issues for a
|
|
95
|
+
// manifest that explodes at runtime. Skip non-Telo no-namespace modules;
|
|
96
|
+
// they have no x-telo-ref identity to declare anyway.
|
|
97
|
+
if (!namespace && moduleName !== "Telo") return;
|
|
83
98
|
const identity = namespace ? `${namespace}/${moduleName}` : "telo";
|
|
84
99
|
this.identityMap.set(identity, moduleName);
|
|
85
100
|
this.reverseIdentityMap.set(moduleName, identity);
|