124 lines
6.6 KiB
Python
124 lines
6.6 KiB
Python
"""Allowlisted deterministic business executors, never dynamic imports/eval."""
|
|
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
|
|
EXECUTORS = {"quality", "ratio", "overdue", "compare", "required", "duplicate", "model"}
|
|
|
|
|
|
def number(value):
|
|
if value is None or isinstance(value, bool):
|
|
raise ValueError("缺少有效数值")
|
|
try:
|
|
result = Decimal(str(value))
|
|
except (InvalidOperation, ValueError, TypeError):
|
|
raise ValueError("金额必须是十进制数") from None
|
|
if not result.is_finite() or abs(result) > Decimal("1e18"):
|
|
raise ValueError("数值超出支持范围")
|
|
return result
|
|
|
|
|
|
def ratio(numerator, denominator):
|
|
n, d = number(numerator), number(denominator)
|
|
if d <= 0:
|
|
raise ValueError("分母必须大于零")
|
|
return str((n / d).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP))
|
|
|
|
|
|
|
|
def applicability_checks(skill, values, sources):
|
|
"""One predicate implementation for deterministic and model Skills (BR-66)."""
|
|
fields = []
|
|
def matches(condition):
|
|
field = condition['field']; fields.append(field)
|
|
value = values.get(field)
|
|
if value is None:
|
|
return None
|
|
expected = condition.get('threshold', condition.get('value'))
|
|
op = condition['operator']
|
|
if op == 'eq' and isinstance(expected, bool):
|
|
return value == expected
|
|
try:
|
|
a, b = number(value), number(expected)
|
|
return {'eq': a == b, 'lt': a < b, 'lte': a <= b, 'gt': a > b, 'gte': a >= b}[op]
|
|
except (ValueError, KeyError):
|
|
return None
|
|
conditions = [matches(c) for c in skill.get('conditions', [])]
|
|
exceptions = [matches(c) for c in skill.get('exceptions', [])]
|
|
exception_field = skill.get('exception_field')
|
|
if exception_field:
|
|
fields.append(exception_field)
|
|
exceptions.append(values.get(exception_field) not in (None, '', False))
|
|
result = ('not_applicable' if False in conditions or True in exceptions else
|
|
'unknown' if None in conditions or None in exceptions else None)
|
|
if result is None:
|
|
return None
|
|
return [{'code': skill.get('code', skill['id'] + '.applicability'),
|
|
'title': skill.get('title', skill['name']) + '适用性', 'result': result,
|
|
'fields': fields, 'evidence_ids': [r['id'] for f in fields for r in sources.get(f, [])],
|
|
'basis_ids': [skill['id'] + '@' + skill['version']], 'severity': 'attention',
|
|
'suggested_action': '补充适用条件依据' if result == 'unknown' else '本次不适用,保留判定依据'}]
|
|
|
|
|
|
def execute_skill(skill, values, sources):
|
|
engine = skill.get("engine")
|
|
if engine not in EXECUTORS or engine == "model":
|
|
raise ValueError("不支持的确定性执行器")
|
|
gate = applicability_checks(skill, values, sources)
|
|
if gate is not None:
|
|
return {}, gate
|
|
checks, outputs = [], {}
|
|
fields = skill.get("inputs", [])
|
|
def check(code, title, result, refs, action="请核实材料与口径"):
|
|
checks.append({"code": code, "title": title, "result": result, "fields": refs, "suggested_action": action,
|
|
"evidence_ids": [r["id"] for f in refs for r in sources.get(f, [])],
|
|
"basis_ids": [skill["id"] + "@" + skill["version"]], "severity": "attention"})
|
|
if engine in ("quality", "required"):
|
|
for field in (list(values) if "*" in fields else fields):
|
|
check(skill.get("code", "missing." + field), skill.get("title", field + "完整性"),
|
|
"unknown" if values.get(field) is None else "pass", [field], "补充对应材料和来源依据")
|
|
elif engine == "compare":
|
|
a, b = skill["left"], skill["right"]
|
|
result = "unknown" if values.get(a) is None or values.get(b) is None else ("pass" if number(values[a]) == number(values[b]) else "issue")
|
|
refs = sources.get(a, []) + sources.get(b, [])
|
|
if not sources.get(a) or not sources.get(b) or any(not r.get(k) for r in refs for k in ("currency", "unit", "scope", "period")) or any(len({r.get(k) for r in refs}) != 1 for k in ("currency", "unit", "scope", "period")):
|
|
result = "unknown"
|
|
check(skill["code"], skill["name"], result, [a, b])
|
|
elif engine == "duplicate":
|
|
value = values.get(skill["field"])
|
|
check(skill["code"], skill["name"], "unknown" if value is None else ("issue" if number(value) > 0 else "pass"), [skill["field"]], "核对设备编号并提交去重清单")
|
|
elif engine == "ratio":
|
|
a, b = skill["numerator"], skill["denominator"]
|
|
refs = sources.get(a, []) + sources.get(b, [])
|
|
if not sources.get(a) or not sources.get(b) or any(not r.get(k) for r in refs for k in ("currency", "unit", "scope")):
|
|
raise ValueError("缺少币种、单位或口径依据,不能计算")
|
|
for dimension in ("currency", "unit", "scope"):
|
|
if len({r.get(dimension) for r in refs if r.get(dimension)}) > 1:
|
|
raise ValueError(dimension + "不一致,不能交叉计算")
|
|
if a.startswith("financial.") and b.startswith("financial.") and len({r.get("period") for r in refs}) > 1:
|
|
raise ValueError("期间不一致,不能交叉计算")
|
|
outputs[skill["output"]] = ratio(values.get(a), values.get(b))
|
|
else:
|
|
for field in fields:
|
|
check("input." + field, field + "输入检查", "unknown" if values.get(field) is None else "pass", [field])
|
|
return outputs, checks
|
|
|
|
|
|
def execute_rules(rule, values, sources):
|
|
checks = []
|
|
for index, condition in enumerate(rule.get("conditions", [])):
|
|
field = condition["field"]
|
|
applicable = condition.get("when", {})
|
|
if any(values.get(k) != v for k, v in applicable.items()):
|
|
result = "not_applicable"
|
|
else:
|
|
try:
|
|
value, threshold = number(values.get(field)), number(condition["threshold"])
|
|
hit = {"gt": value > threshold, "lt": value < threshold, "eq": value == threshold,
|
|
"gte": value >= threshold, "lte": value <= threshold}[condition["operator"]]
|
|
result = "issue" if hit else "pass"
|
|
except ValueError:
|
|
result = "unknown"
|
|
checks.append({"code": condition.get("code", rule["id"] + "." + str(index)), "title": condition.get("title", field),
|
|
"result": result, "fields": [field], "value": values.get(field), "evidence_ids": [r["id"] for r in sources.get(field, [])],
|
|
"basis_ids": [rule["id"] + "@" + rule["version"]], "suggested_action": condition.get("suggestion", "请核实"), "severity": "attention"})
|
|
return checks
|