ucode 0.1.0 → 0.2.0

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 (187) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/TODO.full/00-README.md +116 -0
  4. data/TODO.full/01-panglyph-vision.md +112 -0
  5. data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
  6. data/TODO.full/03-panglyph-font-builder.md +201 -0
  7. data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
  8. data/TODO.full/05-ucode-0-1-1-release.md +139 -0
  9. data/TODO.full/06-fontisan-remove-audit.md +142 -0
  10. data/TODO.full/07-fontisan-remove-ucd.md +125 -0
  11. data/TODO.full/08-archive-private-bin-build.md +143 -0
  12. data/TODO.full/09-archive-public-structure.md +164 -0
  13. data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
  14. data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
  15. data/TODO.full/12-implementation-order.md +216 -0
  16. data/TODO.full/13-fontisan-font-writer-api.md +189 -0
  17. data/TODO.full/14-fontisan-table-writers.md +66 -0
  18. data/TODO.full/15-panglyph-builder-real.md +82 -0
  19. data/TODO.full/16-archive-public-sync-workflows.md +167 -0
  20. data/TODO.full/17-fontist-org-font-picker.md +73 -0
  21. data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
  22. data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
  23. data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
  24. data/TODO.new/00-README.md +30 -0
  25. data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
  26. data/TODO.new/24-universal-glyph-set-build.md +189 -0
  27. data/TODO.new/25-font-audit-against-universal-set.md +195 -0
  28. data/TODO.new/26-missing-glyph-reporter.md +189 -0
  29. data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
  30. data/TODO.new/28-implementation-order-update.md +187 -0
  31. data/TODO.new/29-universal-set-curation-uc17.md +312 -0
  32. data/TODO.new/30-tier1-font-acquisition.md +241 -0
  33. data/TODO.new/31-universal-set-production-build.md +205 -0
  34. data/TODO.new/32-uc17-coverage-matrix.md +165 -0
  35. data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
  36. data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
  37. data/TODO.new/35-universal-set-production-run.md +160 -0
  38. data/TODO.new/36-per-font-coverage-audit.md +145 -0
  39. data/TODO.new/37-coverage-highlight-reporter.md +125 -0
  40. data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
  41. data/TODO.new/39-implementation-order-update-32-38.md +258 -0
  42. data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
  43. data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
  44. data/config/specialist_fonts.yml +102 -0
  45. data/config/unicode17_tier1_fonts.yml +42 -0
  46. data/config/unicode17_universal_glyph_set.yml +293 -0
  47. data/lib/ucode/audit/block_aggregator.rb +57 -29
  48. data/lib/ucode/audit/browser/face_page.rb +128 -0
  49. data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
  50. data/lib/ucode/audit/browser/library_page.rb +74 -0
  51. data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
  52. data/lib/ucode/audit/browser/template.rb +47 -0
  53. data/lib/ucode/audit/browser/templates/face.css +200 -0
  54. data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
  55. data/lib/ucode/audit/browser/templates/face.js +298 -0
  56. data/lib/ucode/audit/browser/templates/library.css +119 -0
  57. data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
  58. data/lib/ucode/audit/browser/templates/library.js +99 -0
  59. data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
  60. data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
  61. data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
  62. data/lib/ucode/audit/browser.rb +32 -0
  63. data/lib/ucode/audit/context.rb +27 -1
  64. data/lib/ucode/audit/coverage_reference.rb +103 -0
  65. data/lib/ucode/audit/differ.rb +121 -0
  66. data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
  67. data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
  68. data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
  69. data/lib/ucode/audit/emitter/face_directory.rb +212 -0
  70. data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
  71. data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
  72. data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
  73. data/lib/ucode/audit/emitter/paths.rb +312 -0
  74. data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
  75. data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
  76. data/lib/ucode/audit/emitter.rb +29 -0
  77. data/lib/ucode/audit/extractors/aggregations.rb +31 -2
  78. data/lib/ucode/audit/face_auditor.rb +86 -0
  79. data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
  80. data/lib/ucode/audit/formatters/audit_text.rb +411 -0
  81. data/lib/ucode/audit/formatters/color.rb +48 -0
  82. data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
  83. data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
  84. data/lib/ucode/audit/formatters.rb +23 -0
  85. data/lib/ucode/audit/library_aggregator.rb +86 -0
  86. data/lib/ucode/audit/library_auditor.rb +105 -0
  87. data/lib/ucode/audit/reference_factory.rb +66 -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 +32 -0
  97. data/lib/ucode/cli.rb +366 -41
  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.rb +19 -0
  104. data/lib/ucode/commands/block_feed.rb +73 -0
  105. data/lib/ucode/commands/build.rb +4 -0
  106. data/lib/ucode/commands/canonical_build.rb +137 -0
  107. data/lib/ucode/commands/fetch.rb +49 -15
  108. data/lib/ucode/commands/glyphs.rb +25 -67
  109. data/lib/ucode/commands/lookup.rb +11 -11
  110. data/lib/ucode/commands/parse.rb +7 -5
  111. data/lib/ucode/commands/release.rb +114 -0
  112. data/lib/ucode/commands/universal_set.rb +207 -0
  113. data/lib/ucode/commands.rb +5 -0
  114. data/lib/ucode/coordinator/indices.rb +49 -2
  115. data/lib/ucode/coordinator.rb +138 -5
  116. data/lib/ucode/error.rb +30 -2
  117. data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
  118. data/lib/ucode/fetch/font_fetcher.rb +16 -0
  119. data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
  120. data/lib/ucode/fetch.rb +7 -3
  121. data/lib/ucode/glyphs/pipeline.rb +106 -0
  122. data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
  123. data/lib/ucode/glyphs/real_fonts.rb +1 -0
  124. data/lib/ucode/glyphs/resolver.rb +62 -0
  125. data/lib/ucode/glyphs/source.rb +48 -0
  126. data/lib/ucode/glyphs/source_builder.rb +61 -0
  127. data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
  128. data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
  129. data/lib/ucode/glyphs/source_config.rb +104 -0
  130. data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
  131. data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
  132. data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
  133. data/lib/ucode/glyphs/sources.rb +20 -0
  134. data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
  135. data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
  136. data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
  137. data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
  138. data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
  139. data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
  140. data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
  141. data/lib/ucode/glyphs/universal_set.rb +45 -0
  142. data/lib/ucode/glyphs.rb +7 -0
  143. data/lib/ucode/models/audit/baseline.rb +6 -0
  144. data/lib/ucode/models/audit/block_summary.rb +7 -0
  145. data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
  146. data/lib/ucode/models/audit/release_face.rb +42 -0
  147. data/lib/ucode/models/audit/release_formula.rb +33 -0
  148. data/lib/ucode/models/audit/release_manifest.rb +43 -0
  149. data/lib/ucode/models/audit/release_universal_set.rb +37 -0
  150. data/lib/ucode/models/audit.rb +9 -0
  151. data/lib/ucode/models/block.rb +2 -0
  152. data/lib/ucode/models/build_report.rb +109 -0
  153. data/lib/ucode/models/codepoint/glyph.rb +42 -0
  154. data/lib/ucode/models/codepoint.rb +3 -0
  155. data/lib/ucode/models/glyph_source.rb +86 -0
  156. data/lib/ucode/models/glyph_source_map.rb +138 -0
  157. data/lib/ucode/models/specialist_font.rb +70 -0
  158. data/lib/ucode/models/specialist_font_manifest.rb +48 -0
  159. data/lib/ucode/models/unihan_entry.rb +81 -9
  160. data/lib/ucode/models/unihan_field.rb +21 -0
  161. data/lib/ucode/models/universal_set_entry.rb +47 -0
  162. data/lib/ucode/models/universal_set_manifest.rb +78 -0
  163. data/lib/ucode/models/validation_report.rb +99 -0
  164. data/lib/ucode/models.rb +9 -0
  165. data/lib/ucode/parsers/named_sequences.rb +5 -5
  166. data/lib/ucode/parsers/unihan.rb +50 -19
  167. data/lib/ucode/repo/aggregate_writer.rb +82 -288
  168. data/lib/ucode/repo/block_feed_emitter.rb +153 -0
  169. data/lib/ucode/repo/build_report_accumulator.rb +138 -0
  170. data/lib/ucode/repo/build_report_writer.rb +46 -0
  171. data/lib/ucode/repo/build_validator.rb +229 -0
  172. data/lib/ucode/repo/codepoint_writer.rb +50 -1
  173. data/lib/ucode/repo/paths.rb +8 -0
  174. data/lib/ucode/repo/writers/blocks_writer.rb +73 -0
  175. data/lib/ucode/repo/writers/enums_writer.rb +38 -0
  176. data/lib/ucode/repo/writers/indexes_writer.rb +53 -0
  177. data/lib/ucode/repo/writers/manifest_writer.rb +78 -0
  178. data/lib/ucode/repo/writers/named_sequences_writer.rb +47 -0
  179. data/lib/ucode/repo/writers/planes_writer.rb +82 -0
  180. data/lib/ucode/repo/writers/relationships_writer.rb +71 -0
  181. data/lib/ucode/repo/writers/scripts_writer.rb +54 -0
  182. data/lib/ucode/repo/writers.rb +20 -0
  183. data/lib/ucode/repo.rb +5 -0
  184. data/lib/ucode/version.rb +1 -1
  185. data/schema/block-feed.output.schema.yml +134 -0
  186. metadata +158 -4
  187. data/Gemfile.lock +0 -406
@@ -4,43 +4,47 @@ require "pathname"
4
4
 
5
5
  require "ucode/cache"
6
6
  require "ucode/fetch"
7
- require "ucode/version_resolver"
7
+ require "ucode/glyphs/source_config"
8
+ require "ucode/parsers"
8
9
 
9
10
  module Ucode
10
11
  module Commands
11
12
  # `ucode fetch` — downloads UCD/Unihan/Code-Charts sources into the
12
- # per-version cache. Three subactions: ucd, unihan, charts.
13
+ # per-version cache, plus the specialist Tier 1 fonts referenced by
14
+ # the curated source config.
13
15
  #
14
- # Thin shell over `Ucode::Fetch::*`. The command layer's job is to
15
- # resolve the version intent and format the result; the fetcher does
16
- # the network I/O.
16
+ # Thin shell over `Ucode::Fetch::*`. The command takes a resolved
17
+ # version string; CLI callers resolve via {VersionResolver.resolve}
18
+ # once and thread it through. See Candidate 4 of the 2026-06-29
19
+ # architecture review.
17
20
  class FetchCommand
18
- # @param version_intent [nil, :default, :latest, String]
21
+ DEFAULT_SPECIALIST_FONTS_MANIFEST =
22
+ Ucode::Glyphs::SourceConfig::DEFAULT_PATH.dirname.join("specialist_fonts.yml")
23
+ private_constant :DEFAULT_SPECIALIST_FONTS_MANIFEST
24
+
25
+ # @param version [String] resolved UCD version
19
26
  # @param force [Boolean]
20
27
  # @return [Hash] { version:, ucd_dir: }
21
- def fetch_ucd(version_intent, force: false)
22
- version = VersionResolver.resolve(version_intent)
28
+ def fetch_ucd(version, force: false)
23
29
  Cache.ensure_version_dir!(version)
24
30
  path = Fetch::UcdZip.call(version, force: force)
25
31
  { version: version, ucd_dir: path }
26
32
  end
27
33
 
28
- # @param version_intent [nil, :default, :latest, String]
34
+ # @param version [String] resolved UCD version
29
35
  # @param force [Boolean]
30
36
  # @return [Hash] { version:, unihan_dir: }
31
- def fetch_unihan(version_intent, force: false)
32
- version = VersionResolver.resolve(version_intent)
37
+ def fetch_unihan(version, force: false)
33
38
  Cache.ensure_version_dir!(version)
34
39
  path = Fetch::UnihanZip.call(version, force: force)
35
40
  { version: version, unihan_dir: path }
36
41
  end
37
42
 
38
- # @param version_intent [nil, :default, :latest, String]
43
+ # @param version [String] resolved UCD version
39
44
  # @param block_first_cps [Array<Integer>, nil] nil = all known blocks
40
45
  # @param force [Boolean]
41
46
  # @return [Hash] { version:, downloaded: }
42
- def fetch_charts(version_intent, block_first_cps: nil, force: false)
43
- version = VersionResolver.resolve(version_intent)
47
+ def fetch_charts(version, block_first_cps: nil, force: false)
44
48
  Cache.ensure_version_dir!(version)
45
49
 
46
50
  cps = block_first_cps || default_block_first_cps(version)
@@ -48,6 +52,36 @@ module Ucode
48
52
  { version: version, downloaded: count }
49
53
  end
50
54
 
55
+ # Fetch specialist Tier 1 fonts listed in the manifest. Returns
56
+ # a structured summary; per-font detail lives on the returned
57
+ # results array (one {Fetch::FontFetcher::Result} per entry).
58
+ #
59
+ # @param manifest_path [String, Pathname, nil] defaults to
60
+ # `config/specialist_fonts.yml`.
61
+ # @param only_label [String, nil] restrict to one label.
62
+ # @param allow_proprietary [Boolean] required for non-OFL entries.
63
+ # @param dry_run [Boolean] plan only; no network or disk writes.
64
+ # @return [Hash] { manifest:, total:, downloaded:, skipped:,
65
+ # failed:, local:, planned:, results: }
66
+ def fetch_fonts(manifest_path: nil, only_label: nil, allow_proprietary: false,
67
+ dry_run: false)
68
+ path = Pathname.new(manifest_path || DEFAULT_SPECIALIST_FONTS_MANIFEST)
69
+ results = Fetch::SpecialistFontFetcher.new(
70
+ manifest_path: path,
71
+ allow_proprietary: allow_proprietary,
72
+ dry_run: dry_run,
73
+ ).call(only_label: only_label)
74
+
75
+ { manifest: path.to_s,
76
+ total: results.size,
77
+ downloaded: results.count(&:downloaded?),
78
+ skipped: results.count(&:skipped?),
79
+ failed: results.count(&:failed?),
80
+ local: results.count(&:local?),
81
+ planned: results.count(&:planned?),
82
+ results: results }
83
+ end
84
+
51
85
  private
52
86
 
53
87
  def default_block_first_cps(version)
@@ -59,4 +93,4 @@ module Ucode
59
93
  end
60
94
  end
61
95
  end
62
- end
96
+ end
@@ -1,19 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require "set"
5
4
 
6
- require "ucode/cache"
7
5
  require "ucode/glyphs"
8
- require "ucode/parsers"
9
- require "ucode/version_resolver"
10
6
 
11
7
  module Ucode
12
8
  module Commands
13
9
  # `ucode glyphs` — extract per-codepoint SVGs from Code Charts PDFs.
14
- # Builds block specs from the cached Blocks.txt + per-block PDFs (or
15
- # monolith fallback), then drains them through the Glyphs::Writer
16
- # worker pool.
10
+ # Thin Thor-facing wrapper around {Ucode::Glyphs::Pipeline}:
11
+ # opt-in gate + experimental warning live here; the pipeline
12
+ # assembly (block loading, fetcher, per-block specs) lives in
13
+ # {Ucode::Glyphs::Pipeline}.
17
14
  #
18
15
  # **Status (v0.1): EXPERIMENTAL.** The cell-extraction pipeline
19
16
  # currently includes cell-border decorations alongside the actual
@@ -23,16 +20,16 @@ module Ucode
23
20
  # pipeline can be iterated on without churning the CLI surface, but
24
21
  # callers MUST opt in via `include_glyphs: true` (CLI: `--include-glyphs`)
25
22
  # and will receive a printed warning. Tracked for v0.2.
23
+ #
24
+ # Takes a resolved version string; CLI callers resolve via
25
+ # {VersionResolver.resolve} once and thread it through. See
26
+ # Candidate 4 of the 2026-06-29 architecture review.
26
27
  class GlyphsCommand
27
28
  ExperimentalWarning = "ucode glyphs is experimental in v0.1: " \
28
29
  "extracted SVGs include cell-border decorations " \
29
30
  "alongside the character outline."
30
31
  private_constant :ExperimentalWarning
31
32
 
32
- MonolithPath = "CodeCharts.pdf"
33
- PageMapCache = "data/codecharts_page_map.json"
34
- private_constant :MonolithPath, :PageMapCache
35
-
36
33
  class << self
37
34
  # @return [String] the experimental-status banner. Exposed so the
38
35
  # CLI and BuildCommand surface the same message verbatim.
@@ -41,7 +38,7 @@ module Ucode
41
38
  end
42
39
  end
43
40
 
44
- # @param version_intent [nil, :default, :latest, String]
41
+ # @param version [String] resolved UCD version
45
42
  # @param output_root [String, Pathname]
46
43
  # @param block_filter [Array<String>, nil] block ids to limit to;
47
44
  # nil = every block
@@ -55,75 +52,36 @@ module Ucode
55
52
  # written here exactly once before work begins.
56
53
  # @return [Hash] aggregated Writer tally + version, or a `skipped`
57
54
  # payload when opt-in is false.
58
- def call(version_intent, output_root:,
59
- block_filter: nil, force: false, monolith_path: MonolithPath,
55
+ def call(version, output_root:,
56
+ block_filter: nil, force: false,
57
+ monolith_path: Glyphs::Pipeline::DEFAULT_MONOLITH_PATH,
60
58
  include_glyphs: false, warn: nil)
61
- return skipped(version_intent) unless include_glyphs
59
+ return skipped(version) unless include_glyphs
62
60
 
63
61
  warn&.puts(ExperimentalWarning)
64
- version = VersionResolver.resolve(version_intent)
65
- root = Pathname.new(output_root)
66
62
 
67
- blocks = load_blocks(version, block_filter)
68
- fetcher = build_fetcher(version, monolith_path, blocks)
69
- specs = blocks.map { |block| spec_for(block, fetcher, force) }.compact
63
+ pipeline = Glyphs::Pipeline.new(
64
+ version: version,
65
+ block_filter: block_filter,
66
+ monolith_path: monolith_path,
67
+ )
68
+ specs = pipeline.build_specs(force: force)
70
69
 
71
- writer = Glyphs::Writer.new(output_root: root,
72
- parallel_workers: workers)
70
+ writer = Glyphs::Writer.new(
71
+ output_root: Pathname.new(output_root),
72
+ parallel_workers: workers,
73
+ )
73
74
  tally = writer.write_all(specs)
74
75
  tally.merge(version: version, block_count: specs.size)
75
76
  end
76
77
 
77
78
  private
78
79
 
79
- def load_blocks(version, block_filter)
80
- ucd_dir = Cache.ucd_dir(version)
81
- path = ucd_dir.join("Blocks.txt")
82
- return [] unless path.exist?
83
-
84
- all = Parsers::Blocks.each_record(path).to_a
85
- return all unless block_filter && !block_filter.empty?
86
-
87
- filter_set = block_filter.to_set
88
- all.select { |block| filter_set.include?(block.id) }
89
- end
90
-
91
- def build_fetcher(version, monolith_path, blocks)
92
- monolith = Pathname.new(monolith_path)
93
- monolith = monolith.exist? ? monolith : nil
94
- Glyphs::PdfFetcher.new(
95
- version,
96
- monolith_path: monolith,
97
- blocks: blocks,
98
- page_map_cache: PageMapCache,
99
- )
100
- end
101
-
102
- def spec_for(block, fetcher, force)
103
- pdf_path = fetcher.fetch(block_first_cp: block.range_first, force: force)
104
- return nil unless pdf_path
105
-
106
- { block: block, pdf_path: pdf_path, page_map: page_map_for(block) }
107
- end
108
-
109
- # Heuristic page map: per-block PDFs are page 1 = title, page 2 =
110
- # first chart page starting at the block's first codepoint. True for
111
- # most BMP blocks; multi-page blocks (CJK) need a richer resolver.
112
- # Mismatches yield placeholder SVGs only — never wrong glyphs.
113
- def page_map_for(block)
114
- { 2 => block.range_first }
115
- end
116
-
117
80
  def workers
118
81
  Ucode.configuration.parallel_workers
119
82
  end
120
83
 
121
- def skipped(version_intent)
122
- version = begin
123
- VersionResolver.resolve(version_intent)
124
- rescue UnknownVersionError
125
- version_intent
126
- end
84
+ def skipped(version)
127
85
  {
128
86
  version: version,
129
87
  skipped: true,
@@ -133,4 +91,4 @@ module Ucode
133
91
  end
134
92
  end
135
93
  end
136
- end
94
+ end
@@ -5,42 +5,42 @@ require "pathname"
5
5
  require "ucode/cache"
6
6
  require "ucode/database"
7
7
  require "ucode/repo"
8
- require "ucode/version_resolver"
9
8
 
10
9
  module Ucode
11
10
  module Commands
12
11
  # `ucode lookup` — read-only lookups against the SQLite cache and
13
12
  # the output JSON tree. Three subactions: block, script, char.
13
+ #
14
+ # Takes a resolved version string; CLI callers resolve via
15
+ # {VersionResolver.resolve} once and thread it through. See
16
+ # Candidate 4 of the 2026-06-29 architecture review.
14
17
  class LookupCommand
15
18
  BlockResult = Struct.new(:codepoint, :block, keyword_init: true)
16
19
  ScriptResult = Struct.new(:codepoint, :script, keyword_init: true)
17
20
  CharResult = Struct.new(:codepoint, :block_id, :glyph_path, keyword_init: true)
18
21
  private_constant :BlockResult, :ScriptResult, :CharResult
19
22
 
20
- # @param version_intent [nil, :default, :latest, String]
23
+ # @param version [String] resolved UCD version
21
24
  # @param codepoint [Integer]
22
25
  # @return [BlockResult]
23
- def lookup_block(version_intent, codepoint:)
24
- version = VersionResolver.resolve(version_intent)
26
+ def lookup_block(version, codepoint:)
25
27
  with_db(version) { |db| db.lookup_block(codepoint) }
26
28
  .then { |block| BlockResult.new(codepoint: codepoint, block: block) }
27
29
  end
28
30
 
29
- # @param version_intent [nil, :default, :latest, String]
31
+ # @param version [String] resolved UCD version
30
32
  # @param codepoint [Integer]
31
33
  # @return [ScriptResult]
32
- def lookup_script(version_intent, codepoint:)
33
- version = VersionResolver.resolve(version_intent)
34
+ def lookup_script(version, codepoint:)
34
35
  with_db(version) { |db| db.lookup_script(codepoint) }
35
36
  .then { |script| ScriptResult.new(codepoint: codepoint, script: script) }
36
37
  end
37
38
 
38
- # @param version_intent [nil, :default, :latest, String]
39
+ # @param version [String] resolved UCD version
39
40
  # @param codepoint [Integer]
40
41
  # @param output_root [String, Pathname]
41
42
  # @return [CharResult]
42
- def lookup_char(version_intent, codepoint:, output_root:)
43
- version = VersionResolver.resolve(version_intent)
43
+ def lookup_char(version, codepoint:, output_root:)
44
44
  block_id = with_db(version) { |db| db.lookup_block(codepoint) }
45
45
  glyph = block_id ? glyph_path(output_root, block_id, codepoint) : nil
46
46
  CharResult.new(codepoint: codepoint, block_id: block_id, glyph_path: glyph)
@@ -62,4 +62,4 @@ module Ucode
62
62
  end
63
63
  end
64
64
  end
65
- end
65
+ end
@@ -6,19 +6,21 @@ require "ucode/cache"
6
6
  require "ucode/coordinator"
7
7
  require "ucode/parsers"
8
8
  require "ucode/repo"
9
- require "ucode/version_resolver"
10
9
 
11
10
  module Ucode
12
11
  module Commands
13
12
  # `ucode parse` — streams the Coordinator output into the on-disk
14
13
  # JSON tree at `output/`. Single pass: enrich + write per-cp JSON +
15
14
  # accumulate aggregates + final flush.
15
+ #
16
+ # Takes a resolved version string; CLI callers resolve via
17
+ # {VersionResolver.resolve} once and thread it through. See
18
+ # Candidate 4 of the 2026-06-29 architecture review.
16
19
  class ParseCommand
17
- # @param version_intent [nil, :default, :latest, String]
20
+ # @param version [String] resolved UCD version
18
21
  # @param output_root [String, Pathname]
19
22
  # @return [Hash] { version:, codepoint_count: }
20
- def call(version_intent, output_root:)
21
- version = VersionResolver.resolve(version_intent)
23
+ def call(version, output_root:)
22
24
  root = Pathname.new(output_root)
23
25
  ucd_dir = Cache.ucd_dir(version)
24
26
  unihan_dir = Cache.unihan_dir(version)
@@ -59,4 +61,4 @@ module Ucode
59
61
  end
60
62
  end
61
63
  end
62
- end
64
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "time"
5
+
6
+ require "ucode/audit"
7
+ require "ucode/audit/library_auditor"
8
+ require "ucode/audit/release"
9
+ require "ucode/audit/emitter/paths"
10
+
11
+ module Ucode
12
+ module Commands
13
+ # `ucode release` — assemble the fontist.org-consumable release
14
+ # tree (TODO 27).
15
+ #
16
+ # Walks a directory of per-formula font subdirectories, audits each
17
+ # via {Audit::LibraryAuditor}, and passes the resulting
18
+ # {Audit::Release::FormulaAudits} list to {Audit::Release::Emitter}.
19
+ #
20
+ # The release tree lives at `<output_root>/font_audit_release/`.
21
+ # The CI collector job invokes this after matrix-auditing every
22
+ # formula and pre-staging the universal-set directory.
23
+ class ReleaseCommand
24
+ FormulaSource = Struct.new(:slug, :path, keyword_init: true)
25
+
26
+ Result = Struct.new(:release_root, :formulas_total, :faces_total,
27
+ :formulas, :universal_set_available,
28
+ :library_index_written, :manifest_written,
29
+ :error, keyword_init: true)
30
+
31
+ # @param from [String, Pathname] directory containing one
32
+ # subdirectory per formula. Each subdirectory's name becomes
33
+ # the formula slug; its contents are audited via
34
+ # {Audit::LibraryAuditor} (recursive walk).
35
+ # @param output_root [String, Pathname] parent of the release
36
+ # root. Release tree lives at
37
+ # `<output_root>/font_audit_release/`.
38
+ # @param universal_set_root [String, Pathname, nil] location of
39
+ # the universal_glyph_set directory. Defaults to
40
+ # `<release_root>/universal_glyph_set` inside the release tree.
41
+ # @param unicode_version [String, nil] baseline UCD version.
42
+ # @param recursive [Boolean] recursively walk each formula
43
+ # subdirectory. Default true.
44
+ # @param brief [Boolean] cheap-extractor-only audit mode.
45
+ # @param browse [Boolean] also emit per-face HTML browsers.
46
+ # @param source_config_sha256 [String, nil] sha256 of the Tier 1
47
+ # source-config YAML (TODO 23). Recorded in the manifest for
48
+ # curation provenance.
49
+ # @param reference [Audit::CoverageReference, nil] baseline
50
+ # forwarded to every per-face audit (TODO 25).
51
+ # @param generated_at [String] ISO8601 timestamp. Default: now.
52
+ # @return [Result]
53
+ def call(from:, output_root:, universal_set_root: nil, unicode_version: nil,
54
+ recursive: true, brief: false, browse: true,
55
+ source_config_sha256: nil, reference: nil,
56
+ generated_at: Time.now.utc.iso8601)
57
+ formula_sources = discover_formulas(from)
58
+ formulas = formula_sources.map do |src|
59
+ summary = audit_formula(src.path, recursive: recursive,
60
+ unicode_version: unicode_version,
61
+ brief: brief, reference: reference)
62
+ Ucode::Audit::Release::FormulaAudits.new(slug: src.slug, summary: summary)
63
+ end
64
+
65
+ emitter = Ucode::Audit::Release::Emitter.new(
66
+ output_root: output_root,
67
+ universal_set_root: universal_set_root,
68
+ with_missing_glyph_pages: browse,
69
+ )
70
+ emit_result = emitter.emit(
71
+ formulas: formulas,
72
+ unicode_version: unicode_version,
73
+ generated_at: generated_at,
74
+ source_config_sha256: source_config_sha256,
75
+ )
76
+
77
+ Result.new(
78
+ release_root: emit_result.release_root,
79
+ formulas_total: emit_result.formulas_total,
80
+ faces_total: emit_result.faces_total,
81
+ formulas: formula_sources,
82
+ universal_set_available: emit_result.universal_set_available,
83
+ library_index_written: emit_result.library_index_written,
84
+ manifest_written: emit_result.manifest_written,
85
+ )
86
+ rescue StandardError => e
87
+ Result.new(error: "#{e.class}: #{e.message}")
88
+ end
89
+
90
+ private
91
+
92
+ def discover_formulas(from)
93
+ Pathname.new(from).children.select(&:directory?).sort.map do |d|
94
+ FormulaSource.new(slug: d.basename.to_s, path: d.to_s)
95
+ end
96
+ end
97
+
98
+ def audit_formula(path, recursive:, unicode_version:, brief:, reference:)
99
+ options = audit_options(unicode_version: unicode_version, brief: brief)
100
+ auditor = Ucode::Audit::LibraryAuditor.new(
101
+ path, recursive: recursive, options: options, reference: reference
102
+ )
103
+ auditor.audit
104
+ end
105
+
106
+ def audit_options(unicode_version:, brief:)
107
+ opts = {}
108
+ opts[:ucd_version] = unicode_version if unicode_version
109
+ opts[:audit_brief] = true if brief
110
+ opts
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "pathname"
6
+
7
+ require "ucode/cache"
8
+ require "ucode/commands"
9
+ require "ucode/coordinator"
10
+ require "ucode/database"
11
+ require "ucode/glyphs"
12
+ require "ucode/models"
13
+ require "ucode/version"
14
+ require "ucode/version_resolver"
15
+
16
+ module Ucode
17
+ module Commands
18
+ # `ucode universal-set` subcommands (TODOs 24 + 31):
19
+ #
20
+ # - `BuildCommand` — drain the codepoint stream through the
21
+ # 4-tier resolver and write glyphs + manifest. Runs
22
+ # {PreBuildCheck} first; emits {CoverageReport} after.
23
+ # - `PreCheckCommand` — standalone pre-build validation
24
+ # (config + fonts + coverage assertion).
25
+ # - `ReportCommand` — re-emit coverage reports from an
26
+ # existing manifest. Useful for iterating on curation without
27
+ # re-running the build.
28
+ # - `ValidateCommand` — post-build structural validation
29
+ # (manifest parses, every entry has a glyph, totals reconcile,
30
+ # provenance recorded).
31
+ #
32
+ # The set is the canonical reference for "what Unicode 17 looks
33
+ # like" — every assigned codepoint has exactly one glyph, with
34
+ # documented provenance. Audits (TODO 25) and the fontist.org
35
+ # consumer (TODO 27) read the manifest to answer "is this
36
+ # codepoint in the universal set?" without re-reading every SVG.
37
+ module UniversalSet
38
+ # `ucode universal-set build` action class. Pure Ruby — Thor
39
+ # (in `lib/ucode/cli.rb`) is responsible only for argument
40
+ # parsing and dispatch.
41
+ class BuildCommand
42
+ # @param version [String] resolved UCD version
43
+ # @param output_root [String, Pathname] directory that will
44
+ # hold `manifest.json`, `glyphs/`, `reports/`.
45
+ # @param source_config_path [String, Pathname, nil] override
46
+ # the Tier 1 font config YAML; nil uses the default at
47
+ # `Ucode::Glyphs::SourceConfig::DEFAULT_PATH`.
48
+ # @param resolver [Ucode::Glyphs::Resolver, nil] inject a
49
+ # pre-built resolver (skips SourceBuilder + PreBuildCheck);
50
+ # used by tests.
51
+ # @param block_filter [String, nil] limit the build to one
52
+ # block (canonical underscore form). Useful for partial
53
+ # rebuilds when iterating on Tier 1 curation.
54
+ # @param parallel_workers [Integer] forwarded to the Builder.
55
+ # Defaults to {Ucode::Configuration#parallel_workers}.
56
+ # @param skip_pre_check [Boolean] when true, skip the
57
+ # {PreBuildCheck} step. Used by tests that inject a custom
58
+ # resolver and don't have a real source config on disk.
59
+ # @return [Hash] { version:, manifest_path:, totals:,
60
+ # by_tier:, coverage:, validation: }
61
+ def call(version, output_root:, source_config_path: nil,
62
+ resolver: nil, block_filter: nil,
63
+ parallel_workers: default_workers, skip_pre_check: false)
64
+ root = Pathname.new(output_root)
65
+
66
+ config_path = source_config_path_or_default(source_config_path)
67
+ sha = source_config_sha256(config_path)
68
+ database = Database.open(version)
69
+
70
+ run_pre_check(config_path, database) unless skip_pre_check
71
+
72
+ resolved_resolver = resolver || build_resolver(version, config_path, database)
73
+
74
+ builder = Glyphs::UniversalSet::Builder.new(
75
+ output_root: root,
76
+ resolver: resolved_resolver,
77
+ unicode_version: version,
78
+ ucode_version: Ucode::VERSION,
79
+ source_config_sha256: sha,
80
+ parallel_workers: parallel_workers,
81
+ block_filter: block_filter,
82
+ )
83
+
84
+ manifest_path = builder.build(codepoint_enum(version))
85
+
86
+ manifest = Ucode::Models::UniversalSetManifest.from_hash(
87
+ JSON.parse(manifest_path.read),
88
+ )
89
+ coverage = Glyphs::UniversalSet::CoverageReport
90
+ .new(root, database: database).emit(manifest)
91
+ validation = Glyphs::UniversalSet::Validator
92
+ .new(root, unicode_version: version).validate
93
+ {
94
+ version: version,
95
+ manifest_path: manifest_path,
96
+ totals: manifest.totals.to_hash,
97
+ by_tier: manifest.by_tier,
98
+ coverage: coverage,
99
+ validation: validation,
100
+ }
101
+ end
102
+
103
+ private
104
+
105
+ def default_workers
106
+ Ucode.configuration.parallel_workers
107
+ end
108
+
109
+ def source_config_path_or_default(path)
110
+ return Glyphs::SourceConfig::DEFAULT_PATH if path.nil?
111
+
112
+ Pathname.new(path)
113
+ end
114
+
115
+ def source_config_sha256(path)
116
+ return "" unless path.exist?
117
+
118
+ Digest::SHA256.file(path).hexdigest
119
+ end
120
+
121
+ def run_pre_check(config_path, database)
122
+ Glyphs::UniversalSet::PreBuildCheck.new(
123
+ source_config_path: config_path,
124
+ database: database,
125
+ ).call
126
+ end
127
+
128
+ def build_resolver(_version, config_path, database)
129
+ config = Glyphs::SourceConfig.new(path: config_path)
130
+ builder = Glyphs::SourceBuilder.new(config: config, database: database)
131
+ Glyphs::Resolver.new(sources: builder.tier1_sources(install: false))
132
+ end
133
+
134
+ def codepoint_enum(version)
135
+ ucd_dir = Cache.ucd_dir(version)
136
+ unihan_dir = Cache.unihan_dir(version)
137
+ Coordinator.new.each_codepoint(ucd_dir: ucd_dir, unihan_dir: unihan_dir)
138
+ end
139
+ end
140
+
141
+ # `ucode universal-set pre-check` — standalone pre-build
142
+ # validation. Runs the three TODO 31 §Pre-build validation
143
+ # checks (config loads, fonts present, coverage assertion runs)
144
+ # without starting the 4-hour build.
145
+ class PreCheckCommand
146
+ # @param version [String] resolved UCD version
147
+ # @param source_config_path [String, Pathname, nil]
148
+ # @param cmaps [#covers?] injectable; defaults to
149
+ # RealFonts::CmapCache.
150
+ # @param font_locator [#locate] injectable; defaults to a
151
+ # fresh FontLocator.
152
+ # @return [Ucode::Glyphs::UniversalSet::PreBuildReport]
153
+ # @raise [Ucode::UniversalSetPreBuildError] when missing_fonts
154
+ # is non-empty or the config fails to load.
155
+ def call(version, source_config_path: nil, cmaps: nil,
156
+ font_locator: nil)
157
+ database = Database.open(version)
158
+ config_path = source_config_path || Glyphs::SourceConfig::DEFAULT_PATH
159
+
160
+ kwargs = { source_config_path: config_path, database: database }
161
+ kwargs[:cmaps] = cmaps if cmaps
162
+ kwargs[:font_locator] = font_locator if font_locator
163
+ Glyphs::UniversalSet::PreBuildCheck.new(**kwargs).call
164
+ end
165
+ end
166
+
167
+ # `ucode universal-set report` — re-emit coverage reports from
168
+ # an existing manifest. Useful when iterating on the manifest
169
+ # shape (or regenerating reports after a model change) without
170
+ # re-running the build.
171
+ class ReportCommand
172
+ # @param version [String] resolved UCD version
173
+ # @param output_root [String, Pathname] directory holding
174
+ # `manifest.json`.
175
+ # @return [Hash] the {CoverageReport#emit} payload.
176
+ def call(version, output_root:)
177
+ root = Pathname.new(output_root)
178
+ manifest_path = root.join("manifest.json")
179
+ raise Ucode::Error, "manifest not found at #{manifest_path}" unless manifest_path.exist?
180
+
181
+ manifest = Ucode::Models::UniversalSetManifest.from_hash(
182
+ JSON.parse(manifest_path.read),
183
+ )
184
+ database = Database.open(version)
185
+ Glyphs::UniversalSet::CoverageReport.new(root, database: database)
186
+ .emit(manifest)
187
+ end
188
+ end
189
+
190
+ # `ucode universal-set validate` — post-build structural
191
+ # validation. Reads `manifest.json` + `glyphs/` and runs the
192
+ # four checks (manifest_loadable, glyph_files_present,
193
+ # totals_reconcile, provenance_complete).
194
+ class ValidateCommand
195
+ # @param output_root [String, Pathname]
196
+ # @param version [String, nil] resolved UCD version, used only
197
+ # to stamp the report's unicode_version when the manifest's
198
+ # recorded value is missing.
199
+ # @return [Hash] the {Validator#validate} payload.
200
+ def call(output_root, version: nil)
201
+ Glyphs::UniversalSet::Validator
202
+ .new(output_root, unicode_version: version).validate
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -14,6 +14,11 @@ module Ucode
14
14
  autoload :LookupCommand, "ucode/commands/lookup"
15
15
  autoload :CacheCommand, "ucode/commands/cache"
16
16
  autoload :BuildCommand, "ucode/commands/build"
17
+ autoload :CanonicalBuildCommand, "ucode/commands/canonical_build"
17
18
  autoload :FontCoverageCommand, "ucode/commands/font_coverage"
19
+ autoload :UniversalSet, "ucode/commands/universal_set"
20
+ autoload :Audit, "ucode/commands/audit"
21
+ autoload :ReleaseCommand, "ucode/commands/release"
22
+ autoload :BlockFeedCommand, "ucode/commands/block_feed"
18
23
  end
19
24
  end