chronaxis.core.temporal
源码基线:8904c2f744951857659abd40aaa3535d57faf53c
1"""保存条件在各时刻的证据、检测规则及已确认事件,供两个检测后端共用。23输入为整数纳秒区间、条件状态、实际更新与观测结束证据;输出不可变且经过4检查的模型。TRUE和FALSE必须没有结果质量原因,UNKNOWN必须保留原因。5连续区间左闭右开;真实末点可单独观察,但不延长活动。来源或会话输入结束6不自动等于条件变假。事件以稳定内容生成标识,并保存SourceRef用于追溯。7本模块只依赖标准库及核心模型,不读取来源、不执行检测、不持有规则状态。8"""910# 冻结模型与默认字段便于保留显式证据。11from dataclasses import dataclass12# 稳定内容身份,不用秒数文件名充当事件ID。13from hashlib import sha25614# 已有规范身份与来源定位结构,不读旧资源。15from .binding import _identity16# 区间模型已检查跨度和来源引用结构。17from .models import Interval, SourceRef18# 三值状态不得隐式bool化。19from .types import QualityFlag as Q, Truth20# 数据标识规则复用,无静默改名。21from .validation import require_identifier, require_text22# 所有时间仍为显式int64纳秒。23from ..timebase.conversion import require_int64_ns242526def _require_truth(state, quality):27 """检查已知结果无质量原因与UNKNOWN有原因的基础契约。2829 参数: state为Truth,quality为已知Q组合。30 返回: None,不把UNKNOWN变False。31 异常: TypeError/ValueError为类型或质量矛盾。32 """33 # 已知结果不能携带基础策略禁止的质量位。34 if not isinstance(state, Truth) or not isinstance(quality, Q):35 # 裸bool或任意质量数值不能替代经过检查的条件证据。36 raise TypeError('条件需要Truth及QualityFlag')37 # unknown原因不能空,正常真/假质量只能NONE。38 if (state is Truth.UNKNOWN) != (quality != Q.NONE):39 # 诊断与结果质量通过独立字段保留。40 raise ValueError('条件truth与quality矛盾')414243@dataclass(frozen=True)44class ConditionSpan:45 """保存条件连续成立、不成立或无法确定的一段纳秒区间。4647 start_ns包含起点,end_ns排除终点;区间必须为正长度且跨度可由int64表示。48 state和quality共同说明条件及未知原因。模型不可变,不根据消息周期补齐间隙。49 """5051 start_ns: int # 连续区间包含起点。52 end_ns: int # 连续区间不包含终点。53 state: Truth # 真/假/未知,不靠数值truthiness。54 quality: Q = Q.NONE # UNKNOWN原因,known为NONE。5556 def __post_init__(self):57 """检查正区间与truth质量,禁止补齐缺观测。5859 参数: 无;读取四个字段。60 返回: None,保持原声明。61 异常: TypeError/ValueError/OverflowError为结构/区间/质量。62 """63 # 复用已有半开时间区间不变量。64 interval = Interval(self.start_ns, self.end_ns)65 # 零长度不是连续条件区间。66 if interval.end_ns == interval.start_ns:67 # 真实点由ConditionTick.point_state另表达。68 raise ValueError('ConditionSpan需正长度')69 # 区间状态和原因严格一致。70 _require_truth(self.state, self.quality)717273@dataclass(frozen=True)74class ConditionTick:75 """保存一个完整时间组的连续状态、点状态及实际更新证据。7677 state描述从当前时刻到下一时刻的连续状态;point_state描述当前点,包括真实78 末点。actual_unknown表示真实更新明确导致点结果未知,用于区分实际中断与79 同刻发生的有效期到期。observation_end和terminal分别表示依赖来源及会话80 观测结束。字段不可变,默认点状态继承连续状态,不制造实际更新。81 """8283 time_ns: int # 已归一化int64纳秒。84 state: Truth # 连续右侧状态,不扩展终点。85 quality: Q = Q.NONE # 连续unknown原因。86 actual_update: bool = True # 完整组确有条件依赖更新/明确条件证据。87 observation_end: bool = False # 当前有依赖来源观测结束。88 terminal: bool = False # 当前会话真实连续终点,只能全输入末组。89 point_state: Truth | None = None # None表示与continuous同证据。90 point_quality: Q | None = None # None采用continuous质量。91 actual_unknown: bool = False # 实际无效/实际更新导致未知,优先interrupted。92 diagnostic_quality: Q = Q.NONE # 全部依赖和边界原因,保留原异常诊断。9394 def __post_init__(self):95 """规范默认点证据并检查actual/terminal/quality结构。9697 参数: 无,读取上述字段。98 返回: None,默认point只继承显式state,不制造新更新。99 异常: TypeError/ValueError为类型、状态或实际未知证据冲突。100 """101 # 精确纳秒,bool不作整数时间。102 require_int64_ns(self.time_ns)103 # 连续known/unknown契约独立校验。104 _require_truth(self.state, self.quality)105 # 每个证据标志严格bool。106 for value in (self.actual_update, self.observation_end, self.terminal, self.actual_unknown):107 # 不用任意字符串或整数真值推测证据。108 if type(value) is not bool:109 # 结构错误先于检测。110 raise TypeError('时序证据标志需bool')111 # 未提供point时继承当前明确状态。112 object.__setattr__(self, 'point_state', self.state if self.point_state is None else self.point_state)113 # 未提供point原因时只继承连续原因。114 object.__setattr__(self, 'point_quality', self.quality if self.point_quality is None else self.point_quality)115 # point与continuous质量政策相同,但范围语义不同。116 _require_truth(self.point_state, self.point_quality)117 # 诊断也需已知组合,不得存任意负位。118 if not isinstance(self.diagnostic_quality, Q):119 # 不静默cast非法mask。120 raise TypeError('diagnostic_quality需要QualityFlag')121 # 明确actual_unknown只能针对真正更新的点未知。122 if self.actual_unknown and (not self.actual_update or self.point_state is not Truth.UNKNOWN):123 # 有效期到期或观测结束而没有实际更新证据,不能算作实际中断。124 raise ValueError('actual_unknown需实际更新且点UNKNOWN')125126127@dataclass(frozen=True)128class TemporalRule:129 """声明一个数据集会话中的持续条件或边沿检测规则。130131 rule_type接受condition_duration、rising_edge、falling_edge和start_only。minimum_duration_ns是非负int64阈值,132 边沿规则必须为0;rising_edge/start_only可用emit_initial输出首次已知TRUE初始化点。133 start_only的window_duration_ns必须为正,真实触发后的固定时长内抑制新边沿。134 对象不可变,只保存配置,不初始化检测器状态。135 """136137 rule_id: str # 稳定规则ID,与同名不同会话隔离。138 dataset_id: str # 事件ID范围,不按秒数命名。139 session_id: str # 当前分析会话。140 rule_type: str # condition_duration/rising_edge/falling_edge/start_only实际已实现白名单。141 minimum_duration_ns: int = 0 # condition_duration阈值;edge必须0。142 emit_initial: bool = False # rising_edge/start_only首次已知TRUE初始化策略。143 window_duration_ns: int = 0 # 仅start_only的固定抑制时长,其他模式必须0。144 duration_comparison: str = '>=' # 持续时长门槛:达到或严格超过;不改变实际起止。145146 def __post_init__(self):147 """校验规则范围、非负时间及基础rule_type参数。148149 参数: 无,读取规则字段。150 返回: None,不初始化任何全局运行状态。151 异常: TypeError/ValueError/OverflowError为配置错误。152 """153 # 独立命名范围不使用任意路径作为ID。154 for field in ('rule_id', 'dataset_id', 'session_id'):155 # 复用现有简单identifier协议。156 require_identifier(getattr(self, field), field)157 # 阈值精确ns,不猜秒或消息周期。158 require_int64_ns(self.minimum_duration_ns, 'minimum_duration_ns')159 # 严格比较只属于持续规则;其他规则不能携带无效开关。160 if self.duration_comparison not in ('>=', '>') or (self.rule_type != 'condition_duration' and self.duration_comparison != '>='):161 raise ValueError('duration_comparison仅持续条件支持>=或>')162 # 固定窗口和持续确认门槛不同,不能复用一个字段猜旧模式意义。163 object.__setattr__(self, 'window_duration_ns', require_int64_ns(self.window_duration_ns, 'window_duration_ns'))164 # 不负时长/隐式bool策略/未实现kind。165 if self.minimum_duration_ns < 0 or type(self.emit_initial) is not bool or self.rule_type not in ('condition_duration', 'rising_edge', 'falling_edge', 'start_only'):166 # sequence使用独立规则模型,不由本模型承载。167 raise ValueError('非法首版时序规则/阈值/初始化策略')168 # edge不能暗带持续门槛,falling_edge/condition_duration无初始化点参数。169 if self.rule_type != 'condition_duration' and self.minimum_duration_ns != 0 or self.emit_initial and self.rule_type not in ('rising_edge', 'start_only'):170 # 参数不混为旧模式的不同condition_duration。171 raise ValueError('规则参数与kind不匹配')172 # start_only窗口为正;其他kind不能暗带抑制或改变已有事件ID。173 if (self.rule_type == 'start_only' and self.window_duration_ns <= 0) or (self.rule_type != 'start_only' and self.window_duration_ns != 0):174 raise ValueError('window_duration_ns仅且必须用于正时长start_only')175176 @property177 def qualifying_duration_ns(self):178 """返回最早满足比较的整数纳秒时长,不改变用户声明的门槛。179180 参数: 无。返回: Python整数;严格大于最大int64时可超出int64,表示域内不可达到。181 异常: 无。时间模型精度为1ns,因此>60s的首个可表示时长为60000000001ns;182 这不是把用户阈值粗略提高到60.1秒,固定/DBC模式仍只随实际驱动记录确认。183 """184 return self.minimum_duration_ns + (self.duration_comparison == '>')185186187@dataclass(frozen=True)188class Event:189 """保存一段已确认活动或一个真实触发点,并关联检测规则和原来源。190191 point事件的start_ns等于confirmed_ns,end_ns为None,status为point。192 activity事件为正长度左闭右开区间;confirmed_ns可以恰好等于end_ns。193 活动的closed表示条件已知结束,interrupted表示连续性中断,truncated表示194 观测结束而没有实际结束证据。边界、质量、诊断和来源引用均不可变。195 timeout表示开始/结束规则在结束条件可判断时达到时限,结束质量必须为NONE。196 """197198 event_id: str # 按稳定内容SHA,不依批次/遍历次序。199 rule: TemporalRule # 完整规则及dataset/session身份。200 event_type: str # point或activity。201 start_ns: int # 真实onset或点时间。202 confirmed_ns: int # 精确门槛时间,不当活动起点。203 end_ns: int | None # point为None,activity结束边界。204 status: str # closed/timeout/interrupted/truncated/point;timeout用于开始结束时限。205 trigger_kind: str # condition_duration/rising_edge/falling_edge/initial。206 quality: Q # 结束原因质量,不用0恢复失败。207 dependency_quality: Q # 期间及结束证据的完整诊断。208 source_refs: tuple[SourceRef, ...] = () # 真实绑定原来源定位,合成span可空。209210 def __post_init__(self):211 """检查事件时间不变量、范围与来源,保留确认=end。212213 参数: 无,读取事件字段。214 返回: None,不修改边界或生成片段。215 异常: TypeError/ValueError/OverflowError为事件结构/时间/身份错误。216 """217 # 正常输出须携带真实规则/质量及来源容器。218 if not isinstance(self.rule, TemporalRule) or not isinstance(self.quality, Q) or not isinstance(self.dependency_quality, Q) or not isinstance(self.source_refs, tuple):219 # 不接无类型事件字典作为完成事实。220 raise TypeError('Event字段需typed模型')221 # start/confirmed都是明确ns。222 require_int64_ns(self.start_ns, 'start_ns')223 # 确认不能先于真实onset。224 require_int64_ns(self.confirmed_ns, 'confirmed_ns')225 # ID需真实非空文本,稳定SHA由make_event产生。226 require_text(self.event_id, 'event_id')227 # 缺数据不能伪装已证实的超时;依赖诊断仍可保留不影响结束判断的原因。228 if self.status == 'timeout' and self.quality != Q.NONE:229 raise ValueError('timeout不能带未知结束质量')230 # 点事件只有单时刻,无连续范围。231 if self.event_type == 'point':232 # 新模式的触发种类与规则一致,不允许自由组装相反边沿或伪初始化。233 if self.rule.rule_type == 'start_only' and (self.trigger_kind not in ('start_only', 'initial') or self.trigger_kind == 'initial' and not self.rule.emit_initial) or self.rule.rule_type != 'start_only' and self.trigger_kind == 'start_only':234 raise ValueError('start_only点触发种类与规则不匹配')235 # 初始点/边沿都不能增面积。236 if self.status != 'point' or self.end_ns is not None or self.start_ns != self.confirmed_ns or self.trigger_kind not in ('rising_edge', 'falling_edge', 'initial', 'start_only') or self.rule.rule_type == 'condition_duration' or self.quality != Q.NONE:237 # 不把point伪装零长度活动。238 raise ValueError('point事件边界/类型矛盾')239 else:240 # 活动严格正长,已确认且终点可相等。241 if self.event_type != 'activity' or self.status not in ('closed', 'timeout', 'interrupted', 'truncated') or self.trigger_kind != 'condition_duration' or self.end_ns is None or self.rule.rule_type != 'condition_duration':242 # 不输出内部pending/active占位。243 raise ValueError('activity事件结构非法')244 # 区间跨度也需合法int64。245 interval = Interval(self.start_ns, self.end_ns)246 # 终点真实新TRUE不得建立零长度活动。247 if interval.start_ns == interval.end_ns or not self.start_ns <= self.confirmed_ns <= self.end_ns:248 # 门槛恰好end仍合法确认。249 raise ValueError('activity确认/连续边界矛盾')250 # 来源属于该会话,不能继承其他任务引用。251 for ref in self.source_refs:252 # 合成ConditionSpan允许无原SourceRef,但非空必须真实typed。253 if not isinstance(ref, SourceRef) or ref.session_id != self.rule.session_id:254 # 不接受仅同名来源作为归属证明。255 raise ValueError('Event来源与session不一致')256257258def make_event(rule, start_ns, confirmed_ns, end_ns, status, quality, diagnostic, refs=(), *, trigger='condition_duration') -> Event:259 """根据规则和时间事实生成稳定事件标识,再构造已确认事件。260261 参数:262 rule (TemporalRule): 当前检测规则,包含数据集和会话身份。263 start_ns、confirmed_ns (int): 活动起点和确认时间,或触发点时间,单位为纳秒。264 end_ns (int | None): 活动结束时间,单位为纳秒;点事件使用None。265 status (str): closed、timeout、interrupted、truncated或point。266 quality、diagnostic (QualityFlag): 结束质量和完整依赖诊断。267 refs (tuple[SourceRef, ...]): 当前实际来源引用,默认空tuple。268 trigger (str): 触发种类,默认condition_duration,边沿或初始化点另行声明。269 返回:270 Event: 通过模型检查的事件。标识由规则及时间事实生成,不受批次切分、271 质量诊断或可选语义元数据变化影响。272 异常:273 TypeError、ValueError或OverflowError: 事件模型或身份材料不符合约束。274 副作用:275 无,不加载来源,不修改规则或检测器状态。276 """277 # 规则范围和事件内容稳定,质量/metadata变化不冒充另一时间事实。278 payload = (rule.dataset_id, rule.session_id, rule.rule_id, rule.rule_type, rule.minimum_duration_ns, rule.emit_initial, start_ns, confirmed_ns, end_ns, status, trigger)279 # 仅新模式增加固定窗口身份材料;已有三个kind的SHA内容逐字保持。280 if rule.rule_type == 'start_only':281 payload += (rule.window_duration_ns,)282 # 旧默认事件ID保持;严格比较作为新的规则身份,避免同端点混淆。283 if rule.duration_comparison != '>=':284 payload += (rule.duration_comparison,)285 # 完整SHA避免同起止秒数/不同规则碰撞。286 identity = sha256(_identity(payload).encode('utf-8')).hexdigest()287 # source_refs保留当前真实来源,不参与规则时间事实的隐式改写。288 return Event(identity, rule, 'point' if status == 'point' else 'activity', start_ns, confirmed_ns, end_ns, status, trigger, quality, diagnostic, refs)289290291@dataclass(frozen=True)292class ConditionTimeline:293 """保存一个完整会话的条件时间轴,检测器可以按批消费其中的时间组。294295 首末时间组必须对应start_ns和end_ns;最后一组是唯一的terminal。时间严格296 递增,同刻更新已经归并;模型不会排序或补齐缺口。source_refs保存原来源,297 与规则会话的归属关系由检测器检查。批次切分不改变条件证据。298 """299300 start_ns: int # 连续分析起点。301 end_ns: int # 连续分析终点,真实点仍可观察。302 ticks: tuple[ConditionTick, ...] # 完整时间组,含TTL/source/EOF证据。303 source_refs: tuple[SourceRef, ...] = () # 实际condition依赖源定位。304305 def __post_init__(self):306 """检查时间轴完整性,不填充未知间隙或末周期。307308 参数: 无,读取domain/ticks/source_refs。309 返回: None,保持显式时间证据。310 异常: TypeError/ValueError/OverflowError为结构/边界/顺序。311 """312 # 域跨度必须可表示,零域只含真实末点。313 Interval(self.start_ns, self.end_ns)314 # 不能用可变列表或空轴冒充完整会话。315 if not isinstance(self.ticks, tuple) or not self.ticks or not isinstance(self.source_refs, tuple):316 # 空观测可显式一个terminal UNKNOWN点,不自动补。317 raise TypeError('timeline需非空tuple ticks及tuple refs')318 # typed组本身已检查continuous/point质量。319 if any(not isinstance(tick, ConditionTick) for tick in self.ticks):320 # 不接自由字典作为完整证据。321 raise TypeError('timeline需要ConditionTick')322 # 真实domain首末完整,不丢EOF边界。323 if self.ticks[0].time_ns != self.start_ns or self.ticks[-1].time_ns != self.end_ns or not self.ticks[-1].terminal:324 # 保证检测finish只发生真正会话末点。325 raise ValueError('timeline首末/terminal与domain不一致')326 # 全轴严格递增,原子同时间组已在上游收齐。327 for index, tick in enumerate(self.ticks):328 # terminal只能出现在真实末组。329 if index < len(self.ticks)-1 and tick.terminal or index and tick.time_ns <= self.ticks[index-1].time_ns:330 # 不将batch/file尾当EOF或排序掩盖回退。331 raise ValueError('timeline中途terminal/时间回退/重复组')332 # 来源结构真实typed,归属稍后与rule.session核对。333 if any(not isinstance(ref, SourceRef) for ref in self.source_refs):334 # 不用名字字符串冒充定位。335 raise TypeError('timeline source_refs需SourceRef')336337338def timeline_from_spans(spans: tuple[ConditionSpan, ...], *, terminal_state: Truth | None = None,339 terminal_quality: Q = Q.NONE, terminal_actual: bool = False) -> ConditionTimeline:340 """把完整手算半开区间转换为明确边界证据,不伪造EOF FALSE。341342 参数: spans为正长度、相邻、完整tuple;terminal_state/quality为可选真实343 末点状态,terminal_actual明确是否真实更新,默认EOF沿用历史点状态。344 返回: ConditionTimeline;末端continuous UNKNOWN/MISSING,仅point保留证据。345 异常: TypeError/ValueError为结构/缺口/point更新声明矛盾。346 """347 # 合成条件资料也必须typed完整,不填补missing域。348 if not isinstance(spans, tuple) or not spans or any(not isinstance(item, ConditionSpan) for item in spans):349 # 零域用ConditionTick直接声明,无正区间不能猜范围。350 raise TypeError('spans需要非空tuple ConditionSpan')351 # 手算明确条件更新可作为边沿证据。352 ticks = []353 # 区间边界是静态输入协议,不是参考/数组状态算法。354 for index, span in enumerate(spans):355 # 间隙/重叠都不能静默补齐或合并真实活动。356 if index and spans[index-1].end_ns != span.start_ns:357 # 用显式UNKNOWN span表达缺口。358 raise ValueError('ConditionSpan需完整相邻,不自动填充')359 # 给定边界是真实状态证据;unknown说明实际无效。360 ticks.append(ConditionTick(span.start_ns, span.state, span.quality, actual_unknown=span.state is Truth.UNKNOWN, diagnostic_quality=span.quality))361 # 默认末点沿用最后明示span的历史状态,不制造更新。362 point_state = spans[-1].state if terminal_state is None else terminal_state363 # 未覆写point时保留末span的原质量。364 point_quality = spans[-1].quality if terminal_state is None else terminal_quality365 # 真实末点覆写必须明确actual,不能冒充纯EOF。366 if terminal_actual and terminal_state is None:367 # 默认历史值不算新末点证据。368 raise ValueError('terminal_actual需明确terminal_state')369 # 连续EOF排除而point包含,真实false/invalid优先结束事实。370 ticks.append(ConditionTick(spans[-1].end_ns, Truth.UNKNOWN, Q.MISSING, terminal_actual, True, True,371 point_state, point_quality, terminal_actual and point_state is Truth.UNKNOWN, Q.MISSING | point_quality))372 # 域与完整时间组由模型统一校验。373 return ConditionTimeline(spans[0].start_ns, spans[-1].end_ns, tuple(ticks))374