ucode 0.1.1 → 0.2.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/TODO.extract-code-chart/01-pdf-fetch-validation.md +80 -0
  3. data/TODO.extract-code-chart/02-block-name-resolver.md +68 -0
  4. data/TODO.extract-code-chart/03-codechart-namespace.md +82 -0
  5. data/TODO.extract-code-chart/04-codechart-extractor.md +154 -0
  6. data/TODO.extract-code-chart/05-provenance-and-sidecar.md +147 -0
  7. data/TODO.extract-code-chart/06-codechart-writer.md +134 -0
  8. data/TODO.extract-code-chart/07-codechart-cli.md +135 -0
  9. data/TODO.extract-code-chart/08-specs.md +87 -0
  10. data/lib/ucode/audit/reference_factory.rb +66 -0
  11. data/lib/ucode/audit.rb +1 -0
  12. data/lib/ucode/cli.rb +134 -16
  13. data/lib/ucode/code_chart/extractor.rb +122 -0
  14. data/lib/ucode/code_chart/provenance.rb +81 -0
  15. data/lib/ucode/code_chart/sidecar.rb +52 -0
  16. data/lib/ucode/code_chart/writer.rb +128 -0
  17. data/lib/ucode/code_chart.rb +39 -0
  18. data/lib/ucode/commands/audit.rb +0 -1
  19. data/lib/ucode/commands/build.rb +4 -0
  20. data/lib/ucode/commands/canonical_build.rb +2 -3
  21. data/lib/ucode/commands/fetch.rb +12 -14
  22. data/lib/ucode/commands/glyphs.rb +25 -67
  23. data/lib/ucode/commands/lookup.rb +11 -11
  24. data/lib/ucode/commands/parse.rb +7 -5
  25. data/lib/ucode/commands/release.rb +0 -1
  26. data/lib/ucode/commands/universal_set.rb +10 -14
  27. data/lib/ucode/coordinator/indices.rb +38 -2
  28. data/lib/ucode/error.rb +11 -0
  29. data/lib/ucode/fetch/code_charts.rb +1 -1
  30. data/lib/ucode/fetch/http.rb +84 -14
  31. data/lib/ucode/glyphs/pipeline.rb +106 -0
  32. data/lib/ucode/glyphs.rb +1 -0
  33. data/lib/ucode/parsers/blocks.rb +34 -0
  34. data/lib/ucode/repo/aggregate_writer.rb +60 -298
  35. data/lib/ucode/repo/writers/blocks_writer.rb +73 -0
  36. data/lib/ucode/repo/writers/enums_writer.rb +38 -0
  37. data/lib/ucode/repo/writers/indexes_writer.rb +53 -0
  38. data/lib/ucode/repo/writers/manifest_writer.rb +78 -0
  39. data/lib/ucode/repo/writers/named_sequences_writer.rb +47 -0
  40. data/lib/ucode/repo/writers/planes_writer.rb +82 -0
  41. data/lib/ucode/repo/writers/relationships_writer.rb +71 -0
  42. data/lib/ucode/repo/writers/scripts_writer.rb +54 -0
  43. data/lib/ucode/repo/writers.rb +20 -0
  44. data/lib/ucode/repo.rb +1 -0
  45. data/lib/ucode/version.rb +1 -1
  46. data/lib/ucode.rb +3 -0
  47. data/ucode.gemspec +56 -0
  48. metadata +31 -5
  49. data/Gemfile.lock +0 -406
  50. data/lib/ucode/commands/audit/reference_builder.rb +0 -64
@@ -0,0 +1,135 @@
1
+ # TODO 07 — CodeChart CLI
2
+
3
+ ## Status
4
+
5
+ Pending. Depends on TODO 06 (Writer), TODO 04 (Extractor),
6
+ TODO 02 (block name resolver).
7
+
8
+ ## Goal
9
+
10
+ `ucode code-chart fetch | extract | list` — the REQ (R4) commands.
11
+ Thin Thor wrappers that delegate to the existing `CodeChart::*`
12
+ modules. No orchestration logic in the CLI; every command is a
13
+ single delegation.
14
+
15
+ ## Files
16
+
17
+ - `lib/ucode/cli.rb` — add the `CodeChartCmd` Thor subcommand class.
18
+ - `spec/ucode/cli_spec.rb` (extend existing) — verify the new
19
+ subcommand wires up.
20
+
21
+ ## Design
22
+
23
+ ### Subcommand shape
24
+
25
+ ```ruby
26
+ class Cli < Thor
27
+ # …existing commands…
28
+
29
+ class CodeChartCmd < Thor
30
+ desc "fetch --block BLOCK", "Download the Code Charts PDF for a block"
31
+ option :block, type: :string, required: true,
32
+ desc: "Block identifier (e.g. Sidetic)"
33
+ def fetch
34
+ puts JSON.pretty_generate(
35
+ Commands::FetchCommand.new.fetch_charts(
36
+ VersionResolver.resolve(nil),
37
+ block_first_cps: [block_first_cp!(options[:block])],
38
+ ),
39
+ )
40
+ end
41
+
42
+ desc "extract --block BLOCK --to DIR", "Extract per-codepoint SVG + provenance sidecars"
43
+ option :block, type: :string, required: true,
44
+ desc: "Block identifier (e.g. Sidetic)"
45
+ option :to, type: :string, required: true,
46
+ desc: "Output directory"
47
+ def extract
48
+ # ...
49
+ end
50
+
51
+ desc "list", "List blocks that have Code Charts PDFs available locally"
52
+ def list
53
+ # ...
54
+ end
55
+ end
56
+
57
+ desc "code-chart", "Extract per-codepoint SVG glyphs from Unicode Code Charts PDFs"
58
+ subcommand "code-chart", CodeChartCmd
59
+ end
60
+ ```
61
+
62
+ ### `extract` flow
63
+
64
+ ```ruby
65
+ def extract
66
+ Ucode::Commands::FetchCommand.new.fetch_charts(
67
+ VersionResolver.resolve(nil),
68
+ block_first_cps: [block_first_cp!(options[:block])],
69
+ )
70
+ blocks_txt = Ucode::Cache.ucd_dir(VersionResolver.resolve(nil)).join("Blocks.txt")
71
+ block = Parsers::Blocks.find_by_name(blocks_txt, options[:block]) or
72
+ raise Thor::Error, "Unknown block: #{options[:block].inspect}"
73
+ pdf = Ucode::Glyphs::PdfFetcher.new(
74
+ VersionResolver.resolve(nil),
75
+ monolith_path: nil,
76
+ blocks: [block],
77
+ ).fetch(block_first_cp: block.range_first, force: false) or
78
+ raise Thor::Error, "PDF unavailable for block #{options[:block]}"
79
+
80
+ writer = Ucode::CodeChart::Writer.new(
81
+ output_root: Pathname.new(options[:to]),
82
+ pdf_path: pdf,
83
+ blocks_txt: blocks_txt,
84
+ )
85
+ summary = writer.write(block)
86
+ puts JSON.pretty_generate(summary.to_h.compact)
87
+ end
88
+
89
+ def block_first_cp!(block_id)
90
+ cache = Ucode::Cache.ucd_dir(VersionResolver.resolve(nil))
91
+ block = Ucode::Parsers::Blocks.find_by_name(cache.join("Blocks.txt"), block_id)
92
+ raise Thor::Error, "Unknown block: #{block_id.inspect}" unless block
93
+ block.range_first
94
+ end
95
+ ```
96
+
97
+ ### Why `Ucode::Commands::FetchCommand.new.fetch_charts` for fetch
98
+
99
+ `fetch_charts` is the existing CLI hook for "download a Code Charts
100
+ PDF for these block first-cps." We just call it with the block's
101
+ first cp. Reuse, don't reimplement.
102
+
103
+ ### Why resolve version once at the top of `extract`
104
+
105
+ Per Candidate 4 of the architecture review (`refactor/build-context-resolve-version-once`,
106
+ merged): every CLI method resolves the version once and threads it
107
+ through. This CLI method does the same — one call per invocation.
108
+
109
+ ### Why no separate `Commands::CodeChartCommand` class
110
+
111
+ Following the existing pattern (e.g. `Cli::Audit` calls
112
+ `Commands::Audit::*Command` *only* when the logic is non-trivial).
113
+ The CodeChart commands are trivial delegations — a one-liner each.
114
+ The CLI methods call `CodeChart::Writer` and `CodeChart::Extractor`
115
+ directly. Adding a `Commands::CodeChartCommand` class would be
116
+ indirection without a payoff.
117
+
118
+ If the extract logic grows (e.g. progress reporting, partial
119
+ extraction), extract it into a Command class at that point.
120
+
121
+ ## Acceptance
122
+
123
+ - `ucode code-chart fetch --block Sidetic` downloads the PDF.
124
+ - `ucode code-chart extract --block Sidetic --to /tmp/s/` extracts
125
+ to the given directory.
126
+ - `ucode code-chart list` prints available blocks.
127
+ - Unknown block names produce a clean error, not a stack trace.
128
+ - The CLI matches the REQ's signature exactly.
129
+
130
+ ## Out of scope
131
+
132
+ - `--version` flag (the REQ doesn't specify, and existing commands
133
+ default to the configured version).
134
+ - `--format svg|glif` (the REQ specifies SVG; `.glif` output is a
135
+ future extension).
@@ -0,0 +1,87 @@
1
+ # TODO 08 — CodeChart specs
2
+
3
+ ## Status
4
+
5
+ Pending. Depends on TODOs 01–07.
6
+
7
+ ## Goal
8
+
9
+ Comprehensive spec coverage for every new module. Per the project
10
+ rules: real model instances (no doubles), tight focused tests,
11
+ behavior assertions (not interaction counts).
12
+
13
+ ## Files
14
+
15
+ Already enumerated per TODO. Final spec coverage:
16
+
17
+ - `spec/ucode/fetch/code_charts_spec.rb` — happy path + HTTP 4xx +
18
+ wrong content-type + non-PDF body. (TODO 01)
19
+ - `spec/ucode/parsers/blocks_spec.rb` (extend) — `find_by_name`
20
+ happy path + nil on miss. (TODO 02)
21
+ - `spec/ucode/code_chart/extractor_spec.rb` — constructor invariants,
22
+ Resolver wiring, integration test against fixture PDF. (TODO 04)
23
+ - `spec/ucode/code_chart/provenance_spec.rb` — value object
24
+ construction + `to_h` schema. (TODO 05)
25
+ - `spec/ucode/code_chart/sidecar_spec.rb` — write sidecar, idempotent
26
+ re-write. (TODO 05)
27
+ - `spec/ucode/code_chart/writer_spec.rb` — full lifecycle:
28
+ extract → write → summary. Idempotent re-run produces byte-identical
29
+ files. (TODO 06)
30
+ - `spec/ucode/cli_spec.rb` (extend) — verify `ucode code-chart fetch`,
31
+ `extract`, `list` wire up. (TODO 07)
32
+
33
+ ## Design
34
+
35
+ ### Fixture strategy
36
+
37
+ The existing `spec/fixtures/pdfs/basic_latin.pdf` is the only PDF
38
+ fixture in the repo. It's tiny and validates the integration path.
39
+ The Sidetic + Egyptian Ext-B PDFs are large (whole-block) and would
40
+ inflate the repo. The integration spec uses `basic_latin.pdf` to
41
+ exercise the full pipeline; per-codepoint assertions cover
42
+ representative cases.
43
+
44
+ If Sidetic-specific behavior must be tested, a smaller fixture PDF
45
+ cropped to ~5 codepoints would be the right tool — out of scope for
46
+ this TODO.
47
+
48
+ ### No doubles policy
49
+
50
+ The project's `~/.claude/CLAUDE.md` rule: no doubles. All specs use
51
+ real instances:
52
+ - `Ucode::Models::Block.new(...)` for test blocks.
53
+ - A temp directory + real `Blocks.txt` text for parser specs.
54
+ - The real `Ucode::Glyphs::Resolver` for extractor specs.
55
+
56
+ ### Idempotency assertion pattern
57
+
58
+ `Writer#write` idempotency is asserted via byte-equality:
59
+
60
+ ```ruby
61
+ first_run = writer.write(block)
62
+ first_size = File.stat(svg_path).size
63
+ sleep 0.01 # ensure mtime changes would be detectable
64
+ second_run = writer.write(block)
65
+ second_size = File.stat(svg_path).size
66
+ expect(second_size).to eq(first_size)
67
+ expect(File.read(svg_path)).to eq(expected_svg_bytes)
68
+ ```
69
+
70
+ This is the existing pattern from `spec/ucode/repo/aggregate_writer_spec.rb`
71
+ (idiempotency spec there). Reuse.
72
+
73
+ ## Acceptance
74
+
75
+ - `bundle exec rspec spec/ucode/code_chart/ spec/ucode/fetch/code_charts_spec.rb spec/ucode/parsers/blocks_spec.rb`
76
+ passes 100%.
77
+ - Coverage for the new files is ≥ 95% (per the project's per-file
78
+ floor of 30% + the overall 80% minimum).
79
+ - No doubles are introduced (verify with `grep -r "double(" spec/ucode/code_chart/`).
80
+ - The integration spec exercises both the Extractor and Writer
81
+ together end-to-end.
82
+
83
+ ## Out of scope
84
+
85
+ - Performance benchmarks — separate concern.
86
+ - Sidetic-specific fixtures — requires PDF curation beyond the
87
+ scope of this feature.
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Ucode
6
+ module Audit
7
+ # Translates CLI flags into a {Ucode::Audit::CoverageReference}.
8
+ #
9
+ # The audit CLI exposes the universal-set reference via a
10
+ # `--reference-universal-set=<path>` flag (and a default lookup
11
+ # at `output/universal_glyph_set/manifest.json`). This factory
12
+ # resolves the flag value into a concrete reference instance
13
+ # backed by a freshly-opened {Ucode::Database}, so the command
14
+ # classes don't repeat the same branching.
15
+ #
16
+ # Behavior:
17
+ #
18
+ # - flag = "none" → nil (force UCD-only even if a default manifest exists)
19
+ # - flag = path to .json → {Ucode::Audit::UniversalSetReference}
20
+ # - flag = nil → look at DEFAULT_MANIFEST_PATH; use it if present,
21
+ # else nil (UCD-only)
22
+ #
23
+ # Lives in the {Ucode::Audit} namespace (not {Ucode::Commands::Audit})
24
+ # so the Audit module owns its own entry point — programmatic callers
25
+ # don't need to round-trip through the CLI to obtain a reference.
26
+ module ReferenceFactory
27
+ DEFAULT_MANIFEST_PATH = Pathname.new("output/universal_glyph_set/manifest.json")
28
+
29
+ module_function
30
+
31
+ # @param flag [String, nil] value of the
32
+ # `--reference-universal-set` CLI option.
33
+ # @param version [String, nil] UCD version for the database
34
+ # that backs the reference. When nil, the default UCD
35
+ # version is resolved.
36
+ # @return [Ucode::Audit::CoverageReference, nil]
37
+ def build_from_cli(flag:, version: nil)
38
+ return nil if flag == "none"
39
+
40
+ path = resolve_manifest_path(flag)
41
+ return nil unless path && File.exist?(path)
42
+
43
+ database = open_database(version)
44
+ return nil unless database
45
+
46
+ Ucode::Audit::UniversalSetReference.new(
47
+ manifest: path, database: database,
48
+ )
49
+ end
50
+
51
+ def resolve_manifest_path(flag)
52
+ return Pathname.new(flag) if flag && flag != "none"
53
+ return DEFAULT_MANIFEST_PATH if DEFAULT_MANIFEST_PATH.exist?
54
+
55
+ nil
56
+ end
57
+
58
+ def open_database(version)
59
+ resolved = version || Ucode::VersionResolver.resolve(nil)
60
+ Ucode::Database.open(resolved)
61
+ rescue Ucode::UnknownVersionError, Ucode::DatabaseMissingError
62
+ nil
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/ucode/audit.rb CHANGED
@@ -32,6 +32,7 @@ module Ucode
32
32
  autoload :CoverageReference, "ucode/audit/coverage_reference"
33
33
  autoload :UcdOnlyReference, "ucode/audit/ucd_only_reference"
34
34
  autoload :UniversalSetReference, "ucode/audit/universal_set_reference"
35
+ autoload :ReferenceFactory, "ucode/audit/reference_factory"
35
36
 
36
37
  # Per-face orchestrator (TODO 11) — shared by LibraryAuditor and
37
38
  # the future CLI AuditCommand.
data/lib/ucode/cli.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require "thor"
4
4
 
5
5
  require "ucode/commands"
6
+ require "ucode/code_chart"
7
+ require "ucode/version_resolver"
6
8
 
7
9
  module Ucode
8
10
  # Top-level CLI entry.
@@ -10,6 +12,12 @@ module Ucode
10
12
  # **Thin Thor**: every method delegates to a `Commands::*Command`
11
13
  # class and only formats the result. The Command classes are pure
12
14
  # and testable in-process — Thor never holds business logic.
15
+ #
16
+ # **Version resolution lives here** — each top-level command resolves
17
+ # the user-supplied intent (nil / :default / :latest / explicit string)
18
+ # exactly once via `VersionResolver.resolve` and threads the resolved
19
+ # string into the dispatched Command. Sub-commands never re-resolve.
20
+ # See Candidate 4 of the 2026-06-29 architecture review.
13
21
  class Cli < Thor
14
22
  package_name "ucode"
15
23
 
@@ -28,13 +36,17 @@ module Ucode
28
36
  desc "ucd [VERSION]", "Download UCD.zip"
29
37
  option :force, type: :boolean, default: false, desc: "Re-download even if cached"
30
38
  def ucd(version = nil)
31
- puts format_result Commands::FetchCommand.new.fetch_ucd(version, force: options[:force])
39
+ puts format_result Commands::FetchCommand.new.fetch_ucd(
40
+ VersionResolver.resolve(version), force: options[:force],
41
+ )
32
42
  end
33
43
 
34
44
  desc "unihan [VERSION]", "Download Unihan.zip"
35
45
  option :force, type: :boolean, default: false
36
46
  def unihan(version = nil)
37
- puts format_result Commands::FetchCommand.new.fetch_unihan(version, force: options[:force])
47
+ puts format_result Commands::FetchCommand.new.fetch_unihan(
48
+ VersionResolver.resolve(version), force: options[:force],
49
+ )
38
50
  end
39
51
 
40
52
  desc "charts [VERSION]", "Download per-block Code Charts PDFs"
@@ -43,7 +55,8 @@ module Ucode
43
55
  def charts(version = nil)
44
56
  cps = options[:block]&.map { |id| block_id_to_first_cp(id) }&.compact
45
57
  puts format_result Commands::FetchCommand.new
46
- .fetch_charts(version, block_first_cps: cps, force: options[:force])
58
+ .fetch_charts(VersionResolver.resolve(version),
59
+ block_first_cps: cps, force: options[:force])
47
60
  end
48
61
 
49
62
  desc "fonts", "Download specialist Tier 1 fonts (config/specialist_fonts.yml)"
@@ -90,7 +103,9 @@ module Ucode
90
103
  desc "parse [VERSION]", "Stream UCD → output/"
91
104
  option :to, type: :string, default: "./output", desc: "Output directory"
92
105
  def parse(version = nil)
93
- result = Commands::ParseCommand.new.call(version, output_root: options[:to])
106
+ result = Commands::ParseCommand.new.call(
107
+ VersionResolver.resolve(version), output_root: options[:to],
108
+ )
94
109
  puts JSON.pretty_generate(result)
95
110
  end
96
111
 
@@ -111,7 +126,7 @@ module Ucode
111
126
  desc: "Opt into the experimental v0.1 pipeline"
112
127
  def glyphs(version = nil)
113
128
  result = Commands::GlyphsCommand.new.call(
114
- version,
129
+ VersionResolver.resolve(version),
115
130
  output_root: options[:to],
116
131
  block_filter: options[:block],
117
132
  force: options[:force],
@@ -143,6 +158,104 @@ module Ucode
143
158
  desc "site", "Generate the Vitepress site"
144
159
  subcommand "site", Site
145
160
 
161
+ # ─────────────── code-chart ───────────────
162
+ # Extract per-codepoint SVG glyphs from a Unicode Code Charts PDF.
163
+ # One folder per block under --to, with <U+XXXX>.svg + .json pairs.
164
+ class CodeChartCmd < Thor
165
+ desc "fetch --block BLOCK [VERSION]", "Download the Code Charts PDF for a block"
166
+ option :block, type: :string, required: true,
167
+ desc: "Block identifier (e.g. Sidetic, Basic_Latin)"
168
+ def fetch(version = nil)
169
+ with_codechart_errors do
170
+ block_first_cp = resolve_block_first_cp!(options[:block], version)
171
+ result = Commands::FetchCommand.new.fetch_charts(
172
+ VersionResolver.resolve(version),
173
+ block_first_cps: [block_first_cp],
174
+ )
175
+ puts JSON.pretty_generate(result)
176
+ end
177
+ end
178
+
179
+ desc "extract --block BLOCK --to DIR [VERSION]",
180
+ "Extract per-codepoint SVG + provenance sidecars from a Code Charts PDF"
181
+ option :block, type: :string, required: true,
182
+ desc: "Block identifier (e.g. Sidetic)"
183
+ option :to, type: :string, required: true,
184
+ desc: "Output directory (will contain <block_id>/<U+XXXX>.svg + .json)"
185
+ def extract(version = nil)
186
+ with_codechart_errors do
187
+ version_str = VersionResolver.resolve(version)
188
+ block = resolve_block!(options[:block], version_str)
189
+ block_first_cp = block.range_first
190
+
191
+ # Download (idempotent — re-runs skip when the PDF is cached).
192
+ Commands::FetchCommand.new.fetch_charts(version_str, block_first_cps: [block_first_cp])
193
+
194
+ pdf = Ucode::Glyphs::PdfFetcher.new(version_str)
195
+ .fetch(block_first_cp: block_first_cp)
196
+ raise Ucode::CodeChartNotFoundError.new(
197
+ "Code Charts PDF unavailable for block #{block.id.inspect}",
198
+ context: { block_id: block.id, version: version_str },
199
+ ) unless pdf
200
+
201
+ writer = Ucode::CodeChart::Writer.new(
202
+ output_root: Pathname.new(options[:to]),
203
+ pdf_path: pdf,
204
+ ucd_version: version_str,
205
+ )
206
+ summary = writer.write(block)
207
+ puts JSON.pretty_generate(summary.to_h.compact)
208
+ end
209
+ end
210
+
211
+ desc "list", "List cached Code Charts PDFs under the version's cache"
212
+ def list
213
+ version = VersionResolver.resolve(nil)
214
+ pdfs_dir = Ucode::Cache.pdfs_dir(version)
215
+ files = pdfs_dir.exist? ? pdfs_dir.children.sort : []
216
+ if files.empty?
217
+ puts "(no cached Code Charts PDFs)"
218
+ return
219
+ end
220
+ files.each do |f|
221
+ puts f.basename.to_s
222
+ end
223
+ end
224
+
225
+ private
226
+
227
+ # Resolve a block name to its first codepoint via the cached
228
+ # Blocks.txt. Raises {Ucode::UnknownBlockError} on miss.
229
+ def resolve_block!(block_id, version)
230
+ blocks_txt = Ucode::Cache.ucd_dir(VersionResolver.resolve(version)).join("Blocks.txt")
231
+ Ucode::Parsers::Blocks.find_by_id!(blocks_txt, block_id)
232
+ end
233
+
234
+ def resolve_block_first_cp!(block_id, version)
235
+ resolve_block!(block_id, version).range_first
236
+ end
237
+
238
+ # Convert semantic Ucode errors into Thor errors so Thor's
239
+ # dispatch prints the message cleanly instead of a stack trace.
240
+ # Thor's `start` rescues only `Thor::Error`; without this bridge,
241
+ # any `Ucode::Error` subclass propagates as an uncaught exception.
242
+ def with_codechart_errors
243
+ yield
244
+ rescue Ucode::Error => e
245
+ raise Thor::Error, e.message
246
+ end
247
+ end
248
+
249
+ # Register the subcommand under the underscored method name
250
+ # (`code_chart`). Thor's `normalize_command_name` converts the
251
+ # user's hyphenated form (`code-chart`) to the underscored form
252
+ # before lookup, so `ucode code-chart <cmd>` dispatches correctly.
253
+ # `desc` first registers the method as a Thor command so the
254
+ # dispatch table has an entry; `subcommand` then attaches the
255
+ # CodeChartCmd class to it.
256
+ desc "code_chart <command>", "Extract SVG glyphs from Unicode Code Charts PDFs"
257
+ subcommand "code_chart", CodeChartCmd
258
+
146
259
  # ─────────────── lookup ───────────────
147
260
  class Lookup < Thor
148
261
  desc "block CODEPOINT", "Block name covering CODEPOINT (integer or 0xNNNN)"
@@ -150,7 +263,9 @@ module Ucode
150
263
  def block(codepoint)
151
264
  cp = parse_cp(codepoint)
152
265
  with_db_handling do
153
- result = Commands::LookupCommand.new.lookup_block(options[:version], codepoint: cp)
266
+ result = Commands::LookupCommand.new.lookup_block(
267
+ VersionResolver.resolve(options[:version]), codepoint: cp,
268
+ )
154
269
  puts "#{format("U+%04X", cp)} → #{result.block || "(unassigned)"}"
155
270
  end
156
271
  end
@@ -160,7 +275,9 @@ module Ucode
160
275
  def script(codepoint)
161
276
  cp = parse_cp(codepoint)
162
277
  with_db_handling do
163
- result = Commands::LookupCommand.new.lookup_script(options[:version], codepoint: cp)
278
+ result = Commands::LookupCommand.new.lookup_script(
279
+ VersionResolver.resolve(options[:version]), codepoint: cp,
280
+ )
164
281
  puts "#{format("U+%04X", cp)} → #{result.script || "(none)"}"
165
282
  end
166
283
  end
@@ -172,7 +289,8 @@ module Ucode
172
289
  cp = parse_cp(codepoint)
173
290
  with_db_handling do
174
291
  result = Commands::LookupCommand.new
175
- .lookup_char(options[:version], codepoint: cp, output_root: options[:from])
292
+ .lookup_char(VersionResolver.resolve(options[:version]),
293
+ codepoint: cp, output_root: options[:from])
176
294
  puts "#{format("U+%04X", cp)} block=#{result.block_id} glyph=#{result.glyph_path}"
177
295
  end
178
296
  end
@@ -274,7 +392,7 @@ module Ucode
274
392
  desc: "Emit per-block missing-glyph galleries " \
275
393
  "(requires --browse + --universal-set-root)"
276
394
  def font(path)
277
- reference = Commands::Audit::ReferenceBuilder.build(
395
+ reference = Ucode::Audit::ReferenceFactory.build_from_cli(
278
396
  flag: options[:reference_universal_set],
279
397
  version: options[:unicode_version],
280
398
  )
@@ -312,7 +430,7 @@ module Ucode
312
430
  option :with_missing_glyph_pages, type: :boolean, default: false,
313
431
  desc: "Emit per-block missing-glyph galleries"
314
432
  def collection(path)
315
- reference = Commands::Audit::ReferenceBuilder.build(
433
+ reference = Ucode::Audit::ReferenceFactory.build_from_cli(
316
434
  flag: options[:reference_universal_set],
317
435
  version: options[:unicode_version],
318
436
  )
@@ -349,7 +467,7 @@ module Ucode
349
467
  option :with_missing_glyph_pages, type: :boolean, default: false,
350
468
  desc: "Emit per-block missing-glyph galleries"
351
469
  def library(dir)
352
- reference = Commands::Audit::ReferenceBuilder.build(
470
+ reference = Ucode::Audit::ReferenceFactory.build_from_cli(
353
471
  flag: options[:reference_universal_set],
354
472
  version: options[:unicode_version],
355
473
  )
@@ -430,7 +548,7 @@ module Ucode
430
548
  desc: "Worker pool size (default: Ucode.configuration.parallel_workers)"
431
549
  def build(version = nil)
432
550
  result = Commands::UniversalSet::BuildCommand.new.call(
433
- version,
551
+ VersionResolver.resolve(version),
434
552
  output_root: options[:to],
435
553
  source_config_path: options[:source_config],
436
554
  block_filter: options[:block],
@@ -448,7 +566,7 @@ module Ucode
448
566
  desc: "Path to a Tier 1 source config YAML"
449
567
  def pre_check(version = nil)
450
568
  report = Commands::UniversalSet::PreCheckCommand.new.call(
451
- version,
569
+ VersionResolver.resolve(version),
452
570
  source_config_path: options[:source_config],
453
571
  )
454
572
  puts JSON.pretty_generate(report.to_h)
@@ -463,7 +581,7 @@ module Ucode
463
581
  desc: "Output directory holding manifest.json"
464
582
  def report(version = nil)
465
583
  result = Commands::UniversalSet::ReportCommand.new.call(
466
- version,
584
+ VersionResolver.resolve(version),
467
585
  output_root: options[:from],
468
586
  )
469
587
  puts JSON.pretty_generate(result)
@@ -475,7 +593,7 @@ module Ucode
475
593
  def validate(output_root = "./output/universal_glyph_set")
476
594
  result = Commands::UniversalSet::ValidateCommand.new.call(
477
595
  output_root,
478
- version_intent: options[:version],
596
+ version: options[:version] && VersionResolver.resolve(options[:version]),
479
597
  )
480
598
  puts JSON.pretty_generate(result)
481
599
  exit 1 unless result[:passed]
@@ -518,7 +636,7 @@ module Ucode
518
636
  desc: "Path to universal-set manifest (or 'none') " \
519
637
  "for the per-face coverage reference"
520
638
  def release
521
- reference = Commands::Audit::ReferenceBuilder.build(
639
+ reference = Ucode::Audit::ReferenceFactory.build_from_cli(
522
640
  flag: options[:reference_universal_set],
523
641
  version: options[:unicode_version],
524
642
  )