Cai
2026-09-01 9de34a52319e9c5432dfb3988e79a7d420304405
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from __future__ import annotations
 
import copy
import json
from pathlib import Path
from typing import Any
 
 
ALLOWED_TOP = {"normalization", "valuation", "analysis"}
PROTECTED_KEYS = {
    "price",
    "price_timestamp",
    "diluted_shares",
    "shares_date",
    "platform_market_cap",
    "revenue",
    "attributable_profit",
    "deduct_profit",
    "period_end",
    "publish_date",
    "source_id",
    "url",
    "raw_hash",
}
 
 
class JudgmentConflict(ValueError):
    pass
 
 
def _scan(value: Any, path: str = "") -> None:
    if isinstance(value, dict):
        for key, child in value.items():
            current = f"{path}.{key}" if path else key
            reference_only = current.startswith("normalization.adjustments[") and key == "source_id"
            if key in PROTECTED_KEYS and not reference_only:
                raise JudgmentConflict(f"E_JUDGMENT_CONFLICT:禁止字段 {current}")
            _scan(child, current)
    elif isinstance(value, list):
        for index, child in enumerate(value):
            _scan(child, f"{path}[{index}]")
 
 
def load_overlay(path: Path) -> dict[str, Any]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    unknown = set(payload) - ALLOWED_TOP
    if unknown:
        raise JudgmentConflict(f"E_JUDGMENT_CONFLICT:未知顶层字段 {sorted(unknown)}")
    _scan(payload)
    scenarios = ((payload.get("valuation") or {}).get("scenarios") or [])
    if not scenarios or sum(1 for item in scenarios if item.get("role") == "base") != 1:
        raise JudgmentConflict("judgment 必须包含且只包含一个 base 情景")
    return payload
 
 
def apply_overlay(data_snapshot: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
    snapshot = copy.deepcopy(data_snapshot)
    before = {
        key: copy.deepcopy(snapshot.get(key))
        for key in ("meta", "market", "financials", "balance_sheet", "institutions", "sources")
    }
    for key in ALLOWED_TOP:
        if key in overlay:
            snapshot[key] = copy.deepcopy(overlay[key])
    known_sources = {item["id"] for item in data_snapshot.get("sources", [])}
    for adjustment in snapshot.get("normalization", {}).get("adjustments", []):
        source_id = adjustment.get("source_id")
        if source_id not in known_sources:
            raise JudgmentConflict(f"E_JUDGMENT_CONFLICT:未知只读来源引用 {source_id}")
    after = {key: snapshot.get(key) for key in before}
    if before != after:
        raise JudgmentConflict("E_JUDGMENT_CONFLICT:保护数据被覆盖")
    return snapshot