@@ -115,7 +115,13 @@ def evaluate_flag_dependency(
115115 device_id = None ,
116116):
117117 """
118- Evaluate a flag dependency property according to the dependency chain algorithm.
118+ Evaluate a flag dependency condition under local evaluation.
119+
120+ The dependency_chain only establishes the order in which flags are evaluated
121+ and cached (the referenced flag itself is always the last member). The
122+ outcome is decided solely by comparing the referenced flag's evaluated value
123+ against the condition's expected value; ancestors influence it only through
124+ the referenced flag's own recursive evaluation.
119125
120126 Args:
121127 property: Flag property with type="flag" and dependency_chain
@@ -127,7 +133,12 @@ def evaluate_flag_dependency(
127133 device_id: The device ID for bucketing (optional)
128134
129135 Returns:
130- bool: True if all dependencies in the chain evaluate to True, False otherwise
136+ bool: Whether the referenced flag's evaluated value matches the
137+ condition's expected value.
138+
139+ Raises:
140+ InconclusiveMatchError: If the condition is malformed or the chain
141+ cannot be conclusively evaluated locally.
131142 """
132143 if flags_by_key is None or evaluation_cache is None :
133144 # Cannot evaluate flag dependencies without required context
@@ -151,93 +162,81 @@ def evaluate_flag_dependency(
151162 f"Circular dependency detected for flag '{ property .get ('key' , 'unknown' )} '"
152163 )
153164
154- # Evaluate all dependencies in the chain order
155- for dep_flag_key in dependency_chain :
156- if dep_flag_key not in evaluation_cache :
157- # Need to evaluate this dependency first
158- dep_flag = flags_by_key .get (dep_flag_key )
159- if not dep_flag :
160- # Missing flag dependency - cannot evaluate locally
161- evaluation_cache [dep_flag_key ] = None
162- raise InconclusiveMatchError (
163- f"Cannot evaluate flag dependency '{ dep_flag_key } ' - flag not found in local flags"
164- )
165- else :
166- # Check if the flag is active (same check as in client._compute_flag_locally)
167- if not dep_flag .get ("active" ):
168- evaluation_cache [dep_flag_key ] = False
169- else :
170- # Recursively evaluate the dependency
171- try :
172- dep_flag_filters = dep_flag .get ("filters" ) or {}
173- dep_aggregation_group_type_index = dep_flag_filters .get (
174- "aggregation_group_type_index"
175- )
176- if dep_aggregation_group_type_index is not None :
177- # Group flags should continue bucketing by the group key
178- # from the current evaluation context.
179- dep_bucketing_value = distinct_id
180- else :
181- dep_bucketing_value = resolve_bucketing_value (
182- dep_flag , distinct_id , device_id
183- )
184- dep_result = match_feature_flag_properties (
185- dep_flag ,
186- distinct_id ,
187- properties ,
188- cohort_properties = cohort_properties ,
189- flags_by_key = flags_by_key ,
190- evaluation_cache = evaluation_cache ,
191- device_id = device_id ,
192- bucketing_value = dep_bucketing_value ,
193- )
194- evaluation_cache [dep_flag_key ] = dep_result
195- except InconclusiveMatchError as e :
196- # If we can't evaluate a dependency, store None and propagate the error
197- evaluation_cache [dep_flag_key ] = None
198- raise InconclusiveMatchError (
199- f"Cannot evaluate flag dependency '{ dep_flag_key } ': { e } "
200- ) from e
201-
202- # Check the cached result
203- cached_result = evaluation_cache [dep_flag_key ]
204- if cached_result is None :
205- # Previously inconclusive - raise error again
206- raise InconclusiveMatchError (
207- f"Flag dependency '{ dep_flag_key } ' was previously inconclusive"
208- )
209- elif not cached_result :
210- # Definitive False result - dependency failed
211- return False
212-
213- # All dependencies in the chain have been evaluated successfully
214- # Now check if the final flag value matches the expected value in the property
165+ # Validate the condition shape before walking the chain. Test `is None`, not
166+ # truthiness: `False` is a valid expected value (the case this exists for).
215167 flag_key = property .get ("key" )
216168 expected_value = property .get ("value" )
217169 operator = property .get ("operator" , "exact" )
218170
219- if flag_key and expected_value is not None :
220- # Get the actual value of the flag we're checking
221- actual_value = evaluation_cache .get (flag_key )
171+ if operator != "flag_evaluates_to" :
172+ raise InconclusiveMatchError (
173+ f"Flag dependency property for '{ flag_key or 'unknown' } ' has invalid operator '{ operator } '"
174+ )
175+ if not flag_key or expected_value is None :
176+ raise InconclusiveMatchError (
177+ f"Flag dependency property for '{ flag_key or 'unknown' } ' is missing a key or value"
178+ )
179+
180+ # Evaluate and cache each flag in the chain; members already cached are
181+ # skipped. This does not decide the outcome — it only populates the cache.
182+ for dep_flag_key in dependency_chain :
183+ if dep_flag_key in evaluation_cache :
184+ continue
222185
223- if actual_value is None :
224- # Flag wasn't evaluated - this shouldn't happen if dependency chain is correct
186+ dep_flag = flags_by_key .get (dep_flag_key )
187+ if not dep_flag :
188+ # Missing flag dependency - cannot evaluate locally
189+ evaluation_cache [dep_flag_key ] = None
225190 raise InconclusiveMatchError (
226- f"Flag ' { flag_key } ' was not evaluated despite being in dependency chain "
191+ f"Cannot evaluate flag dependency ' { dep_flag_key } ' - flag not found in local flags "
227192 )
228193
229- # For flag dependencies, we need to compare the actual flag result with expected value
230- # using the flag_evaluates_to operator logic
231- if operator == "flag_evaluates_to" :
232- return matches_dependency_value (expected_value , actual_value )
233- else :
234- # This should never happen, but just to be defensive.
235- raise InconclusiveMatchError (
236- f"Flag dependency property for '{ property .get ('key' , 'unknown' )} ' has invalid operator '{ operator } '"
194+ # Check if the flag is active (same check as in client._compute_flag_locally)
195+ if not dep_flag .get ("active" ):
196+ evaluation_cache [dep_flag_key ] = False
197+ continue
198+
199+ # Recursively evaluate the dependency
200+ try :
201+ dep_flag_filters = dep_flag .get ("filters" ) or {}
202+ dep_aggregation_group_type_index = dep_flag_filters .get (
203+ "aggregation_group_type_index"
204+ )
205+ if dep_aggregation_group_type_index is not None :
206+ # Group flags should continue bucketing by the group key
207+ # from the current evaluation context.
208+ dep_bucketing_value = distinct_id
209+ else :
210+ dep_bucketing_value = resolve_bucketing_value (
211+ dep_flag , distinct_id , device_id
212+ )
213+ dep_result = match_feature_flag_properties (
214+ dep_flag ,
215+ distinct_id ,
216+ properties ,
217+ cohort_properties = cohort_properties ,
218+ flags_by_key = flags_by_key ,
219+ evaluation_cache = evaluation_cache ,
220+ device_id = device_id ,
221+ bucketing_value = dep_bucketing_value ,
237222 )
223+ evaluation_cache [dep_flag_key ] = dep_result
224+ except InconclusiveMatchError as e :
225+ # If we can't evaluate a dependency, store None and propagate the error
226+ evaluation_cache [dep_flag_key ] = None
227+ raise InconclusiveMatchError (
228+ f"Cannot evaluate flag dependency '{ dep_flag_key } ': { e } "
229+ ) from e
238230
239- # If no value check needed, return True (all dependencies passed)
240- return True
231+ # The condition matches iff the referenced flag's value matches the expected
232+ # value. None means inconclusive or not evaluated — distinct from a
233+ # definitive False, which must be allowed to match `expected_value=False`.
234+ actual_value = evaluation_cache .get (flag_key )
235+ if actual_value is None :
236+ raise InconclusiveMatchError (
237+ f"Flag dependency '{ flag_key } ' was inconclusive or not evaluated"
238+ )
239+ return matches_dependency_value (expected_value , actual_value )
241240
242241
243242def matches_dependency_value (expected_value , actual_value ):
0 commit comments