diff --git a/backend/app/engine/governance.py b/backend/app/engine/governance.py index 80428b49..708f0e46 100644 --- a/backend/app/engine/governance.py +++ b/backend/app/engine/governance.py @@ -47,9 +47,9 @@ def lint_for_publish(spec: GraphSpec, *, level: str, checkers: Mapping[str, Any] strict = level == "governed" def flag(message: str, *, code: str, node_id: str | None = None, hard: bool = False, - field: str | None = None) -> None: + field: str | None = None, sql_level: str | None = None) -> None: result.add(message, level="error" if (hard and strict) else "warning", node_id=node_id, - field=field, code=code) + field=field, code=code, sql_level=sql_level) has_contract_output = False for node in spec.nodes: @@ -139,7 +139,9 @@ def _lint_sql(spec: GraphSpec, flag: Callable[..., None], checkers: Mapping[str, for node, check in graph_checks(spec.nodes, checkers): if check.level == "info": continue - flag(check.message, code=check.code, node_id=node.id, hard=check.level == "error", field="args.sql") + # sql_level:检查本来的级别。已发布档 level 一律 warning,界面靠它说清这是错误级、只是这一档不拦 + flag(check.message, code=check.code, node_id=node.id, hard=check.level == "error", field="args.sql", + sql_level=check.level) #: validate 里和门禁 G4、G5 说同一件事的两条提示(schema.evidence_issues) diff --git a/backend/app/engine/schema.py b/backend/app/engine/schema.py index e756edb4..43a28688 100644 --- a/backend/app/engine/schema.py +++ b/backend/app/engine/schema.py @@ -154,6 +154,10 @@ class ValidationIssue(BaseModel): code: str | None = None #: 这条问题对应的修复 id(engine/autofix.py 算出来的)。只在发布检查的结果里填 fix: str | None = None + #: 发布门禁里基于数据目录的 SQL 检查(governance._lint_sql):这条检查本来的级别(error / warning)。level 说的是 + #: 拦不拦(已发布档一律 warning),前端要按它说清「错误级问题,已发布只提醒、受管会拦」。别的问题没有这一项, + #: 序列化时也不出现 + sql_level: Literal["error", "warning"] | None = Field(default=None, exclude_if=lambda v: v is None) class ValidationResult(BaseModel): @@ -161,10 +165,11 @@ class ValidationResult(BaseModel): issues: list[ValidationIssue] = Field(default_factory=list) def add(self, message: str, *, level: str = "error", node_id: str | None = None, - edge_id: str | None = None, field: str | None = None, code: str | None = None) -> None: + edge_id: str | None = None, field: str | None = None, code: str | None = None, + sql_level: str | None = None) -> None: self.issues.append( ValidationIssue(level=level, message=message, node_id=node_id, # type: ignore[arg-type] - edge_id=edge_id, field=field, code=code) + edge_id=edge_id, field=field, code=code, sql_level=sql_level) # type: ignore[arg-type] ) if level == "error": self.ok = False diff --git a/backend/tests/test_publish_sqlcheck.py b/backend/tests/test_publish_sqlcheck.py index 4323af65..4bb72e73 100644 --- a/backend/tests/test_publish_sqlcheck.py +++ b/backend/tests/test_publish_sqlcheck.py @@ -84,8 +84,14 @@ async def test_governed_level_blocks_error_level_sql_problems(scenic_db): [issue] = [i for i in issues if i.code == "fanout_sum"] assert issue.message.startswith("「订单」关联「订单明细」是一对多") # 已发布级别:同一个问题只警告 - assert _sql_issues(publish_issues(spec, level="published", checkers=checkers)) == [ - ("fanout_sum", "warning", "fetch", "args.sql")] + published = publish_issues(spec, level="published", checkers=checkers) + assert _sql_issues(published) == [("fanout_sum", "warning", "fetch", "args.sql")] + # 拦不拦(level)之外,带着检查本来的级别(sql_level):界面据此说「错误级问题,已发布只提醒、受管会拦」 + assert [i.sql_level for i in issues if i.field == "args.sql"] == ["error"] + assert [i.sql_level for i in published if i.field == "args.sql"] == ["error"] + # 别的问题没有这一项,序列化时也不出现 + assert all("sql_level" not in i.model_dump() for i in published if i.field != "args.sql") + assert [i.model_dump()["sql_level"] for i in published if i.field == "args.sql"] == ["error"] # 依据只是推断:info 不进门禁 assert _sql_issues(lint_for_publish(spec, level="governed", checkers=await _checkers(scenic_db, "proposed")) .issues) == [] @@ -153,6 +159,10 @@ async def test_publish_endpoints_block_governed_fanout(client, scenic_source): assert checked["ok"] is False [issue] = [i for i in checked["issues"] if i["code"] == "fanout_sum"] assert issue["level"] == "error" and issue["node_id"] == "fetch" and issue["field"] == "args.sql" + assert issue["sql_level"] == "error" + hint = (await client.post(f"/api/workflows/{wid}/publish-check", json={"level": "published"})).json() + assert [(i["level"], i["sql_level"]) for i in hint["issues"] if i["code"] == "fanout_sum"] == [("warning", "error")] + assert all("sql_level" not in i for i in hint["issues"] if i.get("field") != "args.sql") assert issue["fix"] is None, "SQL 写错没有确定性的修复,交给人或助手改" fixed = (await client.post(f"/api/workflows/{wid}/autofix", json={"level": "governed", "apply": []})).json() diff --git a/frontend/src/canvas/PublishDialog.tsx b/frontend/src/canvas/PublishDialog.tsx index 087c6fd0..10b5a867 100644 --- a/frontend/src/canvas/PublishDialog.tsx +++ b/frontend/src/canvas/PublishDialog.tsx @@ -445,13 +445,15 @@ export function usePreflight(workflowId: string | undefined, level: PublishLevel } /** - * 发布前检查里一条 SQL 检查问题原本的级别。门禁只收错误和提醒两级(提示的依据只是推断,不进门禁),问题的 level 换成了 - * 「拦不拦」:受管档错误级拦(error),其余都是 warning——原来是哪一级看不出来了。错误级只有两条规则 - * (服务端 sqlcheck._ERROR_CODES:一对多关联后重复计算、存量跨期加总),按规则编号认回来 + * 发布前检查里一条 SQL 检查问题本来的级别。问题的 level 说的是「拦不拦」(已发布档一律 warning),本来是错误还是提醒 + * 看服务端给的 sql_level(governance._lint_sql)。老服务端不给时按受管档的 level 认;已发布档就认不出来,按提醒说, + * 不把拿不准的说重 */ -const SQL_ERROR_CODES = new Set(['fanout_sum', 'stock_summed']) -const publishSqlLevel = (code: unknown): 'error' | 'warning' | null => - (isSqlCheckCode(code) ? (SQL_ERROR_CODES.has(code) ? 'error' : 'warning') : null) +const publishSqlLevel = (issue: ValidationIssue): 'error' | 'warning' | null => { + if (!isSqlCheckCode(issue.code)) return null + if (issue.sql_level === 'error' || issue.sql_level === 'warning') return issue.sql_level + return issue.level === 'error' ? 'error' : 'warning' +} function fixLabelOf(data: PublishCheck | undefined, id: string): string { return data?.fixes.find((f) => f.id === id)?.label ?? '' @@ -478,7 +480,7 @@ export function Preflight({ pf, gate, onLocate, stale }: { const errors = (rows ?? []).filter((r) => r.issue.level === 'error').length const warns = (rows ?? []).length - errors // 已发布档只提醒、不拦的 SQL 检查错误级问题:结论那句话里点出来,别和「没指定模型」这类提示混成一个数 - const sqlErrors = (rows ?? []).filter((r) => r.issue.level !== 'error' && publishSqlLevel(r.issue.code) === 'error').length + const sqlErrors = (rows ?? []).filter((r) => r.issue.level !== 'error' && publishSqlLevel(r.issue) === 'error').length const canFix = !lock && check.status !== 'unsupported' const autoIds = issues ? autoFixIds(issues, fixes) : [] const labelOf = (id?: string | null) => (id ? nodeOf(id)?.label || id : '') @@ -564,7 +566,7 @@ function PreflightRow({ issue, others, fix, where, pf, onLocate }: { }) { const err = issue.level === 'error' const Icon = err ? XCircle : AlertTriangle - const sqlLevel = publishSqlLevel(issue.code) + const sqlLevel = publishSqlLevel(issue) return (