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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/TODO.full/00-README.md +116 -0
- data/TODO.full/01-panglyph-vision.md +112 -0
- data/TODO.full/02-panglyph-repo-bootstrap.md +184 -0
- data/TODO.full/03-panglyph-font-builder.md +201 -0
- data/TODO.full/04-panglyph-publish-pipeline.md +126 -0
- data/TODO.full/05-ucode-0-1-1-release.md +139 -0
- data/TODO.full/06-fontisan-remove-audit.md +142 -0
- data/TODO.full/07-fontisan-remove-ucd.md +125 -0
- data/TODO.full/08-archive-private-bin-build.md +143 -0
- data/TODO.full/09-archive-public-structure.md +164 -0
- data/TODO.full/10-fontist-org-woff-glyphs.md +131 -0
- data/TODO.full/11-fontist-org-audit-coverage.md +140 -0
- data/TODO.full/12-implementation-order.md +216 -0
- data/TODO.full/13-fontisan-font-writer-api.md +189 -0
- data/TODO.full/14-fontisan-table-writers.md +66 -0
- data/TODO.full/15-panglyph-builder-real.md +82 -0
- data/TODO.full/16-archive-public-sync-workflows.md +167 -0
- data/TODO.full/17-fontist-org-font-picker.md +73 -0
- data/TODO.full/18-comprehensive-spec-coverage.md +64 -0
- data/TODO.full/19-ucode-0-1-2-patch.md +32 -0
- data/TODO.full/20-fontisan-0-2-23-release.md +52 -0
- data/TODO.new/00-README.md +30 -0
- data/TODO.new/23-universal-glyph-set-source-map.md +312 -0
- data/TODO.new/24-universal-glyph-set-build.md +189 -0
- data/TODO.new/25-font-audit-against-universal-set.md +195 -0
- data/TODO.new/26-missing-glyph-reporter.md +189 -0
- data/TODO.new/27-fontist-org-consumer-integration.md +200 -0
- data/TODO.new/28-implementation-order-update.md +187 -0
- data/TODO.new/29-universal-set-curation-uc17.md +312 -0
- data/TODO.new/30-tier1-font-acquisition.md +241 -0
- data/TODO.new/31-universal-set-production-build.md +205 -0
- data/TODO.new/32-uc17-coverage-matrix.md +165 -0
- data/TODO.new/33-specialist-font-acquisition-refresh.md +138 -0
- data/TODO.new/34-pillar2-content-stream-correlator.md +147 -0
- data/TODO.new/35-universal-set-production-run.md +160 -0
- data/TODO.new/36-per-font-coverage-audit.md +145 -0
- data/TODO.new/37-coverage-highlight-reporter.md +125 -0
- data/TODO.new/38-fontist-org-glyph-consumer.md +141 -0
- data/TODO.new/39-implementation-order-update-32-38.md +258 -0
- data/TODO.new/40-archive-private-uses-ucode-audit.md +124 -0
- data/TODO.new/41-ucode-unicode-archive-bridge.md +160 -0
- data/config/specialist_fonts.yml +102 -0
- data/config/unicode17_tier1_fonts.yml +42 -0
- data/config/unicode17_universal_glyph_set.yml +293 -0
- data/lib/ucode/audit/block_aggregator.rb +57 -29
- data/lib/ucode/audit/browser/face_page.rb +128 -0
- data/lib/ucode/audit/browser/glyph_panel.rb +124 -0
- data/lib/ucode/audit/browser/library_page.rb +74 -0
- data/lib/ucode/audit/browser/missing_glyph_page.rb +87 -0
- data/lib/ucode/audit/browser/template.rb +47 -0
- data/lib/ucode/audit/browser/templates/face.css +200 -0
- data/lib/ucode/audit/browser/templates/face.html.erb +41 -0
- data/lib/ucode/audit/browser/templates/face.js +298 -0
- data/lib/ucode/audit/browser/templates/library.css +119 -0
- data/lib/ucode/audit/browser/templates/library.html.erb +42 -0
- data/lib/ucode/audit/browser/templates/library.js +99 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.css +119 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.html.erb +58 -0
- data/lib/ucode/audit/browser/templates/missing_glyph_page.js +2 -0
- data/lib/ucode/audit/browser.rb +32 -0
- data/lib/ucode/audit/context.rb +27 -1
- data/lib/ucode/audit/coverage_reference.rb +103 -0
- data/lib/ucode/audit/differ.rb +121 -0
- data/lib/ucode/audit/emitter/block_emitter.rb +52 -0
- data/lib/ucode/audit/emitter/codepoint_emitter.rb +87 -0
- data/lib/ucode/audit/emitter/collection_emitter.rb +80 -0
- data/lib/ucode/audit/emitter/face_directory.rb +212 -0
- data/lib/ucode/audit/emitter/glyph_emitter.rb +48 -0
- data/lib/ucode/audit/emitter/index_emitter.rb +149 -0
- data/lib/ucode/audit/emitter/library_emitter.rb +96 -0
- data/lib/ucode/audit/emitter/paths.rb +312 -0
- data/lib/ucode/audit/emitter/plane_emitter.rb +29 -0
- data/lib/ucode/audit/emitter/script_emitter.rb +29 -0
- data/lib/ucode/audit/emitter.rb +29 -0
- data/lib/ucode/audit/extractors/aggregations.rb +31 -2
- data/lib/ucode/audit/face_auditor.rb +86 -0
- data/lib/ucode/audit/formatters/audit_diff_text.rb +112 -0
- data/lib/ucode/audit/formatters/audit_text.rb +411 -0
- data/lib/ucode/audit/formatters/color.rb +48 -0
- data/lib/ucode/audit/formatters/library_summary_text.rb +98 -0
- data/lib/ucode/audit/formatters/text_formatter.rb +83 -0
- data/lib/ucode/audit/formatters.rb +23 -0
- data/lib/ucode/audit/library_aggregator.rb +86 -0
- data/lib/ucode/audit/library_auditor.rb +105 -0
- data/lib/ucode/audit/reference_factory.rb +66 -0
- data/lib/ucode/audit/release/emitter.rb +152 -0
- data/lib/ucode/audit/release/face_card.rb +93 -0
- data/lib/ucode/audit/release/formula_audits.rb +50 -0
- data/lib/ucode/audit/release/library_index_builder.rb +78 -0
- data/lib/ucode/audit/release/manifest_builder.rb +127 -0
- data/lib/ucode/audit/release.rb +42 -0
- data/lib/ucode/audit/ucd_only_reference.rb +81 -0
- data/lib/ucode/audit/universal_set_reference.rb +136 -0
- data/lib/ucode/audit.rb +32 -0
- data/lib/ucode/cli.rb +366 -41
- data/lib/ucode/commands/audit/browser_command.rb +82 -0
- data/lib/ucode/commands/audit/collection_command.rb +103 -0
- data/lib/ucode/commands/audit/compare_command.rb +188 -0
- data/lib/ucode/commands/audit/font_command.rb +140 -0
- data/lib/ucode/commands/audit/library_command.rb +87 -0
- data/lib/ucode/commands/audit.rb +19 -0
- data/lib/ucode/commands/block_feed.rb +73 -0
- data/lib/ucode/commands/build.rb +4 -0
- data/lib/ucode/commands/canonical_build.rb +137 -0
- data/lib/ucode/commands/fetch.rb +49 -15
- data/lib/ucode/commands/glyphs.rb +25 -67
- data/lib/ucode/commands/lookup.rb +11 -11
- data/lib/ucode/commands/parse.rb +7 -5
- data/lib/ucode/commands/release.rb +114 -0
- data/lib/ucode/commands/universal_set.rb +207 -0
- data/lib/ucode/commands.rb +5 -0
- data/lib/ucode/coordinator/indices.rb +49 -2
- data/lib/ucode/coordinator.rb +138 -5
- data/lib/ucode/error.rb +30 -2
- data/lib/ucode/fetch/font_fetcher/result.rb +39 -0
- data/lib/ucode/fetch/font_fetcher.rb +16 -0
- data/lib/ucode/fetch/specialist_font_fetcher.rb +280 -0
- data/lib/ucode/fetch.rb +7 -3
- data/lib/ucode/glyphs/pipeline.rb +106 -0
- data/lib/ucode/glyphs/real_fonts/cmap_cache.rb +74 -0
- data/lib/ucode/glyphs/real_fonts.rb +1 -0
- data/lib/ucode/glyphs/resolver.rb +62 -0
- data/lib/ucode/glyphs/source.rb +48 -0
- data/lib/ucode/glyphs/source_builder.rb +61 -0
- data/lib/ucode/glyphs/source_config/coverage_assertion.rb +79 -0
- data/lib/ucode/glyphs/source_config/gap_report.rb +54 -0
- data/lib/ucode/glyphs/source_config.rb +104 -0
- data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +63 -0
- data/lib/ucode/glyphs/sources/pillar3_last_resort.rb +51 -0
- data/lib/ucode/glyphs/sources/tier1_real_font.rb +104 -0
- data/lib/ucode/glyphs/sources.rb +20 -0
- data/lib/ucode/glyphs/universal_set/builder.rb +161 -0
- data/lib/ucode/glyphs/universal_set/coverage_report.rb +139 -0
- data/lib/ucode/glyphs/universal_set/idempotency.rb +86 -0
- data/lib/ucode/glyphs/universal_set/manifest_accumulator.rb +195 -0
- data/lib/ucode/glyphs/universal_set/manifest_writer.rb +61 -0
- data/lib/ucode/glyphs/universal_set/pre_build_check.rb +197 -0
- data/lib/ucode/glyphs/universal_set/validator.rb +204 -0
- data/lib/ucode/glyphs/universal_set.rb +45 -0
- data/lib/ucode/glyphs.rb +7 -0
- data/lib/ucode/models/audit/baseline.rb +6 -0
- data/lib/ucode/models/audit/block_summary.rb +7 -0
- data/lib/ucode/models/audit/codepoint_provenance.rb +39 -0
- data/lib/ucode/models/audit/release_face.rb +42 -0
- data/lib/ucode/models/audit/release_formula.rb +33 -0
- data/lib/ucode/models/audit/release_manifest.rb +43 -0
- data/lib/ucode/models/audit/release_universal_set.rb +37 -0
- data/lib/ucode/models/audit.rb +9 -0
- data/lib/ucode/models/block.rb +2 -0
- data/lib/ucode/models/build_report.rb +109 -0
- data/lib/ucode/models/codepoint/glyph.rb +42 -0
- data/lib/ucode/models/codepoint.rb +3 -0
- data/lib/ucode/models/glyph_source.rb +86 -0
- data/lib/ucode/models/glyph_source_map.rb +138 -0
- data/lib/ucode/models/specialist_font.rb +70 -0
- data/lib/ucode/models/specialist_font_manifest.rb +48 -0
- data/lib/ucode/models/unihan_entry.rb +81 -9
- data/lib/ucode/models/unihan_field.rb +21 -0
- data/lib/ucode/models/universal_set_entry.rb +47 -0
- data/lib/ucode/models/universal_set_manifest.rb +78 -0
- data/lib/ucode/models/validation_report.rb +99 -0
- data/lib/ucode/models.rb +9 -0
- data/lib/ucode/parsers/named_sequences.rb +5 -5
- data/lib/ucode/parsers/unihan.rb +50 -19
- data/lib/ucode/repo/aggregate_writer.rb +82 -288
- data/lib/ucode/repo/block_feed_emitter.rb +153 -0
- data/lib/ucode/repo/build_report_accumulator.rb +138 -0
- data/lib/ucode/repo/build_report_writer.rb +46 -0
- data/lib/ucode/repo/build_validator.rb +229 -0
- data/lib/ucode/repo/codepoint_writer.rb +50 -1
- data/lib/ucode/repo/paths.rb +8 -0
- data/lib/ucode/repo/writers/blocks_writer.rb +73 -0
- data/lib/ucode/repo/writers/enums_writer.rb +38 -0
- data/lib/ucode/repo/writers/indexes_writer.rb +53 -0
- data/lib/ucode/repo/writers/manifest_writer.rb +78 -0
- data/lib/ucode/repo/writers/named_sequences_writer.rb +47 -0
- data/lib/ucode/repo/writers/planes_writer.rb +82 -0
- data/lib/ucode/repo/writers/relationships_writer.rb +71 -0
- data/lib/ucode/repo/writers/scripts_writer.rb +54 -0
- data/lib/ucode/repo/writers.rb +20 -0
- data/lib/ucode/repo.rb +5 -0
- data/lib/ucode/version.rb +1 -1
- data/schema/block-feed.output.schema.yml +134 -0
- metadata +158 -4
- data/Gemfile.lock +0 -406
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
require "ucode/models/glyph_source"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Models
|
|
9
|
+
# Top-level shape of `config/unicode17_universal_glyph_set.yml`.
|
|
10
|
+
# Pairs envelope metadata (Unicode + ucode version, generated_at)
|
|
11
|
+
# with the block→sources map itself.
|
|
12
|
+
#
|
|
13
|
+
# Block keys are the verbatim Unicode block name with runs of
|
|
14
|
+
# whitespace collapsed to a single underscore — the canonical
|
|
15
|
+
# block id used everywhere else in this codebase (see
|
|
16
|
+
# {Ucode::Parsers::Blocks}): "Basic_Latin", "Greek_and_Coptic",
|
|
17
|
+
# "CJK_Unified_Ideographs_Extension_J". Never slugified beyond
|
|
18
|
+
# whitespace collapsing.
|
|
19
|
+
#
|
|
20
|
+
# Wire shape (note: `map:` is a hash keyed by block id, not an
|
|
21
|
+
# array):
|
|
22
|
+
#
|
|
23
|
+
# unicode_version: "17.0.0"
|
|
24
|
+
# ucode_version: "0.2.0"
|
|
25
|
+
# generated_at: "2026-06-28T00:00:00Z"
|
|
26
|
+
# default_sources: # applies when a block's sources are absent/empty
|
|
27
|
+
# - kind: fontist
|
|
28
|
+
# label: noto-sans
|
|
29
|
+
# priority: 1
|
|
30
|
+
# license: OFL
|
|
31
|
+
# map:
|
|
32
|
+
# Basic_Latin:
|
|
33
|
+
# sources:
|
|
34
|
+
# - kind: fontist
|
|
35
|
+
# label: noto-sans
|
|
36
|
+
# priority: 1
|
|
37
|
+
# Sidetic:
|
|
38
|
+
# sources: []
|
|
39
|
+
#
|
|
40
|
+
# An entry with `sources: []` (or omitted) is valid: it declares
|
|
41
|
+
# "no block-specific Tier 1 font; fall back to `default_sources`,
|
|
42
|
+
# then to Pillars 1-3". The fallback chain is implemented in
|
|
43
|
+
# {#sources_for}; the raw map is left untouched.
|
|
44
|
+
#
|
|
45
|
+
# The hash is stored as a raw `:hash` attribute (lutaml-model
|
|
46
|
+
# collection semantics don't pair cleanly with a hash-keyed wire
|
|
47
|
+
# shape); the typed accessors wrap each entry's raw hashes in
|
|
48
|
+
# {GlyphSource} instances on demand.
|
|
49
|
+
class GlyphSourceMap < Lutaml::Model::Serializable
|
|
50
|
+
attribute :unicode_version, :string
|
|
51
|
+
attribute :ucode_version, :string
|
|
52
|
+
attribute :generated_at, :string
|
|
53
|
+
attribute :default_sources_raw, :hash, collection: true, default: -> { [] }
|
|
54
|
+
attribute :block_sources, :hash, default: -> { {} }
|
|
55
|
+
|
|
56
|
+
key_value do
|
|
57
|
+
map "unicode_version", to: :unicode_version
|
|
58
|
+
map "ucode_version", to: :ucode_version
|
|
59
|
+
map "generated_at", to: :generated_at
|
|
60
|
+
map "default_sources", to: :default_sources_raw
|
|
61
|
+
map "map", to: :block_sources
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @param block_id [String] verbatim block id (underscore form)
|
|
65
|
+
# @return [Array<GlyphSource>] sources for the block, in
|
|
66
|
+
# priority order (ascending). Falls through block-specific →
|
|
67
|
+
# `default_sources` → empty.
|
|
68
|
+
def sources_for(block_id)
|
|
69
|
+
raw = block_sources[block_id]
|
|
70
|
+
list = extract_sources_list(raw)
|
|
71
|
+
list = default_sources_list if list.empty?
|
|
72
|
+
list.map { |h| GlyphSource.from_hash(h.transform_keys(&:to_s)) }
|
|
73
|
+
.sort_by(&:priority)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Array<GlyphSource>] the top-level default sources,
|
|
77
|
+
# typed and priority-sorted. Empty when not declared.
|
|
78
|
+
def default_sources
|
|
79
|
+
default_sources_list
|
|
80
|
+
.map { |h| GlyphSource.from_hash(h.transform_keys(&:to_s)) }
|
|
81
|
+
.sort_by(&:priority)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @param block_id [String]
|
|
85
|
+
# @return [Boolean] true if the block has any entry in the map
|
|
86
|
+
# (even with empty sources). Does not consider `default_sources`.
|
|
87
|
+
def has_block?(block_id)
|
|
88
|
+
block_sources.key?(block_id)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Array<String>] every block_id that appears in the map
|
|
92
|
+
# (regardless of whether it has sources).
|
|
93
|
+
def block_ids
|
|
94
|
+
block_sources.keys
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @return [Array<String>] block_ids whose own `sources:` list has
|
|
98
|
+
# at least one entry. Blocks relying on `default_sources` are
|
|
99
|
+
# excluded — they have no block-specific policy.
|
|
100
|
+
def configured_block_ids
|
|
101
|
+
block_sources.each_with_object([]) do |(block_id, raw), acc|
|
|
102
|
+
acc << block_id if any_sources?(raw)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
# Each block's value in the YAML is either:
|
|
109
|
+
# - `{sources: [...]}` (canonical form), or
|
|
110
|
+
# - `[...]` (shorthand: the sources list directly).
|
|
111
|
+
# Return the sources array in both cases; empty for `nil`.
|
|
112
|
+
def extract_sources_list(raw)
|
|
113
|
+
return [] if raw.nil?
|
|
114
|
+
return raw if raw.is_a?(Array)
|
|
115
|
+
return Array(raw["sources"]) if raw.is_a?(Hash) && raw.key?("sources")
|
|
116
|
+
return Array(raw[:sources]) if raw.is_a?(Hash) && raw.key?(:sources)
|
|
117
|
+
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Each block's value in the YAML is either `{sources: [...]}` or
|
|
122
|
+
# directly an array (shorthand). Normalize to the array of
|
|
123
|
+
# source-hashes form.
|
|
124
|
+
def any_sources?(raw)
|
|
125
|
+
return false if raw.nil?
|
|
126
|
+
return raw.any? if raw.is_a?(Array)
|
|
127
|
+
|
|
128
|
+
raw.is_a?(Hash) && Array(raw["sources"] || raw[:sources]).any?
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# `default_sources` on the wire is a list of source hashes. Older
|
|
132
|
+
# configs may omit it; treat absence as an empty list.
|
|
133
|
+
def default_sources_list
|
|
134
|
+
Array(default_sources_raw)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Models
|
|
7
|
+
# One entry in `config/specialist_fonts.yml` — a Tier 1 font that
|
|
8
|
+
# fontist's formula index does not carry (academic sites, SIL
|
|
9
|
+
# downloads, GitHub releases). The fetcher walks a list of these
|
|
10
|
+
# and materializes each `path` on disk.
|
|
11
|
+
#
|
|
12
|
+
# Wire shape (YAML):
|
|
13
|
+
#
|
|
14
|
+
# label: Lentariso
|
|
15
|
+
# version: "1.033"
|
|
16
|
+
# license: OFL
|
|
17
|
+
# url: "https://github.com/.../Lentariso.otf"
|
|
18
|
+
# sha256: "<hex>" # null until first successful fetch
|
|
19
|
+
# path: "data/fonts/Lentariso.otf"
|
|
20
|
+
# extract: false
|
|
21
|
+
# extract_member: null # required when extract: true
|
|
22
|
+
# provenance: "Imperial Aramaic / Phoenician / Sidetic coverage"
|
|
23
|
+
#
|
|
24
|
+
# `url: null` marks a local-only entry: the user supplies the
|
|
25
|
+
# file at `path` (which may use `~` and shell globs); the fetcher
|
|
26
|
+
# never attempts a network download for these.
|
|
27
|
+
class SpecialistFont < Lutaml::Model::Serializable
|
|
28
|
+
LICENSE_OFL = "OFL"
|
|
29
|
+
private_constant :LICENSE_OFL
|
|
30
|
+
|
|
31
|
+
attribute :label, :string
|
|
32
|
+
attribute :version, :string
|
|
33
|
+
attribute :license, :string, default: -> { LICENSE_OFL }
|
|
34
|
+
attribute :url, :string
|
|
35
|
+
attribute :sha256, :string
|
|
36
|
+
attribute :path, :string
|
|
37
|
+
attribute :extract, :boolean, default: -> { false }
|
|
38
|
+
attribute :extract_member, :string
|
|
39
|
+
attribute :provenance, :string
|
|
40
|
+
|
|
41
|
+
key_value do
|
|
42
|
+
map "label", to: :label
|
|
43
|
+
map "version", to: :version
|
|
44
|
+
map "license", to: :license
|
|
45
|
+
map "url", to: :url
|
|
46
|
+
map "sha256", to: :sha256
|
|
47
|
+
map "path", to: :path
|
|
48
|
+
map "extract", to: :extract
|
|
49
|
+
map "extract_member", to: :extract_member
|
|
50
|
+
map "provenance", to: :provenance
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def local_only?
|
|
54
|
+
url.nil? || url.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ofl?
|
|
58
|
+
license == LICENSE_OFL
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def hash_known?
|
|
62
|
+
!sha256.nil? && !sha256.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def extract?
|
|
66
|
+
extract == true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
require "ucode/models/specialist_font"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Models
|
|
9
|
+
# Typed view over `config/specialist_fonts.yml`. Carries the full
|
|
10
|
+
# list of {SpecialistFont} entries; provides lookup by label so
|
|
11
|
+
# the fetcher can honor `--label Lentariso` without scanning the
|
|
12
|
+
# array itself.
|
|
13
|
+
#
|
|
14
|
+
# The manifest is pure data — it does not know the path it was
|
|
15
|
+
# loaded from. Persistence of computed SHA256 hashes back to disk
|
|
16
|
+
# is the responsibility of {Ucode::Fetch::SpecialistFontFetcher},
|
|
17
|
+
# which owns the file path and writes atomically after a run.
|
|
18
|
+
class SpecialistFontManifest < Lutaml::Model::Serializable
|
|
19
|
+
attribute :fonts, SpecialistFont, collection: true
|
|
20
|
+
|
|
21
|
+
key_value do
|
|
22
|
+
map "fonts", to: :fonts
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param label [String] exact label match
|
|
26
|
+
# @return [SpecialistFont, nil]
|
|
27
|
+
def find_by_label(label)
|
|
28
|
+
fonts.find { |font| font.label == label }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Array<String>] labels of every entry, in declared order
|
|
32
|
+
def labels
|
|
33
|
+
fonts.map(&:label)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param label [String]
|
|
37
|
+
# @return [SpecialistFontManifest] a new manifest containing only
|
|
38
|
+
# the matching font. Returns self unchanged if the label is
|
|
39
|
+
# unknown (the fetcher reports it as a separate failure).
|
|
40
|
+
def only(label)
|
|
41
|
+
match = find_by_label(label)
|
|
42
|
+
return self if match.nil?
|
|
43
|
+
|
|
44
|
+
self.class.new(fonts: [match])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -2,21 +2,93 @@
|
|
|
2
2
|
|
|
3
3
|
require "lutaml/model"
|
|
4
4
|
|
|
5
|
+
require "ucode/models/unihan_field"
|
|
6
|
+
|
|
5
7
|
module Ucode
|
|
6
8
|
module Models
|
|
7
|
-
# Unihan dictionary data for CJK codepoints
|
|
8
|
-
#
|
|
9
|
-
#
|
|
9
|
+
# Unihan dictionary data for CJK codepoints, grouped into the 8
|
|
10
|
+
# categories defined by the Unihan standard. Each category
|
|
11
|
+
# corresponds to one Unihan file:
|
|
12
|
+
#
|
|
13
|
+
# Unihan_DictionaryIndices.txt → dictionary_indices
|
|
14
|
+
# Unihan_DictionaryLikeData.txt → dictionary_like_data
|
|
15
|
+
# Unihan_IRGSources.txt → irg_sources
|
|
16
|
+
# Unihan_NumericValues.txt → numeric_values
|
|
17
|
+
# Unihan_RadicalStrokeCounts.txt → radical_stroke_counts
|
|
18
|
+
# Unihan_Readings.txt → readings
|
|
19
|
+
# Unihan_Variants.txt → variants
|
|
20
|
+
# Unihan_OtherMappings.txt → other_mappings
|
|
10
21
|
#
|
|
11
|
-
#
|
|
12
|
-
# is
|
|
13
|
-
#
|
|
14
|
-
#
|
|
22
|
+
# Each category attribute is a collection of {UnihanField} records.
|
|
23
|
+
# Category is set at parse time from the source filename (via
|
|
24
|
+
# `FILE_TO_CATEGORY`) — Unicode does not reorganize files across
|
|
25
|
+
# versions, so this is stable without per-field hardcoding.
|
|
15
26
|
class UnihanEntry < Lutaml::Model::Serializable
|
|
16
|
-
attribute
|
|
27
|
+
# Symbol → attribute name. Mirrors the 8 Unihan files.
|
|
28
|
+
CATEGORIES = {
|
|
29
|
+
dictionary_indices: :dictionary_indices,
|
|
30
|
+
dictionary_like_data: :dictionary_like_data,
|
|
31
|
+
irg_sources: :irg_sources,
|
|
32
|
+
numeric_values: :numeric_values,
|
|
33
|
+
radical_stroke_counts: :radical_stroke_counts,
|
|
34
|
+
readings: :readings,
|
|
35
|
+
variants: :variants,
|
|
36
|
+
other_mappings: :other_mappings,
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Filename → category symbol. Used by the parser to bucket
|
|
40
|
+
# records without callers needing to know the mapping.
|
|
41
|
+
FILE_TO_CATEGORY = {
|
|
42
|
+
"Unihan_DictionaryIndices.txt" => :dictionary_indices,
|
|
43
|
+
"Unihan_DictionaryLikeData.txt" => :dictionary_like_data,
|
|
44
|
+
"Unihan_IRGSources.txt" => :irg_sources,
|
|
45
|
+
"Unihan_NumericValues.txt" => :numeric_values,
|
|
46
|
+
"Unihan_RadicalStrokeCounts.txt" => :radical_stroke_counts,
|
|
47
|
+
"Unihan_Readings.txt" => :readings,
|
|
48
|
+
"Unihan_Variants.txt" => :variants,
|
|
49
|
+
"Unihan_OtherMappings.txt" => :other_mappings,
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
attribute :dictionary_indices, UnihanField, collection: true, default: -> { [] }
|
|
53
|
+
attribute :dictionary_like_data, UnihanField, collection: true, default: -> { [] }
|
|
54
|
+
attribute :irg_sources, UnihanField, collection: true, default: -> { [] }
|
|
55
|
+
attribute :numeric_values, UnihanField, collection: true, default: -> { [] }
|
|
56
|
+
attribute :radical_stroke_counts, UnihanField, collection: true, default: -> { [] }
|
|
57
|
+
attribute :readings, UnihanField, collection: true, default: -> { [] }
|
|
58
|
+
attribute :variants, UnihanField, collection: true, default: -> { [] }
|
|
59
|
+
attribute :other_mappings, UnihanField, collection: true, default: -> { [] }
|
|
60
|
+
|
|
61
|
+
# Pushes a field into the right category bucket. Used by the
|
|
62
|
+
# Coordinator when streaming records from the parser.
|
|
63
|
+
#
|
|
64
|
+
# @param category [Symbol] one of CATEGORIES keys
|
|
65
|
+
# @param name [String] e.g. "kMandarin"
|
|
66
|
+
# @param values [Array<String>] space-split values from Unihan
|
|
67
|
+
def add(category, name, values)
|
|
68
|
+
attr_name = CATEGORIES.fetch(category) { return }
|
|
69
|
+
public_send(attr_name) << UnihanField.new(name: name, values: values)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# True if any category has data.
|
|
73
|
+
def any?
|
|
74
|
+
CATEGORIES.keys.any? { |sym| !public_send(sym).empty? }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# All fields across every category, flattened to {name => values}.
|
|
78
|
+
# Iteration helper for consumers that want a flat view (search
|
|
79
|
+
# indexing, downstream filtering).
|
|
80
|
+
#
|
|
81
|
+
# @return [Hash{String => Array<String>}]
|
|
82
|
+
def all_fields
|
|
83
|
+
CATEGORIES.keys.each_with_object({}) do |sym, h|
|
|
84
|
+
public_send(sym).each { |f| h[f.name] = f.values }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
17
87
|
|
|
18
88
|
key_value do
|
|
19
|
-
|
|
89
|
+
CATEGORIES.each do |symbol, attr_name|
|
|
90
|
+
map attr_name, to: symbol
|
|
91
|
+
end
|
|
20
92
|
end
|
|
21
93
|
end
|
|
22
94
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Models
|
|
7
|
+
# One Unihan field assignment: a k-field name plus its space-split
|
|
8
|
+
# values. e.g. `kMandarin → ["jìng"]`, `kHanyuPinyin → ["64047.030:jìng"]`.
|
|
9
|
+
# The values list is uniform across all Unihan fields — even single-valued
|
|
10
|
+
# ones are arrays, which simplifies consumer logic.
|
|
11
|
+
class UnihanField < Lutaml::Model::Serializable
|
|
12
|
+
attribute :name, :string
|
|
13
|
+
attribute :values, :string, collection: true, default: -> { [] }
|
|
14
|
+
|
|
15
|
+
key_value do
|
|
16
|
+
map "name", to: :name
|
|
17
|
+
map "values", to: :values
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Models
|
|
7
|
+
# One row in a {UniversalSetManifest}. Records the resolved glyph
|
|
8
|
+
# for a single codepoint: which tier produced it, which source
|
|
9
|
+
# font, and a stable content hash + size so downstream consumers
|
|
10
|
+
# can detect changes without re-reading the SVG.
|
|
11
|
+
#
|
|
12
|
+
# Wire shape (one entry per assigned codepoint in the manifest's
|
|
13
|
+
# `entries:` array):
|
|
14
|
+
#
|
|
15
|
+
# {
|
|
16
|
+
# "codepoint": 65,
|
|
17
|
+
# "id": "U+0041",
|
|
18
|
+
# "tier": "tier-1",
|
|
19
|
+
# "source": "noto-sans",
|
|
20
|
+
# "svg_sha256": "abc...",
|
|
21
|
+
# "svg_size_bytes": 412
|
|
22
|
+
# }
|
|
23
|
+
#
|
|
24
|
+
# `source` is the source identifier extracted from the resolver
|
|
25
|
+
# {Ucode::Glyphs::Source::Result#provenance} — i.e. the part after
|
|
26
|
+
# the `tier:` prefix ("noto-sans" for "tier-1:noto-sans"). This is
|
|
27
|
+
# what audits (TODO 25) group by when answering "how many
|
|
28
|
+
# codepoints does font X cover in this set?".
|
|
29
|
+
class UniversalSetEntry < Lutaml::Model::Serializable
|
|
30
|
+
attribute :codepoint, :integer
|
|
31
|
+
attribute :id, :string
|
|
32
|
+
attribute :tier, :string
|
|
33
|
+
attribute :source, :string
|
|
34
|
+
attribute :svg_sha256, :string
|
|
35
|
+
attribute :svg_size_bytes, :integer, default: 0
|
|
36
|
+
|
|
37
|
+
key_value do
|
|
38
|
+
map "codepoint", to: :codepoint
|
|
39
|
+
map "id", to: :id
|
|
40
|
+
map "tier", to: :tier
|
|
41
|
+
map "source", to: :source
|
|
42
|
+
map "svg_sha256", to: :svg_sha256
|
|
43
|
+
map "svg_size_bytes", to: :svg_size_bytes
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
require "ucode/models/universal_set_entry"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Models
|
|
9
|
+
# Manifest emitted at the end of a universal glyph set build
|
|
10
|
+
# (TODO 24). The single index into the set: every codepoint that
|
|
11
|
+
# was attempted gets one {UniversalSetEntry}, and the totals +
|
|
12
|
+
# per-tier rollups let consumers (audits, fontist.org) answer
|
|
13
|
+
# "what does this set cover?" without reading every SVG.
|
|
14
|
+
#
|
|
15
|
+
# Wire shape:
|
|
16
|
+
#
|
|
17
|
+
# {
|
|
18
|
+
# "unicode_version": "17.0.0",
|
|
19
|
+
# "ucode_version": "0.2.0",
|
|
20
|
+
# "generated_at": "2026-06-28T00:00:00Z",
|
|
21
|
+
# "source_config_sha256": "abc...",
|
|
22
|
+
# "totals": {
|
|
23
|
+
# "codepoints_assigned": 150012,
|
|
24
|
+
# "codepoints_built": 150012,
|
|
25
|
+
# "codepoints_skipped": 0,
|
|
26
|
+
# "codepoints_failed": 0
|
|
27
|
+
# },
|
|
28
|
+
# "by_tier": {
|
|
29
|
+
# "tier-1": 148512, "pillar-1": 800,
|
|
30
|
+
# "pillar-2": 200, "pillar-3": 1500
|
|
31
|
+
# },
|
|
32
|
+
# "entries": [ { ... UniversalSetEntry ... }, ... ]
|
|
33
|
+
# }
|
|
34
|
+
#
|
|
35
|
+
# `source_config_sha256` pins which Tier 1 source map produced
|
|
36
|
+
# this set. Audits use it to detect drift between the reference
|
|
37
|
+
# set and the config they were validated against.
|
|
38
|
+
#
|
|
39
|
+
# This class is passive — accumulation logic lives in
|
|
40
|
+
# {Ucode::Glyphs::UniversalSet::ManifestAccumulator}; this class
|
|
41
|
+
# only describes the wire shape and handles (de)serialization via
|
|
42
|
+
# lutaml-model.
|
|
43
|
+
class UniversalSetManifest < Lutaml::Model::Serializable
|
|
44
|
+
# Total counts for one build run.
|
|
45
|
+
class Totals < Lutaml::Model::Serializable
|
|
46
|
+
attribute :codepoints_assigned, :integer, default: 0
|
|
47
|
+
attribute :codepoints_built, :integer, default: 0
|
|
48
|
+
attribute :codepoints_skipped, :integer, default: 0
|
|
49
|
+
attribute :codepoints_failed, :integer, default: 0
|
|
50
|
+
|
|
51
|
+
key_value do
|
|
52
|
+
map "codepoints_assigned", to: :codepoints_assigned
|
|
53
|
+
map "codepoints_built", to: :codepoints_built
|
|
54
|
+
map "codepoints_skipped", to: :codepoints_skipped
|
|
55
|
+
map "codepoints_failed", to: :codepoints_failed
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
attribute :unicode_version, :string
|
|
60
|
+
attribute :ucode_version, :string
|
|
61
|
+
attribute :generated_at, :string
|
|
62
|
+
attribute :source_config_sha256, :string
|
|
63
|
+
attribute :totals, Totals
|
|
64
|
+
attribute :by_tier, :hash, default: -> { {} }
|
|
65
|
+
attribute :entries, UniversalSetEntry, collection: true, default: -> { [] }
|
|
66
|
+
|
|
67
|
+
key_value do
|
|
68
|
+
map "unicode_version", to: :unicode_version
|
|
69
|
+
map "ucode_version", to: :ucode_version
|
|
70
|
+
map "generated_at", to: :generated_at
|
|
71
|
+
map "source_config_sha256", to: :source_config_sha256
|
|
72
|
+
map "totals", to: :totals
|
|
73
|
+
map "by_tier", to: :by_tier
|
|
74
|
+
map "entries", to: :entries
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
|
|
5
|
+
module Ucode
|
|
6
|
+
module Models
|
|
7
|
+
# Post-build validation report (TODO 21 §Validation). Emitted as
|
|
8
|
+
# `output/validation-report.json` by {Ucode::Repo::BuildValidator}
|
|
9
|
+
# after a canonical build run. Records the outcome of the four
|
|
10
|
+
# automated validation checks:
|
|
11
|
+
#
|
|
12
|
+
# 1. `completeness` — every codepoint folder has both
|
|
13
|
+
# `index.json` and `glyph.svg`.
|
|
14
|
+
# 2. `schema` — every `index.json` deserializes via
|
|
15
|
+
# `Ucode::Models::CodePoint.from_hash`.
|
|
16
|
+
# 3. `provenance_sanity` — every deserialized CodePoint carries
|
|
17
|
+
# a non-nil `glyph.source.tier`.
|
|
18
|
+
# 4. `block_coverage` — per-block built count matches the
|
|
19
|
+
# baseline (skipped when no baseline is supplied).
|
|
20
|
+
#
|
|
21
|
+
# The fifth TODO 21 check (sample inspection) is manual and out
|
|
22
|
+
# of scope for the automated validator.
|
|
23
|
+
#
|
|
24
|
+
# Like {BuildReport}, this model is passive: the accumulation
|
|
25
|
+
# logic lives in {Ucode::Repo::BuildValidator}; this class only
|
|
26
|
+
# describes the wire shape and handles (de)serialization.
|
|
27
|
+
class ValidationReport < Lutaml::Model::Serializable
|
|
28
|
+
# Aggregate pass/fail counts for the run.
|
|
29
|
+
class Totals < Lutaml::Model::Serializable
|
|
30
|
+
attribute :codepoints_checked, :integer, default: 0
|
|
31
|
+
attribute :failures, :integer, default: 0
|
|
32
|
+
attribute :checks_run, :integer, default: 0
|
|
33
|
+
attribute :checks_passed, :integer, default: 0
|
|
34
|
+
|
|
35
|
+
key_value do
|
|
36
|
+
map "codepoints_checked", to: :codepoints_checked
|
|
37
|
+
map "failures", to: :failures
|
|
38
|
+
map "checks_run", to: :checks_run
|
|
39
|
+
map "checks_passed", to: :checks_passed
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Per-check summary. `status` is one of `passed` / `failed` /
|
|
44
|
+
# `skipped`. `total` is the number of codepoints the check
|
|
45
|
+
# evaluated against (0 for `skipped`). `failures` is the count
|
|
46
|
+
# of recorded failures for this check.
|
|
47
|
+
class CheckSummary < Lutaml::Model::Serializable
|
|
48
|
+
STATUS_PASSED = "passed"
|
|
49
|
+
STATUS_FAILED = "failed"
|
|
50
|
+
STATUS_SKIPPED = "skipped"
|
|
51
|
+
|
|
52
|
+
attribute :name, :string
|
|
53
|
+
attribute :status, :string
|
|
54
|
+
attribute :total, :integer, default: 0
|
|
55
|
+
attribute :failures, :integer, default: 0
|
|
56
|
+
|
|
57
|
+
key_value do
|
|
58
|
+
map "name", to: :name
|
|
59
|
+
map "status", to: :status
|
|
60
|
+
map "total", to: :total
|
|
61
|
+
map "failures", to: :failures
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# One failure record. `codepoint` is the integer codepoint (or
|
|
66
|
+
# nil for structural failures like block_coverage); `block` is
|
|
67
|
+
# the verbatim block id (folder name); `check` names the check
|
|
68
|
+
# that produced this failure; `message` is a free-form
|
|
69
|
+
# human-readable explanation.
|
|
70
|
+
class Failure < Lutaml::Model::Serializable
|
|
71
|
+
attribute :codepoint, :integer
|
|
72
|
+
attribute :block, :string
|
|
73
|
+
attribute :check, :string
|
|
74
|
+
attribute :message, :string
|
|
75
|
+
|
|
76
|
+
key_value do
|
|
77
|
+
map "codepoint", to: :codepoint
|
|
78
|
+
map "block", to: :block
|
|
79
|
+
map "check", to: :check
|
|
80
|
+
map "message", to: :message
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
attribute :unicode_version, :string
|
|
85
|
+
attribute :generated_at, :string
|
|
86
|
+
attribute :totals, Totals
|
|
87
|
+
attribute :checks, CheckSummary, collection: true, default: -> { [] }
|
|
88
|
+
attribute :failures, Failure, collection: true, default: -> { [] }
|
|
89
|
+
|
|
90
|
+
key_value do
|
|
91
|
+
map "unicode_version", to: :unicode_version
|
|
92
|
+
map "generated_at", to: :generated_at
|
|
93
|
+
map "totals", to: :totals
|
|
94
|
+
map "checks", to: :checks
|
|
95
|
+
map "failures", to: :failures
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/ucode/models.rb
CHANGED
|
@@ -31,6 +31,7 @@ module Ucode
|
|
|
31
31
|
autoload :Script, "ucode/models/script"
|
|
32
32
|
autoload :CodePoint, "ucode/models/codepoint"
|
|
33
33
|
autoload :UnihanEntry, "ucode/models/unihan_entry"
|
|
34
|
+
autoload :UnihanField, "ucode/models/unihan_field"
|
|
34
35
|
autoload :NamesListEntry, "ucode/models/names_list_entry"
|
|
35
36
|
autoload :NameAlias, "ucode/models/name_alias"
|
|
36
37
|
autoload :NamedSequence, "ucode/models/named_sequence"
|
|
@@ -43,5 +44,13 @@ module Ucode
|
|
|
43
44
|
autoload :BinaryPropertyAssignment, "ucode/models/binary_property_assignment"
|
|
44
45
|
autoload :Relationship, "ucode/models/relationship"
|
|
45
46
|
autoload :Audit, "ucode/models/audit"
|
|
47
|
+
autoload :BuildReport, "ucode/models/build_report"
|
|
48
|
+
autoload :ValidationReport, "ucode/models/validation_report"
|
|
49
|
+
autoload :GlyphSource, "ucode/models/glyph_source"
|
|
50
|
+
autoload :GlyphSourceMap, "ucode/models/glyph_source_map"
|
|
51
|
+
autoload :SpecialistFont, "ucode/models/specialist_font"
|
|
52
|
+
autoload :SpecialistFontManifest, "ucode/models/specialist_font_manifest"
|
|
53
|
+
autoload :UniversalSetEntry, "ucode/models/universal_set_entry"
|
|
54
|
+
autoload :UniversalSetManifest, "ucode/models/universal_set_manifest"
|
|
46
55
|
end
|
|
47
56
|
end
|
|
@@ -8,10 +8,10 @@ module Ucode
|
|
|
8
8
|
# Parses `NamedSequences.txt` — named multi-codepoint sequences.
|
|
9
9
|
#
|
|
10
10
|
# Format (UAX #44):
|
|
11
|
-
# cp1 cp2 cp3
|
|
11
|
+
# Name; cp1 cp2 cp3 ...
|
|
12
12
|
#
|
|
13
|
-
# The first field is
|
|
14
|
-
#
|
|
13
|
+
# The first field is the human-readable name; the second is a
|
|
14
|
+
# space-separated list of hex codepoints.
|
|
15
15
|
class NamedSequences < Base
|
|
16
16
|
class << self
|
|
17
17
|
# Yields one NamedSequence per non-comment line. Returns a lazy
|
|
@@ -23,8 +23,8 @@ module Ucode
|
|
|
23
23
|
fields = line.fields
|
|
24
24
|
next if fields.length < 2
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
name = fields[0]
|
|
27
|
+
sequence_field = fields[1]
|
|
28
28
|
next if name.nil? || name.empty?
|
|
29
29
|
|
|
30
30
|
yield Models::NamedSequence.new(
|