定义资产校验逻辑总览(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 后)

组件位置层职责
SpecValidationFacadespec-api validate门面唯一入口:beanIssues(Bundle) / referencesBundle / referencesOne,产出 ValidationIssue
JsonSchemaValidatorspec-api validateL1自由结构形状校验(networknt 2020-12,schema 资产 /schemas/*.schema.json)
SpecDTO 注解spec-api dtoL2必填/级联/跨字段断言(唯一 Bean 规则源)
SpecStructuralValidatorspec-api validateL3a文档内图规则(DAG 端点/widget 树/workflow 节点 config/推理计划引用)
SpecReferenceRulesspec-api validateL3b跨引用规则唯一来源(锚定集合参数化)
SpecConstraintValidatordefinition-biz门面调用写路径 Bean 门(违规→400)
CrossReferenceValidatoragent-build薄适配器包链锚定构建(live API)→ 门面 → error maps
SpecReferenceCheckerdefinition-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_TYPEname
PROPERTYapiName
LINK_TYPEname
METRICname
ACTIONname
ACCESS_POLICYname
PAGE_VIEWname
APPname
DATASETname
DATASOURCEname
PIPELINEname
WORKFLOWname
ANALYSIS_PLANname
RULEmetadata.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
类别规则
必填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「工作流缺少必填入参」
导出 schemaexport.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 收口

已知差异:AnalysisIntent DSL 文档使用 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_packageok=false + errors(每条带文件路径前缀,模型可按路径 edit_asset 修复)+ warnings
发布投影失败HTTP 200 + result.status=FAILED + release snapshot 记录 error(已知边界:半发布态,见 §7)

7. 已知缺口与演进计划

  1. 包安装门(importCompiled)未接入跨引用校验 PR-112c 已完成:spec-api 新增 BundleCrossReferencePort,agent-build 桥接实现,安装门经 ObjectProvider 可选注入并入 blocked(实现缺失时降级为仅结构门)。
  2. PIPELINE 节点类型两处不一致 PR-112c 已完成:结构门白名单与运行态 NodeTypeEnum 对齐(移除 CODE_EXECUTION,出现即报错)。
  3. String 值域未强制:METRIC.metricType/aggregationType、ACCESS_POLICY.subjectType/action/effect、DATASET.materializationType、DATASOURCE.type(存量数据兼容,收紧需先盘点)。
  4. PROPERTY.reference.objectRef 引用存在性 PR-112c 已完成(包链 ⑥b + 发布链分支)。
  5. PAGE_VIEW route 唯一性 PR-112c 已完成(包内独立页面去重 + 内嵌页面与独立页面冲突拦截);props.groupBy[].propertyId 引用仍未校验。
  6. ANALYSIS_PLAN 仅做轻量结构门,语义校验在运行态。另:ANALYSIS_PLAN 结构门原语白名单含 condition 但不含 rule,与 AnalysisIntent DSL 存在命名差异。
  7. JSON Schema 草稿层:编辑器实时红线(FE/BE 共享 schema),草稿提示前移(草稿仍允许脏)。
  8. 发布投影失败 200+FAILED 的半发布态:考虑失败时回滚 release snapshot 登记。
  9. 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 注入)