namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Health;
/// Authority 的快照语义种类。决定除「生产运行是否成功」之外还要不要校验表级不变量。
public static class S8AuthorityKind
{
/// 当前态镜像表(如 mdp_std_so):upsert 覆盖、无发布步骤、读者不按批次取数 ⇒ 无表级不变量可校验。
public const string CurrentStateMirror = "CURRENT_STATE_MIRROR";
/// 当前快照表(如 dwd_requirement_examine_detail):按批次累积 + 末尾原子翻牌发布 ⇒ 必须校验发布不变量。
public const string PublishedSnapshot = "PUBLISHED_SNAPSHOT";
}
///
/// 数据集的 Authority 声明:健康判定需要知道的一切,全部声明在数据集定义处。
///
/// 为什么放在数据集定义而不是规则上:不变量是 per-dataset 的。
/// Rule02 与 Rule03 共用同一个生产者作业(实测 job_code 同为 S1_MDP_SYNC_TRANSFORM、
/// 同一次 run、同一个 batch_id),仅靠运行日志无法区分二者;把不变量挂在数据集上,
/// Resolver 内部就不需要 switch (ruleCode)。
///
/// 调度器不感知物理表名:它只传 DatasetCode,
/// 由 MDP 适配实现去查具体的表与运行日志。
///
public sealed class S8AuthoritySpec
{
/// 生产该 Authority 的转换作业编码,对应 mdp_transform_run_log.job_code。
public string ProducerJobCode { get; init; } = string.Empty;
/// 见 。
public string AuthorityKind { get; init; } = S8AuthorityKind.CurrentStateMirror;
///
/// 最近一次成功距今超过该时长即判 STALE。
///
/// 必须显式声明,不从作业周期推导。实测四条理由:
/// ① 声明周期 60 分钟,而同租户实际相邻成功间隔为 28–39 分钟,声明值高估;
/// ② 近 14 天 3530 个间隔中,最大 8.04 小时、超过 3 小时的有 46 次 ——
/// 若按「3 × 周期 = 3 小时」判定,14 天内会产生 46 次假 STALE;
/// ③ 触发器调度的是「入队」而非「执行」,每租户实际起跑时间还叠加了不可控的队列延迟;
/// ④ 触发器 Args 的单位与形态不统一(PeriodTrigger 与 CronTrigger 混存,
/// [Period(ms)] 与 [PeriodSeconds(s)] 都序列化成裸数字)。
/// 且该表可被作业管理界面在运行期改写,不应让门禁阈值随之漂移。
///
public TimeSpan StaleWindow { get; init; } = TimeSpan.FromHours(12);
///
/// 查找「最近一次成功」时的回溯窗口。不是可选优化:
/// 实测无界回溯在长期停摆的租户上单次耗时 146ms(反向扫描 7122 条索引项),
/// 加 7 天上界后降到 7.3ms。且语义上等价 —— 「回溯窗口内没有成功」本身就是 STALE 结论。
///
public TimeSpan LookbackWindow { get; init; } = TimeSpan.FromDays(7);
///
/// 生产者是否可信。
/// false 表示该 Authority 存在多个写入方、且部分写入方不写运行日志,
/// 于是 run-log 无法证明表里最终的数据出自谁 —— 此时一律判 UNKNOWN、拦截恢复。
/// 这是 Rule01 的现状(dwd_supplier_delivery 除 MDP 转换外,
/// 还有两个 API 路径直写且不留 run-log)。
///
public bool ProducerTrusted { get; init; } = true;
/// ProducerTrusted 为 false 时的原因码,见 。
public string UntrustedReasonCode { get; init; } = S8AuthorityHealthReason.Rule01MultiWriterUntrusted;
// ── 以下仅 AuthorityKind = PUBLISHED_SNAPSHOT 时使用 ──────────────────────────────
// 声明在这里而不是写死在 Resolver 里,是为了让 Resolver 保持通用:
// 它只按声明去查,内部不出现 switch (datasetCode) 或 switch (tableName)。
// 调度器仍然只传 DatasetCode,不接触任何物理表名。
/// 快照表名。
public string? SnapshotTable { get; init; }
/// 快照表里标识批次的列。
public string SnapshotBatchColumn { get; init; } = "calc_batch_id";
/// 快照表里标识「属于当前批次」的列(值为 1 即当前)。
public string SnapshotCurrentFlagColumn { get; init; } = "is_current_flag";
///
/// 当前快照内不允许为空的列。
/// 用绝对计数校验(COUNT(*) WHERE 当前 AND 列 IS NULL 必须为 0),
/// 而不是空值率 —— 计数在空快照上天然真空成立,与 HEALTHY_EMPTY 零冲突;
/// 写成比率则会在空集上 0 除、语义未定义。
/// 只填「结构上不可能为空」的列。例如 bom_level 就不能放进来 ——
/// 转换侧对非数字层级刻意留 NULL("脏值留 NULL,不猜层级"),把它列为必填等于按设计制造假 DEGRADED。
///
public string[] SnapshotRequiredColumns { get; init; } = Array.Empty();
}