Skip to main content

nasaga_runtime_core/
catalog.rs

1//! 动态 Definition Catalog 与参与方能力目录使用的公开数据合同。
2//!
3//! 完整流程顺序只来自 [`DefinitionArtifact`]。参与方能力只能证明某个 owner 当前能够执行
4//! 其中一个步骤,不能反向拼装流程。
5
6use std::collections::{BTreeMap, BTreeSet};
7use std::time::Duration;
8
9use nasaga_core::{
10    CancelMode, Compensation, DefinitionVersion, ResolutionMode, ResolutionSpec, ServiceIdentity,
11    StepDefinition, StepName, TimeoutPolicy, WorkflowDefinition, WorkflowName,
12    OPAQUE_IDENTIFIER_MAX_BYTES, STRUCTURED_IDENTIFIER_MAX_BYTES,
13};
14use serde::{Deserialize, Serialize};
15use sha2::{Digest as _, Sha256};
16use url::Url;
17
18use crate::DefinitionRegistry;
19
20/// capability 审计对象键的最大字符数:tenant、四个结构性身份、u32 十进制版本和五个分隔符。
21pub const CATALOG_OBJECT_KEY_MAX_LEN: usize =
22    OPAQUE_IDENTIFIER_MAX_BYTES + STRUCTURED_IDENTIFIER_MAX_BYTES * 4 + 10 + 5;
23
24/// 业务作用:表示 Catalog 中不可变 definition 的生命周期。
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum DefinitionLifecycle {
28    /// 已持久化但尚不能创建实例。
29    Candidate,
30    /// 已通过控制面门禁,可以创建新实例。
31    Active,
32    /// 禁止创建新实例,但继续服务已有实例。
33    Deprecated,
34    /// 已确认不存在运行期引用,不再装载。
35    Retired,
36}
37
38impl DefinitionLifecycle {
39    /// 业务作用:返回数据库和协议共同使用的稳定名称。
40    ///
41    /// 参数说明: 无。
42    ///
43    /// 返回:生命周期的稳定文本。
44    pub fn as_str(self) -> &'static str {
45        match self {
46            Self::Candidate => "candidate",
47            Self::Active => "active",
48            Self::Deprecated => "deprecated",
49            Self::Retired => "retired",
50        }
51    }
52}
53
54/// 业务作用:表达定义发布的幂等裁决,调用方无需解析错误文本。
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
56#[serde(rename_all = "snake_case")]
57pub enum DefinitionPublishDisposition {
58    /// 首次写入候选定义。
59    Published,
60    /// 相同键与摘要已存在,持久内容未变化。
61    Duplicate,
62}
63
64/// 业务作用:封闭 Definition Catalog 可由协议层安全公开的确定性拒绝类别。
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[non_exhaustive]
67pub enum DefinitionCatalogError {
68    /// 请求字段不满足 Catalog 的封闭格式或范围合同。
69    InvalidArgument,
70    /// 认证主体不是目标 definition 或 capability 的持有者。
71    PermissionDenied,
72    /// 目标 definition 不存在。
73    NotFound,
74    /// 生命周期、集群确认或 capability 等业务前置条件尚未成立。
75    FailedPrecondition,
76    /// 相同 tenant/workflow/version 已绑定另一份不可变摘要。
77    DigestConflict,
78    /// 调用方预期的 definition seal 已不再是当前持久事实。
79    PreconditionFailed,
80    /// 相同操作身份已绑定另一组动作参数。
81    OperationConflict,
82}
83
84impl DefinitionCatalogError {
85    /// 业务作用:从持久事务上下文包裹的错误链提取 Catalog 确定性拒绝。
86    ///
87    /// 参数说明:`error` 是 Catalog 写入口返回的完整错误链。
88    ///
89    /// 返回:命中封闭拒绝时返回对应类别;其它失败返回 `None`。
90    pub fn from_error(error: &anyhow::Error) -> Option<Self> {
91        error
92            .chain()
93            .find_map(|cause| cause.downcast_ref::<Self>().copied())
94    }
95}
96
97impl std::fmt::Display for DefinitionCatalogError {
98    /// 业务作用:输出不携带 definition 正文或租户信息的稳定 Catalog 拒绝摘要。
99    ///
100    /// 参数说明:`formatter` 是标准格式化输出目标。
101    ///
102    /// 返回:摘要写入成功时返回 `Ok`;格式化失败返回对应错误。
103    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        formatter.write_str(match self {
105            Self::InvalidArgument => "Saga Catalog request is invalid",
106            Self::PermissionDenied => "Saga Catalog actor is not authorized for this object",
107            Self::NotFound => "Saga definition was not found",
108            Self::FailedPrecondition => "Saga Catalog business precondition is not satisfied",
109            Self::DigestConflict => "Saga definition key has a different immutable digest",
110            Self::PreconditionFailed => "Saga definition digest no longer matches",
111            Self::OperationConflict => {
112                "Saga definition operation identity has different parameters"
113            }
114        })
115    }
116}
117
118impl std::error::Error for DefinitionCatalogError {}
119
120/// 业务作用:声明步骤的补偿能力。
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[serde(rename_all = "kebab-case")]
123pub enum DefinitionCompensation {
124    /// 步骤成功后可进入逆序补偿计划。
125    Compensable,
126    /// 步骤是不可撤销 pivot。
127    NonCompensable,
128}
129
130impl From<DefinitionCompensation> for Compensation {
131    /// 业务作用:把控制面补偿声明转换为状态机强类型。
132    ///
133    /// 参数说明:`value` 是公开补偿枚举。
134    ///
135    /// 返回:对应的核心补偿类型。
136    fn from(value: DefinitionCompensation) -> Self {
137        match value {
138            DefinitionCompensation::Compensable => Self::Compensable,
139            DefinitionCompensation::NonCompensable => Self::NonCompensable,
140        }
141    }
142}
143
144impl From<Compensation> for DefinitionCompensation {
145    /// 业务作用:把状态机补偿类型投影为可持久化字段。
146    ///
147    /// 参数说明:`value` 是已校验步骤的补偿能力。
148    ///
149    /// 返回:稳定的控制面补偿枚举。
150    fn from(value: Compensation) -> Self {
151        match value {
152            Compensation::Compensable => Self::Compensable,
153            Compensation::NonCompensable => Self::NonCompensable,
154        }
155    }
156}
157
158/// 业务作用:声明步骤的取消屏障形态。
159#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "kebab-case")]
161pub enum DefinitionCancelMode {
162    /// 本地 gate 与业务写竞争同一事务。
163    LocalFenceable,
164    /// 通过外部幂等取消命令建立屏障。
165    ExternallyCancellable,
166    /// 无法取消,只能查询或对账。
167    ResolveOnly,
168}
169
170impl From<DefinitionCancelMode> for CancelMode {
171    /// 业务作用:把控制面取消声明转换为状态机强类型。
172    ///
173    /// 参数说明:`value` 是公开取消枚举。
174    ///
175    /// 返回:对应的核心取消形态。
176    fn from(value: DefinitionCancelMode) -> Self {
177        match value {
178            DefinitionCancelMode::LocalFenceable => Self::LocalFenceable,
179            DefinitionCancelMode::ExternallyCancellable => Self::ExternallyCancellable,
180            DefinitionCancelMode::ResolveOnly => Self::ResolveOnly,
181        }
182    }
183}
184
185impl From<CancelMode> for DefinitionCancelMode {
186    /// 业务作用:把状态机取消形态投影为可持久化字段。
187    ///
188    /// 参数说明:`value` 是已校验步骤的取消形态。
189    ///
190    /// 返回:稳定的控制面取消枚举。
191    fn from(value: CancelMode) -> Self {
192        match value {
193            CancelMode::LocalFenceable => Self::LocalFenceable,
194            CancelMode::ExternallyCancellable => Self::ExternallyCancellable,
195            CancelMode::ResolveOnly => Self::ResolveOnly,
196        }
197    }
198}
199
200/// 业务作用:声明未知结果的解决通道。
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "snake_case")]
203pub enum DefinitionResolutionMode {
204    /// 外部系统主动回调确定结果。
205    Callback,
206    /// 参与方按稳定效果身份主动查询。
207    Poll,
208    /// 交由受审计人工裁决。
209    Manual,
210}
211
212impl From<DefinitionResolutionMode> for ResolutionMode {
213    /// 业务作用:把控制面解决模式转换为状态机强类型。
214    ///
215    /// 参数说明:`value` 是公开解决模式。
216    ///
217    /// 返回:对应的核心解决模式。
218    fn from(value: DefinitionResolutionMode) -> Self {
219        match value {
220            DefinitionResolutionMode::Callback => Self::Callback,
221            DefinitionResolutionMode::Poll => Self::Poll,
222            DefinitionResolutionMode::Manual => Self::Manual,
223        }
224    }
225}
226
227impl From<ResolutionMode> for DefinitionResolutionMode {
228    /// 业务作用:把状态机解决模式投影为可持久化字段。
229    ///
230    /// 参数说明:`value` 是已校验步骤的解决通道。
231    ///
232    /// 返回:稳定的控制面解决枚举。
233    fn from(value: ResolutionMode) -> Self {
234        match value {
235            ResolutionMode::Callback => Self::Callback,
236            ResolutionMode::Poll => Self::Poll,
237            ResolutionMode::Manual => Self::Manual,
238        }
239    }
240}
241
242/// 业务作用:表达禁止未知结果或为未知结果配置有界解决通道。
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(untagged)]
245pub enum DefinitionResolution {
246    /// 必须是文本 `forbidden`,表示本地事务结果总是可知。
247    Forbidden(String),
248    /// 允许未知结果,并声明解决通道和总预算。
249    Allowed {
250        /// 解决通道。
251        mode: DefinitionResolutionMode,
252        /// 从首次未知结果开始计算的总预算毫秒数。
253        budget_ms: u64,
254    },
255}
256
257/// 业务作用:声明步骤超时后的迟到成功裁决。
258#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
259#[serde(rename_all = "kebab-case")]
260pub enum DefinitionTimeoutPolicy {
261    /// 接受迟到成功并继续正向流程。
262    AcceptLateSuccess,
263    /// 把迟到成功纳入冻结补偿计划。
264    CompensateLateSuccess,
265}
266
267impl From<DefinitionTimeoutPolicy> for TimeoutPolicy {
268    /// 业务作用:把控制面超时策略转换为状态机强类型。
269    ///
270    /// 参数说明:`value` 是公开超时策略。
271    ///
272    /// 返回:对应的核心策略。
273    fn from(value: DefinitionTimeoutPolicy) -> Self {
274        match value {
275            DefinitionTimeoutPolicy::AcceptLateSuccess => Self::AcceptLateSuccess,
276            DefinitionTimeoutPolicy::CompensateLateSuccess => Self::CompensateLateSuccess,
277        }
278    }
279}
280
281impl From<TimeoutPolicy> for DefinitionTimeoutPolicy {
282    /// 业务作用:把状态机超时策略投影为可持久化字段。
283    ///
284    /// 参数说明:`value` 是已校验步骤的迟到结果策略。
285    ///
286    /// 返回:稳定的控制面超时策略。
287    fn from(value: TimeoutPolicy) -> Self {
288        match value {
289            TimeoutPolicy::AcceptLateSuccess => Self::AcceptLateSuccess,
290            TimeoutPolicy::CompensateLateSuccess => Self::CompensateLateSuccess,
291        }
292    }
293}
294
295/// 业务作用:完整描述 definition 中一个有序步骤的业务和失败语义。
296#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297#[serde(deny_unknown_fields)]
298pub struct DefinitionStepArtifact {
299    /// 步骤正文合同;缺省为无 schema 的 JSON,非默认值参与 definition seal。
300    #[serde(default, skip_serializing_if = "Option::is_none")]
301    pub payload_contract: Option<nasaga_core::SagaPayloadContract>,
302    /// 稳定步骤名称。
303    pub name: String,
304    /// 唯一有权作证结果的逻辑服务身份。
305    pub owner: String,
306    /// 补偿能力。
307    pub compensation: DefinitionCompensation,
308    /// 取消屏障形态。
309    pub cancel_mode: DefinitionCancelMode,
310    /// 未知结果解决声明。
311    pub resolution: DefinitionResolution,
312    /// 正向执行期限毫秒数。
313    pub timeout_ms: u64,
314    /// 迟到成功策略。
315    pub timeout_policy: DefinitionTimeoutPolicy,
316}
317
318/// 业务作用:承载 workflow owner 发布的完整、有序且带 seal 的流程合同。
319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(deny_unknown_fields)]
321pub struct DefinitionArtifact {
322    /// 定义适用的租户域。
323    pub tenant: String,
324    /// 获授权发布完整流程的逻辑主体。
325    pub workflow_owner: String,
326    /// 稳定 workflow 名称。
327    pub workflow: String,
328    /// 从一开始递增的不可变定义版本。
329    pub definition_version: u32,
330    /// 按正向顺序排列的完整步骤集合。
331    pub steps: Vec<DefinitionStepArtifact>,
332    /// 发布者根据完整 definition 计算的 canonical 摘要。
333    pub seal: String,
334}
335
336impl DefinitionArtifact {
337    /// 业务作用:转换为状态机 definition,并复验 seal 覆盖完整业务语义。
338    ///
339    /// 参数说明: 无。
340    ///
341    /// 返回:字段、流程不变量与 seal 全部成立时返回 definition;否则拒绝进入 Catalog。
342    pub fn to_definition(&self) -> anyhow::Result<WorkflowDefinition> {
343        let workflow =
344            WorkflowName::new(&self.workflow).map_err(|error| anyhow::anyhow!(error.code()))?;
345        let version = DefinitionVersion::new(self.definition_version)
346            .map_err(|error| anyhow::anyhow!(error.code()))?;
347        let mut steps = Vec::with_capacity(self.steps.len());
348        for artifact in &self.steps {
349            let resolution = match &artifact.resolution {
350                DefinitionResolution::Forbidden(value) if value == "forbidden" => {
351                    ResolutionSpec::forbidden()
352                }
353                DefinitionResolution::Forbidden(_) => {
354                    anyhow::bail!("definition resolution text must be `forbidden`");
355                }
356                DefinitionResolution::Allowed { mode, budget_ms } => {
357                    ResolutionSpec::allowed((*mode).into(), Duration::from_millis(*budget_ms))
358                }
359            };
360            steps.push(
361                StepDefinition::new(
362                    StepName::new(&artifact.name).map_err(|error| anyhow::anyhow!(error.code()))?,
363                    ServiceIdentity::new(&artifact.owner)
364                        .map_err(|error| anyhow::anyhow!(error.code()))?,
365                    artifact.compensation.into(),
366                    artifact.cancel_mode.into(),
367                    resolution,
368                    Duration::from_millis(artifact.timeout_ms),
369                    artifact.timeout_policy.into(),
370                )
371                .with_payload_contract(artifact.payload_contract.clone().unwrap_or_default())?,
372            );
373        }
374        let definition = WorkflowDefinition::new(workflow, version, steps)
375            .map_err(|error| anyhow::anyhow!(error.code()))?;
376        if definition.digest() != self.seal {
377            anyhow::bail!("definition seal does not match canonical workflow content");
378        }
379        Ok(definition)
380    }
381
382    /// 业务作用:从已校验 definition 生成可发布 artifact,确保 seal 与实际步骤合同同源。
383    ///
384    /// 参数说明:`tenant` 是租户域,`workflow_owner` 是发布主体,`definition` 是完整流程。
385    ///
386    /// 返回:字段顺序稳定且 seal 已计算的 artifact。
387    pub fn from_definition(
388        tenant: impl Into<String>,
389        workflow_owner: impl Into<String>,
390        definition: &WorkflowDefinition,
391    ) -> Self {
392        let steps = definition
393            .steps()
394            .iter()
395            .map(|step| DefinitionStepArtifact {
396                payload_contract: (step.payload_contract()
397                    != &nasaga_core::SagaPayloadContract::default())
398                    .then(|| step.payload_contract().clone()),
399                name: step.name().as_str().to_owned(),
400                owner: step.owner().as_str().to_owned(),
401                compensation: step.compensation().into(),
402                cancel_mode: step.cancel_mode().into(),
403                resolution: match step.resolution().mode() {
404                    Some(mode) => DefinitionResolution::Allowed {
405                        mode: mode.into(),
406                        budget_ms: u64::try_from(step.resolution().budget().as_millis())
407                            .unwrap_or(u64::MAX),
408                    },
409                    None => DefinitionResolution::Forbidden("forbidden".to_owned()),
410                },
411                timeout_ms: u64::try_from(step.timeout().as_millis()).unwrap_or(u64::MAX),
412                timeout_policy: step.timeout_policy().into(),
413            })
414            .collect();
415        Self {
416            tenant: tenant.into(),
417            workflow_owner: workflow_owner.into(),
418            workflow: definition.name().as_str().to_owned(),
419            definition_version: definition.version().get(),
420            steps,
421            seal: definition.digest(),
422        }
423    }
424}
425
426/// 业务作用:返回持久 Catalog 中 definition 的内容与当前 generation。
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(deny_unknown_fields)]
429pub struct DefinitionRecord {
430    /// 完整流程 artifact。
431    pub artifact: DefinitionArtifact,
432    /// 当前生命周期。
433    pub lifecycle: DefinitionLifecycle,
434    /// 最近一次生命周期变更形成的 Catalog generation。
435    pub catalog_generation: u64,
436    /// definition 首次持久化的数据库时间。
437    pub published_at_ms: i64,
438    /// definition 首次进入 active 的数据库时间;候选尚未激活时为空。
439    pub activated_at_ms: Option<i64>,
440}
441
442/// 业务作用:冻结一次 definition 激活必须满足的集群快照与数据面路由合同。
443#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct DefinitionActivationGate {
445    orchestrator_service_identity: ServiceIdentity,
446    catalog_generation: u64,
447    snapshot_digest: String,
448    transport: String,
449    redis_key_tag: Option<String>,
450    address_policy_digest: Option<String>,
451    publisher_contract_digest: Option<String>,
452    result_backend_digest: Option<String>,
453    result_contract_digests: BTreeMap<String, BTreeSet<String>>,
454    activation_contract_digest: String,
455}
456
457/// 业务作用:把人工 definition 生命周期动作的 CAS、幂等身份与审计原因冻结为一个持久裁决输入。
458#[derive(Debug, Clone, PartialEq, Eq)]
459pub struct DefinitionLifecycleOperation {
460    expected_sha256: String,
461    operation_id: String,
462    reason: String,
463}
464
465impl DefinitionLifecycleOperation {
466    /// 业务作用:校验并构造可进入 Catalog 事务的生命周期操作合同。
467    ///
468    /// 参数说明:`expected_sha256` 是调用方观察到的 seal,`operation_id` 是主体范围内的幂等键,
469    /// `reason` 是必须持久保存的人工动作原因。
470    ///
471    /// 返回:字段格式和长度均安全时返回合同;空值、非法摘要或超长文本返回错误。
472    pub fn new(
473        expected_sha256: impl Into<String>,
474        operation_id: impl Into<String>,
475        reason: impl Into<String>,
476    ) -> anyhow::Result<Self> {
477        let expected_sha256 = expected_sha256.into();
478        let operation_id = operation_id.into();
479        let reason = reason.into();
480        anyhow::ensure!(
481            expected_sha256.len() == 64
482                && expected_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()),
483            "definition expected digest is invalid"
484        );
485        anyhow::ensure!(
486            !operation_id.is_empty()
487                && operation_id == operation_id.trim()
488                && operation_id.len() <= 190
489                && !operation_id.chars().any(char::is_control),
490            "definition operation identity is invalid"
491        );
492        anyhow::ensure!(
493            !reason.is_empty()
494                && reason == reason.trim()
495                && reason.len() <= 512
496                && !reason.chars().any(char::is_control),
497            "definition operation reason is invalid"
498        );
499        Ok(Self {
500            expected_sha256: expected_sha256.to_ascii_lowercase(),
501            operation_id,
502            reason,
503        })
504    }
505
506    /// 业务作用:为 validated 自动激活构造绑定 definition seal、身份和 Catalog generation 的定长幂等操作。
507    ///
508    /// 参数说明:`artifact` 是待激活的完整 definition,`catalog_generation` 是已确认快照,`reason` 是持久审计原因。
509    ///
510    /// 返回:返回长度不受租户或流程名影响的合法操作;seal 或原因不符合生命周期合同时返回错误。
511    pub fn for_validated_activation(
512        artifact: &DefinitionArtifact,
513        catalog_generation: u64,
514        reason: impl Into<String>,
515    ) -> anyhow::Result<Self> {
516        let mut canonical = Vec::new();
517        for field in [
518            b"nasaga-validated-activation".as_slice(),
519            artifact.seal.as_bytes(),
520            artifact.tenant.as_bytes(),
521            artifact.workflow.as_bytes(),
522        ] {
523            canonical.extend_from_slice(&(field.len() as u64).to_be_bytes());
524            canonical.extend_from_slice(field);
525        }
526        canonical.extend_from_slice(&artifact.definition_version.to_be_bytes());
527        canonical.extend_from_slice(&catalog_generation.to_be_bytes());
528        let mut hasher = Sha256::new();
529        hasher.update(canonical);
530        Self::new(
531            artifact.seal.clone(),
532            format!("validated-{}", hex::encode(hasher.finalize())),
533            reason,
534        )
535    }
536
537    /// 业务作用:返回必须在持有 definition 行锁时复验的 seal。
538    ///
539    /// 参数说明: 无。
540    ///
541    /// 返回:规范化的小写 SHA-256。
542    pub fn expected_sha256(&self) -> &str {
543        &self.expected_sha256
544    }
545
546    /// 业务作用:返回人工生命周期动作的幂等身份。
547    ///
548    /// 参数说明: 无。
549    ///
550    /// 返回:已通过长度与非空校验的稳定身份。
551    pub fn operation_id(&self) -> &str {
552        &self.operation_id
553    }
554
555    /// 业务作用:返回与生命周期动作不可分持久化的审计原因。
556    ///
557    /// 参数说明: 无。
558    ///
559    /// 返回:已通过长度与非空校验的原因文本。
560    pub fn reason(&self) -> &str {
561        &self.reason
562    }
563}
564
565impl DefinitionActivationGate {
566    /// 业务作用:构造只能由同代 Ready 副本和指定数据面共同满足的激活门禁。
567    ///
568    /// 参数说明:`orchestrator_service_identity` 定位逻辑控制面,`catalog_generation` 与
569    /// `snapshot_digest` 固定副本已确认快照,`transport` 固定本次部署的数据面协议。
570    ///
571    /// 返回:摘要和协议属于封闭集合时返回门禁;无效输入拒绝进入数据库激活事务。
572    pub fn new(
573        orchestrator_service_identity: ServiceIdentity,
574        catalog_generation: u64,
575        snapshot_digest: impl Into<String>,
576        transport: impl Into<String>,
577    ) -> anyhow::Result<Self> {
578        let snapshot_digest = snapshot_digest.into();
579        anyhow::ensure!(
580            snapshot_digest.len() == 64
581                && snapshot_digest.bytes().all(|byte| byte.is_ascii_hexdigit()),
582            "catalog snapshot digest is invalid"
583        );
584        let transport = transport.into();
585        anyhow::ensure!(
586            matches!(
587                transport.as_str(),
588                "http" | "grpc" | "kafka" | "redis-stream"
589            ),
590            "definition activation transport is unsupported"
591        );
592        let mut gate = Self {
593            orchestrator_service_identity,
594            catalog_generation,
595            snapshot_digest,
596            transport,
597            redis_key_tag: None,
598            address_policy_digest: None,
599            publisher_contract_digest: None,
600            result_backend_digest: None,
601            result_contract_digests: BTreeMap::new(),
602            activation_contract_digest: String::new(),
603        };
604        gate.refresh_activation_contract_digest();
605        Ok(gate)
606    }
607
608    /// 业务作用:把 Redis Cluster 的实际同槽配置绑定进 definition 激活门禁,确保数据库裁决
609    /// 与稍后构造 publisher 使用同一份 route 上下文。
610    ///
611    /// 参数说明:`key_tag` 是运行配置要求 capability stream 首个生效 hash tag 匹配的值。
612    ///
613    /// 返回:仅在 transport 为 Redis Streams 且 tag 满足名称合同时返回门禁;其它组合返回错误。
614    pub fn with_redis_key_tag(mut self, key_tag: impl Into<String>) -> anyhow::Result<Self> {
615        let key_tag = key_tag.into();
616        anyhow::ensure!(
617            self.transport == "redis-stream" && valid_redis_key_tag(&key_tag),
618            "definition activation Redis key tag is invalid"
619        );
620        self.redis_key_tag = Some(key_tag);
621        self.refresh_activation_contract_digest();
622        Ok(self)
623    }
624
625    /// 业务作用:把当前 transport 的受信地址政策绑定进副本确认与 definition 激活门禁。
626    ///
627    /// 参数说明:`digest` 是调用方对 transport 专属 canonical allowlist 计算的 SHA-256。
628    ///
629    /// 返回:摘要合法时返回更新后的门禁;格式无效时拒绝形成可持久确认的运行时合同。
630    pub fn with_address_policy_digest(mut self, digest: impl Into<String>) -> anyhow::Result<Self> {
631        let digest = digest.into();
632        anyhow::ensure!(
633            digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()),
634            "definition activation address policy digest is invalid"
635        );
636        self.address_policy_digest = Some(digest.to_ascii_lowercase());
637        self.refresh_activation_contract_digest();
638        Ok(self)
639    }
640
641    /// 业务作用:把最终 publisher 使用的路由、凭据、client 与超时合同绑定进副本确认,
642    /// 防止同一 Catalog 快照被运行参数不同的副本同时发布为权威。
643    ///
644    /// 参数说明:`digest` 是调用方对已解析 publisher 输入计算的 canonical SHA-256。
645    ///
646    /// 返回:摘要格式合法时返回更新后的门禁;格式无效时拒绝形成持久确认。
647    pub fn with_publisher_contract_digest(
648        mut self,
649        digest: impl Into<String>,
650    ) -> anyhow::Result<Self> {
651        let digest = digest.into();
652        anyhow::ensure!(
653            digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()),
654            "definition activation publisher contract digest is invalid"
655        );
656        self.publisher_contract_digest = Some(digest.to_ascii_lowercase());
657        self.refresh_activation_contract_digest();
658        Ok(self)
659    }
660
661    /// 业务作用:把 Kafka result 返回路径的后端身份绑定进激活事务,确保参与方能力与
662    /// Orchestrator consumer 使用同一套 broker 命名边界。
663    ///
664    /// 参数说明:`digest` 是规范化 broker 地址集合与 result topic 前缀形成的 SHA-256。
665    ///
666    /// 返回:仅在 transport 为 Kafka 且摘要格式合法时返回门禁;其它组合拒绝激活。
667    pub fn with_result_backend_digest(mut self, digest: impl Into<String>) -> anyhow::Result<Self> {
668        let digest = digest.into();
669        anyhow::ensure!(
670            self.transport == "kafka"
671                && digest.len() == 64
672                && digest.bytes().all(|byte| byte.is_ascii_hexdigit()),
673            "definition activation result backend digest is invalid"
674        );
675        self.result_backend_digest = Some(digest.to_ascii_lowercase());
676        self.refresh_activation_contract_digest();
677        Ok(self)
678    }
679
680    /// 业务作用:把一个 participant owner 的 result 发送凭据与 Orchestrator 接收凭据绑定进激活事务。
681    ///
682    /// 参数说明:`owner` 是 definition step 的服务身份,`digest` 是协议专属凭据合同的 SHA-256。
683    ///
684    /// 返回:身份与摘要合法时返回门禁;非法输入拒绝形成副本确认合同。
685    pub fn with_result_contract_digest(
686        mut self,
687        owner: impl Into<String>,
688        digest: impl Into<String>,
689    ) -> anyhow::Result<Self> {
690        let owner = owner.into();
691        ServiceIdentity::new(&owner)?;
692        let digest = digest.into();
693        anyhow::ensure!(
694            digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()),
695            "definition activation result contract digest is invalid"
696        );
697        self.result_contract_digests
698            .entry(owner)
699            .or_default()
700            .insert(digest.to_ascii_lowercase());
701        self.refresh_activation_contract_digest();
702        Ok(self)
703    }
704
705    /// 业务作用:重算副本必须共同确认的运行时路由合同摘要。
706    ///
707    /// 参数说明: 无。
708    ///
709    /// 返回:无;transport、Redis tag、地址政策与 publisher 合同按带类型边界的固定顺序写入摘要。
710    fn refresh_activation_contract_digest(&mut self) {
711        let mut digest = Sha256::new();
712        update_snapshot_digest(&mut digest, b"nasaga-activation-contract");
713        update_snapshot_digest(&mut digest, self.transport.as_bytes());
714        match self.redis_key_tag.as_deref() {
715            Some(key_tag) => {
716                digest.update([1]);
717                update_snapshot_digest(&mut digest, key_tag.as_bytes());
718            }
719            None => digest.update([0]),
720        }
721        match self.address_policy_digest.as_deref() {
722            Some(policy_digest) => {
723                digest.update([1]);
724                update_snapshot_digest(&mut digest, policy_digest.as_bytes());
725            }
726            None => digest.update([0]),
727        }
728        match self.publisher_contract_digest.as_deref() {
729            Some(publisher_digest) => {
730                digest.update([1]);
731                update_snapshot_digest(&mut digest, publisher_digest.as_bytes());
732            }
733            None => digest.update([0]),
734        }
735        match self.result_backend_digest.as_deref() {
736            Some(result_backend_digest) => {
737                digest.update([1]);
738                update_snapshot_digest(&mut digest, result_backend_digest.as_bytes());
739            }
740            None => digest.update([0]),
741        }
742        digest.update((self.result_contract_digests.len() as u64).to_be_bytes());
743        for (owner, contracts) in &self.result_contract_digests {
744            update_snapshot_digest(&mut digest, owner.as_bytes());
745            digest.update((contracts.len() as u64).to_be_bytes());
746            for contract in contracts {
747                update_snapshot_digest(&mut digest, contract.as_bytes());
748            }
749        }
750        self.activation_contract_digest = hex::encode(digest.finalize());
751    }
752
753    /// 业务作用:返回有权确认本次激活快照的逻辑 Orchestrator 身份。
754    ///
755    /// 参数说明: 无。
756    ///
757    /// 返回:构造门禁时已校验的长期服务身份。
758    pub fn orchestrator_service_identity(&self) -> &ServiceIdentity {
759        &self.orchestrator_service_identity
760    }
761
762    /// 业务作用:返回待激活 definition 所依赖的精确 Catalog generation。
763    ///
764    /// 参数说明: 无。
765    ///
766    /// 返回:副本确认和数据库状态必须共同命中的代际。
767    pub fn catalog_generation(&self) -> u64 {
768        self.catalog_generation
769    }
770
771    /// 业务作用:返回全部有效 Ready 副本必须一致确认的完整快照摘要。
772    ///
773    /// 参数说明: 无。
774    ///
775    /// 返回:六十四位十六进制摘要。
776    pub fn snapshot_digest(&self) -> &str {
777        &self.snapshot_digest
778    }
779
780    /// 业务作用:返回 definition 每个步骤在激活时必须具备的受管数据面协议。
781    ///
782    /// 参数说明: 无。
783    ///
784    /// 返回:HTTP、gRPC、Kafka 或 Redis Streams 的稳定协议名。
785    pub fn transport(&self) -> &str {
786        &self.transport
787    }
788
789    /// 业务作用:返回 Redis Streams 激活时必须由 capability route 满足的 Cluster 同槽 tag。
790    ///
791    /// 参数说明: 无。
792    ///
793    /// 返回:Redis 激活门禁绑定的 tag;其它 transport 或尚未补齐上下文时为空。
794    pub fn redis_key_tag(&self) -> Option<&str> {
795        self.redis_key_tag.as_deref()
796    }
797
798    /// 业务作用:返回 Kafka candidate 必须匹配的 result broker 与 topic 命名合同摘要。
799    ///
800    /// 参数说明: 无。
801    ///
802    /// 返回:Kafka 激活门禁已绑定的摘要;其它 transport 返回空。
803    pub fn result_backend_digest(&self) -> Option<&str> {
804        self.result_backend_digest.as_deref()
805    }
806
807    /// 业务作用:判断 capability 声明的 result 凭据合同是否属于该 owner 的受信接收集合。
808    ///
809    /// 参数说明:`owner` 是步骤服务身份,`digest` 是 participant 发布的协议专属合同摘要。
810    ///
811    /// 返回:门禁中存在完全一致的 owner 与摘要时为真。
812    pub fn accepts_result_contract(&self, owner: &str, digest: &str) -> bool {
813        self.result_contract_digests
814            .get(owner)
815            .is_some_and(|contracts| contracts.contains(digest))
816    }
817
818    /// 业务作用:返回全部 Ready 副本必须一致确认的数据面与地址政策摘要。
819    ///
820    /// 参数说明: 无。
821    ///
822    /// 返回:绑定 transport、Redis tag、地址政策和最终 publisher 输入的 canonical SHA-256。
823    pub fn activation_contract_digest(&self) -> &str {
824        &self.activation_contract_digest
825    }
826}
827
828/// 业务作用:声明参与方当前可执行步骤的协议和逐实例地址事实。
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(deny_unknown_fields)]
831pub struct CapabilityDescriptor {
832    /// 本 handler 接受的正文合同,必须与 definition 中对应步骤一致。
833    #[serde(default, skip_serializing_if = "Option::is_none")]
834    pub payload_contract: Option<nasaga_core::SagaPayloadContract>,
835    /// 能力获准服务的租户,必须与 definition key 和授权范围一致。
836    pub tenant: String,
837    /// 能力所属逻辑服务身份,必须与认证主体一致。
838    pub owner: String,
839    /// 当前副本的稳定身份,用于租约续租。
840    pub replica_identity: String,
841    /// 支持的 workflow。
842    pub workflow: String,
843    /// 支持的 definition 版本。
844    pub definition_version: u32,
845    /// 支持的步骤名称。
846    pub step: String,
847    /// 步骤补偿能力。
848    pub compensation: DefinitionCompensation,
849    /// 步骤取消形态。
850    pub cancel_mode: DefinitionCancelMode,
851    /// 是否托管未知结果解决入口。
852    pub allow_unknown: bool,
853    /// 托管的解决模式;禁止未知时为空。
854    pub resolution_mode: Option<DefinitionResolutionMode>,
855    /// command transport 的稳定名称。
856    pub transport: String,
857    /// 当前实例 origin 或 broker route,不包含凭据。
858    pub endpoint: String,
859    /// HTTP 模式下实际生效的 Saga 基础路径。
860    pub effective_saga_base_path: Option<String>,
861    /// result producer 与 Orchestrator 接收端共同持有的协议专属凭据或后端合同摘要。
862    #[serde(default, skip_serializing_if = "Option::is_none")]
863    pub result_contract_digest: Option<String>,
864    /// endpoint 与路径绑定的单调 route generation;持久值由 Catalog 在主键行锁内分配。
865    pub route_generation: u64,
866    /// 客户端建议的租约时长;服务端执行有界裁剪。
867    pub requested_lease_ms: u64,
868}
869
870impl CapabilityDescriptor {
871    /// 业务作用:计算不受续租建议影响的 capability 内容摘要,供幂等续租与 route 变更检测。
872    ///
873    /// 参数说明: 无。
874    ///
875    /// 返回:覆盖 owner、步骤合同、transport、endpoint 与 route generation 的小写十六进制摘要。
876    pub fn digest(&self) -> String {
877        let mut canonical = self.clone();
878        canonical.requested_lease_ms = 0;
879        let bytes = serde_json::to_vec(&canonical)
880            .expect("serializing a capability descriptor cannot fail");
881        let mut hasher = Sha256::new();
882        hasher.update(bytes);
883        hex::encode(hasher.finalize())
884    }
885
886    /// 业务作用:计算不含租约建议和已分配代际的路由合同摘要,供 Catalog 判断真实内容是否变化。
887    ///
888    /// 参数说明: 无。
889    ///
890    /// 返回:覆盖身份、步骤语义、transport、endpoint 与有效路径的稳定小写十六进制摘要。
891    pub fn route_contract_digest(&self) -> String {
892        let mut canonical = self.clone();
893        canonical.route_generation = 0;
894        canonical.requested_lease_ms = 0;
895        let bytes = serde_json::to_vec(&canonical)
896            .expect("serializing a capability descriptor cannot fail");
897        let mut hasher = Sha256::new();
898        hasher.update(bytes);
899        hex::encode(hasher.finalize())
900    }
901
902    /// 业务作用:复验 capability 与 definition 中被引用步骤的不可变语义一致。
903    ///
904    /// 参数说明:`tenant` 是候选流程的授权租户,`definition` 是候选流程,`step` 是其中一个步骤。
905    ///
906    /// 返回:owner、步骤身份、版本与能力完全一致时为真。
907    pub fn matches_step(
908        &self,
909        tenant: &str,
910        definition: &WorkflowDefinition,
911        step: &StepDefinition,
912    ) -> bool {
913        self.payload_contract.clone().unwrap_or_default() == *step.payload_contract()
914            && self.tenant == tenant
915            && self.workflow == definition.name().as_str()
916            && self.definition_version == definition.version().get()
917            && self.step == step.name().as_str()
918            && self.owner == step.owner().as_str()
919            && self.compensation == step.compensation().into()
920            && self.cancel_mode == step.cancel_mode().into()
921            && self.allow_unknown == step.resolution().allow_unknown()
922            && self.resolution_mode == step.resolution().mode().map(Into::into)
923    }
924}
925
926/// 业务作用:按 command transport 复验 capability 的完整可发布 route 合同,确保目录准入、
927/// candidate 激活与实际 publisher 对同一 descriptor 得出一致结论。
928///
929/// 参数说明:`descriptor` 携带 transport、endpoint、HTTP 专属有效路径与 result 凭据合同摘要。
930///
931/// 返回:HTTP origin 与路径、gRPC HTTPS origin、Kafka topic 或 Redis stream 名称完整合法时成功;
932/// transport 专属字段缺失、越界或出现在其它 transport 时返回参数错误。
933pub fn validate_capability_route_contract(
934    descriptor: &CapabilityDescriptor,
935) -> Result<(), DefinitionCatalogError> {
936    descriptor
937        .payload_contract
938        .clone()
939        .unwrap_or_default()
940        .validate()
941        .map_err(|_| DefinitionCatalogError::InvalidArgument)?;
942    let path_contract_is_valid = match descriptor.transport.as_str() {
943        "http" => descriptor
944            .effective_saga_base_path
945            .as_deref()
946            .is_some_and(valid_capability_http_base_path),
947        "grpc" | "kafka" | "redis-stream" => descriptor.effective_saga_base_path.is_none(),
948        _ => false,
949    };
950    if !path_contract_is_valid {
951        return Err(DefinitionCatalogError::InvalidArgument);
952    }
953    let result_contract_is_valid = match descriptor.transport.as_str() {
954        "http" | "grpc" | "kafka" | "redis-stream" => descriptor
955            .result_contract_digest
956            .as_deref()
957            .is_some_and(|digest| {
958                digest.len() == 64
959                    && digest == digest.to_ascii_lowercase()
960                    && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
961            }),
962        _ => false,
963    };
964    if !result_contract_is_valid {
965        return Err(DefinitionCatalogError::InvalidArgument);
966    }
967
968    let endpoint_is_valid = match descriptor.transport.as_str() {
969        "http" => valid_capability_origin(&descriptor.endpoint, &["http", "https"]),
970        "grpc" => valid_capability_origin(&descriptor.endpoint, &["https"]),
971        "kafka" => {
972            !descriptor.endpoint.is_empty()
973                && descriptor.endpoint.len() <= 249
974                && descriptor.endpoint.trim() == descriptor.endpoint
975                && !matches!(descriptor.endpoint.as_str(), "." | "..")
976                && descriptor
977                    .endpoint
978                    .bytes()
979                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
980        }
981        "redis-stream" => validate_redis_stream_route(&descriptor.endpoint, None).is_ok(),
982        _ => false,
983    };
984    if endpoint_is_valid {
985        Ok(())
986    } else {
987        Err(DefinitionCatalogError::InvalidArgument)
988    }
989}
990
991/// 业务作用:判断 capability 网络 endpoint 是否为 publisher 与地址政策都能安全解释的纯 origin。
992///
993/// 参数说明:`endpoint` 是目录输入,`schemes` 是当前 transport 允许的协议集合。
994///
995/// 返回:URI 含合法 host、允许协议且不携带凭据、路径、query 或 fragment 时为真。
996fn valid_capability_origin(endpoint: &str, schemes: &[&str]) -> bool {
997    let raw_origin_is_canonical = endpoint
998        .split_once("://")
999        .is_some_and(|(scheme, authority)| {
1000            schemes.contains(&scheme)
1001                && !authority.is_empty()
1002                && (authority.bytes().all(|byte| byte != b'/')
1003                    || (authority.ends_with('/')
1004                        && authority[..authority.len() - 1]
1005                            .bytes()
1006                            .all(|byte| byte != b'/')))
1007        });
1008    Url::parse(endpoint).is_ok_and(|url| {
1009        endpoint.trim() == endpoint
1010            && raw_origin_is_canonical
1011            && schemes.contains(&url.scheme())
1012            && url.host_str().is_some_and(|host| !host.is_empty())
1013            && url.username().is_empty()
1014            && url.password().is_none()
1015            && url.query().is_none()
1016            && url.fragment().is_none()
1017            && matches!(url.path(), "" | "/")
1018    })
1019}
1020
1021/// 业务作用:按 Redis Cluster 实际选 slot 的规则校验 stream 名与运行配置的同槽合同。
1022///
1023/// 参数说明:`stream` 是 capability 或 publisher 使用的完整 key,`key_tag` 是可选的受信配置值。
1024///
1025/// 返回:名称有界,且提供 tag 时首个生效 hash tag 精确匹配则成功;否则返回参数错误。
1026pub fn validate_redis_stream_route(
1027    stream: &str,
1028    key_tag: Option<&str>,
1029) -> Result<(), DefinitionCatalogError> {
1030    let valid = !stream.is_empty()
1031        && stream.len() <= 190
1032        && !stream.chars().any(char::is_control)
1033        && key_tag.is_none_or(|tag| {
1034            valid_redis_key_tag(tag) && first_redis_hash_tag(stream) == Some(tag)
1035        });
1036    if valid {
1037        Ok(())
1038    } else {
1039        Err(DefinitionCatalogError::InvalidArgument)
1040    }
1041}
1042
1043/// 业务作用:校验运行配置中的 Redis Cluster hash tag 可安全嵌入 stream key。
1044///
1045/// 参数说明:`tag` 是不含花括号的配置值。
1046///
1047/// 返回:非空、有界、无控制字符和花括号时为真。
1048fn valid_redis_key_tag(tag: &str) -> bool {
1049    !tag.is_empty()
1050        && tag.len() <= 64
1051        && !tag.contains(['{', '}'])
1052        && !tag.chars().any(char::is_control)
1053}
1054
1055/// 业务作用:按 Redis 服务端规则解析 key 首对花括号形成的实际 hash tag,保持 activation 与
1056/// publisher 的 Cluster slot 裁决一致。
1057///
1058/// 参数说明:`name` 是完整 Redis key。
1059///
1060/// 返回:首对花括号之间非空时返回 tag;空 tag、未闭合或不存在时返回空。
1061fn first_redis_hash_tag(name: &str) -> Option<&str> {
1062    let open = name.find('{')?;
1063    let close = name[open + 1..].find('}')?;
1064    if close == 0 {
1065        return None;
1066    }
1067    Some(&name[open + 1..open + 1 + close])
1068}
1069
1070/// 业务作用:判断 HTTP capability 的有效 Saga path 是否能无歧义地追加固定操作名并参与签名。
1071///
1072/// 参数说明:`path` 是已经包含应用 context 的目录路径。
1073///
1074/// 返回:非根、无尾斜杠、编码、模板或特殊路径段的 canonical path 为真。
1075fn valid_capability_http_base_path(path: &str) -> bool {
1076    path != "/"
1077        && path.starts_with('/')
1078        && !path.ends_with('/')
1079        && path.bytes().all(|byte| {
1080            byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'-' | b'_' | b'.' | b'~')
1081        })
1082        && !path.contains("//")
1083        && !path.contains('%')
1084        && !path.contains('?')
1085        && !path.contains('#')
1086        && !path.contains('*')
1087        && !path.contains('{')
1088        && !path.contains('}')
1089        && !path.split('/').any(|segment| matches!(segment, "." | ".."))
1090}
1091
1092/// 业务作用:返回服务端接受的 capability 租约与幂等摘要。
1093#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1094#[serde(deny_unknown_fields)]
1095pub struct CapabilityReceipt {
1096    /// 服务端数据库时钟计算的租约截止时间。
1097    pub accepted_until_ms: i64,
1098    /// 不含租约截止时间的 capability 内容摘要。
1099    pub capability_digest: String,
1100    /// Catalog 在 capability 主键行锁内确认的单调 route generation。
1101    pub route_generation: u64,
1102    /// 本次写入所处 Catalog generation。
1103    pub catalog_generation: u64,
1104}
1105
1106/// 业务作用:保存能力目录中一个仍在租约内的逐实例 route。
1107#[derive(Debug, Clone, PartialEq, Eq)]
1108pub struct RegisteredCapability {
1109    /// 参与方发布的完整能力描述。
1110    pub descriptor: CapabilityDescriptor,
1111    /// 服务端数据库时钟确定的租约截止时间。
1112    pub accepted_until_ms: i64,
1113    /// 持久化内容摘要。
1114    pub capability_digest: String,
1115}
1116
1117/// 业务作用:从有效能力快照中按完整步骤身份选择唯一数据面 route,避免同租户同名 endpoint
1118/// 的无关能力替代目标 descriptor 参与地址政策裁决。
1119///
1120/// 参数说明:
1121/// - `capabilities`: 当前 Catalog generation 内仍在租约期的完整能力集合。
1122/// - `transport`: 当前数据面的稳定协议名。
1123/// - `tenant`: definition 所属租户。
1124/// - `definition`: 当前调度实例固定的完整 definition。
1125/// - `step`: definition 中待选择 route 的完整步骤合同。
1126///
1127/// 返回:没有匹配能力时返回 `Ok(None)`;所有匹配副本声明同一合法 endpoint 与有效路径,且 Kafka
1128/// result 后端摘要一致时返回其中一个完整 descriptor;route 非法或出现不同目标时返回错误。
1129pub fn select_capability_route<'a>(
1130    capabilities: &'a [RegisteredCapability],
1131    transport: &str,
1132    tenant: &str,
1133    definition: &WorkflowDefinition,
1134    step: &StepDefinition,
1135) -> Result<Option<&'a CapabilityDescriptor>, DefinitionCatalogError> {
1136    let mut selected: Option<&CapabilityDescriptor> = None;
1137    for descriptor in capabilities
1138        .iter()
1139        .map(|capability| &capability.descriptor)
1140        .filter(|descriptor| {
1141            descriptor.transport == transport && descriptor.matches_step(tenant, definition, step)
1142        })
1143    {
1144        validate_capability_route_contract(descriptor)?;
1145        if selected.is_some_and(|current| {
1146            current.endpoint != descriptor.endpoint
1147                || current.effective_saga_base_path != descriptor.effective_saga_base_path
1148                || (transport == "kafka"
1149                    && current.result_contract_digest != descriptor.result_contract_digest)
1150        }) {
1151            return Err(DefinitionCatalogError::FailedPrecondition);
1152        }
1153        selected.get_or_insert(descriptor);
1154    }
1155    Ok(selected)
1156}
1157
1158/// 业务作用:取得同一步骤的完整逐实例直接路由集合,允许 HTTP/gRPC 副本使用独立 endpoint。
1159/// 参数说明:`capabilities` 是仍在租期内的同代快照;`transport`、`tenant`、`definition`、`step` 共同限定步骤合同。
1160/// 返回:合法 HTTP/gRPC 路由按 endpoint 与副本身份排序;消息代理仍要求唯一目标,非法或同副本冲突时拒绝。
1161pub fn select_capability_routes<'a>(
1162    capabilities: &'a [RegisteredCapability],
1163    transport: &str,
1164    tenant: &str,
1165    definition: &WorkflowDefinition,
1166    step: &StepDefinition,
1167) -> Result<Vec<&'a CapabilityDescriptor>, DefinitionCatalogError> {
1168    if !matches!(transport, "http" | "grpc") {
1169        return select_capability_route(capabilities, transport, tenant, definition, step)
1170            .map(|route| route.into_iter().collect());
1171    }
1172    let mut routes: Vec<&CapabilityDescriptor> = Vec::new();
1173    for descriptor in capabilities
1174        .iter()
1175        .map(|capability| &capability.descriptor)
1176        .filter(|descriptor| {
1177            descriptor.transport == transport && descriptor.matches_step(tenant, definition, step)
1178        })
1179    {
1180        validate_capability_route_contract(descriptor)?;
1181        // 同一副本不能同时自报互斥地址或结果合同,否则不得随机挑选其中一份权威。
1182        if routes.iter().any(|current| {
1183            current.replica_identity == descriptor.replica_identity && *current != descriptor
1184        }) {
1185            return Err(DefinitionCatalogError::FailedPrecondition);
1186        }
1187        routes.push(descriptor);
1188    }
1189    routes.sort_by(|left, right| {
1190        (&left.endpoint, &left.replica_identity).cmp(&(&right.endpoint, &right.replica_identity))
1191    });
1192    routes.dedup();
1193    Ok(routes)
1194}
1195
1196/// 业务作用:把同一数据库 generation 的 definition 与有效 capability 形成不可拆分加载结果。
1197#[derive(Debug, Clone)]
1198pub struct DynamicCatalogSnapshot {
1199    /// 本次读取确认的共享 generation。
1200    pub generation: u64,
1201    /// definition 与 route 内容形成的确定性摘要,供多副本确认同一代快照。
1202    pub snapshot_digest: String,
1203    /// active 与仍被旧实例引用的 deprecated definition 快照。
1204    pub registry: DefinitionRegistry,
1205    /// 当前 generation 中候选与可运行 definition 的完整持久化记录。
1206    pub definition_records: Vec<DefinitionRecord>,
1207    /// 读取时仍在服务端租约内的参与方逐实例能力。
1208    pub capabilities: Vec<RegisteredCapability>,
1209}
1210
1211impl DynamicCatalogSnapshot {
1212    /// 业务作用:把已复验的 definition 与 capability 组合为带确定性内容摘要的代际快照。
1213    ///
1214    /// 参数说明:`generation` 是共享代际,`registry` 是运行定义,`capabilities` 是有效路由。
1215    ///
1216    /// 返回:返回可供多副本确认的不可拆分快照。
1217    pub fn new(
1218        generation: u64,
1219        registry: DefinitionRegistry,
1220        capabilities: Vec<RegisteredCapability>,
1221    ) -> Self {
1222        Self::with_definition_records(generation, registry, Vec::new(), capabilities)
1223    }
1224
1225    /// 业务作用:把 candidate、active、deprecated definition 与有效 capability 共同绑定进快照摘要。
1226    ///
1227    /// 参数说明:`generation` 是共享代际,`registry` 只承载可运行定义,`definition_records` 还包含
1228    /// 尚未激活的候选定义,`capabilities` 是读取时有效的逐实例路由。
1229    ///
1230    /// 返回:摘要覆盖租户、owner、生命周期、seal 和路由且字段边界无歧义的不可拆分快照。
1231    pub fn with_definition_records(
1232        generation: u64,
1233        registry: DefinitionRegistry,
1234        mut definition_records: Vec<DefinitionRecord>,
1235        mut capabilities: Vec<RegisteredCapability>,
1236    ) -> Self {
1237        definition_records.sort_by(|left, right| {
1238            (
1239                &left.artifact.tenant,
1240                &left.artifact.workflow,
1241                left.artifact.definition_version,
1242            )
1243                .cmp(&(
1244                    &right.artifact.tenant,
1245                    &right.artifact.workflow,
1246                    right.artifact.definition_version,
1247                ))
1248        });
1249        capabilities.sort_by(|left, right| {
1250            (
1251                &left.descriptor.tenant,
1252                &left.descriptor.owner,
1253                &left.descriptor.replica_identity,
1254                &left.descriptor.workflow,
1255                left.descriptor.definition_version,
1256                &left.descriptor.step,
1257            )
1258                .cmp(&(
1259                    &right.descriptor.tenant,
1260                    &right.descriptor.owner,
1261                    &right.descriptor.replica_identity,
1262                    &right.descriptor.workflow,
1263                    right.descriptor.definition_version,
1264                    &right.descriptor.step,
1265                ))
1266        });
1267        let mut digest = Sha256::new();
1268        digest.update(generation.to_be_bytes());
1269        // 可运行 registry 也必须进入摘要,否则静态快照或租户投影漂移时仍可伪装成同代内容。
1270        let mut runtime_definitions = registry
1271            .definitions()
1272            .map(|definition| {
1273                (
1274                    definition.name().as_str(),
1275                    definition.version().get(),
1276                    definition.digest(),
1277                )
1278            })
1279            .collect::<Vec<_>>();
1280        runtime_definitions.sort();
1281        for (workflow, version, definition_digest) in runtime_definitions {
1282            update_snapshot_digest(&mut digest, workflow.as_bytes());
1283            digest.update(version.to_be_bytes());
1284            update_snapshot_digest(&mut digest, definition_digest.as_bytes());
1285        }
1286        for record in &definition_records {
1287            update_snapshot_digest(&mut digest, record.artifact.tenant.as_bytes());
1288            update_snapshot_digest(&mut digest, record.artifact.workflow_owner.as_bytes());
1289            update_snapshot_digest(&mut digest, record.artifact.workflow.as_bytes());
1290            digest.update(record.artifact.definition_version.to_be_bytes());
1291            update_snapshot_digest(&mut digest, record.lifecycle.as_str().as_bytes());
1292            update_snapshot_digest(&mut digest, record.artifact.seal.as_bytes());
1293            digest.update(record.catalog_generation.to_be_bytes());
1294        }
1295        for capability in &capabilities {
1296            update_snapshot_digest(&mut digest, capability.descriptor.tenant.as_bytes());
1297            update_snapshot_digest(&mut digest, capability.descriptor.owner.as_bytes());
1298            update_snapshot_digest(
1299                &mut digest,
1300                capability.descriptor.replica_identity.as_bytes(),
1301            );
1302            update_snapshot_digest(&mut digest, capability.descriptor.workflow.as_bytes());
1303            digest.update(capability.descriptor.definition_version.to_be_bytes());
1304            update_snapshot_digest(&mut digest, capability.descriptor.step.as_bytes());
1305            update_snapshot_digest(&mut digest, capability.capability_digest.as_bytes());
1306        }
1307        Self {
1308            generation,
1309            snapshot_digest: hex::encode(digest.finalize()),
1310            registry,
1311            definition_records,
1312            capabilities,
1313        }
1314    }
1315}
1316
1317/// 业务作用:以长度前缀把可变字段加入 Catalog 摘要,避免相邻字段拼接产生边界歧义。
1318///
1319/// 参数说明:`digest` 是当前 SHA-256 状态,`value` 是一个完整领域字段的原始字节。
1320///
1321/// 返回:无;字段长度和内容按固定顺序写入摘要状态。
1322fn update_snapshot_digest(digest: &mut Sha256, value: &[u8]) {
1323    digest.update((value.len() as u64).to_be_bytes());
1324    digest.update(value);
1325}