@@ -148,16 +148,41 @@ def _target_of(
148148
149149
150150def _check_hidden (mark : OpenForm , target : WebFunction ) -> None :
151- names = {param .name for param in target .schema .params }
152-
153151 for name in mark .hidden :
154- if name not in names :
152+ if not _has_hidden_path ( target . plan , name ) :
155153 raise ReturnContractError (
156154 f"unknown hidden field { name !r} for OpenForm target "
157155 f"{ target .slug !r} "
158156 )
159157
160158
159+ def _has_hidden_path (node : dict [str , Any ], path : str ) -> bool :
160+ """Resolve visibility against the same plan the browser compiles.
161+
162+ Optional and choice wrappers do not add path components. A list consumes
163+ '*', so its rule also applies to items created later in the browser.
164+ """
165+ kind = node ["kind" ]
166+ if kind in ("form" , "object" ):
167+ return any (
168+ path == field ["name" ] or (
169+ path .startswith (field ["name" ] + "." )
170+ and _has_hidden_path (field ["node" ], path [len (field ["name" ]) + 1 :])
171+ )
172+ for field in node ["fields" ]
173+ )
174+ if kind == "list" :
175+ return path == "*" or (
176+ path .startswith ("*." ) and _has_hidden_path (node ["item" ], path [2 :])
177+ )
178+ if kind == "optional" :
179+ return _has_hidden_path (node ["node" ], path )
180+ if kind == "choice" :
181+ return any (_has_hidden_path (branch ["node" ], path )
182+ for branch in node ["branches" ])
183+ return False
184+
185+
161186FunctionEntry : TypeAlias = Callable [..., Any ] | WebFunction
162187FunctionInput : TypeAlias = (
163188 WebFunctions | FunctionEntry | Iterable [FunctionEntry ]
0 commit comments