The flagged option now keeps its weight and takes part in normal draws. It is still taken, without a draw, when constraints or weights leave nothing to pick. Flagging an option never changes a normal pick, and a fallback weighted 0 only ever stands in. Editors give it its normal share and show a fallback chip beside it.
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Wildcard fallback option (schema v9).
|
|
|
|
A wildcard may mark one non-null option ``fallback: true``. That option rolls
|
|
like any other (its weight, the node's filters + toggles and constraints all
|
|
apply), and is ALSO taken, without an rng draw, when nothing is left to pick:
|
|
every option, the fallback included, was excluded by a constraint (an Only
|
|
rule included) or weighted 0. Without a fallback such a wildcard binds an
|
|
empty string, which is what it did before v9 and still does. Flagging an
|
|
option never changes a normal draw. A fallback weighted 0 is only ever used
|
|
as the fallback.
|
|
|
|
Shared by the top-level wildcard handler and the nested ``@{uuid}`` resolver
|
|
so both surfaces agree. The node's filters + toggles decide whether the
|
|
fallback is available at all (``find_fallback`` runs after them).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
|
|
def is_fallback(option: Any) -> bool:
|
|
"""True for an option flagged as the wildcard's fallback. The null option
|
|
can't be one (an empty fallback is what having none already means)."""
|
|
return (
|
|
isinstance(option, dict)
|
|
and option.get("fallback") is True
|
|
and not option.get("is_null")
|
|
)
|
|
|
|
|
|
def find_fallback(options: list[dict[str, Any]]) -> dict[str, Any] | None:
|
|
"""The first flagged option in ``options`` (validation allows one), as it
|
|
was before weights + constraints. It stays in the draw pool."""
|
|
return next((o for o in options if is_fallback(o)), None)
|
|
|
|
|
|
def pool_is_dead(options: list[dict[str, Any]]) -> bool:
|
|
"""True when a weighted draw over ``options`` has nothing to pick."""
|
|
total = 0.0
|
|
for o in options:
|
|
try:
|
|
total += max(0.0, float(o.get("weight", 1)))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return total <= 0
|
|
|
|
|
|
def fallback_warning(
|
|
target_uuid: str, fallback: dict[str, Any], *, constrained: bool,
|
|
) -> dict[str, Any]:
|
|
"""The info-level ``fallback_used`` note WP Debug and the Test Runner show
|
|
in place of ``constraint_excludes_all_options``."""
|
|
why = "constraints excluded every option" if constrained else "no option was left to pick"
|
|
return {
|
|
"type": "fallback_used",
|
|
"severity": "info",
|
|
"module_id": target_uuid,
|
|
"source_field": "",
|
|
"position": 0,
|
|
"token_index": None,
|
|
"detail": {
|
|
"target_wildcard_id": target_uuid,
|
|
"option_id": fallback.get("id"),
|
|
"value": fallback.get("value", ""),
|
|
"reason": "constraints" if constrained else "empty_pool",
|
|
},
|
|
"message": f"{why}; used the fallback {fallback.get('value', '')!r}",
|
|
}
|