From 1cd9cab7b4faae144323862cb684a9cee667d42a Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Tue, 3 Nov 2026 23:08:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E4=B9=89=E8=AF=8D=E9=9A=8F=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E4=B8=80=E8=B5=B7=E7=95=99=E5=9C=A8=E5=8F=A3=E5=BE=84?= =?UTF-8?q?=E5=8D=A1=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 文档里叫「月活」,卡上指标叫「月度活跃买家」;同义词丢了,以后按名字找这份口径就永远对不上。 合并时带上(大小写与空白不算新说法),并让口径描述把它交出去。 --- backend/app/ledger/descriptor.py | 2 +- backend/app/ledger/docdraft.py | 17 ++++++++++++++++- backend/tests/test_ledger_descriptor.py | 12 ++++++++++++ backend/tests/test_ledger_docdraft.py | 2 ++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/app/ledger/descriptor.py b/backend/app/ledger/descriptor.py index e6e4e48a..d4088085 100644 --- a/backend/app/ledger/descriptor.py +++ b/backend/app/ledger/descriptor.py @@ -29,7 +29,7 @@ from app.tools.datasource import QUERY_PREFIX #: 指标只写口径卡配置里真有的这几个键 METRIC_KEYS = ("id", "name", "unit", "decimals", "format", "kind", "definition", "params", "external", "rule", - "assessment") + "assessment", "synonyms") async def list_calibers(session: AsyncSession, *, workflow_id: str | None = None) -> list[dict[str, Any]]: diff --git a/backend/app/ledger/docdraft.py b/backend/app/ledger/docdraft.py index 8e688865..7d070dbd 100644 --- a/backend/app/ledger/docdraft.py +++ b/backend/app/ledger/docdraft.py @@ -536,7 +536,9 @@ def _find_card(card_metrics: list[dict], draft: Mapping[str, Any]) -> dict | Non def merge_definition(card_metrics: list[dict], draft_metrics: list[dict]) -> tuple[list[dict], list[str]]: """把草稿的 definition 并进同名 / 同义词的口径卡指标;不改 expression / id。 - 只并 definition:草稿不知道取数,改写式子或 id 会破坏已有口径的算法。匹配不上的进 warnings,由人决定新建还是忽略。 + 只并 definition(和卡上还没有的同义词):草稿不知道取数,改写式子或 id 会破坏已有口径的算法。匹配不上的进 + warnings,由人决定新建还是忽略。同义词要带下去——文档里的叫法就是以后按名字找这份口径时的另一种说法,丢了它, + 「月活」和「月度活跃买家」就永远对不上。 """ merged = copy.deepcopy(list(card_metrics)) warnings: list[str] = [] @@ -549,6 +551,19 @@ def merge_definition(card_metrics: list[dict], draft_metrics: list[dict]) -> tup if target is None: warnings.append(f"草稿指标「{name or '(未命名)'}」在口径卡上找不到同名或同义词指标,定义未合并") continue + synonyms = draft.get("synonyms") + if isinstance(synonyms, list): + kept = [s.strip() for s in synonyms if isinstance(s, str) and s.strip()] + if kept: + have = target.get("synonyms") + have = [s for s in have if isinstance(s, str)] if isinstance(have, list) else [] + # 大小写和空白不算新说法:卡上已有「GMV」时不再加一条「gmv」 + known = {s.strip().casefold() for s in have} + added = [s for s in kept if s.casefold() not in known] + for s in added: + known.add(s.casefold()) + if added: + target["synonyms"] = have + added definition = draft.get("definition") if not isinstance(definition, Mapping) or not definition: warnings.append(f"草稿指标「{name}」没有可合并的定义说明") diff --git a/backend/tests/test_ledger_descriptor.py b/backend/tests/test_ledger_descriptor.py index c0541a61..324d0921 100644 --- a/backend/tests/test_ledger_descriptor.py +++ b/backend/tests/test_ledger_descriptor.py @@ -131,3 +131,15 @@ async def test_an_external_only_card_is_not_promised_automatic_issuance(client): assert made["composable"] is True assert made["issuance"]["auto_issue"] is False assert any("没有取数节点" in r for r in made["issuance"]["reasons"]) + + +async def test_metric_synonyms_reach_the_descriptor(client): + """文档带来的同义词要出现在机器可读描述里:按名字找口径时,「月活」和指标名一样是入口。""" + graph = S.caliber_graph() + for node in graph["nodes"]: + if node["type"] == "metrics": + node["data"]["config"]["metrics"][0]["synonyms"] = ["月活", "MAU"] + wf = await S.publish(graph=graph) + [made] = await descriptors(client, wf) + [metric] = made["metrics"] + assert metric["synonyms"] == ["月活", "MAU"] diff --git a/backend/tests/test_ledger_docdraft.py b/backend/tests/test_ledger_docdraft.py index 4acb42df..8af2c0a9 100644 --- a/backend/tests/test_ledger_docdraft.py +++ b/backend/tests/test_ledger_docdraft.py @@ -177,6 +177,8 @@ def test_merge_definition_matches_by_synonym_and_keeps_expression(): assert card["id"] == "gmv" assert card["expression"] == "cell(nodes.q, 0, '金额')" assert card["definition"] == {"text": "新说明", "owner": "财务", "scope": "全渠道"} + # 文档里的叫法要留在卡上:以后按同义词找这份口径,「成交额」才对得上 + assert card["synonyms"] == ["GMV"] assert len(warnings) == 1 and "客单价" in warnings[0]