63 lines
2.1 KiB
Python
63 lines
2.1 KiB
Python
"""Pure constraint re-weight math (SP3). No engine/ctx imports — testable bare.
|
|
|
|
One multiply operator at three levels: across an option's matching matrix
|
|
cells (multi-tag), across source picks (multi-pick), and — by the caller —
|
|
across constraints. `exclude` (factor 0) is the absorbing element.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
EXCLUDE = object() # sentinel: option drops out of the pool
|
|
|
|
def _apply_rule(rule: dict[str, Any]):
|
|
mode = rule.get("mode")
|
|
if mode == "exclude":
|
|
return EXCLUDE
|
|
if mode in ("boost", "reduce"):
|
|
try:
|
|
return max(0.0, float(rule.get("factor", 1.0)))
|
|
except (TypeError, ValueError):
|
|
return 1.0
|
|
return 1.0 # allow / unknown -> no weight change
|
|
|
|
def combine_constraint_factor(source_picks, option, matrix, exceptions):
|
|
matrix = matrix or {}
|
|
exc_by_pair: dict[tuple[str, str], dict] = {}
|
|
for e in (exceptions or []):
|
|
if not isinstance(e, dict):
|
|
continue
|
|
s = e.get("source_value")
|
|
if s is None:
|
|
s = e.get("source")
|
|
t = e.get("target_value")
|
|
if t is None:
|
|
t = e.get("target")
|
|
if isinstance(s, str) and isinstance(t, str):
|
|
exc_by_pair[(s, t)] = e
|
|
opt_value = str(option.get("value", ""))
|
|
opt_tags = option.get("tags") or []
|
|
factor = 1.0
|
|
for pick in (source_picks or []):
|
|
p_value = str(pick.get("value", ""))
|
|
p_tags = pick.get("tags") or []
|
|
exc = exc_by_pair.get((p_value, opt_value))
|
|
if exc is not None:
|
|
r = _apply_rule(exc)
|
|
if r is EXCLUDE:
|
|
return EXCLUDE
|
|
factor *= r
|
|
continue
|
|
for s in p_tags:
|
|
row = matrix.get(s)
|
|
if not isinstance(row, dict):
|
|
continue
|
|
for t in opt_tags:
|
|
rule = row.get(t)
|
|
if isinstance(rule, dict):
|
|
r = _apply_rule(rule)
|
|
if r is EXCLUDE:
|
|
return EXCLUDE
|
|
factor *= r
|
|
return factor
|