审查日期:2026-06-15 审查范围:RFC 0001 — 0004 vs 当前 packages/primitives 与 packages/temporal 全部源码
| 审查维度 | 合规率 | 缺项数 | 问题数 |
|---|---|---|---|
| 0004 primitives API | 100% | 0 | 0 |
| 0003 temporal 模型 | 100% | 0 | 0 |
| 0002 工程工具链 | 100% | 0 | 0 |
| 0001 架构原则 | 100% | 0 | 0 |
| 第二阶段包 | — | 10 项(已在计划中覆盖) | 0 |
| 系统性发现 | — | — | 4 项(低严重度) |
结论:当前的代码与 RFC 要求高度一致。项目处于良好状态。当前阶段边界以
docs/plans/ 为准,执行状态与任务拆分以 docs/tasks/ 为准。
| 要求 | 实现位置 | 状态 |
|---|---|---|
| Angle 五单位构造(度/弧度/turn/角分/角秒) | angle.ts 5 组 fromXxx / parseXxx |
✅ |
| Angle 五种单位输出 | angle.ts toDegrees/toRadians/toTurns/toArcminutes/toArcseconds |
✅ |
| Angle 四种归一化 | normalizeDegrees [0,360), normalizeRadians [0,2π), normalizeTurns, normalizeSignedDegrees [-180,180) |
✅ |
| 构造时不自动归一化 | 构造器直接赋值,不调用 normalize | ✅ |
| Duration 固定单位 | duration.ts 秒/毫秒/日/儒略年/儒略世纪 |
✅ |
| 1 日 = 86400 秒、1 儒略年 = 365.25 日 | duration.ts L6-L9 硬编码常量 |
✅ |
| Result 类型及守卫 | result.ts Ok/Err + ok/err/isOk/isErr |
✅ |
| Tolerance 类型 | tolerance.ts { readonly absolute: number; readonly relative?: number } |
✅ |
| almostEqual 函数 | tolerance.ts 含 tolerance 合法性校验 |
✅ |
| NaN/Infinity 拒绝 | internal/number.ts assertFiniteNumber |
✅ |
| fromXxx / parseXxx 双层 API | Angle 和 Duration 全部构造均提供 | ✅ |
| PrimitiveError 含 DivisionByZero / InvalidNumber / InvalidTolerance | errors.ts L2 |
✅ |
| 无运行时第三方依赖 | package.json dependencies 为空 |
✅ |
| 不反向依赖 temporal | 无 import from "@epheon/temporal" |
✅ |
| 公共 API 中文 TSDoc 含 @param/@returns/@throws | 所有公共函数和方法 | ✅ |
| 内部模块不导出到公共入口 | index.ts 不导出 internal/ |
✅ |
| 测试优先读 standards/ fixture | angle.test.ts / duration.test.ts |
✅ |
第一阶段暂缓的 Interval 和 Maybe 未导出,与 RFC 一致。
| 要求 | 实现位置 | 状态 |
|---|---|---|
| Instant 类 | instant.ts fromUTC / parseUTC / toJulianDay / toJulianEphemerisDay / toTT / toUT1 / toUTCDateTime / toUTCFields |
✅ |
| fromUTC 抛 TemporalError | instant.ts L66 |
✅ |
| parseUTC 返回 Result | instant.ts L79 |
✅ |
| 不可变值对象 | private #fields、无 setter |
✅ |
| JulianDay 值对象 | julian-day.ts fromNumber / parseNumber / toNumber |
✅ |
| JulianEphemerisDay 值对象 | julian-day.ts fromNumber / parseNumber / toNumber |
✅ |
| DeltaTProvider 函数类型 | providers.ts (instant: Instant) => Duration |
✅ |
| LeapSecondProvider 函数类型 | providers.ts (input: UtcDateTime) => number |
✅ |
| fixedLeapSeconds / fixedDeltaT | providers.ts L27-L39 |
✅ |
| 时间尺度转换纯函数 | internal/time-scale.ts 三个导出纯函数 |
✅ |
| UTC 字符串解析 | internal/utc-parser.ts ISO 8601 + Z 或 ±HH:mm |
✅ |
| Gregorian 日期校验 | internal/gregorian.ts assertGregorianDateTime |
✅ |
| Gregorian → JD | internal/gregorian.ts gregorianToJulianDay |
✅ |
| 拒绝无 offset 输入 | 正则要求末尾必须 Z 或 ±HH:mm | ✅ |
| missing provider 抛对应错误 | MissingLeapSecondProvider / MissingDeltaTProvider | ✅ |
| provider 抛错收敛为 InvalidTimeScaleInput | instant.ts readOptionalLeapSeconds |
✅ |
| 5 种 TemporalError 错误码 | errors.ts L2-L7 |
✅ |
| 不读系统时区 | 无 Date.now / Intl / process.tz | ✅ |
| 不读当前时间 | 无隐式时间读取 | ✅ |
| 测试读 standards/ fixture | instant.test.ts / time-scale.test.ts / julian-day.test.ts |
✅ |
| 内部实现不泄漏到公共 API | index.ts 导出的全是公共类型 |
✅ |
| 要求 | 配置文件/位置 | 状态 |
|---|---|---|
| pnpm workspace | pnpm-workspace.yaml packages: ["packages/*"] |
✅ |
| TypeScript strict mode | tsconfig.json strict true, exactOptionalPropertyTypes true, noUncheckedIndexedAccess true |
✅ |
| ESM + CJS 双格式 | tsdown.config.ts format: ["esm", "cjs"] |
✅ |
| tsdown 构建 | tsdown.config.ts in each package |
✅ |
| Vitest workspace | vitest.config.ts projects: ["packages/*"] |
✅ |
| Moonrepo 任务编排 | .moon/workspace.yml, tasks/all.yml, packages/*/moon.yml |
✅ |
| Oxlint 代码检查 | .oxlintrc.json |
✅ |
| Oxfmt 格式化 | .oxfmtrc.json |
✅ |
| Lefthook pre-commit | lefthook.yml |
✅ |
| Changesets 发布 | .changeset/config.json |
✅ |
| EditorConfig | .editorconfig |
✅ |
| GitHub CI | .github/workflows/ci.yml |
✅ |
| Renovate 依赖更新 | .github/renovate.json |
✅ |
| 开发期 exports 指向源码 | "exports": { ".": "./src/index.ts" } |
✅ |
| publishConfig 分离 | publishConfig.exports 指向 dist/ |
✅ |
| standards/ 含 fixture | 5 个 JSON 文件 | ✅ |
| 测试使用显式 tolerance | expectAlmostEqual with DAY_TOLERANCE / SECOND_TOLERANCE |
✅ |
| 原则 | 体现 | 状态 |
|---|---|---|
| 值对象 OOP + 纯函数 FP | Angle/Duration/Instant = class;time-scale = 纯函数 | ✅ |
| Provider 注入外部数据 | DeltaTProvider / LeapSecondProvider | ✅ |
| 算法与数据分离 | 数据在 standards/,代码在 packages/ |
✅ |
| 客观层与主观层分离 | primitives/temporal 无术数语义 | ✅ |
| 不可变性 | readonly #field + private constructor |
✅ |
| Result 表达可恢复错误 | Result<T, TemporalError> |
✅ |
| 精度显式声明 | Tolerance type |
✅ |
| 无隐式默认精度 | almostEqual 必须传 tolerance | ✅ |
| 无深层 class 继承 | 无 extends,无 abstract base | ✅ |
| 测试显式 tolerance | expectAlmostEqual |
✅ |
以下均为 RFC 0001 中列出的长期目标但在第一阶段明确暂缓的能力,不需要在当前阶段 实现。
| 缺项 | 计划对应任务 | 前置条件 |
|---|---|---|
| Vector3 | A4 | 无,可立即执行 |
| Polynomial | A5 | 无,可立即执行 |
| RootFinder | A6 | 无,可立即执行 |
@epheon/reference 包 |
C1 | RFC 0005 定稿 |
@epheon/ephemerides 抽象接口包 |
C2 | RFC 0006 定稿 |
| 真实 Delta-T 数据 | C3 | RFC 0008 定稿 |
| 真实闰秒数据 | C3 | RFC 0008 定稿 |
| 太阳黄经实现 | C4 | C2 完成 |
| 节气求解 | C5 | C4 完成 |
| 朔望求解 | C6 | C4 + 月亮算法完成 |
| 中国历法规则包 | C7 | C5 + C6 完成 |
文件: packages/temporal/src/internal/time-scale.ts L46
/**
* @throws PrimitiveError 当相减结果为 NaN 或 Infinity 时抛出,错误码为 InvalidNumber。
*/ttMinusUtcToUT1MinusUtc 内部调用 Duration.subtract(),后者可能抛出
PrimitiveError。但 time-scale.ts 是 temporal 包的 internal 模块,不应该
在自己的 TSDoc 中承诺 primitives 的错误类型。调用方 Instant.toUT1() 已经把
所有异常包装成 TemporalError。
影响: 阅读这个函数 TSDoc 的开发者会误以为它直接暴露 PrimitiveError。
实际上对外表现是 TemporalError(InvalidTimeScaleInput)。
建议修复: 把 @throws 行改为描述实际行为,例如 @throws TemporalError 当 Duration.subtract 内部检测到非有限计算结果时抛出,错误码为 InvalidTimeScaleInput。
严重度: 低。
文件:
packages/primitives/src/internal/number.ts— 固定错误码InvalidNumberpackages/temporal/src/internal/number.ts— 接受调用方指定错误码
两份实现逻辑完全相同,但签名不同。这是刻意分离 —— 每个包拥有自己的校验函数, 不跨包引用 internal 模块,符合 RFC 0002 的包依赖规则。
影响: 无。违反 DRY 但属于架构层面的刻意选择。
建议: 不修改。如果后续发现 primitives 也需要灵活错误码,可以统一签名。
严重度: 无。
RFC 0001 Section V.1 定义 @epheon/spec 为 "标准定义层"。当前项目没有这个包。
当前等效机制: 协议和类型分布在 @epheon/primitives(Tolerance、Result)和
@epheon/temporal(Provider type)中,通过 index.ts 导出边界来保证接口稳定性。
建议: 不在当前阶段创建 @epheon/spec。当出现第三方实现需要独立协议时再创建。
严重度: 低(设计选择)。
RFC 0003 明确 "第一阶段不接受闰秒输入边界"。当前 assertGregorianDateTime 检查
second < 60,这意味着 23:59:60 会通过校验(但在闰秒日是不可逆的)。
当前行为: second=60 不会导致系统崩溃,但解析出的 JD/JDE 在闰秒日那一刻
会比实际 TAI 多 1 秒。由于 leap second provider 处理实际的 UTC→TAI 转换,
这个 1 秒差异最终会被 provider 补偿——前提是 provider 知道输入中包含了 "60"。
建议: 在本阶段不做修改,在 RFC 0008(Delta-T 与闰秒数据模型)中明确
second=60 的输入处理策略。
严重度: 低。当前不影响测试结果(所有测试用例的 second 都在 [0, 59])。
packages/primitives/src/
├── angle.ts ✅ 全量合规 RFC 0004
├── duration.ts ✅ 全量合规 RFC 0004
├── errors.ts ✅ 全量合规 RFC 0004
├── index.ts ✅ 导出边界合规 RFC 0004
├── result.ts ✅ 全量合规 RFC 0004
├── tolerance.ts ✅ 全量合规 RFC 0004
└── internal/
└── number.ts ✅ 全量合规 RFC 0004
packages/temporal/src/
├── errors.ts ✅ 全量合规 RFC 0003
├── index.ts ✅ 导出边界合规 RFC 0003
├── instant.ts ✅ 全量合规 RFC 0003
├── julian-day.ts ✅ 全量合规 RFC 0003
├── providers.ts ✅ 全量合规 RFC 0003
├── utc-date-time.ts ✅ 全量合规 RFC 0003
└── internal/
├── constants.ts ✅
├── gregorian.ts ✅ 全量合规 RFC 0003
├── number.ts ✅
├── time-scale.ts ⚠️ TSDoc 问题(问题1)
└── utc-parser.ts ✅ 全量合规 RFC 0003
工程配置
├── tsconfig.json ✅
├── vitest.config.ts ✅
├── pnpm-workspace.yaml ✅
├── .editorconfig ✅
├── .github/workflows/ci.yml ✅
└── .changeset/config.json ✅