fontisan 0.2.4 → 0.2.5

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +150 -30
  3. data/README.adoc +497 -242
  4. data/lib/fontisan/cli.rb +67 -6
  5. data/lib/fontisan/commands/validate_command.rb +107 -151
  6. data/lib/fontisan/converters/woff2_encoder.rb +7 -29
  7. data/lib/fontisan/models/validation_report.rb +227 -0
  8. data/lib/fontisan/pipeline/transformation_pipeline.rb +4 -8
  9. data/lib/fontisan/tables/cmap.rb +82 -2
  10. data/lib/fontisan/tables/glyf.rb +118 -0
  11. data/lib/fontisan/tables/head.rb +60 -0
  12. data/lib/fontisan/tables/hhea.rb +74 -0
  13. data/lib/fontisan/tables/maxp.rb +60 -0
  14. data/lib/fontisan/tables/name.rb +76 -0
  15. data/lib/fontisan/tables/os2.rb +113 -0
  16. data/lib/fontisan/tables/post.rb +57 -0
  17. data/lib/fontisan/validators/basic_validator.rb +85 -0
  18. data/lib/fontisan/validators/font_book_validator.rb +130 -0
  19. data/lib/fontisan/validators/opentype_validator.rb +112 -0
  20. data/lib/fontisan/validators/profile_loader.rb +139 -0
  21. data/lib/fontisan/validators/validator.rb +484 -0
  22. data/lib/fontisan/validators/web_font_validator.rb +102 -0
  23. data/lib/fontisan/version.rb +1 -1
  24. data/lib/fontisan.rb +78 -6
  25. metadata +7 -11
  26. data/lib/fontisan/config/validation_rules.yml +0 -149
  27. data/lib/fontisan/validation/checksum_validator.rb +0 -170
  28. data/lib/fontisan/validation/consistency_validator.rb +0 -197
  29. data/lib/fontisan/validation/structure_validator.rb +0 -198
  30. data/lib/fontisan/validation/table_validator.rb +0 -158
  31. data/lib/fontisan/validation/validator.rb +0 -152
  32. data/lib/fontisan/validation/variable_font_validator.rb +0 -218
  33. data/lib/fontisan/validation/woff2_header_validator.rb +0 -278
  34. data/lib/fontisan/validation/woff2_table_validator.rb +0 -270
  35. data/lib/fontisan/validation/woff2_validator.rb +0 -248
@@ -1,197 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Validation
5
- # ConsistencyValidator validates cross-table consistency
6
- #
7
- # This validator ensures that references between tables are valid,
8
- # such as cmap glyph references, hmtx entry counts, and variable
9
- # font table consistency.
10
- #
11
- # Single Responsibility: Cross-table data consistency validation
12
- #
13
- # @example Validating consistency
14
- # validator = ConsistencyValidator.new(rules)
15
- # issues = validator.validate(font)
16
- class ConsistencyValidator
17
- # Initialize consistency validator
18
- #
19
- # @param rules [Hash] Validation rules configuration
20
- def initialize(rules)
21
- @rules = rules
22
- @consistency_config = rules["consistency_checks"] || {}
23
- end
24
-
25
- # Validate font consistency
26
- #
27
- # @param font [TrueTypeFont, OpenTypeFont] The font to validate
28
- # @return [Array<Hash>] Array of validation issues
29
- def validate(font)
30
- issues = []
31
-
32
- # Check hmtx consistency if enabled
33
- issues.concat(check_hmtx_consistency(font)) if should_check?("check_hmtx_consistency")
34
-
35
- # Check name table consistency if enabled
36
- issues.concat(check_name_consistency(font)) if should_check?("check_name_consistency")
37
-
38
- # Check variable font consistency if enabled
39
- issues.concat(check_variable_consistency(font)) if should_check?("check_variable_consistency")
40
-
41
- issues
42
- end
43
-
44
- private
45
-
46
- # Check if a validation should be performed
47
- #
48
- # @param check_name [String] The check name
49
- # @return [Boolean] true if check should be performed
50
- def should_check?(check_name)
51
- @rules.dig("validation_levels", "standard", check_name)
52
- end
53
-
54
- # Check hmtx entry count matches glyph count
55
- #
56
- # @param font [TrueTypeFont, OpenTypeFont] The font
57
- # @return [Array<Hash>] Array of hmtx consistency issues
58
- def check_hmtx_consistency(font)
59
- issues = []
60
-
61
- hmtx = font.table(Constants::HMTX_TAG)
62
- maxp = font.table(Constants::MAXP_TAG)
63
- hhea = font.table(Constants::HHEA_TAG)
64
-
65
- return issues unless hmtx && maxp && hhea
66
-
67
- glyph_count = maxp.num_glyphs
68
- num_of_long_hor_metrics = hhea.number_of_h_metrics
69
-
70
- # Verify the structure makes sense
71
- if num_of_long_hor_metrics > glyph_count
72
- issues << {
73
- severity: "error",
74
- category: "consistency",
75
- message: "hhea number_of_h_metrics (#{num_of_long_hor_metrics}) exceeds glyph count (#{glyph_count})",
76
- location: "hhea/hmtx tables",
77
- }
78
- end
79
-
80
- if num_of_long_hor_metrics < 1
81
- issues << {
82
- severity: "error",
83
- category: "consistency",
84
- message: "hhea number_of_h_metrics is #{num_of_long_hor_metrics}, must be at least 1",
85
- location: "hhea table",
86
- }
87
- end
88
-
89
- issues
90
- end
91
-
92
- # Check name table for consistency issues
93
- #
94
- # @param font [TrueTypeFont, OpenTypeFont] The font
95
- # @return [Array<Hash>] Array of name table issues
96
- def check_name_consistency(font)
97
- issues = []
98
-
99
- name = font.table(Constants::NAME_TAG)
100
- return issues unless name
101
-
102
- # Check that required name IDs are present
103
- required_name_ids = [
104
- Tables::Name::FAMILY, # 1
105
- Tables::Name::SUBFAMILY, # 2
106
- Tables::Name::FULL_NAME, # 4
107
- Tables::Name::VERSION, # 5
108
- Tables::Name::POSTSCRIPT_NAME, # 6
109
- ]
110
-
111
- required_name_ids.each do |name_id|
112
- has_entry = name.name_records.any? do |record|
113
- record.name_id == name_id
114
- end
115
- unless has_entry
116
- issues << {
117
- severity: "warning",
118
- category: "consistency",
119
- message: "Missing recommended name ID #{name_id}",
120
- location: "name table",
121
- }
122
- end
123
- end
124
-
125
- # Check for duplicate entries (same platform/encoding/language/nameID)
126
- seen = {}
127
- name.name_records.each do |record|
128
- key = [record.platform_id, record.encoding_id, record.language_id,
129
- record.name_id]
130
- if seen[key]
131
- issues << {
132
- severity: "warning",
133
- category: "consistency",
134
- message: "Duplicate name record: platform=#{record.platform_id}, encoding=#{record.encoding_id}, language=#{record.language_id}, nameID=#{record.name_id}",
135
- location: "name table",
136
- }
137
- end
138
- seen[key] = true
139
- end
140
-
141
- issues
142
- end
143
-
144
- # Check variable font table consistency
145
- #
146
- # @param font [TrueTypeFont, OpenTypeFont] The font
147
- # @return [Array<Hash>] Array of variable font issues
148
- def check_variable_consistency(font)
149
- issues = []
150
-
151
- # Only check if this is a variable font
152
- return issues unless font.has_table?(Constants::FVAR_TAG)
153
-
154
- fvar = font.table(Constants::FVAR_TAG)
155
- return issues unless fvar
156
-
157
- axis_count = fvar.axes.length
158
-
159
- # For TrueType variable fonts, check gvar consistency
160
- if font.has_table?(Constants::GVAR_TAG)
161
- gvar = font.table(Constants::GVAR_TAG)
162
- gvar_axis_count = gvar.axis_count
163
- if gvar_axis_count != axis_count
164
- issues << {
165
- severity: "error",
166
- category: "consistency",
167
- message: "fvar axis count (#{axis_count}) doesn't match gvar axis count (#{gvar_axis_count})",
168
- location: "fvar/gvar tables",
169
- }
170
- end
171
- end
172
-
173
- # Check that recommended variation tables are present
174
- unless font.has_table?(Constants::GVAR_TAG) || font.has_table?(Constants::CFF2_TAG)
175
- issues << {
176
- severity: "error",
177
- category: "consistency",
178
- message: "Variable font missing gvar (TrueType) or CFF2 (CFF) table",
179
- location: "variable font",
180
- }
181
- end
182
-
183
- # Check for recommended metrics variation tables
184
- unless font.has_table?(Constants::HVAR_TAG)
185
- issues << {
186
- severity: "info",
187
- category: "consistency",
188
- message: "Variable font missing HVAR table (recommended for better rendering)",
189
- location: nil,
190
- }
191
- end
192
-
193
- issues
194
- end
195
- end
196
- end
197
- end
@@ -1,198 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Fontisan
4
- module Validation
5
- # StructureValidator validates the structural integrity of fonts
6
- #
7
- # This validator checks the SFNT structure, table offsets, table ordering,
8
- # and other structural properties that ensure the font file is well-formed.
9
- #
10
- # Single Responsibility: Font structure and SFNT format validation
11
- #
12
- # @example Validating structure
13
- # validator = StructureValidator.new(rules)
14
- # issues = validator.validate(font)
15
- class StructureValidator
16
- # Initialize structure validator
17
- #
18
- # @param rules [Hash] Validation rules configuration
19
- def initialize(rules)
20
- @rules = rules
21
- @structure_config = rules["structure_validation"] || {}
22
- end
23
-
24
- # Validate font structure
25
- #
26
- # @param font [TrueTypeFont, OpenTypeFont] The font to validate
27
- # @return [Array<Hash>] Array of validation issues
28
- def validate(font)
29
- issues = []
30
-
31
- # Check glyph count consistency
32
- issues.concat(check_glyph_consistency(font))
33
-
34
- # Check table offsets
35
- issues.concat(check_table_offsets(font)) if @rules.dig(
36
- "validation_levels", "standard", "check_table_offsets"
37
- )
38
-
39
- # Check table ordering (optional optimization check)
40
- issues.concat(check_table_ordering(font)) if @rules.dig(
41
- "validation_levels", "standard", "check_table_ordering"
42
- )
43
-
44
- issues
45
- end
46
-
47
- private
48
-
49
- # Check glyph count consistency across tables
50
- #
51
- # @param font [TrueTypeFont, OpenTypeFont] The font
52
- # @return [Array<Hash>] Array of consistency issues
53
- def check_glyph_consistency(font)
54
- issues = []
55
-
56
- # Get glyph count from maxp table
57
- maxp = font.table(Constants::MAXP_TAG)
58
- return issues unless maxp
59
-
60
- expected_count = maxp.num_glyphs
61
-
62
- # For TrueType fonts, check glyf table glyph count
63
- if font.has_table?(Constants::GLYF_TAG)
64
- glyf = font.table(Constants::GLYF_TAG)
65
- actual_count = glyf.glyphs.length if glyf.respond_to?(:glyphs)
66
-
67
- if actual_count && actual_count != expected_count
68
- issues << {
69
- severity: "error",
70
- category: "structure",
71
- message: "Glyph count mismatch: maxp=#{expected_count}, glyf=#{actual_count}",
72
- location: "glyf table",
73
- }
74
- end
75
- end
76
-
77
- # Check glyph count bounds with safe defaults
78
- min_glyph_count = @structure_config["min_glyph_count"] || 1
79
- max_glyph_count = @structure_config["max_glyph_count"] || 65536
80
-
81
- if expected_count < min_glyph_count
82
- issues << {
83
- severity: "error",
84
- category: "structure",
85
- message: "Glyph count (#{expected_count}) below minimum (#{min_glyph_count})",
86
- location: "maxp table",
87
- }
88
- end
89
-
90
- if expected_count > max_glyph_count
91
- issues << {
92
- severity: "error",
93
- category: "structure",
94
- message: "Glyph count (#{expected_count}) exceeds maximum (#{max_glyph_count})",
95
- location: "maxp table",
96
- }
97
- end
98
-
99
- issues
100
- end
101
-
102
- # Check that table offsets are valid
103
- #
104
- # @param font [TrueTypeFont, OpenTypeFont] The font
105
- # @return [Array<Hash>] Array of offset issues
106
- def check_table_offsets(font)
107
- issues = []
108
-
109
- min_offset = @structure_config["min_table_offset"] || 12
110
- max_size = @structure_config["max_table_size"] || 104857600
111
-
112
- font.tables.each do |table_entry|
113
- tag = table_entry.tag
114
- offset = table_entry.offset
115
- length = table_entry.table_length
116
-
117
- # Check minimum offset
118
- if offset < min_offset
119
- issues << {
120
- severity: "error",
121
- category: "structure",
122
- message: "Table '#{tag}' has invalid offset: #{offset} (minimum: #{min_offset})",
123
- location: "#{tag} table directory",
124
- }
125
- end
126
-
127
- # Check for reasonable table size
128
- if length > max_size
129
- issues << {
130
- severity: "warning",
131
- category: "structure",
132
- message: "Table '#{tag}' has unusually large size: #{length} bytes",
133
- location: "#{tag} table",
134
- }
135
- end
136
-
137
- # Check alignment (tables should be 4-byte aligned)
138
- alignment = @structure_config["table_alignment"] || 4
139
- if offset % alignment != 0
140
- issues << {
141
- severity: "warning",
142
- category: "structure",
143
- message: "Table '#{tag}' is not #{alignment}-byte aligned (offset: #{offset})",
144
- location: "#{tag} table directory",
145
- }
146
- end
147
- end
148
-
149
- issues
150
- end
151
-
152
- # Check table ordering (optimization check, not critical)
153
- #
154
- # @param font [TrueTypeFont, OpenTypeFont] The font
155
- # @return [Array<Hash>] Array of ordering issues
156
- def check_table_ordering(font)
157
- issues = []
158
-
159
- # Recommended table order for optimal loading
160
- recommended_order = [
161
- Constants::HEAD_TAG,
162
- Constants::HHEA_TAG,
163
- Constants::MAXP_TAG,
164
- Constants::OS2_TAG,
165
- Constants::NAME_TAG,
166
- Constants::CMAP_TAG,
167
- Constants::POST_TAG,
168
- Constants::GLYF_TAG,
169
- Constants::LOCA_TAG,
170
- Constants::HMTX_TAG,
171
- ]
172
-
173
- # Get actual table order
174
- actual_order = font.table_names
175
-
176
- # Check if critical tables are in recommended order
177
- critical_tables = recommended_order.take(7) # head through post
178
- actual_critical = actual_order.select do |tag|
179
- critical_tables.include?(tag)
180
- end
181
- expected_critical = critical_tables.select do |tag|
182
- actual_order.include?(tag)
183
- end
184
-
185
- if actual_critical != expected_critical
186
- issues << {
187
- severity: "info",
188
- category: "structure",
189
- message: "Tables not in optimal order for performance",
190
- location: nil,
191
- }
192
- end
193
-
194
- issues
195
- end
196
- end
197
- end
198
- end
@@ -1,158 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
-
5
- module Fontisan
6
- module Validation
7
- # TableValidator validates the presence and correctness of font tables
8
- #
9
- # This validator checks that all required tables are present in the font
10
- # based on the font type (TrueType, OpenType/CFF, Variable) and validates
11
- # table-specific properties like versioning.
12
- #
13
- # Single Responsibility: Table presence and table-level validation
14
- #
15
- # @example Validating tables
16
- # validator = TableValidator.new(rules)
17
- # issues = validator.validate(font)
18
- class TableValidator
19
- # Initialize table validator
20
- #
21
- # @param rules [Hash] Validation rules configuration
22
- def initialize(rules)
23
- @rules = rules
24
- @required_tables = rules["required_tables"]
25
- end
26
-
27
- # Validate font tables
28
- #
29
- # @param font [TrueTypeFont, OpenTypeFont] The font to validate
30
- # @return [Array<Hash>] Array of validation issues
31
- def validate(font)
32
- issues = []
33
-
34
- # Determine font type
35
- font_type = determine_font_type(font)
36
-
37
- # Check required tables based on font type
38
- issues.concat(check_required_tables(font, font_type))
39
-
40
- # Check table-specific validations if tables exist
41
- issues.concat(check_table_versions(font)) if @rules["check_table_versions"]
42
-
43
- issues
44
- end
45
-
46
- private
47
-
48
- # Determine the font type
49
- #
50
- # @param font [TrueTypeFont, OpenTypeFont] The font
51
- # @return [Symbol] :truetype, :opentype_cff, or :variable
52
- def determine_font_type(font)
53
- if font.has_table?(Constants::FVAR_TAG)
54
- :variable
55
- elsif font.has_table?(Constants::CFF_TAG) || font.has_table?("CFF2")
56
- :opentype_cff
57
- else
58
- :truetype
59
- end
60
- end
61
-
62
- # Check that required tables are present
63
- #
64
- # @param font [TrueTypeFont, OpenTypeFont] The font
65
- # @param font_type [Symbol] The font type
66
- # @return [Array<Hash>] Array of missing table issues
67
- def check_required_tables(font, font_type)
68
- issues = []
69
-
70
- # Get required tables for this font type
71
- required = @required_tables["all"].dup
72
-
73
- case font_type
74
- when :truetype
75
- required.concat(@required_tables["truetype"])
76
- when :opentype_cff
77
- required.concat(@required_tables["opentype_cff"])
78
- when :variable
79
- required.concat(@required_tables["variable"])
80
- end
81
-
82
- # Check each required table
83
- required.each do |table_tag|
84
- next if font.has_table?(table_tag)
85
-
86
- # Special case: CFF or CFF2 are alternatives
87
- if (table_tag == Constants::CFF_TAG) && font.has_table?("CFF2")
88
- next
89
- end
90
-
91
- issues << {
92
- severity: "error",
93
- category: "tables",
94
- message: "Missing required table: #{table_tag}",
95
- location: nil,
96
- }
97
- end
98
-
99
- issues
100
- end
101
-
102
- # Check table version compatibility
103
- #
104
- # @param font [TrueTypeFont, OpenTypeFont] The font
105
- # @return [Array<Hash>] Array of version issues
106
- def check_table_versions(font)
107
- issues = []
108
-
109
- # Check head table version
110
- if font.has_table?(Constants::HEAD_TAG)
111
- head = font.table(Constants::HEAD_TAG)
112
- unless valid_head_version?(head)
113
- issues << {
114
- severity: "warning",
115
- category: "tables",
116
- message: "Unsupported head table version: #{head.major_version}.#{head.minor_version}",
117
- location: Constants::HEAD_TAG,
118
- }
119
- end
120
- end
121
-
122
- # Check maxp table version
123
- if font.has_table?(Constants::MAXP_TAG)
124
- maxp = font.table(Constants::MAXP_TAG)
125
- unless valid_maxp_version?(maxp)
126
- issues << {
127
- severity: "warning",
128
- category: "tables",
129
- message: "Unsupported maxp table version: #{maxp.version}",
130
- location: Constants::MAXP_TAG,
131
- }
132
- end
133
- end
134
-
135
- issues
136
- end
137
-
138
- # Check if head table version is valid
139
- #
140
- # @param head [Tables::Head] The head table
141
- # @return [Boolean] true if version is valid
142
- def valid_head_version?(head)
143
- # Head table version should be 1.0
144
- head.major_version == 1 && head.minor_version.zero?
145
- end
146
-
147
- # Check if maxp table version is valid
148
- #
149
- # @param maxp [Tables::Maxp] The maxp table
150
- # @return [Boolean] true if version is valid
151
- def valid_maxp_version?(maxp)
152
- # Version 0.5 for CFF fonts, 1.0 for TrueType fonts
153
- version = maxp.version
154
- [0x00005000, 0x00010000].include?(version)
155
- end
156
- end
157
- end
158
- end
@@ -1,152 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
- require_relative "../models/validation_report"
5
- require_relative "table_validator"
6
- require_relative "structure_validator"
7
- require_relative "consistency_validator"
8
- require_relative "checksum_validator"
9
-
10
- module Fontisan
11
- module Validation
12
- # Validator is the main orchestrator for font validation
13
- #
14
- # This class coordinates all validation checks (tables, structure,
15
- # consistency, checksums) and produces a comprehensive ValidationReport.
16
- #
17
- # Single Responsibility: Orchestration of validation workflow
18
- #
19
- # @example Validating a font
20
- # validator = Validator.new(level: :standard)
21
- # report = validator.validate(font, font_path)
22
- # puts report.text_summary
23
- class Validator
24
- # Validation levels
25
- LEVELS = %i[strict standard lenient].freeze
26
-
27
- # Initialize validator
28
- #
29
- # @param level [Symbol] Validation level (:strict, :standard, :lenient)
30
- # @param rules_path [String, nil] Path to custom rules file
31
- def initialize(level: :standard, rules_path: nil)
32
- @level = level
33
- validate_level!
34
-
35
- @rules = load_rules(rules_path)
36
- @table_validator = TableValidator.new(@rules)
37
- @structure_validator = StructureValidator.new(@rules)
38
- @consistency_validator = ConsistencyValidator.new(@rules)
39
- @checksum_validator = ChecksumValidator.new(@rules)
40
- end
41
-
42
- # Validate a font
43
- #
44
- # @param font [TrueTypeFont, OpenTypeFont] The font to validate
45
- # @param font_path [String] Path to the font file
46
- # @return [Models::ValidationReport] Validation report
47
- def validate(font, font_path)
48
- report = Models::ValidationReport.new(
49
- font_path: font_path,
50
- valid: true,
51
- )
52
-
53
- begin
54
- # Run all validation checks
55
- all_issues = []
56
-
57
- # 1. Table validation
58
- all_issues.concat(@table_validator.validate(font))
59
-
60
- # 2. Structure validation
61
- all_issues.concat(@structure_validator.validate(font))
62
-
63
- # 3. Consistency validation
64
- all_issues.concat(@consistency_validator.validate(font))
65
-
66
- # 4. Checksum validation (requires file path)
67
- all_issues.concat(@checksum_validator.validate(font, font_path))
68
-
69
- # Add issues to report
70
- all_issues.each do |issue|
71
- case issue[:severity]
72
- when "error"
73
- report.add_error(issue[:category], issue[:message],
74
- issue[:location])
75
- when "warning"
76
- report.add_warning(issue[:category], issue[:message],
77
- issue[:location])
78
- when "info"
79
- report.add_info(issue[:category], issue[:message],
80
- issue[:location])
81
- end
82
- end
83
-
84
- # Determine overall validity based on level
85
- report.valid = determine_validity(report)
86
- rescue StandardError => e
87
- report.add_error("validation", "Validation failed: #{e.message}", nil)
88
- report.valid = false
89
- end
90
-
91
- report
92
- end
93
-
94
- # Get the current validation level
95
- #
96
- # @return [Symbol] The validation level
97
- attr_reader :level
98
-
99
- private
100
-
101
- # Validate that the level is supported
102
- #
103
- # @raise [ArgumentError] if level is invalid
104
- # @return [void]
105
- def validate_level!
106
- unless LEVELS.include?(@level)
107
- raise ArgumentError,
108
- "Invalid validation level: #{@level}. Must be one of: #{LEVELS.join(', ')}"
109
- end
110
- end
111
-
112
- # Load validation rules
113
- #
114
- # @param rules_path [String, nil] Path to custom rules file
115
- # @return [Hash] The rules configuration
116
- def load_rules(rules_path)
117
- path = rules_path || default_rules_path
118
- YAML.load_file(path)
119
- rescue Errno::ENOENT
120
- raise "Validation rules file not found: #{path}"
121
- rescue Psych::SyntaxError => e
122
- raise "Invalid validation rules YAML: #{e.message}"
123
- end
124
-
125
- # Get the default rules path
126
- #
127
- # @return [String] Path to default rules file
128
- def default_rules_path
129
- File.join(__dir__, "..", "config", "validation_rules.yml")
130
- end
131
-
132
- # Determine if font is valid based on validation level
133
- #
134
- # @param report [Models::ValidationReport] The validation report
135
- # @return [Boolean] true if font is valid for the given level
136
- def determine_validity(report)
137
- case @level
138
- when :strict
139
- # Strict: no errors, no warnings
140
- !report.has_errors? && !report.has_warnings?
141
- when :standard
142
- # Standard: no errors (warnings allowed)
143
- !report.has_errors?
144
- when :lenient
145
- # Lenient: no critical errors (some errors may be acceptable)
146
- # For now, treat lenient same as standard
147
- !report.has_errors?
148
- end
149
- end
150
- end
151
- end
152
- end