本手册是项目的单一事实来源(single source of truth):定位、进度、架构、里程碑、工程约定都在这里。每完成一个里程碑提交时同步更新 §02 快照与 §04 状态。
一句话定位:纯 MoonBit 的 iCalendar(RFC 5545)解析 / 重复事件展开库,长成 mooncakes.io 生态第一个 CalDAV(RFC 4791)日历服务器——把竞品停在「中段计算层」的流水线,接到「真实日历客户端可直连」的服务端。
立项依据(申报书可直接引用,全部 2026-09-08 核实):
caldav / webdav / webcal / calendar-import 均返回 No modules found。| 包 | 内容 | 实现行数 | 测试 | 状态 |
|---|---|---|---|---|
| ical/text | 折行展开(§3.1)、内容行解析(引号参数)、文本反转义、带行号 ParseError | 309 | 20 用例 | 已落地 |
| ical/model | 组件树(BEGIN/END 嵌套、未知组件保留)、IcalDateTime(四态时间,Eq)、ZoneTable(固定偏移 + feed 内嵌 VTIMEZONE)、Event 类型化视图(S1:parse_events / Component::events,RRULE 原文、EXDATE、RECURRENCE-ID) | 846 | 53 用例 | 已落地 |
| demo | 可运行入口:内嵌真实形态 fixture(折叠/转义/TZID/RRULE/EXDATE/全天/改期覆盖),打印事件清单(moon run demo) | 71 | — | S1 起步 |
| ical/fetch | 取件层:fetch_ics(url)(@http.get 薄层 + 状态检查,非 200 抛 FetchError);demo 支持 URL 参数端到端 | 35 | — | 已落地 |
| ical/rrule | 完整日历级 RRULE 展开(S2/S3/S8):四档 FREQ × INTERVAL/COUNT/UNTIL、序数 BYDAY、正负 BYMONTHDAY、BYMONTH、BYSETPOS、WKST;expand_series 合并 EXDATE、RECURRENCE-ID 改期/取消/THISANDFUTURE,ZoneTable 解析结果贯穿展开 | 约 1200 | 31 用例(graham 第一档 23 条 + 第二档 8 条 + 合并/时区) | 已落地 |
| ical/serialize | 组件树 → .ics 文本(S4):CRLF、75-octets 折行(多字节不劈开)、参数值按需重引号;@text.escape_value 与 unescape 对称;roundtrip 组件树相等且幂等 | 172 | 11 用例(roundtrip/折叠/引号/空值) | 已落地 |
| ical/store | vdir 存储(S5):每事件一个 .ics 文件;FNV-1a 内容寻址 ETag(同字节同 tag);事件名白名单(字母数字._- 且非点开头)防路径逃逸;get 缺失是 None 不是错 | 193 | 5 用例(ETag/拒绝名/磁盘往返) | 已落地 |
| ical/httpd | HTTP/1.1 请求层(S5):手解析请求行/头/Content-Length 体,方法是 token(WebDAV 方法天然可读);chunked 拒收(411);裸 socket 读写(ServerConnection 是帧化 reader 不适用);S6 在此之上加 WebDAV/CalDAV 语义 | 328 | 10 用例(MemoryReader 驱动) | 请求层已落地 |
| ical/caldav | WebDAV/CalDAV 语义(S6):发现链、PROPFIND Depth 0/1、calendar-query/multiget、XML 安全转义、If-Match/If-None-Match;cmd/serve 接入 well-known、MKCALENDAR 与标准 201/204/207/412 状态 | 292 | 5 用例 + 11 项 curl 活体验收 | 已落地 |
合计:0.2.0 开发分支严格全目标测试为 wasm 150、wasm-gc 137、JavaScript 150、native 155;S5 21 项与 S6 11 项真实 curl 活体验收全绿。Thunderbird 155.0.1(Windows)已完成发现、读取、创建、原位修改与删除的真实 CalDAV 联调。
请求自上而下流经六层,每层一个包、单向依赖;层与层之间只经公开接口(.mbti)通信:
@async 的 send_responseMilky2018/xmlical/text: unfold(String) -> Array[String]
parse_content_line(String, Int) -> ContentLine raise
unescape_value(String) -> String
suberror ParseError::BadLine(line_no~, line~, message~)
ical/model: parse_components(Array[String]) -> Array[Component] raise
parse_date_time(ContentLine, ZoneTable, line_no?) -> IcalDateTime raise
parse_single_date_time(String, ZoneTable, line_no?) -> IcalDateTime raise
parse_utc_offset(String, line_no~, raw~) -> Int raise
build_zone_table(Array[Component]) -> ZoneTable
Component::{property, properties_named, events}
IcalDateTime::{instant_seconds, is_utc, is_floating, compare} // derive Eq(S1 起)
ZoneTable::{empty, builtin_common, with_builtin, insert, lookup}
parse_events(String) -> Array[Event] raise
parse_date_time_value(String, ZoneTable, tzid?, value_is_date?, line_no?) raise
struct Event { uid, summary, location, description : String?
dtstart, dtend, recurrence_id : IcalDateTime?
rrule : String? exdates : Array[IcalDateTime] }
Event::all_day(Self) -> Bool // impl Show
哲学:每步一个最小目标 → 代码 + 测试 → 验收 → 一次提交。单步超过半天就拆。任何一步卡住,先砍范围再加班。
| # | 目标 | 规模 | 验收(过不了的验收不算完成) | 状态 |
|---|---|---|---|---|
| S0 | 清场:删模板/探针,README/moon.mod 就位,登记接缝 | 0 行 | moon check && moon test 全绿,工作区干净 | 完成 0ab8978 |
| S1 | 类型化事件层:Event 结构 + Component::events()(映射层,无新算法) | ~150 | demo 读本地 fixture .ics 打印事件清单 | 完成(S1) |
| S1.5 | 取件切片:ical/fetch(@http.get 薄层,非 200 抛 FetchError)+ demo 支持 URL 输入:URL → 事件清单 端到端。网络测试不进 CI(Google 源本网络不可达,演示用 calendarlabs) | ~130 | 给真实可达 .ics URL,打印解析后事件清单;CI 仍走本地 fixture | 完成(S1.5) |
| S2 | RRULE 解析(不展开):第一档子句 → Rule;不支持子句显式报错 | ~200 | graham 32 个用例的 rule 串全部按档位解析或报错 | 完成(S2) |
| S3 | 第一档展开:DAILY/WEEKLY/MONTHLY + INTERVAL/COUNT/UNTIL/BYDAY(无offset)/BYMONTHDAY(正) + DTSTART 回退特例 | ~400 | RFC 附录 A 第一档 ~20 用例对拍全绿(语料:graham/rrule 测试套件,见 THIRD-PARTY-NOTICES.md) | 完成(S3) |
| S4 | iCalendar 序列化写回:fold(75字符)/escape/组件树→ics(CalDAV 响应体就是它,故提前) | ~200 | parse → serialize → parse 往返不变 | 完成(S4) |
| S5 | HTTP/1.1 请求层 + vdir 存储:accept + 手解析请求行/头/Content-Length 体;OPTIONS/GET/PUT/DELETE | ~350 | curl 完成事件 PUT/GET/DELETE 往返,ETag 正确变化;chunked 请求回 411 | 完成(S5) |
| S6 | WebDAV/CalDAV 核心:PROPFIND(Depth 0/1)、REPORT(calendar-query/multiget)、MKCALENDAR、well-known 重定向、发现链、If-Match/If-None-Match | ~450 | curl 模拟 Thunderbird 发现链 + REPORT 序列全绿(录成集成测试) | 完成(S6) |
| S7 | 真实客户端联调:以 Thunderbird 完整 CRUD 为首版验收;DAVx5 / Apple 日历降为赛后互操作候选 | 0.5–1 天 | ≥1 个真实客户端完整走通;产出互操作矩阵(客户端 × 操作 × 结果) | 完成(Thunderbird 155.0.1 CRUD) |
| S8 | 库层深度:RECURRENCE-ID 覆盖合并(改期/取消/THISANDFUTURE)+ 第二档 BY*(BYSETPOS/WKST/序数 BYDAY/负 BYMONTHDAY)+ ZoneTable 接入展开链 | ~500–700 | 附录 A 全 32 用例按档位通过;RECURRENCE-ID 合并用例通过 | 完成(S8) |
| S9 | 发布打磨:CI(check/test --target all + fmt + info)、mooncakes 发布、申报书、根包 re-export 门面、互操作矩阵进 README | 0.5 天 | CI 绿、moon publish 成功、申报书一页 | 完成(0.1.0 已发布,scope frozen) |
| R1 | 系列合并性能:一次索引同 UID overrides/EXDATE,按序游标应用 THISANDFUTURE | ~100 | 2000 occurrences + 500 无关 overrides 的 native release 基准由 20.27 ms 降至 1.64 ms;语义回归与全目标测试通过 | 完成(约 12.4×) |
| R2 | CalDAV REPORT 热路径:StringView 零拷贝扫描 + href 哈希选择 | ~100 | 500 hrefs × 500 resources 的 native release 基准由 68.17 ms 降至 3.87 ms;命名空间前缀回归与全目标测试通过 | 完成(约 17.6×) |
| R3 | 0.2 根门面收口:新增整文档 parse_calendar,顶层只暴露完整 parse / recurrence / serialize 工作流 | ~50 | pkg.generated.mbti 明确移除协议与文本层拼装 API;子包高级入口保留;门面往返测试通过 | 完成(待发布) |
| R4 | 通用 ICS 解析链 StringView 化:unfold、内容行、参数和 EXDATE 列表仅在所有权边界分配 | ~120 | 1000 VEVENT 的 native release 基准由 25.83 ms 降至 15.10 ms;既有 Unicode 折行与全目标回归通过 | 完成(约 1.71×) |
| R5 | RRULE 预归一化:排序 BYMONTH/BYMONTHDAY,并缓存 28–31 天月份的实际候选日 | ~80 | 密集月度规则 10000 occurrences 的 native release 基准由 5.83 ms 降至约 5.19 ms;闰年、负月日、原 Rule 不变的回归通过 | 完成(约 11%) |
| R8–R11 | 赛后优化审计落地:测试盲区补齐(15 项)、AI 生成痕迹清理(净 -105 行)、StringView 残余迁移(R4 边界之后的全部值解析)、respond 缓冲 + ZoneTable Map 索引;HttpRequest.meth 经实测确认为避让保留字 method,不改名 | ~-160 | 四目标 150/137/150/155 全绿;基准保持 R 系列基线(parse 15.6 ms、multiget 3.7 ms、series 1.68 ms、monthly 5.5 ms);.mbti 仅 ZoneTable.entries 一处预期变更 | 完成 |
每个里程碑严格走同一循环,禁止「先写一大坨再补测试」:
assert_eq / assert_true(pattern is …) 级别的断言测试;边界(空输入、畸形输入、行号)必须覆盖。///| 分隔,每块带一句话文档注释说明「为什么」。moon fmt && moon check && moon test && moon info,然后检查 .mbti diff——接口变化必须是预期内的,意外的 diff 说明改坏了边界。git -c core.hooksPath=.githooks commit …——项目钩子会跑 moon check 并拒绝「改源码但不更新 PROGRESS.md」的提交。docs/PROGRESS.md(跨会话状态源:本地文件,不入库——§1 快照、§2 里程碑、§3 会话日志)与本手册 §02/§04/§07。钩子会校验它在上次提交后被更新过。新格式(rr_moon_mod + rr_moon_pkg)下的硬规则,违反即编译失败或静默 bug:
import { "moonbitlang/async/http" @http } 写在所在目录的 moon.pkg;.mbt 文件里写 import "..." 是硬错误(3001/3002)。测试专用导入用 import { … } for "test"。pub(all) suberror Name { Ctor(field~ : Type) };Error 是内建 opaque 类型不是 trait,impl Error for X 必失败。展示用 pub impl Show for Name with fn output(...)。Ctor(a=1, b=2),匹配 Ctor(a=x, b=y);位置式会报 requires 0 positional arguments。a..<b(右开)/ a..=b(闭);for 头写坏会在下游报假错误(unbound identifier / 类型不匹配)——先修 for 头。String::trim() 接 StringView::data():data() 返回整个原串不是切片,trim 会静默消失。用 String::from_array(s.to_array()[i:j]) 或手写字符数组裁剪(ical/text/content_line.mbt 里的 trim_ascii 就是这么写的)。moon ide doc "Type::method"(搜 core + 已注册依赖),不要凭记忆写 stdlib 名字。async fn main 需要根包 moonbitlang/async 一并导入;可执行包声明 pkgtype(kind: "executable")。原则:业务代码永不 import 兜底实现,只 import trait;每个临时实现标注 UPSTREAM-GAP;探针转绿 = 上游已补齐 = 立即执行切换配方。完整表格在 docs/upstream-seams.md,此处摘要:
| # | 能力 | 现状(2026-09-08) | 本项目策略 |
|---|---|---|---|
| S1 | js 目标本地文件 | @async/fs native 可用,js 空壳 | CalDAV 服务主目标 native,js 支持延后 |
| S2 | IANA tzdb / DST 历史 | mooncakes 无 tzdb 包(x/time 仅 fixed_zone) | ZoneTable:固定偏移 + feed 内嵌 VTIMEZONE;边界写进 README |
| S3 | HTTP 抓取 | @async/http client 双目标可用(M0 实测) | 保留 Fetcher 抽象,S9 可选支线 |
| S4 | async 运行时稳定性 | README 自述 experimental | API 触点收敛在 httpd/store 两处 |
| S5 | 系统本地时区 | 需自行 FFI | 暂不需要(服务用 UTC + 显式偏移) |
| S6 | WebDAV 方法承载 | RequestMethod 封闭枚举(types.mbt:16),无 PROPFIND/REPORT/MKCALENDAR | S5 手解析请求行(方法只是 token);上游扩展后一行切回 read_request |
// 响应侧(直接用,无障碍):
conn.send_response(200, "OK", extra_headers={ "Content-Type": "text/calendar" })
conn <+ body // 字符串插值写入
conn.end_response()
// 请求侧(绕开 read_request 的方法枚举):
ServerConnection 实现了 @io.Reader(server.mbt:65)
// → 自己读字节流,解析请求行 "PROPFIND /cal/ HTTP/1.1"、头、Content-Length 体
// XML:Milky2018/xml 0.4.1(拉式解析)或 moonbit-community/XMLParser 0.2.5
// JSON:moonbitlang/core/json(REPORT/调试输出用)
411 Length Required(客户端 PUT 几乎不用 chunked)。| 风险 | 等级 | 对策 |
|---|---|---|
| 手解析 HTTP 边界情况(keep-alive、多值头、Expect: 100-continue) | 中 | 明确 411/501 拒绝并写进边界;S7 联调日志驱动补齐;Radicale 哲学「够用即止」 |
| 真实客户端差异不可控(发现链/PROPFIND 形状各家不同) | 中 | S7 独立成步预留整天;优先 Thunderbird(最宽容、文档全),DAVx5 次之;问题→修复→复测记进互操作矩阵 |
| XML 命名空间(客户端前缀任意:D:/d:/默认 ns) | 中 | 按 (namespace-uri, local-name) 匹配而非前缀;测试带前缀变体 |
| RRULE 序号算术(BYSETPOS/负值/序数 BYDAY)正确性 | 中 | 隔离在 S8 独立成步;附录 A 全量对拍;graham/rrule 上游源码(GitHub)随时对照 |
| 九月场规则未确认(本地技能只有八月场章程) | 低 | 以八月场为底线准备(一页 MD 申报书/CI/mooncakes/≥5 提交);提交前官方渠道确认一次 |
| 规模不足(当前 962 行 vs 4–10k 参考) | 低 | S1–S8 累计估 ~2.6k 实现 + ~2.5k 测试 ≈ 5k+,落在区间;宁加用例不堆无关功能 |
八月场验收硬标准 → 里程碑映射(九月场以官方章程为准):
| 验收项 | 满足于 |
|---|---|
| MoonBit 为主语言 | 全程 |
| 可运行示例 | S1 起有 fixture demo,S5 起服务器可 moon run,S7 起真实客户端 demo |
| 核心路径测试 | S1–S6 各自带测试;S3/S8 附录 A 对拍;S6 curl 序列集成测试 |
| CI | S9(moon check/test --target all + fmt + info,参考 moonbit-community/.github 的 check.yml) |
| 发布 mooncakes.io | S9 已完成:justinwongcn/moon-ical@0.1.0 可由 moon search 检索与安装 |
| 默认分支 ≥5 真实提交 | S0–S9 每步一提交,自然 ≥10 |
| 申报书(一页 Markdown) | S9 定稿:定位 + 竞品矩阵切分(引调研文档 §02)+ 互操作矩阵 + 边界 |
| AI 辅助合规 | AI 生成内容可解释、可测试、来源明确;申报书如实声明 AI 参与 |