unitsdb 2.1.1 → 2.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +8 -1
  3. data/.gitignore +2 -0
  4. data/.gitmodules +4 -3
  5. data/.rubocop.yml +13 -8
  6. data/.rubocop_todo.yml +217 -100
  7. data/CLAUDE.md +55 -0
  8. data/Gemfile +4 -1
  9. data/README.adoc +283 -16
  10. data/data/dimensions.yaml +1864 -0
  11. data/data/prefixes.yaml +874 -0
  12. data/data/quantities.yaml +3715 -0
  13. data/data/scales.yaml +97 -0
  14. data/data/schemas/dimensions-schema.yaml +153 -0
  15. data/data/schemas/prefixes-schema.yaml +155 -0
  16. data/data/schemas/quantities-schema.yaml +117 -0
  17. data/data/schemas/scales-schema.yaml +106 -0
  18. data/data/schemas/unit_systems-schema.yaml +116 -0
  19. data/data/schemas/units-schema.yaml +215 -0
  20. data/data/unit_systems.yaml +78 -0
  21. data/data/units.yaml +14052 -0
  22. data/exe/unitsdb +7 -1
  23. data/lib/unitsdb/cli.rb +42 -15
  24. data/lib/unitsdb/commands/_modify.rb +40 -4
  25. data/lib/unitsdb/commands/base.rb +6 -2
  26. data/lib/unitsdb/commands/check_si/si_formatter.rb +488 -0
  27. data/lib/unitsdb/commands/check_si/si_matcher.rb +487 -0
  28. data/lib/unitsdb/commands/check_si/si_ttl_parser.rb +103 -0
  29. data/lib/unitsdb/commands/check_si/si_updater.rb +254 -0
  30. data/lib/unitsdb/commands/check_si.rb +54 -35
  31. data/lib/unitsdb/commands/get.rb +11 -10
  32. data/lib/unitsdb/commands/normalize.rb +21 -7
  33. data/lib/unitsdb/commands/qudt/check.rb +150 -0
  34. data/lib/unitsdb/commands/qudt/formatter.rb +194 -0
  35. data/lib/unitsdb/commands/qudt/matcher.rb +746 -0
  36. data/lib/unitsdb/commands/qudt/ttl_parser.rb +403 -0
  37. data/lib/unitsdb/commands/qudt/update.rb +126 -0
  38. data/lib/unitsdb/commands/qudt/updater.rb +189 -0
  39. data/lib/unitsdb/commands/qudt.rb +82 -0
  40. data/lib/unitsdb/commands/release.rb +12 -9
  41. data/lib/unitsdb/commands/search.rb +12 -11
  42. data/lib/unitsdb/commands/ucum/check.rb +42 -29
  43. data/lib/unitsdb/commands/ucum/formatter.rb +2 -1
  44. data/lib/unitsdb/commands/ucum/matcher.rb +23 -9
  45. data/lib/unitsdb/commands/ucum/update.rb +14 -13
  46. data/lib/unitsdb/commands/ucum/updater.rb +40 -6
  47. data/lib/unitsdb/commands/ucum/xml_parser.rb +0 -2
  48. data/lib/unitsdb/commands/ucum.rb +44 -4
  49. data/lib/unitsdb/commands/validate/identifiers.rb +2 -4
  50. data/lib/unitsdb/commands/validate/qudt_references.rb +111 -0
  51. data/lib/unitsdb/commands/validate/references.rb +36 -19
  52. data/lib/unitsdb/commands/validate/si_references.rb +3 -5
  53. data/lib/unitsdb/commands/validate/ucum_references.rb +105 -0
  54. data/lib/unitsdb/commands/validate.rb +67 -11
  55. data/lib/unitsdb/commands.rb +20 -0
  56. data/lib/unitsdb/database.rb +90 -52
  57. data/lib/unitsdb/dimension.rb +1 -4
  58. data/lib/unitsdb/dimension_details.rb +0 -1
  59. data/lib/unitsdb/dimensions.rb +0 -2
  60. data/lib/unitsdb/errors.rb +7 -0
  61. data/lib/unitsdb/prefix.rb +0 -4
  62. data/lib/unitsdb/prefix_reference.rb +0 -2
  63. data/lib/unitsdb/prefixes.rb +0 -1
  64. data/lib/unitsdb/quantities.rb +0 -2
  65. data/lib/unitsdb/quantity.rb +0 -6
  66. data/lib/unitsdb/qudt.rb +100 -0
  67. data/lib/unitsdb/root_unit_reference.rb +0 -3
  68. data/lib/unitsdb/scale.rb +0 -4
  69. data/lib/unitsdb/scale_reference.rb +0 -2
  70. data/lib/unitsdb/scales.rb +0 -2
  71. data/lib/unitsdb/si_derived_base.rb +0 -2
  72. data/lib/unitsdb/ucum.rb +14 -10
  73. data/lib/unitsdb/unit.rb +0 -10
  74. data/lib/unitsdb/unit_reference.rb +0 -2
  75. data/lib/unitsdb/unit_system.rb +1 -3
  76. data/lib/unitsdb/unit_system_reference.rb +0 -2
  77. data/lib/unitsdb/unit_systems.rb +0 -2
  78. data/lib/unitsdb/units.rb +0 -2
  79. data/lib/unitsdb/utils.rb +32 -21
  80. data/lib/unitsdb/version.rb +5 -1
  81. data/lib/unitsdb.rb +62 -14
  82. data/unitsdb.gemspec +6 -3
  83. metadata +52 -13
  84. data/lib/unitsdb/commands/si_formatter.rb +0 -485
  85. data/lib/unitsdb/commands/si_matcher.rb +0 -470
  86. data/lib/unitsdb/commands/si_ttl_parser.rb +0 -100
  87. data/lib/unitsdb/commands/si_updater.rb +0 -212
data/exe/unitsdb CHANGED
@@ -4,4 +4,10 @@
4
4
  require "unitsdb"
5
5
  require "unitsdb/cli"
6
6
 
7
- Unitsdb::CLI.start(ARGV)
7
+ begin
8
+ Unitsdb::Cli.start(ARGV)
9
+ rescue SystemExit => e
10
+ # Only print if not already handled (exit code 0 = normal exit)
11
+ # Exit code 1 from our commands is already handled, don't duplicate
12
+ raise if e.status.zero?
13
+ end
data/lib/unitsdb/cli.rb CHANGED
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "thor"
4
- require_relative "commands/base"
5
- require_relative "commands/validate"
6
- require_relative "commands/_modify"
7
- require_relative "commands/ucum"
8
4
  require "fileutils"
9
5
 
10
6
  module Unitsdb
11
- class CLI < Thor
7
+ class Cli < Thor
8
+ # Enable --trace globally for all subcommands
9
+ # When enabled, Thor shows full backtraces on error
10
+ class_option :trace, type: :boolean, default: false,
11
+ desc: "Show full backtrace on error"
12
+
12
13
  # Fix Thor deprecation warning
13
14
  def self.exit_on_failure?
14
15
  true
@@ -17,10 +18,14 @@ module Unitsdb
17
18
  desc "ucum SUBCOMMAND", "UCUM-related commands"
18
19
  subcommand "ucum", Commands::UcumCommand
19
20
 
21
+ desc "qudt SUBCOMMAND", "QUDT-related commands"
22
+ subcommand "qudt", Commands::QudtCommand
23
+
20
24
  desc "_modify SUBCOMMAND", "Commands that modify the database"
21
25
  subcommand "_modify", Commands::ModifyCommand
22
26
 
23
- desc "validate SUBCOMMAND", "Validate database files for different conditions"
27
+ desc "validate SUBCOMMAND",
28
+ "Validate database files for different conditions"
24
29
  subcommand "validate", Commands::ValidateCommand
25
30
 
26
31
  desc "search QUERY", "Search for entities containing the given text"
@@ -36,8 +41,7 @@ module Unitsdb
36
41
  desc: "Path to UnitsDB database (required)"
37
42
 
38
43
  def search(query)
39
- require_relative "commands/search"
40
- Commands::Search.new(options).run(query)
44
+ run_command(Commands::Search, :run, query)
41
45
  end
42
46
 
43
47
  desc "get ID", "Get detailed information about an entity by ID"
@@ -48,11 +52,11 @@ module Unitsdb
48
52
  option :database, type: :string, required: true, aliases: "-d",
49
53
  desc: "Path to UnitsDB database (required)"
50
54
  def get(id)
51
- require_relative "commands/get"
52
- Commands::Get.new(options).get(id)
55
+ run_command(Commands::Get, :get, id)
53
56
  end
54
57
 
55
- desc "check_si", "Check and update SI digital framework references in UnitsDB"
58
+ desc "check_si",
59
+ "Check and update SI digital framework references in UnitsDB"
56
60
  option :entity_type, type: :string, aliases: "-e",
57
61
  desc: "Entity type to check (units, quantities, or prefixes). If not specified, all types are checked"
58
62
  option :ttl_dir, type: :string, required: true, aliases: "-t",
@@ -67,8 +71,7 @@ module Unitsdb
67
71
  desc: "Path to UnitsDB database (required)"
68
72
 
69
73
  def check_si
70
- require_relative "commands/check_si"
71
- Commands::CheckSi.new(options).run
74
+ run_command(Commands::CheckSiCommand, :run)
72
75
  end
73
76
 
74
77
  desc "release", "Create release files (unified YAML and/or ZIP archive)"
@@ -81,8 +84,32 @@ module Unitsdb
81
84
  option :database, type: :string, required: true, aliases: "-d",
82
85
  desc: "Path to UnitsDB database (required)"
83
86
  def release
84
- require_relative "commands/release"
85
- Commands::Release.new(options).run
87
+ run_command(Commands::Release, :run)
88
+ end
89
+
90
+ private
91
+
92
+ def run_command(command_class, method, *args)
93
+ command = command_class.new(options)
94
+ command.send(method, *args)
95
+ rescue Unitsdb::Errors::CLIRuntimeError => e
96
+ handle_cli_error(e)
97
+ rescue StandardError => e
98
+ handle_error(e)
99
+ end
100
+
101
+ def handle_cli_error(error)
102
+ raise error if debugging?
103
+
104
+ warn "Error: #{error.message}"
105
+ exit 1
106
+ end
107
+
108
+ def handle_error(error)
109
+ raise error if debugging?
110
+
111
+ warn "Error: #{error.message}"
112
+ exit 1
86
113
  end
87
114
  end
88
115
  end
@@ -5,8 +5,16 @@ require "thor"
5
5
  module Unitsdb
6
6
  module Commands
7
7
  class ModifyCommand < Thor
8
- desc "normalize [INPUT] [OUTPUT]", "Normalize a YAML file or all YAML files with --all"
9
- method_option :sort, type: :string, default: "nist",
8
+ # Inherit trace option from parent CLI
9
+ class_option :trace, type: :boolean, default: false,
10
+ desc: "Show full backtrace on error"
11
+
12
+ desc "normalize INPUT OUTPUT",
13
+ "Normalize a YAML file or all YAML files with --all"
14
+ method_option :sort, type: :string,
15
+ default: "nist",
16
+ enum: ["short", "nist", "unitsml", "none"],
17
+ aliases: "-s",
10
18
  desc: "Sort units by: 'short' (name), 'nist' (ID, default), 'unitsml' (ID), or 'none'"
11
19
  method_option :database, type: :string, required: true, aliases: "-d",
12
20
  desc: "Path to UnitsDB database (required)"
@@ -14,8 +22,36 @@ module Unitsdb
14
22
  desc: "Process all YAML files in the repository"
15
23
 
16
24
  def normalize(input = nil, output = nil)
17
- require_relative "normalize"
18
- Normalize.new(options).run(input, output)
25
+ run_command(Normalize, options, input, output)
26
+ rescue Unitsdb::Errors::CLIRuntimeError => e
27
+ handle_cli_error(e)
28
+ rescue StandardError => e
29
+ handle_error(e)
30
+ end
31
+
32
+ private
33
+
34
+ def run_command(command_class, options, *args)
35
+ command = command_class.new(options)
36
+ command.run(*args)
37
+ end
38
+
39
+ def handle_cli_error(error)
40
+ if options[:trace]
41
+ raise error
42
+ else
43
+ warn "Error: #{error.message}"
44
+ exit 1
45
+ end
46
+ end
47
+
48
+ def handle_error(error)
49
+ if options[:trace]
50
+ raise error
51
+ else
52
+ warn "Error: #{error.message}"
53
+ exit 1
54
+ end
19
55
  end
20
56
  end
21
57
  end
@@ -15,11 +15,15 @@ module Unitsdb
15
15
 
16
16
  def load_database(path = nil)
17
17
  path ||= @options[:database]
18
- raise Unitsdb::Errors::DatabaseError, "Database path not specified" unless path
18
+ unless path
19
+ raise Unitsdb::Errors::DatabaseError,
20
+ "Database path not specified"
21
+ end
19
22
 
20
23
  Unitsdb::Database.from_db(path)
21
24
  rescue StandardError => e
22
- raise Unitsdb::Errors::DatabaseError, "Failed to load database: #{e.message}"
25
+ raise Unitsdb::Errors::DatabaseError,
26
+ "Failed to load database: #{e.message}"
23
27
  end
24
28
  end
25
29
  end
@@ -0,0 +1,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "terminal-table"
4
+
5
+ module Unitsdb
6
+ module Commands
7
+ module CheckSi
8
+ # Formatter for SI check results
9
+ module SiFormatter
10
+ module_function
11
+
12
+ # Display TTL → DB results
13
+ def display_si_results(entity_type, matches, missing_matches,
14
+ unmatched_ttl)
15
+ puts "\n=== #{entity_type.capitalize} with matching SI references ==="
16
+ if matches.empty?
17
+ puts "None"
18
+ else
19
+ rows = []
20
+ matches.each do |match|
21
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:si_uri])
22
+ rows << [
23
+ "UnitsDB: #{match[:entity_id]}",
24
+ "(#{match[:entity_name] || 'unnamed'})",
25
+ ]
26
+ rows << [
27
+ "SI TTL: #{si_suffix}",
28
+ "(#{match[:si_label] || match[:si_name] || 'unnamed'})",
29
+ ]
30
+ rows << :separator unless match == matches.last
31
+ end
32
+
33
+ table = Terminal::Table.new(
34
+ title: "Valid SI Reference Mappings",
35
+ rows: rows,
36
+ )
37
+ puts table
38
+ end
39
+
40
+ puts "\n=== #{entity_type.capitalize} without SI references ==="
41
+ if missing_matches.empty?
42
+ puts "None"
43
+ else
44
+ # Split matches into exact and potential
45
+ exact_matches = []
46
+ potential_matches = []
47
+
48
+ missing_matches.each do |match|
49
+ # Get match details
50
+ match_details = match[:match_details]
51
+ match_desc = match_details&.dig(:match_desc) || ""
52
+
53
+ # Symbol matches and partial matches should always be potential matches
54
+ if %w[symbol_match partial_match].include?(match_desc)
55
+ potential_matches << match
56
+ elsif match_details&.dig(:exact) == false
57
+ potential_matches << match
58
+ else
59
+ exact_matches << match
60
+ end
61
+ end
62
+
63
+ # Display exact matches
64
+ puts "\n=== Exact Matches (#{exact_matches.size}) ==="
65
+ if exact_matches.empty?
66
+ puts "None"
67
+ else
68
+ rows = []
69
+ exact_matches.each do |match|
70
+ # First row: UnitsDB entity
71
+ rows << [
72
+ "UnitsDB: #{match[:entity_id]}",
73
+ "(#{match[:entity_name] || 'unnamed'})",
74
+ ]
75
+
76
+ # Handle multiple SI matches in a single cell if present
77
+ if match[:multiple_si]
78
+ # Ensure no duplicate URIs
79
+ si_text_parts = []
80
+ si_label_parts = []
81
+ seen_uris = {}
82
+
83
+ match[:multiple_si].each do |si_data|
84
+ uri = si_data[:uri]
85
+ next if seen_uris[uri] # Skip if we've already seen this URI
86
+
87
+ seen_uris[uri] = true
88
+
89
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
90
+ si_text_parts << suffix
91
+ si_label_parts << (si_data[:label] || si_data[:name])
92
+ end
93
+
94
+ rows << [
95
+ "SI TTL: #{si_text_parts.join(', ')}",
96
+ "(#{si_label_parts.join(', ')})",
97
+ ]
98
+ else
99
+ # Second row: SI TTL suffix and label/name
100
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:si_uri])
101
+ rows << [
102
+ "SI TTL: #{si_suffix}",
103
+ "(#{match[:si_label] || match[:si_name] || 'unnamed'})",
104
+ ]
105
+ end
106
+
107
+ # Status line with match type
108
+ match_details = match[:match_details]
109
+ match_desc = match_details&.dig(:match_desc) || ""
110
+ match_info = format_match_info(match_desc)
111
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference (#{match_info})"
112
+
113
+ rows << [
114
+ "Status: #{status_text}",
115
+ "✗",
116
+ ]
117
+ rows << :separator unless match == exact_matches.last
118
+ end
119
+
120
+ table = Terminal::Table.new(
121
+ title: "Exact Match Missing SI References",
122
+ rows: rows,
123
+ )
124
+ puts table
125
+ end
126
+
127
+ # Display potential matches
128
+ puts "\n=== Potential Matches (#{potential_matches.size}) ==="
129
+ if potential_matches.empty?
130
+ puts "None"
131
+ else
132
+ rows = []
133
+ potential_matches.each do |match|
134
+ # First row: UnitsDB entity
135
+ rows << [
136
+ "UnitsDB: #{match[:entity_id]}",
137
+ "(#{match[:entity_name] || 'unnamed'})",
138
+ ]
139
+
140
+ # Handle multiple SI matches in a single cell if present
141
+ if match[:multiple_si]
142
+ # Ensure no duplicate URIs
143
+ si_text_parts = []
144
+ seen_uris = {}
145
+
146
+ match[:multiple_si].each do |si_data|
147
+ uri = si_data[:uri]
148
+ next if seen_uris[uri] # Skip if we've already seen this URI
149
+
150
+ seen_uris[uri] = true
151
+
152
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
153
+ si_text_parts << "#{suffix} (#{si_data[:label] || si_data[:name]})"
154
+ end
155
+
156
+ rows << [
157
+ "SI TTL: #{si_text_parts.join(', ')}",
158
+ "",
159
+ ]
160
+ else
161
+ # Single TTL entity
162
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:si_uri])
163
+ rows << [
164
+ "SI TTL: #{si_suffix}",
165
+ "(#{match[:si_label] || match[:si_name] || 'unnamed'})",
166
+ ]
167
+ end
168
+
169
+ # Status line
170
+ match_details = match[:match_details]
171
+ match_desc = match_details&.dig(:match_desc) || ""
172
+ match_info = format_match_info(match_desc)
173
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference"
174
+
175
+ rows << [
176
+ "Status: #{status_text}",
177
+ "✗",
178
+ ]
179
+ rows << :separator unless match == potential_matches.last
180
+ end
181
+
182
+ table = Terminal::Table.new(
183
+ title: "Potential Match Missing SI References",
184
+ rows: rows,
185
+ )
186
+ puts table
187
+ end
188
+ end
189
+
190
+ puts "\n=== SI #{entity_type.capitalize} not mapped to our database ==="
191
+ if unmatched_ttl.empty?
192
+ puts "None (All TTL entities are referenced - Good job!)"
193
+ else
194
+ # Group unmatched ttl entities by their URI to avoid duplicates
195
+ grouped_unmatched = {}
196
+
197
+ unmatched_ttl.each do |entity|
198
+ uri = entity[:uri]
199
+ grouped_unmatched[uri] = entity unless grouped_unmatched.key?(uri)
200
+ end
201
+
202
+ rows = []
203
+ unique_entities = grouped_unmatched.values
204
+
205
+ unique_entities.each do |entity|
206
+ # Create the SI TTL row
207
+ si_suffix = SiTtlParser.extract_identifying_suffix(entity[:uri])
208
+ ttl_row = ["SI TTL: #{si_suffix}",
209
+ "(#{entity[:label] || entity[:name] || 'unnamed'})"]
210
+
211
+ rows << ttl_row
212
+ rows << [
213
+ "Status: No matching UnitsDB entity",
214
+ "?",
215
+ ]
216
+ rows << :separator unless entity == unique_entities.last
217
+ end
218
+
219
+ table = Terminal::Table.new(
220
+ title: "Unmapped SI Entities",
221
+ rows: rows,
222
+ )
223
+ puts table
224
+ end
225
+ end
226
+
227
+ # Display DB → TTL results
228
+ def display_db_results(entity_type, matches, missing_refs, unmatched_db)
229
+ puts "\n=== Summary of database entities referencing SI ==="
230
+ puts "#{entity_type.capitalize} with SI references: #{matches.size}"
231
+ puts "#{entity_type.capitalize} missing SI references: #{missing_refs.size}"
232
+ puts "Database #{entity_type} not matching any SI entity: #{unmatched_db.size}"
233
+
234
+ # Show entities with valid references
235
+ unless matches.empty?
236
+ puts "\n=== #{entity_type.capitalize} with SI references ==="
237
+ rows = []
238
+ matches.each do |match|
239
+ db_entity = match[:db_entity]
240
+ entity_id = match[:entity_id] || db_entity.short
241
+ entity_name = db_entity.respond_to?(:names) ? db_entity.names&.first : "unnamed"
242
+ si_suffix = SiTtlParser.extract_identifying_suffix(match[:ttl_uri])
243
+
244
+ ttl_label = match[:ttl_entity] ? (match[:ttl_entity][:label] || match[:ttl_entity][:name]) : "Unknown"
245
+
246
+ rows << [
247
+ "UnitsDB: #{entity_id}",
248
+ "(#{entity_name})",
249
+ ]
250
+ rows << [
251
+ "SI TTL: #{si_suffix}",
252
+ "(#{ttl_label})",
253
+ ]
254
+ rows << :separator unless match == matches.last
255
+ end
256
+
257
+ table = Terminal::Table.new(
258
+ title: "Valid SI References",
259
+ rows: rows,
260
+ )
261
+ puts table
262
+ end
263
+
264
+ puts "\n=== #{entity_type.capitalize} that should reference SI ==="
265
+ if missing_refs.empty?
266
+ puts "None"
267
+ else
268
+ # Split missing_refs into exact and potential matches
269
+ exact_matches = []
270
+ potential_matches = []
271
+
272
+ missing_refs.each do |match|
273
+ # Determine match type
274
+ ttl_entities = match[:ttl_entities]
275
+ uri = ttl_entities.first[:uri]
276
+ match_type = "Exact match" # Default
277
+ match_type = match[:match_types][uri] if match[:match_types] && match[:match_types][uri]
278
+
279
+ # Get match description if available
280
+ entity_id = match[:db_entity].short
281
+ match_pair_key = "#{entity_id}:#{ttl_entities.first[:uri]}"
282
+ match_details = Unitsdb::Commands::CheckSi::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
283
+ match_desc = match_details[:match_desc] if match_details && match_details[:match_desc]
284
+
285
+ # Symbol matches and partial matches should always be potential matches
286
+ if %w[symbol_match partial_match].include?(match_desc)
287
+ potential_matches << match
288
+ elsif match_type == "Exact match"
289
+ exact_matches << match
290
+ else
291
+ potential_matches << match
292
+ end
293
+ end
294
+
295
+ # Display exact matches
296
+ puts "\n=== Exact Matches (#{exact_matches.size}) ==="
297
+ if exact_matches.empty?
298
+ puts "None"
299
+ else
300
+ rows = []
301
+ exact_matches.each do |match|
302
+ db_entity = match[:db_entity]
303
+ entity_id = match[:entity_id] || db_entity.short
304
+ entity_name = db_entity.respond_to?(:names) ? db_entity.names&.first : "unnamed"
305
+
306
+ # Handle multiple TTL entities in a single row
307
+ ttl_entities = match[:ttl_entities]
308
+ if ttl_entities.size == 1
309
+ # Single TTL entity
310
+ ttl_entity = ttl_entities.first
311
+ si_suffix = SiTtlParser.extract_identifying_suffix(ttl_entity[:uri])
312
+
313
+ rows << [
314
+ "UnitsDB: #{entity_id}",
315
+ "(#{entity_name})",
316
+ ]
317
+ rows << [
318
+ "SI TTL: #{si_suffix}",
319
+ "(#{ttl_entity[:label] || ttl_entity[:name] || 'unnamed'})",
320
+ ]
321
+ else
322
+ # Multiple TTL entities, combine them - ensure no duplicates
323
+ si_text_parts = []
324
+ seen_uris = {}
325
+
326
+ ttl_entities.each do |ttl_entity|
327
+ uri = ttl_entity[:uri]
328
+ next if seen_uris[uri] # Skip if we've already seen this URI
329
+
330
+ seen_uris[uri] = true
331
+
332
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
333
+ si_text_parts << "#{suffix} (#{ttl_entity[:label] || ttl_entity[:name] || 'unnamed'})"
334
+ end
335
+
336
+ si_text = si_text_parts.join(", ")
337
+
338
+ rows << [
339
+ "UnitsDB: #{entity_id}",
340
+ "(#{entity_name})",
341
+ ]
342
+ rows << [
343
+ "SI TTL: #{si_text}",
344
+ "",
345
+ ]
346
+ end
347
+
348
+ # Get match details for this match
349
+ match_pair_key = "#{db_entity.short}:#{ttl_entities.first[:uri]}"
350
+ match_details = Unitsdb::Commands::CheckSi::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
351
+
352
+ # Format match info
353
+ match_info = ""
354
+ match_info = format_match_info(match_details[:match_desc]) if match_details
355
+
356
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference (#{match_info})"
357
+ rows << [
358
+ "Status: #{status_text}",
359
+ "✗",
360
+ ]
361
+ rows << :separator unless match == exact_matches.last
362
+ end
363
+
364
+ table = Terminal::Table.new(
365
+ title: "Exact Match Missing SI References",
366
+ rows: rows,
367
+ )
368
+ puts table
369
+ end
370
+
371
+ # Display potential matches
372
+ puts "\n=== Potential Matches (#{potential_matches.size}) ==="
373
+ if potential_matches.empty?
374
+ puts "None"
375
+ else
376
+ rows = []
377
+ potential_matches.each do |match|
378
+ db_entity = match[:db_entity]
379
+ entity_id = match[:entity_id] || db_entity.short
380
+ entity_name = db_entity.respond_to?(:names) ? db_entity.names&.first : "unnamed"
381
+
382
+ # Handle multiple TTL entities in a single row
383
+ ttl_entities = match[:ttl_entities]
384
+ if ttl_entities.size == 1
385
+ # Single TTL entity
386
+ ttl_entity = ttl_entities.first
387
+ si_suffix = SiTtlParser.extract_identifying_suffix(ttl_entity[:uri])
388
+
389
+ rows << [
390
+ "UnitsDB: #{entity_id}",
391
+ "(#{entity_name})",
392
+ ]
393
+ rows << [
394
+ "SI TTL: #{si_suffix}",
395
+ "(#{ttl_entity[:label] || ttl_entity[:name] || 'unnamed'})",
396
+ ]
397
+ else
398
+ # Multiple TTL entities, combine them - ensure no duplicates
399
+ si_text_parts = []
400
+ seen_uris = {}
401
+
402
+ ttl_entities.each do |ttl_entity|
403
+ uri = ttl_entity[:uri]
404
+ next if seen_uris[uri] # Skip if we've already seen this URI
405
+
406
+ seen_uris[uri] = true
407
+
408
+ suffix = SiTtlParser.extract_identifying_suffix(uri)
409
+ si_text_parts << "#{suffix} (#{ttl_entity[:label] || ttl_entity[:name] || 'unnamed'})"
410
+ end
411
+
412
+ si_text = si_text_parts.join(", ")
413
+
414
+ rows << [
415
+ "UnitsDB: #{entity_id}",
416
+ "(#{entity_name})",
417
+ ]
418
+ rows << [
419
+ "SI TTL: #{si_text}",
420
+ "",
421
+ ]
422
+ end
423
+
424
+ # Get match details
425
+ match_pair_key = "#{db_entity.short}:#{ttl_entities.first[:uri]}"
426
+ match_details = Unitsdb::Commands::CheckSi::SiMatcher.instance_variable_get(:@match_details)&.dig(match_pair_key)
427
+
428
+ # Format match info
429
+ match_info = ""
430
+ match_info = format_match_info(match_details[:match_desc]) if match_details
431
+
432
+ status_text = match_info.empty? ? "Missing reference" : "Missing reference (#{match_info})"
433
+ rows << [
434
+ "Status: #{status_text}",
435
+ "✗",
436
+ ]
437
+ rows << :separator unless match == potential_matches.last
438
+ end
439
+
440
+ table = Terminal::Table.new(
441
+ title: "Potential Match Missing SI References",
442
+ rows: rows,
443
+ )
444
+ puts table
445
+ end
446
+ end
447
+ end
448
+
449
+ # Print direction header
450
+ def print_direction_header(direction)
451
+ case direction
452
+ when "SI → UnitsDB"
453
+ puts "\n=== Checking SI → UnitsDB (TTL entities referenced by database) ==="
454
+ when "UnitsDB → SI"
455
+ puts "\n=== Checking UnitsDB → SI (database entities referencing TTL) ==="
456
+ end
457
+
458
+ puts "\n=== Instructions for #{direction} direction ==="
459
+ case direction
460
+ when "SI → UnitsDB"
461
+ puts "If you are the UnitsDB Register Manager, please ensure that all SI entities have proper references in the UnitsDB database."
462
+ puts "For each missing reference, add a reference with the appropriate URI and 'authority: \"si-digital-framework\"'."
463
+ when "UnitsDB → SI"
464
+ puts "If you are the UnitsDB Register Manager, please add SI references to UnitsDB entities that should have them."
465
+ puts "For each entity that should reference SI, add a reference with 'authority: \"si-digital-framework\"' and the SI TTL URI."
466
+ end
467
+ end
468
+
469
+ def set_match_details(details)
470
+ @match_details = details
471
+ end
472
+
473
+ # Format match info for display
474
+ def format_match_info(match_desc)
475
+ {
476
+ "short_to_name" => "short → name",
477
+ "short_to_label" => "short → label",
478
+ "name_to_name" => "name → name",
479
+ "name_to_label" => "name → label",
480
+ "name_to_alt_label" => "name → alt_label",
481
+ "symbol_match" => "symbol → symbol",
482
+ "partial_match" => "partial match",
483
+ }[match_desc] || ""
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end