ucode 0.1.0 → 0.1.1

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 (174) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/Gemfile.lock +2 -2
  4. data/TODO.full/00-README.md +116 -0
  5. data/TODO.full/01-panglyph-vision.md +112 -0
  6. data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
  7. data/TODO.full/03-panglyph-font-builder.md +201 -0
  8. data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
  9. data/TODO.full/05-ucode-0-1-1-release.md +139 -0
  10. data/TODO.full/06-fontisan-remove-audit.md +142 -0
  11. data/TODO.full/07-fontisan-remove-ucd.md +125 -0
  12. data/TODO.full/08-archive-private-bin-build.md +143 -0
  13. data/TODO.full/09-archive-public-structure.md +164 -0
  14. data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
  15. data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
  16. data/TODO.full/12-implementation-order.md +216 -0
  17. data/TODO.full/13-fontisan-font-writer-api.md +189 -0
  18. data/TODO.full/14-fontisan-table-writers.md +66 -0
  19. data/TODO.full/15-panglyph-builder-real.md +82 -0
  20. data/TODO.full/16-archive-public-sync-workflows.md +167 -0
  21. data/TODO.full/17-fontist-org-font-picker.md +73 -0
  22. data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
  23. data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
  24. data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
  25. data/TODO.new/00-README.md +30 -0
  26. data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
  27. data/TODO.new/24-universal-glyph-set-build.md +189 -0
  28. data/TODO.new/25-font-audit-against-universal-set.md +195 -0
  29. data/TODO.new/26-missing-glyph-reporter.md +189 -0
  30. data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
  31. data/TODO.new/28-implementation-order-update.md +187 -0
  32. data/TODO.new/29-universal-set-curation-uc17.md +312 -0
  33. data/TODO.new/30-tier1-font-acquisition.md +241 -0
  34. data/TODO.new/31-universal-set-production-build.md +205 -0
  35. data/TODO.new/32-uc17-coverage-matrix.md +165 -0
  36. data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
  37. data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
  38. data/TODO.new/35-universal-set-production-run.md +160 -0
  39. data/TODO.new/36-per-font-coverage-audit.md +145 -0
  40. data/TODO.new/37-coverage-highlight-reporter.md +125 -0
  41. data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
  42. data/TODO.new/39-implementation-order-update-32-38.md +258 -0
  43. data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
  44. data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
  45. data/config/specialist_fonts.yml +102 -0
  46. data/config/unicode17_tier1_fonts.yml +42 -0
  47. data/config/unicode17_universal_glyph_set.yml +293 -0
  48. data/lib/ucode/audit/block_aggregator.rb +57 -29
  49. data/lib/ucode/audit/browser/face_page.rb +128 -0
  50. data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
  51. data/lib/ucode/audit/browser/library_page.rb +74 -0
  52. data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
  53. data/lib/ucode/audit/browser/template.rb +47 -0
  54. data/lib/ucode/audit/browser/templates/face.css +200 -0
  55. data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
  56. data/lib/ucode/audit/browser/templates/face.js +298 -0
  57. data/lib/ucode/audit/browser/templates/library.css +119 -0
  58. data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
  59. data/lib/ucode/audit/browser/templates/library.js +99 -0
  60. data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
  61. data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
  62. data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
  63. data/lib/ucode/audit/browser.rb +32 -0
  64. data/lib/ucode/audit/context.rb +27 -1
  65. data/lib/ucode/audit/coverage_reference.rb +103 -0
  66. data/lib/ucode/audit/differ.rb +121 -0
  67. data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
  68. data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
  69. data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
  70. data/lib/ucode/audit/emitter/face_directory.rb +212 -0
  71. data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
  72. data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
  73. data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
  74. data/lib/ucode/audit/emitter/paths.rb +312 -0
  75. data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
  76. data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
  77. data/lib/ucode/audit/emitter.rb +29 -0
  78. data/lib/ucode/audit/extractors/aggregations.rb +31 -2
  79. data/lib/ucode/audit/face_auditor.rb +86 -0
  80. data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
  81. data/lib/ucode/audit/formatters/audit_text.rb +411 -0
  82. data/lib/ucode/audit/formatters/color.rb +48 -0
  83. data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
  84. data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
  85. data/lib/ucode/audit/formatters.rb +23 -0
  86. data/lib/ucode/audit/library_aggregator.rb +86 -0
  87. data/lib/ucode/audit/library_auditor.rb +105 -0
  88. data/lib/ucode/audit/release/emitter.rb +152 -0
  89. data/lib/ucode/audit/release/face_card.rb +93 -0
  90. data/lib/ucode/audit/release/formula_audits.rb +50 -0
  91. data/lib/ucode/audit/release/library_index_builder.rb +78 -0
  92. data/lib/ucode/audit/release/manifest_builder.rb +127 -0
  93. data/lib/ucode/audit/release.rb +42 -0
  94. data/lib/ucode/audit/ucd_only_reference.rb +81 -0
  95. data/lib/ucode/audit/universal_set_reference.rb +136 -0
  96. data/lib/ucode/audit.rb +31 -0
  97. data/lib/ucode/cli.rb +339 -33
  98. data/lib/ucode/commands/audit/browser_command.rb +82 -0
  99. data/lib/ucode/commands/audit/collection_command.rb +103 -0
  100. data/lib/ucode/commands/audit/compare_command.rb +188 -0
  101. data/lib/ucode/commands/audit/font_command.rb +140 -0
  102. data/lib/ucode/commands/audit/library_command.rb +87 -0
  103. data/lib/ucode/commands/audit/reference_builder.rb +64 -0
  104. data/lib/ucode/commands/audit.rb +20 -0
  105. data/lib/ucode/commands/block_feed.rb +73 -0
  106. data/lib/ucode/commands/canonical_build.rb +138 -0
  107. data/lib/ucode/commands/fetch.rb +37 -1
  108. data/lib/ucode/commands/release.rb +115 -0
  109. data/lib/ucode/commands/universal_set.rb +211 -0
  110. data/lib/ucode/commands.rb +5 -0
  111. data/lib/ucode/coordinator/indices.rb +11 -0
  112. data/lib/ucode/coordinator.rb +138 -5
  113. data/lib/ucode/error.rb +30 -2
  114. data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
  115. data/lib/ucode/fetch/font_fetcher.rb +16 -0
  116. data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
  117. data/lib/ucode/fetch.rb +7 -3
  118. data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
  119. data/lib/ucode/glyphs/real_fonts.rb +1 -0
  120. data/lib/ucode/glyphs/resolver.rb +62 -0
  121. data/lib/ucode/glyphs/source.rb +48 -0
  122. data/lib/ucode/glyphs/source_builder.rb +61 -0
  123. data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
  124. data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
  125. data/lib/ucode/glyphs/source_config.rb +104 -0
  126. data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
  127. data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
  128. data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
  129. data/lib/ucode/glyphs/sources.rb +20 -0
  130. data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
  131. data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
  132. data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
  133. data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
  134. data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
  135. data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
  136. data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
  137. data/lib/ucode/glyphs/universal_set.rb +45 -0
  138. data/lib/ucode/glyphs.rb +6 -0
  139. data/lib/ucode/models/audit/baseline.rb +6 -0
  140. data/lib/ucode/models/audit/block_summary.rb +7 -0
  141. data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
  142. data/lib/ucode/models/audit/release_face.rb +42 -0
  143. data/lib/ucode/models/audit/release_formula.rb +33 -0
  144. data/lib/ucode/models/audit/release_manifest.rb +43 -0
  145. data/lib/ucode/models/audit/release_universal_set.rb +37 -0
  146. data/lib/ucode/models/audit.rb +9 -0
  147. data/lib/ucode/models/block.rb +2 -0
  148. data/lib/ucode/models/build_report.rb +109 -0
  149. data/lib/ucode/models/codepoint/glyph.rb +42 -0
  150. data/lib/ucode/models/codepoint.rb +3 -0
  151. data/lib/ucode/models/glyph_source.rb +86 -0
  152. data/lib/ucode/models/glyph_source_map.rb +138 -0
  153. data/lib/ucode/models/specialist_font.rb +70 -0
  154. data/lib/ucode/models/specialist_font_manifest.rb +48 -0
  155. data/lib/ucode/models/unihan_entry.rb +81 -9
  156. data/lib/ucode/models/unihan_field.rb +21 -0
  157. data/lib/ucode/models/universal_set_entry.rb +47 -0
  158. data/lib/ucode/models/universal_set_manifest.rb +78 -0
  159. data/lib/ucode/models/validation_report.rb +99 -0
  160. data/lib/ucode/models.rb +9 -0
  161. data/lib/ucode/parsers/named_sequences.rb +5 -5
  162. data/lib/ucode/parsers/unihan.rb +50 -19
  163. data/lib/ucode/repo/aggregate_writer.rb +34 -2
  164. data/lib/ucode/repo/block_feed_emitter.rb +153 -0
  165. data/lib/ucode/repo/build_report_accumulator.rb +138 -0
  166. data/lib/ucode/repo/build_report_writer.rb +46 -0
  167. data/lib/ucode/repo/build_validator.rb +229 -0
  168. data/lib/ucode/repo/codepoint_writer.rb +50 -1
  169. data/lib/ucode/repo/paths.rb +8 -0
  170. data/lib/ucode/repo.rb +4 -0
  171. data/lib/ucode/version.rb +1 -1
  172. data/schema/block-feed.output.schema.yml +134 -0
  173. metadata +143 -2
  174. data/ucode.gemspec +0 -56
@@ -0,0 +1,293 @@
1
+ ---
2
+ unicode_version: 17.0.0
3
+ ucode_version: 0.1.1
4
+ generated_at: '2026-06-28T00:00:00Z'
5
+ default_sources:
6
+ - kind: fontist
7
+ label: noto-sans
8
+ priority: 1
9
+ license: OFL
10
+ provenance: Universal fallback for Latin/Cyrillic/Greek/symbols and any block without
11
+ a specialist entry below
12
+ map:
13
+ CJK_Unified_Ideographs_Extension_A:
14
+ sources:
15
+ - kind: path
16
+ label: FSung-2
17
+ priority: 1
18
+ license: OFL
19
+ path: "~/Downloads/全宋體/FSung-2.ttf"
20
+ - kind: fontist
21
+ label: noto-sans-cjk-jp
22
+ priority: 99
23
+ CJK_Unified_Ideographs:
24
+ sources:
25
+ - kind: path
26
+ label: FSung-1
27
+ priority: 1
28
+ license: OFL
29
+ provenance: Taiwan MOE 全宋體; covers U+4E00..U+9FFF core
30
+ path: "~/Downloads/全宋體/FSung-1.ttf"
31
+ - kind: fontist
32
+ label: noto-sans-cjk-jp
33
+ priority: 99
34
+ provenance: Catch-all fallback for CJK codepoints FSung misses
35
+ Sidetic:
36
+ sources:
37
+ - kind: fontist
38
+ label: lentariso
39
+ priority: 1
40
+ license: OFL
41
+ provenance: Lentariso >=1.029 (github.com/Bry10022/Lentariso); 26/26 cmap-verified
42
+ - kind: fontist
43
+ label: noto-sans-sidetic
44
+ priority: 2
45
+ Sharada_Supplement:
46
+ sources:
47
+ - kind: fontist
48
+ label: noto-sans-sharada
49
+ priority: 1
50
+ license: OFL
51
+ Tolong_Siki:
52
+ sources:
53
+ - kind: fontist
54
+ label: noto-sans-tolong-siki
55
+ priority: 1
56
+ license: OFL
57
+ Egyptian_Hieroglyphs:
58
+ sources:
59
+ - kind: path
60
+ label: UniHieroglyphica
61
+ priority: 1
62
+ license: OFL
63
+ provenance: suignard.com; authoritative for Egyptian Hieroglyphs
64
+ path: data/fonts/UniHieroglyphica.ttf
65
+ Egyptian_Hieroglyph_Format_Controls:
66
+ sources:
67
+ - kind: path
68
+ label: Egyptian-Text
69
+ priority: 1
70
+ license: OFL
71
+ provenance: microsoft/font-tools; OFL
72
+ path: data/fonts/EgyptianText-Regular.ttf
73
+ Egyptian_Hieroglyphs_Extended-A:
74
+ sources:
75
+ - kind: path
76
+ label: UniHieroglyphica
77
+ priority: 1
78
+ license: OFL
79
+ path: data/fonts/UniHieroglyphica.ttf
80
+ Beria_Erfe:
81
+ sources:
82
+ - kind: fontist
83
+ label: kedebideri
84
+ priority: 1
85
+ license: OFL
86
+ provenance: Kedebideri 3.001 (software.sil.org/kedebideri); 50/50 cmap-verified
87
+ Tangut:
88
+ sources:
89
+ - kind: fontist
90
+ label: noto-sans-tangut
91
+ priority: 1
92
+ license: OFL
93
+ Tangut_Components:
94
+ sources:
95
+ - kind: fontist
96
+ label: noto-sans-tangut
97
+ priority: 1
98
+ license: OFL
99
+ Tangut_Supplement:
100
+ sources:
101
+ - kind: fontist
102
+ label: noto-sans-tangut
103
+ priority: 1
104
+ license: OFL
105
+ Symbols_for_Legacy_Computing_Supplement:
106
+ sources:
107
+ - kind: fontist
108
+ label: babelstone-pseudographica
109
+ priority: 1
110
+ license: OFL
111
+ provenance: BabelStone; partial Unicode 17 coverage
112
+ Miscellaneous_Symbols_Supplement:
113
+ sources:
114
+ - kind: fontist
115
+ label: noto-sans-symbols-2
116
+ priority: 1
117
+ license: OFL
118
+ Musical_Symbols:
119
+ sources:
120
+ - kind: fontist
121
+ label: noto-music
122
+ priority: 1
123
+ license: OFL
124
+ Tai_Yo:
125
+ sources:
126
+ - kind: path
127
+ label: NotoSerifTaiYo
128
+ priority: 1
129
+ license: OFL
130
+ provenance: translationcommons.org; proven via correlate-v4 Pillar 2 extraction
131
+ path: data/fonts/NotoSerifTaiYo.ttf
132
+ Adlam:
133
+ sources:
134
+ - kind: path
135
+ label: NotoSansAdlam
136
+ priority: 1
137
+ license: OFL
138
+ provenance: Noto Sans Adlam fixture; 88/88 cmap-verified at Unicode 16 baseline
139
+ path: spec/fixtures/fonts/NotoSansAdlam-Regular.ttf
140
+ Alchemical_Symbols:
141
+ sources:
142
+ - kind: fontist
143
+ label: noto-sans-symbols
144
+ priority: 1
145
+ license: OFL
146
+ - kind: fontist
147
+ label: symbola
148
+ priority: 2
149
+ Supplemental_Arrows-C:
150
+ sources:
151
+ - kind: fontist
152
+ label: symbola
153
+ priority: 1
154
+ CJK_Unified_Ideographs_Extension_B:
155
+ sources:
156
+ - kind: path
157
+ label: FSung-2
158
+ priority: 1
159
+ license: OFL
160
+ path: "~/Downloads/全宋體/FSung-2.ttf"
161
+ - kind: fontist
162
+ label: noto-sans-cjk-jp
163
+ priority: 99
164
+ CJK_Unified_Ideographs_Extension_C:
165
+ sources:
166
+ - kind: path
167
+ label: FSung-2
168
+ priority: 1
169
+ license: OFL
170
+ path: "~/Downloads/全宋體/FSung-2.ttf"
171
+ - kind: fontist
172
+ label: noto-sans-cjk-jp
173
+ priority: 99
174
+ CJK_Unified_Ideographs_Extension_D:
175
+ sources:
176
+ - kind: path
177
+ label: FSung-2
178
+ priority: 1
179
+ license: OFL
180
+ path: "~/Downloads/全宋體/FSung-2.ttf"
181
+ - kind: fontist
182
+ label: noto-sans-cjk-jp
183
+ priority: 99
184
+ CJK_Unified_Ideographs_Extension_E:
185
+ sources:
186
+ - kind: path
187
+ label: FSung-3
188
+ priority: 1
189
+ license: OFL
190
+ path: "~/Downloads/全宋體/FSung-3.ttf"
191
+ - kind: fontist
192
+ label: noto-sans-cjk-jp
193
+ priority: 99
194
+ CJK_Unified_Ideographs_Extension_F:
195
+ sources:
196
+ - kind: path
197
+ label: FSung-3
198
+ priority: 1
199
+ license: OFL
200
+ path: "~/Downloads/全宋體/FSung-3.ttf"
201
+ - kind: fontist
202
+ label: noto-sans-cjk-jp
203
+ priority: 99
204
+ CJK_Unified_Ideographs_Extension_I:
205
+ sources:
206
+ - kind: path
207
+ label: FSung-3
208
+ priority: 1
209
+ license: OFL
210
+ path: "~/Downloads/全宋體/FSung-3.ttf"
211
+ CJK_Unified_Ideographs_Extension_G:
212
+ sources:
213
+ - kind: path
214
+ label: FSung-3
215
+ priority: 1
216
+ license: OFL
217
+ path: "~/Downloads/全宋體/FSung-3.ttf"
218
+ - kind: fontist
219
+ label: noto-sans-cjk-jp
220
+ priority: 99
221
+ CJK_Unified_Ideographs_Extension_H:
222
+ sources:
223
+ - kind: path
224
+ label: FSung-3
225
+ priority: 1
226
+ license: OFL
227
+ provenance: FSung-3 cmap-verified 4192/4192 for Ext H
228
+ path: "~/Downloads/全宋體/FSung-3.ttf"
229
+ CJK_Unified_Ideographs_Extension_J:
230
+ sources:
231
+ - kind: path
232
+ label: FSung-3
233
+ priority: 1
234
+ license: OFL
235
+ provenance: FSung-3 cmap-verified 4298/4298 for Ext J (Unicode 17 new block)
236
+ path: "~/Downloads/全宋體/FSung-3.ttf"
237
+ Egyptian_Hieroglyphs_Extended-B:
238
+ sources:
239
+ - kind: fontist
240
+ label: UniHieroglyphica
241
+ priority: 1
242
+ license: OFL
243
+ provenance: suignard.com/UniHieroglyphica v16; authoritative for Egyptian Hieroglyphs
244
+ Extended-B (UC17 new block)
245
+ Arabic_Extended-B:
246
+ sources:
247
+ - kind: fontist
248
+ label: noto-sans-arabic
249
+ priority: 1
250
+ license: OFL
251
+ provenance: UC17 additions to Arabic Extended-B
252
+ Arabic_Extended-C:
253
+ sources:
254
+ - kind: fontist
255
+ label: noto-sans-arabic
256
+ priority: 1
257
+ license: OFL
258
+ provenance: Arabic Extended-C (UC17 new block)
259
+ Telugu:
260
+ sources:
261
+ - kind: fontist
262
+ label: noto-sans-telugu
263
+ priority: 1
264
+ license: OFL
265
+ provenance: UC17 Telugu addition
266
+ Kannada:
267
+ sources:
268
+ - kind: fontist
269
+ label: noto-sans-kannada
270
+ priority: 1
271
+ license: OFL
272
+ provenance: UC17 Kannada addition
273
+ Chess_Symbols:
274
+ sources:
275
+ - kind: fontist
276
+ label: noto-sans-symbols-2
277
+ priority: 1
278
+ license: OFL
279
+ provenance: UC17 additions to Chess Symbols
280
+ Transport_and_Map_Symbols:
281
+ sources:
282
+ - kind: fontist
283
+ label: noto-sans-symbols-2
284
+ priority: 1
285
+ license: OFL
286
+ provenance: UC17 additions to Transport and Map Symbols
287
+ Symbols_and_Pictographs_Extended-A:
288
+ sources:
289
+ - kind: fontist
290
+ label: noto-sans-symbols-2
291
+ priority: 1
292
+ license: OFL
293
+ provenance: UC17 additions to Symbols and Pictographs Extended-A
@@ -3,55 +3,72 @@
3
3
  module Ucode
4
4
  module Audit
5
5
  # Produces one {Models::Audit::BlockSummary} per touched Unicode block
6
- # for a font's cmap codepoint set, compared against a ucode UCD
7
- # baseline.
6
+ # for a font's cmap codepoint set, compared against a
7
+ # {CoverageReference}.
8
8
  #
9
- # Pure transformation: takes the resolved baseline Database + the
10
- # font's codepoint list, returns BlockSummary[]. No I/O beyond the
11
- # database lookups, no mutation of inputs.
9
+ # Pure transformation: takes a reference + the font's codepoint list,
10
+ # returns BlockSummary[]. No I/O beyond the reference's lookups, no
11
+ # mutation of inputs.
12
12
  #
13
- # The "assigned" set for a block is derived from the Database's
14
- # ranges-with-that-name. The Database stores coalesced runs of
15
- # consecutive assigned codepoints grouped by block name, so the
16
- # union of those ranges IS the assigned set for that block.
13
+ # The "assigned" set for a block comes from
14
+ # `reference.entries_for_block(name)`. For a {UcdOnlyReference}
15
+ # that's every codepoint in the block's UCD ranges. For a
16
+ # {UniversalSetReference} it's every codepoint the universal glyph
17
+ # set built a glyph for in that block — each entry carries tier +
18
+ # source provenance that gets attached to the missing-codepoint
19
+ # list (TODO 25).
17
20
  class BlockAggregator
18
- # @param database [Ucode::Database, nil] resolved baseline. When
19
- # nil, #call returns an empty array — caller should treat that
20
- # as "no UCD baseline available" and surface a warning.
21
- def initialize(database)
22
- @database = database
21
+ # @param reference [CoverageReference, Ucode::Database, nil]
22
+ # pluggable baseline. For backwards compatibility a raw
23
+ # Ucode::Database is still accepted and wrapped in a
24
+ # {UcdOnlyReference} at construction time. When nil, #call
25
+ # returns an empty array.
26
+ def initialize(reference)
27
+ @reference = coerce_reference(reference)
23
28
  end
24
29
 
25
30
  # @param codepoints [Enumerable<Integer>]
26
31
  # @return [Array<Models::Audit::BlockSummary>] sorted by first_cp
27
32
  def call(codepoints)
28
- return [] if @database.nil? || codepoints.empty?
33
+ return [] if @reference.nil? || codepoints.empty?
29
34
 
30
35
  grouped = group_by_block(codepoints)
31
- grouped.map { |name, covered| build_summary(name, covered) }
36
+ grouped.filter_map { |name, covered| build_summary(name, covered) }
32
37
  .sort_by(&:first_cp)
33
38
  end
34
39
 
35
40
  private
36
41
 
42
+ def coerce_reference(input)
43
+ return nil if input.nil?
44
+ return input if input.is_a?(CoverageReference)
45
+
46
+ UcdOnlyReference.new(database: input)
47
+ end
48
+
37
49
  def group_by_block(codepoints)
38
50
  codepoints.each_with_object(Hash.new { |h, k| h[k] = [] }) do |cp, acc|
39
- name = @database.lookup_block(cp)
51
+ name = block_name_for(cp)
40
52
  acc[name] << cp if name
41
53
  end
42
54
  end
43
55
 
56
+ def block_name_for(codepoint)
57
+ @reference.block_name_for(codepoint)
58
+ end
59
+
44
60
  def build_summary(name, covered_cps)
45
- ranges = @database.block_ranges_by_name(name)
46
- # ranges is non-empty here: the name came from lookup_block,
47
- # which only returns names present in the blocks table.
48
- first_cp = ranges.map(&:first_cp).min
49
- last_cp = ranges.map(&:last_cp).max
50
- assigned_set = expand_assigned(ranges)
61
+ entries = @reference.entries_for_block(name)
62
+ return nil if entries.empty?
63
+
64
+ first_cp = entries.first.codepoint
65
+ last_cp = entries.last.codepoint
66
+ assigned_set = entries.to_set(&:codepoint)
51
67
  covered_set = covered_cps.to_set & assigned_set
52
68
  missing_set = assigned_set - covered_set
69
+ missing_sorted = missing_set.sort
53
70
 
54
- Models::Audit::BlockSummary.new(
71
+ kwargs = {
55
72
  name: name,
56
73
  first_cp: first_cp,
57
74
  last_cp: last_cp,
@@ -65,14 +82,25 @@ module Ucode
65
82
  covered_count: covered_set.size,
66
83
  total_assigned: assigned_set.size,
67
84
  ),
68
- missing_codepoints: missing_set.sort,
85
+ missing_codepoints: missing_sorted,
69
86
  covered_codepoints: covered_set.sort,
70
- )
87
+ }
88
+
89
+ provenance = @reference.provenance_for(missing_sorted)
90
+ kwargs[:missing_codepoint_provenance] = provenance_rows(missing_sorted, provenance) if provenance
91
+
92
+ Models::Audit::BlockSummary.new(**kwargs)
71
93
  end
72
94
 
73
- def expand_assigned(ranges)
74
- ranges.each_with_object(Set.new) do |r, acc|
75
- (r.first_cp..r.last_cp).each { |cp| acc << cp }
95
+ def provenance_rows(codepoints, rows)
96
+ return [] if rows.nil? || rows.empty?
97
+
98
+ codepoints.zip(rows).map do |cp, row|
99
+ Models::Audit::CodepointProvenance.new(
100
+ codepoint: cp,
101
+ tier: row[:tier],
102
+ source: row[:source],
103
+ )
76
104
  end
77
105
  end
78
106
 
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "json"
5
+
6
+ require "ucode/repo/atomic_writes"
7
+ require "ucode/audit/browser"
8
+ require "ucode/audit/browser/template"
9
+ require "ucode/audit/emitter/paths"
10
+ require "ucode/audit/emitter/index_emitter"
11
+
12
+ module Ucode
13
+ module Audit
14
+ module Browser
15
+ # Renders one face's `index.html` — a fully self-contained
16
+ # browser page for one audited font.
17
+ #
18
+ # The page inlines the same JSON shape that {Emitter::IndexEmitter}
19
+ # writes to `index.json`, plus inlined CSS and JS. Opening the
20
+ # file via `file://` renders the overview immediately; lazy
21
+ # fetches of the per-block chunks (`blocks/<NAME>.json`,
22
+ # `codepoints/<NAME>.json`, `glyphs/U+XXXX.svg`) work when the
23
+ # directory is served over HTTP.
24
+ #
25
+ # Self-contained: no external CSS, no external JS, no CDN.
26
+ # Portable: the entire `<label>/` directory can be moved/served
27
+ # whole and the page still works.
28
+ #
29
+ # Two construction modes — pass exactly one of:
30
+ # - `report:` — a live {Models::Audit::AuditReport}. The JSON
31
+ # shape is derived via {Emitter::IndexEmitter#build_index}.
32
+ # Used by {Emitter::FaceDirectory} when emitting alongside
33
+ # the audit.
34
+ # - `overview_json:` — a pre-built JSON string of the overview
35
+ # shape. Used by {Commands::AuditBrowserCommand} when
36
+ # regenerating HTML from an existing `index.json`.
37
+ class FacePage
38
+ include Ucode::Repo::AtomicWrites
39
+
40
+ # @param report [Models::Audit::AuditReport, nil]
41
+ # @param overview_json [String, nil] pre-built overview JSON
42
+ # @param verbose [Boolean] when true, the rendered page
43
+ # advertises per-block codepoint detail chunks
44
+ # @param with_glyphs [Boolean] when true, the rendered page
45
+ # advertises that `glyphs/U+XXXX.svg` chunks exist
46
+ # @param universal_set_root [String, Pathname, nil] when both
47
+ # this and `face_dir:` are present and the root exists, the
48
+ # inlined overview JSON carries a `universal_set` section
49
+ # with relative paths. The face browser JS uses these to
50
+ # fetch universal-set glyphs for missing-codepoint chips.
51
+ # @param face_dir [String, Pathname, nil] destination face
52
+ # directory. Required when `universal_set_root:` is set and
53
+ # the caller wants relative paths resolved; otherwise
54
+ # optional. `write(face_dir)` overrides this.
55
+ def initialize(report: nil, overview_json: nil, verbose: false,
56
+ with_glyphs: false, universal_set_root: nil, face_dir: nil)
57
+ raise ArgumentError, "pass exactly one of report: / overview_json:" \
58
+ unless report.nil? ^ overview_json.nil?
59
+
60
+ @report = report
61
+ @overview_json = overview_json
62
+ @verbose = verbose
63
+ @with_glyphs = with_glyphs
64
+ @universal_set_root = universal_set_root
65
+ @face_dir = face_dir
66
+ end
67
+
68
+ # Write the rendered page to `<face_dir>/index.html`.
69
+ # @param face_dir [String, Pathname]
70
+ # @return [Boolean] true if written, false if skipped
71
+ def write(face_dir)
72
+ @face_dir = Pathname.new(face_dir)
73
+ write_atomic(@face_dir.join("index.html"), render)
74
+ end
75
+
76
+ # Render the page as a string. Useful in tests.
77
+ # @return [String]
78
+ def render
79
+ Template.new(:face).render(
80
+ overview_json: overview_json,
81
+ page_title: page_title,
82
+ verbose: @verbose,
83
+ with_glyphs: @with_glyphs,
84
+ universal_set: universal_set_section,
85
+ )
86
+ end
87
+
88
+ private
89
+
90
+ def overview_hash
91
+ return @overview_hash if @overview_hash
92
+ return JSON.parse(@overview_json) if @overview_json
93
+
94
+ @overview_hash = Ucode::Audit::Emitter::IndexEmitter.new.build_index(
95
+ @report,
96
+ universal_set_root: @face_dir ? @universal_set_root : nil,
97
+ face_dir: @face_dir,
98
+ )
99
+ end
100
+
101
+ def overview_json
102
+ return @overview_json if @overview_json
103
+
104
+ JSON.generate(overview_hash)
105
+ end
106
+
107
+ def universal_set_section
108
+ overview_hash["universal_set"] || { "available" => false }
109
+ end
110
+
111
+ def page_title
112
+ @report ? report_title : derived_title
113
+ end
114
+
115
+ def report_title
116
+ [@report.family_name, @report.subfamily_name].compact.join(" ")
117
+ end
118
+
119
+ def derived_title
120
+ font = JSON.parse(@overview_json)["font"] || {}
121
+ [font["family_name"], font["subfamily_name"]].compact.join(" ")
122
+ rescue JSON::ParserError
123
+ "ucode audit"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "json"
5
+
6
+ require "ucode/audit/browser"
7
+
8
+ module Ucode
9
+ module Audit
10
+ module Browser
11
+ # Service that builds per-codepoint glyph panel data for the
12
+ # missing-glyph reporter (TODO 26).
13
+ #
14
+ # Two consumers:
15
+ #
16
+ # - {MissingGlyphPage} — standalone per-block gallery; calls
17
+ # {#to_hash} once per missing codepoint to inline SVG markup.
18
+ # - The face browser JS — via the `universal_set` section that
19
+ # {Emitter::IndexEmitter} embeds in `index.json`. The JS
20
+ # fetches `<glyphs_dir>/U+XXXX.svg` at runtime using those
21
+ # paths; this class doesn't need to be loaded client-side.
22
+ #
23
+ # Reads the universal-set manifest once and builds a
24
+ # codepoint → {Models::UniversalSetEntry} index so per-codepoint
25
+ # lookups are O(1). SVG markup is read on demand from
26
+ # `<universal_set_root>/glyphs/U+XXXX.svg`.
27
+ #
28
+ # When the universal-set root is `nil` or unreachable on disk,
29
+ # {#available?} returns false and {#to_hash} returns a minimal
30
+ # stub with `available: false`, `svg: nil`. Consumers render a
31
+ # text-only fallback in that case — the surrounding page still
32
+ # works.
33
+ class GlyphPanel
34
+ GLYPHS_DIRNAME = "glyphs"
35
+ MANIFEST_FILENAME = "manifest.json"
36
+ private_constant :GLYPHS_DIRNAME, :MANIFEST_FILENAME
37
+
38
+ # @param universal_set_root [String, Pathname, nil] root of the
39
+ # universal-set build (e.g. "output/universal_glyph_set").
40
+ # nil when no set is co-located.
41
+ def initialize(universal_set_root:)
42
+ @root = universal_set_root.nil? ? nil : Pathname.new(universal_set_root)
43
+ @available = set_available?
44
+ @entries_by_cp = @available ? build_entries_index : {}
45
+ end
46
+
47
+ # @return [Boolean] true when the universal-set root, manifest,
48
+ # and glyphs directory are all reachable on disk
49
+ def available?
50
+ @available
51
+ end
52
+
53
+ # @param codepoint [Integer]
54
+ # @return [Hash] panel payload:
55
+ # - "codepoint" => Integer
56
+ # - "id" => "U+XXXX"
57
+ # - "available" => Boolean (per-codepoint glyph file exists)
58
+ # - "svg" => String markup, or nil when the SVG file
59
+ # is missing or the universal set is unavailable
60
+ # - "tier" => String (e.g. "tier-1"), or nil
61
+ # - "source" => String (e.g. "noto-sans"), or nil
62
+ def to_hash(codepoint)
63
+ {
64
+ "codepoint" => codepoint,
65
+ "id" => cp_id(codepoint),
66
+ "available" => glyph_available?(codepoint),
67
+ "svg" => read_svg(codepoint),
68
+ "tier" => entry_for(codepoint)&.tier,
69
+ "source" => entry_for(codepoint)&.source,
70
+ }
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :root
76
+
77
+ def set_available?
78
+ return false if root.nil?
79
+
80
+ root.directory? && manifest_path.exist? && glyphs_dir.directory?
81
+ end
82
+
83
+ def manifest_path
84
+ root.join(MANIFEST_FILENAME)
85
+ end
86
+
87
+ def glyphs_dir
88
+ root.join(GLYPHS_DIRNAME)
89
+ end
90
+
91
+ def glyph_path(codepoint)
92
+ glyphs_dir.join("#{cp_id(codepoint)}.svg")
93
+ end
94
+
95
+ def glyph_available?(codepoint)
96
+ return false unless @available
97
+
98
+ glyph_path(codepoint).exist?
99
+ end
100
+
101
+ def read_svg(codepoint)
102
+ return nil unless @available
103
+
104
+ path = glyph_path(codepoint)
105
+ path.exist? ? path.read : nil
106
+ end
107
+
108
+ def entry_for(codepoint)
109
+ @entries_by_cp[codepoint]
110
+ end
111
+
112
+ def build_entries_index
113
+ hash = JSON.parse(manifest_path.read)
114
+ manifest = Ucode::Models::UniversalSetManifest.from_hash(hash)
115
+ manifest.entries.to_h { |e| [e.codepoint, e] }
116
+ end
117
+
118
+ def cp_id(codepoint)
119
+ format("U+%04X", codepoint.to_i)
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end