# Scheduler Module Rules

## 适用范围

适用于 `src/host/scheduler/`。本文件定义定时任务模块特有的稳定治理边界与修改规则；通用规范遵循根 `AGENTS.md`，测试规范遵循 `tests/AGENTS.md`。

本文件描述治理原则，不是源码结构说明。文件移动、重命名或内部重构后，只要职责、事实源、依赖方向与对外契约未变，本文件不随之修改。

## 核心职责

按用户配置的**执行窗口**与**触发策略**，驱动既有编排运行时运行所选工作流模板的实例：

- 任务配置的校验、规范化与持久化；
- 时间语义：时区换算、触发点推算、窗口判定；
- 决策与推进：何时触发、何时挂起、何时续跑、何时跳过；
- 触发时的实例化语义（模板 → 实例）与会话策略分派。

判断归属：该修改是否改变**定时任务的配置契约、时间语义或触发/挂起/续跑决策**？若主要属于时间基元之外的其他领域（编排推进、Agent 语义、持久化原语、UI 展示、HTTP 端点），应放到对应模块。

## 非职责

- 不实现编排状态机、运行锁与断点续跑机制——这些归编排运行时，本模块只**调用**其既有能力；
- 不实现会话与 Agent 的创建/恢复机制——那属官方运行时能力适配层；
- 不拥有持久化原语（原子写、锁、崩溃恢复）——只按自己的数据文件复用它们；
- 不决定 HTTP 语义（状态码、端点形态）与用户界面；
- 不直接修改其他模块的内部状态。

## 时间语义契约

时间语义必须确定、可测、与时区显式绑定：

- 时间的解析/格式化/换算/推算一律为**纯函数**：相同输入 → 相同输出，不读系统时钟、不读环境时区、不依赖全局状态；需要"当前时刻"时经参数传入；
- 绝对时间在存储与比较层面统一用 UTC 毫秒；用户可见的时刻（"HH:mm"、日期）一律按任务自身声明的时区解释——**禁止**使用进程本地时区或隐式时区；
- 跨天区间（结束不晚于开始）必须与跨天触发点、跨日期窗口扫描的语义一致，不得各处自行解释；
- 调度语义分两层且语义固定：第一层「执行窗口」决定何时允许运行，第二层「触发策略」决定何时应当触发；**触发成立的条件是两层同时满足**，不得用其中一层代替另一层。

## 单一事实源

同一概念只允许一处本体：

- 时间解析、时区换算、窗口判定、触发点推算各自只有一处实现，使用方引用本体而非重写；
- 任务字段的**形状收敛**、**业务校验**、**保存前规范化**是三个独立步骤，各有唯一实现且顺序固定（收敛 → 校验 → 规范化）；调用方不得自行拼装任务实体；
- 任务的持久化形态（含运行时字段）只有一处定义，读写经同一条通道。

## 子域边界

模块内部存在变化原因不同的子域：时间基元、窗口判定、触发计算、任务配置契约、持久化、决策引擎、实例化。

- 子域之间只通过明确的输入输出协作，不得互相承担对方职责；
- 时间基元不理解"任务"；窗口判定与触发计算不理解持久化；配置契约不读盘、不决策；决策引擎不重复实现时间算法；
- 新增能力先判断属于哪个子域，再放到该子域的职责中，不得为就近而堆进已有位置。

## 状态与生命周期

- 本模块持有两类状态：**磁盘上的任务定义与运行时游标**（跨重启保留）与**进程内的引擎运行态**（定时器、内存缓存、启动基准）；
- 进程内运行态归引擎实例所有，随实例的启动/停止/清理走完整生命周期；任何外部模块不得直接读写它；
- 启动必须幂等；停止/清理后不得再启动（终止即终止，不得因再次调用而复活）；
- 定时扫描必须**可重入安全**：扫描耗时可能超过扫描间隔，重叠触发时必须合并为一次扫描，禁止对同一任务并发决策（否则会出现重复触发、重复挂起、游标互相覆盖）；
- 单任务失败不得影响其余任务：失败必须记录到该任务的可观测结果上，然后继续；
- 丢弃触发（并发策略）必须**只推进游标而不改变运行**，不得中断正在执行的轮次。

## 副作用显式

本模块的副作用只有：读/写自己的任务文件、创建会话、启动/挂起/续跑编排运行、写日志。

- 副作用必须出现在有明确职责的位置，不得藏进看似纯计算的辅助函数；
- 纯计算与状态修改必须分离：决策所需的全部事实由调用方或参数传入，决策本身不产生副作用；
- 外部能力（编排运行时、数据层、会话创建、时钟、日志、扫描间隔）一律经**显式依赖缝**注入，便于替换与测试；本模块不得直接 import 具体运行时实现。

## 持久化边界

- 任务文件只有一个写者（本模块的存储实现）；其它模块不得绕过它写同一文件；
- 读改写必须在同一临界区内完成；运行时字段的更新必须合并写，不得整文件覆盖导致并发丢更新；
- 磁盘状态与进程内状态不一致时，以「可恢复的最小假设」处理（例如游标缺失按启动基准推进），且该假设必须可在注释与测试中追溯。

## 公共边界

- 模块外只允许经模块的**公共入口**导入，禁止直接引用模块内部文件；
- 只把真实、稳定、可维护的契约放进公共入口（引擎与其依赖缝、存储、任务配置契约、稳定的时间/窗口/触发纯函数、实例化）；
- 内部实现（决策的状态迁移细节、任务文件的磁盘形态与读取守卫、内存缓存与定时器结构）不进入公共入口；禁止「全量 barrel」；
- 新增公共符号前先判断它是否表达对外稳定契约；放宽入口比收紧入口的代价更高。

## 依赖边界

- 只允许依赖共享契约层（任务实体与窗口/触发配置的形状）与本模块内部；
- 不得依赖 remote / tools / storage 的具体实现 / agent / client；
- 对外部能力的依赖必须是抽象缝（接口 + 注入），方向单向：调用方依赖本模块的决策，本模块不反向依赖任何调用方。

## 契约稳定性

- 任务字段、运行态字段、状态枚举与"最近一次结果"取值属公共契约：修改必须同步调用方、测试与文档，并说明旧数据的读取行为；
- 字段增删遵循兼容优先：新增字段默认可选；删除或改义必须给出兼容路径与剥除时机；
- 时间语义变化（例如"什么算窗口内""跨天如何解释""下一触发点如何取"）属行为变化，必须记录：旧行为 → 新行为 → 原因 → 影响范围 → 测试变化；
- 会话工作目录的决策（显式路径优先 / 继承创建者）只有一处实现，调用方不得复制该决策；显式路径的**存在性校验属接受该配置的保存端点**，运行期不重复校验——该不变式必须在实现处写明。

## 模块内部组织

- 文件结构不是架构契约，禁止为"看起来模块化"机械拆分；拆分依据只能是职责、变化原因、依赖边界与测试边界；
- 文件命名必须表达**稳定职责与所属维度**（例如同族概念保持一致的命名维度），名称失真的文件应立即重命名并同步全部引用；
- 不得创建 `utils.ts`、`helpers.ts`、`common.ts`、`shared.ts`、`misc.ts` 等无明确职责的聚合文件；
- 内部文件之间使用相对路径导入，公共入口不含实现。

## 修改前检查

1. 改动属于时间基元、窗口判定、触发计算、任务配置契约、持久化、决策引擎还是实例化？
2. 是否改变时间语义或触发/挂起/续跑决策？受影响的调用方与测试是哪些？
3. 是否新增/修改任务字段或运行态字段？兼容路径与测试是否同步？
4. 是否引入非确定性依赖（时钟 / 随机 / 环境时区 / IO / 全局状态）？
5. 是否产生第二份事实源（重复的时间算法、重复的字段收敛、重复的持久化形态）？
6. 是否越界进入其他模块职责（编排状态机、会话创建、HTTP 语义）？

## 验证

除根 `AGENTS.md` 的标准命令外，本模块改动必须额外确认：

- 时间相关断言全部使用**固定日期与时区**，不依赖运行机器的本地时区与当前时间；
- 窗口边界、跨天区间、跨日期扫描、无解配置（永不触发）等边界行为均有测试锁定；
- 决策路径的关键分支（触发 / 触发失败 / 并发跳过 / 窗口挂起 / 窗口续跑 / 用户接管 / 停用 / 重启游标）均有测试锁定；
- 重叠扫描、重复启动、终止后再启动等并发与生命周期行为有测试锁定；
- 公共入口导出面未意外扩大、无旧路径残留、无循环依赖、无重复实现。

## 核心原则

1. 时间语义确定且时区显式，纯计算不读时钟。
2. 窗口与触发两层语义固定，缺一不触发。
3. 收敛、校验、规范化各有唯一实现且顺序固定。
4. 扫描可重入安全，终止后不复活。
5. 外部能力经显式依赖缝注入，副作用不藏于纯函数。
6. 模块外只经公共入口协作，内部实现不外溢。
7. 语义变化必须显式记录并锁定测试。
