cjk-semantic-split 0.1.3__tar.gz → 0.2.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.
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/.gitignore +32 -3
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/PKG-INFO +1 -1
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/docs/superpowers/specs/2026-07-16-cjk-semantic-split-design.md +167 -75
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/pyproject.toml +2 -2
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/scripts/harvest.py +69 -29
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/__init__.py +1 -1
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_l1.txt +74410 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_l2.txt +6 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_l3.txt +8 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/decomp_patches.txt +6 -0
- cjk_semantic_split-0.2.0/src/chinese_decompose/data/primitives.txt +1510 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/dsl.py +34 -3
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/loader.py +64 -5
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/primitives.py +25 -4
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_decompose.py +7 -7
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_harvest.py +13 -11
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_primitives.py +13 -10
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_smoke.py +1 -1
- cjk_semantic_split-0.1.3/.hypothesis/.gitignore +0 -9
- cjk_semantic_split-0.1.3/.hypothesis/constants/28b72037d99ba6eb +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/2b726390919ba924 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/3f14f0ebb32e0fff +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/447154ffb3443956 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/448896e190e1a139 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/44fdbf8fc7dec723 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/487391a4cba1e887 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/6dc13c365eb3deeb +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/7d0a29d738e12257 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/a686eee4be3e3873 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/a80ac1b9c26466c7 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/cd01bf70c5c5b577 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/dc1aa6aba7748109 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/e6674edc9b17fd2d +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/eb3de9a3db1e637c +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/constants/f2e388b96604c388 +0 -4
- cjk_semantic_split-0.1.3/.hypothesis/unicode_data/15.1.0/charmap.json.gz +0 -0
- cjk_semantic_split-0.1.3/.superpowers/sdd/.gitignore +0 -1
- cjk_semantic_split-0.1.3/.superpowers/sdd/progress.md +0 -43
- cjk_semantic_split-0.1.3/.superpowers/sdd/review-22d28a8..564c3e6.diff +0 -227
- cjk_semantic_split-0.1.3/.superpowers/sdd/review-564c3e6..0b4b3f6.diff +0 -146
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-1-brief.md +0 -237
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-1-report.md +0 -79
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-10-report.md +0 -12
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-11-report.md +0 -7
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-12-report.md +0 -8
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-13-report.md +0 -13
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-14-report.md +0 -7
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-15-report.md +0 -11
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-2-brief.md +0 -161
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-2-report.md +0 -76
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-3-brief.md +0 -249
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-3-report.md +0 -74
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-4-report.md +0 -9
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-5-report.md +0 -19
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-6-report.md +0 -13
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-7-report.md +0 -21
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-8-report.md +0 -14
- cjk_semantic_split-0.1.3/.superpowers/sdd/task-9-report.md +0 -19
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/decomp_l1.json +0 -1
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/decomp_l2.json +0 -1
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/decomp_l3.json +0 -5
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/decomp_patches.json +0 -1
- cjk_semantic_split-0.1.3/src/chinese_decompose/data/primitives.json +0 -1
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/.github/workflows/ci.yml +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/LICENSE +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/README.md +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/docs/component-position-encoding.md +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/docs/decomposition-logic.md +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/docs/superpowers/plans/2026-07-16-cjk-semantic-split-implementation.md +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/scripts/__init__.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/scripts/review_conflicts.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/__main__.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/api.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/errors.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/split.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/src/chinese_decompose/structures.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/__init__.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/conftest.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_api.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_cli.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_compose.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_dsl.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_loader.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_properties.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_read_structure.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_split.py +0 -0
- {cjk_semantic_split-0.1.3 → cjk_semantic_split-0.2.0}/tests/test_structures.py +0 -0
|
@@ -4,6 +4,8 @@ __pycache__/
|
|
|
4
4
|
*$py.class
|
|
5
5
|
*.so
|
|
6
6
|
.Python
|
|
7
|
+
|
|
8
|
+
# Build / distribution / packaging
|
|
7
9
|
build/
|
|
8
10
|
develop-eggs/
|
|
9
11
|
dist/
|
|
@@ -17,17 +19,20 @@ sdist/
|
|
|
17
19
|
var/
|
|
18
20
|
wheels/
|
|
19
21
|
*.egg-info/
|
|
20
|
-
|
|
22
|
+
*.egg-info
|
|
21
23
|
*.egg
|
|
22
24
|
MANIFEST
|
|
25
|
+
.installed.cfg
|
|
23
26
|
|
|
24
27
|
# Virtual envs
|
|
25
28
|
.venv/
|
|
26
29
|
venv/
|
|
27
30
|
env/
|
|
28
31
|
ENV/
|
|
32
|
+
.env
|
|
33
|
+
.env.local
|
|
29
34
|
|
|
30
|
-
# Test / coverage
|
|
35
|
+
# Test / coverage / type-checker caches
|
|
31
36
|
.pytest_cache/
|
|
32
37
|
.coverage
|
|
33
38
|
.coverage.*
|
|
@@ -35,10 +40,34 @@ htmlcov/
|
|
|
35
40
|
.tox/
|
|
36
41
|
.cache
|
|
37
42
|
coverage.xml
|
|
43
|
+
coverage.txt
|
|
38
44
|
*.cover
|
|
45
|
+
.hypothesis/
|
|
46
|
+
.mypy_cache/
|
|
47
|
+
.pyre/
|
|
48
|
+
.pytype/
|
|
49
|
+
.ruff_cache/
|
|
39
50
|
|
|
40
|
-
#
|
|
51
|
+
# IDEs / editors
|
|
41
52
|
.idea/
|
|
42
53
|
.vscode/
|
|
54
|
+
.vscode-test/
|
|
43
55
|
*.swp
|
|
56
|
+
*.swo
|
|
57
|
+
*~
|
|
44
58
|
.DS_Store
|
|
59
|
+
Thumbs.db
|
|
60
|
+
|
|
61
|
+
# AI tooling session data
|
|
62
|
+
.aider*
|
|
63
|
+
.claude/
|
|
64
|
+
.superpowers/
|
|
65
|
+
|
|
66
|
+
# Project-specific scratch
|
|
67
|
+
/dist/
|
|
68
|
+
|
|
69
|
+
# Logs / runtime
|
|
70
|
+
*.log
|
|
71
|
+
*.pid
|
|
72
|
+
*.seed
|
|
73
|
+
*.pid.lock
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cjk-semantic-split
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Recursive atomic decomposition of CJK characters with 9-grid 2-digit spatial encoding. 将汉字递归拆解为原子部件,并在 9 宫格 2 位数字空间编码中标记每个组件的位置。
|
|
5
5
|
Project-URL: Source, https://github.com/yueguanyu/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script.git
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/yueguanyu/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script/issues
|
|
@@ -25,10 +25,11 @@ A Python library that, given any CJK Unified Ideograph, returns a flat DSL strin
|
|
|
25
25
|
|
|
26
26
|
### 1.3 Reference Standards & Sources
|
|
27
27
|
|
|
28
|
-
- ISO/IEC 10646 IDC (Ideographic Description Characters): `吅 吕 罒 目 回 冂 凵 匚 厂 勹 匕 .` — repurposed as initial structure inspiration
|
|
29
|
-
- Wikimedia Commons:Chinese_characters_decomposition —
|
|
30
|
-
-
|
|
31
|
-
-
|
|
28
|
+
- ISO/IEC 10646 IDC (Ideographic Description Characters): `吅 吕 罒 目 回 冂 凵 匚 厂 勹 匕 .` — repurposed as initial structure inspiration; same notation appears as Unicode IDC `⿰ ⿱ ⿲ ⿳ ⿴ ⿵ ⿶ ⿷ ⿸ ⿹ ⿺ ⿻ `.
|
|
29
|
+
- Wikimedia Commons:Chinese_characters_decomposition — visual decomposition **principle** (primitives are visually irreducible units, not structurally minimal); also the planned source for Tier 2/3 harvesting.
|
|
30
|
+
- **yi-bai/ids** (MIT-licensed) — current **operational** source for Tier 1 (~82k chars); provides the same IDC notation that Unihan `kIDS` historically used.
|
|
31
|
+
- Unihan Database (`kIDS` field) — historically the primary Tier 1 source; no longer included as a separate file in current Unicode distributions (the `kIDS` field was removed from Unihan.txt in 9.x+).
|
|
32
|
+
- CHISE project — historical reference for canonical primitive sets.
|
|
32
33
|
|
|
33
34
|
---
|
|
34
35
|
|
|
@@ -91,16 +92,18 @@ Full table (`P ⊙ Q → R`). `-` indicates a geometrically invalid combination
|
|
|
91
92
|
| P\Q | 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 9X |
|
|
92
93
|
|-------|----|----|----|----|----|----|----|----|----|----|
|
|
93
94
|
| **00**| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 9X |
|
|
94
|
-
| **10**| 10 | 10 | - | 50 | 60 | 50 | 60 | - | - |
|
|
95
|
-
| **20**| 20 | - | 20 | 70 | 80 | - | - | 70 | 80 |
|
|
96
|
-
| **30**| 30 | 50 | 70 | 30 | 30 | 50 | 50 | 70 | 70 |
|
|
97
|
-
| **40**| 40 | 60 | 80 | 40 | 40 | 60 | 60 | 80 | 80 |
|
|
98
|
-
| **50**| 50 | 50 | 50 | 50 | 50 | 50 | 50 | 70 | 70 |
|
|
99
|
-
| **60**| 60 | 60 | 60 | 60 | 60 | 60 | 60 | 80 | 80 |
|
|
100
|
-
| **70**| 70 | 50 | 70 | 70 | 70 | 50 | 50 | 70 | 70 |
|
|
101
|
-
| **80**| 80 | 60 | 80 | 80 | 80 | 60 | 60 | 80 | 80 |
|
|
95
|
+
| **10**| 10 | 10 | - | 50 | 60 | 50 | 60 | - | - | 9X |
|
|
96
|
+
| **20**| 20 | - | 20 | 70 | 80 | - | - | 70 | 80 | 9X |
|
|
97
|
+
| **30**| 30 | 50 | 70 | 30 | 30 | 50 | 50 | 70 | 70 | 9X |
|
|
98
|
+
| **40**| 40 | 60 | 80 | 40 | 40 | 60 | 60 | 80 | 80 | 9X |
|
|
99
|
+
| **50**| 50 | 50 | 50 | 50 | 50 | 50 | 50 | 70 | 70 | 9X |
|
|
100
|
+
| **60**| 60 | 60 | 60 | 60 | 60 | 60 | 60 | 80 | 80 | 9X |
|
|
101
|
+
| **70**| 70 | 50 | 70 | 70 | 70 | 50 | 50 | 70 | 70 | 9X |
|
|
102
|
+
| **80**| 80 | 60 | 80 | 80 | 80 | 60 | 60 | 80 | 80 | 9X |
|
|
102
103
|
| **9X**| 00 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | - |
|
|
103
104
|
|
|
105
|
+
**Note on the `9X` columns in rows 10–80**: spec §2.2 only formalized "surround parent passes children through" (the row `9X`). Real CJK data nests surrounds inside directional parents (e.g. `明` contains `日`, which decomposes as `⿴口一` — surround-with-content inside a left-zone). The implementation adds the **symmetric** rule "a surround child of any parent retains its own slot" so the table is closed for the surround column too. This is documented in `src/chinese_decompose/structures.py` near `COMPOSE_TABLE`.
|
|
106
|
+
|
|
104
107
|
**Worked example** (踩):
|
|
105
108
|
```
|
|
106
109
|
踩 → 足 at 30 + 采 at 40
|
|
@@ -109,7 +112,7 @@ Recurse:
|
|
|
109
112
|
足_final = 30 (already primitive-leaf)
|
|
110
113
|
爪_final = 40 ⊙ 10 = 60 (top-right)
|
|
111
114
|
木_final = 40 ⊙ 20 = 80 (bottom-right)
|
|
112
|
-
DSL: 踩[30]:足[60]:爪[80]:木
|
|
115
|
+
DSL: 踩 [30]:足 [60]:爪 [80]:木
|
|
113
116
|
```
|
|
114
117
|
|
|
115
118
|
---
|
|
@@ -118,13 +121,16 @@ DSL: 踩[30]:足[60]:爪[80]:木
|
|
|
118
121
|
|
|
119
122
|
### 3.1 Definition
|
|
120
123
|
|
|
121
|
-
A primitive is a non-decomposable semantic component. Examples:
|
|
124
|
+
A primitive is a non-decomposable semantic component — a visual atom that the walker treats as a leaf. Examples:
|
|
122
125
|
|
|
123
126
|
```
|
|
124
127
|
木 火 土 金 水 人 口 手 心 日 月 目 耳 鼻 舌 身 爪 戈 矢 石
|
|
125
128
|
一 乙 二 三 四 五 六 七 八 九 十 丁 卜 厂 寸 干 弓 牛 犬 玉 王
|
|
129
|
+
宀 冖 冫 氵 扌 忄 讠 纟 亻 刂 艹 阝 彳 凵 匚 廴 疒 攴
|
|
126
130
|
```
|
|
127
131
|
|
|
132
|
+
`primitives.json` is **authoritative** for atomicity. This means: if the bundled JSON lists `日` as primitive, the walker stops at `日` even if a deeper data tier (e.g. yi-bai/ids) records `日 → ⿴口一`. The rule follows the **wikimedia commons** visual decomposition view — primitives are the smallest irreducible units, not whatever deeper structure the source suggests. See §6.3 for the runtime resolution order.
|
|
133
|
+
|
|
128
134
|
### 3.2 Storage & Override
|
|
129
135
|
|
|
130
136
|
- Default set bundled as `chinese_decompose/data/primitives.json` (~250 entries).
|
|
@@ -147,12 +153,16 @@ A character is primitive if and only if it appears in the resolved primitive set
|
|
|
147
153
|
|
|
148
154
|
## 4. Decomposition Data Sources & Tiers
|
|
149
155
|
|
|
150
|
-
| Tier | File | Source
|
|
151
|
-
|
|
152
|
-
| 1 | `data/decomp_l1.json` |
|
|
153
|
-
| 2 | `data/decomp_l2.json` | wikimedia Commons + cleanup
|
|
154
|
-
| 3 | `data/decomp_l3.json` | wikimedia "next-layer" harvest
|
|
155
|
-
| 4 | `data/decomp_patches.json` | Manual curation
|
|
156
|
+
| Tier | File | Source | Coverage target | Description |
|
|
157
|
+
|------|------------------------------|---------------------------------------|-----------------|-------------|
|
|
158
|
+
| 1 | `data/decomp_l1.json` | **yi-bai/ids** (MIT, via jsDelivr CDN) | ≥ 81,000 chars | Currently ~82k; covers CJK Unified + Ext-A |
|
|
159
|
+
| 2 | `data/decomp_l2.json` | wikimedia Commons + cleanup | ≥ 22,000 chars | Cross-checked against Tier 1; fills gaps |
|
|
160
|
+
| 3 | `data/decomp_l3.json` | wikimedia "next-layer" harvest | ≥ 80,000 chars | Recursive harvest; supports 踩 → 足 爪 木 |
|
|
161
|
+
| 4 | `data/decomp_patches.json` | Manual curation | dispute cases | Last-wins overrides for ambiguous/dubious entries |
|
|
162
|
+
|
|
163
|
+
Note on Unihan `kIDS`: the field that originally seeded Tier 1 was removed from current Unicode distributions (Unihan 9.x+ no longer ships a single combined `Unihan.txt`). The project switched to **yi-bai/ids** which provides the same IDC notation (⿰⿱⿲⿳⿴⿵…) and is actively maintained.
|
|
164
|
+
|
|
165
|
+
Note on IDs that are skipped during harvest: 3-cell horizontal (⿲), 3-cell vertical (⿳), and overlaid (⿻//) — see §11 (open questions).
|
|
156
166
|
|
|
157
167
|
### 4.1 Loader Resolution
|
|
158
168
|
|
|
@@ -196,39 +206,71 @@ The data pipeline is **not** part of the Python package; it is a separate offlin
|
|
|
196
206
|
|
|
197
207
|
## 5. DSL Format
|
|
198
208
|
|
|
199
|
-
### 5.1
|
|
209
|
+
### 5.1 Format Convention
|
|
210
|
+
|
|
211
|
+
The DSL is a **staggered** encoding: each segment's bracket carries the position of the *next* segment's character. The last leaf is bare. A single space separates every character from its following `[pos]` bracket.
|
|
200
212
|
|
|
201
213
|
```
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
| "50" | "60" | "70" | "80"
|
|
207
|
-
| "14" | "16" | "34" | "36"
|
|
208
|
-
| "90" | "91" | "92" | "93" | "94" | "95" | "96" | "97" | "98" | "99"
|
|
214
|
+
char_pos_segment = char " " "[" pos "]" (* space is part of the format *)
|
|
215
|
+
last_leaf = char (* no bracket, no preceding space *)
|
|
216
|
+
|
|
217
|
+
dsl_segment = char_pos_segment (":" char_pos_segment)* ":" last_leaf
|
|
209
218
|
```
|
|
210
219
|
|
|
211
|
-
Primitives terminate expansion
|
|
220
|
+
Primitives terminate expansion — they emit only `char` (no brackets, no space).
|
|
212
221
|
|
|
213
|
-
|
|
222
|
+
The bracket carries the **next** leaf's position (not the current character's). Example walk for `踩`:
|
|
214
223
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
```
|
|
225
|
+
踩 [30]:足 [60]:爪 [80]:木
|
|
226
|
+
↑ ↑ ↑ ↑ ↑ ↑ ↑
|
|
227
|
+
root pos | | | | last
|
|
228
|
+
| | |
|
|
229
|
+
↓ ↓ ↓
|
|
230
|
+
pos pos last (no bracket, no space)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Reading top-to-bottom:
|
|
234
|
+
- `踩 [30]` — root `踩`, next leaf's position is 30 → meaning `足` will appear at composed position 30
|
|
235
|
+
- `足 [60]` — first leaf `足`, next leaf's position is 60 → `爪` at position 60
|
|
236
|
+
- `爪 [80]` — second leaf `爪`, next leaf's position is 80 → `木` at position 80
|
|
237
|
+
- `木` — third (last) leaf, bare, no bracket
|
|
238
|
+
|
|
239
|
+
The bracket offset-by-one encodes the same final positions as the explicit `tree.positions` field (`(30, 60, 80)` here) but inline for human reading.
|
|
225
240
|
|
|
226
|
-
### 5.
|
|
241
|
+
### 5.2 Grammar (informal EBNF)
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
dsl_segment = atom (":" atom)* (* the leaf count is determined by data *)
|
|
245
|
+
atom = primitive (* bare char, no brackets, no space *)
|
|
246
|
+
| char " " "[" pos "]"
|
|
247
|
+
primitve = the char itself (no expansion needed)
|
|
248
|
+
|
|
249
|
+
pos = "00" | "10" | "20" | "30" | "40"
|
|
250
|
+
| "50" | "60" | "70" | "80"
|
|
251
|
+
| "14" | "16" | "34" | "36"
|
|
252
|
+
| "90" | "91" | "92" | "93" | "94" | "95" | "96" | "97" | "98" | "99"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The parser tolerates the same character-pos-bracket form on every segment; the final segment is bare by convention (the parser also accepts a space-then-`[` on the final segment for backward compatibility with one-level emit).
|
|
256
|
+
|
|
257
|
+
### 5.3 Examples
|
|
258
|
+
|
|
259
|
+
| Input | Output DSL | Notes |
|
|
260
|
+
|-------|-----------------------------------------------------|-------|
|
|
261
|
+
| 一 | `一` | primitive — bare |
|
|
262
|
+
| 好 | `好 [30]:女 [40]:子` | 2 leaves, root + 2 atom segments |
|
|
263
|
+
| 踩 | `踩 [30]:足 [60]:爪 [80]:木` | 3 leaves, intermediate 采 absorbed into final composed positions |
|
|
264
|
+
| 森 | `森 [10]:木 [20]:木 [40]:木` | 3 leaves, vertical 3-cell |
|
|
265
|
+
| 国 | `国 [90]:囗 [0]:玉` | surround + content |
|
|
266
|
+
| ? | `?[?]:?` | unknown-char marker (non-strict mode) — fixed sentinel, brackets use literal `?` |
|
|
267
|
+
|
|
268
|
+
### 5.4 Multi-Character Input
|
|
227
269
|
|
|
228
270
|
For an input string of length > 1, per-character DSL segments are joined by `|`:
|
|
229
271
|
|
|
230
272
|
```
|
|
231
|
-
decompose("踩好") → 踩[30]:足[60]:爪[80]:木|好[30]:女[40]:子
|
|
273
|
+
decompose("踩好") → 踩 [30]:足 [60]:爪 [80]:木|好 [30]:女 [40]:子
|
|
232
274
|
```
|
|
233
275
|
|
|
234
276
|
---
|
|
@@ -238,29 +280,36 @@ decompose("踩好") → 踩[30]:足[60]:爪[80]:木|好[30]:女[40]:子
|
|
|
238
280
|
```python
|
|
239
281
|
from chinese_decompose import (
|
|
240
282
|
decompose,
|
|
283
|
+
read_structure,
|
|
241
284
|
DecompositionTree,
|
|
242
|
-
|
|
285
|
+
parse_dsl,
|
|
286
|
+
serialize_tree,
|
|
243
287
|
set_primitives,
|
|
244
|
-
PrimitiveSet,
|
|
245
288
|
get_primitives,
|
|
246
289
|
)
|
|
247
290
|
|
|
248
|
-
# Primary
|
|
249
|
-
decompose("踩") # → str DSL
|
|
250
|
-
decompose("踩好") # →
|
|
251
|
-
decompose("踩", as_tree=True) # → DecompositionTree
|
|
291
|
+
# Primary APIs
|
|
292
|
+
decompose("踩") # → str DSL (staggered format with spaces)
|
|
293
|
+
decompose("踩好") # → multi-char DSL joined by '|'
|
|
294
|
+
decompose("踩", as_tree=True) # → list[DecompositionTree] (FLAT: intermediates absorbed)
|
|
295
|
+
|
|
296
|
+
# Structural read (NESTED — preserves intermediate non-leaf nodes)
|
|
297
|
+
read_structure("踩") # → list[DecompositionTree]
|
|
298
|
+
# trees[0].children[1] is 采 (intermediate node preserved)
|
|
299
|
+
# trees[0].children[1].position == 40 (its slot in 踩's frame)
|
|
252
300
|
|
|
253
301
|
# Programmatic access
|
|
254
|
-
tree =
|
|
302
|
+
tree = read_structure("踩")[0]
|
|
255
303
|
tree.char # '踩'
|
|
256
|
-
tree.
|
|
257
|
-
tree.
|
|
258
|
-
tree.children #
|
|
259
|
-
tree.to_dsl() #
|
|
304
|
+
tree.atoms # ('足', '爪', '木') — flat list of leaves in DFS order
|
|
305
|
+
tree.positions # (30, 60, 80) — composed final position of each leaf
|
|
306
|
+
tree.children # tuple[DecompositionTree, ...] — may include intermediates
|
|
307
|
+
tree.to_dsl() # same as serialize_tree(tree)
|
|
260
308
|
|
|
261
|
-
# Round-trip
|
|
262
|
-
parsed =
|
|
263
|
-
parsed.
|
|
309
|
+
# Round-trip (parser tolerant of the staggered form)
|
|
310
|
+
parsed = parse_dsl("踩 [30]:足 [60]:爪 [80]:木")
|
|
311
|
+
parsed.char # '踩'
|
|
312
|
+
parsed.atoms # ('足', '爪', '木')
|
|
264
313
|
|
|
265
314
|
# Primitive control
|
|
266
315
|
set_primitives({"木", "火", "水", ...})
|
|
@@ -269,18 +318,30 @@ get_primitives() # → frozenset[str]
|
|
|
269
318
|
|
|
270
319
|
### 6.1 Return Types
|
|
271
320
|
|
|
272
|
-
- `decompose(text: str, as_tree: bool = False) -> str | DecompositionTree`
|
|
273
|
-
- `
|
|
321
|
+
- `decompose(text: str, as_tree: bool = False, strict: bool = True, loader=None) -> str | list[DecompositionTree]`
|
|
322
|
+
- `read_structure(text: str, strict: bool = True, loader=None) -> list[DecompositionTree]`
|
|
323
|
+
- `DecompositionTree` is an immutable `@dataclass(frozen=True)` with:
|
|
324
|
+
- `char: str`
|
|
325
|
+
- `children: tuple[DecompositionTree, ...]` (empty for primitives and unknown markers)
|
|
326
|
+
- `position: int | None` (composed final position; `None` means root)
|
|
327
|
+
- Derived properties: `atoms: tuple[str, ...]`, `positions: tuple[int, ...]` (flat over DFS)
|
|
274
328
|
|
|
275
329
|
### 6.2 Exceptions
|
|
276
330
|
|
|
277
331
|
| Exception | Raised when |
|
|
278
332
|
|------------------------------|-------------|
|
|
279
|
-
| `UnknownCharacterError` | A character in `text` has no entry across all tiers. |
|
|
280
|
-
| `MalformedDSLParseError` |
|
|
333
|
+
| `UnknownCharacterError` | A character in `text` has no entry across all tiers (and `strict=True`). |
|
|
334
|
+
| `MalformedDSLParseError` | DSL parser fails on input string (bad position, missing colons, etc.). |
|
|
281
335
|
| `InvalidPositionCodeError` | Data file contains a code outside the valid 2-digit set (see §2.1). |
|
|
336
|
+
| `CompositionError` | `compose(parent, child)` is called with a geometrically invalid pair (per §2.3 table). |
|
|
337
|
+
|
|
338
|
+
Unknown character handling: with `strict=False`, decompose emits `?[?]:?` marker tokens in place of unknown chars. `read_structure` emits a marker tree `DecompositionTree(char="?", children=(DecompositionTree(char="?", children=(), position=0),))`.
|
|
282
339
|
|
|
283
|
-
|
|
340
|
+
### 6.3 Caller-Atomicity Rule
|
|
341
|
+
|
|
342
|
+
The bundled `primitives.json` is the **authoritative atomicity definition**: any character in it is treated as atomic by the walker, even if some data tier records a further decomposition for it. This matches the wikimedia commons visual decomposition view — primitives are the visual irreducible units, not whatever deeper IDS structure the source data suggests.
|
|
343
|
+
|
|
344
|
+
Priority on lookup: `is_primitive(char)` short-circuits before loader lookup, so prims win over data (last-wins at the loader level is moot for prims). This rule is **load-bearing** for characters that appear both in `primitives.json` and in `decomp_l{1,2,3}.json` (e.g. `日`).
|
|
284
345
|
|
|
285
346
|
---
|
|
286
347
|
|
|
@@ -347,28 +408,47 @@ Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script/
|
|
|
347
408
|
|
|
348
409
|
### 9.1 `python -m chinese_decompose`
|
|
349
410
|
|
|
411
|
+
The default output format is **JSON**. Use `--format dsl` for the staggered DSL string.
|
|
412
|
+
|
|
350
413
|
```bash
|
|
351
414
|
$ python -m chinese_decompose 踩
|
|
352
|
-
|
|
415
|
+
[
|
|
416
|
+
{
|
|
417
|
+
"char": "踩",
|
|
418
|
+
"atoms": ["足", "爪", "木"],
|
|
419
|
+
"positions": [30, 60, 80],
|
|
420
|
+
"dsl": "踩 [30]:足 [60]:爪 [80]:木"
|
|
421
|
+
}
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
$ python -m chinese_decompose 踩 --format dsl
|
|
425
|
+
踩 [30]:足 [60]:爪 [80]:木
|
|
353
426
|
|
|
354
427
|
$ python -m chinese_decompose 囚 --format json
|
|
355
|
-
|
|
428
|
+
[
|
|
429
|
+
{
|
|
430
|
+
"char": "囚",
|
|
431
|
+
"atoms": ["囗", "人"],
|
|
432
|
+
"positions": [90, 0],
|
|
433
|
+
"dsl": "囚 [90]:囗 [0]:人"
|
|
434
|
+
}
|
|
435
|
+
]
|
|
356
436
|
|
|
357
437
|
$ python -m chinese_decompose --self-test
|
|
358
|
-
|
|
438
|
+
chinese_decompose self-test
|
|
439
|
+
version: 0.1.3
|
|
440
|
+
characters indexed: 81686
|
|
441
|
+
tiers: 4 (l1, l2, l3, patches)
|
|
359
442
|
```
|
|
360
443
|
|
|
361
|
-
Flags: `--format {dsl,
|
|
444
|
+
Flags: `--format {json,dsl,tree}`, `--no-strict`, `--primitives <file>`.
|
|
362
445
|
|
|
363
446
|
### 9.2 Offline Harvest Script (`scripts/harvest.py`)
|
|
364
447
|
|
|
365
|
-
-
|
|
366
|
-
-
|
|
367
|
-
- Normalizes to data format.
|
|
368
|
-
- Emits `decomp_l{1,2,3}.json` next to sources.
|
|
369
|
-
- Logs tier conflicts to `scripts/conflicts.log`.
|
|
448
|
+
- **Tier 1** (currently implemented): downloads **yi-bai/ids** (MIT-licensed, ~102k entries) via the jsDelivr CDN mirror, parses TSV-formatted IDC notation, and writes `decomp_l1.json`. Per the §6.3 rule, it never strips primitives from `primitives.json` — it only **adds** new leaves it discovered.
|
|
449
|
+
- **Tier 2/3**: planned (wikimedia Commons scrape + next-layer recursive harvest); returns `NotImplementedError` if invoked with `--tier l2` or `--tier l3` today.
|
|
370
450
|
|
|
371
|
-
This script is **not** part of the installable package; it lives in `scripts/` and is invoked manually when refreshing data.
|
|
451
|
+
This script is **not** part of the installable package; it lives in `scripts/` and is invoked manually when refreshing data. It requires network access on first run.
|
|
372
452
|
|
|
373
453
|
---
|
|
374
454
|
|
|
@@ -386,8 +466,20 @@ This script is **not** part of the installable package; it lives in `scripts/` a
|
|
|
386
466
|
|
|
387
467
|
## 11. Open Questions & Future Work
|
|
388
468
|
|
|
469
|
+
### Shipped (since the spec was written)
|
|
470
|
+
|
|
471
|
+
- DSL format finalized: **staggered bracket + space between char-and-position** (`踩 [30]:足 [60]:爪 [80]:木`); last leaf bare.
|
|
472
|
+
- CLI default format is **JSON**; `--format dsl` for the string form.
|
|
473
|
+
- **`read_structure()`** API added: returns `list[DecompositionTree]` that preserves **intermediate non-leaf nodes** (e.g. `采` inside `踩`), unlike `decompose()` which collapses them into a flat atom list.
|
|
474
|
+
- **Real-data harvest (v0.1.3)**: `scripts/harvest.py` now pulls ~82k characters from yi-bai/ids; bundled in `decomp_l1.json` at install time.
|
|
475
|
+
- **Caller-atomicity rule** (§6.3): `primitives.json` is the authoritative primitive set; characters in it stay atomic even when layered data says otherwise.
|
|
476
|
+
|
|
477
|
+
### Still open
|
|
478
|
+
|
|
389
479
|
- **Position grammar extensions**: future versions may add codes for repeated patterns (e.g. repetitive 吕 罒 structures) as sub-positions.
|
|
390
|
-
- **
|
|
391
|
-
- **
|
|
392
|
-
- **
|
|
393
|
-
- **
|
|
480
|
+
- **3-cell IDC handling**: `⿲` (LMR) and `⿳` (TMB) entries are currently **skipped** by the harvester (out of scope for the 2-digit schema). They are needed for full coverage.
|
|
481
|
+
- **Overlaid IDC handling**: `⿻`/``/`` patterns skipped.
|
|
482
|
+
- **Bidirectional mapping**: `compose(atoms → char)` inverse — not in v0.x.
|
|
483
|
+
- **Heuristic decomposer**: for characters with no data entry, attempt primitive grouping — out of scope.
|
|
484
|
+
- **CJK Extensions B–G yi-bai coverage**: ~82k chars covers most of the Unified Ideographs block + Extension A but doesn't sweep Extensions B–F systematically. Tiers 2/3 (wikimedia scrape + recursive next-layer) remain unimplemented.
|
|
485
|
+
- **Performance**: prototype assumed sub-millisecond per character; v0.1.3 ships without benchmarks, defer to v0.2 if needed.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cjk-semantic-split"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Recursive atomic decomposition of CJK characters with 9-grid 2-digit spatial encoding. 将汉字递归拆解为原子部件,并在 9 宫格 2 位数字空间编码中标记每个组件的位置。"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -40,7 +40,7 @@ dev = [
|
|
|
40
40
|
|
|
41
41
|
[tool.hatch.build.targets.wheel]
|
|
42
42
|
packages = ["src/chinese_decompose"]
|
|
43
|
-
include = ["src/chinese_decompose/data/*.
|
|
43
|
+
include = ["src/chinese_decompose/data/*.txt"]
|
|
44
44
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
46
|
testpaths = ["tests"]
|
|
@@ -7,7 +7,7 @@ notation (⿰⿱⿴⿵⿶⿷⿸⿹⿺, plus 3-cell and overlaid variants w
|
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
9
|
python -m scripts.harvest --tier l1 \
|
|
10
|
-
--output ../src/chinese_decompose/data/decomp_l1.
|
|
10
|
+
--output ../src/chinese_decompose/data/decomp_l1.txt
|
|
11
11
|
|
|
12
12
|
Source file format (tab-separated):
|
|
13
13
|
|
|
@@ -153,6 +153,11 @@ def _walk_kids(
|
|
|
153
153
|
return False
|
|
154
154
|
composed = compose_fn(parent_position, local_pos)
|
|
155
155
|
if len(comp_str) == 1:
|
|
156
|
+
# Reject leaves that are themselves IDC codes (e.g. degenerate
|
|
157
|
+
# yi-bai entries where ⿰ appears as a slot — that's a parse
|
|
158
|
+
# artifact, not a real atom).
|
|
159
|
+
if comp_str in IDC_TABLE_2_CELL:
|
|
160
|
+
return False
|
|
156
161
|
out.append((comp_str, composed))
|
|
157
162
|
else:
|
|
158
163
|
# Recurse into subtree
|
|
@@ -221,8 +226,40 @@ def parse_ids_source(text: str, compose_fn) -> tuple[dict[str, list[tuple[str, i
|
|
|
221
226
|
return entries, refs
|
|
222
227
|
|
|
223
228
|
|
|
229
|
+
def _serialize_dsl(char: str, comps: list[tuple[str, int]]) -> str:
|
|
230
|
+
"""Build the staggered DSL string for a character + its components.
|
|
231
|
+
|
|
232
|
+
Matches the format used by ``DecompositionTree.to_dsl()``:
|
|
233
|
+
"踩 [30]:足 [60]:爪 [80]:木" — the bracket carries the *next* leaf's
|
|
234
|
+
position; the last leaf is bare. A primitive char with no children
|
|
235
|
+
yields just the bare char.
|
|
236
|
+
"""
|
|
237
|
+
if not comps:
|
|
238
|
+
return char # primitive — bare char
|
|
239
|
+
parts = [f"{char} [{comps[0][1]}]"]
|
|
240
|
+
leaves = [c for c, _ in comps]
|
|
241
|
+
leaf_positions = [p for _, p in comps]
|
|
242
|
+
for i in range(len(leaves) - 1):
|
|
243
|
+
parts.append(f"{leaves[i]} [{leaf_positions[i + 1]}]")
|
|
244
|
+
parts.append(leaves[-1])
|
|
245
|
+
return ":".join(parts)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _load_existing_primitives_txt(path: Path) -> set[str]:
|
|
249
|
+
if not path.exists():
|
|
250
|
+
return set()
|
|
251
|
+
chars: set[str] = set()
|
|
252
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
253
|
+
line = line.strip()
|
|
254
|
+
if not line or line.startswith("#"):
|
|
255
|
+
continue
|
|
256
|
+
if len(line) == 1:
|
|
257
|
+
chars.add(line)
|
|
258
|
+
return chars
|
|
259
|
+
|
|
260
|
+
|
|
224
261
|
def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
225
|
-
"""Download IDS dataset, parse, write decomp_l1.
|
|
262
|
+
"""Download IDS dataset, parse, write decomp_l1.txt and extend primitives.txt."""
|
|
226
263
|
print(f"Downloading {IDS_SOURCES['lv0']}...", file=sys.stderr)
|
|
227
264
|
text = fetch_text(IDS_SOURCES["lv0"])
|
|
228
265
|
print(f" fetched {len(text.splitlines())} lines", file=sys.stderr)
|
|
@@ -232,18 +269,26 @@ def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
|
232
269
|
print(f" entries: {len(entries)}, primitive refs: {len(refs)}", file=sys.stderr)
|
|
233
270
|
|
|
234
271
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
272
|
+
|
|
273
|
+
# Emit Tier 1 as TXT (DSL format, one line per char). Format:
|
|
274
|
+
# <char>\t<DSL>
|
|
275
|
+
# Blank lines and lines starting with `#` are comments.
|
|
276
|
+
lines = [
|
|
277
|
+
f"# Tier 1 decomposition data harvested from yi-bai/ids (MIT) via jsDelivr",
|
|
278
|
+
"# Format per line: <char>\\t<DSL>",
|
|
279
|
+
"# DSL is the staggered form: each bracket carries the NEXT atom's",
|
|
280
|
+
"# position; the last atom is bare. Lines starting with # are comments.",
|
|
281
|
+
"# Empty DSL after the tab means the character is primitive at this tier.",
|
|
282
|
+
]
|
|
283
|
+
for char in sorted(entries):
|
|
284
|
+
lines.append(f"{char}\t{_serialize_dsl(char, entries[char])}")
|
|
285
|
+
output_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
240
286
|
size_kb = output_path.stat().st_size // 1024
|
|
241
287
|
print(f" wrote {output_path} ({size_kb} KB)", file=sys.stderr)
|
|
242
288
|
|
|
243
289
|
# Find all characters that get further decomposed in `entries`
|
|
244
|
-
# (recursively, since chars may appear nested). These MUST be
|
|
245
|
-
#
|
|
246
|
-
# is in both primitives and l1, l1 wins (per last-wins loader order).
|
|
290
|
+
# (recursively, since chars may appear nested). These MUST NOT be
|
|
291
|
+
# promoted into the primitive set.
|
|
247
292
|
def _collect_decomposable(char: str, seen: set[str], into: set[str]) -> None:
|
|
248
293
|
if char in seen:
|
|
249
294
|
return
|
|
@@ -260,33 +305,28 @@ def emit_tier1(output_path: Path, compose_fn) -> dict:
|
|
|
260
305
|
for char in entries:
|
|
261
306
|
_collect_decomposable(char, set(), decomposable)
|
|
262
307
|
|
|
263
|
-
# Primitive leaves: characters in `refs` that NEVER get decomposed (i.e.,
|
|
264
|
-
# not in the `decomposable` set we just built). These stay in the
|
|
265
|
-
# primitive set; the rest get removed.
|
|
266
308
|
leaves_without_decomp = {
|
|
267
309
|
c for c in refs if c not in decomposable and _is_data_char(c)
|
|
268
310
|
}
|
|
269
311
|
|
|
270
|
-
# Append new
|
|
271
|
-
#
|
|
272
|
-
#
|
|
273
|
-
#
|
|
274
|
-
# new leaves — never remove from the curated set.
|
|
312
|
+
# Append new leaves to primitives.txt. Per the wikimedia commons
|
|
313
|
+
# atomicity principle (§6.3 of the spec), primitives.txt is the
|
|
314
|
+
# authoritative atomicity definition — we only ADD new leaves; never
|
|
315
|
+
# remove from the curated set.
|
|
275
316
|
primitives_path = (
|
|
276
|
-
Path(__file__).parent.parent / "src" / "chinese_decompose" / "data" / "primitives.
|
|
317
|
+
Path(__file__).parent.parent / "src" / "chinese_decompose" / "data" / "primitives.txt"
|
|
277
318
|
)
|
|
278
|
-
|
|
279
|
-
existing = set(json.loads(primitives_path.read_text(encoding="utf-8"))["primitives"])
|
|
280
|
-
else:
|
|
281
|
-
existing = set()
|
|
319
|
+
existing = _load_existing_primitives_txt(primitives_path)
|
|
282
320
|
new_primitives = sorted(existing | leaves_without_decomp)
|
|
321
|
+
primitives_lines = [
|
|
322
|
+
"# chinese-decompose primitives (authoritative atomic components per wikimedia commons view)",
|
|
323
|
+
"# Format: one character per line. Lines starting with # are comments.",
|
|
324
|
+
"# Characters listed here are treated as atomic by the walker regardless",
|
|
325
|
+
"# of any deeper structure recorded in decomp_*.txt.",
|
|
326
|
+
]
|
|
327
|
+
primitives_lines += new_primitives
|
|
283
328
|
primitives_path.write_text(
|
|
284
|
-
|
|
285
|
-
{"primitives": new_primitives},
|
|
286
|
-
ensure_ascii=False,
|
|
287
|
-
separators=(",", ":"),
|
|
288
|
-
),
|
|
289
|
-
encoding="utf-8",
|
|
329
|
+
"\n".join(primitives_lines) + "\n", encoding="utf-8"
|
|
290
330
|
)
|
|
291
331
|
added = len(new_primitives) - len(existing)
|
|
292
332
|
print(
|