From b6f7cc837807342aea789bdcbce72d2bbe8dce4c Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 20 Oct 2026 10:39:53 +0800 Subject: [PATCH] =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=AE=9E=E4=BE=8B=E9=A1=B5?= =?UTF-8?q?=EF=BC=9A=E6=88=90=E6=9E=9C=E6=B2=A1=E9=85=8D=E5=87=BA=E5=85=B7?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E6=97=B6=EF=BC=88=E5=8D=B0=E7=AB=A0=E4=B8=8A?= =?UTF-8?q?=E6=98=AF=E3=80=8C=E6=9C=AA=E5=A3=B0=E6=98=8E=E5=87=BA=E5=85=B7?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E3=80=8D=EF=BC=89=EF=BC=8C=E6=9C=AC=E8=AF=A5?= =?UTF-8?q?=E9=99=8D=E6=A1=A3=E7=9A=84=E7=BC=BA=E5=8F=A3=E7=85=A7=E5=AE=9E?= =?UTF-8?q?=E5=86=99=E8=BF=9B=E5=8D=B0=E7=AB=A0=E7=9A=84=E8=AF=B4=E6=98=8E?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8F=A3=E5=BE=84=E5=8D=A1=E9=87=8C=E8=BE=93?= =?UTF-8?q?=E5=85=A5=E5=8F=96=E8=87=AA=E3=80=8C=E6=9F=A5=E8=AF=A2=E5=8C=BA?= =?UTF-8?q?=E9=97=B4=E9=87=8C=E8=BF=99=E5=BC=A0=E8=A1=A8=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E3=80=8D=E7=9A=84=E5=88=97=E7=9A=84=E6=8C=87?= =?UTF-8?q?=E6=A0=87=E3=80=81=E5=8F=8C=E7=9B=B2=E5=A4=8D=E7=AE=97=E7=9A=84?= =?UTF-8?q?=E7=BC=BA=E5=8F=A3=EF=BC=88=E6=97=A0=E6=B3=95=E5=A4=8D=E7=AE=97?= =?UTF-8?q?=E3=80=81=E5=AE=A1=E6=A0=B8=E4=BA=BA=E6=B2=A1=E9=87=87=E7=94=A8?= =?UTF-8?q?=E5=8E=9F=E6=B5=81=E7=A8=8B=E2=80=A6=E2=80=A6=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E9=85=8D=E4=BA=86=E5=A5=91=E7=BA=A6=E7=9A=84=E5=B7=B2=E7=BB=8F?= =?UTF-8?q?=E6=8C=89=E7=BC=BA=E5=8F=A3=E9=99=8D=E6=A1=A3=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=8F=A6=E5=86=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/apps/resolve.py | 24 +++++++++++++++++++++++- backend/tests/test_metric_no_data.py | 12 ++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/backend/app/apps/resolve.py b/backend/app/apps/resolve.py index f033259f..9174f987 100644 --- a/backend/app/apps/resolve.py +++ b/backend/app/apps/resolve.py @@ -75,6 +75,24 @@ STATUS_TEXT = {"queued": "排队中", "running": "运行中", "interrupted": " REJECTED_TEXT = "被驳回" TIER_TEXT = {"formal": "完整出具", "degraded": "降档出具", "withheld": "不予出具"} NO_TIER = "未声明出具契约" +#: 成果没配出具契约(印章上是「未声明出具契约」)时,本该降档的缺口照实写进 notes:不然实例页上看不出来 +UNGATED_NO_DATA = "指标{names}所依据的查询区间里表里没有数据,这些 0 不代表真的是 0(成果没有配出具契约,没有按缺口降档)" +UNGATED_DUAL = "双盲复算有缺口:{gaps}(成果没有配出具契约,没有按缺口降档)" + + +def ungated_notes(cards: list[dict[str, Any]], dual: dict[str, Any] | None) -> list[str]: + """没配出具契约的成果,印章上要照实写的缺口:口径卡里输入取自「查询区间里这张表没有数据」的列的指标(no_data), + 双盲复算的缺口(产出的 issuance_gaps:无法复算、审核人没采用原流程……)。配了契约的这些已经按缺口降档,不另写。""" + notes: list[str] = [] + names = list(dict.fromkeys(str(m.get("name") or m.get("id")) for card in cards for m in card.get("metrics") or [] + if isinstance(m, dict) and m.get("no_data"))) + if names: + shown = "".join(f"「{n}」" for n in names[:3]) + (f"等 {len(names)} 个" if len(names) > 3 else "") + notes.append(UNGATED_NO_DATA.format(names=shown)) + gaps = [str(g) for g in (dual or {}).get("issuance_gaps") or [] if g] + if gaps: + notes.append(UNGATED_DUAL.format(gaps=";".join(gaps[:2]) + ("等" if len(gaps) > 2 else ""))) + return notes #: 蓝图试运行(docs/design/normal-user-apps.md N4):印章的来源、说明,趋势只有一个点时的说明,预览里入口按钮为什么点不了 TRIAL_FROM = "试运行" TRIAL_NOTE = "数据是真的,但这不是正式结果:发布之后才会按设定出数" @@ -315,7 +333,8 @@ async def stamp_of(session: AsyncSession, source: Source) -> dict[str, Any]: if run.data_versions: parts.append("数据版本 " + "、".join(_version_text(v) for v in run.data_versions.values() if isinstance(v, dict))) calibers: list[dict[str, str]] = [] - for _, _, card in _sealed_cards(sealed): + cards = [card for _, _, card in _sealed_cards(sealed)] + for card in cards: item = {"name": str(card.get("caliber") or ""), "version": str(card.get("caliber_version") or "")} if item["name"] and item not in calibers: calibers.append(item) @@ -339,6 +358,9 @@ async def stamp_of(session: AsyncSession, source: Source) -> dict[str, Any]: # 个人应用最近一次刷新没成功、这里给的是上一次的(_exploratory_source) notes.append(source.message) notes += [gap for entry in (run.data_versions or {}).values() if (gap := _records_gap(entry))] + if tier not in TIER_TEXT: + # 没配出具契约:双盲复算的缺口、查询区间里没有数据的指标不会降档,照实写在这里 + notes += ungated_notes(cards, sealed.payload_of(sealed.nodes_of("dual_check"))) if not sealed.trusted: notes.append("这次运行还没有封存(或封存核对不一致),数字仅供参考") return { diff --git a/backend/tests/test_metric_no_data.py b/backend/tests/test_metric_no_data.py index 3f3ddd0c..b43dd375 100644 --- a/backend/tests/test_metric_no_data.py +++ b/backend/tests/test_metric_no_data.py @@ -183,3 +183,15 @@ async def test_the_report_prompt_carries_the_rule(plant, monkeypatch): await finished(graph) prompt = "\n".join(str(m.content) for m in seen[0]) assert NO_DATA_RULE in prompt and "这段时间表里没有数据" in prompt + + +def test_an_ungated_result_says_its_gaps_on_the_stamp(): + """成果没配出具契约时,实例页的印章照实写这些缺口(配了契约的已经按缺口降档)。""" + from app.apps.resolve import ungated_notes + + cards = [{"metrics": [{"id": "visitors", "name": "上周到访人数", "no_data": True}, {"id": "pv", "name": "客流PV"}]}] + dual = {"issuance_gaps": ["指标「销售额」无法复算:盲算已达每项最多查询次数(1 次)"]} + notes = ungated_notes(cards, dual) + assert notes[0].startswith("指标「上周到访人数」所依据的查询区间里表里没有数据") and "没有配出具契约" in notes[0] + assert notes[1].startswith("双盲复算有缺口:指标「销售额」无法复算") + assert ungated_notes([{"metrics": [{"id": "pv", "name": "客流PV"}]}], None) == []