@telorun/analyzer 0.10.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +3 -3
  2. package/dist/analysis-registry.d.ts +7 -0
  3. package/dist/analysis-registry.d.ts.map +1 -1
  4. package/dist/analysis-registry.js +38 -0
  5. package/dist/analyzer.d.ts.map +1 -1
  6. package/dist/analyzer.js +198 -6
  7. package/dist/builtins.d.ts.map +1 -1
  8. package/dist/builtins.js +158 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/kernel-globals.d.ts.map +1 -1
  13. package/dist/kernel-globals.js +9 -11
  14. package/dist/normalize-inline-resources.d.ts.map +1 -1
  15. package/dist/normalize-inline-resources.js +26 -14
  16. package/dist/position-metadata.d.ts +5 -1
  17. package/dist/position-metadata.d.ts.map +1 -1
  18. package/dist/position-metadata.js +8 -1
  19. package/dist/reference-field-map.d.ts +21 -4
  20. package/dist/reference-field-map.d.ts.map +1 -1
  21. package/dist/reference-field-map.js +35 -19
  22. package/dist/residual-schema.d.ts +23 -0
  23. package/dist/residual-schema.d.ts.map +1 -0
  24. package/dist/residual-schema.js +45 -0
  25. package/dist/rewrite-synthetic-origins.d.ts +10 -0
  26. package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
  27. package/dist/rewrite-synthetic-origins.js +55 -0
  28. package/dist/validate-cel-context.d.ts +61 -7
  29. package/dist/validate-cel-context.d.ts.map +1 -1
  30. package/dist/validate-cel-context.js +90 -8
  31. package/dist/validate-provider-coherence.d.ts +23 -0
  32. package/dist/validate-provider-coherence.d.ts.map +1 -0
  33. package/dist/validate-provider-coherence.js +148 -0
  34. package/dist/validate-references.js +24 -24
  35. package/package.json +5 -3
  36. package/src/analysis-registry.ts +37 -0
  37. package/src/analyzer.ts +240 -8
  38. package/src/builtins.ts +158 -1
  39. package/src/index.ts +1 -0
  40. package/src/kernel-globals.ts +9 -11
  41. package/src/normalize-inline-resources.ts +48 -13
  42. package/src/position-metadata.ts +8 -1
  43. package/src/reference-field-map.ts +46 -18
  44. package/src/residual-schema.ts +49 -0
  45. package/src/rewrite-synthetic-origins.ts +75 -0
  46. package/src/validate-cel-context.ts +111 -8
  47. package/src/validate-provider-coherence.ts +166 -0
  48. package/src/validate-references.ts +24 -24
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
 
@@ -68,14 +70,108 @@ function lookupDefinitionTypeField(
68
70
 
69
71
  const SOURCE = "telo-analyzer";
70
72
 
73
+ /** Build a closed JSON Schema for the `self` CEL variable available inside a
74
+ * `Telo.Definition` template body. Mirrors the runtime template controller's
75
+ * `const self = { ...resource, name: resource.metadata.name };` — every
76
+ * property the user declared in `schema:` plus synthetic `name` / `kind` and
77
+ * the metadata sub-object (kept open since metadata legitimately carries
78
+ * arbitrary user-added fields). */
79
+ function buildSelfSchema(definition: Record<string, any>): Record<string, any> {
80
+ const userSchema = (definition.schema ?? {}) as Record<string, any>;
81
+ const userProps = (userSchema.properties ?? {}) as Record<string, any>;
82
+ const userRequired = Array.isArray(userSchema.required) ? userSchema.required : [];
83
+ return {
84
+ type: "object",
85
+ additionalProperties: false,
86
+ properties: {
87
+ ...userProps,
88
+ name: { type: "string" },
89
+ kind: { type: "string" },
90
+ metadata: {
91
+ type: "object",
92
+ additionalProperties: true,
93
+ properties: { name: { type: "string" } },
94
+ },
95
+ },
96
+ required: [...userRequired, "name", "kind"],
97
+ };
98
+ }
99
+
100
+ /** Build the JSON Schema for the `inputs` CEL variable available inside an
101
+ * invocable template body. Three-layer fallback mirroring the runtime's
102
+ * caller-supplied inputs:
103
+ * 1. The definition's own `inputType:` field (preferred).
104
+ * 2. The `extends:`-declared abstract's `inputType:` (so a concrete
105
+ * definition inheriting a contract gets typed inputs without
106
+ * redeclaring them).
107
+ * 3. Undefined — caller signals opaque `map<string, dyn>` upstream. */
108
+ function lookupTemplateInputsSchema(
109
+ definition: Record<string, any>,
110
+ defs: DefinitionRegistry,
111
+ aliases: AliasResolver,
112
+ allManifests: Record<string, any>[],
113
+ ): Record<string, any> | undefined {
114
+ const own = resolveTypeFieldToSchema(definition.inputType, allManifests);
115
+ if (own) return own;
116
+ const ext = definition.extends as string | undefined;
117
+ if (typeof ext === "string" && ext.length > 0) {
118
+ const canonical = aliases.resolveKind(ext) ?? ext;
119
+ const abstractDef = defs.resolve(canonical);
120
+ if (abstractDef) {
121
+ const inherited = resolveTypeFieldToSchema(
122
+ (abstractDef as unknown as Record<string, unknown>).inputType,
123
+ allManifests,
124
+ );
125
+ if (inherited) return inherited;
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ /** Returns a "resolver-facing" view of the manifest where the fields used as
132
+ * navigation roots by Telo.Definition's `x-telo-context-from-root` annotations
133
+ * have been pre-augmented:
134
+ * - `schema` → augmented `self` schema (synthetic `name`/`kind`/metadata).
135
+ * - `inputType` → resolved with extends fallback when the field isn't
136
+ * declared directly on the definition.
137
+ *
138
+ * For non-definition manifests the original object is returned. */
139
+ function manifestRootForResolver(
140
+ m: Record<string, any>,
141
+ defs: DefinitionRegistry,
142
+ aliases: AliasResolver,
143
+ allManifests: Record<string, any>[],
144
+ ): Record<string, any> {
145
+ if (m.kind !== "Telo.Definition") return m;
146
+ const inputs = lookupTemplateInputsSchema(m, defs, aliases, allManifests);
147
+ return {
148
+ ...m,
149
+ schema: buildSelfSchema(m),
150
+ ...(inputs ? { inputType: inputs } : {}),
151
+ };
152
+ }
153
+
71
154
  /**
72
155
  * Walk a JSON Schema tree and collect all `x-telo-context` annotations,
73
156
  * returning them as `{ scope, schema }` pairs using JSONPath-style scopes —
74
157
  * the same format the analyzer uses for CEL context validation.
158
+ *
159
+ * Result is sorted by scope specificity (longer scope first) so that the
160
+ * per-expression resolver's first-match-wins logic picks the most-specific
161
+ * context. Without this, a broader ancestor scope (e.g. `$.resources[*]`)
162
+ * could shadow a narrower descendant scope whose activation differs.
75
163
  */
76
164
  function extractContextsFromSchema(
77
165
  schema: Record<string, any>,
78
166
  path = "$",
167
+ ): Array<{ scope: string; schema: Record<string, any> }> {
168
+ const all = collectContexts(schema, path);
169
+ return all.sort((a, b) => b.scope.length - a.scope.length);
170
+ }
171
+
172
+ function collectContexts(
173
+ schema: Record<string, any>,
174
+ path: string,
79
175
  ): Array<{ scope: string; schema: Record<string, any> }> {
80
176
  if (!schema || typeof schema !== "object") return [];
81
177
  const results: Array<{ scope: string; schema: Record<string, any> }> = [];
@@ -86,18 +182,18 @@ function extractContextsFromSchema(
86
182
 
87
183
  if (schema.properties) {
88
184
  for (const [key, value] of Object.entries(schema.properties as Record<string, any>)) {
89
- results.push(...extractContextsFromSchema(value, `${path}.${key}`));
185
+ results.push(...collectContexts(value, `${path}.${key}`));
90
186
  }
91
187
  }
92
188
 
93
189
  if (schema.items && typeof schema.items === "object") {
94
- results.push(...extractContextsFromSchema(schema.items, `${path}[*]`));
190
+ results.push(...collectContexts(schema.items, `${path}[*]`));
95
191
  }
96
192
 
97
193
  for (const key of ["oneOf", "anyOf", "allOf"] as const) {
98
194
  if (Array.isArray(schema[key])) {
99
195
  for (const subschema of schema[key]) {
100
- results.push(...extractContextsFromSchema(subschema, path));
196
+ results.push(...collectContexts(subschema, path));
101
197
  }
102
198
  }
103
199
  }
@@ -535,6 +631,35 @@ export class StaticAnalyzer {
535
631
  }
536
632
  }
537
633
 
634
+ // Library env: rejection — `env:` on a Library `variables` / `secrets`
635
+ // entry is forbidden. The Library entry schema is otherwise open so that
636
+ // any JSON Schema property schema is valid; this targeted check produces
637
+ // a clear diagnostic instead of a generic "additional property" error.
638
+ for (const m of allManifests) {
639
+ if (m.kind !== "Telo.Library") continue;
640
+ const filePath = (m.metadata as { source?: string } | undefined)?.source;
641
+ const moduleName = m.metadata?.name as string | undefined;
642
+ const resource = moduleName ? { kind: m.kind, name: moduleName } : undefined;
643
+ for (const block of ["variables", "secrets"] as const) {
644
+ const entries = (m as Record<string, any>)[block];
645
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) continue;
646
+ for (const [entryName, entry] of Object.entries(entries)) {
647
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
648
+ if ("env" in (entry as Record<string, unknown>)) {
649
+ diagnostics.push({
650
+ severity: DiagnosticSeverity.Error,
651
+ code: "LIBRARY_ENV_KEY_REJECTED",
652
+ source: SOURCE,
653
+ message:
654
+ `Telo.Library ${block}/${entryName}: 'env:' is only permitted on Telo.Application entries. ` +
655
+ `Libraries must receive values from importers via the parent manifest's variables / secrets block.`,
656
+ data: { resource, filePath, path: `${block}.${entryName}.env` },
657
+ });
658
+ }
659
+ }
660
+ }
661
+ }
662
+
538
663
  // Build typed kernel globals schema so x-telo-context chain validation
539
664
  // recognises variables, secrets, resources, env automatically
540
665
  const kernelGlobals = buildKernelGlobalsSchema(allManifests);
@@ -552,7 +677,11 @@ export class StaticAnalyzer {
552
677
  });
553
678
  continue;
554
679
  }
555
- if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") {
680
+ // Abstracts carry only inputType / outputType schema fields and no template
681
+ // body — nothing for the per-resource walk to validate. Definitions are now
682
+ // walked: their template bodies (`resources` / `invoke` / `run` / `provide`)
683
+ // contain CEL that must be checked against `self` / `inputs` / `result`.
684
+ if (m.kind === "Telo.Abstract") {
556
685
  continue;
557
686
  }
558
687
 
@@ -612,6 +741,97 @@ export class StaticAnalyzer {
612
741
  // (Invocation context compatibility check is handled via x-telo-context in the CEL pass below)
613
742
  }
614
743
 
744
+ // Template-body structural validations: check that template entry-points produce
745
+ // values matching the contract of their dispatch target and (for `provide:`)
746
+ // the abstract this definition `extends`. CEL fields inside the templated
747
+ // values are replaced with type-appropriate placeholders before AJV runs —
748
+ // same pattern as the per-resource schema validation above.
749
+ for (const m of allManifests) {
750
+ if (m.kind !== "Telo.Definition") continue;
751
+ const filePath = (m.metadata as { source?: string } | undefined)?.source;
752
+ const name = (m.metadata as any)?.name as string | undefined;
753
+ if (!name) continue;
754
+ const resource = { kind: m.kind, name };
755
+ const md = m as Record<string, any>;
756
+
757
+ const emitTargetMismatch = (
758
+ targetKind: string,
759
+ valueSchema: Record<string, any>,
760
+ value: unknown,
761
+ path: string,
762
+ ) => {
763
+ const substituted = substituteCelFields(value, valueSchema);
764
+ const issues = validateAgainstSchema(substituted, valueSchema);
765
+ for (const issue of issues) {
766
+ diagnostics.push({
767
+ severity: DiagnosticSeverity.Error,
768
+ code: "TEMPLATE_TARGET_MISMATCH",
769
+ source: SOURCE,
770
+ message: `${m.kind}/${name}: ${path} does not satisfy ${targetKind}'s contract: ${issue.message}`,
771
+ data: { resource, filePath, path: issue.path ? `${path}.${issue.path}` : path },
772
+ });
773
+ }
774
+ };
775
+
776
+ // Resolve the dispatch target's kind, if statically known. Object-form
777
+ // `invoke: { kind, name }` and `provide: { kind, name }` carry it; the
778
+ // string-form `invoke: "name"` does not (the matching resource entry would
779
+ // need to be located by expanded name — out of scope here).
780
+ const invoke = md.invoke;
781
+ const provide = md.provide;
782
+ let dispatchKind: string | undefined;
783
+ if (invoke && typeof invoke === "object" && !Array.isArray(invoke) && typeof invoke.kind === "string") {
784
+ dispatchKind = invoke.kind;
785
+ } else if (
786
+ provide &&
787
+ typeof provide === "object" &&
788
+ !Array.isArray(provide) &&
789
+ typeof provide.kind === "string"
790
+ ) {
791
+ dispatchKind = provide.kind;
792
+ }
793
+
794
+ // Top-level `inputs:` (sibling of `invoke:` / `provide:`) carries the
795
+ // values passed to the dispatch target's invoke(). Validate against the
796
+ // target's declared `inputType` when both sides have one.
797
+ if (dispatchKind && md.inputs && typeof md.inputs === "object") {
798
+ const targetSchema = lookupDefinitionTypeField(
799
+ dispatchKind,
800
+ "inputType",
801
+ defs,
802
+ aliases,
803
+ allManifests as Record<string, any>[],
804
+ );
805
+ if (targetSchema) {
806
+ emitTargetMismatch(dispatchKind, targetSchema, md.inputs, "inputs");
807
+ }
808
+ }
809
+
810
+ // Top-level `result:` is a post-call mapping that must satisfy the abstract
811
+ // this definition `extends` (`outputType`). It's a sibling of whichever
812
+ // dispatch entry-point declared a kind-typed target (`provide:` or
813
+ // `invoke:`). The target's outputType lives on the dispatcher's `kind`
814
+ // and is what `result` is typed against *inside* CEL — separate role.
815
+ const hasDispatchObject =
816
+ (provide && typeof provide === "object" && !Array.isArray(provide)) ||
817
+ (invoke && typeof invoke === "object" && !Array.isArray(invoke));
818
+ if (hasDispatchObject && md.result && typeof md.result === "object") {
819
+ const extendsValue = md.extends as string | undefined;
820
+ if (typeof extendsValue === "string" && extendsValue.length > 0) {
821
+ const abstractSchema = lookupDefinitionTypeField(
822
+ extendsValue,
823
+ "outputType",
824
+ defs,
825
+ aliases,
826
+ allManifests as Record<string, any>[],
827
+ );
828
+ if (abstractSchema) {
829
+ emitTargetMismatch(extendsValue, abstractSchema, md.result, "result");
830
+ }
831
+ }
832
+ }
833
+ }
834
+
615
835
  // Validate CEL syntax and context variable access in all manifests
616
836
  for (const m of allManifests) {
617
837
  const resource = { kind: m.kind, name: m.metadata?.name as string };
@@ -670,11 +890,18 @@ export class StaticAnalyzer {
670
890
  const manifestItem = matchedScope
671
891
  ? getManifestItem(path, matchedScope, m as Record<string, any>)
672
892
  : (m as Record<string, any>);
673
- const resolvedContext = resolveContextAnnotations(
674
- matchedContext,
675
- manifestItem,
893
+ const rootForResolver = manifestRootForResolver(
894
+ m as Record<string, any>,
895
+ defs,
896
+ aliases,
676
897
  allManifests as Record<string, any>[],
677
898
  );
899
+ const resolvedContext = resolveContextAnnotations(matchedContext, manifestItem, {
900
+ manifestRoot: rootForResolver,
901
+ defs,
902
+ aliases,
903
+ allManifests: allManifests as Record<string, any>[],
904
+ });
678
905
  effectiveContext = mergeKernelGlobalsIntoContext(resolvedContext, kernelGlobals);
679
906
  }
680
907
 
@@ -722,10 +949,15 @@ export class StaticAnalyzer {
722
949
  // Validate `extends` fields and flag legacy `capability: <UserAbstract>` overload.
723
950
  diagnostics.push(...validateExtends(allManifests, defs, aliases));
724
951
 
952
+ // Validate provider coherence rules for `provide:` template-target definitions.
953
+ diagnostics.push(...validateProviderCoherence(allManifests, defs, aliases));
954
+
725
955
  // Validate throws: declarations and catches: coverage (rules 1, 2, 4, 7)
726
956
  diagnostics.push(...validateThrowsCoverage(allManifests, defs, aliases, this.celEnv));
727
957
 
728
- return diagnostics;
958
+ // Reroute diagnostics on synthetic (inline-extracted) resources back to
959
+ // the chain root so position-index lookups land on the parent doc.
960
+ return rewriteSyntheticOrigins(diagnostics, allManifests);
729
961
  }
730
962
 
731
963
  analyzeErrors(
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
- schema: { type: "object" },
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",
@@ -108,6 +227,44 @@ export const KERNEL_BUILTINS: ResourceDefinition[] = [
108
227
  type: "array",
109
228
  items: { type: "string" },
110
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
+ },
111
268
  },
112
269
  required: ["metadata"],
113
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,
@@ -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. Falls back to
76
- * an open map when the module declares no variables/secrets. */
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
- if (!schemaMap || typeof schemaMap !== "object" || Array.isArray(schemaMap)) {
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
  }