@telorun/analyzer 0.56.0 → 0.56.1

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/README.md CHANGED
@@ -23,9 +23,9 @@ Built to be language-agnostic and infinitely extensible.
23
23
 
24
24
  ```bash
25
25
  # Reconcile your manifest into a running backend
26
- $ telo ./examples/hello-api
26
+ $ telo ./examples/todo-app
27
27
 
28
- {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8844"}
28
+ {"level":30,"time":1771610393008,"pid":1310178,"hostname":"dev","msg":"Server listening at http://127.0.0.1:8077"}
29
29
  ```
30
30
 
31
31
  ## Why use Telo?
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIzE,OAAO,EACL,aAAa,EAEb,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,0BAA0B,CAAC;AAsBlC,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAiBvF,OAAO,EAAsB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,YAAY,CAAC;AAqF/F;;;;;uDAKuD;AACvD,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,YAAY,EACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAClC,aAAa,GAAG;IAAE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS,CAAA;CAAE,CAO9F;AA6FD;0CAC0C;AAC1C,eAAO,MAAM,eAAe,GAAI,OAAO;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,KAAG,OACT,CAAC;AAErD;8EAC8E;AAC9E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGhD;AAuBD;2DAC2D;AAC3D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EACvC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACxB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CASjC;AAED;4EAC4E;AAC5E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAmBvG;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,OAAO,EAAE,EAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,EAC/C,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAC3D,IAAI,CAiDN;AA8mBD,MAAM,WAAW,qBAAqB;IACpC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;gBAEzB,OAAO,GAAE,qBAA0B;IAI/C;;;;;;;;;;;;;;OAcG;IACH,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,EAC3B,eAAe,CAAC,EAAE,eAAe,GAChC,kBAAkB,EAAE;IAywCvB,aAAa,CACX,SAAS,EAAE,gBAAgB,EAAE,EAC7B,OAAO,CAAC,EAAE,eAAe,EACzB,QAAQ,CAAC,EAAE,gBAAgB,EAC3B,eAAe,CAAC,EAAE,eAAe,GAChC,kBAAkB,EAAE;IAMvB,SAAS,CACP,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,EAI1B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,GACtC,gBAAgB,EAAE;IAyBrB,OAAO,CACL,SAAS,EAAE,gBAAgB,EAAE,EAC7B,QAAQ,EAAE,gBAAgB,GACzB;QAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAsB5F"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"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"}
package/dist/analyzer.js CHANGED
@@ -7,7 +7,7 @@ import { effectiveAuthorSchema } from "./extends-resolution.js";
7
7
  import { PERMISSIVE_CONTRACT, resolveContract, } from "./invocation-contract.js";
8
8
  import { buildCallGraph } from "./call-graph.js";
9
9
  import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
10
- import { buildKernelGlobalsSchema, KERNEL_GLOBAL_NAMES, mergeKernelGlobalsIntoContext, } from "./kernel-globals.js";
10
+ import { buildKernelGlobalsIndex, KERNEL_GLOBAL_NAMES, mergeKernelGlobalsIntoContext, } from "./kernel-globals.js";
11
11
  import { buildObservedStateIndex, buildObservedStateResourcesSchema, collectRunReachableNames, observedStateRead, validateObservedStateDeclarations, } from "./validate-observed-state.js";
12
12
  import { computeSuggestKind } from "./kind-suggest.js";
13
13
  import { visitManifest } from "./manifest-visitor.js";
@@ -1361,7 +1361,7 @@ export class StaticAnalyzer {
1361
1361
  : new Set();
1362
1362
  // Build typed kernel globals schema so x-telo-context chain validation
1363
1363
  // recognises variables, secrets, resources, env automatically
1364
- const kernelGlobals = buildKernelGlobalsSchema(allManifests, observedState);
1364
+ const kernelGlobals = buildKernelGlobalsIndex(allManifests, observedState);
1365
1365
  // Fallback context for CEL in a slot with no `x-telo-context` annotation:
1366
1366
  // everything stays open except the typed `.status` nodes, so unknown-field
1367
1367
  // checking reaches observed state everywhere without newly rejecting any
@@ -1377,7 +1377,8 @@ export class StaticAnalyzer {
1377
1377
  : null;
1378
1378
  // The module doc (Application/Library) carries the Application-only `ports`
1379
1379
  // namespace; threaded into per-resource CEL typing so `${{ ports.X }}`
1380
- // resolves its nominal brand cross-doc.
1380
+ // resolves its nominal brand cross-doc. A flattened set holds exactly one —
1381
+ // the entry's; see `buildKernelGlobalsSchema`.
1381
1382
  const moduleManifest = allManifests.find((mm) => mm.kind === "Telo.Application") ??
1382
1383
  allManifests.find((mm) => mm.kind === "Telo.Library");
1383
1384
  // Validate each non-definition, non-system resource
@@ -1849,7 +1850,11 @@ export class StaticAnalyzer {
1849
1850
  aliases,
1850
1851
  allManifests: allManifests,
1851
1852
  });
1852
- effectiveContext = mergeKernelGlobalsIntoContext(withBindingNames(resolvedContext, m), kernelGlobals);
1853
+ effectiveContext = mergeKernelGlobalsIntoContext(withBindingNames(resolvedContext, m),
1854
+ // Typed in the module that DECLARED this resource — for a manifest
1855
+ // forwarded from an imported library, that is its `moduleGlobals`
1856
+ // stamp, not the consuming application's block.
1857
+ kernelGlobals.forResource(m));
1853
1858
  }
1854
1859
  else if (observedStateContext) {
1855
1860
  // No `x-telo-context` matched, so nothing was chain-validated here
@@ -33,8 +33,27 @@ export declare function parseExportEntry(entry: string): ParsedExportEntry;
33
33
  * `byModuleName` and `!ref Alias.name` resolves, while `validate-references` /
34
34
  * the per-resource validation loop never re-walk or re-validate it against the
35
35
  * consumer's scope. A consumer that instead emits every module doc as a peer
36
- * local manifest silently breaks both. */
36
+ * local manifest silently breaks both.
37
+ *
38
+ * Dropping the module doc costs the consumer's analysis the one thing a
39
+ * forwarded manifest's CEL still needs: the `variables` / `secrets` / `ports`
40
+ * contract its expressions are evaluated against. That contract is carried
41
+ * across as {@link ModuleGlobals} on each forwarded manifest instead, so the
42
+ * consumer can check those reads in the DECLARING module's scope rather than
43
+ * either skipping them (a diagnostic performed nowhere) or judging them by its
44
+ * own block (a hard error the library author cannot act on). Stamped here
45
+ * because this is the only point where a manifest and its module doc are both
46
+ * in hand — by the time `analyze()` sees the flat list, the doc is gone. */
37
47
  export declare function selectModuleManifestsForAnalysis(moduleManifests: ResourceManifest[], isRoot: boolean): ResourceManifest[];
48
+ /** The declaring module's config contract, carried on a forwarded manifest so
49
+ * the consumer's analysis can type its CEL globals in the right scope. Only the
50
+ * namespaces whose typing is scope-dependent: `resources` is deliberately
51
+ * absent, see {@link readModuleGlobals}. */
52
+ export interface ModuleGlobals {
53
+ variables?: Record<string, unknown>;
54
+ secrets?: Record<string, unknown>;
55
+ ports?: Record<string, unknown>;
56
+ }
38
57
  /** Produce the flat manifest list `analyze()` consumes today.
39
58
  *
40
59
  * Combines the entry module's manifests with `Telo.Definition`,
@@ -1 +1 @@
1
- {"version":3,"file":"flatten-for-analyzer.d.ts","sourceRoot":"","sources":["../src/flatten-for-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;yEAEyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;4CAI4C;AAC5C,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAGjE;AAED;;;;;;;;;;;;;;;;;;;2CAmB2C;AAC3C,wBAAgB,gCAAgC,CAC9C,eAAe,EAAE,gBAAgB,EAAE,EACnC,MAAM,EAAE,OAAO,GACd,gBAAgB,EAAE,CA6BpB;AAED;;;;;;;;;;;;;;;;;;;6EAmB6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,gBAAgB,EAAE,CAiDzE;AAED;;uBAEuB;AACvB,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;qEAEqE;AACrE,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GAC/C,YAAY,EAAE,CAShB;AAED;;;;;;uFAMuF;AACvF,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACnE,IAAI,CAgDN;AAED;;;;0EAI0E;AAC1E,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAE,CAAC,EACvF,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACnE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA2ClC;AAED;;;;;4DAK4D;AAC5D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EAC5E,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,IAAI,CAWN;AAED;;;;;;;;;;;;;;iGAciG;AACjG,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EAC5E,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,GACpD,IAAI,CAMN;AAgDD;;;;;oEAKoE;AACpE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,WAAW,GAAG,mBAAmB,EAAE,CAuBpF;AAED;;;;;6BAK6B;AAC7B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAEzE"}
1
+ {"version":3,"file":"flatten-for-analyzer.d.ts","sourceRoot":"","sources":["../src/flatten-for-analyzer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGnE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAEtE;;yEAEyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;4CAI4C;AAC5C,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAGjE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6EA6B6E;AAC7E,wBAAgB,gCAAgC,CAC9C,eAAe,EAAE,gBAAgB,EAAE,EACnC,MAAM,EAAE,OAAO,GACd,gBAAgB,EAAE,CAmCpB;AAED;;;6CAG6C;AAC7C,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAkCD;;;;;;;;;;;;;;;;;;;6EAmB6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,WAAW,GAAG,gBAAgB,EAAE,CAiDzE;AAED;;uBAEuB;AACvB,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;qEAEqE;AACrE,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,gBAAgB,EAAE,SAAS,OAAO,EAAE,GAAG,SAAS,GAC/C,YAAY,EAAE,CAShB;AAED;;;;;;uFAMuF;AACvF,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACnE,IAAI,CAgDN;AAED;;;;0EAI0E;AAC1E,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAA;CAAE,CAAC,EACvF,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACnE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA2ClC;AAED;;;;;4DAK4D;AAC5D,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EAC5E,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC9C,IAAI,CAWN;AAED;;;;;;;;;;;;;;iGAciG;AACjG,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,CAAC;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAC,EAC5E,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,GACpD,IAAI,CAMN;AAgDD;;;;;oEAKoE;AACpE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,WAAW,GAAG,mBAAmB,EAAE,CAuBpF;AAED;;;;;6BAK6B;AAC7B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,gBAAgB,EAAE,CAEzE"}
@@ -27,7 +27,17 @@ export function parseExportEntry(entry) {
27
27
  * `byModuleName` and `!ref Alias.name` resolves, while `validate-references` /
28
28
  * the per-resource validation loop never re-walk or re-validate it against the
29
29
  * consumer's scope. A consumer that instead emits every module doc as a peer
30
- * local manifest silently breaks both. */
30
+ * local manifest silently breaks both.
31
+ *
32
+ * Dropping the module doc costs the consumer's analysis the one thing a
33
+ * forwarded manifest's CEL still needs: the `variables` / `secrets` / `ports`
34
+ * contract its expressions are evaluated against. That contract is carried
35
+ * across as {@link ModuleGlobals} on each forwarded manifest instead, so the
36
+ * consumer can check those reads in the DECLARING module's scope rather than
37
+ * either skipping them (a diagnostic performed nowhere) or judging them by its
38
+ * own block (a hard error the library author cannot act on). Stamped here
39
+ * because this is the only point where a manifest and its module doc are both
40
+ * in hand — by the time `analyze()` sees the flat list, the doc is gone. */
31
41
  export function selectModuleManifestsForAnalysis(moduleManifests, isRoot) {
32
42
  if (isRoot)
33
43
  return moduleManifests;
@@ -41,22 +51,57 @@ export function selectModuleManifestsForAnalysis(moduleManifests, isRoot) {
41
51
  continue;
42
52
  exportedResources.add(parseExportEntry(entry).name);
43
53
  }
54
+ const moduleGlobals = readModuleGlobals(libDoc);
44
55
  const out = [];
45
56
  for (const m of moduleManifests) {
46
57
  if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract" || m.kind === "Telo.Import") {
47
- out.push(m);
58
+ out.push(withModuleGlobals(m, moduleGlobals));
48
59
  }
49
60
  else if (!isModuleKind(m.kind) &&
50
61
  typeof m.metadata?.name === "string" &&
51
62
  exportedResources.has(m.metadata.name)) {
52
63
  out.push({
53
64
  ...m,
54
- metadata: { ...m.metadata, forwardedExport: true },
65
+ metadata: {
66
+ ...m.metadata,
67
+ forwardedExport: true,
68
+ ...(moduleGlobals ? { moduleGlobals } : {}),
69
+ },
55
70
  });
56
71
  }
57
72
  }
58
73
  return out;
59
74
  }
75
+ /** Read the schema-map blocks off a module doc, or `undefined` when it declares
76
+ * none (the globals then stay permissive, exactly as an absent doc does).
77
+ *
78
+ * `resources` is NOT carried. The flat list holds only this module's EXPORTED
79
+ * instances, so a name list built from it would omit every internal one — and a
80
+ * `with:`-scoped resource is declared inline rather than as a doc, so it would
81
+ * be missing too. Both would read as "no such resource" in a consumer's pass:
82
+ * a false positive worse than the unchecked read it replaced. The consumer
83
+ * leaves a forwarded manifest's `resources` node open and the library's own
84
+ * analysis, which sees every name, does that check. */
85
+ function readModuleGlobals(libDoc) {
86
+ const doc = libDoc;
87
+ if (!doc)
88
+ return undefined;
89
+ const globals = {};
90
+ for (const key of ["variables", "secrets", "ports"]) {
91
+ const block = doc[key];
92
+ if (block && typeof block === "object" && !Array.isArray(block))
93
+ globals[key] = block;
94
+ }
95
+ return Object.keys(globals).length > 0 ? globals : undefined;
96
+ }
97
+ function withModuleGlobals(m, moduleGlobals) {
98
+ if (!moduleGlobals)
99
+ return m;
100
+ return {
101
+ ...m,
102
+ metadata: { ...m.metadata, moduleGlobals },
103
+ };
104
+ }
60
105
  /** Produce the flat manifest list `analyze()` consumes today.
61
106
  *
62
107
  * Combines the entry module's manifests with `Telo.Definition`,
@@ -11,20 +11,34 @@ import type { ResourceManifest } from "@telorun/sdk";
11
11
  * and referencing `variables.X` / `secrets.X`.
12
12
  */
13
13
  export declare const KERNEL_GLOBAL_NAMES: readonly ["variables", "secrets", "resources", "ports"];
14
+ /** Kernel globals as ONE resource's declaring module sees them. */
15
+ export interface KernelGlobalsIndex {
16
+ forResource(m: ResourceManifest): Record<string, any>;
17
+ }
14
18
  /**
15
- * Build a typed JSON Schema describing the kernel globals available in the
16
- * given manifest set. Used to merge into `x-telo-context` schemas so that
17
- * chain-access validation recognises kernel globals without module authors
19
+ * Build the typed JSON Schema describing the kernel globals available to each
20
+ * resource in a manifest set. Used to merge into `x-telo-context` schemas so
21
+ * that chain-access validation recognises kernel globals without module authors
18
22
  * having to re-declare them.
19
23
  *
20
- * - `variables` / `secrets`: typed from the root module doc prefer
21
- * Telo.Application when present, otherwise fall back to Telo.Library.
22
- * Applications are the root whose variables/secrets contract governs CEL
23
- * in the outer module; Libraries are only relevant when the caller scoped
24
- * the manifest list to a single library's file.
25
- * - `resources`: enumerates all non-system resource names
24
+ * `variables` / `secrets` / `ports` are typed **per declaring module**, because
25
+ * that is the contract the resource's CEL is evaluated against at runtime. An
26
+ * application analysis is flattened `selectModuleManifestsForAnalysis` drops
27
+ * an imported library's module doc and carries its config blocks across as
28
+ * `metadata.moduleGlobals` instead so a resource forwarded from a library is
29
+ * typed from that stamp, and everything else from the entry module's own doc
30
+ * (the only module doc left in the set).
31
+ *
32
+ * Typing every resource from the entry doc is what made a library's
33
+ * `variables.x` a hard error the library author could not act on, and — in the
34
+ * other direction — let a library read a variable it never declared whenever the
35
+ * app happened to declare that name.
36
+ *
37
+ * `resources` is NOT per module: it enumerates every non-system resource name in
38
+ * the set, and stays OPEN for a forwarded manifest (see `readModuleGlobals` for
39
+ * why a name list cannot be carried across the boundary honestly).
26
40
  */
27
- export declare function buildKernelGlobalsSchema(manifests: ResourceManifest[],
41
+ export declare function buildKernelGlobalsIndex(manifests: ResourceManifest[],
28
42
  /** Every resource a CEL read can name, including scope-declared ones (see
29
43
  * `buildObservedStateIndex`). Kinds that declare a `status:` get a typed,
30
44
  * closed `status` node; every other resource node stays open, so no flat read
@@ -32,7 +46,7 @@ export declare function buildKernelGlobalsSchema(manifests: ResourceManifest[],
32
46
  resources?: ReadonlyMap<string, {
33
47
  kind: string;
34
48
  status?: Record<string, any>;
35
- }>): Record<string, any>;
49
+ }>): KernelGlobalsIndex;
36
50
  /**
37
51
  * Merge kernel globals into an `x-telo-context` schema so chain-access
38
52
  * validation recognises `variables`, `secrets`, `resources`, `ports`
@@ -1 +1 @@
1
- {"version":3,"file":"kernel-globals.d.ts","sourceRoot":"","sources":["../src/kernel-globals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,yDAA0D,CAAC;AAS3F;;;;;;;;;;;;GAYG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,gBAAgB,EAAE;AAC7B;;;2CAG2C;AAC3C,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,GAC9E,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CA+CrB;AA4CD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB"}
1
+ {"version":3,"file":"kernel-globals.d.ts","sourceRoot":"","sources":["../src/kernel-globals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAIrD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,yDAA0D,CAAC;AAS3F,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,gBAAgB,EAAE;AAC7B;;;2CAG2C;AAC3C,SAAS,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAAE,CAAC,GAC9E,kBAAkB,CA0BpB;AAmGD;;;;;;;GAOG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CASrB"}
@@ -19,26 +19,68 @@ const SYSTEM_KINDS = new Set([
19
19
  "Telo.Abstract",
20
20
  ]);
21
21
  /**
22
- * Build a typed JSON Schema describing the kernel globals available in the
23
- * given manifest set. Used to merge into `x-telo-context` schemas so that
24
- * chain-access validation recognises kernel globals without module authors
22
+ * Build the typed JSON Schema describing the kernel globals available to each
23
+ * resource in a manifest set. Used to merge into `x-telo-context` schemas so
24
+ * that chain-access validation recognises kernel globals without module authors
25
25
  * having to re-declare them.
26
26
  *
27
- * - `variables` / `secrets`: typed from the root module doc prefer
28
- * Telo.Application when present, otherwise fall back to Telo.Library.
29
- * Applications are the root whose variables/secrets contract governs CEL
30
- * in the outer module; Libraries are only relevant when the caller scoped
31
- * the manifest list to a single library's file.
32
- * - `resources`: enumerates all non-system resource names
27
+ * `variables` / `secrets` / `ports` are typed **per declaring module**, because
28
+ * that is the contract the resource's CEL is evaluated against at runtime. An
29
+ * application analysis is flattened `selectModuleManifestsForAnalysis` drops
30
+ * an imported library's module doc and carries its config blocks across as
31
+ * `metadata.moduleGlobals` instead so a resource forwarded from a library is
32
+ * typed from that stamp, and everything else from the entry module's own doc
33
+ * (the only module doc left in the set).
34
+ *
35
+ * Typing every resource from the entry doc is what made a library's
36
+ * `variables.x` a hard error the library author could not act on, and — in the
37
+ * other direction — let a library read a variable it never declared whenever the
38
+ * app happened to declare that name.
39
+ *
40
+ * `resources` is NOT per module: it enumerates every non-system resource name in
41
+ * the set, and stays OPEN for a forwarded manifest (see `readModuleGlobals` for
42
+ * why a name list cannot be carried across the boundary honestly).
33
43
  */
34
- export function buildKernelGlobalsSchema(manifests,
44
+ export function buildKernelGlobalsIndex(manifests,
35
45
  /** Every resource a CEL read can name, including scope-declared ones (see
36
46
  * `buildObservedStateIndex`). Kinds that declare a `status:` get a typed,
37
47
  * closed `status` node; every other resource node stays open, so no flat read
38
48
  * that passes today can start failing. */
39
49
  resources) {
40
- const moduleManifest = manifests.find((m) => m.kind === "Telo.Application") ??
50
+ const entryDoc = manifests.find((m) => m.kind === "Telo.Application") ??
41
51
  manifests.find((m) => m.kind === "Telo.Library");
52
+ const entrySchema = globalsSchema(entryDoc, buildResourcesSchema(manifests, resources));
53
+ const openResources = { type: "object", additionalProperties: true };
54
+ const byModule = new Map();
55
+ return {
56
+ forResource(m) {
57
+ const meta = m.metadata;
58
+ const stamped = meta?.moduleGlobals;
59
+ if (!stamped)
60
+ return entrySchema;
61
+ const key = meta?.module ?? "";
62
+ const cached = byModule.get(key);
63
+ if (cached)
64
+ return cached;
65
+ const schema = globalsSchema(stamped, openResources);
66
+ byModule.set(key, schema);
67
+ return schema;
68
+ },
69
+ };
70
+ }
71
+ function globalsSchema(doc, resourcesSchema) {
72
+ return {
73
+ type: "object",
74
+ properties: {
75
+ variables: buildSchemaMapSchema(doc?.variables),
76
+ secrets: buildSchemaMapSchema(doc?.secrets),
77
+ resources: resourcesSchema,
78
+ ports: buildPortsSchema(doc?.ports),
79
+ },
80
+ };
81
+ }
82
+ /** Every non-system resource name in the set, plus the scope-declared ones. */
83
+ function buildResourcesSchema(manifests, resources) {
42
84
  const resourceProps = {};
43
85
  for (const m of manifests) {
44
86
  const name = m.metadata?.name;
@@ -67,19 +109,7 @@ resources) {
67
109
  continue;
68
110
  applyObservedStateNode(resourceProps, key, entry.status);
69
111
  }
70
- return {
71
- type: "object",
72
- properties: {
73
- variables: buildSchemaMapSchema(moduleManifest?.variables),
74
- secrets: buildSchemaMapSchema(moduleManifest?.secrets),
75
- resources: {
76
- type: "object",
77
- properties: resourceProps,
78
- additionalProperties: false,
79
- },
80
- ports: buildPortsSchema(moduleManifest?.ports),
81
- },
82
- };
112
+ return { type: "object", properties: resourceProps, additionalProperties: false };
83
113
  }
84
114
  /** Build the closed `ports` chain-access schema: each declared port is an
85
115
  * integer, so `ports.<name>` resolves and `ports.typo` (or member access past
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/analyzer",
3
- "version": "0.56.0",
3
+ "version": "0.56.1",
4
4
  "description": "Telo Analyzer - Static manifest validator for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -48,7 +48,7 @@
48
48
  "@types/node": "^20.0.0",
49
49
  "typescript": "^5.0.0",
50
50
  "vitest": "^2.1.8",
51
- "@telorun/sdk": "0.68.0"
51
+ "@telorun/sdk": "0.70.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@telorun/sdk": "*"
package/src/analyzer.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  import { buildCallGraph } from "./call-graph.js";
26
26
  import { buildDependencyGraph, formatCycle } from "./dependency-graph.js";
27
27
  import {
28
- buildKernelGlobalsSchema,
28
+ buildKernelGlobalsIndex,
29
29
  KERNEL_GLOBAL_NAMES,
30
30
  mergeKernelGlobalsIntoContext,
31
31
  } from "./kernel-globals.js";
@@ -1613,7 +1613,7 @@ export class StaticAnalyzer {
1613
1613
 
1614
1614
  // Build typed kernel globals schema so x-telo-context chain validation
1615
1615
  // recognises variables, secrets, resources, env automatically
1616
- const kernelGlobals = buildKernelGlobalsSchema(allManifests, observedState);
1616
+ const kernelGlobals = buildKernelGlobalsIndex(allManifests, observedState);
1617
1617
 
1618
1618
  // Fallback context for CEL in a slot with no `x-telo-context` annotation:
1619
1619
  // everything stays open except the typed `.status` nodes, so unknown-field
@@ -1632,7 +1632,8 @@ export class StaticAnalyzer {
1632
1632
 
1633
1633
  // The module doc (Application/Library) carries the Application-only `ports`
1634
1634
  // namespace; threaded into per-resource CEL typing so `${{ ports.X }}`
1635
- // resolves its nominal brand cross-doc.
1635
+ // resolves its nominal brand cross-doc. A flattened set holds exactly one —
1636
+ // the entry's; see `buildKernelGlobalsSchema`.
1636
1637
  const moduleManifest =
1637
1638
  allManifests.find((mm) => mm.kind === "Telo.Application") ??
1638
1639
  allManifests.find((mm) => mm.kind === "Telo.Library");
@@ -2217,7 +2218,10 @@ export class StaticAnalyzer {
2217
2218
  });
2218
2219
  effectiveContext = mergeKernelGlobalsIntoContext(
2219
2220
  withBindingNames(resolvedContext, m as Record<string, any>),
2220
- kernelGlobals,
2221
+ // Typed in the module that DECLARED this resource — for a manifest
2222
+ // forwarded from an imported library, that is its `moduleGlobals`
2223
+ // stamp, not the consuming application's block.
2224
+ kernelGlobals.forResource(m),
2221
2225
  );
2222
2226
  } else if (observedStateContext) {
2223
2227
  // No `x-telo-context` matched, so nothing was chain-validated here
@@ -41,7 +41,17 @@ export function parseExportEntry(entry: string): ParsedExportEntry {
41
41
  * `byModuleName` and `!ref Alias.name` resolves, while `validate-references` /
42
42
  * the per-resource validation loop never re-walk or re-validate it against the
43
43
  * consumer's scope. A consumer that instead emits every module doc as a peer
44
- * local manifest silently breaks both. */
44
+ * local manifest silently breaks both.
45
+ *
46
+ * Dropping the module doc costs the consumer's analysis the one thing a
47
+ * forwarded manifest's CEL still needs: the `variables` / `secrets` / `ports`
48
+ * contract its expressions are evaluated against. That contract is carried
49
+ * across as {@link ModuleGlobals} on each forwarded manifest instead, so the
50
+ * consumer can check those reads in the DECLARING module's scope rather than
51
+ * either skipping them (a diagnostic performed nowhere) or judging them by its
52
+ * own block (a hard error the library author cannot act on). Stamped here
53
+ * because this is the only point where a manifest and its module doc are both
54
+ * in hand — by the time `analyze()` sees the flat list, the doc is gone. */
45
55
  export function selectModuleManifestsForAnalysis(
46
56
  moduleManifests: ResourceManifest[],
47
57
  isRoot: boolean,
@@ -58,10 +68,12 @@ export function selectModuleManifestsForAnalysis(
58
68
  exportedResources.add(parseExportEntry(entry).name);
59
69
  }
60
70
 
71
+ const moduleGlobals = readModuleGlobals(libDoc);
72
+
61
73
  const out: ResourceManifest[] = [];
62
74
  for (const m of moduleManifests) {
63
75
  if (m.kind === "Telo.Definition" || m.kind === "Telo.Abstract" || m.kind === "Telo.Import") {
64
- out.push(m);
76
+ out.push(withModuleGlobals(m, moduleGlobals));
65
77
  } else if (
66
78
  !isModuleKind(m.kind) &&
67
79
  typeof m.metadata?.name === "string" &&
@@ -69,13 +81,59 @@ export function selectModuleManifestsForAnalysis(
69
81
  ) {
70
82
  out.push({
71
83
  ...m,
72
- metadata: { ...m.metadata, forwardedExport: true } as ResourceManifest["metadata"],
84
+ metadata: {
85
+ ...m.metadata,
86
+ forwardedExport: true,
87
+ ...(moduleGlobals ? { moduleGlobals } : {}),
88
+ } as ResourceManifest["metadata"],
73
89
  });
74
90
  }
75
91
  }
76
92
  return out;
77
93
  }
78
94
 
95
+ /** The declaring module's config contract, carried on a forwarded manifest so
96
+ * the consumer's analysis can type its CEL globals in the right scope. Only the
97
+ * namespaces whose typing is scope-dependent: `resources` is deliberately
98
+ * absent, see {@link readModuleGlobals}. */
99
+ export interface ModuleGlobals {
100
+ variables?: Record<string, unknown>;
101
+ secrets?: Record<string, unknown>;
102
+ ports?: Record<string, unknown>;
103
+ }
104
+
105
+ /** Read the schema-map blocks off a module doc, or `undefined` when it declares
106
+ * none (the globals then stay permissive, exactly as an absent doc does).
107
+ *
108
+ * `resources` is NOT carried. The flat list holds only this module's EXPORTED
109
+ * instances, so a name list built from it would omit every internal one — and a
110
+ * `with:`-scoped resource is declared inline rather than as a doc, so it would
111
+ * be missing too. Both would read as "no such resource" in a consumer's pass:
112
+ * a false positive worse than the unchecked read it replaced. The consumer
113
+ * leaves a forwarded manifest's `resources` node open and the library's own
114
+ * analysis, which sees every name, does that check. */
115
+ function readModuleGlobals(libDoc: ResourceManifest | undefined): ModuleGlobals | undefined {
116
+ const doc = libDoc as Record<string, any> | undefined;
117
+ if (!doc) return undefined;
118
+ const globals: ModuleGlobals = {};
119
+ for (const key of ["variables", "secrets", "ports"] as const) {
120
+ const block = doc[key];
121
+ if (block && typeof block === "object" && !Array.isArray(block)) globals[key] = block;
122
+ }
123
+ return Object.keys(globals).length > 0 ? globals : undefined;
124
+ }
125
+
126
+ function withModuleGlobals(
127
+ m: ResourceManifest,
128
+ moduleGlobals: ModuleGlobals | undefined,
129
+ ): ResourceManifest {
130
+ if (!moduleGlobals) return m;
131
+ return {
132
+ ...m,
133
+ metadata: { ...m.metadata, moduleGlobals } as ResourceManifest["metadata"],
134
+ };
135
+ }
136
+
79
137
  /** Produce the flat manifest list `analyze()` consumes today.
80
138
  *
81
139
  * Combines the entry module's manifests with `Telo.Definition`,
@@ -22,28 +22,43 @@ const SYSTEM_KINDS = new Set([
22
22
  "Telo.Abstract",
23
23
  ]);
24
24
 
25
+ /** Kernel globals as ONE resource's declaring module sees them. */
26
+ export interface KernelGlobalsIndex {
27
+ forResource(m: ResourceManifest): Record<string, any>;
28
+ }
29
+
25
30
  /**
26
- * Build a typed JSON Schema describing the kernel globals available in the
27
- * given manifest set. Used to merge into `x-telo-context` schemas so that
28
- * chain-access validation recognises kernel globals without module authors
31
+ * Build the typed JSON Schema describing the kernel globals available to each
32
+ * resource in a manifest set. Used to merge into `x-telo-context` schemas so
33
+ * that chain-access validation recognises kernel globals without module authors
29
34
  * having to re-declare them.
30
35
  *
31
- * - `variables` / `secrets`: typed from the root module doc prefer
32
- * Telo.Application when present, otherwise fall back to Telo.Library.
33
- * Applications are the root whose variables/secrets contract governs CEL
34
- * in the outer module; Libraries are only relevant when the caller scoped
35
- * the manifest list to a single library's file.
36
- * - `resources`: enumerates all non-system resource names
36
+ * `variables` / `secrets` / `ports` are typed **per declaring module**, because
37
+ * that is the contract the resource's CEL is evaluated against at runtime. An
38
+ * application analysis is flattened `selectModuleManifestsForAnalysis` drops
39
+ * an imported library's module doc and carries its config blocks across as
40
+ * `metadata.moduleGlobals` instead so a resource forwarded from a library is
41
+ * typed from that stamp, and everything else from the entry module's own doc
42
+ * (the only module doc left in the set).
43
+ *
44
+ * Typing every resource from the entry doc is what made a library's
45
+ * `variables.x` a hard error the library author could not act on, and — in the
46
+ * other direction — let a library read a variable it never declared whenever the
47
+ * app happened to declare that name.
48
+ *
49
+ * `resources` is NOT per module: it enumerates every non-system resource name in
50
+ * the set, and stays OPEN for a forwarded manifest (see `readModuleGlobals` for
51
+ * why a name list cannot be carried across the boundary honestly).
37
52
  */
38
- export function buildKernelGlobalsSchema(
53
+ export function buildKernelGlobalsIndex(
39
54
  manifests: ResourceManifest[],
40
55
  /** Every resource a CEL read can name, including scope-declared ones (see
41
56
  * `buildObservedStateIndex`). Kinds that declare a `status:` get a typed,
42
57
  * closed `status` node; every other resource node stays open, so no flat read
43
58
  * that passes today can start failing. */
44
59
  resources?: ReadonlyMap<string, { kind: string; status?: Record<string, any> }>,
45
- ): Record<string, any> {
46
- const moduleManifest =
60
+ ): KernelGlobalsIndex {
61
+ const entryDoc =
47
62
  (manifests.find((m) => m.kind === "Telo.Application") as
48
63
  | Record<string, any>
49
64
  | undefined) ??
@@ -51,6 +66,52 @@ export function buildKernelGlobalsSchema(
51
66
  | Record<string, any>
52
67
  | undefined);
53
68
 
69
+ const entrySchema = globalsSchema(entryDoc, buildResourcesSchema(manifests, resources));
70
+ const openResources = { type: "object", additionalProperties: true };
71
+ const byModule = new Map<string, Record<string, any>>();
72
+
73
+ return {
74
+ forResource(m: ResourceManifest): Record<string, any> {
75
+ const meta = m.metadata as { module?: string; moduleGlobals?: ModuleGlobals } | undefined;
76
+ const stamped = meta?.moduleGlobals;
77
+ if (!stamped) return entrySchema;
78
+ const key = meta?.module ?? "";
79
+ const cached = byModule.get(key);
80
+ if (cached) return cached;
81
+ const schema = globalsSchema(stamped, openResources);
82
+ byModule.set(key, schema);
83
+ return schema;
84
+ },
85
+ };
86
+ }
87
+
88
+ /** The blocks a declaring module contributes to its resources' CEL globals. */
89
+ interface ModuleGlobals {
90
+ variables?: Record<string, unknown>;
91
+ secrets?: Record<string, unknown>;
92
+ ports?: Record<string, unknown>;
93
+ }
94
+
95
+ function globalsSchema(
96
+ doc: ModuleGlobals | Record<string, any> | undefined,
97
+ resourcesSchema: Record<string, any>,
98
+ ): Record<string, any> {
99
+ return {
100
+ type: "object",
101
+ properties: {
102
+ variables: buildSchemaMapSchema(doc?.variables as Record<string, any> | undefined),
103
+ secrets: buildSchemaMapSchema(doc?.secrets as Record<string, any> | undefined),
104
+ resources: resourcesSchema,
105
+ ports: buildPortsSchema(doc?.ports as Record<string, any> | undefined),
106
+ },
107
+ };
108
+ }
109
+
110
+ /** Every non-system resource name in the set, plus the scope-declared ones. */
111
+ function buildResourcesSchema(
112
+ manifests: ResourceManifest[],
113
+ resources?: ReadonlyMap<string, { kind: string; status?: Record<string, any> }>,
114
+ ): Record<string, any> {
54
115
  const resourceProps: Record<string, any> = {};
55
116
  for (const m of manifests) {
56
117
  const name = m.metadata?.name as string | undefined;
@@ -76,19 +137,7 @@ export function buildKernelGlobalsSchema(
76
137
  applyObservedStateNode(resourceProps, key, entry.status);
77
138
  }
78
139
 
79
- return {
80
- type: "object",
81
- properties: {
82
- variables: buildSchemaMapSchema(moduleManifest?.variables),
83
- secrets: buildSchemaMapSchema(moduleManifest?.secrets),
84
- resources: {
85
- type: "object",
86
- properties: resourceProps,
87
- additionalProperties: false,
88
- },
89
- ports: buildPortsSchema(moduleManifest?.ports),
90
- },
91
- };
140
+ return { type: "object", properties: resourceProps, additionalProperties: false };
92
141
  }
93
142
 
94
143
  /** Build the closed `ports` chain-access schema: each declared port is an