ucode 0.3.3 → 0.5.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -6
  3. data/config/unicode17_universal_glyph_set.yml +1 -1
  4. data/lib/ucode/cli.rb +201 -32
  5. data/lib/ucode/code_chart/batch_runner.rb +184 -0
  6. data/lib/ucode/code_chart/block_index.rb +89 -0
  7. data/lib/ucode/code_chart/coverage_gap_index.rb +115 -0
  8. data/lib/ucode/code_chart/extractor.rb +65 -13
  9. data/lib/ucode/code_chart/fetcher.rb +112 -0
  10. data/lib/ucode/code_chart/gap_analyzer/block_gap.rb +35 -0
  11. data/lib/ucode/code_chart/gap_analyzer/essenfont_manifest.rb +60 -0
  12. data/lib/ucode/code_chart/gap_analyzer/manifest.rb +39 -0
  13. data/lib/ucode/code_chart/gap_analyzer.rb +83 -0
  14. data/lib/ucode/code_chart/provenance.rb +99 -58
  15. data/lib/ucode/code_chart/sidecar.rb +3 -4
  16. data/lib/ucode/code_chart/verifier/builder.rb +49 -0
  17. data/lib/ucode/code_chart/verifier/mutool_strategy.rb +80 -0
  18. data/lib/ucode/code_chart/verifier/page_render_cache.rb +63 -0
  19. data/lib/ucode/code_chart/verifier/result.rb +27 -0
  20. data/lib/ucode/code_chart/verifier/resvg_strategy.rb +71 -0
  21. data/lib/ucode/code_chart/verifier/strategy.rb +73 -0
  22. data/lib/ucode/code_chart/verifier.rb +152 -0
  23. data/lib/ucode/code_chart/writer.rb +21 -14
  24. data/lib/ucode/code_chart.rb +6 -0
  25. data/lib/ucode/error.rb +16 -1
  26. data/lib/ucode/fetch/http.rb +23 -3
  27. data/lib/ucode/glyphs/embedded_fonts/catalog.rb +66 -3
  28. data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/correlator_strategy.rb +46 -0
  29. data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/strategy.rb +54 -0
  30. data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/tounicode_strategy.rb +41 -0
  31. data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/trace_strategy.rb +64 -0
  32. data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper.rb +150 -90
  33. data/lib/ucode/glyphs/embedded_fonts/mutool/draw.rb +26 -0
  34. data/lib/ucode/glyphs/embedded_fonts/mutool/info.rb +23 -0
  35. data/lib/ucode/glyphs/embedded_fonts/mutool/show.rb +36 -0
  36. data/lib/ucode/glyphs/embedded_fonts/mutool/trace.rb +26 -0
  37. data/lib/ucode/glyphs/embedded_fonts/mutool.rb +30 -0
  38. data/lib/ucode/glyphs/embedded_fonts/page_trace_cache.rb +112 -0
  39. data/lib/ucode/glyphs/embedded_fonts/pdf_indexer.rb +50 -56
  40. data/lib/ucode/glyphs/embedded_fonts/{pdf_location.rb → pdf_source.rb} +1 -1
  41. data/lib/ucode/glyphs/embedded_fonts/renderer.rb +19 -2
  42. data/lib/ucode/glyphs/embedded_fonts/trace_runner.rb +12 -28
  43. data/lib/ucode/glyphs/embedded_fonts.rb +3 -1
  44. data/lib/ucode/glyphs/real_fonts/coverage_auditor.rb +0 -4
  45. data/lib/ucode/glyphs/real_fonts/font_coverage_report.rb +0 -2
  46. data/lib/ucode/glyphs/real_fonts/writer.rb +0 -2
  47. data/lib/ucode/glyphs/source.rb +11 -1
  48. data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +11 -6
  49. data/lib/ucode/version.rb +1 -1
  50. data/lib/ucode.rb +2 -0
  51. metadata +28 -74
  52. data/TODO.extract-code-chart/01-pdf-fetch-validation.md +0 -80
  53. data/TODO.extract-code-chart/02-block-name-resolver.md +0 -68
  54. data/TODO.extract-code-chart/03-codechart-namespace.md +0 -82
  55. data/TODO.extract-code-chart/04-codechart-extractor.md +0 -154
  56. data/TODO.extract-code-chart/05-provenance-and-sidecar.md +0 -147
  57. data/TODO.extract-code-chart/06-codechart-writer.md +0 -134
  58. data/TODO.extract-code-chart/07-codechart-cli.md +0 -135
  59. data/TODO.extract-code-chart/08-specs.md +0 -87
  60. data/TODO.full/00-README.md +0 -116
  61. data/TODO.full/01-panglyph-vision.md +0 -112
  62. data/TODO.full/02-panglyph-repo-bootstrap.md +0 -184
  63. data/TODO.full/03-panglyph-font-builder.md +0 -201
  64. data/TODO.full/04-panglyph-publish-pipeline.md +0 -126
  65. data/TODO.full/05-ucode-0-1-1-release.md +0 -139
  66. data/TODO.full/06-fontisan-remove-audit.md +0 -142
  67. data/TODO.full/07-fontisan-remove-ucd.md +0 -125
  68. data/TODO.full/08-archive-private-bin-build.md +0 -143
  69. data/TODO.full/09-archive-public-structure.md +0 -164
  70. data/TODO.full/10-fontist-org-woff-glyphs.md +0 -131
  71. data/TODO.full/11-fontist-org-audit-coverage.md +0 -140
  72. data/TODO.full/12-implementation-order.md +0 -216
  73. data/TODO.full/13-fontisan-font-writer-api.md +0 -189
  74. data/TODO.full/14-fontisan-table-writers.md +0 -66
  75. data/TODO.full/15-panglyph-builder-real.md +0 -82
  76. data/TODO.full/16-archive-public-sync-workflows.md +0 -167
  77. data/TODO.full/17-fontist-org-font-picker.md +0 -73
  78. data/TODO.full/18-comprehensive-spec-coverage.md +0 -64
  79. data/TODO.full/19-ucode-0-1-2-patch.md +0 -32
  80. data/TODO.full/20-fontisan-0-2-23-release.md +0 -52
  81. data/TODO.new/00-README.md +0 -96
  82. data/TODO.new/01-pillar-terminology-alignment.md +0 -69
  83. data/TODO.new/02-audit-schema-design.md +0 -255
  84. data/TODO.new/03-directory-output-spec.md +0 -203
  85. data/TODO.new/04-fontist-org-contract.md +0 -173
  86. data/TODO.new/05-baseline-unicode17-coverage-audit.md +0 -144
  87. data/TODO.new/06-audit-namespace-skeleton.md +0 -105
  88. data/TODO.new/07-audit-models-port.md +0 -132
  89. data/TODO.new/08-extractors-cheap-port.md +0 -113
  90. data/TODO.new/09-extractors-expensive-port.md +0 -99
  91. data/TODO.new/10-aggregations-ucd-rewrite.md +0 -168
  92. data/TODO.new/11-differ-and-library-auditor-port.md +0 -102
  93. data/TODO.new/12-formatters-port.md +0 -115
  94. data/TODO.new/13-directory-emitter.md +0 -147
  95. data/TODO.new/14-html-face-browser.md +0 -144
  96. data/TODO.new/15-html-library-browser.md +0 -102
  97. data/TODO.new/16-cli-audit-subcommands.md +0 -142
  98. data/TODO.new/17-fontisan-cleanup-audit.md +0 -147
  99. data/TODO.new/18-fontisan-cleanup-ucd.md +0 -156
  100. data/TODO.new/19-fontisan-docs-update.md +0 -155
  101. data/TODO.new/20-canonical-resolver-4-tier.md +0 -182
  102. data/TODO.new/21-canonical-unicode17-build.md +0 -148
  103. data/TODO.new/22-implementation-order.md +0 -176
  104. data/TODO.new/23-universal-glyph-set-source-map.md +0 -312
  105. data/TODO.new/24-universal-glyph-set-build.md +0 -189
  106. data/TODO.new/25-font-audit-against-universal-set.md +0 -195
  107. data/TODO.new/26-missing-glyph-reporter.md +0 -189
  108. data/TODO.new/27-fontist-org-consumer-integration.md +0 -200
  109. data/TODO.new/28-implementation-order-update.md +0 -187
  110. data/TODO.new/29-universal-set-curation-uc17.md +0 -312
  111. data/TODO.new/30-tier1-font-acquisition.md +0 -241
  112. data/TODO.new/31-universal-set-production-build.md +0 -205
  113. data/TODO.new/32-uc17-coverage-matrix.md +0 -165
  114. data/TODO.new/33-specialist-font-acquisition-refresh.md +0 -138
  115. data/TODO.new/34-pillar2-content-stream-correlator.md +0 -147
  116. data/TODO.new/35-universal-set-production-run.md +0 -160
  117. data/TODO.new/36-per-font-coverage-audit.md +0 -145
  118. data/TODO.new/37-coverage-highlight-reporter.md +0 -125
  119. data/TODO.new/38-fontist-org-glyph-consumer.md +0 -141
  120. data/TODO.new/39-implementation-order-update-32-38.md +0 -258
  121. data/TODO.new/40-archive-private-uses-ucode-audit.md +0 -124
  122. data/TODO.new/41-ucode-unicode-archive-bridge.md +0 -160
@@ -1,82 +0,0 @@
1
- # TODO 03 — CodeChart namespace
2
-
3
- ## Status
4
-
5
- Pending. Depends on TODO 02 (block name resolver) so the Extractor
6
- can consume it; depends on TODO 01 (error class) so the namespace
7
- can declare typed errors.
8
-
9
- ## Goal
10
-
11
- Establish the `Ucode::CodeChart` namespace as the home for the
12
- Code Charts per-codepoint extraction feature. The REQ calls this
13
- namespace `Ucode::CodeChart::*`; we follow the REQ.
14
-
15
- This is the autoload-hub file plus the autoload declaration in
16
- `lib/ucode.rb`.
17
-
18
- ## Files
19
-
20
- - `lib/ucode/code_chart.rb` — new autoload hub (defines
21
- `Ucode::CodeChart` and declares child autoloads).
22
- - `lib/ucode.rb` — add `autoload :CodeChart, "ucode/code_chart"` in
23
- the namespace-hubs block.
24
-
25
- ## Design
26
-
27
- ### Autoload hub shape
28
-
29
- ```ruby
30
- # lib/ucode/code_chart.rb
31
- module Ucode
32
- module CodeChart
33
- autoload :Extractor, "ucode/code_chart/extractor"
34
- autoload :Provenance, "ucode/code_chart/provenance"
35
- autoload :Sidecar, "ucode/code_chart/sidecar"
36
- autoload :Writer, "ucode/code_chart/writer"
37
- end
38
- end
39
- ```
40
-
41
- Per the global rule (`~/.claude/CLAUDE.md`): declare autoloads in the
42
- immediate parent namespace's file. `Ucode::CodeChart` is the immediate
43
- parent of `Extractor`, `Provenance`, `Sidecar`, `Writer`; this file
44
- is the immediate parent's file.
45
-
46
- `Ucode` is the immediate parent of `CodeChart`; the autoload
47
- declaration `autoload :CodeChart, "ucode/code_chart"` goes in
48
- `lib/ucode.rb`.
49
-
50
- ### Why a new namespace (not under `Ucode::Glyphs`)
51
-
52
- `Ucode::Glyphs::*` is the existing 4-tier sourcing pipeline
53
- (`EmbeddedFonts`, `RealFonts`, `LastResort`, `Writer`). The REQ's
54
- `CodeChart::*` is a feature-facing namespace that orchestrates the
55
- glyphs pipeline for one specific use case (extracting from a per-block
56
- PDF for the essenfont donor pipeline). Keeping the feature-facing
57
- namespace separate from the implementation namespace:
58
-
59
- - Lets callers say `Ucode::CodeChart.extract(block: "Sidetic")`
60
- without first knowing about `Glyphs::EmbeddedFonts`.
61
- - Makes it easy to swap the implementation later (different
62
- resolution strategy, alternative PDF parser) without breaking the
63
- public API.
64
- - Keeps `Glyphs::` focused on tier mechanics, free of feature
65
- ergonomics.
66
-
67
- The REQ's namespace name is what we use.
68
-
69
- ## Acceptance
70
-
71
- - `lib/ucode/code_chart.rb` exists with the autoload declarations.
72
- - `lib/ucode.rb` has the new `autoload :CodeChart, "ucode/code_chart"`
73
- in the namespace-hubs block.
74
- - `Ucode::CodeChart` resolves to a module without loading any of its
75
- children.
76
-
77
- ## Out of scope
78
-
79
- - `Ucode::CodeChart::Command` (Thin wrapper). The CLI lives in
80
- `lib/ucode/cli.rb` per the existing pattern; no separate
81
- `Commands::CodeChartCommand` is introduced (single source of
82
- truth for CLI dispatch).
@@ -1,154 +0,0 @@
1
- # TODO 04 — CodeChart::Extractor
2
-
3
- ## Status
4
-
5
- Pending. Depends on TODO 01 (error class), TODO 02 (block name
6
- resolver), TODO 03 (namespace).
7
-
8
- ## Goal
9
-
10
- `Ucode::CodeChart::Extractor` is the single entry point for
11
- "extract every assigned codepoint in block X as a standalone SVG."
12
-
13
- It orchestrates the existing 4-tier resolver (one source of truth for
14
- "how do I get the SVG for a given codepoint") and returns a list of
15
- extraction results — one per codepoint — that the downstream Writer
16
- serializes to disk.
17
-
18
- This is *not* a new extraction pipeline; it is the existing
19
- `Ucode::Glyphs::Resolver` with per-block inputs pre-configured.
20
-
21
- ## Files
22
-
23
- - `lib/ucode/code_chart/extractor.rb` — `Ucode::CodeChart::Extractor`
24
- class.
25
- - `spec/ucode/code_chart/extractor_spec.rb` — model/value-object
26
- specs (constructor invariants, Resolver wiring) plus an integration
27
- test against the fixture `spec/fixtures/pdfs/basic_latin.pdf`.
28
-
29
- ## Design
30
-
31
- ### Class shape
32
-
33
- ```ruby
34
- class Ucode::CodeChart::Extractor
35
- Result = Struct.new(:codepoint, :svg, :tier, :provenance, :base_font,
36
- :gid, keyword_init: true)
37
-
38
- def initialize(block:, blocks_txt:, pdf_fetcher: nil,
39
- font_cache_dir: nil, last_resort_root: nil)
40
- @block = block # Models::Block
41
- @blocks_txt = blocks_txt # Pathname
42
- @pdf_fetcher = pdf_fetcher # optional injectable
43
- @font_cache_dir = font_cache_dir # default: data/pdf-fonts/
44
- @last_resort_root = last_resort_root
45
- end
46
-
47
- # Walks every assigned codepoint in @block and returns one Result
48
- # per codepoint. Codepoints with no glyph from any tier are
49
- # silently skipped (no Result yielded) — the REQ's "skip
50
- # unassigned codepoints with a warning" is satisfied by the
51
- # Resolver returning nil for them.
52
- #
53
- # @return [Array<Result>]
54
- def extract
55
- end
56
- end
57
- ```
58
-
59
- ### Wiring (single source of truth)
60
-
61
- The Extractor does NOT implement tier selection. It builds a
62
- `Ucode::Glyphs::Resolver` and calls `resolver.resolve(codepoint)` for
63
- each cp. The Resolver's tier order is preserved (Pillar 1 → 2 → 3
64
- for this feature; no Tier 1 because we're starting from the Code
65
- Charts PDF, not a real-font source).
66
-
67
- ```ruby
68
- def build_resolver
69
- pdf = fetch_pdf!
70
- embedded_source = Glyphs::EmbeddedFonts::Source.new(
71
- pdf: pdf, cache_dir: @font_cache_dir,
72
- )
73
- catalog = Glyphs::EmbeddedFonts::Catalog.new(embedded_source)
74
- pillar1 = Glyphs::Sources::Pillar1EmbeddedTounicode.new(
75
- renderer: Glyphs::EmbeddedFonts::Renderer.new(catalog),
76
- )
77
- pillar3 = Glyphs::Sources::Pillar3LastResort.new(
78
- renderer: Glyphs::LastResort::Renderer.new(
79
- Glyphs::LastResort::Source.new(root: @last_resort_root),
80
- ),
81
- )
82
- Glyphs::Resolver.new(
83
- sources: [pillar1, pillar3],
84
- order: %i[pillar1 pillar3],
85
- )
86
- end
87
- ```
88
-
89
- The tier ordering is documented inline: we skip Pillar 2 because for
90
- the CodeChart use case the catalog's ToUnicode is the dominant path
91
- and Pillar 2 (positional correlation) is reserved for fonts where
92
- Pillar 1 fails. If a future use case needs Pillar 2, add it without
93
- changing this constructor — that's the OCP payoff of consuming the
94
- Resolver.
95
-
96
- ### Why no Tier 1
97
-
98
- Tier 1 (real-font cmap) needs a configured `SourceConfig` mapping
99
- block → font. The CodeChart use case is for blocks where no real
100
- font exists (Sidetic, Egyptian Ext-B). Tier 1 wouldn't contribute
101
- anything. The Extractor accepts a Tier 1 source in the future by
102
- having callers pass a fully-built Resolver instead of constructing
103
- one internally.
104
-
105
- ### Why PDF fetch is delegated to `PdfFetcher`
106
-
107
- `Ucode::Glyphs::PdfFetcher` is the existing seam for resolving a
108
- block to its PDF on disk (per-block cache + monolith fallback). It
109
- already handles `force:` and the cache directory. The Extractor
110
- constructs a `PdfFetcher` per call (cheap — it's just a path
111
- resolver) and reuses it across codepoints.
112
-
113
- ### Per-codepoint loop
114
-
115
- ```ruby
116
- def extract
117
- resolver = build_resolver
118
- @block.codepoint_ids.flat_map do |cp_id|
119
- cp = Integer(cp_id.delete_prefix("U+"), 16)
120
- resolver_result = resolver.resolve(cp)
121
- next nil unless resolver_result&.svg
122
-
123
- Result.new(
124
- codepoint: cp,
125
- svg: resolver_result.svg,
126
- tier: resolver_result.tier,
127
- provenance: resolver_result.provenance,
128
- )
129
- end.compact
130
- end
131
- ```
132
-
133
- The Resolver returns a `Sources::Result` (tier + codepoint + svg +
134
- provenance). We adapt that to the Extractor's `Result` (with
135
- codepoint + svg + tier + provenance), stripping the resolver-specific
136
- shape at the boundary.
137
-
138
- ## Acceptance
139
-
140
- - `Extractor.new(block: ..., blocks_txt: ...)` constructs without
141
- raising when the block and PDF are present.
142
- - `#extract` returns one Result per codepoint that any tier
143
- produced a glyph for.
144
- - `#extract` skips codepoints no tier could produce (returns no
145
- Result, not a Result-with-nil).
146
- - Integration test: against the fixture PDF, at least one codepoint's
147
- Result has tier `:pillar1` and provenance `"pillar-1:embedded-tounicode"`.
148
-
149
- ## Out of scope
150
-
151
- - Writing files — that's `CodeChart::Writer` (TODO 06).
152
- - Provenance JSON — that's `CodeChart::Sidecar` (TODO 05).
153
- - Tier 1 (real-font) source injection — not needed for the REQ's
154
- blocks. Future extension point if a real-font fallback is desired.
@@ -1,147 +0,0 @@
1
- # TODO 05 — CodeChart::Provenance and CodeChart::Sidecar
2
-
3
- ## Status
4
-
5
- Pending. Depends on TODO 03 (namespace). Depends on TODO 04 (Extractor)
6
- because Sidecar consumes the Extractor's Result.
7
-
8
- ## Goal
9
-
10
- `Provenance` is the value object carrying the metadata the REQ (R5)
11
- requires for each extracted SVG's sidecar JSON. `Sidecar` is the
12
- writer that serializes one Provenance to disk next to its SVG.
13
-
14
- ## Files
15
-
16
- - `lib/ucode/code_chart/provenance.rb` — `Ucode::CodeChart::Provenance`
17
- Struct with all REQ R5 fields plus the construction helper.
18
- - `lib/ucode/code_chart/sidecar.rb` — `Ucode::CodeChart::Sidecar`
19
- class (writes a sidecar JSON next to an SVG, idempotent via the
20
- existing `Ucode::Repo::AtomicWrites`).
21
- - `spec/ucode/code_chart/provenance_spec.rb`
22
- - `spec/ucode/code_chart/sidecar_spec.rb`
23
-
24
- ## Design
25
-
26
- ### Provenance value object
27
-
28
- The REQ (R5) lists these fields:
29
-
30
- ```json
31
- {
32
- "codepoint": "U+10920",
33
- "block": "Sidetic",
34
- "source_pdf_url": "https://www.unicode.org/charts/PDF/U-10920.pdf",
35
- "source_pdf_sha256": "...",
36
- "ucd_version": "17.0.0",
37
- "extracted_at": "2026-06-30T12:00:00Z",
38
- "extractor_version": "0.1.0"
39
- }
40
- ```
41
-
42
- `Struct` is the right tool — single source of truth for the schema,
43
- keyword-init for clarity, immutable-by-convention. Mirror the
44
- existing `Ucode::Repo::BuildReportAccumulator` pattern.
45
-
46
- ```ruby
47
- Provenance = Struct.new(
48
- :codepoint, # String "U+10920"
49
- :block, # String "Sidetic"
50
- :source_pdf_url, # String
51
- :source_pdf_sha256, # String (hex digest)
52
- :ucd_version, # String "17.0.0"
53
- :extracted_at, # String ISO8601 UTC
54
- :extractor_version, # String "0.2.0"
55
- keyword_init: true,
56
- )
57
- ```
58
-
59
- `extractor_version` reads from `Ucode::VERSION` so it stays in sync
60
- with the gem — single source of truth.
61
-
62
- `extracted_at` is set at construction (not at file write) so the
63
- field describes the extraction event, not the serialization event.
64
-
65
- ### Provenance → Hash serialization
66
-
67
- `Provenance#to_h` returns the hash form. NO hand-rolled `to_json` /
68
- `from_json` per the global rule — `Provenance` is a value object, but
69
- its schema is simple enough that `to_h` + `JSON.pretty_generate` is
70
- the framework-driven approach (lutaml-model is overkill for a flat
71
- struct).
72
-
73
- Wait — re-reading the global rule: "ALL (de)serialization goes through
74
- the framework. In Coradoc and any project using `lutaml-model`." So
75
- the rule is for projects using lutaml-model, and ucode uses lutaml-model
76
- for UCD models. This Provenance struct is not a UCD model — it's a
77
- feature-local value object. JSON via `JSON.pretty_generate(provenance.to_h)`
78
- is acceptable and avoids ceremony.
79
-
80
- `to_h` produces a hash with the Struct's keyword keys. No
81
- indirection, no lutaml-model mapping for what is effectively a
82
- record.
83
-
84
- ### Sidecar writer
85
-
86
- ```ruby
87
- class Sidecar
88
- include Ucode::Repo::AtomicWrites
89
-
90
- def initialize(output_root:)
91
- @output_root = Pathname.new(output_root)
92
- end
93
-
94
- # Writes <output_root>/<cp_id>.json next to the corresponding SVG.
95
- # Idempotent: re-writing the same content is a no-op (byte-stable).
96
- #
97
- # @param provenance [Ucode::CodeChart::Provenance]
98
- # @return [Pathname] the written path
99
- def write(provenance)
100
- path = path_for(provenance)
101
- payload = JSON.pretty_generate(provenance.to_h)
102
- write_atomic(path, payload + "\n")
103
- path
104
- end
105
-
106
- private
107
-
108
- def path_for(provenance)
109
- @output_root.join("#{provenance.codepoint}.json")
110
- end
111
- end
112
- ```
113
-
114
- `Repo::AtomicWrites#write_atomic` is the project's single source of
115
- truth for idempotent file writes — bytes-identical re-writes are
116
- no-ops (the temp-file rename is skipped when content matches).
117
- Reuse, don't reimplement.
118
-
119
- ### Why a separate Sidecar class
120
-
121
- A `Provenance.to_disk(path)` method would couple the value object
122
- to I/O. Keeping the writer separate lets:
123
- - Tests assert `Provenance#to_h` without touching disk.
124
- - The Writer (TODO 06) compose `Extractor` + `Sidecar` with explicit
125
- dependency injection (seam for testing).
126
- - Future formats (e.g. a different sidecar schema) replace Sidecar
127
- without touching Provenance.
128
-
129
- This is MECE: Provenance is data; Sidecar is I/O; Writer is
130
- orchestration.
131
-
132
- ## Acceptance
133
-
134
- - `Provenance.new(codepoint: "U+10920", block: "Sidetic", ...)`
135
- constructs without raising.
136
- - `Provenance#to_h` returns a Hash with exactly the REQ's fields.
137
- - `Sidecar#write(provenance)` writes `<codepoint>.json` next to
138
- where the SVG lives; the JSON content matches `Provenance#to_h`.
139
- - Re-writing the same Provenance is a no-op (file unchanged).
140
- - Specs cover all five REQ fields plus the idempotency guarantee.
141
-
142
- ## Out of scope
143
-
144
- - License attribution text — the REQ mentions `LICENSE-SOURCES.md`
145
- obligations, but that's a fontist-side concern (downstream
146
- essenfont build). Ucode emits provenance; the consumer stitches
147
- attribution.
@@ -1,134 +0,0 @@
1
- # TODO 06 — CodeChart::Writer
2
-
3
- ## Status
4
-
5
- Pending. Depends on TODO 04 (Extractor) and TODO 05 (Provenance +
6
- Sidecar).
7
-
8
- ## Goal
9
-
10
- `Ucode::CodeChart::Writer` is the single entry point for
11
- "extract every codepoint in block X and write SVG + sidecar JSON
12
- files under output_dir." It's the orchestration layer the CLI
13
- calls and the only thing that touches disk.
14
-
15
- ## Files
16
-
17
- - `lib/ucode/code_chart/writer.rb` — `Ucode::CodeChart::Writer` class.
18
- - `spec/ucode/code_chart/writer_spec.rb`
19
-
20
- ## Design
21
-
22
- ### Class shape
23
-
24
- ```ruby
25
- class Ucode::CodeChart::Writer
26
- Summary = Struct.new(:block, :codepoints_total, :svgs_written,
27
- :sidecars_written, :pdf_sha256, keyword_init: true)
28
-
29
- def initialize(output_root:, pdf_path:, cache_dir: nil,
30
- last_resort_root: nil, blocks_txt: nil)
31
- @output_root = Pathname.new(output_root)
32
- @pdf_path = Pathname.new(pdf_path)
33
- @cache_dir = cache_dir
34
- @last_resort_root = last_resort_root
35
- @blocks_txt = blocks_txt || Ucode::Cache.ucd_dir(Ucode::VersionResolver.resolve(nil)).join("Blocks.txt")
36
- @sidecar = Sidecar.new(output_root: @output_root)
37
- end
38
-
39
- # Extracts every codepoint in @block (a Models::Block) and writes
40
- # SVG + sidecar JSON under @output_root. Returns a Summary.
41
- #
42
- # @param block [Ucode::Models::Block]
43
- # @return [Summary]
44
- def write(block)
45
- end
46
- end
47
- ```
48
-
49
- ### Per-codepoint flow (single source of truth)
50
-
51
- ```ruby
52
- def write(block)
53
- output_root_for(block).mkpath
54
- pdf_sha = sha256(@pdf_path)
55
-
56
- extractor = Extractor.new(
57
- block: block,
58
- blocks_txt: @blocks_txt,
59
- pdf_path: @pdf_path,
60
- cache_dir: @cache_dir,
61
- last_resort_root: @last_resort_root,
62
- )
63
- results = extractor.extract
64
-
65
- svgs = 0
66
- sidecars = 0
67
- results.each do |result|
68
- svg_path = output_root_for(block).join("#{cp_id(result.codepoint)}.svg")
69
- File.write(svg_path, result.svg) unless svg_path.exist? && File.read(svg_path) == result.svg
70
- svgs += 1 if svg_path.exist?
71
-
72
- provenance = build_provenance(block, result.codepoint, pdf_sha)
73
- @sidecar.write(provenance)
74
- sidecars += 1
75
- end
76
-
77
- Summary.new(
78
- block: block.id,
79
- codepoints_total: results.size,
80
- svgs_written: svgs,
81
- sidecars_written: sidecars,
82
- pdf_sha256: pdf_sha,
83
- )
84
- end
85
- ```
86
-
87
- ### Why not use `Repo::AtomicWrites` for the SVGs
88
-
89
- The Sidecar uses `Repo::AtomicWrites` because JSON has a stable
90
- canonical form. SVG output from `EmbeddedFonts::Svg#to_s` is also
91
- byte-stable, but the writer pattern is simpler with `File.write` —
92
- the byte-equality check above guarantees idempotency at the I/O
93
- layer. Both paths reach the same outcome.
94
-
95
- If future output formats gain non-stable serialization (timestamps,
96
- random IDs), the SVG path will need `Repo::AtomicWrites` too. Until
97
- then, simpler is better.
98
-
99
- ### Why compute `pdf_sha256` once
100
-
101
- Every Provenance in this block carries the same `source_pdf_sha256`.
102
- Computing it once avoids 32+ disk reads of the PDF per block
103
- extraction. The Writer is the single place that knows "one block,
104
- one PDF, one hash" — pushing the calculation into Sidecar or
105
- Provenance would require either (a) repeated computation per
106
- Provenance or (b) a parameter-passing thread through Extractor. Both
107
- violate locality.
108
-
109
- ### Output layout
110
-
111
- `<output_root>/<block_id>/<U+XXXX>.svg` and `<U+XXXX>.json`.
112
-
113
- One folder per block keeps the `Writer`'s output self-contained and
114
- discoverable — a downstream consumer (fontisan) can iterate a block's
115
- folder without scanning the whole tree. This mirrors the existing
116
- `Ucode::Repo::Writers::BlocksWriter` output convention (one folder
117
- per block, index.json inside).
118
-
119
- ## Acceptance
120
-
121
- - `Writer#write(block)` creates `<output_root>/<block_id>/` and
122
- fills it with `<U+XXXX>.svg` + `<U+XXXX>.json` for every
123
- extracted codepoint.
124
- - Re-running `Writer#write(block)` with no changes produces
125
- byte-identical files (no rewrites).
126
- - `Summary#svgs_written` equals the number of extracted codepoints.
127
- - Specs cover the full lifecycle including idempotency.
128
-
129
- ## Out of scope
130
-
131
- - Per-block write isolation — concurrent `Writer#write` calls for
132
- different blocks are safe (different folders), but the Writer is
133
- not thread-safe within a single block. That's a parallel-extraction
134
- concern, not a per-block concern.
@@ -1,135 +0,0 @@
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).