diff --git a/CHANGELOG.md b/CHANGELOG.md index 080fe963..0a496ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ GitHub Release 的发布说明;版本与发布流程见 [docs/RELEASE.md](docs ## [1.0.0] - 2026-10-16 +首个正式版。 + +- 键盘和设置页统一为蓝色强调色,工具栏图标加深;修复「切换键盘」和「工具」页内容偏左的问题。 +- 语音模型页:新增官方模型下载地址(可复制),并支持直接导入官方 `.tar.bz2` 模型包,不用再自己解压。 +- 短信验证码:键盘开着时收到新短信,验证码候选会自动刷新;能识别更多写法(换行、全角数字、带空格或连字符的分组、英文标签),电话、日期、订单号不再误识别。 +- 九键拼音补全了更多拼音组合,开启模糊音后 `li`、`xiang` 等读法也能出现。 +- 修复在切换布局时点空格偶尔不输入的问题。 +- 手写输入入口暂时隐藏。 +- 恢复豆包对齐前的按键布局和键帽间距。 + +### 开发记录 + +下面是给维护者的细节,不进入 GitHub Release 的发布说明。 + +- 切换布局时,待处理的行重建会把 `View` 在松手时排进队列的点击一并取消,空格轻点因此丢失。重建轮询现在在看到手指抬起后让出一拍,点击先跑完再重建(`ImeKeyboardView.pendingRowRebuildPoll`)。 + - 键盘与设置页统一蓝色强调色,加深工具栏图标并恢复原工具栏顺序;修复切换键盘与工具页网格偏左,窗口变宽或变窄时保持居中。语音模型页统一内容与卡片边距,增加可复制的官方下载地址及导入步骤。 - 短信验证码候选现在会自己刷新。此前顶栏那条「验证码 123456,点击输入」只在键盘弹出或设置变化时算一次,键盘开着时新收到的短信不会出现;现在注册短信内容变化的监听(`Telephony.Sms.CONTENT_URI`),200 毫秒防抖后重算一次候选,权限被收回或键盘收起时自动注销。取出验证码的规则也放宽到实际收到的各种写法:标签与验证码之间可以换行、中文冒号与全角数字都能认,数字可以带空格或连字符分组(`876 523`),英文标签(`code`、`passcode`、`OTP`、`verification code` 等)同样识别;同时收紧误报,电话、日期、订单号和「共/请于/有效期」这类状态句子里的数字不再当验证码。 diff --git a/README.md b/README.md index d3224ca3..4c144018 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,15 @@

A local-first Chinese input method for Android.

- Release + Release Android CI License: GPL-3.0-only

English · 简体中文

-> **Beta software (0.0.x).** -> Features, settings and data formats can change between releases. -> Do not use openIME as your only keyboard. +> **openIME 1.0 is the first stable release.** +> Few devices have been tested. Keep another keyboard installed as a fallback. > Report problems in [Issues](https://github.com/Slacker-LLC/openIME/issues). openIME is a system keyboard for Android. @@ -52,23 +51,28 @@ The app has no `INTERNET` permission, so your text cannot leave the phone. ## Features -- **Keyboards:** 26-key pinyin, nine-key pinyin, stroke, English 26-key, digits and symbols. +- **Keyboards:** 26-key pinyin, nine-key pinyin, double pinyin (Xiaohe and Ziranma), Wubi, stroke, English 26-key, digits and symbols. Emoji, symbols, clipboard history, quick phrases, text editing and a floating keyboard are also available. - **Pinyin engine:** [librime](https://github.com/rime/librime) with about 900,000 Rime Ice dictionary entries. The dictionary is compiled at build time. The full dictionary is ready a few seconds after the first start. -- **Pinyin input:** full pinyin, abbreviations, manual word splitting, fuzzy pinyin, user-word learning and simplified/traditional conversion. +- **Pinyin input:** full pinyin, abbreviations, manual word splitting, fuzzy pinyin (15 groups), user-word learning, poem-line suggestions and simplified/traditional conversion. - **Nine-key input:** a column on the left lists the pinyin for the next character. Tap one syllable to lock it. - **Voice input:** hold the space key and speak. openIME inserts the text when you release the key. The bilingual (Chinese and English) model is in the APK and works without a network. Options remove filler words and replace punctuation with spaces. + You can also import your own streaming model. + 「设置 → 语音模型」 (Settings → Voice model) lists the official download links to copy, and it imports the official `.tar.bz2` packages as they are. - **Gestures:** swipe up on the delete key to clear the text. Swipe left or right on the space key to move the cursor. +- **Quick format input:** optional switches in the settings for spaces around numbers and English words, + a period on a double space, calculator results in the candidate bar, first-letter capitalization and a key-sound volume. - **Emoji suggestions:** after you choose a word, the suggestion bar shows related emoji. The word list is in the APK. - **Autofill (Android 11 and later):** account names and codes from your password manager appear in the toolbar. +- **SMS code chip (optional):** the toolbar shows a code from a new text message, in the common formats, and one tap types it. - **Numbers and symbols:** each letter key shows a number or symbol. Swipe up to enter it. Press and hold to open a popup, then slide to a cell and release. - **Display support:** portrait, landscape, tablets, foldables, dark theme and large font sizes. @@ -104,7 +108,7 @@ You can install a new release over an old one. ### Known limitations -- Handwriting input has no recognition engine yet. The entry point is hidden. +- Handwriting input is hidden for now. The code stays in the app, but the entry is off. - The nine-key keyboard does not work with a physical keyboard. - Some vendor Android versions limit background input methods. Few devices have been tested. - Android does not install a version that is older than the installed one. diff --git a/README.zh-CN.md b/README.zh-CN.md index 310793dd..b7adcbca 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,15 +7,15 @@

本地优先的 Android 中文输入法

- 版本 + 版本 Android CI 许可证

English · 简体中文

-> **当前处于 Beta 测试阶段(0.0.x)。** 功能和稳定性仍在验证,接口与数据格式可能调整, -> 请不要把它作为日常唯一的输入法。欢迎通过 [Issues](https://github.com/Slacker-LLC/openIME/issues) 反馈问题。 +> **openIME 1.0 是首个正式版。** 实机验证的机型还不多,请保留另一款输入法作为备用。 +> 欢迎通过 [Issues](https://github.com/Slacker-LLC/openIME/issues) 反馈问题。 openIME 是一款独立的 Android 系统输入法。拼音候选、用户词库学习和语音识别全部在设备本地完成, 应用**没有 `INTERNET` 权限**,输入内容不会离开手机。 @@ -43,14 +43,16 @@ openIME 是一款独立的 Android 系统输入法。拼音候选、用户词库 ## 功能 -- **键盘**:26 键拼音、九键拼音、笔画、英文 26 键、数字与符号;Emoji、符号、剪贴板、文本编辑、浮动键盘。 -- **拼音**:全拼、简拼、手动分词、候选展开、用户词库学习、简繁转换。 +- **键盘**:26 键拼音、九键拼音、双拼(小鹤、自然码)、五笔、笔画、英文 26 键、数字与符号;Emoji、符号、剪贴板、文本编辑、浮动键盘。 +- **拼音**:全拼、简拼、手动分词、候选展开、模糊音(15 组)、用户词库学习、诗句联想、简繁转换。 引擎为 librime,内置约 90 万条 Rime Ice 词典记录,打包时已预先编译;安装或升级后第一次打开键盘,几秒内即可用完整词典。 - **九键**:输入时左栏列出下一个字的拼音,一个字选一个音节,选定后自动移到下一个字。 -- **语音输入**:长按空格说话,松手后识别并上屏;使用内置的中英双语模型,不联网;可去掉“嗯”“呃”等语气词,也可把标点写成空格。 +- **语音输入**:长按空格说话,松手后识别并上屏;使用内置的中英双语模型,不联网;可去掉“嗯”“呃”等语气词,也可把标点写成空格;也可以导入自己的流式模型:「设置 → 语音模型」列出可复制的官方下载地址,官方 `.tar.bz2` 包不用解压,直接导入。 - **手势**:删除键上滑清空;空格左右滑动移动光标,滑动时底行其他按键锁定。 +- **快捷格式输入**:设置里可选的开关,包括数字、英文前后加空格,双击空格输入句号,候选栏直接给算式结果,首字母自动大写和按键音量。 - **表情联想**:选词后联想栏先给出相关表情(开心 → 😊),词表内置,不联网。 - **自动填充**:Android 11+ 上,密码管理器的账号、验证码直接显示在键盘工具栏位置。 +- **短信验证码**(可选):工具栏显示新短信里的验证码,常见写法都能识别,点一下即输入。 - **数字和符号**:26 键字母键右上角印着数字和符号,上滑直接输入;长按弹出选择面板,滑到目标松手输入。 - **适配**:横竖屏、平板与折叠屏、深色模式、大字号;终端、远程桌面、游戏等原始按键输入框; 外接键盘可直接打拼音。详见 [兼容性说明](docs/COMPATIBILITY.md)。 @@ -76,7 +78,7 @@ openIME 是一款独立的 Android 系统输入法。拼音候选、用户词库 ### 已知限制 -- 手写输入尚未接入识别引擎,入口默认隐藏。 +- 手写输入暂时隐藏:代码仍在应用里,入口已关闭。 - 九键暂不支持与外接键盘同时使用。 - 部分厂商系统对输入法的后台限制不同,尚未在大量真机上验证。 - 版本号小于此前已装版本时,系统会拒绝覆盖安装;需要先卸载(卸载前可导出用户数据)。 diff --git a/SECURITY.md b/SECURITY.md index ad056c00..8116dbe2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -30,7 +30,7 @@ Do not publish details before the fix is released. | Version | Status | |---|---| -| The latest release (beta or stable) | Receives security fixes. We publish them as a new patch or beta version and list them in the change log. | +| The latest stable release | Receives security fixes. We publish them as a new patch version and list them in the change log. | | Older releases and debug builds | Not maintained. Update to the latest release. | To verify an APK, compare its SHA-256 checksum with the release notes. diff --git a/docs/APP_UI_SPEC.md b/docs/APP_UI_SPEC.md index 0a2baed7..a7ecf15a 100644 --- a/docs/APP_UI_SPEC.md +++ b/docs/APP_UI_SPEC.md @@ -60,6 +60,7 @@ Files: `values/colors.xml` and `values-night/colors.xml`. Panels in the keyboard use the keyboard tokens (`ImeTheme.IOS`). Switches and quick switches in the keyboard use the keyboard accent color. +The keyboard and the app use the same accent: `SetupUi.accent` equals the keyboard theme's `tokens.primary` in light and dark. ## Layout @@ -114,8 +115,19 @@ Switches and quick switches in the keyboard use the keyboard accent color. The panel has no voice input (hold space instead) and no keyboard switch (the toolbar has one). - The last group in preferences and in the keyboard settings is 关于与数据 ("About and data"). It has two rows: 关于 ("About": version, privacy, diagnostics) and 数据管理 ("Data management": export, import, pre-uninstall note). +- The 切换键盘 ("Switch keyboard") and tools pages center their tile rows in the window. + The rows stay centered when the window width changes. - The title row has the same background as the panel. The back button has no background and shows feedback only when pressed. +## Voice model page + +- The page uses the same 16 dp side margin as the other pages, and its cards use the same width and corner radius. + Text and cards do not drift left or right of the page edge. +- It lists the built-in model and the imported models, with the import button. +- Below the import button it lists the official model packages with their sizes. + Each address can be copied; the page does not download anything. + The text says that the official `.tar.bz2` package imports as it is. + ## Width and font changes - In preferences, segmented controls and sliders always show the label above and the control below. Narrow screens need no layout switch. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 10e51254..809c1abd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -159,7 +159,8 @@ Split them next. Follow the package rules above and add no cross-package depende | 26-key pinyin | `ImeKeyboardView`, `CandidatePipeline`, Rime | Production | | Nine-key pinyin | `ImeKeyboardView`, `NineKeyLocalDecoder`, Rime | Production. No English nine-key. | | Stroke | `StrokeKeyboardRenderer`, `StrokeLexicon` | Production | -| Wubi 26-key | `ImeKeyboardView`, `CandidatePipeline`, Rime (`wubi86`) | Production. The table ships only in builds with the local data. | +| Double pinyin | `DoublePinyinScheme`, `ImeKeyboardView`, Rime | Production. Xiaohe and Ziranma schemes. | +| Wubi 26-key | `ImeKeyboardView`, `CandidatePipeline`, Rime (`wubi86`) | Production. | | English 26-key | `CandidatePipeline`, `CandidateEngine` | Production | | Digits, phone, decimal | `EditorInfoAdapter`, `NumericKeyboardRenderer` | Production | | Symbols | `SymbolCatalog`, `CustomSymbolRepository` | Production | @@ -167,8 +168,9 @@ Split them next. Follow the package rules above and add no cross-package depende | Clipboard history | `ClipboardHistoryRepository`, `InputConnectionGateway` | Production. See the privacy rules above. | | Quick phrases | `QuickPhraseRepository`, `QuickPhraseEditActivity` | Production | | Text editing | `InputConnectionGateway` | Production. Available actions depend on the editor. | -| Voice | `VoiceModelLifecycleManager`, sherpa-onnx | Production. Hold space to speak. | -| Handwriting | `HandwritingPadView`, `StrokeMatchHandwritingProvider` | Stroke-table recognition. Builds without the local table hide the entry. | +| Voice | `VoiceModelLifecycleManager`, sherpa-onnx | Production. Hold space to speak. The voice model page imports `.zip`, `.tar`, `.tar.gz` and `.tar.bz2` packages. | +| SMS code chip | `SmsCodeExtractor`, `ImeTopZone` | Production, optional. A content observer on the SMS provider refreshes the chip; it is off until the user grants read-SMS. | +| Handwriting | `HandwritingPadView`, `StrokeMatchHandwritingProvider` | Hidden. `HandwritingFeaturePolicy.ENTRY_OFFERED` is `false`; set it to `true` to show the entry again. | | Floating keyboard | `FloatingKeyboardController`, `LocalVoiceImeService` | Production | | Settings | `ImeSettingsRepository`, settings UI | Production | diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 46b2c97c..14ff4685 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -34,6 +34,19 @@ The service clears the response when the user leaves the field. Android 10 and earlier, and providers without inline support, use the system dropdown menu. +## SMS code chip + +The chip is optional and off by default. +When the user turns it on, openIME asks for the read-SMS permission and reads only messages from the last 5 minutes. +A content observer on `Telephony.Sms.CONTENT_URI` recomputes the chip 200 ms after the inbox changes, so a message that arrives while the keyboard is open shows at once. +The observer is removed when the permission is revoked or the keyboard hides. +`SmsCodeExtractor` accepts a label (Chinese or English: `code`, `passcode`, `OTP`, `verification code` and others) followed by a code. +The label and the code can be on different lines, the colon can be full-width, and digits can be full-width or grouped with spaces or hyphens. +It ignores phone numbers, dates, order numbers and status sentences. +The chip does not show in password fields or in fields that ask for no personalised learning. + +Verification: `SmsCodeExtractorTest`. + Verification: `InlineChipTrackerTest` and `scripts/beta3_e2e.py autofill`. The debug build includes a test provider (`TestAutofillService`) and `AutofillTestActivity`. diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index d4dc960a..a272d5e1 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -406,3 +406,16 @@ Add new entries at the end. - **The nine-key reading list and its lattice follow the reference's size, not this port's caps.** The reference enumerates the whole open digit tail on every key press, and nothing along the way cuts a list. Its left-list builder (`libshell.so` `0x5ba1e0`) pushes one string per marked syllable id (`0x5ba574 bl 0x3ce68c` → `0x5ba5f0 vector::push_back`), then one per letter of the open head key plus one per `+0x20` twin (`0x5ba650 ldrb w8,[x21]` → push at `0x5ba6ac` → `0x5ba6c4 add w8, w8, #0x20` → push at `0x5ba704`), then sorts and dedups (`0x5ba74c bl 0x5bd8a8`); its only vector call is `reserve(marked + 8)` (`0x5ba430`), the handler assigns the whole vector into the task result (`0x467034`) and the UI assigns the whole table into the filter list (`0x2d26d0`). Decoding `syllable.dat` end to end gives 64 → 10 items, 9426 → 18, 5426 → 15, against 3 / 9 / 8 here. Fuzzy is a digit-level widening of the lattice rather than a lookup option: every data3 record carries the smallest rule mask that reaches its spelling and pass2 (`0x5b962c`) admits it when `mask ⊆ 用户fuzzy` — node `64` holds `li` with mask `0x0008`, `426` holds `fang` `0x0050` and `hang` `0x0040`, and with every rule on `9426` runs from 19 readings to 32. The port now lists the head key's syllable-capable letters for a tail of any length, raises the rail bound 12 → 48, the reading bound 12 → 24 and the librime reading count 6 → 12, and files one fuzzy-variant entry per syllable under its plain spelling's digits carrying the rules it needs, admitted only while every one of them is switched on. *Reason:* the reference's own shape is a table keyed by digits whose records carry a mask; copying that shape, instead of filtering paths after the fact, is what makes the extra readings appear exactly when the user switched those rules on and never before. The rail keeps plain spellings only, which is what the reference's left builder does (it takes records whose mask is zero, `0x5ba378`), so a fuzzy reading reaches the candidates and the splits without inventing a rail item the reference would not show. The bounds that remain are stated as such: one reading costs one librime JNI query here, so 12 is a latency guard rather than a coverage decision, and the rail's 48 is the scroll list's own ceiling. Two pieces of the reference's list are deliberately not ported: the upper-case letter twins exist for the nine-key English the owner ruled out, and the leading digit item's rendered string is unverified — the id table names it `digit0..digit9` but no pushed string was checked byte for byte, so it is left out rather than guessed. One correction is recorded instead of followed: `ENGINE-SPEC.md:222` calls the `0x6c0508` family a nine-key fuzzy path, while the disassembly places it in the wubi engine (`oime_engine::wubi::Engine` vtable, caller `0x6b394c`). +- **`.tar.bz2` model packages import through Apache Commons Compress.** + The official sherpa-onnx releases publish models as `tar.bz2`. The importer detects the format by the `BZh` magic (plus a level digit 1–9) and by the extensions `.tar.bz2`, `.tbz2` and `.tbz`, and the file picker also accepts `application/x-bzip2`. + Decompression uses `BZip2CompressorInputStream` from `org.apache.commons:commons-compress:1.27.1` (Apache-2.0), because Android's `java.util.zip` reads only zip and gzip. The path-traversal and size checks are the same as for the other formats. + *Reason:* users had to unpack and rename the official package by hand. One small, well-known library removes that step. The app still downloads nothing: the page only lists addresses to copy. +- **The SMS chip follows the inbox with a content observer.** + The chip was computed only when the keyboard opened or a setting changed, so a message that arrived with the keyboard open never showed. The keyboard now registers a `ContentObserver` on `Telephony.Sms.CONTENT_URI`, recomputes after a 200 ms debounce, and unregisters when the permission is revoked or the keyboard hides. + *Reason:* a one-time password usually arrives while the user is already in the field. The permission stays opt-in and the 5-minute window stays. +- **Handwriting is hidden by one flag.** + `HandwritingFeaturePolicy.ENTRY_OFFERED` is `false` (owner's request, 2026-10-16). The provider, the pad and the stroke table stay in the build, so setting the flag back to `true` restores the entry in the keyboard switcher and the tools page. + *Reason:* the stroke-matching pad is still rough. Hiding the entry costs one constant; removing the code would cost the work. +- **A pending row rebuild yields one turn before it runs.** + `View.onTouchEvent` posts the click on `ACTION_UP`, and detaching a view cancels that post. The pending-rebuild poll could be due before the post, so a rebuild right after a Space tap swallowed the tap. The poll now sees the finger lift, posts itself once more, and rebuilds on the second run, after the click. + *Reason:* the earlier fix kept the view alive while the finger was down; this closes the gap between the lift and the posted click. diff --git a/docs/LOCAL_VOICE_MODEL.md b/docs/LOCAL_VOICE_MODEL.md index bb571626..80af3353 100644 --- a/docs/LOCAL_VOICE_MODEL.md +++ b/docs/LOCAL_VOICE_MODEL.md @@ -102,7 +102,7 @@ The model never changes during a recording. ## Import your own streaming model -The app never downloads anything. 「设置 → 语音模型」 imports a local package: +The app never downloads anything. 「设置 → 语音模型」 lists the official sherpa-onnx download links (`VoiceModelDownloads`) for the user to copy into a browser, and imports a local package: - Archives: `.zip`, `.tar`, `.tar.gz`, `.tgz`, `.tar.bz2` / `.tbz2` / `.tbz`. The official sherpa-onnx releases publish their model packages as `tar.bz2`, so those import as they arrive, with no unpacking or renaming. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 5c026945..c492d6ed 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -55,6 +55,10 @@ We maintain only the newest MINOR line. Security fixes ship as PATCH versions. Empty sections and versions or dates in the wrong order are not allowed. `release_check.py check` enforces this in CI, so the version and the change log change together. - The body of this section is the GitHub release note. Write it so that users can understand it. + Keep it short: one line for each change that a user can see. + Put maintainer detail (addresses, reverse-engineering notes, internal causes) under the heading `### 开发记录` at the end of the section. + `release_check.py notes` stops before that heading, so the detail stays in the change log and out of the release. + A section with nothing above the heading fails the check. - For a withdrawn version, add ` [YANKED]` after the heading and publish a higher version (a beta increases `N`). ## Release assets diff --git a/docs/REPOSITORY.md b/docs/REPOSITORY.md index 7653e89f..69a614c3 100644 --- a/docs/REPOSITORY.md +++ b/docs/REPOSITORY.md @@ -45,7 +45,7 @@ Do not change a rule only on the web page. The ruleset `release-tags` covers `v*`. Only a repository administrator can create such a tag. After creation, nobody except an administrator can move or delete it. -A release tag is `vX.Y.Z` or `vX.Y.Z-beta.N` and must equal `VERSION`. See [RELEASE.md](RELEASE.md). +A release tag is `vX.Y.Z` (stable) or `vX.Y.Z-beta.N` (beta) and must equal `VERSION`. See [RELEASE.md](RELEASE.md). ## Security settings diff --git a/scripts/release_build.sh b/scripts/release_build.sh index a90b6e80..fd0ac99b 100755 --- a/scripts/release_build.sh +++ b/scripts/release_build.sh @@ -157,7 +157,7 @@ ${LITE_SHA256:+- \`$LITE_NAME\`:**精简版**,不含语音模型,体积小 ### 已知限制 -- 手写输入尚未接入识别引擎,入口默认隐藏;九键暂不支持与外接键盘同时使用。 +- 手写输入暂时隐藏;九键暂不支持与外接键盘同时使用。 - 首次安装后需要完成完整词典部署,期间候选质量略低。 - 发布包只含 arm64-v8a;模拟器请使用 Debug 构建。 - 系统不允许降级或换签名覆盖安装:安装此前版本号更高的包或 Debug 包时,需要先卸载(卸载前可在「设置 → 关于与数据 → 数据管理」导出用户数据)。 diff --git a/scripts/release_check.py b/scripts/release_check.py index 9bd380b5..5103e00c 100755 --- a/scripts/release_check.py +++ b/scripts/release_check.py @@ -166,13 +166,27 @@ def check_changelog(text: str, version: str) -> None: raise ReleaseCheckError(f"CHANGELOG.md marks [{version}] as [YANKED]; bump the version") +DEVELOPMENT_NOTES_HEADING = "### 开发记录" + + def changelog_notes(text: str, version: str) -> str: _, sections = parse_changelog(text) for section in sections: if section.version == version: if not section.body: raise ReleaseCheckError(f"CHANGELOG.md [{version}] is empty") - return section.body + # Everything under the development-notes heading is for maintainers: + # it stays in the change log and is left out of the release notes. + lines = section.body.split("\n") + if DEVELOPMENT_NOTES_HEADING in lines: + lines = lines[: lines.index(DEVELOPMENT_NOTES_HEADING)] + notes = "\n".join(lines).strip() + if not notes: + raise ReleaseCheckError( + f"CHANGELOG.md [{version}] has no user-facing notes above " + f"'{DEVELOPMENT_NOTES_HEADING}'" + ) + return notes raise ReleaseCheckError(f"CHANGELOG.md has no section for {version}") diff --git a/scripts/test_release_check.py b/scripts/test_release_check.py index 42753e0a..89d2f3eb 100644 --- a/scripts/test_release_check.py +++ b/scripts/test_release_check.py @@ -105,6 +105,13 @@ def test_notes_are_exactly_the_section_body(self) -> None: with self.assertRaises(rc.ReleaseCheckError): rc.changelog_notes(CHANGELOG, "9.9.9") + def test_development_notes_stay_out_of_the_release_notes(self) -> None: + text = "## [Unreleased]\n\n## [1.0.0] - 2026-10-02\n- 给用户看的。\n\n### 开发记录\n- 给维护者看的。\n" + self.assertEqual(rc.changelog_notes(text, "1.0.0"), "- 给用户看的。") + only_dev = "## [Unreleased]\n\n## [1.0.0] - 2026-10-02\n### 开发记录\n- a\n" + with self.assertRaisesRegex(rc.ReleaseCheckError, "user-facing"): + rc.changelog_notes(only_dev, "1.0.0") + class BadgingTests(unittest.TestCase): SAMPLE = (