@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.
Files changed (92) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +3 -3
  3. package/dist/adapters/http-adapter.d.ts +10 -0
  4. package/dist/adapters/http-adapter.d.ts.map +1 -0
  5. package/dist/adapters/http-adapter.js +18 -0
  6. package/dist/adapters/node-adapter.d.ts +17 -0
  7. package/dist/adapters/node-adapter.d.ts.map +1 -0
  8. package/dist/adapters/node-adapter.js +71 -0
  9. package/dist/adapters/registry-adapter.d.ts +15 -0
  10. package/dist/adapters/registry-adapter.d.ts.map +1 -0
  11. package/dist/adapters/registry-adapter.js +53 -0
  12. package/dist/analysis-registry.d.ts +7 -0
  13. package/dist/analysis-registry.d.ts.map +1 -1
  14. package/dist/analysis-registry.js +38 -0
  15. package/dist/analyzer.d.ts +15 -0
  16. package/dist/analyzer.d.ts.map +1 -1
  17. package/dist/analyzer.js +268 -7
  18. package/dist/builtins.d.ts.map +1 -1
  19. package/dist/builtins.js +172 -1
  20. package/dist/definition-registry.d.ts.map +1 -1
  21. package/dist/definition-registry.js +16 -0
  22. package/dist/dependency-graph.d.ts.map +1 -1
  23. package/dist/dependency-graph.js +27 -13
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +2 -0
  27. package/dist/kernel-globals.d.ts.map +1 -1
  28. package/dist/kernel-globals.js +9 -11
  29. package/dist/manifest-loader.d.ts +23 -1
  30. package/dist/manifest-loader.d.ts.map +1 -1
  31. package/dist/manifest-loader.js +66 -3
  32. package/dist/normalize-inline-resources.d.ts.map +1 -1
  33. package/dist/normalize-inline-resources.js +26 -14
  34. package/dist/position-metadata.d.ts +11 -2
  35. package/dist/position-metadata.d.ts.map +1 -1
  36. package/dist/position-metadata.js +18 -3
  37. package/dist/precompile.d.ts.map +1 -1
  38. package/dist/precompile.js +9 -1
  39. package/dist/reference-field-map.d.ts +21 -4
  40. package/dist/reference-field-map.d.ts.map +1 -1
  41. package/dist/reference-field-map.js +93 -25
  42. package/dist/residual-schema.d.ts +23 -0
  43. package/dist/residual-schema.d.ts.map +1 -0
  44. package/dist/residual-schema.js +45 -0
  45. package/dist/resolve-ref-sentinels.d.ts +27 -0
  46. package/dist/resolve-ref-sentinels.d.ts.map +1 -0
  47. package/dist/resolve-ref-sentinels.js +114 -0
  48. package/dist/rewrite-synthetic-origins.d.ts +10 -0
  49. package/dist/rewrite-synthetic-origins.d.ts.map +1 -0
  50. package/dist/rewrite-synthetic-origins.js +55 -0
  51. package/dist/schema-compat.d.ts +7 -1
  52. package/dist/schema-compat.d.ts.map +1 -1
  53. package/dist/schema-compat.js +19 -2
  54. package/dist/system-kinds.d.ts +25 -0
  55. package/dist/system-kinds.d.ts.map +1 -0
  56. package/dist/system-kinds.js +34 -0
  57. package/dist/types.d.ts +12 -0
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/validate-cel-context.d.ts +61 -7
  60. package/dist/validate-cel-context.d.ts.map +1 -1
  61. package/dist/validate-cel-context.js +90 -8
  62. package/dist/validate-provider-coherence.d.ts +23 -0
  63. package/dist/validate-provider-coherence.d.ts.map +1 -0
  64. package/dist/validate-provider-coherence.js +148 -0
  65. package/dist/validate-references.d.ts.map +1 -1
  66. package/dist/validate-references.js +141 -36
  67. package/dist/with-synthetic-positions.d.ts +28 -0
  68. package/dist/with-synthetic-positions.d.ts.map +1 -0
  69. package/dist/with-synthetic-positions.js +45 -0
  70. package/package.json +7 -4
  71. package/src/analysis-registry.ts +37 -0
  72. package/src/analyzer.ts +313 -9
  73. package/src/builtins.ts +172 -1
  74. package/src/definition-registry.ts +15 -0
  75. package/src/dependency-graph.ts +27 -14
  76. package/src/index.ts +2 -0
  77. package/src/kernel-globals.ts +9 -11
  78. package/src/manifest-loader.ts +69 -4
  79. package/src/normalize-inline-resources.ts +48 -13
  80. package/src/position-metadata.ts +18 -3
  81. package/src/precompile.ts +8 -1
  82. package/src/reference-field-map.ts +129 -24
  83. package/src/residual-schema.ts +49 -0
  84. package/src/resolve-ref-sentinels.ts +127 -0
  85. package/src/rewrite-synthetic-origins.ts +75 -0
  86. package/src/schema-compat.ts +19 -2
  87. package/src/system-kinds.ts +37 -0
  88. package/src/types.ts +12 -0
  89. package/src/validate-cel-context.ts +111 -8
  90. package/src/validate-provider-coherence.ts +166 -0
  91. package/src/validate-references.ts +138 -35
  92. 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(...extractContextsFromSchema(value, `${path}.${key}`));
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(...extractContextsFromSchema(schema.items, `${path}[*]`));
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(...extractContextsFromSchema(subschema, path));
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
- if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract") {
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 resolvedContext = resolveContextAnnotations(
674
- matchedContext,
675
- manifestItem,
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
- return diagnostics;
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
- return normalizeInlineResources(
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
- 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",
@@ -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);