cjk-semantic-split 0.1.2__tar.gz → 0.1.3__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 (85) hide show
  1. cjk_semantic_split-0.1.3/.hypothesis/constants/28b72037d99ba6eb +4 -0
  2. cjk_semantic_split-0.1.3/.hypothesis/constants/a80ac1b9c26466c7 +4 -0
  3. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/PKG-INFO +1 -1
  4. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/pyproject.toml +1 -1
  5. cjk_semantic_split-0.1.3/scripts/harvest.py +325 -0
  6. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/__init__.py +1 -1
  7. cjk_semantic_split-0.1.3/src/chinese_decompose/data/decomp_l1.json +1 -0
  8. cjk_semantic_split-0.1.3/src/chinese_decompose/data/primitives.json +1 -0
  9. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/structures.py +26 -0
  10. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_harvest.py +9 -3
  11. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_read_structure.py +9 -9
  12. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_smoke.py +1 -1
  13. cjk_semantic_split-0.1.2/scripts/harvest.py +0 -52
  14. cjk_semantic_split-0.1.2/src/chinese_decompose/data/decomp_l1.json +0 -12
  15. cjk_semantic_split-0.1.2/src/chinese_decompose/data/primitives.json +0 -21
  16. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.github/workflows/ci.yml +0 -0
  17. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.gitignore +0 -0
  18. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/.gitignore +0 -0
  19. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/2b726390919ba924 +0 -0
  20. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/3f14f0ebb32e0fff +0 -0
  21. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/447154ffb3443956 +0 -0
  22. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/448896e190e1a139 +0 -0
  23. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/44fdbf8fc7dec723 +0 -0
  24. /cjk_semantic_split-0.1.2/.hypothesis/constants/a80ac1b9c26466c7 → /cjk_semantic_split-0.1.3/.hypothesis/constants/487391a4cba1e887 +0 -0
  25. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/6dc13c365eb3deeb +0 -0
  26. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/7d0a29d738e12257 +0 -0
  27. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/a686eee4be3e3873 +0 -0
  28. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/cd01bf70c5c5b577 +0 -0
  29. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/dc1aa6aba7748109 +0 -0
  30. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/e6674edc9b17fd2d +0 -0
  31. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/eb3de9a3db1e637c +0 -0
  32. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/constants/f2e388b96604c388 +0 -0
  33. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.hypothesis/unicode_data/15.1.0/charmap.json.gz +0 -0
  34. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/.gitignore +0 -0
  35. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/progress.md +0 -0
  36. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/review-22d28a8..564c3e6.diff +0 -0
  37. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/review-564c3e6..0b4b3f6.diff +0 -0
  38. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-1-brief.md +0 -0
  39. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-1-report.md +0 -0
  40. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-10-report.md +0 -0
  41. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-11-report.md +0 -0
  42. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-12-report.md +0 -0
  43. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-13-report.md +0 -0
  44. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-14-report.md +0 -0
  45. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-15-report.md +0 -0
  46. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-2-brief.md +0 -0
  47. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-2-report.md +0 -0
  48. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-3-brief.md +0 -0
  49. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-3-report.md +0 -0
  50. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-4-report.md +0 -0
  51. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-5-report.md +0 -0
  52. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-6-report.md +0 -0
  53. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-7-report.md +0 -0
  54. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-8-report.md +0 -0
  55. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/.superpowers/sdd/task-9-report.md +0 -0
  56. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/LICENSE +0 -0
  57. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/README.md +0 -0
  58. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/component-position-encoding.md +0 -0
  59. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/decomposition-logic.md +0 -0
  60. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/superpowers/plans/2026-07-16-cjk-semantic-split-implementation.md +0 -0
  61. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/docs/superpowers/specs/2026-07-16-cjk-semantic-split-design.md +0 -0
  62. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/scripts/__init__.py +0 -0
  63. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/scripts/review_conflicts.py +0 -0
  64. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/__main__.py +0 -0
  65. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/api.py +0 -0
  66. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/data/decomp_l2.json +0 -0
  67. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/data/decomp_l3.json +0 -0
  68. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/data/decomp_patches.json +0 -0
  69. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/dsl.py +0 -0
  70. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/errors.py +0 -0
  71. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/loader.py +0 -0
  72. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/primitives.py +0 -0
  73. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/src/chinese_decompose/split.py +0 -0
  74. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/__init__.py +0 -0
  75. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/conftest.py +0 -0
  76. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_api.py +0 -0
  77. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_cli.py +0 -0
  78. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_compose.py +0 -0
  79. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_decompose.py +0 -0
  80. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_dsl.py +0 -0
  81. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_loader.py +0 -0
  82. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_primitives.py +0 -0
  83. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_properties.py +0 -0
  84. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_split.py +0 -0
  85. {cjk_semantic_split-0.1.2 → cjk_semantic_split-0.1.3}/tests/test_structures.py +0 -0
@@ -0,0 +1,4 @@
1
+ # file: /Users/mac/Develop/gitstore/full_brain_intelligence/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script/src/chinese_decompose/__init__.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ ['0.1.3', 'CompositionError', 'DecompositionTree', 'Loader', 'VALID_POSITIONS', '__version__', 'clear_cache', 'compose', 'decompose', 'get_primitives', 'is_primitive', 'make_loader', 'parse_dsl', 'read_structure', 'reset_primitives', 'serialize_tree', 'set_primitives', 'split_one']
@@ -0,0 +1,4 @@
1
+ # file: /Users/mac/Develop/gitstore/full_brain_intelligence/Semantic-Character-Splitting-and-Structural-Coding-for-Chinese-Script/src/chinese_decompose/structures.py
2
+ # hypothesis_version: 6.156.6
3
+
4
+ []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cjk-semantic-split
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "cjk-semantic-split"
7
- version = "0.1.2"
7
+ version = "0.1.3"
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"
@@ -0,0 +1,325 @@
1
+ """Offline data pipeline (spec §4.3, §9.2).
2
+
3
+ Fetches CJK decomposition data from a maintained IDS (Ideographic Description
4
+ Sequences) dataset on jsDelivr CDN (mirroring github.com/yi-bai/ids). The
5
+ dataset is MIT-licensed and covers ~102k CJK characters using Unicode IDC
6
+ notation (⿰⿱⿴⿵⿶⿷⿸⿹⿺⿾⿿, plus 3-cell and overlaid variants we skip).
7
+
8
+ Usage:
9
+ python -m scripts.harvest --tier l1 \
10
+ --output ../src/chinese_decompose/data/decomp_l1.json
11
+
12
+ Source file format (tab-separated):
13
+
14
+ <char>\t<IDC-1>\t<IDC-2>\t...
15
+
16
+ Multiple alternative decompositions (when present) are joined by ";". We take
17
+ the first alternative. Trailing "t" / "K" / "H" / "d" suffixes on each atom
18
+ indicate shape variants — we strip them.
19
+
20
+ Sub-graphs within a parent IDC (e.g. ⿱⿰日月一) are recursively expanded and
21
+ component positions composed via the parent ⊙ child rule (see
22
+ ``docs/component-position-encoding.md``).
23
+
24
+ IDC → position table (2-cell only supported in v0.x):
25
+
26
+ ⿰ A B → A at 30, B at 40 (left-right)
27
+ ⿱ A B → A at 10, B at 20 (top-bottom)
28
+ ⿴ A B → A at 90, B at 0 (full surround)
29
+ ⿵ A B → A at 93, B at 0 (surround from above)
30
+ ⿶ A B → A at 91, B at 0 (surround from below)
31
+ ⿷ A B → A at 92, B at 0 (surround from left)
32
+ ⿸ A B → A at 95, B at 0 (surround from upper-left)
33
+ ⿹ A B → A at 97, B at 0 (surround from upper-right)
34
+ ⿺ A B → A at 99, B at 0 (surround from lower-left)
35
+ ⿾ A B → A at 94, B at 0 (surround from right)
36
+ ⿿ A B → A at 96, B at 0 (surround from lower-right)
37
+
38
+ 3-cell horizontal (⿲), 3-cell vertical (⿳), and overlaid (⿻/⿼/⿽) entries
39
+ are skipped — they require additional position slots outside v0.x schema.
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import argparse
44
+ import io
45
+ import json
46
+ import re
47
+ import sys
48
+ import urllib.request
49
+ import zipfile
50
+ from pathlib import Path
51
+
52
+
53
+ def _import_for_compose():
54
+ from chinese_decompose.structures import compose as _compose
55
+ from chinese_decompose.errors import InvalidPositionCodeError
56
+ return _compose, InvalidPositionCodeError
57
+
58
+
59
+ # 2-cell IDC codes → [pos_for_left/top/wall, pos_for_right/bottom/content]
60
+ IDC_TABLE_2_CELL: dict[str, list[int]] = {
61
+ "⿰": [30, 40], # ⿰ left-right
62
+ "⿱": [10, 20], # ⿱ top-bottom
63
+ "⿴": [90, 0], # ⿴ full surround
64
+ "⿵": [93, 0], # ⿵ surround above
65
+ "⿶": [91, 0], # ⿶ surround below
66
+ "⿷": [92, 0], # ⿷ surround left
67
+ "⿸": [95, 0], # ⿸ surround upper-left
68
+ "⿹": [97, 0], # ⿹ surround upper-right
69
+ "⿺": [99, 0], # ⿺ surround lower-left
70
+ "⿾": [94, 0], # ⿾ surround right (extension)
71
+ "⿿": [96, 0], # ⿿ surround lower-right (extension)
72
+ }
73
+
74
+ # Leaf characters that are recognized as atoms in the IDS source. CJK Unified
75
+ # Ideographs, Extension A, Extensions B–F (supplementary plane), and the CJK
76
+ # Compatibility Ideographs block. Strokes (single code points in 1E000+) and
77
+ # other auxiliary blocks are excluded — they would dilute the primitives set.
78
+ def _is_data_char(c: str) -> bool:
79
+ cp = ord(c)
80
+ return (
81
+ 0x3400 <= cp <= 0x4DBF # CJK Extension A
82
+ or 0x4E00 <= cp <= 0x9FFF # CJK Unified Ideographs
83
+ or 0x20000 <= cp <= 0x2FFFF # CJK Extensions B–F + supplementary
84
+ or 0xF900 <= cp <= 0xFAFF # CJK Compatibility Ideographs
85
+ )
86
+
87
+
88
+ # Strip trailing shape-variant markers: t, K, H, d, etc.
89
+ # Examples from the source: "一.(T)" → "一"; "一t(t)" → "一"; "乚w一" → "乚, 一" (split)
90
+ # The "w" / "d" / "K" / "t" / "H" markers indicate variant positioning. We strip
91
+ # them and the surrounding parens, leaving just the CJK characters.
92
+ _VARIANT_MARKER = re.compile(r"[\(\)\[\]]")
93
+ _VARIANT_LETTER = re.compile(r"[a-zA-Z]")
94
+
95
+
96
+ def _clean_atom_string(s: str) -> str:
97
+ """Strip parens, letters, dots, and other markers from an IDS atom segment.
98
+
99
+ Returns only the CJK / IDC characters. Variants like '一t(t)' → '一'.
100
+ """
101
+ out_chars = []
102
+ for c in s:
103
+ if _is_data_char(c) or c in IDC_TABLE_2_CELL:
104
+ out_chars.append(c)
105
+ # else: drop letters, parens, dots, etc.
106
+ return "".join(out_chars)
107
+
108
+
109
+ def _consume_one(s: str) -> tuple[str | None, str]:
110
+ """Consume one component from the start of s.
111
+
112
+ Returns (component_string, remaining). The component is either a single
113
+ CJK character or an entire nested IDC subtree.
114
+ """
115
+ if not s:
116
+ return None, ""
117
+ c = s[0]
118
+ if c in IDC_TABLE_2_CELL:
119
+ rest = s[1:]
120
+ for _ in IDC_TABLE_2_CELL[c]:
121
+ child, rest = _consume_one(rest)
122
+ if child is None:
123
+ return None, ""
124
+ return c, rest
125
+ if _is_data_char(c):
126
+ return c, s[1:]
127
+ return None, ""
128
+
129
+
130
+ def _walk_kids(
131
+ s: str,
132
+ parent_position: int,
133
+ out: list[tuple[str, int]],
134
+ compose_fn,
135
+ ) -> bool:
136
+ """Recursively expand an IDS string into one-level (atom, final_position) pairs.
137
+
138
+ Returns True on success, False if input contains unsupported IDC pattern
139
+ (caller should skip).
140
+ """
141
+ if not s:
142
+ return True
143
+ if len(s) == 1:
144
+ return False
145
+ idc = s[0]
146
+ if idc not in IDC_TABLE_2_CELL:
147
+ # ⿲ ⿳ ⿻ ⿼ ⿽ not supported.
148
+ return False
149
+ rest = s[1:]
150
+ for local_pos in IDC_TABLE_2_CELL[idc]:
151
+ comp_str, rest = _consume_one(rest)
152
+ if comp_str is None:
153
+ return False
154
+ composed = compose_fn(parent_position, local_pos)
155
+ if len(comp_str) == 1:
156
+ out.append((comp_str, composed))
157
+ else:
158
+ # Recurse into subtree
159
+ if not _walk_kids(comp_str, composed, out, compose_fn):
160
+ return False
161
+ if rest:
162
+ return False
163
+ return True
164
+
165
+
166
+ def _parse_kids(s: str, compose_fn) -> list[tuple[str, int]] | None:
167
+ """Parse IDS value, return one-level decomp list or None if unsupported."""
168
+ out: list[tuple[str, int]] = []
169
+ if _walk_kids(s, parent_position=0, out=out, compose_fn=compose_fn):
170
+ return out
171
+ return None
172
+
173
+
174
+ # --- IDS source download + parse ---
175
+
176
+ # jsDelivr CDN mirror of github.com/yi-bai/ids (MIT licensed).
177
+ IDS_SOURCES: dict[str, str] = {
178
+ "lv0": "https://cdn.jsdelivr.net/gh/yi-bai/ids@master/ids_lv0.txt",
179
+ "lv1": "https://cdn.jsdelivr.net/gh/yi-bai/ids@master/ids_lv1.txt",
180
+ "lv2": "https://cdn.jsdelivr.net/gh/yi-bai/ids@master/ids_lv2.txt",
181
+ }
182
+
183
+
184
+ def fetch_text(url: str) -> str:
185
+ req = urllib.request.Request(url, headers={"User-Agent": "chinese_decompose/0.1"})
186
+ with urllib.request.urlopen(req, timeout=120) as resp:
187
+ return resp.read().decode("utf-8")
188
+
189
+
190
+ def parse_ids_source(text: str, compose_fn) -> tuple[dict[str, list[tuple[str, int]]], set[str]]:
191
+ """Parse yi-bai/ids .txt format, return (entries, primitive_refs).
192
+
193
+ Format per line: ``<char>\\t<ids-alt-1>\\t<ids-alt-2>...``
194
+
195
+ Each ``ids-alt-N`` is an IDS expression possibly followed by ";alt2;alt3...".
196
+ We pick the first alt that parses cleanly.
197
+ """
198
+ entries: dict[str, list[tuple[str, int]]] = {}
199
+ refs: set[str] = set()
200
+ for line in text.splitlines():
201
+ if not line.strip():
202
+ continue
203
+ parts = line.split("\t")
204
+ char_field = parts[0]
205
+ if len(char_field) != 1 or not _is_data_char(char_field):
206
+ continue
207
+ char = char_field
208
+ # Pick first alt that parses
209
+ for ids_field in parts[1:]:
210
+ # Take only the first alt (before ';')
211
+ first_alt = ids_field.split(";", 1)[0]
212
+ cleaned = _clean_atom_string(first_alt)
213
+ if not cleaned:
214
+ continue
215
+ result = _parse_kids(cleaned, compose_fn)
216
+ if result is not None and result:
217
+ entries[char] = result
218
+ for c, _ in result:
219
+ refs.add(c)
220
+ break
221
+ return entries, refs
222
+
223
+
224
+ def emit_tier1(output_path: Path, compose_fn) -> dict:
225
+ """Download IDS dataset, parse, write decomp_l1.json and extend primitives.json."""
226
+ print(f"Downloading {IDS_SOURCES['lv0']}...", file=sys.stderr)
227
+ text = fetch_text(IDS_SOURCES["lv0"])
228
+ print(f" fetched {len(text.splitlines())} lines", file=sys.stderr)
229
+
230
+ print("Parsing IDS entries...", file=sys.stderr)
231
+ entries, refs = parse_ids_source(text, compose_fn)
232
+ print(f" entries: {len(entries)}, primitive refs: {len(refs)}", file=sys.stderr)
233
+
234
+ output_path.parent.mkdir(parents=True, exist_ok=True)
235
+ payload = {"_meta": {"tier": 1, "source": "github.com/yi-bai/ids", "license": "MIT"}}
236
+ payload.update({char: [[c, p] for c, p in v] for char, v in entries.items()})
237
+ output_path.write_text(
238
+ json.dumps(payload, ensure_ascii=False, separators=(",", ":")), encoding="utf-8"
239
+ )
240
+ size_kb = output_path.stat().st_size // 1024
241
+ print(f" wrote {output_path} ({size_kb} KB)", file=sys.stderr)
242
+
243
+ # Find all characters that get further decomposed in `entries`
244
+ # (recursively, since chars may appear nested). These MUST be removed
245
+ # from the primitive set so walker behavior is unambiguous — if a char
246
+ # is in both primitives and l1, l1 wins (per last-wins loader order).
247
+ def _collect_decomposable(char: str, seen: set[str], into: set[str]) -> None:
248
+ if char in seen:
249
+ return
250
+ seen.add(char)
251
+ direct = entries.get(char)
252
+ if direct is None:
253
+ return
254
+ into.add(char)
255
+ for c, _ in direct:
256
+ if _is_data_char(c) and len(c) == 1:
257
+ _collect_decomposable(c, seen, into)
258
+
259
+ decomposable: set[str] = set()
260
+ for char in entries:
261
+ _collect_decomposable(char, set(), decomposable)
262
+
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
+ leaves_without_decomp = {
267
+ c for c in refs if c not in decomposable and _is_data_char(c)
268
+ }
269
+
270
+ # Append new primitives to primitives.json. The bundled primitives.json is
271
+ # the AUTHORITATIVE atomicity definition (per wikimedia commons' visual
272
+ # decomposition view): any character in it is treated as atomic, even if
273
+ # the IDS source happens to have a further decomposition. We only ADD
274
+ # new leaves — never remove from the curated set.
275
+ primitives_path = (
276
+ Path(__file__).parent.parent / "src" / "chinese_decompose" / "data" / "primitives.json"
277
+ )
278
+ if primitives_path.exists():
279
+ existing = set(json.loads(primitives_path.read_text(encoding="utf-8"))["primitives"])
280
+ else:
281
+ existing = set()
282
+ new_primitives = sorted(existing | leaves_without_decomp)
283
+ primitives_path.write_text(
284
+ json.dumps(
285
+ {"primitives": new_primitives},
286
+ ensure_ascii=False,
287
+ separators=(",", ":"),
288
+ ),
289
+ encoding="utf-8",
290
+ )
291
+ added = len(new_primitives) - len(existing)
292
+ print(
293
+ f" appended {added} new primitives (leaves without IDS expansion); "
294
+ f"total = {len(new_primitives)} "
295
+ f"({primitives_path.stat().st_size // 1024} KB)",
296
+ file=sys.stderr,
297
+ )
298
+ return entries
299
+
300
+
301
+ def main(argv: list[str] | None = None) -> int:
302
+ parser = argparse.ArgumentParser(description="Harvest CJK decomposition data.")
303
+ parser.add_argument("--output", type=Path, required=True, help="Output JSON path.")
304
+ parser.add_argument(
305
+ "--tier",
306
+ choices=["l1"],
307
+ default="l1",
308
+ help="Which tier to harvest. Currently only 'l1' (yi-bai/ids lv0) is supported.",
309
+ )
310
+ args = parser.parse_args(argv)
311
+
312
+ if args.tier != "l1":
313
+ print(
314
+ f"Tier {args.tier!r} not implemented; only 'l1' is currently supported.",
315
+ file=sys.stderr,
316
+ )
317
+ return 2
318
+
319
+ compose_fn, _ = _import_for_compose()
320
+ emit_tier1(args.output, compose_fn)
321
+ return 0
322
+
323
+
324
+ if __name__ == "__main__":
325
+ sys.exit(main())
@@ -1,5 +1,5 @@
1
1
  """chinese_decompose: recursive atomic decomposition of CJK characters."""
2
- __version__ = "0.1.2"
2
+ __version__ = "0.1.3"
3
3
 
4
4
  from chinese_decompose.api import (
5
5
  DecompositionTree,