using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
using Admin.NET.Plugin.AiDOP.Service.S8.Rules.DataAccess;
namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules.Definitions;
/// 规则目录:回答「这个 rule_code 在当前代码版本里是否被定义、定义成什么」。
public interface IS8RuleCatalog
{
/// 取定义;未定义返回 null。
S8RuleDefinition? TryGet(string? ruleCode);
/// 取定义;未定义抛 ,消息带 reason code。
S8RuleDefinition GetRequired(string? ruleCode);
/// 是否已定义。
bool IsDefined(string? ruleCode);
/// 全部已定义规则。Batch 2 的 provisioning 将按此为每个租户落 runtime policy 行。
IReadOnlyCollection Definitions { get; }
}
///
/// 默认目录实现:聚合所有 ,并在构造时做完整性校验。
///
/// 为什么校验放在构造函数而不是运行期:一条定义写错(rule_type 不在词表、
/// TIMEOUT 却没给 Timeout 语义、scene 不是 S1–S7)属于**发布事故**,
/// 应该在服务起来的时候就炸掉,而不是等某条规则跑到那一 tick 才报错——
/// 那时错误只出现在后台日志里,页面上一切正常。
///
/// 注册为 Singleton + 启动期强制解析(见 Startup.Configure)。
/// 数据集侧 S8DatasetCatalog 注册为 Transient,其重复定义的失败形态是
/// 「首次解析该服务的那个请求 500」而非启动崩溃 —— 该问题在
/// S8BusinessDatasetDefinitions 的注释里已被记录为已知弱点。规则侧不重复这个弱点。
///
public class S8RuleCatalog : IS8RuleCatalog
{
/// 规则在当前代码版本中无定义。与「配置错了」严格区分:它意味着该规则不该运行。
public const string ReasonNotFound = "rule_definition_not_found";
private readonly Dictionary _definitions;
public S8RuleCatalog(IEnumerable sources)
{
// rule_code 在 DB 侧是大小写敏感比较(dedup_key 逐字拼接),
// 但目录查找用不敏感比较可以把「大小写写错」暴露成重复定义而不是静默两条,
// 与 S8DatasetCatalog 的 OrdinalIgnoreCase 一致。
_definitions = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var source in sources)
{
foreach (var definition in source.GetDefinitions())
{
Validate(definition, source.GetType().Name);
var code = definition.RuleCode.Trim();
if (_definitions.ContainsKey(code))
throw new InvalidOperationException(
$"监控规则 {code} 被重复定义(来源包含 {source.GetType().Name})");
_definitions[code] = definition;
}
}
}
public IReadOnlyCollection Definitions => _definitions.Values;
public bool IsDefined(string? ruleCode) =>
!string.IsNullOrWhiteSpace(ruleCode) && _definitions.ContainsKey(ruleCode!.Trim());
public S8RuleDefinition? TryGet(string? ruleCode)
{
if (string.IsNullOrWhiteSpace(ruleCode)) return null;
return _definitions.TryGetValue(ruleCode.Trim(), out var definition) ? definition : null;
}
public S8RuleDefinition GetRequired(string? ruleCode) =>
TryGet(ruleCode) ?? throw new S8BizException(
$"[{ReasonNotFound}] 监控规则 {(string.IsNullOrWhiteSpace(ruleCode) ? "(空)" : ruleCode)} 在当前版本中没有代码定义,无法配置或运行");
///
/// 定义完整性校验。每一条都对应一种「存得下但跑不通」的历史故障形态,不是形式检查。
///
private static void Validate(S8RuleDefinition d, string sourceName)
{
void Fail(string message) =>
throw new InvalidOperationException($"监控规则定义非法(来源 {sourceName}):{message}");
if (string.IsNullOrWhiteSpace(d.RuleCode)) Fail("rule_code 不能为空");
if (string.IsNullOrWhiteSpace(d.DisplayName)) Fail($"{d.RuleCode} 缺少 DisplayName(配置页的规则名称没有其他来源)");
if (string.IsNullOrWhiteSpace(d.DatasetCode)) Fail($"{d.RuleCode} 缺少 dataset_code");
if (string.IsNullOrWhiteSpace(d.ExceptionTypeCode)) Fail($"{d.RuleCode} 缺少 exception_type_code");
if (string.IsNullOrWhiteSpace(d.SourceObjectType)) Fail($"{d.RuleCode} 缺少 source_object_type(它参与 dedup_key)");
// rule_type 必须在真实存在 evaluator 的词表内,且**大小写逐字相等**。
// 刻意不用 S8RuleTypeFieldRequirements.IsKnown —— 它内部做 ToUpperInvariant,
// 于是 "timeout" 能过校验;而调度器与 evaluator 的 switch 是 ordinal 敏感的,
// 那条定义会一路存下来、最后在运行期报 unsupported_rule_type。
// 「存得下但跑不通」正是本 Catalog 要消灭的形态。
if (!S8RuleTypeFieldRequirements.KnownRuleTypes.Contains(d.RuleType, StringComparer.Ordinal))
Fail($"{d.RuleCode} 的 rule_type 非法:{d.RuleType};当前仅支持 "
+ string.Join(" / ", S8RuleTypeFieldRequirements.KnownRuleTypes));
// scene / stage 必须是严格 S1–S7,拒 legacy 复合场景(与 CreateAsync 既有口径一致)。
if (!S8ModuleCode.IsValid(d.SceneCode))
Fail($"{d.RuleCode} 的 scene_code 非法:{d.SceneCode};当前仅支持 " + string.Join(" / ", S8ModuleCode.All));
if (!S8ModuleCode.IsValid(d.StageCode))
Fail($"{d.RuleCode} 的 stage_code 非法:{d.StageCode}");
// 类型与语义必须配套:声明 TIMEOUT 却不给 Timeout 语义,运行期只会表现为 NullReference。
switch (d.RuleType)
{
case var t when t == S8TimeoutRuleEvaluator.RuleTypeCode:
if (d.Timeout == null) Fail($"{d.RuleCode} 声明 TIMEOUT 但未提供 TIMEOUT 判定语义");
break;
case var t when t == S8ShortageRuleEvaluator.RuleTypeCode:
if (d.Shortage == null) Fail($"{d.RuleCode} 声明 SHORTAGE 但未提供 SHORTAGE 判定语义");
break;
case var t when t == S8OutOfRangeRuleEvaluator.RuleTypeCode:
if (d.OutOfRange == null) Fail($"{d.RuleCode} 声明 OUT_OF_RANGE 但未提供 OUT_OF_RANGE 判定语义");
break;
}
var policy = d.Parameters ?? new S8RuleParameterPolicy();
if (policy.AllowedSeverities == null || policy.AllowedSeverities.Count == 0)
Fail($"{d.RuleCode} 的 AllowedSeverities 不能为空");
if (!policy.AllowedSeverities!.Contains(policy.SeverityDefault, StringComparer.Ordinal))
Fail($"{d.RuleCode} 的默认严重度 {policy.SeverityDefault} 不在允许集合内");
}
}