refactor(architecture): unify event facts and policy

This commit is contained in:
jxxghp
2026-08-27 09:56:27 +08:00
parent 86157be2a9
commit 1133557849
16 changed files with 4327 additions and 932 deletions
+124 -128
View File
@@ -9,7 +9,7 @@ import importlib.util
import json
import subprocess
import sys
from collections import defaultdict
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any, Optional
@@ -19,9 +19,9 @@ except ModuleNotFoundError:
from egress import collect_direct_egress
try:
from scripts.architecture.event_consumers import collect_event_consumers
from scripts.architecture.event_facts import collect_event_facts
except ModuleNotFoundError:
from event_consumers import collect_event_consumers
from event_facts import collect_event_facts
PROJECT_ROOT = Path(__file__).resolve().parents[2]
APP_ROOT = PROJECT_ROOT / "app"
@@ -701,17 +701,6 @@ def collect_run_module_diagnostics() -> dict[str, Any]:
}
def _event_reference(node: ast.AST) -> str | None:
"""从 AST 节点解析 EventType/ChainEventType 的静态成员引用。"""
if (
isinstance(node, ast.Attribute)
and isinstance(node.value, ast.Name)
and node.value.id in {"EventType", "ChainEventType"}
):
return f"{node.value.id}.{node.attr}"
return None
def _event_enum_members(enum_name: str) -> tuple[str, ...]:
"""从 schema 源码读取事件枚举成员,避免基线脚本导入宿主运行时。"""
tree = parse_source(APP_ROOT / "schemas" / "types.py")
@@ -738,120 +727,91 @@ def _event_enum_members(enum_name: str) -> tuple[str, ...]:
)
def _collect_event_locations() -> tuple[
list[str],
dict[str, list[dict[str, Any]]],
dict[str, list[dict[str, Any]]],
list[dict[str, Any]],
list[dict[str, Any]],
]:
"""扫描事件枚举及其生产、消费位置,供语义和诊断视图复用。"""
def collect_current_event_facts() -> dict[str, list[dict[str, Any]]]:
"""收集排除插件副本的当前宿主 Event producer/consumer 事实。"""
event_members = _event_enum_members("EventType")
chain_event_members = _event_enum_members("ChainEventType")
modules = discover_modules()
producers: dict[str, list[dict[str, Any]]] = defaultdict(list)
dynamic_producers: list[dict[str, Any]] = []
consumers, dynamic_consumers = collect_event_consumers(
modules,
return collect_event_facts(
discover_modules(),
{
"EventType": event_members,
"ChainEventType": chain_event_members,
},
)
for module_name, path in modules.items():
tree = parse_source(path)
for node in ast.walk(tree):
if not isinstance(node, ast.Call) or not isinstance(
node.func,
ast.Attribute,
):
continue
location = {"caller": module_name, "line": node.lineno}
if node.func.attr in {"send_event", "async_send_event"}:
reference = _event_reference(node.args[0]) if node.args else None
if reference:
producers[reference].append(location)
else:
dynamic_producers.append(location)
enum_names = [
*(f"EventType.{member}" for member in event_members),
*(f"ChainEventType.{member}" for member in chain_event_members),
]
return (
enum_names,
producers,
consumers,
dynamic_producers,
dynamic_consumers,
def _line_free_event_fact(fact: dict[str, Any]) -> dict[str, Any]:
"""移除只用于诊断的源码行号,保留完整稳定事件身份。"""
return {key: value for key, value in fact.items() if key != "line"}
def collect_event_fact_contract() -> dict[str, Any]:
"""生成逐调用事实与按 fingerprint 索引的宿主事件契约。"""
event_names = sorted([
*(
f"EventType.{member}"
for member in _event_enum_members("EventType")
),
*(
f"ChainEventType.{member}"
for member in _event_enum_members("ChainEventType")
),
])
current = collect_current_event_facts()
producers = sorted(
(_line_free_event_fact(fact) for fact in current["producers"]),
key=lambda fact: (fact["caller"], fact["qualname"], fact["fingerprint"]),
)
def collect_event_contracts() -> dict[str, Any]:
"""收集不受源码行号变化影响的宿主事件语义契约。"""
(
enum_names,
producers,
consumers,
dynamic_producers,
dynamic_consumers,
) = _collect_event_locations()
contracts = {
name: {
"producers": _aggregate_locations(
producers.get(name, []),
("caller",),
consumers = sorted(
(_line_free_event_fact(fact) for fact in current["consumers"]),
key=lambda fact: (fact["caller"], fact["qualname"], fact["fingerprint"]),
)
all_facts = (*producers, *consumers)
event_index = {
event_name: {
"producer_fingerprints": sorted(
fact["fingerprint"]
for fact in producers
if event_name in fact["events"]
),
"consumers": _aggregate_locations(
consumers.get(name, []),
("caller",),
"consumer_fingerprints": sorted(
fact["fingerprint"]
for fact in consumers
if event_name in fact["events"]
),
}
for name in sorted(enum_names)
for event_name in event_names
}
return {
"event_count": len(contracts),
"producer_count": sum(len(items) for items in producers.values()),
"consumer_count": sum(len(items) for items in consumers.values()),
"events": contracts,
"dynamic_producers": _aggregate_locations(dynamic_producers, ("caller",)),
"dynamic_consumers": _aggregate_locations(dynamic_consumers, ("caller",)),
"event_count": len(event_names),
"producer_call_count": len(producers),
"static_producer_call_count": sum(
not fact["dynamic"] and not fact["invalid"] for fact in producers
),
"dynamic_producer_count": sum(fact["dynamic"] for fact in producers),
"invalid_producer_count": sum(fact["invalid"] for fact in producers),
"producer_event_reference_count": sum(
len(fact["events"]) for fact in producers
),
"consumer_registration_count": len(consumers),
"static_consumer_count": sum(
not fact["dynamic"] and not fact["invalid"] for fact in consumers
),
"dynamic_consumer_count": sum(fact["dynamic"] for fact in consumers),
"invalid_consumer_count": sum(fact["invalid"] for fact in consumers),
"consumer_event_reference_count": sum(
len(fact["events"]) for fact in consumers
),
"fact_count": len(all_facts),
"producers": producers,
"consumers": consumers,
"event_index": event_index,
}
def collect_event_diagnostics() -> dict[str, Any]:
"""收集事件生产与消费的当前源码位置,仅用于人工诊断。"""
(
enum_names,
producers,
consumers,
dynamic_producers,
dynamic_consumers,
) = _collect_event_locations()
return {
"events": {
name: {
"producers": sorted(
producers.get(name, []),
key=lambda item: (item["caller"], item["line"]),
),
"consumers": sorted(
consumers.get(name, []),
key=lambda item: (item["caller"], item["line"]),
),
}
for name in sorted(enum_names)
},
"dynamic_producers": sorted(
dynamic_producers,
key=lambda item: (item["caller"], item["line"]),
),
"dynamic_consumers": sorted(
dynamic_consumers,
key=lambda item: (item["caller"], item["line"]),
),
}
def collect_event_fact_diagnostics() -> dict[str, list[dict[str, Any]]]:
"""返回带源码行号的逐调用事件事实,仅用于人工诊断。"""
return collect_current_event_facts()
def _sdk_all_names(tree: ast.Module, path: Path) -> tuple[str, ...]:
@@ -1021,10 +981,15 @@ def collect_compat_manifest() -> dict[str, Any]:
def collect_runtime_baseline() -> dict[str, Any]:
"""生成模块调度、SDK 和兼容层公开契约基线。"""
return {
"schema_version": 2,
"schema_version": 3,
"scope": {
"repository": "MoviePilot",
"roots": ["app"],
"excluded": ["app/plugins"],
},
"run_module": collect_run_module_contracts(),
"module_method_specs": collect_module_method_specs(),
"events": collect_event_contracts(),
"event_facts": collect_event_fact_contract(),
"event_specs": collect_event_specs(),
"sdk_exports": collect_sdk_exports(),
"compat_manifest": collect_compat_manifest(),
@@ -1087,7 +1052,7 @@ def collect_runtime_diagnostics() -> dict[str, Any]:
"""生成带当前源码行号的运行契约诊断视图,不写入语义 fixture。"""
return {
"run_module": collect_run_module_diagnostics(),
"events": collect_event_diagnostics(),
"event_facts": collect_event_fact_diagnostics(),
}
@@ -1295,10 +1260,8 @@ def _display_path(path: Path) -> Path:
return path
def _migrate_runtime_baseline(value: dict[str, Any]) -> dict[str, Any]:
"""把包含源码行号的 v1 运行契约转换为稳定语义结构"""
if value.get("schema_version") != 1:
return value
def _migrate_runtime_v1_to_v2(value: dict[str, Any]) -> dict[str, Any]:
"""把包含源码行号的 v1 运行契约转换为 v2 聚合语义"""
run_module = value["run_module"]
events = value["events"]
return {
@@ -1349,6 +1312,37 @@ def _migrate_runtime_baseline(value: dict[str, Any]) -> dict[str, Any]:
}
def _migrate_runtime_v2_to_v3(value: dict[str, Any]) -> dict[str, Any]:
"""把 v2 聚合事件投影为显式待刷新的 v3 兼容视图。"""
return {
"schema_version": 3,
"scope": {
"repository": "MoviePilot",
"roots": ["app"],
"excluded": ["app/plugins"],
},
"run_module": value["run_module"],
"module_method_specs": value.get("module_method_specs", {}),
"event_facts": {
"migration_required": True,
"legacy_v2_projection": value["events"],
},
"event_specs": value.get("event_specs", {}),
"sdk_exports": value["sdk_exports"],
"compat_manifest": value["compat_manifest"],
}
def _migrate_runtime_baseline(value: dict[str, Any]) -> dict[str, Any]:
"""链式迁移旧运行契约,保证检查只报告语义变化而不崩溃。"""
migrated = value
if migrated.get("schema_version") == 1:
migrated = _migrate_runtime_v1_to_v2(migrated)
if migrated.get("schema_version") == 2:
migrated = _migrate_runtime_v2_to_v3(migrated)
return migrated
def _migrate_plugin_baseline(value: dict[str, Any]) -> dict[str, Any]:
"""把来源信息混排的 v2 插件基线转换为 scope/provenance 结构。"""
if value.get("schema_version") != 2:
@@ -1484,18 +1478,20 @@ def _compare_semantic_values(
)
return
if isinstance(expected, list) and isinstance(actual, list):
expected_items = {
json.dumps(item, ensure_ascii=False, sort_keys=True): item
expected_keys = [
json.dumps(item, ensure_ascii=False, sort_keys=True)
for item in expected
}
actual_items = {
json.dumps(item, ensure_ascii=False, sort_keys=True): item
]
actual_keys = [
json.dumps(item, ensure_ascii=False, sort_keys=True)
for item in actual
}
for key in sorted(expected_items.keys() - actual_items.keys()):
report["removed"].append({"path": path, "value": expected_items[key]})
for key in sorted(actual_items.keys() - expected_items.keys()):
report["added"].append({"path": path, "value": actual_items[key]})
]
expected_counts = Counter(expected_keys)
actual_counts = Counter(actual_keys)
for key in sorted((expected_counts - actual_counts).elements()):
report["removed"].append({"path": path, "value": json.loads(key)})
for key in sorted((actual_counts - expected_counts).elements()):
report["added"].append({"path": path, "value": json.loads(key)})
return
if expected != actual:
report["changed"].append(
@@ -1,7 +1,9 @@
"""静态收集可证明的宿主 EventManager consumer。"""
"""静态收集可证明的宿主 EventManager producer 与 consumer 事实"""
import ast
from collections import defaultdict
import hashlib
import json
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Literal, TypeAlias
@@ -9,6 +11,7 @@ from typing import Any, Literal, TypeAlias
_DEFAULT_IDENTITY = "<default>"
_DYNAMIC_IDENTITY = "<dynamic>"
_EVENT_MANAGER_METHODS = {"add_event_listener", "register"}
_PRODUCER_METHODS = {"async_send_event", "send_event"}
_COMPREHENSION_SCOPES = (
ast.ListComp,
ast.SetComp,
@@ -33,6 +36,8 @@ class _Symbol:
"manager_class",
"manager_factory",
"manager_instance",
"publisher_instance",
"injected_owner",
"type_checking",
]
value: str = ""
@@ -45,6 +50,20 @@ class _EventSelection:
events: tuple[str, ...]
kind: Literal["member", "enum", "list"]
dynamic: bool = False
invalid: bool = False
@dataclass(frozen=True, slots=True)
class _BoundEventMethod:
"""记录已证明 receiver 的 EventManager 或 EventPublisher 绑定方法。"""
method: Literal[
"add_event_listener",
"register",
"send_event",
"async_send_event",
]
receiver_kind: str
@dataclass(frozen=True, slots=True)
@@ -53,6 +72,7 @@ class _DecoratorFactory:
selection: _EventSelection
priority: str
receiver_kind: str
@dataclass(frozen=True, slots=True)
@@ -63,9 +83,12 @@ class _Registration:
selection: _EventSelection
handler: str
priority: str
receiver_kind: str
_ScopeValue: TypeAlias = _Symbol | _EventSelection | _DecoratorFactory | None
_ScopeValue: TypeAlias = (
_Symbol | _EventSelection | _DecoratorFactory | _BoundEventMethod | None
)
def _expression_name(node: ast.AST | None) -> str:
@@ -98,15 +121,47 @@ def _priority_identity(node: ast.AST | None) -> str:
return _DYNAMIC_IDENTITY
def _location_sort_key(item: dict[str, Any]) -> tuple[str, int, str]:
"""返回 consumer location 的稳定排序键"""
def fingerprint_event_fact(fact: Mapping[str, object]) -> str:
"""计算排除诊断行号与已有摘要后的字段敏感 SHA256"""
payload = {
key: value
for key, value in fact.items()
if key not in {"fingerprint", "line"}
}
encoded = json.dumps(
payload,
ensure_ascii=True,
separators=(",", ":"),
sort_keys=True,
).encode("utf-8")
return hashlib.sha256(encoded).hexdigest()
def _fact_sort_key(item: dict[str, Any]) -> tuple[str, str, int, str]:
"""返回逐调用 Event fact 的确定排序键。"""
return (
str(item["caller"]),
str(item["qualname"]),
int(item["line"]),
str(item["identity"]),
str(item["fingerprint"]),
)
def _annotation_names(node: ast.AST | None) -> set[str]:
"""提取类型注解中的有限点分名称。"""
if node is None:
return set()
if isinstance(node, ast.Name):
return {node.id}
if isinstance(node, ast.Attribute):
return {_expression_name(node)}
return {
name
for child in ast.iter_child_nodes(node)
for name in _annotation_names(child)
}
def _bound_names(target: ast.AST) -> set[str]:
"""返回赋值目标在当前 lexical scope 绑定的名称。"""
if isinstance(target, ast.Name):
@@ -236,8 +291,221 @@ def _function_local_names(
return local_names - global_names - nonlocal_names
class _EventConsumerCollector(ast.NodeVisitor):
"""以有限 lexical provenance 收集单个宿主模块的事件消费者。"""
def _event_port_symbol(
annotation: ast.AST | None,
canonical_aliases: Mapping[str, str] | None = None,
) -> _Symbol | None:
"""把明确的 EventPublisher/EventManager 注解转换为 receiver provenance。"""
annotation_names = _annotation_names(annotation)
leaf_names = {name.rsplit(".", 1)[-1] for name in annotation_names}
if any(name.endswith("EventPublisher") for name in leaf_names):
return _Symbol("publisher_instance", "injected_event_publisher")
if any(name.endswith("EventManagerPort") for name in leaf_names):
return _Symbol("manager_instance", "injected_event_manager")
for name in annotation_names:
head, *tail = name.split(".")
canonical = ".".join(
((canonical_aliases or {}).get(head, head), *tail)
)
if canonical == "app.runtime.events.EventManager":
return _Symbol("manager_instance", "injected_event_manager")
return None
def _function_parameter_symbols(
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda,
canonical_aliases: Mapping[str, str] | None = None,
) -> dict[str, _Symbol]:
"""收集函数参数上明确声明的 Event 发布端口。"""
arguments = (
*node.args.posonlyargs,
*node.args.args,
*node.args.kwonlyargs,
)
return {
argument.arg: symbol
for argument in arguments
if (
symbol := _event_port_symbol(
argument.annotation,
canonical_aliases,
)
) is not None
}
_InjectedFieldKey: TypeAlias = tuple[str, str]
def _module_import_aliases(tree: ast.Module) -> dict[str, str]:
"""收集解析类继承关系所需的模块级 import 别名。"""
aliases: dict[str, str] = {}
for node in tree.body:
if isinstance(node, ast.Import):
for item in node.names:
bound = item.asname or item.name.split(".", 1)[0]
aliases[bound] = item.name if item.asname else bound
elif isinstance(node, ast.ImportFrom) and not node.level and node.module:
for item in node.names:
if item.name != "*":
aliases[item.asname or item.name] = f"{node.module}.{item.name}"
return aliases
def _iter_classes(
statements: list[ast.stmt],
prefix: tuple[str, ...] = (),
) -> list[tuple[str, ast.ClassDef]]:
"""按 lexical qualname 返回语句中的类定义,不进入函数 scope。"""
classes: list[tuple[str, ast.ClassDef]] = []
for statement in statements:
if not isinstance(statement, ast.ClassDef):
continue
qualname = ".".join((*prefix, statement.name))
classes.append((qualname, statement))
classes.extend(_iter_classes(statement.body, (*prefix, statement.name)))
return classes
def _canonical_class_name(
node: ast.expr,
*,
module_name: str,
aliases: dict[str, str],
) -> str:
"""把有限 Name/Attribute 基类表达式还原成 canonical 类名。"""
name = _expression_name(node)
if not name:
return ""
head, *tail = name.split(".")
if head in aliases:
return ".".join((aliases[head], *tail))
return f"{module_name}.{name}"
def _discover_injected_event_fields(
trees: dict[str, ast.Module],
) -> dict[_InjectedFieldKey, dict[str, _Symbol]]:
"""按 owning class 发现构造注入字段,并沿已知继承关系传播。"""
classes = {
f"{module_name}.{qualname}": (module_name, qualname, node)
for module_name, tree in trees.items()
for qualname, node in _iter_classes(tree.body)
}
aliases = {
module_name: _module_import_aliases(tree)
for module_name, tree in trees.items()
}
fields: dict[str, dict[str, _Symbol]] = {
canonical: {} for canonical in classes
}
bases_by_class: dict[str, set[str]] = {
canonical: set() for canonical in classes
}
descendants_by_class: dict[str, set[str]] = {
canonical: set() for canonical in classes
}
for canonical, (module_name, _qualname, class_node) in classes.items():
for base in class_node.bases:
base_name = _canonical_class_name(
base,
module_name=module_name,
aliases=aliases[module_name],
)
if base_name in classes:
bases_by_class[canonical].add(base_name)
descendants_by_class[base_name].add(canonical)
for canonical, (module_name, qualname, class_node) in classes.items():
class_fields = fields[canonical]
for constructor in (
node
for node in class_node.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "__init__"
):
parameters = _function_parameter_symbols(
constructor,
aliases[module_name],
)
for candidate in constructor.body:
candidates = ast.walk(candidate) if not isinstance(
candidate,
(ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef),
) else ()
for assignment in candidates:
if isinstance(assignment, ast.Assign):
pairs = (
(target, assignment.value)
for target in assignment.targets
)
elif isinstance(assignment, ast.AnnAssign) and assignment.value:
pairs = ((assignment.target, assignment.value),)
else:
continue
for target, value in pairs:
if not (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
):
continue
symbol = (
parameters.get(value.id)
if isinstance(value, ast.Name)
else None
)
if (
symbol is None
and module_name == "app.chain"
and qualname == "ChainBase"
and target.attr == "eventmanager"
and isinstance(value, ast.Attribute)
and value.attr == "event_manager"
):
symbol = _Symbol(
"manager_instance",
"injected_event_manager",
)
if symbol is not None:
class_fields[target.attr] = symbol
direct_fields = {
canonical: dict(class_fields)
for canonical, class_fields in fields.items()
if class_fields
}
for owner, owner_fields in direct_fields.items():
def reachable(
starts: set[str],
relations: dict[str, set[str]],
) -> set[str]:
"""返回给定继承方向上的传递闭包。"""
pending = list(starts)
reached: set[str] = set()
while pending:
current = pending.pop()
if current in reached:
continue
reached.add(current)
pending.extend(relations[current])
return reached
descendants = reachable({owner}, descendants_by_class)
targets = set(descendants)
targets.update(reachable(descendants, bases_by_class))
for target in targets:
fields[target].update(owner_fields)
return {
(module_name, qualname): fields[canonical]
for canonical, (module_name, qualname, _node) in classes.items()
if fields[canonical]
}
class _EventFactCollector(ast.NodeVisitor):
"""以有限 lexical provenance 收集单个宿主模块的 Event 事实。"""
def __init__(
self,
@@ -246,18 +514,21 @@ class _EventConsumerCollector(ast.NodeVisitor):
*,
collect_facts: bool,
module_final_scope: dict[str, _ScopeValue] | None = None,
injected_fields: dict[_InjectedFieldKey, dict[str, _Symbol]] | None = None,
) -> None:
"""初始化模块、事件枚举、收集模式和最终模块绑定"""
"""初始化模块、事件枚举、收集模式及已证明的注入字段"""
self._module_name = module_name
self._event_members = event_members
self._collect_facts = collect_facts
self._module_final_scope = module_final_scope or {}
self._injected_fields = injected_fields or {}
self._scopes: list[dict[str, _ScopeValue]] = [{}]
self._scope_kinds = ["module"]
self._function_final_scopes: list[dict[str, _ScopeValue]] = []
self._qualnames: list[str] = []
self.static: dict[str, list[dict[str, Any]]] = defaultdict(list)
self.dynamic: list[dict[str, Any]] = []
self._class_qualnames: list[str] = []
self.producers: list[dict[str, Any]] = []
self.consumers: list[dict[str, Any]] = []
def module_scope(self) -> dict[str, _ScopeValue]:
"""返回按模块执行顺序收敛后的符号状态。"""
@@ -304,22 +575,70 @@ class _EventConsumerCollector(ast.NodeVisitor):
scope_kinds: list[str],
) -> dict[str, _ScopeValue]:
"""无事实副作用地计算一组语句执行后的最内层 scope。"""
discovery = _EventConsumerCollector(
discovery = _EventFactCollector(
self._module_name,
self._event_members,
collect_facts=False,
module_final_scope=self._module_final_scope,
injected_fields=self._injected_fields,
)
discovery._scopes = [dict(scope) for scope in scopes]
discovery._scope_kinds = list(scope_kinds)
discovery._qualnames = list(self._qualnames)
discovery._class_qualnames = list(self._class_qualnames)
for statement in statements:
discovery.visit(statement)
return dict(discovery._scopes[-1])
def _function_initial_scope(
self,
node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda,
) -> dict[str, _ScopeValue]:
"""构造函数调用期的局部符号,并保留明确的参数端口 provenance。"""
scope: dict[str, _ScopeValue] = {
name: None for name in _function_local_names(node)
}
canonical_aliases = {
name: "app.runtime.events.EventManager"
for name, value in self._module_final_scope.items()
if isinstance(value, _Symbol) and value.kind == "manager_class"
}
if (
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "__init__"
and self._class_qualnames
):
scope.update(_function_parameter_symbols(node, canonical_aliases))
positional = (*node.args.posonlyargs, *node.args.args)
if (
self._module_name == "app.runtime.events"
and self._qualnames == ["EventManager"]
and positional
and positional[0].arg == "self"
):
scope["self"] = _Symbol("publisher_instance", "event_manager_self")
elif (
self._class_qualnames
and positional
and not any(
_expression_name(decorator) in {"classmethod", "staticmethod"}
for decorator in getattr(node, "decorator_list", ())
)
and (
self._module_name,
self._class_qualnames[-1],
) in self._injected_fields
):
scope[positional[0].arg] = _Symbol(
"injected_owner",
self._class_qualnames[-1],
)
return scope
def _symbol_for_canonical(self, canonical: str) -> _ScopeValue:
"""把有限 canonical 路径转换为 collector symbol。"""
if canonical == "app.runtime.events.eventmanager":
return _Symbol("manager_instance")
return _Symbol("manager_instance", "canonical_singleton")
if canonical == "app.runtime.events.EventManager":
return _Symbol("manager_class")
if canonical == "typing.TYPE_CHECKING":
@@ -356,11 +675,29 @@ class _EventConsumerCollector(ast.NodeVisitor):
return self._lookup(node.id)
if isinstance(node, ast.Attribute):
parent = self._resolve(node.value)
if isinstance(parent, _Symbol) and parent.kind == "injected_owner":
fields = self._injected_fields.get(
(self._module_name, parent.value),
{},
)
if node.attr in fields:
return fields[node.attr]
if isinstance(parent, _Symbol) and parent.kind == "module":
return self._symbol_for_canonical(f"{parent.value}.{node.attr}")
if isinstance(parent, _Symbol) and parent.kind == "manager_class":
if node.attr == "get_existing_instance":
return _Symbol("manager_factory")
if isinstance(parent, _Symbol) and parent.kind in {
"manager_instance",
"publisher_instance",
}:
methods = (
_PRODUCER_METHODS | _EVENT_MANAGER_METHODS
if parent.kind == "manager_instance"
else _PRODUCER_METHODS
)
if node.attr in methods:
return _BoundEventMethod(node.attr, parent.value)
if isinstance(parent, _EventSelection) and parent.kind == "enum":
enum_name = parent.events[0].split(".", 1)[0] if parent.events else ""
if node.attr in self._event_members.get(enum_name, ()):
@@ -368,6 +705,7 @@ class _EventConsumerCollector(ast.NodeVisitor):
events=(f"{enum_name}.{node.attr}",),
kind="member",
)
return _EventSelection((), "member", invalid=True)
return None
if isinstance(node, ast.List):
return self._resolve_event_selection(
@@ -375,18 +713,30 @@ class _EventConsumerCollector(ast.NodeVisitor):
allow_enum=True,
allow_list=True,
)
if isinstance(node, ast.IfExp):
return self._resolve_event_selection(
node,
allow_enum=False,
allow_list=False,
)
if isinstance(node, ast.Call):
target = self._resolve(node.func)
if isinstance(target, _Symbol) and target.kind in {
"manager_class",
"manager_factory",
}:
return _Symbol("manager_instance")
receiver_kind = (
"constructed_manager"
if target.kind == "manager_class"
else "existing_manager"
)
return _Symbol("manager_instance", receiver_kind)
registration = self._registration(node)
if registration and registration.method == "register":
return _DecoratorFactory(
selection=registration.selection,
priority=registration.priority,
receiver_kind=registration.receiver_kind,
)
return None
@@ -413,6 +763,7 @@ class _EventConsumerCollector(ast.NodeVisitor):
events=tuple(sorted({event for item in selections for event in item.events})),
kind="list",
dynamic=any(item.dynamic for item in selections),
invalid=any(item.invalid for item in selections),
)
if isinstance(node, ast.IfExp):
branches = (
@@ -431,6 +782,7 @@ class _EventConsumerCollector(ast.NodeVisitor):
events=tuple(sorted({event for item in branches for event in item.events})),
kind="list" if allow_list else "member",
dynamic=any(item.dynamic for item in branches),
invalid=any(item.invalid for item in branches),
)
resolved = self._resolve(node)
if isinstance(resolved, _EventSelection):
@@ -468,14 +820,13 @@ class _EventConsumerCollector(ast.NodeVisitor):
def _registration(self, node: ast.Call) -> _Registration | None:
"""仅解析 receiver 已证明为 canonical EventManager 实例的注册。"""
if not isinstance(node.func, ast.Attribute):
return None
method = node.func.attr
if method not in _EVENT_MANAGER_METHODS:
return None
receiver = self._resolve(node.func.value)
if not isinstance(receiver, _Symbol) or receiver.kind != "manager_instance":
bound_method = self._resolve(node.func)
if not (
isinstance(bound_method, _BoundEventMethod)
and bound_method.method in _EVENT_MANAGER_METHODS
):
return None
method = bound_method.method
if method == "register":
arguments = self._bind_call_arguments(
node,
@@ -504,6 +855,34 @@ class _EventConsumerCollector(ast.NodeVisitor):
selection=selection,
handler=_handler_identity(handler_node),
priority=_priority_identity(arguments.get("priority")),
receiver_kind=bound_method.receiver_kind,
)
def _producer_call(
self,
node: ast.Call,
) -> tuple[_BoundEventMethod, _EventSelection] | None:
"""解析 receiver 与调用签名均可证明的 Event producer。"""
bound_method = self._resolve(node.func)
if not (
isinstance(bound_method, _BoundEventMethod)
and bound_method.method in _PRODUCER_METHODS
):
return None
arguments = self._bind_call_arguments(
node,
("etype", "data", "priority"),
frozenset({"etype"}),
)
if arguments is None:
return None
return (
bound_method,
self._resolve_event_selection(
arguments["etype"],
allow_enum=False,
allow_list=False,
),
)
def _decorator_factory_application(
@@ -523,39 +902,84 @@ class _EventConsumerCollector(ast.NodeVisitor):
return None
return factory, arguments["f"]
def _record(
def _base_fact(
self,
node: ast.AST,
selection: _EventSelection,
*,
method: str,
receiver_kind: str,
) -> dict[str, Any]:
"""构造带 line-free fingerprint 的逐调用基础事实。"""
fact = {
"caller": self._module_name,
"line": node.lineno,
"qualname": ".".join(self._qualnames) or "<module>",
"method": method,
"receiver_kind": receiver_kind,
"events": list(selection.events),
"dynamic": selection.dynamic,
"invalid": selection.invalid,
}
fact["fingerprint"] = fingerprint_event_fact(fact)
return fact
def _record_consumer(
self,
node: ast.AST,
selection: _EventSelection,
*,
method: str,
receiver_kind: str,
handler: str,
priority: str,
registration_kind: Literal["decorator", "listener"],
) -> None:
"""写入静态事件位置,并为未知余项写入一条 dynamic 位置"""
"""写入一条 handler、kind 和 priority 完整的 consumer 事实"""
if not self._collect_facts:
return
location = {
"caller": self._module_name,
"line": node.lineno,
fact = self._base_fact(
node,
selection,
method=method,
receiver_kind=receiver_kind,
)
fact.update({
"handler": handler,
"priority": priority,
"registration_kind": registration_kind,
"identity": f"{registration_kind}|{handler}|{priority}",
}
for event in selection.events:
self.static[event].append(dict(location))
if selection.dynamic:
self.dynamic.append(dict(location))
})
fact["fingerprint"] = fingerprint_event_fact(fact)
self.consumers.append(fact)
def _record_producer(
self,
node: ast.Call,
method: _BoundEventMethod,
selection: _EventSelection,
) -> None:
"""写入一条 receiver 已证明的 producer 事实。"""
if not self._collect_facts:
return
self.producers.append(
self._base_fact(
node,
selection,
method=method.method,
receiver_kind=method.receiver_kind,
)
)
def _record_decorator(self, decorator: ast.expr, handler: str) -> bool:
"""记录直接或简单赋值别名形式的 register decorator。"""
factory = self._resolve(decorator)
if not isinstance(factory, _DecoratorFactory):
return False
self._record(
self._record_consumer(
decorator,
factory.selection,
method="register",
receiver_kind=factory.receiver_kind,
handler=handler,
priority=factory.priority,
registration_kind="decorator",
@@ -837,7 +1261,7 @@ class _EventConsumerCollector(ast.NodeVisitor):
body_scopes = [
dict(self._module_final_scope),
*(dict(scope) for scope in self._function_final_scopes),
{name: None for name in _function_local_names(node)},
self._function_initial_scope(node),
]
body_kinds = [
"module",
@@ -867,12 +1291,14 @@ class _EventConsumerCollector(ast.NodeVisitor):
for expression in (*node.bases, *(keyword.value for keyword in node.keywords)):
self.visit(expression)
self._qualnames.append(node.name)
self._class_qualnames.append(".".join(self._qualnames))
self._scopes.append({})
self._scope_kinds.append("class")
for statement in node.body:
self.visit(statement)
self._scope_kinds.pop()
self._scopes.pop()
self._class_qualnames.pop()
self._qualnames.pop()
self._set(node.name, None)
@@ -890,7 +1316,7 @@ class _EventConsumerCollector(ast.NodeVisitor):
body_scopes = [
dict(self._module_final_scope),
*(dict(scope) for scope in self._function_final_scopes),
{name: None for name in _function_local_names(node)},
self._function_initial_scope(node),
]
body_kinds = [
"module",
@@ -911,73 +1337,86 @@ class _EventConsumerCollector(ast.NodeVisitor):
self._scope_kinds = saved_kinds
def visit_Call(self, node: ast.Call) -> None:
"""记录直接 listener 调用或立即应用的 register decorator。"""
"""记录 receiver 调用签名均可证明的 producer/consumer。"""
producer = self._producer_call(node)
if producer is not None:
method, selection = producer
self._record_producer(node, method, selection)
factory_application = self._decorator_factory_application(node)
if factory_application:
factory, handler_node = factory_application
self._record(
self._record_consumer(
node,
factory.selection,
method="register",
receiver_kind=factory.receiver_kind,
handler=_handler_identity(handler_node),
priority=factory.priority,
registration_kind="decorator",
)
else:
registration = self._registration(node)
if not registration or registration.method != "add_event_listener":
self.generic_visit(node)
return
self._record(
node,
registration.selection,
handler=registration.handler,
priority=registration.priority,
registration_kind="listener",
)
if registration and registration.method == "add_event_listener":
self._record_consumer(
node,
registration.selection,
method="add_event_listener",
receiver_kind=registration.receiver_kind,
handler=registration.handler,
priority=registration.priority,
registration_kind="listener",
)
self.generic_visit(node)
def collect_event_consumers(
def collect_event_facts(
modules: dict[str, Path],
event_members: dict[str, tuple[str, ...]],
) -> tuple[dict[str, list[dict[str, Any]]], list[dict[str, Any]]]:
) -> dict[str, list[dict[str, Any]]]:
"""
收集宿主 EventManager 静态和动态 consumer 位置
收集宿主 EventManager 逐调用 producer consumer 事实
只有可追溯到 ``app.runtime.events.eventmanager`` canonical
``EventManager`` 实例的注册才进入事实未知 receiver 直接忽略
只有可追溯到 canonical EventManager其内部 ``self`` 或明确注入事件端口的
调用才进入事实插件模块与未知同名 receiver 始终忽略每条事实携带排除行号
的字段敏感 SHA256供基线稳定追踪
:param modules: 宿主模块名到 Python 源码路径的映射
:param event_members: EventType/ChainEventType 到公开成员名的映射
:return: 静态事件位置映射与事件值未知的位置列表
:return: ``producers`` ``consumers`` 两组稳定排序的逐调用事实
"""
static: dict[str, list[dict[str, Any]]] = defaultdict(list)
dynamic: list[dict[str, Any]] = []
for module_name, path in sorted(modules.items()):
if module_name == "app.plugins" or module_name.startswith("app.plugins."):
continue
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
discovery = _EventConsumerCollector(
trees = {
module_name: ast.parse(
path.read_text(encoding="utf-8-sig"),
filename=str(path),
)
for module_name, path in sorted(modules.items())
if module_name != "app.plugins"
and not module_name.startswith("app.plugins.")
}
injected_fields = _discover_injected_event_fields(trees)
producers: list[dict[str, Any]] = []
consumers: list[dict[str, Any]] = []
for module_name, tree in sorted(trees.items()):
discovery = _EventFactCollector(
module_name,
event_members,
collect_facts=False,
injected_fields=injected_fields,
)
discovery.visit(tree)
collector = _EventConsumerCollector(
collector = _EventFactCollector(
module_name,
event_members,
collect_facts=True,
module_final_scope=discovery.module_scope(),
injected_fields=injected_fields,
)
collector.visit(tree)
for event, locations in collector.static.items():
static[event].extend(locations)
dynamic.extend(collector.dynamic)
producers.extend(collector.producers)
consumers.extend(collector.consumers)
return (
{
event: sorted(locations, key=_location_sort_key)
for event, locations in sorted(static.items())
},
sorted(dynamic, key=_location_sort_key),
)
return {
"producers": sorted(producers, key=_fact_sort_key),
"consumers": sorted(consumers, key=_fact_sort_key),
}
+563
View File
@@ -0,0 +1,563 @@
#!/usr/bin/env python3
"""校验人工审查的宿主 Event consumer 精确政策。"""
import argparse
import json
import re
import sys
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any
try:
from scripts.architecture.event_facts import fingerprint_event_fact
except ModuleNotFoundError:
from event_facts import fingerprint_event_fact
PROJECT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_EVENT_POLICY_PATH = (
PROJECT_ROOT
/ "tests"
/ "fixtures"
/ "architecture"
/ "runtime-contract-policy.json"
)
EVENT_CONSUMER_POLICY_SCOPE = {
"root": "app",
"excluded": ["app.plugins"],
"receiver_contract": "canonical_event_manager_only",
}
EVENT_CONSUMER_FACT_FIELDS = (
"caller",
"qualname",
"method",
"receiver_kind",
"events",
"dynamic",
"invalid",
"handler",
"registration_kind",
"priority",
"fingerprint",
)
EVENT_CONSUMER_POLICY_FIELDS = (
*EVENT_CONSUMER_FACT_FIELDS,
"classification",
"owner",
"reason",
)
STATIC_CLASSIFICATION = "approved_static_registration"
DYNAMIC_CLASSIFICATION = "approved_dynamic_exception"
_FINGERPRINT_PATTERN = re.compile(r"[0-9a-f]{64}")
_WILDCARD_CHARACTERS = frozenset("*?[")
_PLACEHOLDER_REASONS = frozenset({"todo", "tbd"})
@dataclass(frozen=True, slots=True)
class EventPolicyViolation:
"""描述一条可稳定排序和断言的 Event policy 违规。"""
code: str
fingerprint: str | None
detail: str
@dataclass(frozen=True, slots=True)
class EventPolicyCheck:
"""汇总当前 consumer 事实、人工政策和全部校验结果。"""
actual_count: int
reviewed_count: int
static_count: int
dynamic_count: int
invalid_count: int
violations: tuple[EventPolicyViolation, ...]
@property
def ok(self) -> bool:
"""返回当前事实是否完整匹配人工政策。"""
return not self.violations
def _violation(
code: str,
detail: str,
fingerprint: object = None,
) -> EventPolicyViolation:
"""构造字段类型安全的违规对象。"""
return EventPolicyViolation(
code=code,
fingerprint=fingerprint if isinstance(fingerprint, str) else None,
detail=detail,
)
def _sort_violations(
violations: list[EventPolicyViolation],
) -> tuple[EventPolicyViolation, ...]:
"""按错误码、指纹和说明输出确定顺序。"""
return tuple(
sorted(
violations,
key=lambda item: (
item.code,
item.fingerprint or "",
item.detail,
),
)
)
def _has_wildcard(value: object) -> bool:
"""判断字符串或字符串列表是否包含 glob 通配符。"""
if isinstance(value, str):
return any(character in value for character in _WILDCARD_CHARACTERS)
if isinstance(value, list):
return any(_has_wildcard(item) for item in value)
return False
def _fact_projection(value: Mapping[str, object]) -> dict[str, object]:
"""提取 policy 与 collector 共用的完整 line-free consumer 字段。"""
return {
field: value[field]
for field in EVENT_CONSUMER_FACT_FIELDS
if field != "fingerprint" and field in value
}
def _validate_fact_shape(
value: object,
*,
source: str,
) -> list[EventPolicyViolation]:
"""校验 collector fact 或 policy entry 的公共语义字段。"""
if not isinstance(value, Mapping):
return [_violation("invalid_entry", f"{source} 必须是对象")]
violations: list[EventPolicyViolation] = []
fingerprint = value.get("fingerprint")
missing = sorted(set(EVENT_CONSUMER_FACT_FIELDS) - set(value))
if missing:
violations.append(
_violation(
"invalid_entry",
f"{source} 缺少字段:{', '.join(missing)}",
fingerprint,
)
)
return violations
string_fields = (
"caller",
"qualname",
"method",
"receiver_kind",
"handler",
"registration_kind",
"priority",
)
if any(
not isinstance(value.get(field), str) or not str(value[field]).strip()
for field in string_fields
):
violations.append(
_violation(
"invalid_entry",
f"{source} 的 consumer identity 字段必须是非空字符串",
fingerprint,
)
)
events = value.get("events")
if (
not isinstance(events, list)
or any(not isinstance(event, str) or not event for event in events)
or events != sorted(set(events))
):
violations.append(
_violation(
"invalid_entry",
f"{source}.events 必须是排序、去重的字符串列表",
fingerprint,
)
)
if not isinstance(value.get("dynamic"), bool) or not isinstance(
value.get("invalid"), bool
):
violations.append(
_violation(
"invalid_entry",
f"{source}.dynamic/invalid 必须是布尔值",
fingerprint,
)
)
if not isinstance(fingerprint, str) or not _FINGERPRINT_PATTERN.fullmatch(
fingerprint
):
violations.append(
_violation(
"invalid_entry",
f"{source}.fingerprint 必须是 64 位小写 SHA256",
fingerprint,
)
)
wildcard_fields = (
"caller",
"qualname",
"method",
"receiver_kind",
"events",
"handler",
"registration_kind",
"priority",
)
wildcard_names = [
field for field in wildcard_fields if _has_wildcard(value.get(field))
]
if wildcard_names:
violations.append(
_violation(
"wildcard_entry",
f"{source} 禁止通配字段:{', '.join(wildcard_names)}",
fingerprint,
)
)
caller = value.get("caller")
if isinstance(caller, str) and (
caller == "app.plugins" or caller.startswith("app.plugins.")
):
violations.append(
_violation(
"plugin_scope_violation",
f"{source} 不得包含插件副本:{caller}",
fingerprint,
)
)
if value.get("invalid") is True:
violations.append(
_violation(
"invalid_consumer",
f"{source} 的事件选择包含非法成员,不得获得政策准入",
fingerprint,
)
)
return violations
def _validate_policy_entry(
value: object,
*,
index: int,
) -> list[EventPolicyViolation]:
"""校验人工 entry 的精确字段、分类、owner 和理由。"""
source = f"policy.entries[{index}]"
violations = _validate_fact_shape(value, source=source)
if not isinstance(value, Mapping):
return violations
fingerprint = value.get("fingerprint")
if set(value) != set(EVENT_CONSUMER_POLICY_FIELDS):
violations.append(
_violation(
"invalid_entry",
f"{source} 必须精确包含规定字段",
fingerprint,
)
)
dynamic = value.get("dynamic")
expected_classification = (
DYNAMIC_CLASSIFICATION if dynamic is True else STATIC_CLASSIFICATION
)
if value.get("classification") != expected_classification:
violations.append(
_violation(
"classification_mismatch",
f"{source}.classification 应为 {expected_classification}",
fingerprint,
)
)
owner = value.get("owner")
if (
not isinstance(owner, str)
or not owner.strip()
or owner != value.get("caller")
):
violations.append(
_violation(
"owner_mismatch",
f"{source}.owner 必须精确等于 caller",
fingerprint,
)
)
elif _has_wildcard(owner):
violations.append(
_violation(
"wildcard_entry",
f"{source}.owner 禁止通配符",
fingerprint,
)
)
reason = value.get("reason")
if (
not isinstance(reason, str)
or not reason.strip()
or reason.strip().lower() in _PLACEHOLDER_REASONS
):
violations.append(
_violation(
"empty_reason",
f"{source}.reason 必须是非占位的具体理由",
fingerprint,
)
)
return violations
def _fingerprint_errors(
value: Mapping[str, object],
*,
source: str,
) -> list[EventPolicyViolation]:
"""使用统一 Event fact 摘要函数验证 line-free identity。"""
fingerprint = value.get("fingerprint")
if not isinstance(fingerprint, str):
return []
expected = fingerprint_event_fact(_fact_projection(value))
if fingerprint == expected:
return []
return [
_violation(
"fingerprint_mismatch",
f"{source}.fingerprint 与完整 consumer identity 不匹配",
fingerprint,
)
]
def _duplicate_fingerprint_errors(
values: Sequence[Mapping[str, object]],
*,
code: str,
source: str,
) -> list[EventPolicyViolation]:
"""拒绝会被集合比较吞掉的重复 consumer identity。"""
fingerprints: list[str] = [
fingerprint
for value in values
if isinstance((fingerprint := value.get("fingerprint")), str)
]
duplicates = sorted({item for item in fingerprints if fingerprints.count(item) > 1})
return [
_violation(code, f"{source} 存在重复 fingerprint", fingerprint)
for fingerprint in duplicates
]
def validate_event_consumer_policy(
policy: Mapping[str, object],
facts: Sequence[Mapping[str, object]],
) -> EventPolicyCheck:
"""按 exact fingerprint set 校验当前 consumer 事实与人工政策。"""
violations: list[EventPolicyViolation] = []
actual_facts = list(facts)
valid_actual: list[Mapping[str, object]] = []
for index, fact in enumerate(actual_facts):
fact_errors = _validate_fact_shape(fact, source=f"facts[{index}]")
violations.extend(fact_errors)
if not any(error.code == "invalid_entry" for error in fact_errors):
violations.extend(_fingerprint_errors(fact, source=f"facts[{index}]"))
valid_actual.append(fact)
if not isinstance(policy, Mapping) or set(policy) != {
"schema_version",
"scope",
"event_consumers",
}:
violations.append(
_violation("invalid_schema", "policy 顶层字段不符合 schema v1")
)
if isinstance(policy, Mapping) and policy.get("schema_version") != 1:
violations.append(
_violation("invalid_schema", "policy.schema_version 必须为 1")
)
if not isinstance(policy, Mapping) or policy.get("scope") != EVENT_CONSUMER_POLICY_SCOPE:
violations.append(
_violation("scope_mismatch", "policy.scope 必须精确匹配宿主扫描范围")
)
policy_section = policy.get("event_consumers") if isinstance(policy, Mapping) else None
if not isinstance(policy_section, Mapping) or set(policy_section) != {
"match_mode",
"entries",
}:
violations.append(
_violation("invalid_schema", "policy.event_consumers 字段不完整")
)
entries: list[object] = []
else:
if policy_section.get("match_mode") != "exact_fingerprint_set":
violations.append(
_violation(
"invalid_schema",
"policy.event_consumers.match_mode 必须为 exact_fingerprint_set",
)
)
raw_entries = policy_section.get("entries")
if not isinstance(raw_entries, list):
violations.append(
_violation("invalid_schema", "policy.event_consumers.entries 必须是列表")
)
entries = []
else:
entries = raw_entries
valid_entries: list[Mapping[str, object]] = []
for index, entry in enumerate(entries):
entry_errors = _validate_policy_entry(entry, index=index)
violations.extend(entry_errors)
if isinstance(entry, Mapping) and not any(
error.code == "invalid_entry" for error in entry_errors
):
violations.extend(
_fingerprint_errors(entry, source=f"policy.entries[{index}]")
)
valid_entries.append(entry)
violations.extend(
_duplicate_fingerprint_errors(
valid_actual,
code="duplicate_fact_fingerprint",
source="collector facts",
)
)
violations.extend(
_duplicate_fingerprint_errors(
valid_entries,
code="duplicate_policy_fingerprint",
source="policy entries",
)
)
actual_by_fingerprint = {
str(fact["fingerprint"]): fact
for fact in valid_actual
if isinstance(fact.get("fingerprint"), str)
}
policy_by_fingerprint = {
str(entry["fingerprint"]): entry
for entry in valid_entries
if isinstance(entry.get("fingerprint"), str)
}
for fingerprint in sorted(actual_by_fingerprint.keys() - policy_by_fingerprint.keys()):
violations.append(
_violation(
"unreviewed_fact",
"当前 consumer 事实未经过人工政策审查",
fingerprint,
)
)
for fingerprint in sorted(policy_by_fingerprint.keys() - actual_by_fingerprint.keys()):
violations.append(
_violation(
"stale_policy",
"人工政策引用的 consumer 事实已经消失或被替换",
fingerprint,
)
)
for fingerprint in sorted(actual_by_fingerprint.keys() & policy_by_fingerprint.keys()):
if _fact_projection(actual_by_fingerprint[fingerprint]) != _fact_projection(
policy_by_fingerprint[fingerprint]
):
violations.append(
_violation(
"fingerprint_mismatch",
"相同 fingerprint 对应的 consumer identity 不一致",
fingerprint,
)
)
static_count = sum(fact.get("dynamic") is False for fact in actual_facts)
dynamic_count = sum(fact.get("dynamic") is True for fact in actual_facts)
invalid_count = sum(fact.get("invalid") is True for fact in actual_facts)
return EventPolicyCheck(
actual_count=len(actual_facts),
reviewed_count=len(entries),
static_count=static_count,
dynamic_count=dynamic_count,
invalid_count=invalid_count,
violations=_sort_violations(violations),
)
def check_event_consumer_policy(
facts: Sequence[Mapping[str, object]],
policy_path: Path = DEFAULT_EVENT_POLICY_PATH,
) -> EventPolicyCheck:
"""读取人工 policy 文件并校验给定的当前 consumer facts。"""
try:
policy = json.loads(policy_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as error:
facts_list = list(facts)
return EventPolicyCheck(
actual_count=len(facts_list),
reviewed_count=0,
static_count=sum(fact.get("dynamic") is False for fact in facts_list),
dynamic_count=sum(fact.get("dynamic") is True for fact in facts_list),
invalid_count=sum(fact.get("invalid") is True for fact in facts_list),
violations=(
_violation(
"invalid_policy_file",
f"无法读取 Event consumer policy{error}",
),
),
)
if not isinstance(policy, Mapping):
policy = {}
return validate_event_consumer_policy(policy, facts)
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
"""解析只读 Event policy 检查参数。"""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--policy",
type=Path,
default=DEFAULT_EVENT_POLICY_PATH,
help="人工 Event consumer policy 路径",
)
return parser.parse_args(argv)
def main(argv: Sequence[str] | None = None) -> int:
"""收集当前事实并只读检查人工政策,不提供任何写入入口。"""
args = parse_args(argv)
try:
from scripts.architecture.baseline import collect_current_event_facts
except ImportError:
from baseline import collect_current_event_facts
current = collect_current_event_facts()
consumers: Any = current.get("consumers") if isinstance(current, Mapping) else None
if not isinstance(consumers, list):
print("当前 Event facts 缺少 consumers 列表", file=sys.stderr)
return 1
result = check_event_consumer_policy(consumers, args.policy)
if result.ok:
print(
"Event consumer policy 通过:"
f"{result.actual_count} 条事实,"
f"{result.static_count} 条静态注册,"
f"{result.dynamic_count} 条动态例外"
)
return 0
for violation in result.violations:
suffix = f" [{violation.fingerprint}]" if violation.fingerprint else ""
print(f"{violation.code}{suffix}: {violation.detail}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())