mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
数据目录阶段 4A 术语:服务端上界面的目录校验报错改用界面的叫法,表类型写明细表、度量类型写可累加、关联关系两端写本表字段和目标表,给模型看的说法不变
This commit is contained in:
+27
-13
@@ -157,13 +157,26 @@ def _codes_ok(value: Any) -> bool:
|
||||
and all(isinstance(k, str) and isinstance(v, str) and v.strip() for k, v in value.items()))
|
||||
|
||||
|
||||
#: 表类型、度量类型在界面上的叫法(和 frontend/src/lib/terms.ts 的 CATALOG_KIND_LABEL / CATALOG_MEASURE_LABEL 一致)。
|
||||
#: 校验报错会原样上界面(接口 422 的 detail),按它写:以前写「事实表」「流量」,界面上却是「明细表」「可累加」。
|
||||
#: 给模型看的渲染另用 KIND_LABEL / MEASURE_LABEL,保留「事实表」「流量,可跨期加总」——模型认得这套说法
|
||||
UI_KIND_LABEL = {"fact": "明细表", "dimension": "维度表", "snapshot": "快照表", "log": "日志表", "config": "配置表"}
|
||||
UI_MEASURE_LABEL = {"flow": "可累加", "stock": "存量", "ratio": "比率", "identifier": "标识", "status": "状态",
|
||||
"attribute": "属性"}
|
||||
|
||||
|
||||
def _one_of(labels: Iterable[str]) -> str:
|
||||
names = list(labels)
|
||||
return f"应为{'、'.join(names[:-1])}或{names[-1]}之一"
|
||||
|
||||
|
||||
#: 每个字段的值怎么查、不合规时怎么说
|
||||
_TABLE_VALUE_RULES: dict[str, tuple[Any, str]] = {
|
||||
"label": (_text_ok, "应为不超过 500 字的文字"),
|
||||
"description": (lambda v: _text_ok(v, _LONG_TEXT_MAX), "应为不超过 2000 字的文字"),
|
||||
"grain": (_text_ok, "应为不超过 500 字的文字"),
|
||||
"keys": (_str_list_ok, "应为列名的列表"),
|
||||
"kind": (lambda v: v in TABLE_KINDS, "应为事实表、维度表、快照表、日志表或配置表之一"),
|
||||
"kind": (lambda v: v in TABLE_KINDS, _one_of(UI_KIND_LABEL[k] for k in TABLE_KINDS)),
|
||||
"business_date": (_business_date_ok, "应写明日期列,规则和时区写成文字"),
|
||||
"valid_filter": (_text_ok, "应为不超过 500 字的条件"),
|
||||
"dedup": (_text_ok, "应为不超过 500 字的文字"),
|
||||
@@ -172,7 +185,7 @@ _COLUMN_VALUE_RULES: dict[str, tuple[Any, str]] = {
|
||||
"label": (_text_ok, "应为不超过 500 字的文字"),
|
||||
"meaning": (_text_ok, "应为不超过 500 字的文字"),
|
||||
"unit": (lambda v: _text_ok(v, 50), "应为不超过 50 字的文字"),
|
||||
"measure": (lambda v: v in COLUMN_MEASURES, "应为流量、存量、比率、标识、状态或属性之一"),
|
||||
"measure": (lambda v: v in COLUMN_MEASURES, _one_of(UI_MEASURE_LABEL[k] for k in COLUMN_MEASURES)),
|
||||
"codes": (_codes_ok, "应为码值到含义的对照,含义写成文字"),
|
||||
}
|
||||
|
||||
@@ -201,7 +214,7 @@ def _relation_problems(rel: Any, seen: set[str]) -> list[str]:
|
||||
if not isinstance(rel, dict):
|
||||
return ["关联关系应为对象"]
|
||||
rid = rel.get("id")
|
||||
# 带编号时后面空一格再接说明:「关联关系 r1a2 的被指向列…」
|
||||
# 带编号时后面空一格再接说明:「关联关系 r1a2 的目标字段…」。两端的叫法和界面一致:本表字段 → 目标表的目标字段
|
||||
where = f"关联关系 {rid} " if isinstance(rid, str) and rid else "关联关系"
|
||||
problems: list[str] = []
|
||||
if not (isinstance(rid, str) and rid.strip()) or "." in str(rid):
|
||||
@@ -214,13 +227,13 @@ def _relation_problems(rel: Any, seen: set[str]) -> list[str]:
|
||||
problems.append(f"{where}含有不认识的字段「{'、'.join(extra)}」")
|
||||
cols, to_cols = rel.get("columns"), rel.get("to_columns")
|
||||
if not _str_list_ok(cols):
|
||||
problems.append(f"{where}的本表列应为列名的列表")
|
||||
problems.append(f"{where}的本表字段应为列名的列表")
|
||||
if not _text_ok(rel.get("to_table"), 255):
|
||||
problems.append(f"{where}缺少被指向的表")
|
||||
problems.append(f"{where}缺少目标表")
|
||||
if not _str_list_ok(to_cols):
|
||||
problems.append(f"{where}的被指向列应为列名的列表")
|
||||
problems.append(f"{where}的目标字段应为列名的列表")
|
||||
elif _str_list_ok(cols) and len(cols) != len(to_cols):
|
||||
problems.append(f"{where}两端的列数不一致")
|
||||
problems.append(f"{where}两端的字段数不一致")
|
||||
if rel.get("cardinality") not in (*CARDINALITIES, None):
|
||||
problems.append(f"{where}的基数「{rel.get('cardinality')}」不在可选值内")
|
||||
coverage = rel.get("coverage")
|
||||
@@ -1921,10 +1934,11 @@ __all__ = [
|
||||
"DraftReport", "INFERRED_MARK", "ITEM_SOURCES", "ITEM_STATUSES", "JoinEdge", "KIND_LABEL", "MEASURE_LABEL",
|
||||
"MergeStats", "NEW_RELATION_PATH", "PATCH_MAX_CHANGES", "PatchChange", "PatchPlan", "QUERY_SNAPSHOT_KIND",
|
||||
"REVIEW_ACTIONS", "TABLE_FIELDS", "TABLE_FIELD_LABEL", "TABLE_KINDS", "TableDraft", "TableDraftResult",
|
||||
"apply_human_edit", "apply_patch", "describe_slot", "draft_catalog", "draft_structure", "draft_with_model",
|
||||
"fk_relations", "frozen_catalog", "infer_name_relations", "initial_status", "join_paths", "make_item",
|
||||
"merge_notes", "now_iso", "parse_path", "plan_patch", "query_snapshot_meta", "read_catalog", "read_entry",
|
||||
"relation_graph", "relation_id", "render_table_index", "render_table_notes", "resolve_draft_model",
|
||||
"resolve_table_name", "review_entry", "review_item", "same_notes", "save_human_edit", "save_patch",
|
||||
"status_counts", "system_notes_source", "table_usage", "validate_notes", "visible_notes", "write_entry",
|
||||
"UI_KIND_LABEL", "UI_MEASURE_LABEL", "apply_human_edit", "apply_patch", "describe_slot", "draft_catalog",
|
||||
"draft_structure", "draft_with_model", "fk_relations", "frozen_catalog", "infer_name_relations",
|
||||
"initial_status", "join_paths", "make_item", "merge_notes", "now_iso", "parse_path", "plan_patch",
|
||||
"query_snapshot_meta", "read_catalog", "read_entry", "relation_graph", "relation_id", "render_table_index",
|
||||
"render_table_notes", "resolve_draft_model", "resolve_table_name", "review_entry", "review_item", "same_notes",
|
||||
"save_human_edit", "save_patch", "status_counts", "system_notes_source", "table_usage", "validate_notes",
|
||||
"visible_notes", "write_entry",
|
||||
]
|
||||
|
||||
@@ -77,8 +77,8 @@ async def world(scenic_db):
|
||||
created.append(wf.id)
|
||||
return wf
|
||||
|
||||
direct = await template("入园日报", [_graph(_query("q", src, "SELECT COUNT(*) FROM main.VISITS v WHERE v.status = 1",
|
||||
"查询入园人数"))], published=1)
|
||||
counted = _query("q", src, "SELECT COUNT(*) FROM main.VISITS v WHERE v.status = 1", "查询入园人数")
|
||||
direct = await template("入园日报", [_graph(counted)], published=1)
|
||||
agent = await template("入园分析", [_graph(_node("ask", "agent", "分析入园", prompt="分析",
|
||||
tools=[f"db_query__{src}", f"db_schema__{src}"]))],
|
||||
published=1, level="governed")
|
||||
|
||||
@@ -61,7 +61,8 @@ async def test_plan_patch_reports_before_and_after(scenic_db):
|
||||
assert (measure.path, measure.before, measure.before_status, measure.after) == (
|
||||
"columns.visitor_count.measure", "flow", "confirmed", "stock")
|
||||
assert measure.state == "change" and measure.reason == "在园人数是存量,不能跨天相加"
|
||||
assert (kind.path, kind.before, kind.before_status, kind.after, kind.state) == ("kind", None, None, "fact", "change")
|
||||
assert (kind.path, kind.before, kind.before_status, kind.after, kind.state) == (
|
||||
"kind", None, None, "fact", "change")
|
||||
|
||||
|
||||
async def test_codes_are_supplemented_not_replaced(scenic_db):
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""服务端上界面的数据目录说法和界面一致(阶段 4A 术语对齐)。
|
||||
|
||||
界面以 frontend/src/lib/terms.ts 的 CATALOG_TERMS 一组为准:表类型的 fact 写「明细表」,度量类型的 flow 写「可累加」,
|
||||
关联关系两端写「本表字段 → 目标表的目标字段」。校验报错(CatalogInvalid,接口回 422 时原样上界面)以前写的是
|
||||
「事实表」「流量」「被指向的表」,和界面对不上。给模型看的提示词(render_table_notes、起草提示)照旧,不在这里管。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from app.data import catalog
|
||||
from app.data.catalog import make_item
|
||||
|
||||
TERMS = Path(__file__).resolve().parents[2] / "frontend" / "src" / "lib" / "terms.ts"
|
||||
|
||||
|
||||
def _labels(name: str) -> list[str]:
|
||||
"""terms.ts 里一张对照表的中文值,按书写顺序。"""
|
||||
text = TERMS.read_text(encoding="utf-8")
|
||||
block = re.search(rf"export const {name}\b[^=]*=\s*\{{(.*?)\n\}}", text, re.S)
|
||||
assert block, f"terms.ts 里没有 {name}"
|
||||
return re.findall(r":\s*'([^']+)'", block.group(1))
|
||||
|
||||
|
||||
def _choices(labels: list[str]) -> str:
|
||||
return f"应为{'、'.join(labels[:-1])}或{labels[-1]}之一"
|
||||
|
||||
|
||||
def test_kind_and_measure_hints_follow_the_ui_labels():
|
||||
kinds = _labels("CATALOG_KIND_LABEL")
|
||||
measures = _labels("CATALOG_MEASURE_LABEL")
|
||||
assert kinds[0] == "明细表" and measures[0] == "可累加"
|
||||
[kind] = catalog.validate_notes({"kind": make_item("event", "human")})
|
||||
assert kind == f"表的表类型{_choices(kinds)}"
|
||||
[measure] = catalog.validate_notes({"columns": {"amount": {"measure": make_item("sum", "human")}}})
|
||||
assert measure == f"列 amount 的度量类型{_choices(measures)}"
|
||||
for problem in (kind, measure):
|
||||
assert "事实表" not in problem and "流量" not in problem
|
||||
|
||||
|
||||
def test_relation_problems_use_the_ui_words():
|
||||
rel = {"id": "r1", "columns": ["a", "b"], "to_table": "", "to_columns": ["id"], "cardinality": None,
|
||||
"coverage": None, "source": "human", "status": "confirmed"}
|
||||
problems = catalog.validate_notes({"relations": [rel]})
|
||||
assert "关联关系 r1 缺少目标表" in problems
|
||||
rel = {**rel, "to_table": "parks", "to_columns": "id"}
|
||||
assert "关联关系 r1 的目标字段应为列名的列表" in catalog.validate_notes({"relations": [rel]})
|
||||
rel = {**rel, "columns": "a", "to_columns": ["id"]}
|
||||
assert "关联关系 r1 的本表字段应为列名的列表" in catalog.validate_notes({"relations": [rel]})
|
||||
rel = {**rel, "columns": ["a", "b"]}
|
||||
assert "关联关系 r1 两端的字段数不一致" in catalog.validate_notes({"relations": [rel]})
|
||||
text = ";".join(catalog.validate_notes({"relations": [{**rel, "to_table": ""}]}))
|
||||
assert "被指向" not in text and "本表列" not in text
|
||||
|
||||
|
||||
async def test_api_422_uses_the_ui_labels():
|
||||
"""接口把校验报错原样交给界面:整份提交和提案保存都是这一句。"""
|
||||
from app.api.catalog import _invalid
|
||||
|
||||
try:
|
||||
catalog.apply_human_edit({}, {"kind": {"value": "event"}}, table="visits")
|
||||
except catalog.CatalogInvalid as e:
|
||||
detail = _invalid(e).detail
|
||||
assert "明细表" in detail and "事实表" not in detail
|
||||
# 给模型看的渲染照旧:模型认得「事实表」「流量,可跨期加总」
|
||||
text = catalog.render_table_notes({"kind": make_item("fact", "human"),
|
||||
"columns": {"n": {"measure": make_item("flow", "human")}}})
|
||||
assert "表类型:事实表" in text and "流量,可跨期加总" in text
|
||||
Reference in New Issue
Block a user