@telorun/analyzer 0.11.0 → 1.2.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/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.map +1 -1
- package/dist/analyzer.js +53 -9
- package/dist/builtins.d.ts.map +1 -1
- package/dist/builtins.js +44 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -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 +5 -1
- package/dist/position-metadata.d.ts.map +1 -1
- package/dist/position-metadata.js +8 -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 +35 -19
- 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/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/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-cel-context.d.ts +5 -0
- package/dist/validate-cel-context.d.ts.map +1 -1
- package/dist/validate-cel-context.js +27 -15
- 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.js +24 -24
- package/package.json +7 -4
- package/src/analysis-registry.ts +37 -0
- package/src/analyzer.ts +55 -11
- package/src/builtins.ts +44 -1
- package/src/index.ts +1 -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 +8 -1
- package/src/reference-field-map.ts +46 -18
- package/src/residual-schema.ts +49 -0
- package/src/rewrite-synthetic-origins.ts +75 -0
- package/src/types.ts +12 -0
- package/src/validate-cel-context.ts +28 -15
- package/src/validate-provider-coherence.ts +166 -0
- package/src/validate-references.ts +24 -24
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldValues } from "./reference-field-map.js";
|
|
1
|
+
import { isRefEntry, isScopeEntry, isSchemaFromEntry, isInlineResource, resolveFieldEntries, resolveFieldValues } from "./reference-field-map.js";
|
|
2
2
|
import { navigateJsonPointer } from "./schema-compat.js";
|
|
3
3
|
import { DiagnosticSeverity } from "./types.js";
|
|
4
4
|
const SOURCE = "telo-analyzer";
|
|
@@ -119,7 +119,7 @@ export function validateReferences(resources, context) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
for (const val of
|
|
122
|
+
for (const { value: val, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
123
123
|
if (!val)
|
|
124
124
|
continue;
|
|
125
125
|
// Name-only reference (plain string) — look up by name to validate.
|
|
@@ -142,8 +142,8 @@ export function validateReferences(resources, context) {
|
|
|
142
142
|
severity: DiagnosticSeverity.Error,
|
|
143
143
|
code: "UNRESOLVED_REFERENCE",
|
|
144
144
|
source: SOURCE,
|
|
145
|
-
message: `${resourceLabel}: reference at '${
|
|
146
|
-
data: { resource: resourceData, filePath, path:
|
|
145
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${val}' not found`,
|
|
146
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
147
147
|
});
|
|
148
148
|
continue;
|
|
149
149
|
}
|
|
@@ -153,8 +153,8 @@ export function validateReferences(resources, context) {
|
|
|
153
153
|
severity: DiagnosticSeverity.Error,
|
|
154
154
|
code: "REFERENCE_KIND_MISMATCH",
|
|
155
155
|
source: SOURCE,
|
|
156
|
-
message: `${resourceLabel}: reference at '${
|
|
157
|
-
data: { resource: resourceData, filePath, path:
|
|
156
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
157
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
continue;
|
|
@@ -171,8 +171,8 @@ export function validateReferences(resources, context) {
|
|
|
171
171
|
severity: DiagnosticSeverity.Error,
|
|
172
172
|
code: "INVALID_REFERENCE",
|
|
173
173
|
source: SOURCE,
|
|
174
|
-
message: `${resourceLabel}: reference at '${
|
|
175
|
-
data: { resource: resourceData, filePath, path:
|
|
174
|
+
message: `${resourceLabel}: reference at '${concretePath}' must have string 'kind' and 'name' fields`,
|
|
175
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
176
176
|
});
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
@@ -183,8 +183,8 @@ export function validateReferences(resources, context) {
|
|
|
183
183
|
severity: DiagnosticSeverity.Error,
|
|
184
184
|
code: "REFERENCE_KIND_MISMATCH",
|
|
185
185
|
source: SOURCE,
|
|
186
|
-
message: `${resourceLabel}: reference at '${
|
|
187
|
-
data: { resource: resourceData, filePath, path:
|
|
186
|
+
message: `${resourceLabel}: reference at '${concretePath}' → ${kindErrors.join("; ")}`,
|
|
187
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
190
|
// 3. Resolution check — resource with this name must exist.
|
|
@@ -195,8 +195,8 @@ export function validateReferences(resources, context) {
|
|
|
195
195
|
severity: DiagnosticSeverity.Error,
|
|
196
196
|
code: "UNRESOLVED_REFERENCE",
|
|
197
197
|
source: SOURCE,
|
|
198
|
-
message: `${resourceLabel}: reference at '${
|
|
199
|
-
data: { resource: resourceData, filePath, path:
|
|
198
|
+
message: `${resourceLabel}: reference at '${concretePath}' → resource '${refVal.name}' not found`,
|
|
199
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
}
|
|
@@ -279,7 +279,7 @@ export function validateReferences(resources, context) {
|
|
|
279
279
|
});
|
|
280
280
|
continue;
|
|
281
281
|
}
|
|
282
|
-
for (const fieldValue of
|
|
282
|
+
for (const { value: fieldValue, path: concretePath } of resolveFieldEntries(r, fieldPath)) {
|
|
283
283
|
if (fieldValue == null)
|
|
284
284
|
continue;
|
|
285
285
|
const issues = registry.validateWithRefs(fieldValue, subSchema);
|
|
@@ -288,8 +288,8 @@ export function validateReferences(resources, context) {
|
|
|
288
288
|
severity: DiagnosticSeverity.Error,
|
|
289
289
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
290
290
|
source: SOURCE,
|
|
291
|
-
message: `${resourceLabel}: '${
|
|
292
|
-
data: { resource: resourceData, filePath, path:
|
|
291
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${anchorName}${jsonPointer}': ${issue}`,
|
|
292
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
293
293
|
});
|
|
294
294
|
}
|
|
295
295
|
}
|
|
@@ -309,9 +309,9 @@ export function validateReferences(resources, context) {
|
|
|
309
309
|
const anchorValues = resolveFieldValues(r, anchorPath);
|
|
310
310
|
if (anchorValues.length === 0)
|
|
311
311
|
continue; // anchor field not set — nothing to validate
|
|
312
|
-
const
|
|
313
|
-
for (let i = 0; i <
|
|
314
|
-
const fieldValue =
|
|
312
|
+
const fieldEntries = resolveFieldEntries(r, fieldPath);
|
|
313
|
+
for (let i = 0; i < fieldEntries.length; i++) {
|
|
314
|
+
const { value: fieldValue, path: concretePath } = fieldEntries[i];
|
|
315
315
|
if (fieldValue == null)
|
|
316
316
|
continue;
|
|
317
317
|
// For absolute paths, the single anchor applies to all field values.
|
|
@@ -328,8 +328,8 @@ export function validateReferences(resources, context) {
|
|
|
328
328
|
severity: DiagnosticSeverity.Error,
|
|
329
329
|
code: "SCHEMA_FROM_MISSING_PATH",
|
|
330
330
|
source: SOURCE,
|
|
331
|
-
message: `${resourceLabel}: x-telo-schema-from at '${
|
|
332
|
-
data: { resource: resourceData, filePath, path:
|
|
331
|
+
message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema`,
|
|
332
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
333
333
|
});
|
|
334
334
|
continue;
|
|
335
335
|
}
|
|
@@ -339,8 +339,8 @@ export function validateReferences(resources, context) {
|
|
|
339
339
|
severity: DiagnosticSeverity.Error,
|
|
340
340
|
code: "SCHEMA_FROM_MISSING_PATH",
|
|
341
341
|
source: SOURCE,
|
|
342
|
-
message: `${resourceLabel}: x-telo-schema-from at '${
|
|
343
|
-
data: { resource: resourceData, filePath, path:
|
|
342
|
+
message: `${resourceLabel}: x-telo-schema-from at '${concretePath}' → kind '${refVal.kind}' has no schema path '${jsonPointer}'`,
|
|
343
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
344
344
|
});
|
|
345
345
|
continue;
|
|
346
346
|
}
|
|
@@ -350,8 +350,8 @@ export function validateReferences(resources, context) {
|
|
|
350
350
|
severity: DiagnosticSeverity.Error,
|
|
351
351
|
code: "DEPENDENT_SCHEMA_MISMATCH",
|
|
352
352
|
source: SOURCE,
|
|
353
|
-
message: `${resourceLabel}: '${
|
|
354
|
-
data: { resource: resourceData, filePath, path:
|
|
353
|
+
message: `${resourceLabel}: '${concretePath}' does not match schema from '${refVal.kind}${jsonPointer}': ${issue}`,
|
|
354
|
+
data: { resource: resourceData, filePath, path: concretePath },
|
|
355
355
|
});
|
|
356
356
|
}
|
|
357
357
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/analyzer",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Telo Analyzer - Static manifest validator for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"bun": "./src/index.ts",
|
|
30
30
|
"import": "./dist/index.js",
|
|
31
31
|
"default": "./dist/index.js"
|
|
32
|
-
}
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
33
34
|
},
|
|
34
35
|
"files": [
|
|
35
36
|
"dist/**",
|
|
@@ -41,14 +42,16 @@
|
|
|
41
42
|
"ajv-formats": "^3.0.1",
|
|
42
43
|
"jsonpath-plus": "^10.3.0",
|
|
43
44
|
"yaml": "^2.8.3",
|
|
44
|
-
"@telorun/
|
|
45
|
-
"@telorun/templating": "0.2.3"
|
|
45
|
+
"@telorun/templating": "1.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^20.0.0",
|
|
49
49
|
"typescript": "^5.0.0",
|
|
50
50
|
"vitest": "^2.1.8"
|
|
51
51
|
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@telorun/sdk": "1.0.0"
|
|
54
|
+
},
|
|
52
55
|
"scripts": {
|
|
53
56
|
"build": "tsc -p tsconfig.lib.json",
|
|
54
57
|
"test": "vitest run",
|
package/src/analysis-registry.ts
CHANGED
|
@@ -101,6 +101,43 @@ export class AnalysisRegistry {
|
|
|
101
101
|
return computeSuggestKind(badKind, this.aliases, this.defs);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Returns every user-facing (alias-form) kind that satisfies the given
|
|
105
|
+
* `x-telo-ref` constraint string (e.g. `"telo#Invocable"`, `"std/sql#Connection"`).
|
|
106
|
+
* Resolution mirrors `validateReferences.checkKind`: abstract targets expand to
|
|
107
|
+
* the set of definitions extending them; concrete targets yield just themselves.
|
|
108
|
+
* Returns `undefined` when the ref can't be resolved (e.g. unregistered identity),
|
|
109
|
+
* so callers can fall back to the unfiltered kind list. */
|
|
110
|
+
userFacingKindsForRef(xTeloRef: string): string[] | undefined {
|
|
111
|
+
const targetKind = this.defs.resolveRef(xTeloRef);
|
|
112
|
+
if (!targetKind) return undefined;
|
|
113
|
+
const targetDef = this.defs.resolve(targetKind);
|
|
114
|
+
if (!targetDef) return undefined;
|
|
115
|
+
|
|
116
|
+
const canonicalKinds: string[] = [];
|
|
117
|
+
if (targetDef.kind === "Telo.Abstract") {
|
|
118
|
+
for (const def of this.defs.getByExtends(targetKind)) {
|
|
119
|
+
const module = (def.metadata as { module?: string } | undefined)?.module;
|
|
120
|
+
if (module && def.metadata?.name) {
|
|
121
|
+
canonicalKinds.push(`${module}.${def.metadata.name as string}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
canonicalKinds.push(targetKind);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const out = new Set<string>();
|
|
129
|
+
for (const kind of canonicalKinds) {
|
|
130
|
+
const dot = kind.indexOf(".");
|
|
131
|
+
if (dot === -1) continue;
|
|
132
|
+
const moduleName = kind.slice(0, dot);
|
|
133
|
+
const typeName = kind.slice(dot + 1);
|
|
134
|
+
for (const alias of this.aliases.aliasesFor(moduleName)) {
|
|
135
|
+
out.add(`${alias}.${typeName}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return Array.from(out);
|
|
139
|
+
}
|
|
140
|
+
|
|
104
141
|
/** @internal Bridge for StaticAnalyzer — do not use outside the analyzer package. */
|
|
105
142
|
_context(): AnalysisContext {
|
|
106
143
|
return { aliases: this.aliases, definitions: this.defs, aliasesByModule: this.aliasesByModule };
|
package/src/analyzer.ts
CHANGED
|
@@ -14,6 +14,7 @@ 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 { rewriteSyntheticOrigins } from "./rewrite-synthetic-origins.js";
|
|
17
18
|
import {
|
|
18
19
|
celTypeSatisfiesJsonSchema,
|
|
19
20
|
substituteCelFields,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
resolveTypeFieldToSchema,
|
|
29
30
|
} from "./validate-cel-context.js";
|
|
30
31
|
import { validateExtends } from "./validate-extends.js";
|
|
32
|
+
import { validateProviderCoherence } from "./validate-provider-coherence.js";
|
|
31
33
|
import { validateReferences } from "./validate-references.js";
|
|
32
34
|
import { validateThrowsCoverage } from "./validate-throws-coverage.js";
|
|
33
35
|
|
|
@@ -621,6 +623,16 @@ export class StaticAnalyzer {
|
|
|
621
623
|
// Phase 2: extract inline resources from x-telo-ref slots into first-class manifests
|
|
622
624
|
const allManifests = normalizeInlineResources(manifests, defs, aliases, aliasesByModule);
|
|
623
625
|
|
|
626
|
+
// Trusted-input fast path: when the caller has already attested that
|
|
627
|
+
// this exact manifest set passes analysis (e.g. via the kernel's
|
|
628
|
+
// hash-stamped `.validated.json` cache), skip the validation walk.
|
|
629
|
+
// Registration of identities / aliases / definitions and inline-resource
|
|
630
|
+
// normalisation have already run above; that's all downstream
|
|
631
|
+
// consumers (prepare, init loop) require.
|
|
632
|
+
if (options?.skipValidation) {
|
|
633
|
+
return diagnostics;
|
|
634
|
+
}
|
|
635
|
+
|
|
624
636
|
// Build a name→manifest map for looking up referenced resources
|
|
625
637
|
const byName = new Map<string, ResourceManifest>();
|
|
626
638
|
for (const m of allManifests) {
|
|
@@ -629,6 +641,35 @@ export class StaticAnalyzer {
|
|
|
629
641
|
}
|
|
630
642
|
}
|
|
631
643
|
|
|
644
|
+
// Library env: rejection — `env:` on a Library `variables` / `secrets`
|
|
645
|
+
// entry is forbidden. The Library entry schema is otherwise open so that
|
|
646
|
+
// any JSON Schema property schema is valid; this targeted check produces
|
|
647
|
+
// a clear diagnostic instead of a generic "additional property" error.
|
|
648
|
+
for (const m of allManifests) {
|
|
649
|
+
if (m.kind !== "Telo.Library") continue;
|
|
650
|
+
const filePath = (m.metadata as { source?: string } | undefined)?.source;
|
|
651
|
+
const moduleName = m.metadata?.name as string | undefined;
|
|
652
|
+
const resource = moduleName ? { kind: m.kind, name: moduleName } : undefined;
|
|
653
|
+
for (const block of ["variables", "secrets"] as const) {
|
|
654
|
+
const entries = (m as Record<string, any>)[block];
|
|
655
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
|
|
656
|
+
for (const [entryName, entry] of Object.entries(entries)) {
|
|
657
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
658
|
+
if ("env" in (entry as Record<string, unknown>)) {
|
|
659
|
+
diagnostics.push({
|
|
660
|
+
severity: DiagnosticSeverity.Error,
|
|
661
|
+
code: "LIBRARY_ENV_KEY_REJECTED",
|
|
662
|
+
source: SOURCE,
|
|
663
|
+
message:
|
|
664
|
+
`Telo.Library ${block}/${entryName}: 'env:' is only permitted on Telo.Application entries. ` +
|
|
665
|
+
`Libraries must receive values from importers via the parent manifest's variables / secrets block.`,
|
|
666
|
+
data: { resource, filePath, path: `${block}.${entryName}.env` },
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
632
673
|
// Build typed kernel globals schema so x-telo-context chain validation
|
|
633
674
|
// recognises variables, secrets, resources, env automatically
|
|
634
675
|
const kernelGlobals = buildKernelGlobalsSchema(allManifests);
|
|
@@ -776,17 +817,15 @@ export class StaticAnalyzer {
|
|
|
776
817
|
}
|
|
777
818
|
}
|
|
778
819
|
|
|
779
|
-
// Top-level `result:`
|
|
780
|
-
//
|
|
781
|
-
//
|
|
820
|
+
// Top-level `result:` is a post-call mapping that must satisfy the abstract
|
|
821
|
+
// this definition `extends` (`outputType`). It's a sibling of whichever
|
|
822
|
+
// dispatch entry-point declared a kind-typed target (`provide:` or
|
|
823
|
+
// `invoke:`). The target's outputType lives on the dispatcher's `kind`
|
|
782
824
|
// and is what `result` is typed against *inside* CEL — separate role.
|
|
783
|
-
|
|
784
|
-
provide &&
|
|
785
|
-
typeof
|
|
786
|
-
|
|
787
|
-
md.result &&
|
|
788
|
-
typeof md.result === "object"
|
|
789
|
-
) {
|
|
825
|
+
const hasDispatchObject =
|
|
826
|
+
(provide && typeof provide === "object" && !Array.isArray(provide)) ||
|
|
827
|
+
(invoke && typeof invoke === "object" && !Array.isArray(invoke));
|
|
828
|
+
if (hasDispatchObject && md.result && typeof md.result === "object") {
|
|
790
829
|
const extendsValue = md.extends as string | undefined;
|
|
791
830
|
if (typeof extendsValue === "string" && extendsValue.length > 0) {
|
|
792
831
|
const abstractSchema = lookupDefinitionTypeField(
|
|
@@ -920,10 +959,15 @@ export class StaticAnalyzer {
|
|
|
920
959
|
// Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
|
|
921
960
|
diagnostics.push(...validateExtends(allManifests, defs, aliases));
|
|
922
961
|
|
|
962
|
+
// Validate provider coherence rules for `provide:` template-target definitions.
|
|
963
|
+
diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
|
|
964
|
+
|
|
923
965
|
// Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
|
|
924
966
|
diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
|
|
925
967
|
|
|
926
|
-
|
|
968
|
+
// Reroute diagnostics on synthetic (inline-extracted) resources back to
|
|
969
|
+
// the chain root so position-index lookups land on the parent doc.
|
|
970
|
+
return rewriteSyntheticOrigins(diagnostics, allManifests);
|
|
927
971
|
}
|
|
928
972
|
|
|
929
973
|
analyzeErrors(
|
package/src/builtins.ts
CHANGED
|
@@ -149,7 +149,12 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
149
149
|
additionalProperties: false,
|
|
150
150
|
properties: {
|
|
151
151
|
self: { "x-telo-context-from-root": "schema" },
|
|
152
|
-
result: {
|
|
152
|
+
result: {
|
|
153
|
+
"x-telo-context-from-ref-kind": [
|
|
154
|
+
"provide/kind#outputType",
|
|
155
|
+
"invoke/kind#outputType",
|
|
156
|
+
],
|
|
157
|
+
},
|
|
153
158
|
},
|
|
154
159
|
},
|
|
155
160
|
},
|
|
@@ -222,6 +227,44 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
|
|
|
222
227
|
type: "array",
|
|
223
228
|
items: { type: "string" },
|
|
224
229
|
},
|
|
230
|
+
// Application-level environment contract. Each entry layers `env:`
|
|
231
|
+
// (required, names the source env var) and `default:` (optional, used
|
|
232
|
+
// when the env var is unset) on top of an open JSON Schema property
|
|
233
|
+
// schema. `type:` constrains the coercion rule applied to the raw env
|
|
234
|
+
// string (scalars per-type; `object` / `array` via JSON.parse with the
|
|
235
|
+
// matching top-level type). All other JSON Schema keywords are passed
|
|
236
|
+
// through unchanged and applied to the coerced value via the standard
|
|
237
|
+
// schema validator. See kernel/nodejs/src/application-env.ts.
|
|
238
|
+
variables: {
|
|
239
|
+
type: "object",
|
|
240
|
+
additionalProperties: {
|
|
241
|
+
type: "object",
|
|
242
|
+
required: ["env", "type"],
|
|
243
|
+
properties: {
|
|
244
|
+
env: { type: "string" },
|
|
245
|
+
type: {
|
|
246
|
+
type: "string",
|
|
247
|
+
enum: ["string", "integer", "number", "boolean", "object", "array"],
|
|
248
|
+
},
|
|
249
|
+
default: {},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
secrets: {
|
|
254
|
+
type: "object",
|
|
255
|
+
additionalProperties: {
|
|
256
|
+
type: "object",
|
|
257
|
+
required: ["env", "type"],
|
|
258
|
+
properties: {
|
|
259
|
+
env: { type: "string" },
|
|
260
|
+
type: {
|
|
261
|
+
type: "string",
|
|
262
|
+
enum: ["string", "integer", "number", "boolean", "object", "array"],
|
|
263
|
+
},
|
|
264
|
+
default: {},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
225
268
|
},
|
|
226
269
|
required: ["metadata"],
|
|
227
270
|
additionalProperties: false,
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { isModuleKind, MODULE_KINDS } from "./module-kinds.js";
|
|
|
14
14
|
export type { ModuleKind } from "./module-kinds.js";
|
|
15
15
|
export { parseLoadedFile } from "./parse-loaded-file.js";
|
|
16
16
|
export type { ParseOptions } from "./parse-loaded-file.js";
|
|
17
|
+
export { residualEntrySchema, residualEntrySchemaMap } from "./residual-schema.js";
|
|
17
18
|
export {
|
|
18
19
|
buildDocumentPositions,
|
|
19
20
|
buildLineOffsets,
|
package/src/kernel-globals.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResourceManifest } from "@telorun/sdk";
|
|
2
|
+
import { residualEntrySchemaMap } from "./residual-schema.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Kernel global names available in every CEL evaluation context at runtime.
|
|
@@ -72,20 +73,17 @@ export function buildKernelGlobalsSchema(
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/** Wrap a JSON Schema property map (like `Telo.Application.variables`) into a
|
|
75
|
-
* closed object schema suitable for chain-access validation.
|
|
76
|
-
*
|
|
76
|
+
* closed object schema suitable for chain-access validation. For Application
|
|
77
|
+
* entries the per-entry shape carries kernel-specific keys (`env`, `default`)
|
|
78
|
+
* on top of an otherwise-standard JSON Schema property schema; those keys are
|
|
79
|
+
* stripped via `residualEntrySchemaMap` so CEL sees the coerced shape, not
|
|
80
|
+
* the env-binding wrapper. Library entries are pure JSON Schema property
|
|
81
|
+
* schemas and pass through the same call unchanged. Falls back to an open map
|
|
82
|
+
* when the module declares no variables/secrets. */
|
|
77
83
|
function buildSchemaMapSchema(
|
|
78
84
|
schemaMap: Record<string, any> | null | undefined,
|
|
79
85
|
): Record<string, any> {
|
|
80
|
-
|
|
81
|
-
return { type: "object", additionalProperties: true };
|
|
82
|
-
}
|
|
83
|
-
const props: Record<string, any> = {};
|
|
84
|
-
for (const [key, value] of Object.entries(schemaMap)) {
|
|
85
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
86
|
-
props[key] = value;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
86
|
+
const props = residualEntrySchemaMap(schemaMap);
|
|
89
87
|
if (Object.keys(props).length === 0) {
|
|
90
88
|
return { type: "object", additionalProperties: true };
|
|
91
89
|
}
|
package/src/manifest-loader.ts
CHANGED
|
@@ -33,6 +33,14 @@ export class Loader {
|
|
|
33
33
|
* get distinct entries, so neither sees the wrong manifest tree. */
|
|
34
34
|
private readonly fileCache = new Map<string, LoadedFile>();
|
|
35
35
|
|
|
36
|
+
/** requestUrl → canonical `source`. Lets `loadFile` skip the source read
|
|
37
|
+
* when a URL it has already canonicalised is requested again — kernel
|
|
38
|
+
* load → boot and the import-controller each ask the loader for the same
|
|
39
|
+
* modules. Without this fast path every duplicate request re-runs the
|
|
40
|
+
* source's `read()` (a `fetch` for `RegistrySource`, a disk read for
|
|
41
|
+
* `LocalFileSource`). */
|
|
42
|
+
private readonly urlToSource = new Map<string, string>();
|
|
43
|
+
|
|
36
44
|
protected sources: ManifestSource[];
|
|
37
45
|
private readonly celEnv: Environment;
|
|
38
46
|
|
|
@@ -67,8 +75,22 @@ export class Loader {
|
|
|
67
75
|
}
|
|
68
76
|
|
|
69
77
|
async resolveEntryPoint(url: string): Promise<string> {
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
// Route through `loadFile` so the resolved source URL and parsed
|
|
79
|
+
// entry are populated in `urlToSource` + `fileCache` in one read.
|
|
80
|
+
// Callers (kernel.load) immediately call `loadGraph(entryUrl)`
|
|
81
|
+
// afterwards — without this priming, the entry file would be read
|
|
82
|
+
// twice (twice over the network for `RegistrySource`).
|
|
83
|
+
const file = await this.loadFile(url);
|
|
84
|
+
return file.source;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Returns the canonical source URL the loader has already mapped `url`
|
|
88
|
+
* to during a prior `loadFile`/`loadModule`/`loadGraph` call, or
|
|
89
|
+
* `undefined` when the URL has not been seen. Callers use it to test
|
|
90
|
+
* set-membership against a previous graph walk's modules without
|
|
91
|
+
* triggering an extra source read. */
|
|
92
|
+
canonicalize(url: string): string | undefined {
|
|
93
|
+
return this.urlToSource.get(url);
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
// --- New API: returns LoadedFile / LoadedModule / LoadedGraph ----------
|
|
@@ -78,8 +100,42 @@ export class Loader {
|
|
|
78
100
|
* private mutable copy must call `parseLoadedFile` directly with the
|
|
79
101
|
* LoadedFile's `text`. */
|
|
80
102
|
async loadFile(url: string, options?: LoadOptions): Promise<LoadedFile> {
|
|
103
|
+
const compileKey = options?.compile ? "compiled" : "raw";
|
|
104
|
+
const knownSource = this.urlToSource.get(url);
|
|
105
|
+
if (knownSource) {
|
|
106
|
+
const cached = this.fileCache.get(`${compileKey}:${knownSource}`);
|
|
107
|
+
if (cached) return cached;
|
|
108
|
+
// The other compile-mode entry is cached — reparse from its text
|
|
109
|
+
// instead of re-reading the source.
|
|
110
|
+
//
|
|
111
|
+
// NOTE for watch-mode reactivation (cli/nodejs/src/commands/run.ts
|
|
112
|
+
// currently has `setupWatchMode` commented out): this branch
|
|
113
|
+
// assumes file contents don't change underneath a single Loader.
|
|
114
|
+
// Reviving watch mode will need a public `invalidate(url)` (or
|
|
115
|
+
// similar) that drops both `urlToSource[url]` and the cached
|
|
116
|
+
// entries for its canonical source before the loader serves the
|
|
117
|
+
// file again.
|
|
118
|
+
const altKey = `${compileKey === "compiled" ? "raw" : "compiled"}:${knownSource}`;
|
|
119
|
+
const alt = this.fileCache.get(altKey);
|
|
120
|
+
if (alt) {
|
|
121
|
+
const reparsed = parseLoadedFile(knownSource, url, alt.text, {
|
|
122
|
+
compile: options?.compile,
|
|
123
|
+
celEnv: this.celEnv,
|
|
124
|
+
});
|
|
125
|
+
this.fileCache.set(`${compileKey}:${knownSource}`, reparsed);
|
|
126
|
+
return reparsed;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
81
130
|
const { text, source } = await this.pick(url).read(url);
|
|
82
|
-
|
|
131
|
+
this.urlToSource.set(url, source);
|
|
132
|
+
// Also map the canonical source to itself so subsequent `loadFile`
|
|
133
|
+
// calls that already received a canonical URL — `kernel.load` passes
|
|
134
|
+
// the result of `resolveEntryPoint` to `loadGraph`, which then asks
|
|
135
|
+
// for that exact URL — hit the urlToSource fast path instead of
|
|
136
|
+
// falling through to a redundant `pick(url).read(url)`.
|
|
137
|
+
this.urlToSource.set(source, source);
|
|
138
|
+
const cacheKey = `${compileKey}:${source}`;
|
|
83
139
|
const cached = this.fileCache.get(cacheKey);
|
|
84
140
|
if (cached && cached.text === text) return cached;
|
|
85
141
|
|
|
@@ -224,7 +280,16 @@ export class Loader {
|
|
|
224
280
|
return { rootSource, entry, modules, importEdges, errors };
|
|
225
281
|
}
|
|
226
282
|
|
|
227
|
-
|
|
283
|
+
/** Resolve an `import` URL against the file it appears in. Relative /
|
|
284
|
+
* absolute-path forms run through the owning `ManifestSource`'s
|
|
285
|
+
* `resolveRelative`; registry refs and full URLs pass through
|
|
286
|
+
* unchanged. Exposed so the import-controller (and any other
|
|
287
|
+
* caller-side resolver) lands on the *exact same* canonical URL the
|
|
288
|
+
* loader used when walking the entry graph — divergent resolution
|
|
289
|
+
* would silently break optimizations like `canonicalize()`-keyed
|
|
290
|
+
* cache hits whenever a non-trivial `ManifestSource.resolveRelative`
|
|
291
|
+
* is in play. */
|
|
292
|
+
resolveImportUrl(fromSource: string, importSource: string): string {
|
|
228
293
|
if (importSource.startsWith(".") || importSource.startsWith("/")) {
|
|
229
294
|
return this.pick(fromSource).resolveRelative(fromSource, importSource);
|
|
230
295
|
}
|