diff --git a/frontend/src/canvas/PublishDialog.tsx b/frontend/src/canvas/PublishDialog.tsx index a83efba5..087c6fd0 100644 --- a/frontend/src/canvas/PublishDialog.tsx +++ b/frontend/src/canvas/PublishDialog.tsx @@ -12,8 +12,9 @@ import { import { useCatalog } from '../store/catalog' import { Modal, Spinner, toast, useRadioGroup } from '../components/ui' import { humanizeError } from '../lib/errors' -import { PUBLISH_FIX_TEXT as T, WORKFLOW_STATUS_HINT, WORKFLOW_STATUS_LABEL } from '../lib/terms' +import { CATALOG_UI_TEXT as UT, PUBLISH_FIX_TEXT as T, WORKFLOW_STATUS_HINT, WORKFLOW_STATUS_LABEL } from '../lib/terms' import { isSqlCheckCode, sqlRuleLabel } from '../lib/sqlcheck' +import { SqlLevelBadge } from '../run/SqlChecks' import { setLocalActor, useLocalActor } from '../lib/actor' import { autoFixIds, choiceReady, contentSig, fixFieldLabel, fixFor, fixValueLines, fixValueText, isMissingEndpoint, mergeIssues, @@ -443,6 +444,15 @@ export function usePreflight(workflowId: string | undefined, level: PublishLevel } } +/** + * 发布前检查里一条 SQL 检查问题原本的级别。门禁只收错误和提醒两级(提示的依据只是推断,不进门禁),问题的 level 换成了 + * 「拦不拦」:受管档错误级拦(error),其余都是 warning——原来是哪一级看不出来了。错误级只有两条规则 + * (服务端 sqlcheck._ERROR_CODES:一对多关联后重复计算、存量跨期加总),按规则编号认回来 + */ +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) + function fixLabelOf(data: PublishCheck | undefined, id: string): string { return data?.fixes.find((f) => f.id === id)?.label ?? '' } @@ -467,6 +477,8 @@ export function Preflight({ pf, gate, onLocate, stale }: { const rows = useMemo(() => (issues ? mergeIssues(issues) : null), [issues]) 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 canFix = !lock && check.status !== 'unsupported' const autoIds = issues ? autoFixIds(issues, fixes) : [] const labelOf = (id?: string | null) => (id ? nodeOf(id)?.label || id : '') @@ -507,6 +519,7 @@ export function Preflight({ pf, gate, onLocate, stale }: { {gate ? T.gateBlocked(errors) : errors ? T.blocked(errors) : T.passed} {warns > 0 && {errors || gate ? ',' : ';'}{T.warnings(warns)}} + {sqlErrors > 0 && {UT.publishSqlErrors(sqlErrors)}} )} @@ -551,8 +564,9 @@ function PreflightRow({ issue, others, fix, where, pf, onLocate }: { }) { const err = issue.level === 'error' const Icon = err ? XCircle : AlertTriangle + const sqlLevel = publishSqlLevel(issue.code) return ( -
  • +
  • diff --git a/frontend/src/lib/terms.ts b/frontend/src/lib/terms.ts index 831dd0b8..4694850e 100644 --- a/frontend/src/lib/terms.ts +++ b/frontend/src/lib/terms.ts @@ -2533,6 +2533,11 @@ export const CATALOG_UI_TEXT = { patchNote: '说明', // ---- 合并查询的输入:连线时还没选工具、后来才选上的查询节点,一次带出来 mergeAddConnected: (n: number) => `带出连着的 ${formatNumber(n)} 个查询节点`, + // ---- 发布前检查里的 SQL 检查问题:级别标识和证据面板一致,并说清两个发布级别下拦不拦 + publishSqlErrorReminded: 'SQL 检查发现的错误级问题:已发布级别只提醒、不拦发布;受管级别会拦下', + publishSqlErrorBlocked: 'SQL 检查发现的错误级问题:受管级别会拦下发布,请先修改 SQL', + publishSqlWarning: 'SQL 检查的提醒:两个发布级别都只提醒、不拦发布', + publishSqlErrors: (n: number) => `,其中 ${formatNumber(n)} 处是 SQL 检查发现的错误级问题(受管级别会拦下)`, } // =========================================================================== diff --git a/scripts/check-publish.mjs b/scripts/check-publish.mjs index 748b6540..8e094a50 100644 --- a/scripts/check-publish.mjs +++ b/scripts/check-publish.mjs @@ -209,7 +209,7 @@ function lint(graph, level, { followsNever = false } = {}) { issues.push({ level: 'error', node_id: null, code: 'governed.no_contract', fix: id, field: null, message: '受管级别要求至少一个「成果」节点声明出具契约,否则无法判定出具档位' }) fixes.push({ id, code: 'governed.no_contract', node_id: outputs[0].id, kind: 'choice', multiple: true, - label: `为${who(outputs[0])}生成出具契约:核对${report ? who(report) : '「报告撰写」'}的文档,指标来自${who(cards[0])},再选择「必需指标」`, + label: `为${who(outputs[0])}生成出具契约:核对${report ? who(report) : '「报告撰写」'}的文档,指标来自${cards.length ? who(cards[0]) : '「口径卡」'},再选择「必需指标」`, options: cards.flatMap(metricOptions) }) } for (const n of graph.nodes) { @@ -834,6 +834,29 @@ await section('SQL 检查(数据目录阶段 4B):发布前检查写中文 const text = await row.innerText() check('发布弹窗:SQL 检查的问题写节点名和中文规则名,不露规则编号', text.includes('「订单汇总」') && text.includes('一对多关联后重复计算:') && text.includes('会重复计算') && !/fanout_sum|args\.sql/.test(text), text) + // 错误级的 SQL 检查在已发布档只提醒(设计如此),但不能和「没指定模型」这类提示一个样:级别标识和证据面板同一个 + // (SqlLevelBadge:图标 + 「错误」、失败色),并说清已发布级别只提醒、受管级别会拦 + const badge = row.locator('[data-sql-level]') + const badgeLook = await badge.evaluate((el) => { + const probe = document.createElement('span') + probe.style.color = 'var(--st-failed)' + document.body.append(probe) + const failed = getComputedStyle(probe).color + probe.remove() + return { level: el.getAttribute('data-sql-level'), text: el.innerText.trim(), color: getComputedStyle(el).color, failed, icon: !!el.querySelector('svg') } + }).catch(() => null) + check('……带证据面板同一个级别标识:「错误」、失败色、带图标', badgeLook?.level === 'error' && badgeLook.text === '错误' + && badgeLook.color === badgeLook.failed && badgeLook.icon, JSON.stringify(badgeLook)) + check('……写明这是 SQL 检查发现的错误级问题:已发布级别只提醒、不拦,受管级别会拦下', + (await row.locator('[data-preflight-sql-gate="reminded"]').innerText().catch(() => '')) === 'SQL 检查发现的错误级问题:已发布级别只提醒、不拦发布;受管级别会拦下') + check('……结论那句话里点出来,不和别的提示混成一个数', (await dialog(page).locator('[data-preflight-sql-errors="1"]').innerText().catch(() => '')) + .includes('其中 1 处是 SQL 检查发现的错误级问题')) + await dialog(page).locator('[role="radio"]:has-text("受管")').click() + await waitFor(page, async () => (await row.locator('[data-preflight-sql-gate="blocked"]').count()) === 1) + check('换成受管:同一条写「受管级别会拦下发布」,行首是错误', (await row.locator('[data-preflight-sql-gate]').innerText()).includes('受管级别会拦下发布') + && await row.locator('svg[aria-label="错误"]').count() === 1 && await dialog(page).locator('[data-preflight-sql-errors]').count() === 0) + await dialog(page).locator('[role="radio"]:has-text("已发布")').click() + await waitFor(page, async () => (await row.locator('[data-preflight-sql-gate="reminded"]').count()) === 1) await shoot(page, 'preflight-sqlcheck', dialog(page)) await row.locator('button').first().click() const where = () => page.evaluate(() => {