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 (
-
+
{where && !String(issue.message).includes(`「${where}」`) && 「{where}」}
- {/* 对照数据目录的 SQL 检查:先写中文规则名,不露规则编号 */}
+ {/* 对照数据目录的 SQL 检查:级别标识和证据面板同一个(错误 / 提醒),再写中文规则名,不露规则编号 */}
+ {sqlLevel && }
{isSqlCheckCode(issue.code) && {sqlRuleLabel(issue.code)}:}
{issue.message}
+ {/* 这一级在两个发布级别下拦不拦:错误级只有受管档拦,已发布档只提醒(设计如此),要说清楚 */}
+ {sqlLevel && (
+
+ {sqlLevel === 'error' ? (err ? UT.publishSqlErrorBlocked : UT.publishSqlErrorReminded) : UT.publishSqlWarning}
+
+ )}
{issue.node_id && }
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(() => {