From 38906a92ead2773d5a59966e75e68415f2b4235e Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 11:55:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E6=9E=90=E8=83=BD=E5=8A=9B=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=E8=A1=A5=E8=B7=A8=E5=8C=85=E7=BA=A6=E5=AE=9A=EF=BC=9A?= =?UTF-8?q?=E5=86=99=E4=BD=9C=E7=9B=AE=E5=BD=95=E9=87=8C=E5=88=86=E6=9E=90?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=E7=9A=84=E8=AF=BB=E6=B3=95=EF=BC=88catalog?= =?UTF-8?q?=5Fresult=EF=BC=9AA{n}=20=E6=9D=A1=E7=9B=AE=20kind=20analysis?= =?UTF-8?q?=E3=80=81result=20=E4=B8=BA=E6=95=B4=E4=BB=BD=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=EF=BC=89=E3=80=81=E5=8F=A3=E5=BE=84=E5=8D=A1=E6=8C=87=E6=A0=87?= =?UTF-8?q?=E4=B8=8A=E7=9A=84=E5=8F=AF=E6=AF=94=E6=80=A7=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=88MetricComparability=EF=BC=89=E3=80=81=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=96=87=E6=A1=A3=E9=87=8C=E7=B3=BB=E7=BB=9F=E5=86=99?= =?UTF-8?q?=E7=9A=84=E3=80=8C=E5=88=86=E6=9E=90=E8=8C=83=E5=9B=B4=E3=80=8D?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=88ScopeBlock=20/=20ScopeItem=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/analysis/contract.py | 53 ++++++++++++++++++++++++- backend/tests/test_analysis_contract.py | 8 ++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/backend/app/analysis/contract.py b/backend/app/analysis/contract.py index d90d9bdb..e70d4cf8 100644 --- a/backend/app/analysis/contract.py +++ b/backend/app/analysis/contract.py @@ -314,6 +314,54 @@ def ledger_entry(result: AnalysisResult, *, node_id: str, exec_no: int, artifact return entry +# -------------------------------------------------------------------------- +# 跨包的约定:写作目录里的分析条目、口径卡指标上的可比性、报告文档里系统写的「分析范围」 +# -------------------------------------------------------------------------- + + +def catalog_result(entry: Mapping[str, Any] | None) -> AnalysisResult | None: + """写作目录(engine/evidence.build_catalog)里一条分析条目的结论。约定:编号 A{n} 的条目 kind 为 LEDGER_KIND, + result 是从工件取回的整份结论(证据接入负责装上,说法核对、分析范围只经这个函数读)。""" + if not entry or entry.get("kind") != LEDGER_KIND: + return None + result = entry.get("result") + return result if isinstance(result, dict) else None + + +class MetricComparability(TypedDict, total=False): + """口径卡指标上的可比性(00-总方案 §3.4 第 3 条):指标的表达式用到两份同形、只差字面量的查询快照时,口径卡执行时 + 自动做一次可比性检查,记在指标条目的 comparability 字段。写作目录照实提醒,带契约的出口按 verdict 记缺口或标注。""" + + artifact: str # 那份可比性结论的工件 sha256 + verdict: str # comparable / conditional / incomparable + phrase: str + reasons: list[str] # fail、warn 的原因,一句一个 + + +class ScopeItem(TypedDict, total=False): + alias: str # 写作目录编号 A{n} + op: str + mode: str | None + dims: list[str] # 拆解的维度(给人看的叫法) + verdict: str | None + phrase: str + parent: str | None # 下钻:上一层的编号 + reason: str | None # 算不了的原因 + + +class ScopeBlock(TypedDict, total=False): + """报告文档的 analysis_scope 字段:系统写的「分析范围」(00-总方案 §3.6)。结构化数据随文档封存,不进正文、不过 + 裸数字和说法核对(不是模型写的);网页、导出、可验证报告各自按它渲染,编号点得开。""" + + phenomena: list[ScopeItem] # 可比性、基线、趋势 + decomposed: list[ScopeItem] # 拆过的(按下钻链的先后) + excluded: list[ScopeItem] # 判定为分散、差异不足以下结论的 + refused: list[ScopeItem] # 算不了的 + not_decomposed: list[str] | None # 候选维度里没拆过的叫法;None = 候选维度核对不了 + note: str | None # 核对不了时的一句说明 + tests: int # 这次做了几次差异检验 + + #: 复算核对时不比的字段:hypothesis 是 agent 写的话,不是算出来的 NOT_RECOMPUTED = frozenset({"hypothesis"}) @@ -327,6 +375,7 @@ def same(a: Mapping[str, Any], b: Mapping[str, Any]) -> bool: __all__ = [ "ARTIFACT_KIND", "CATALOG_PREFIX", "CAVEATS", "DEFAULTS", "LABELS", "LEDGER_KIND", "MARKER_KIND", "METHODS", "MODES", "NODE_TYPE", "NOT_RECOMPUTED", "OPS", "RANGES", "REASONS", "SETTINGS_KEY", "TOOL_NAMES", "VERDICTS", - "AnalysisInput", "AnalysisRequest", "AnalysisResult", "CallError", "Caveat", "Context", "InputSnapshot", "Probe", - "Prober", "ResultTable", "Verdict", "effective_settings", "ledger_entry", "same", "settings_problem", + "AnalysisInput", "AnalysisRequest", "AnalysisResult", "CallError", "Caveat", "Context", "InputSnapshot", + "MetricComparability", "Probe", "Prober", "ResultTable", "ScopeBlock", "ScopeItem", "Verdict", "catalog_result", + "effective_settings", "ledger_entry", "same", "settings_problem", ] diff --git a/backend/tests/test_analysis_contract.py b/backend/tests/test_analysis_contract.py index b9041aba..cc1ea743 100644 --- a/backend/tests/test_analysis_contract.py +++ b/backend/tests/test_analysis_contract.py @@ -49,3 +49,11 @@ def test_every_op_has_a_method_and_a_tool_name(): modes = c.MODES.get(op, (None,)) assert all((op, m) in c.METHODS for m in modes), op assert c.TOOL_NAMES[op] == f"analysis_{op}" + + +def test_catalog_entries_hand_over_the_whole_result_only_for_analysis(): + result = fake_result() + assert c.catalog_result({"kind": "analysis", "result": result}) is result + assert c.catalog_result({"kind": "query", "result": result}) is None + assert c.catalog_result({"kind": "analysis"}) is None + assert c.catalog_result(None) is None