bb-sync 2.2.1__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {bb_sync-2.2.1 → bb_sync-2.3.0}/.gitignore +2 -2
  2. {bb_sync-2.2.1 → bb_sync-2.3.0}/CHANGELOG.md +25 -0
  3. {bb_sync-2.2.1 → bb_sync-2.3.0}/PKG-INFO +6 -3
  4. {bb_sync-2.2.1 → bb_sync-2.3.0}/README.md +5 -2
  5. {bb_sync-2.2.1 → bb_sync-2.3.0}/docs/scripting.md +1 -1
  6. bb_sync-2.3.0/openspec/changes/archive/.gitkeep +0 -0
  7. bb_sync-2.3.0/openspec/changes/archive/2026-11-04-due-item-source-conflicts/.openspec.yaml +2 -0
  8. bb_sync-2.3.0/openspec/changes/archive/2026-11-04-due-item-source-conflicts/design.md +99 -0
  9. bb_sync-2.3.0/openspec/changes/archive/2026-11-04-due-item-source-conflicts/proposal.md +59 -0
  10. bb_sync-2.3.0/openspec/changes/archive/2026-11-04-due-item-source-conflicts/specs/announcement-due-extraction/spec.md +59 -0
  11. bb_sync-2.3.0/openspec/changes/archive/2026-11-04-due-item-source-conflicts/specs/due-item-reconciliation/spec.md +67 -0
  12. bb_sync-2.3.0/openspec/changes/archive/2026-11-04-due-item-source-conflicts/tasks.md +39 -0
  13. bb_sync-2.3.0/openspec/config.yaml +32 -0
  14. bb_sync-2.3.0/openspec/specs/.gitkeep +0 -0
  15. bb_sync-2.3.0/openspec/specs/announcement-due-extraction/spec.md +58 -0
  16. bb_sync-2.3.0/openspec/specs/due-item-reconciliation/spec.md +71 -0
  17. {bb_sync-2.2.1 → bb_sync-2.3.0}/pyproject.toml +1 -1
  18. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/blackboard/models.py +9 -2
  19. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/blackboard/scraper.py +45 -10
  20. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/due.py +103 -11
  21. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/service.py +5 -1
  22. {bb_sync-2.2.1 → bb_sync-2.3.0}/tests/test_units.py +364 -1
  23. {bb_sync-2.2.1 → bb_sync-2.3.0}/uv.lock +1 -1
  24. {bb_sync-2.2.1 → bb_sync-2.3.0}/.github/workflows/ci.yml +0 -0
  25. {bb_sync-2.2.1 → bb_sync-2.3.0}/.github/workflows/release.yml +0 -0
  26. {bb_sync-2.2.1 → bb_sync-2.3.0}/.pre-commit-config.yaml +0 -0
  27. {bb_sync-2.2.1 → bb_sync-2.3.0}/AGENTS.md +0 -0
  28. {bb_sync-2.2.1 → bb_sync-2.3.0}/CONTRIBUTING.md +0 -0
  29. {bb_sync-2.2.1 → bb_sync-2.3.0}/LICENSE +0 -0
  30. {bb_sync-2.2.1 → bb_sync-2.3.0}/config.yaml +0 -0
  31. {bb_sync-2.2.1 → bb_sync-2.3.0}/docs/architecture.md +0 -0
  32. {bb_sync-2.2.1 → bb_sync-2.3.0}/docs/config.md +0 -0
  33. {bb_sync-2.2.1 → bb_sync-2.3.0}/docs/images/run-plan.png +0 -0
  34. {bb_sync-2.2.1 → bb_sync-2.3.0}/docs/images/run-summary.png +0 -0
  35. {bb_sync-2.2.1 → bb_sync-2.3.0}/docs/testing.md +0 -0
  36. {bb_sync-2.2.1 → bb_sync-2.3.0}/scripts/debug_course.py +0 -0
  37. {bb_sync-2.2.1 → bb_sync-2.3.0}/scripts/debug_courses.py +0 -0
  38. {bb_sync-2.2.1 → bb_sync-2.3.0}/scripts/debug_login.py +0 -0
  39. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/__init__.py +0 -0
  40. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/__main__.py +0 -0
  41. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/blackboard/__init__.py +0 -0
  42. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/blackboard/downloader.py +0 -0
  43. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/blackboard/login.py +0 -0
  44. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/browser/__init__.py +0 -0
  45. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/browser/launcher.py +0 -0
  46. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/cli.py +0 -0
  47. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/__init__.py +0 -0
  48. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/auth.py +0 -0
  49. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/browser.py +0 -0
  50. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/config.py +0 -0
  51. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/course.py +0 -0
  52. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/doctor.py +0 -0
  53. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/due.py +0 -0
  54. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/commands/run.py +0 -0
  55. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/context.py +0 -0
  56. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/__init__.py +0 -0
  57. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/config.py +0 -0
  58. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/config_store.py +0 -0
  59. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/doctor.py +0 -0
  60. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/errors.py +0 -0
  61. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/help.py +0 -0
  62. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/output.py +0 -0
  63. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/core/update.py +0 -0
  64. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/creds.py +0 -0
  65. {bb_sync-2.2.1 → bb_sync-2.3.0}/src/bb_sync/paths.py +0 -0
  66. {bb_sync-2.2.1 → bb_sync-2.3.0}/tests/conftest.py +0 -0
  67. {bb_sync-2.2.1 → bb_sync-2.3.0}/tests/test_cli_e2e.py +0 -0
  68. {bb_sync-2.2.1 → bb_sync-2.3.0}/tests/test_update_e2e.py +0 -0
@@ -19,7 +19,7 @@ debug_*.txt
19
19
  .workbuddy/
20
20
  courses/
21
21
 
22
- # 本地 AI / 工作流工具目录(个人环境,不入库)
22
+ # 本地 AI / 工作流工具目录(个人环境,不入库)。openspec/ 不在此列:
23
+ # 它保存项目的 specs 与 change 归档,属于该跟代码一起版本化的规划产物。
23
24
  .claude/
24
25
  .pi/
25
- openspec/
@@ -2,6 +2,31 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [2.3.0] — 2026-11-04
6
+
7
+ ### 修复
8
+
9
+ - **公告里没有编号的作业名也能认出**:此前作业名规则要求 `HW4` / `Assignment 2`
10
+ 这类带编号写法,CSC5010 的 `Homework on Probabilistic Reasoning`(截止
11
+ 2026-11-15)整条被静默丢弃。现在接受「作业词 + 可选编号 + on / about 主题」的
12
+ 写法,守卫改为「标题或截止行相邻行含作业词」;`Week 4 Materials Uploaded` 这类
13
+ 没有作业语境的日期仍然不收录。
14
+ - **To Do 与公告叫法不一致时不再静默丢弃**:Blackboard 把 HW4 的作业容器命名成
15
+ `Assignment 5`,与同一天公告的 `Homework assignment 4` 冲突,旧版按「To Do 优先」
16
+ 直接丢弃公告条目。现在保留 To Do 的日期与类型,把公告标题记为该条目的别名,
17
+ `due.md` 渲染成 `Assignment 5(公告称 Homework assignment 4)`,文件头部解释括号含义。
18
+ - **同一编号的两种写法不再重复成两条**:`HW4` 与 `Homework assignment 4` 的主题词
19
+ 集合没有交集,旧版判不成同一件事,同一天会列两条;现在补充「同为作业类说法且
20
+ 编号相同」的判定。
21
+ - **分歧提示补全到待办明细**:`bb-sync due` 与 `bb-sync run` 的汇总会打印
22
+ 「发现 N 处来源分歧」,`run` 的明细行与 `due.md` 共用同一套标题渲染,直接显示
23
+ `Assignment 5(公告称 Homework assignment 4)`,不必再回 `due.md` 对照。
24
+
25
+ ### 变更
26
+
27
+ - `due.md` 行格式:带来源分歧的条目会在标题后追加 `(公告称 …)`,文件头部相应多一行说明。
28
+ - `--json` 的 `items` 元素新增 `aliases` 字段(无分歧时为空数组)。
29
+
5
30
  ## [2.2.1] — 2026-11-02
6
31
 
7
32
  ### 修复
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bb-sync
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: 港中深(CUHK-Shenzhen)Blackboard 课件下载与课程资源自动同步:登录 ADFS SSO,增量下载课件/作业/指导、公告与待办
5
5
  Project-URL: Homepage, https://github.com/asdukw/cuhk-shenzhen-blackboard-sync
6
6
  Project-URL: Repository, https://github.com/asdukw/cuhk-shenzhen-blackboard-sync
@@ -73,6 +73,7 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
73
73
  - 课件、作业、指导按课程归好类。重跑只下新增的,结束时逐条列出本轮真正新下载的文件。
74
74
  - 有公告的课程会生成一份 `announcements.md`。没有公告的课程不生成,`bb-sync due` 也不写这个文件。
75
75
  - 全部课程的待办汇总成一份 `due.md`,按「已逾期 → 今天 → 明天 → 未来 7 天 → 之后 → 日期未知 → 已过期事件」排好序。公告里带日期的课堂测验、考试和作业截止时间也一并计入。每条待办都是可勾选的任务列表(`- [ ]`),做完勾上 `- [x]`,下次同步保留完成状态。
76
+ - To Do 名称与公告写法对不上时(例如教务把 HW4 的作业容器命名成 `Assignment 5`),条目会渲染成 `Assignment 5(公告称 Homework assignment 4)`,两个来源的名字都保留,不静默二选一。
76
77
 
77
78
  它不回传作业、不代签到、不抓成绩,也绕不过任何权限。看到的就是你账号里本来能看到的。
78
79
 
@@ -94,10 +95,11 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
94
95
  # Blackboard 待办
95
96
 
96
97
  > 更新时间:2026-10-14 21:03(本地时间)
97
- > 已检查 5 门课程,发现 7 项待办;按优先级和截止日期排列。
98
+ > 已检查 5 门课程,发现 8 项待办;按优先级和截止日期排列。
98
99
  > 其中公告里的测验 1 项、考试 1 项、作业 1 项已一并作为待办列出。
99
100
  > 另有 1 项公告里的测验/考试日期已过,列在最后仅供回看。
100
101
  > 完成一项就在前面打勾(`- [ ]` → `- [x]`),下次同步会保留完成状态。
102
+ > 括号里的「公告称 …」表示 To Do 名称与公告写法对不上,两个名字都来自 Blackboard。
101
103
 
102
104
  ## 已逾期(1)
103
105
 
@@ -111,9 +113,10 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
111
113
 
112
114
  - [ ] **2026-10-15**(明天) — [CSC5010](https://bb.cuhk.edu.cn/…) — 【测验】Quiz 2
113
115
 
114
- ## 未来 7 天(3)
116
+ ## 未来 7 天(4)
115
117
 
116
118
  - [x] **2026-10-17**(还有 3 天) — [ENG1001](https://bb.cuhk.edu.cn/…) — Essay 1 初稿
119
+ - [ ] **2026-10-18**(还有 4 天) — [DDA5002](https://bb.cuhk.edu.cn/…) — Assignment 5(公告称 Homework assignment 4)
117
120
  - [ ] **2026-10-20**(还有 6 天) — [CSC5010](https://bb.cuhk.edu.cn/…) — 【作业】HW4
118
121
  - [ ] **2026-10-21**(还有 7 天) — [MAT1001](https://bb.cuhk.edu.cn/…) — 【考试】期中考试
119
122
 
@@ -22,6 +22,7 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
22
22
  - 课件、作业、指导按课程归好类。重跑只下新增的,结束时逐条列出本轮真正新下载的文件。
23
23
  - 有公告的课程会生成一份 `announcements.md`。没有公告的课程不生成,`bb-sync due` 也不写这个文件。
24
24
  - 全部课程的待办汇总成一份 `due.md`,按「已逾期 → 今天 → 明天 → 未来 7 天 → 之后 → 日期未知 → 已过期事件」排好序。公告里带日期的课堂测验、考试和作业截止时间也一并计入。每条待办都是可勾选的任务列表(`- [ ]`),做完勾上 `- [x]`,下次同步保留完成状态。
25
+ - To Do 名称与公告写法对不上时(例如教务把 HW4 的作业容器命名成 `Assignment 5`),条目会渲染成 `Assignment 5(公告称 Homework assignment 4)`,两个来源的名字都保留,不静默二选一。
25
26
 
26
27
  它不回传作业、不代签到、不抓成绩,也绕不过任何权限。看到的就是你账号里本来能看到的。
27
28
 
@@ -43,10 +44,11 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
43
44
  # Blackboard 待办
44
45
 
45
46
  > 更新时间:2026-10-14 21:03(本地时间)
46
- > 已检查 5 门课程,发现 7 项待办;按优先级和截止日期排列。
47
+ > 已检查 5 门课程,发现 8 项待办;按优先级和截止日期排列。
47
48
  > 其中公告里的测验 1 项、考试 1 项、作业 1 项已一并作为待办列出。
48
49
  > 另有 1 项公告里的测验/考试日期已过,列在最后仅供回看。
49
50
  > 完成一项就在前面打勾(`- [ ]` → `- [x]`),下次同步会保留完成状态。
51
+ > 括号里的「公告称 …」表示 To Do 名称与公告写法对不上,两个名字都来自 Blackboard。
50
52
 
51
53
  ## 已逾期(1)
52
54
 
@@ -60,9 +62,10 @@ Blackboard 上的东西散在「课程内容」「作业」「公告」几个入
60
62
 
61
63
  - [ ] **2026-10-15**(明天) — [CSC5010](https://bb.cuhk.edu.cn/…) — 【测验】Quiz 2
62
64
 
63
- ## 未来 7 天(3)
65
+ ## 未来 7 天(4)
64
66
 
65
67
  - [x] **2026-10-17**(还有 3 天) — [ENG1001](https://bb.cuhk.edu.cn/…) — Essay 1 初稿
68
+ - [ ] **2026-10-18**(还有 4 天) — [DDA5002](https://bb.cuhk.edu.cn/…) — Assignment 5(公告称 Homework assignment 4)
66
69
  - [ ] **2026-10-20**(还有 6 天) — [CSC5010](https://bb.cuhk.edu.cn/…) — 【作业】HW4
67
70
  - [ ] **2026-10-21**(还有 7 天) — [MAT1001](https://bb.cuhk.edu.cn/…) — 【考试】期中考试
68
71
 
@@ -40,7 +40,7 @@ bb-sync --json doctor | jq '.ok'
40
40
  | --- | --- |
41
41
  | `course list` | 数组:`[{"id","code","title","folder"}, ...]` |
42
42
  | `run` | 对象:`{"downloaded","exists","would_download","empty","failed","courses","files"}`;`files` 是本次**真正新落盘**的文件(dry-run 时为空),元素为 `{"course","category","name","path"}`——`name` 相对课程目录、`path` 为绝对路径 |
43
- | `due` | 对象:`{"path","total","failed","announcement_failed","courses","items"}`;`items` 含课程、标题、类型(`task` / `quiz` / `exam`)与 ISO 日期 |
43
+ | `due` | 对象:`{"path","total","failed","announcement_failed","courses","items"}`;`items` 含课程、标题、类型(`task` / `quiz` / `exam`)、ISO 日期与 `aliases`(来源分歧时公告里的另一种叫法,否则为空数组) |
44
44
  | `doctor` | 对象:`{"ok": bool, "checks": [{"label","ok","detail","hint","required"}]}` |
45
45
  | `doctor --list` | 对象:`{"checks": ["python","git", ...]}` |
46
46
  | `auth status` | 对象:`{"configured","source","account"}`(account 已脱敏) |
File without changes
@@ -0,0 +1,2 @@
1
+ schema: spec-driven
2
+ created: 2026-11-04
@@ -0,0 +1,99 @@
1
+ # Design
2
+
3
+ ## Context
4
+
5
+ 现状(详见 proposal.md「Why」):`bb-sync due` 走两条独立路径——课程主页 To Do 模块(结构化,日期权威)和公告解析(文本推断,名称权威)——最后在 `merge_due_items()` 里以「To Do 优先、公告重复条目丢弃」的单赢家方式合并,然后由 `build_due_markdown()` 渲染 `due.md`。
6
+
7
+ 约束:
8
+
9
+ - `DueItem` 是 `frozen=True` 的 dataclass,合并只能通过替换产生新对象。
10
+ - `due.md` 的 `- [x]` 完成状态靠 `_item_signature()`(日期 + 课程 + 标题)从上一版文件里认回。
11
+ - `bb-sync due` 目前只访问两个页面:课程主页与公告页;`Assignments and Exams` 页面只在 `run` 时抓文件。
12
+ - 现有测试 `tests/test_units.py` 已固定了公告解析、合并优先级、渲染与勾选的契约。
13
+
14
+ ## Goals / Non-Goals
15
+
16
+ **Goals:**
17
+
18
+ - 公告里无编号的作业名能被识别,不漏收。
19
+ - To Do 与公告对同一件事的叫法不一致时,两个叫法都保留,并在 `due.md` 里显式标注。
20
+ - 分歧在终端与 `--json` 输出里可观测。
21
+ - 新增规则用真实公告文本做回归夹具。
22
+
23
+ **Non-Goals:**
24
+
25
+ - 不抓取 `Assignments and Exams` 页的附件名去反推正确编号(多一次页面访问、且是另一种证据来源,留给后续)。
26
+ - 不自动「纠正」Blackboard 的错误命名:只呈现分歧,不替教务改标签。
27
+ - 不改公告页的抓取方式与登录流程。
28
+
29
+ ## Decisions
30
+
31
+ ### D1:别名挂在 `DueItem` 上,而不是另建冲突实体
32
+
33
+ 在 `DueItem` 增加 `aliases: tuple[str, ...] = ()`(frozen dataclass 默认空元组),合并时用 `dataclasses.replace()` 把公告标题写进 To Do 条目。
34
+
35
+ - 放弃的方案:新建 `Conflict(first, second)` 实体并让清单同时容纳普通条目与冲突条目。收益是信息更全,但会污染排序、分组、渲染和勾选签名四处,且 `due.md` 的定位是给人看的一条行,不是数据库。
36
+ - 放弃的方案:把两个标题拼成 `Assignment 5 / Homework assignment 4`。它无法区分「分歧」与「同一件事的两种正常写法」,也无法让读者知道哪个来自公告。
37
+
38
+ ### D2:分歧判定归一化为「编号 + 主题词集合」
39
+
40
+ 对两个标题各抽 `(编号, 主题词)`:编号来自标题里 1–2 位的阿拉伯数字;主题词复用现有
41
+ `_title_tokens()` 的停用词与纯数字过滤。「是不是同一件事」和「叫法有没有分歧」分两步:
42
+
43
+ 1. **同一事件判定**(`_same_event`)先看「同课程 + 同日期」,再满足任一条件即可:双方都是
44
+ 作业类说法且编号相同、主题词有交集、或中文标题互相包含。补上「同编号作业」这条是因为
45
+ 仅靠主题词交集,`HW4` 与 `Homework assignment 4` 的词集合(`{hw}` vs
46
+ `{homework, assignment}`)没有交集,同一天会被当成两件事重复列出。
47
+ 2. **分歧判定**(`_titles_conflict`)只在已判定为同一事件后运行:
48
+
49
+ | 条件 | 结论 |
50
+ |---|---|
51
+ | 双方都有编号且不同 | 分歧 |
52
+ | 双方编号相同 | 无分歧(措辞差异) |
53
+ | 一方无编号、主题词有交集 | 无分歧 |
54
+ | 双方都无编号且主题词无交集 | 判不成同一事件,两条都保留,不做猜测性合并 |
55
+
56
+ - 放弃的方案:字符串相似度(`SequenceMatcher`)阈值。`Assignment 5` 与 `Homework assignment 4` 相似度高,正好会把该报的分歧判成重复;而 `HW4` 与 `Homework assignment 4` 相似度低,会把正常的措辞差异判成分歧——方向完全相反。
57
+ - 放弃的方案:只比较原始字符串是否相等。`HW4` vs `Homework assignment 4` 会被误标为分歧。
58
+
59
+ ### D3:渲染把别名放在标题后的全角括号里
60
+
61
+ `- [ ] **2026-11-13**(还有 9 天) — [DDA5002](…) — Assignment 5(公告称 Homework assignment 4)`
62
+
63
+ `due.md` 头部增加一行说明括号含义。别名经 `_escape_markdown()` 处理,避免公告标题里的 `*`、`[` 破坏行结构。
64
+
65
+ ### D4:勾选签名只含基础标题,不含别名
66
+
67
+ `_item_signature()` 保持「日期 + 课程 + 基础标题」。别名变化(公告被删除、措辞被修改)不改变签名,已完成状态不会复位。
68
+
69
+ 已知牺牲:若某次同步后 To Do 条目消失、只剩公告条目,基础标题会从 `Assignment 5` 变成 `Homework assignment 4`,该条的勾选状态会复位。这属于来源本身消失导致的对象身份变化,接受。
70
+
71
+ ### D5:放宽作业名识别,但把守卫移到「语境」而非「编号」
72
+
73
+ `_ASSIGNMENT_NAME_RE` 改为「作业词 + 可选编号 + 可选主题」,并保留「标题或上一行必须含作业词」的语境守卫。截止行仍需同时含日期与截止词,这条不变。
74
+
75
+ - 放弃的方案:继续要求编号,仅对特定课程硬编码例外。不可维护。
76
+ - 放弃的方案:完全取消名字要求,只看到日期就收。会把「材料上传通知里提到的日期」等大量噪音带进待办。
77
+
78
+ ### D6:分歧可观测用最轻的方式
79
+
80
+ `service` 层在汇总里输出分歧条数;`DueItem.as_dict()` 增加 `aliases` 字段(空列表也不省略,保持结构稳定)。不新增命令行开关。
81
+
82
+ ## Risks / Trade-offs
83
+
84
+ - [放宽识别后误收无编号的普通提醒] → 保留「相邻行含作业词 + 截止词 + 日期」三重门;用现状测试里的反例(`Week 4 Materials Uploaded`)作回归夹具,并补充「标题含 homework 但日期指别的事」的用例。
85
+ - [`due.md` 行变长、表格类截图不好看] → 只在实质分歧时出现,且头部有说明;接受。
86
+ - [`as_dict()` 新增字段可能影响下游脚本] → 只增不减;在 CHANGELOG 标 BREAKING 并给出版本号提升(2.3.0)。
87
+ - [编号归一化对中文作业名(「作业 5」「第五次作业」)覆盖不足] → 本次只处理阿拉伯数字;中文写法先不推断分歧,避免误报;记入 Open Questions。
88
+ - [公告页结构变化导致别名来源缺失] → 别名缺失时退化为现状行为,不影响主流程。
89
+
90
+ ## Migration Plan
91
+
92
+ 1. 改动落在 bb-sync,版本按 minor 提升(2.2.1 → 2.3.0),CHANGELOG 记录 `due.md` 行格式与 JSON 字段的 BREAKING 变化。
93
+ 2. 用户重跑 `bb-sync due` 即可得到新格式;`due.md` 是生成物,无需数据迁移。
94
+ 3. 回滚:恢复旧版本后重跑 `bb-sync due`,别名消失、签名不变,已完成状态保留。
95
+
96
+ ## Open Questions
97
+
98
+ - 是否把 `Assignments and Exams` 页的附件名(如 `DDA5002HW4_Fall26.pdf`)作为第三方证据,用于自动判定哪一方的编号更可信。
99
+ - 中文作业命名(「第五次作业」)的归一化是否需要纳入分歧判定。
@@ -0,0 +1,59 @@
1
+ # 变更提案:待办来源冲突显式标注
2
+
3
+ ## Why
4
+
5
+ 2026-11-04 的真实同步里有两条待办出问题,都不是随机故障,而是同一个设计假设的两面:
6
+
7
+ 1. **来源冲突被静默吞掉**。Blackboard 的「Assignments and Exams」页把 HW4 的容器命名成
8
+ `Assignment 5`(附件仍是 `DDA5002HW4_Fall26.pdf`),To Do 模块照抄该容器名;同一天、
9
+ 同一件事的公告却写着 `Homework assignment 4`。`merge_due_items()` 判定两条为「同一件事」
10
+ 后直接丢弃公告条目,于是 `due.md` 只剩下 `Assignment 5`,公告里正确的编号连痕迹都不留。
11
+ To Do 被当作「原生数据更可信」,但原生数据本身可能被教务贴错标签。
12
+ 2. **作业识别门槛过窄**。CSC5010 公告正文写 `Homework on Probabilistic Reasoning is
13
+ assigned.` / `Deadline is Nov 15, 2026`,日期和截止词都能解析,但
14
+ `_ASSIGNMENT_NAME_RE` 强制要求编号(`HW4`、`Assignment 2`),没有数字的作业名一律
15
+ 返回 `None`,整条静默丢弃。
16
+
17
+ 两处漏报在 `due.md` 里都毫无提示,只能靠人肉比对公告才发现。随着学期推进公告量增加,
18
+ 这类静默失败只会更频繁,所以现在改。
19
+
20
+ ## What Changes
21
+
22
+ - **公告作业识别放宽**:允许「作业词 + 可选编号 + 可选主题」的名字形态
23
+ (`Homework on Probabilistic Reasoning`、`Problem Set 2`、`Lab 3`)。守卫从「必须带数字」
24
+ 换成「公告标题或截止行相邻行含作业词」,仍拒绝没有作业语境的日期(例如
25
+ `Week 4 Materials Uploaded` 里的 `deadline is 11:59 pm, Oct. 29th`)。
26
+ - **去重改为对账**:`_same_event()` 命中时不再丢弃公告条目,而是把公告标题作为**别名**
27
+ 保留在 To Do 条目上,日期与类型仍以 To Do 为准。
28
+ - **冲突判定只认实质分歧**:把两个标题归一成 `(编号, 主题词集合)`,只有编号不同、或双方
29
+ 编号相同但主题矛盾时才标注;`HW4` 与 `Homework assignment 4` 属于措辞差异,不标注。
30
+ - **`due.md` 显式标注分歧**:渲染为 `Assignment 5(公告称 Homework assignment 4)`,
31
+ 表头补一行说明括号含义。**BREAKING**:别名存在时 `due.md` 的行内容会变化。
32
+ - **勾选状态不受别名影响**:`_item_signature()` 继续只由「日期 + 课程 + 基础标题」构成,
33
+ 公告措辞变化不会让已勾选条目复位。
34
+ - **分歧可观测**:`bb-sync due` 结束汇总「发现 N 处来源分歧」,`--json` 的条目里带
35
+ `aliases` 字段。**BREAKING**:`as_dict()` 新增字段。
36
+ - **真实案例进测试**:把这两条公告文本与对应的 To Do 条目做成回归夹具,防止同类问题
37
+ 再次静默复发。
38
+
39
+ ## Capabilities
40
+
41
+ ### New Capabilities
42
+
43
+ - `announcement-due-extraction`:从公告正文识别可预期的待办日期(考试 / 测验 / 作业截止),
44
+ 包括作业名识别规则、日期抽取窗口、过期保留期。
45
+ - `due-item-reconciliation`:To Do 与公告两个来源的合并语义、冲突别名、`due.md` 渲染格式、
46
+ 勾选状态签名与运行时可观测性。
47
+
48
+ ### Modified Capabilities
49
+
50
+ 本次是项目首次建立 specs(`openspec/specs/` 目前为空),没有既有能力需要修改。
51
+
52
+ ## Impact
53
+
54
+ - 代码:`src/bb_sync/blackboard/scraper.py`(作业名规则)、`src/bb_sync/blackboard/models.py`
55
+ (`DueItem` 增加别名)、`src/bb_sync/core/due.py`(对账与渲染)、
56
+ `src/bb_sync/core/service.py`(汇总提示)、`src/bb_sync/commands/due.py`(帮助文本)。
57
+ - 输出:`due.md` 行格式、终端汇总、`--json` 结构。
58
+ - 测试:`tests/test_units.py` 现有公告解析与合并用例需扩充;新增两条真实公告夹具。
59
+ - 文档:`README.md` 的 `due.md` 示例、`CHANGELOG.md`。
@@ -0,0 +1,59 @@
1
+ # Spec Delta
2
+
3
+ ## Purpose
4
+
5
+ 从 Blackboard 公告正文中识别可预期的待办日期(考试、测验、作业截止),产出可合并进待办清单的条目,并在无法可靠识别时放弃而不是猜测。
6
+
7
+ ## ADDED Requirements
8
+
9
+ ### Requirement: 作业截止行支持无编号的作业名
10
+
11
+ 系统 SHALL 把「作业词 + 可选编号 + 可选主题」形式的名称识别为作业名,包括
12
+ `Homework on Probabilistic Reasoning`、`Problem Set 2`、`Lab 3`;编号 SHALL 不再是无编号作业名被收录的前提。
13
+
14
+ #### Scenario: 无编号的主题式作业名被收录
15
+
16
+ - **WHEN** 公告标题为 `Homework on Probabilistic Reasoning`,正文含一行 `Deadline is Nov 15, 2026`
17
+ - **THEN** 该公告产生一条 `assignment` 待办,日期为 `2026-11-15`,标题取自公告标题
18
+
19
+ #### Scenario: 编号式作业名继续被收录
20
+
21
+ - **WHEN** 公告正文含 `- **HW4:** Due by **Oct. 31 at 11:59 PM**.`
22
+ - **THEN** 产生一条 `assignment` 待办,标题含 `HW4`,日期为 `2026-10-31`
23
+
24
+ #### Scenario: 截止行上一行可提供作业名
25
+
26
+ - **WHEN** 正文先有一行 `2. Coding Assignment 2 Release`,下一行为 `- Deadline: 23:59, November 2, 2026`
27
+ - **THEN** 产生一条 `assignment` 待办,标题含 `Assignment 2`,日期为 `2026-11-02`
28
+
29
+ ### Requirement: 无作业语境的日期不产生待办
30
+
31
+ 系统 SHALL 在公告标题与截止行相邻行都不含作业词时放弃该截止行,即使该行同时含日期和 `due`/`deadline` 措辞。
32
+
33
+ #### Scenario: 材料上传公告里的截止提醒被忽略
34
+
35
+ - **WHEN** 公告标题为 `Week 4 Materials Uploaded`,正文为 `Reminder: the deadline is 11:59 pm, Oct. 29th.`
36
+ - **THEN** 该公告不产生 `assignment` 待办
37
+
38
+ ### Requirement: 公告待办受发布日期与保留期约束
39
+
40
+ 系统 SHALL 丢弃日期早于公告发布日期、或早于当前日期超过 7 天的公告推断条目。
41
+
42
+ #### Scenario: 过期的测验不再进入待办
43
+
44
+ - **WHEN** 当前日期为 `2026-11-04`,公告提到 `2026-10-20` 的 quiz
45
+ - **THEN** 该 quiz 不进入待办
46
+
47
+ #### Scenario: 保留期内的逾期作业仍保留
48
+
49
+ - **WHEN** 当前日期为 `2026-11-04`,作业公告的截止日期为 `2026-10-31`
50
+ - **THEN** 该 `assignment` 待办仍被收录,供用户确认是否已交
51
+
52
+ ### Requirement: 同一公告内的重复事项只收录一次
53
+
54
+ 系统 SHALL 按「日期 + 标题」对同一公告解析出的条目去重。
55
+
56
+ #### Scenario: 正文两处提到同一截止
57
+
58
+ - **WHEN** 同一公告的正文和标题各出现一次 `Homework 4`、`Due Nov 2, 2026`
59
+ - **THEN** 只产生一条 `assignment` 待办
@@ -0,0 +1,67 @@
1
+ # Spec Delta
2
+
3
+ ## Purpose
4
+
5
+ 把 Blackboard To Do 模块与公告两个来源的待办对账成一份清单:日期与类型取 To Do,来源之间的叫法分歧保留并显式呈现给用户,而不是静默丢弃其中一方。
6
+
7
+ ## ADDED Requirements
8
+
9
+ ### Requirement: 同一事件的公告条目作为别名保留
10
+
11
+ 当 To Do 条目与公告条目属于同一课程、同一天且指向同一件事时,合并结果 SHALL 保留 To Do 条目的日期与类型,并 SHALL 记录公告条目的标题作为别名。
12
+
13
+ #### Scenario: 编号冲突时保留公告标题
14
+
15
+ - **WHEN** To Do 条目为 `Assignment 5`、日期 `2026-11-13`,公告条目为 `Homework assignment 4`、同日
16
+ - **THEN** 合并结果为一条待办,日期 `2026-11-13`,标题 `Assignment 5`,别名包含 `Homework assignment 4`
17
+
18
+ #### Scenario: 没有对应 To Do 时公告条目直接补入
19
+
20
+ - **WHEN** 公告推断出 `2026-11-15` 的作业截止,而 To Do 模块没有当天条目
21
+ - **THEN** 清单中出现一条独立待办,日期 `2026-11-15`,无别名
22
+
23
+ ### Requirement: 仅在实质分歧时标注
24
+
25
+ 系统 SHALL 把待办标题归一为「编号 + 主题词集合」后比较,只在编号不同、或双方编号相同但主题矛盾时标记分歧;纯措辞差异 SHALL NOT 标记。
26
+
27
+ #### Scenario: 措辞差异不标记
28
+
29
+ - **WHEN** To Do 条目为 `HW4`,公告条目为 `Homework assignment 4`,同日
30
+ - **THEN** 合并结果不产生别名标注
31
+
32
+ #### Scenario: 编号不同标记分歧
33
+
34
+ - **WHEN** To Do 条目为 `Assignment 5`,公告条目为 `Homework assignment 4`,同日
35
+ - **THEN** 合并结果带有一个分歧标记
36
+
37
+ ### Requirement: due.md 渲染显式标注来源分歧
38
+
39
+ `due.md` 中带别名的条目 SHALL 在标题后追加 `(公告称 <别名>)`,且文件头部 SHALL 说明该括号的含义。
40
+
41
+ #### Scenario: 冲突条目渲染
42
+
43
+ - **WHEN** 待办标题为 `Assignment 5`,别名为 `Homework assignment 4`,日期 `2026-11-13`
44
+ - **THEN** `due.md` 该行包含 `Assignment 5(公告称 Homework assignment 4)`
45
+
46
+ ### Requirement: 别名不影响勾选状态
47
+
48
+ 完成状态的签名 SHALL 只由「日期 + 课程 + 基础标题」构成,别名出现、消失或变化 SHALL NOT 改变条目的完成状态。
49
+
50
+ #### Scenario: 公告措辞变化后已完成仍为已完成
51
+
52
+ - **WHEN** 上一版 `due.md` 中 `Assignment 5(公告称 Homework assignment 4)` 已标记为 `[x]`,本次同步别名消失
53
+ - **THEN** 新生成的行仍为 `[x]`
54
+
55
+ ### Requirement: 来源分歧在运行时可观测
56
+
57
+ `bb-sync due` SHALL 在运行汇总中报告本次发现的分歧条目数量,机器可读输出 SHALL 为每个条目提供别名列表。
58
+
59
+ #### Scenario: 终端汇总提示分歧
60
+
61
+ - **WHEN** 本次同步产生 1 条带别名的待办
62
+ - **THEN** 终端汇总包含「发现 1 处来源分歧」类提示
63
+
64
+ #### Scenario: JSON 输出包含别名
65
+
66
+ - **WHEN** 以 `--json` 运行时存在带别名的待办
67
+ - **THEN** 该条目的 JSON 对象包含非空的 `aliases` 数组
@@ -0,0 +1,39 @@
1
+ # Tasks
2
+
3
+ ## 1. 放宽公告作业名识别
4
+
5
+ - [x] 1.1 把 `_ASSIGNMENT_NAME_RE` 改为「作业词 + 可选编号 + 可选主题」,并把守卫改成「截止行、上一行或公告标题含作业词」,验证:`pytest tests/test_units.py -k announcement` 全绿,且新增的 `Homework on Probabilistic Reasoning` 用例解析出 `2026-11-15`
6
+ - [x] 1.2 补两条回归夹具(CSC5010 无编号作业公告、DDA5002 `Homework assignment 4` 公告),验证:两条公告分别得到 `assignment` 条目,日期为 `2026-11-15` 与 `2026-11-13`
7
+ - [x] 1.3 补反例测试确认守卫仍然有效(`Week 4 Materials Uploaded` 的截止提醒不产生待办;标题含 `homework` 但截止行日期指别的事也不收录),验证:新增用例通过且未放宽既有用例
8
+ - [x] 1.4 更新 `scraper.py` 中与作业名相关的注释与文档字符串,说明编号可选、语境守卫存在,验证:`ruff check src/bb_sync/blackboard/scraper.py` 通过
9
+
10
+ ## 2. 合并语义改为对账
11
+
12
+ - [x] 2.1 给 `DueItem` 增加 `aliases: tuple[str, ...] = ()` 与 `as_dict()` 的 `aliases` 字段,验证:`pytest tests/test_units.py -k due_item` 与序列化用例通过
13
+ - [x] 2.2 实现标题归一化(编号抽取 + 主题词集合)与分歧判定,验证:`HW4` vs `Homework assignment 4` 判为无分歧、`Assignment 5` vs `Homework assignment 4` 判为分歧,单测覆盖 D2 表格四种情形
14
+ - [x] 2.3 改写 `merge_due_items()`:命中同一事件时用 `dataclasses.replace()` 把公告标题写入 To Do 条目的 `aliases`,验证:合并结果保留 To Do 的日期与类型,且别名非空
15
+ - [x] 2.4 补合并层测试(冲突保留别名、无对应 To Do 时独立补入、措辞差异不产生别名),验证:`pytest tests/test_units.py -k merge` 通过
16
+ - [x] 2.5 更新 CHANGELOG 中该能力的条目草稿(2.3.0 段落),验证:条目描述与 D1/D2 决策一致
17
+
18
+ ## 3. 渲染与可观测性
19
+
20
+ - [x] 3.1 `build_due_markdown()` 在带别名条目后追加 `(公告称 <别名>)`,并转义 Markdown,验证:渲染用例断言行内容含 `Assignment 5(公告称 Homework assignment 4)`
21
+ - [x] 3.2 `due.md` 头部增加括号含义说明行,验证:`test_build_due_markdown_*` 用例断言该行存在
22
+ - [x] 3.3 确认 `_item_signature()` 仍只含基础标题,并补测试:上一版为 `[x]`、本次别名消失,新行为 `[x]`,验证:该用例通过
23
+ - [x] 3.4 `service` 汇总输出「发现 N 处来源分歧」,验证:带别名的扫描结果在汇总字符串里出现该提示,并有对应单测
24
+ - [x] 3.5 `--json` 输出验证:存在别名时条目含非空 `aliases` 数组,验证:JSON 快照或断言用例通过
25
+
26
+ ## 4. 文档与版本
27
+
28
+ - [x] 4.1 更新 `README.md` 中 `due.md` 的示例与说明,加入别名括号样例,验证:示例与 `build_due_markdown()` 实际输出一致
29
+ - [x] 4.2 在 `CHANGELOG.md` 写入 2.3.0 段落,标注 `due.md` 行格式与 `as_dict()` 字段的 BREAKING 变化,验证:`pyproject.toml` 版本号同步为 2.3.0 且 CHANGELOG 段落存在
30
+
31
+ ## 5. 集成验证
32
+
33
+ - [x] 5.1 更新 `openspec` 变更后跑 `pytest` 全量,验证:全部测试通过,无既有用例被静默放宽
34
+ - [x] 5.2 在课程仓库跑一次真实 `bb-sync due`,验证:`due.md` 中 DDA5002 的 11-13 条目出现 `(公告称 Homework assignment 4)`,且 CSC5010 出现 `2026-11-15` 的作业条目
35
+ - [x] 5.3 跑 `openspec validate due-item-source-conflicts --strict`,验证:无 error 与 warning
36
+
37
+ ## Workflow follow-up
38
+
39
+ - 实现完成并经用户确认后,用 `openspec archive due-item-source-conflicts` 归档。
@@ -0,0 +1,32 @@
1
+ schema: spec-driven
2
+
3
+ # Project context (optional)
4
+ # Add only constraints that should guide OpenSpec artifacts and workflows.
5
+ # Include constraints an agent cannot infer by reading the code.
6
+ # Keep general project documentation and discoverable codebase facts out.
7
+ # Example:
8
+ # context: |
9
+ # Designs and tasks must cover Windows, macOS, and Linux
10
+ # Write all artifacts in Spanish
11
+
12
+ # Per-artifact rules (optional)
13
+ # Add custom rules for specific artifacts.
14
+ # Example:
15
+ # rules:
16
+ # proposal:
17
+ # - Keep proposals under 500 words
18
+ # - Always state what is out of scope
19
+ # tasks:
20
+ # - Break tasks into chunks of max 2 hours
21
+
22
+ # Per-operation guidance (optional)
23
+ # Add advisory guidance for how apply and archive work should be conducted.
24
+ # This is separate from artifact rules above.
25
+ # Example:
26
+ # operations:
27
+ # apply:
28
+ # guidance:
29
+ # - Keep test summaries concise
30
+ # archive:
31
+ # guidance:
32
+ # - Summarize the archive outcome before finishing
File without changes
@@ -0,0 +1,58 @@
1
+ # announcement-due-extraction Specification
2
+
3
+ ## Purpose
4
+ 从 Blackboard 公告正文中识别可预期的待办日期(考试、测验、作业截止),产出可合并进待办清单的条目,并在无法可靠识别时放弃而不是猜测。
5
+
6
+ ## Requirements
7
+
8
+ ### Requirement: 作业截止行支持无编号的作业名
9
+
10
+ 系统 SHALL 把「作业词 + 可选编号 + 可选主题」形式的名称识别为作业名,包括
11
+ `Homework on Probabilistic Reasoning`、`Problem Set 2`、`Lab 3`;编号 SHALL 不再是无编号作业名被收录的前提。
12
+
13
+ #### Scenario: 无编号的主题式作业名被收录
14
+
15
+ - **WHEN** 公告标题为 `Homework on Probabilistic Reasoning`,正文含一行 `Deadline is Nov 15, 2026`
16
+ - **THEN** 该公告产生一条 `assignment` 待办,日期为 `2026-11-15`,标题取自公告标题
17
+
18
+ #### Scenario: 编号式作业名继续被收录
19
+
20
+ - **WHEN** 公告正文含 `- **HW4:** Due by **Oct. 31 at 11:59 PM**.`
21
+ - **THEN** 产生一条 `assignment` 待办,标题含 `HW4`,日期为 `2026-10-31`
22
+
23
+ #### Scenario: 截止行上一行可提供作业名
24
+
25
+ - **WHEN** 正文先有一行 `2. Coding Assignment 2 Release`,下一行为 `- Deadline: 23:59, November 2, 2026`
26
+ - **THEN** 产生一条 `assignment` 待办,标题含 `Assignment 2`,日期为 `2026-11-02`
27
+
28
+ ### Requirement: 无作业语境的日期不产生待办
29
+
30
+ 系统 SHALL 在公告标题与截止行相邻行都不含作业词时放弃该截止行,即使该行同时含日期和 `due`/`deadline` 措辞。
31
+
32
+ #### Scenario: 材料上传公告里的截止提醒被忽略
33
+
34
+ - **WHEN** 公告标题为 `Week 4 Materials Uploaded`,正文为 `Reminder: the deadline is 11:59 pm, Oct. 29th.`
35
+ - **THEN** 该公告不产生 `assignment` 待办
36
+
37
+ ### Requirement: 公告待办受发布日期与保留期约束
38
+
39
+ 系统 SHALL 丢弃日期早于公告发布日期、或早于当前日期超过 7 天的公告推断条目。
40
+
41
+ #### Scenario: 过期的测验不再进入待办
42
+
43
+ - **WHEN** 当前日期为 `2026-11-04`,公告提到 `2026-10-20` 的 quiz
44
+ - **THEN** 该 quiz 不进入待办
45
+
46
+ #### Scenario: 保留期内的逾期作业仍保留
47
+
48
+ - **WHEN** 当前日期为 `2026-11-04`,作业公告的截止日期为 `2026-10-31`
49
+ - **THEN** 该 `assignment` 待办仍被收录,供用户确认是否已交
50
+
51
+ ### Requirement: 同一公告内的重复事项只收录一次
52
+
53
+ 系统 SHALL 按「日期 + 标题」对同一公告解析出的条目去重。
54
+
55
+ #### Scenario: 正文两处提到同一截止
56
+
57
+ - **WHEN** 同一公告的正文和标题各出现一次 `Homework 4`、`Due Nov 2, 2026`
58
+ - **THEN** 只产生一条 `assignment` 待办
@@ -0,0 +1,71 @@
1
+ # due-item-reconciliation Specification
2
+
3
+ ## Purpose
4
+ 把 Blackboard To Do 模块与公告两个来源的待办对账成一份清单:日期与类型取 To Do,来源之间的叫法分歧保留并显式呈现给用户,而不是静默丢弃其中一方。
5
+
6
+ ## Requirements
7
+
8
+ ### Requirement: 同一事件的公告条目作为别名保留
9
+
10
+ 当 To Do 条目与公告条目属于同一课程、同一天且指向同一件事时,合并结果 SHALL 保留 To Do 条目的日期与类型,并 SHALL 记录公告条目的标题作为别名。
11
+
12
+ #### Scenario: 编号冲突时保留公告标题
13
+
14
+ - **WHEN** To Do 条目为 `Assignment 5`、日期 `2026-11-13`,公告条目为 `Homework assignment 4`、同日
15
+ - **THEN** 合并结果为一条待办,日期 `2026-11-13`,标题 `Assignment 5`,别名包含 `Homework assignment 4`
16
+
17
+ #### Scenario: 没有对应 To Do 时公告条目直接补入
18
+
19
+ - **WHEN** 公告推断出 `2026-11-15` 的作业截止,而 To Do 模块没有当天条目
20
+ - **THEN** 清单中出现一条独立待办,日期 `2026-11-15`,无别名
21
+
22
+ ### Requirement: 仅在实质分歧时标注
23
+
24
+ 系统 SHALL 把待办标题归一为「编号 + 主题词集合」后比较,只在编号不同、或双方编号相同但主题矛盾时标记分歧;纯措辞差异 SHALL NOT 标记。
25
+
26
+ #### Scenario: 措辞差异不标记
27
+
28
+ - **WHEN** To Do 条目为 `HW4`,公告条目为 `Homework assignment 4`,同日
29
+ - **THEN** 合并结果不产生别名标注
30
+
31
+ #### Scenario: 编号不同标记分歧
32
+
33
+ - **WHEN** To Do 条目为 `Assignment 5`,公告条目为 `Homework assignment 4`,同日
34
+ - **THEN** 合并结果带有一个分歧标记
35
+
36
+ ### Requirement: due.md 渲染显式标注来源分歧
37
+
38
+ `due.md` 中带别名的条目 SHALL 在标题后追加 `(公告称 <别名>)`,且文件头部 SHALL 说明该括号的含义。
39
+
40
+ #### Scenario: 冲突条目渲染
41
+
42
+ - **WHEN** 待办标题为 `Assignment 5`,别名为 `Homework assignment 4`,日期 `2026-11-13`
43
+ - **THEN** `due.md` 该行包含 `Assignment 5(公告称 Homework assignment 4)`
44
+
45
+ ### Requirement: 别名不影响勾选状态
46
+
47
+ 完成状态的签名 SHALL 只由「日期 + 课程 + 基础标题」构成,别名出现、消失或变化 SHALL NOT 改变条目的完成状态。
48
+
49
+ #### Scenario: 公告措辞变化后已完成仍为已完成
50
+
51
+ - **WHEN** 上一版 `due.md` 中 `Assignment 5(公告称 Homework assignment 4)` 已标记为 `[x]`,本次同步别名消失
52
+ - **THEN** 新生成的行仍为 `[x]`
53
+
54
+ ### Requirement: 来源分歧在运行时可观测
55
+
56
+ `bb-sync due` 与 `bb-sync run` SHALL 在运行汇总中报告本次发现的分歧条目数量;终端的待办明细行 SHOULD 在标题后标注公告里的另一种叫法;机器可读输出 SHALL 为每个条目提供别名列表。
57
+
58
+ #### Scenario: 终端汇总提示分歧
59
+
60
+ - **WHEN** 本次同步产生 1 条带别名的待办
61
+ - **THEN** 终端汇总包含「发现 1 处来源分歧」类提示
62
+
63
+ #### Scenario: 待办明细标注分歧
64
+
65
+ - **WHEN** `bb-sync run` 打印待办明细,其中一条标题为 `Assignment 5`、别名为 `Homework assignment 4`
66
+ - **THEN** 该行标题显示为 `Assignment 5(公告称 Homework assignment 4)`
67
+
68
+ #### Scenario: JSON 输出包含别名
69
+
70
+ - **WHEN** 以 `--json` 运行时存在带别名的待办
71
+ - **THEN** 该条目的 JSON 对象包含非空的 `aliases` 数组
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bb-sync"
7
- version = "2.2.1"
7
+ version = "2.3.0"
8
8
  description = "港中深(CUHK-Shenzhen)Blackboard 课件下载与课程资源自动同步:登录 ADFS SSO,增量下载课件/作业/指导、公告与待办"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }