定义资产校验逻辑总览(Validation)
本文按资产类型梳理全部检验逻辑:字段必填与类型、合法值/枚举、类型内跨字段规则、结构规则、跨类型联合校验,以及各条链路的执行时点与错误契约。 对应实现:PR-111(版本写入收敛)、PR-112a(Spec 注解化校验)、PR-112b(跨引用校验扩容)、PR-112c(缺口收口)、PR-113a~d(校验架构收敛:统一门面 + 三层校验)。
0. 目标架构(PR-113 已落地)
链路层(4 条) 草稿编辑 write_asset │ 固化/apply/发布 │ 包安装 import │ agent validate_package
↓ 锚定集合 + Issue 出口各不相同
统一门面 SpecValidationFacade(spec-api validate 包)
├─ L1 JsonSchemaValidator 自由结构形状(pipeline-dag / page-view schema 资产,FE/BE 共享)
├─ L2 Bean 注解约束 SpecDTO 注解为唯一规则源(@NotBlank/@Valid/@AssertTrue)
├─ L3a 文档图规则 SpecStructuralValidator(端点/唯一性/children⊆widgets)
└─ L3b SpecReferenceRules 跨引用规则唯一来源(两套 checker 已合并)
↓ 锚定集合(包链 spec∪现网 / 发布链仅现网)
服务端口 DefinitionReferencePort · DatasetSchemaPort · DatasetQueryApi/DataSourceService- 规则单一来源:跨引用规则只写在
SpecReferenceRules(113b 前 CrossReferenceValidator/SpecReferenceChecker 各一份);Bean 规则只在 SpecDTO 注解上。 - 链路差异最小化:包链/发布链只差锚定集合构建与 Issue 出口(BLOCKED DTO / 400 消息 / agent errors)。
- L1 边界:JSON Schema 管「形状」(类型/必填/枚举),文档内相对引用(边端点、children⊆widgets、id 唯一)无法用 Schema 表达,保留在 L3a 代码层。
1. 校验目标与总原则
- 草稿态允许脏:保存草稿(
saveResource/fd_package_edit_file)不做阻断校验,坏草稿可落库;校验前移到编辑工具的即时提示(edit_asset/write_asset返回 errors,不阻断保存)。 - 运行态必须稳:固化版本、apply、发布、安装四条写路径全部设门,未通过校验的定义不得进入运行态。
- 确定性优先:全部校验为确定性规则(注解 / 结构遍历 / 引用锚定),不依赖 LLM 自查。
- 错误分级:单资源链路抛
BusinessException(400);包链路进blocked清单(整体不落库);warnings 只提示不阻断。
2. 校验器与执行链路
2.1 校验组件(PR-113 后)
| 组件 | 位置 | 层 | 职责 |
|---|---|---|---|
SpecValidationFacade | spec-api validate | 门面 | 唯一入口:beanIssues(Bundle) / referencesBundle / referencesOne,产出 ValidationIssue |
JsonSchemaValidator | spec-api validate | L1 | 自由结构形状校验(networknt 2020-12,schema 资产 /schemas/*.schema.json) |
| SpecDTO 注解 | spec-api dto | L2 | 必填/级联/跨字段断言(唯一 Bean 规则源) |
SpecStructuralValidator | spec-api validate | L3a | 文档内图规则(DAG 端点/widget 树/workflow 节点 config/推理计划引用) |
SpecReferenceRules | spec-api validate | L3b | 跨引用规则唯一来源(锚定集合参数化) |
SpecConstraintValidator | definition-biz | 门面调用 | 写路径 Bean 门(违规→400) |
CrossReferenceValidator | agent-build | 薄适配器 | 包链锚定构建(live API)→ 门面 → error maps |
SpecReferenceChecker | definition-biz | 薄适配器 | 发布链锚定构建(live 端口)→ 门面 → 消息列表 |
锚定端口:DefinitionReferencePort(definitionActiveExists / pageViewRouteExists / activeNaturalKeys / activePageViewRoutes),definition-biz 提供 DefinitionReferencePortImpl。
2.2 执行链路矩阵
| 链路 | Bean 注解校验 | 结构门 | 跨引用校验 | schema 对齐 | 错误出口 |
|---|---|---|---|---|---|
保存草稿(saveResource / write_asset) | —(允许脏) | —(edit/write 返回 errors 提示) | — | — | 提示不阻断 |
固化版本(saveVersion) | ✅ 400 | — | — | — | BusinessException(400) |
apply(applyResource) | ✅ 400 | — | — | — | 400 |
包 upsert(applyImportedResource) | ✅ 400(兜底) | ✅(install 前置) | ✅(validate 工具前置) | ✅ | 400 兜底 |
发布(publishResource,单资源) | ✅ 400 | — | ✅ 400(现网锚定) | — | 400 |
| 发布(bundle 安装链,bundleApply=true) | ✅ | ✅ | ✅(blocked 阶段) | ✅ | 跳过发布链引用校验,避免同包先后安装顺序误报 |
包安装(importCompiled) | ✅(并入 blocked) | ✅ blocked | ✅ blocked(PR-112c:BundleCrossReferencePort 桥接) | ✅ blocked/警告 | FAILED + blockedResources,整体不落库 |
agent 整包校验(validate_package) | ✅ | ✅ | ✅ errors | — | ok=false + errors(带文件路径前缀) |
| 运行态(投影/执行) | — | — | — | — | 投影状态检查(如”未物化先发布”、管道无 INPUT) |
反序列化守门:spec 无法解析为对应 SpecDTO 时,写路径一律
BAD_REQUEST;读路径(历史版本查看等)降级为树形态,不做校验。注意 Jackson 全局FAIL_ON_UNKNOWN_PROPERTIES=disable——拼写错误的字段会被静默丢弃,这是已知边界。
3. 通用规则(全部 14 类资产)
3.1 自然键必填(PR-112a 注解)
所有 SpecDTO 的自然键字段 @NotBlank,缺失即 400(此前是 naturalKey=null 导致的 500 或静默跳过):
| 资源类型 | 自然键字段 |
|---|---|
| OBJECT_TYPE | name |
| PROPERTY | apiName |
| LINK_TYPE | name |
| METRIC | name |
| ACTION | name |
| ACCESS_POLICY | name |
| PAGE_VIEW | name |
| APP | name |
| DATASET | name |
| DATASOURCE | name |
| PIPELINE | name |
| WORKFLOW | name |
| ANALYSIS_PLAN | name |
| RULE | metadata.name(@Valid 级联) |
3.2 级联校验
ObjectTypeSpecDTO.properties(@Valid):内联属性逐个执行 PROPERTY 规则。MetricSpecDTO.objectRef(@Valid):引用对象结构校验。
3.3 自然键不可变
已有资源的自然键不允许通过普通编辑修改;key 无法提取(为 null)时不再静默跳过(由自然键必填注解前置拦截),rename 必须走”删除 + 重建”。
4. 按资产类型的检验逻辑
4.1 OBJECT_TYPE(对象类型)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 枚举/合法值 | datasetBindingMode:EXTERNAL / MANAGED(DatasetBindingModeEnum) |
| 类型内规则 | 内联 properties 级联校验;物化表名 = materialize.tableNamePrefix + name(未配置则直接 name) |
| 跨引用 | EXTERNAL 绑定时 primaryDatasetRef.name 必须为现网已存在数据集(发布链:datasetSchemaPort.datasetExists;包链:spec.datasets ∪ 现网 ACTIVE)。MANAGED 不查(首装时数据集由物化生成) |
| 运行态 | MANAGED 对象”未物化先发布 schema”→ BusinessException(bundleApply 场景降级 SKIPPED_NOT_MATERIALIZED 不阻断) |
4.2 PROPERTY(属性)
| 类别 | 规则 |
|---|---|
| 必填 | apiName |
| 枚举/合法值 | dataType:STRING / INTEGER / LONG / NUMBER / DOUBLE / BOOLEAN / DATE / DATETIME / TIMESTAMP / ENUM / REFERENCE / COMPUTED / JSON / ARRAY / OBJECT(枚举类型字段,非法值反序列化即失败);另有 propertyKind / stabilityLevel / materializationStrategy 枚举 |
| 类型内规则 | ENUM 类型需 semantic.dictMapping;长度约束在 validation.maxLength(无独立 length 字段);引用语义走 reference(objectRef + referenceType + cascadeRule);物理列映射 = datasetColumnName |
| 跨引用 | reference.objectRef → OBJECT_TYPE(包链 ⑥b:内联与独立 objectTypeProperties;发布链 PropertySpecDTO 分支,PR-112c) |
| 同步语义 | 对象发布时属性按 REPLACE 全量同步;bundle REPLACE 模式孤儿属性自动生成 deletion revision |
4.3 LINK_TYPE(关联类型)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 枚举/合法值 | cardinality:ONE_TO_ONE / ONE_TO_MANY / MANY_TO_MANY |
| 类型内规则 | join 连接条件;层级关系配 isHierarchical / maxDepth |
| 跨引用 | sourceObjectRef / targetObjectRef → OBJECT_TYPE(包链 ⑥ + 发布链,现网锚定) |
| 运行态 | 源对象缺失 → 投影 BusinessException NOT_FOUND;目标由 ontology 写服务内部校验 |
4.4 METRIC(指标)
| 类别 | 规则 |
|---|---|
| 必填 | name(修复此前 naturalKey=null → 500 的复发根因) |
| 枚举/合法值 | metricType(String):BASE / AGGREGATE / DERIVED / COMPOSITE;aggregationType(String):COUNT / SUM / AVG / MIN / MAX / COUNT_DISTINCT。spec 侧为 String 未强制值域(运行态枚举兜底),见 §7 |
| 类型内规则(@AssertTrue) | isDerived=true 时必须提供 derivedExpression;apiName 缺省归一为 name(PR-112a 补齐实现) |
| 跨引用 | objectRef → OBJECT_TYPE(包链 ⑥ + 发布链);basePropertyApiName ∈ 所属对象属性(包链 ②);派生表达式 ${dep} ∈ spec.metrics(包链 ③,支持 metric: 前缀) |
4.5 ACTION(动作)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 枚举/合法值 | type:UPDATE_FIELD / CREATE_OBJECT / DELETE_OBJECT / UPSERT_OBJECT / LINK_OBJECT / UNLINK_OBJECT / FUNCTION_BACKED;bindingType:SINGLE_INSTANCE / TRANSITION / SIDE_EFFECT / QUERY / VALIDATION |
| 类型内规则 | FUNCTION_BACKED 类型需提供 execution 执行配置;参数 schema = inputSchema |
| 跨引用 | objectRef → OBJECT_TYPE(包链 ⑥ + 发布链) |
4.6 ACCESS_POLICY(访问策略)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 枚举/合法值 | subjectType:USER / ROLE / GROUP;action:READ / WRITE / MANAGE;effect:ALLOW / DENY。展平字段为 String 未强制,见 §7 |
| 类型内规则 | 推荐 subject + rules 结构化形式(展平字段为兼容);rowFilter / maskedColumns 为自由 JSON;priority 排序 |
| 跨引用 | resourceRef → OBJECT_TYPE(包链 ⑥ + 发布链) |
4.7 RULE(规则)
| 类别 | 规则 |
|---|---|
| 必填 | metadata.name(@Valid 级联到 RuleMetadataDTO) |
| 枚举/合法值 | severity:INFO / WARNING / HIGH / CRITICAL;另有 RulePhase / RuleOperator DSL 枚举族 |
| 类型内规则 | PRE_WRITE 触发规则禁止声明 effects;DSL 结构 = metadata + scope.target + parameters + triggers + ruleType,按类型二选一分支体:constraintBody(conditions + enforcement)或 hypothesisBody(appliesWhen / supportsWhen / refutesWhen / requires / conclusion)。metadata.category 与 executionPolicy 不由用户配置,由平台按分类派生(derivedCategory / RuleCompiler.buildPolicy) |
| 跨引用 | scope.target(kind=ObjectType + name)→ OBJECT_TYPE;编译由 RuleDomainProjectionAdapter 完成,编译物持久化失败抛 IllegalStateException |
4.8 DATASOURCE(数据源)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 枚举/合法值 | type(String):MYSQL / POSTGRESQL / HANA / ORACLE / CLICKHOUSE / KAFKA / S3 / REST_API / ICEBERG / TRINO(注意是 POSTGRESQL 非 POSTGRES);scopeType:PLATFORM / TENANT(共享数据源投影时跳过更新) |
| 类型内规则 | 连接参数与凭据全部在自由 configJson(结构未约束);包内 apiKey 只允许占位符或空值,真值通过容器环境变量注入(编译期校验) |
| 跨引用 | 被 DATASET.sourceId 消费(包链 ①b) |
4.9 DATASET(数据集)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 枚举/合法值 | materializationType(String):EXTERNAL / MATERIALIZED |
| 类型内规则(双重,阻断级) | MATERIALIZED 且 syncSchemaOnInstall(缺省视为 true)时,syncConfigJson 必须含非空 externalSourceId 与 externalTable——Bean @AssertTrue(PR-112a)与结构门 ⑤ 双重覆盖;缺 columns / rowFilter 仅 warning |
| 跨引用 | sourceId ∈ spec.datasources ∪ 现网可见数据源(包链 ①b) |
| 安装期 | schema 列定义不在 spec(安装期从外部源同步);validateSchemaAlignment:本体属性 datasetColumnName 必须存在于数据集物理列(大小写不敏感),数据集尚不存在(如管道 OUTPUT 首装未物化)→ 告警不阻断 |
4.10 PIPELINE(管道)
| 类别 | 规则 |
|---|---|
| 必填 | name;runOnInstall 缺省 true(false 不进运行批) |
| 节点类型全集(结构门,PR-112c 与运行态对齐) | INPUT / PROJECTION / FILTER / JOIN / AGGREGATE / UNION / OUTPUT(与运行态 NodeTypeEnum 一致;CODE_EXECUTION 运行态无对应算子,出现即报错) |
| 节点必填配置 | INPUT→config.datasetId;FILTER→root: {condition, children:[{column,operator,value}]}(字符串表达式旧写法报错);JOIN→joinType + keys:[{left,right}];AGGREGATE→groupBy + aggregations:[{column,func,alias}];PROJECTION→columns 非空([col AS alias]);UNION→unionType(UNION_ALL 等);CODE_EXECUTION→code;OUTPUT→registerDataset != false 时必填 targetTableName(禁止旧写法 datasetId) |
| 边规范 | 必须用 source / target 且端点存在;旧写法 sourceNodeId/targetNodeId 直接报错(编译期 PackageManifestCompiler 会归一 sourceNodeId→source) |
| 图校验 | 结构门只查端点存在;环检测在运行态(DagTopologyUtils Kahn,CycleDetectedException → run FAILED);断链节点编译期表现为空上游(静默) |
| 跨引用 | INPUT datasetId ∈ spec.datasets ∪ 现网 ACTIVE 数据集(包链 ④) |
| 运行态 | 无 INPUT 节点、无 registerDataset=true 的 OUTPUT 节点 → 抛异常;INPUT 缺 datasetId → BAD_REQUEST |
4.11 WORKFLOW(工作流)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 节点类型全集 | START / END / AGENT_TASK / ACTION_TASK / CODE_EXECUTION(FUNCTION 别名) / WEBHOOK / EXCLUSIVE_GATEWAY / LOOP / USER_TASK / SUB_WORKFLOW / ONTOLOGY_MUTATION / WAIT_EVENT / PARALLEL_GATEWAY;未知 type 兜底 GenericWorkflowNodeDTO |
| 结构门(5 类已覆盖) | START / END 节点缺失即 error;ONTOLOGY_QUERY→querySpec.objectName(extract 缺失 warning);CODE_EXECUTION→code;config 缺 _nodeConfigType 导致多态降级丢字段 → error;exports.inputSchema.required ⊆ START.inputParams |
| 图校验(运行态,最完整) | GraphStructureValidator:唯一 START、≥1 END、重复 id、边端点、自环、出入边完整性、网关默认分支;GraphReachabilityValidator:START 可达性、到 END 可达性、DFS 环检测。失败 400 阻断安装 |
| 入参校验(运行前) | START.inputParams 中 required=true 且上下文缺失/空白 → 400「工作流缺少必填入参」 |
| 导出 schema | export.inputSchema 优先,否则由 START.inputParams 自动生成(name/type/required) |
| 边规范 | sourceNodeId / targetNodeId(与管道的 source/target 不同,勿混用) |
4.12 APP(应用壳)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 结构规则 | 结构门:pages 非空,且每个 page.name ∈ 包内 PAGE_VIEW(阻断) |
| 跨引用(PR-112b) | homePageRoute ∈ 包内页面 route(app.pages[].route 内嵌匹配)∪ 现网 PAGE_VIEW route(pageViewRouteExists);不匹配 → 包链 error / 发布链 400(此前运行态静默回退第一个页面) |
| 运行态 | 发布时生成 runtime_snapshot_json(App 必须整包安装,单文件安装 PAGE_VIEW 会覆盖 runtime_snapshot 导致菜单异常) |
4.13 PAGE_VIEW(页面视图)
| 类别 | 规则 |
|---|---|
| 必填 | name |
| 结构门 | widgets 非空;每个 widget 有 widgetId 且唯一;root 存在;root.children 引用 ⊆ widgetId 集合 |
| 跨引用(PR-112b) | widget 绑定:bindings.metric → METRIC、bindings.data → OBJECT_TYPE、props.aggregations[].metricId → METRIC(包链 ⑦ + 发布链,WidgetBindingExtractor 提取);缺失引用此前仅表现为运行时空数据 |
| 已知缺口 | props.groupBy[].propertyId 未校验(route 唯一性已由 PR-112c 结构门覆盖) |
4.14 ANALYSIS_PLAN(分析计划)
| 类别 | 规则 |
|---|---|
| 必填 | name(@NotBlank) |
结构门(SpecStructuralValidator#checkAnalysisPlans) | intent 非空;intent.goal 非空;intent.steps 非空;每个 step 的 id 非空且唯一;每个 step 至少声明一个原语(fact / condition / traverse / capability / expert / loop) |
| 语义校验 | definition 层不解析意图语义;对象/规则存在性、引用一致性与编译由运行态 AnalysisPlanService / IntentValidator / IntentCompiler 收口 |
已知差异:
AnalysisIntentDSL 文档使用rule步骤原语,而 definition 层结构门的原语白名单当前识别condition、尚未识别rule;两者需后端对齐(见 §7)。
5. 跨类型联合校验矩阵
| 引用方向 | 包链(agent validate / 安装) | 发布链(单资源) | 运行态兜底 |
|---|---|---|---|
| OBJECT_TYPE.primaryDatasetRef → DATASET(仅 EXTERNAL) | ✅ | ✅ | 未物化报错 |
| DATASET.sourceId → DATASOURCE | ✅ | — | 同步失败 |
| METRIC.objectRef → OBJECT_TYPE | ✅ ⑥ | ✅ | 投影异常 |
| METRIC.basePropertyApiName → 对象属性 | ✅ ② | — | 查询为空 |
| METRIC ${dep} → spec.metrics | ✅ ③ | — | 表达式失败 |
| LINK_TYPE 源/目标 → OBJECT_TYPE | ✅ ⑥ | ✅ | NOT_FOUND |
| ACTION.objectRef → OBJECT_TYPE | ✅ ⑥ | ✅ | 运行时失败 |
| ACCESS_POLICY.resourceRef → OBJECT_TYPE | ✅ ⑥ | ✅ | 静默失效 |
| PIPELINE INPUT.datasetId → DATASET | ✅ ④ | — | run FAILED |
| PAGE_VIEW bindings.metric → METRIC | ✅ ⑦ | ✅ | 空数据(此前无校验) |
| PAGE_VIEW bindings.data → OBJECT_TYPE | ✅ ⑦ | ✅ | 空数据(此前无校验) |
| APP.pages → 包内 PAGE_VIEW | ✅ 结构门 | — | — |
| APP.homePageRoute → PAGE_VIEW.route | ✅ ⑧ | ✅ | 静默回退第一页(校验门已前置拦截) |
| exports.inputSchema.required ⊆ START.inputParams | ✅ 结构门 | — | 入参校验 |
| PROPERTY.reference.objectRef → OBJECT_TYPE | ✅ ⑥b(PR-112c) | ✅(PR-112c) | — |
| ANALYSIS_PLAN intent 内部步骤/原语 | ✅ 结构门(轻量) | — | 运行态 IntentValidator |
锚定语义:包链 = spec 内部 ∪ 现网 ACTIVE(容忍同包先后安装顺序);发布链 = 仅现网 ACTIVE(bundleApply 发布跳过,由包链把守);端口缺失/查询失败时降级为仅 spec 内部校验。
6. 错误契约
| 场景 | 返回 |
|---|---|
| 单资源固化 / apply / 发布引用缺失 | BusinessException(400),消息格式:规格校验失败:字段 消息;… / 引用校验失败:… |
validateCompiled(编辑链路预览) | status=VALIDATED + blockedResources 清单(BLOCKED 契约保持) |
importCompiled(安装前半程) | blocked 非空 → status=FAILED + blockedResources,整体不落库;blocked 空 → PENDING_FINALIZE |
agent validate_package | ok=false + errors(每条带文件路径前缀,模型可按路径 edit_asset 修复)+ warnings |
| 发布投影失败 | HTTP 200 + result.status=FAILED + release snapshot 记录 error(已知边界:半发布态,见 §7) |
7. 已知缺口与演进计划
包安装门(importCompiled)未接入跨引用校验PR-112c 已完成:spec-api 新增BundleCrossReferencePort,agent-build 桥接实现,安装门经 ObjectProvider 可选注入并入 blocked(实现缺失时降级为仅结构门)。PIPELINE 节点类型两处不一致PR-112c 已完成:结构门白名单与运行态NodeTypeEnum对齐(移除 CODE_EXECUTION,出现即报错)。- String 值域未强制:METRIC.metricType/aggregationType、ACCESS_POLICY.subjectType/action/effect、DATASET.materializationType、DATASOURCE.type(存量数据兼容,收紧需先盘点)。
PROPERTY.reference.objectRef 引用存在性PR-112c 已完成(包链 ⑥b + 发布链分支)。PAGE_VIEW route 唯一性PR-112c 已完成(包内独立页面去重 + 内嵌页面与独立页面冲突拦截);props.groupBy[].propertyId引用仍未校验。- ANALYSIS_PLAN 仅做轻量结构门,语义校验在运行态。另:ANALYSIS_PLAN 结构门原语白名单含
condition但不含rule,与 AnalysisIntent DSL 存在命名差异。 - JSON Schema 草稿层:编辑器实时红线(FE/BE 共享 schema),草稿提示前移(草稿仍允许脏)。
- 发布投影失败 200+FAILED 的半发布态:考虑失败时回滚 release snapshot 登记。
BuildExplorePortBridgeTest.datasetSchemaMapsColumnsAndDegrades为既有失败(与校验链改动无关,已用 stash 对照验证)。
8. 测试覆盖
| 测试类 | 覆盖 |
|---|---|
SpecStructuralValidatorTest(spec-api) | CODE_EXECUTION 拦截、PAGE_VIEW route 唯一、APP 内嵌 route 冲突、ANALYSIS_PLAN 轻量结构校验(goal/steps/stepId/原语) |
SpecConstraintValidatorTest(definition-biz) | 自然键必填、级联(OT.properties)、派生指标断言、DATASET syncConfig 断言、树形态放行 |
SpecReferenceCheckerTest(definition-biz) | ACTION/LINK_TYPE/ACCESS_POLICY/METRIC 对象引用、PAGE_VIEW 绑定、APP.homePageRoute(内嵌命中/未匹配)、EXTERNAL/MANAGED 数据集 |
CrossReferenceValidatorTest(agent-build) | ①②③⑤ 原有规则 + 112b:对象引用/绑定/homeRoute 缺失拦截、现网锚定命中放行 |
DefinitionWriteServiceTest(definition-biz) | 写链路回归(校验器 mock 注入) |