svg_conform 0.1.9 → 0.1.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e4879f5648a06585ca68ae1430f928c327b267c3de96e92ae54333a002127243
4
- data.tar.gz: 7fb876639e3d0d935020089981803d43e2648131bf7a074b0b127605c99009ff
3
+ metadata.gz: 9300ab0026e52b929e0707e14c19ce45394da831dfd5e44f4afad14e1a2237cd
4
+ data.tar.gz: b19b0d2dbce1397abb478105704c994a32f418972240c9c149d375d8be519e2c
5
5
  SHA512:
6
- metadata.gz: ca32f36fb1bcb98fc25be20fc237291f5fa0efedd55c71cb62b8862e60c3628f05673a0d16def9deda237e4a3b53312c78e39d3daf2bb42700e58f388a2d38c8
7
- data.tar.gz: '0086cffd30f8c81aba4e157caf6a0bfe197f4ce597971941e4ddcc136f0ac7cf11460eb8e4103094a6636aa546c5d687c0f724697e441c509a94e1697bc41857'
6
+ metadata.gz: d413baac7ebb781837ff482103d751ce3222c170c613707b8411d5c6614dff71ac874802e0e5df14e276c0d56f537d32259787559359dfb954f6e3392088102d
7
+ data.tar.gz: ca1a62500f77534fca87d5b0e0136328ed81b9d9a7a81510e312a3760f31157e7a42af4934bacda3af2d6915b675bb9f4afdba2746332922cb23351bcc47fba2
data/.gitignore CHANGED
@@ -10,3 +10,7 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
  svgcheck-reference/
13
+
14
+ .kilocode
15
+ .claude
16
+
data/.rubocop_todo.yml CHANGED
@@ -1,11 +1,19 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-01-22 08:33:31 UTC using RuboCop version 1.82.1.
3
+ # on 2026-01-27 00:27:35 UTC using RuboCop version 1.82.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ # Offense count: 1
10
+ # This cop supports safe autocorrection (--autocorrect).
11
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
12
+ # SupportedStyles: with_first_argument, with_fixed_indentation
13
+ Layout/ArgumentAlignment:
14
+ Exclude:
15
+ - 'lib/svg_conform/requirements/invalid_id_references_requirement.rb'
16
+
9
17
  # Offense count: 1
10
18
  # This cop supports safe autocorrection (--autocorrect).
11
19
  # Configuration parameters: EnforcedStyleAlignWith.
@@ -14,7 +22,17 @@ Layout/BlockAlignment:
14
22
  Exclude:
15
23
  - 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
16
24
 
17
- # Offense count: 647
25
+ # Offense count: 1
26
+ # This cop supports safe autocorrection (--autocorrect).
27
+ # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
28
+ # SupportedHashRocketStyles: key, separator, table
29
+ # SupportedColonStyles: key, separator, table
30
+ # SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
31
+ Layout/HashAlignment:
32
+ Exclude:
33
+ - 'spec/svg_conform/validation_context_spec.rb'
34
+
35
+ # Offense count: 662
18
36
  # This cop supports safe autocorrection (--autocorrect).
19
37
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
20
38
  # URISchemes: http, https
@@ -22,6 +40,14 @@ Layout/LineLength:
22
40
  Enabled: false
23
41
 
24
42
  # Offense count: 2
43
+ # This cop supports safe autocorrection (--autocorrect).
44
+ # Configuration parameters: AllowInHeredoc.
45
+ Layout/TrailingWhitespace:
46
+ Exclude:
47
+ - 'lib/svg_conform/requirements/invalid_id_references_requirement.rb'
48
+ - 'spec/svg_conform/validation_context_spec.rb'
49
+
50
+ # Offense count: 3
25
51
  # Configuration parameters: AllowedMethods.
26
52
  # AllowedMethods: enums
27
53
  Lint/ConstantDefinitionInBlock:
@@ -70,7 +96,7 @@ Lint/UnreachableCode:
70
96
  Exclude:
71
97
  - 'lib/svg_conform/commands/check.rb'
72
98
 
73
- # Offense count: 151
99
+ # Offense count: 146
74
100
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
75
101
  Metrics/AbcSize:
76
102
  Enabled: false
@@ -81,17 +107,17 @@ Metrics/AbcSize:
81
107
  Metrics/BlockLength:
82
108
  Max: 253
83
109
 
84
- # Offense count: 6
110
+ # Offense count: 2
85
111
  # Configuration parameters: CountBlocks, CountModifierForms.
86
112
  Metrics/BlockNesting:
87
113
  Max: 4
88
114
 
89
- # Offense count: 127
115
+ # Offense count: 125
90
116
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
91
117
  Metrics/CyclomaticComplexity:
92
118
  Enabled: false
93
119
 
94
- # Offense count: 263
120
+ # Offense count: 259
95
121
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
96
122
  Metrics/MethodLength:
97
123
  Max: 154
@@ -101,18 +127,11 @@ Metrics/MethodLength:
101
127
  Metrics/ParameterLists:
102
128
  Max: 9
103
129
 
104
- # Offense count: 101
130
+ # Offense count: 99
105
131
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
106
132
  Metrics/PerceivedComplexity:
107
133
  Enabled: false
108
134
 
109
- # Offense count: 4
110
- # Configuration parameters: EnforcedStyle, AllowedIdentifiers, AllowedPatterns, ForbiddenIdentifiers, ForbiddenPatterns.
111
- # SupportedStyles: snake_case, camelCase
112
- Naming/VariableName:
113
- Exclude:
114
- - 'check_clippath_font.rb'
115
-
116
135
  # Offense count: 2
117
136
  # Configuration parameters: MinSize.
118
137
  Performance/CollectionLiteralInLoop:
@@ -139,12 +158,12 @@ RSpec/DescribeClass:
139
158
  - 'spec/svg_conform/references/integration_spec.rb'
140
159
  - 'spec/svgcheck_compatibility_spec.rb'
141
160
 
142
- # Offense count: 139
161
+ # Offense count: 157
143
162
  # Configuration parameters: CountAsOne.
144
163
  RSpec/ExampleLength:
145
164
  Max: 53
146
165
 
147
- # Offense count: 2
166
+ # Offense count: 3
148
167
  RSpec/LeakyConstantDeclaration:
149
168
  Exclude:
150
169
  - 'spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb'
@@ -160,7 +179,7 @@ RSpec/MultipleDescribes:
160
179
  Exclude:
161
180
  - 'spec/svg_conform/batch_report_spec.rb'
162
181
 
163
- # Offense count: 111
182
+ # Offense count: 126
164
183
  RSpec/MultipleExpectations:
165
184
  Max: 8
166
185
 
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Performance benchmark for svg_conform optimizations
5
+ # Measures validation speed for single and batch operations
6
+
7
+ require "bundler/setup"
8
+ require "benchmark"
9
+ require_relative "../lib/svg_conform"
10
+
11
+ # Helper method to create test SVG content
12
+ def create_test_svg(element_count: 10, attribute_count: 5)
13
+ elements = (1..element_count).map do |i|
14
+ attrs = (1..attribute_count).map { |j| "attr#{j}='value#{j}'" }.join(" ")
15
+ "<rect id='rect#{i}' x='#{i * 10}' y='#{i * 10}' width='10' height='10' #{attrs}/>"
16
+ end
17
+
18
+ <<~SVG
19
+ <?xml version="1.0"?>
20
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
21
+ #{elements.join("\n ")}
22
+ </svg>
23
+ SVG
24
+ end
25
+
26
+ puts "=" * 80
27
+ puts "svg_conform Performance Benchmark"
28
+ puts "=" * 80
29
+ puts
30
+
31
+ # Test configurations
32
+ test_cases = [
33
+ { name: "Small document", elements: 10, attributes: 3 },
34
+ { name: "Medium document", elements: 50, attributes: 5 },
35
+ { name: "Large document", elements: 100, attributes: 8 },
36
+ ]
37
+
38
+ validator = SvgConform::Validator.new
39
+
40
+ test_cases.each do |test_case|
41
+ puts "Test: #{test_case[:name]}"
42
+ puts " Elements: #{test_case[:elements]}, Attributes per element: #{test_case[:attributes]}"
43
+ puts
44
+
45
+ svg_content = create_test_svg(element_count: test_case[:elements],
46
+ attribute_count: test_case[:attributes])
47
+
48
+ # Warmup run
49
+ validator.validate(svg_content, profile: :svg_1_2_rfc)
50
+
51
+ # Benchmark single validation
52
+ single_time = Benchmark.measure do
53
+ 100.times do
54
+ validator.validate(svg_content, profile: :svg_1_2_rfc)
55
+ end
56
+ end
57
+
58
+ puts " Single validation (100 runs): #{(single_time.real * 1000).round(2)}ms"
59
+ puts " Average per validation: #{(single_time.real * 10).round(2)}ms"
60
+
61
+ # Benchmark batch validation
62
+ contents = Array.new(10) do
63
+ create_test_svg(element_count: test_case[:elements],
64
+ attribute_count: test_case[:attributes])
65
+ end
66
+
67
+ batch_time = Benchmark.measure do
68
+ 10.times do
69
+ contents.each do |content|
70
+ validator.validate(content, profile: :svg_1_2_rfc)
71
+ end
72
+ end
73
+ end
74
+
75
+ puts " Batch validation (10 files × 10 runs): #{(batch_time.real * 1000).round(2)}ms"
76
+ puts " Average per file: #{(batch_time.real * 100).round(2)}ms"
77
+ puts
78
+ end
79
+
80
+ puts "=" * 80
81
+ puts "Memory Profiling"
82
+ puts "=" * 80
83
+ puts
84
+
85
+ if Object.const_defined?(:GC)
86
+ GC.start
87
+ before_mem = `ps -o rss= -p #{Process.pid}`.to_i
88
+
89
+ # Create and validate 100 documents
90
+ 100.times do
91
+ svg = create_test_svg(element_count: 50, attribute_count: 5)
92
+ validator.validate(svg, profile: :svg_1_2_rfc)
93
+ end
94
+
95
+ GC.start
96
+ after_mem = `ps -o rss= -p #{Process.pid}`.to_i
97
+
98
+ mem_increase = after_mem - before_mem
99
+ puts " RSS memory increase: #{mem_increase} KB"
100
+ puts " Per validation: #{(mem_increase / 100.0).round(2)} KB"
101
+ else
102
+ puts " Memory profiling not available on this platform"
103
+ end
104
+
105
+ puts
106
+ puts "=" * 80
107
+ puts "Benchmark Complete"
108
+ puts "=" * 80
109
+ puts
110
+ puts "Optimizations tested:"
111
+ puts " ✓ Phase 1.1: ElementProxy#path_id memoization"
112
+ puts " ✓ Phase 1.2: Element configuration index"
113
+ puts " ✓ Phase 2.1: Requirement classification cache"
114
+ puts " ✓ Phase 2.2: Global properties constant"
115
+ puts " ✓ Phase 2.3: ElementProxy attributes cache"
116
+ puts " ✓ Phase 3.1: Configuration validation cache"
117
+ puts " ✓ Phase 3.3: ProfileCompiler class"
118
+ puts " ✓ Phase 4.1: TrackerFactory extraction"
119
+ puts " ✓ Phase 5.1: Batch validation optimization"
120
+ puts
@@ -0,0 +1,239 @@
1
+ = Performance Optimization Guide
2
+
3
+ This guide documents the performance optimization patterns used in svg_conform.
4
+
5
+ == Goals
6
+
7
+ The performance optimization strategy focuses on:
8
+
9
+ * Reducing validation time for repeated operations
10
+ * Minimizing memory allocations during SAX parsing
11
+ * Caching expensive operations without introducing complexity
12
+ * Maintaining clean separation of concerns
13
+
14
+ == Optimization Patterns
15
+
16
+ === Memoization
17
+
18
+ Cache computed values to avoid repeated calculations.
19
+
20
+ *Example:* `ElementProxy#path_id` uses `@cached_path_id` to store the computed path:
21
+
22
+ [source,ruby]
23
+ ----
24
+ def path_id
25
+ @cached_path_id ||= begin
26
+ parts = @path + ["#{@name}[#{@position]}"]
27
+ "/#{parts.join('/')}"
28
+ end
29
+ end
30
+ ----
31
+
32
+ *When to use:* When a method is called multiple times with the same result and the computation is expensive.
33
+
34
+ *When to avoid:* When the result changes between calls, or the computation is trivial.
35
+
36
+ === Class-Level Caching with Thread Safety
37
+
38
+ Use class-level caches with Mutex for thread-safe shared state.
39
+
40
+ *Example:* Requirement classification cache in `SaxValidationHandler`:
41
+
42
+ [source,ruby]
43
+ ----
44
+ @classification_cache = {}
45
+ @classification_cache_mutex = Mutex.new
46
+
47
+ def classify_requirements_with_cache
48
+ profile_key = @profile.class.name
49
+ classified = self.class.classification_cache_mutex.synchronize do
50
+ self.class.classification_cache[profile_key]
51
+ end
52
+
53
+ # ... compute and cache if not found
54
+ end
55
+ ----
56
+
57
+ *When to use:* When the cached value is shared across all instances and doesn't change.
58
+
59
+ *When to avoid:* When the cached value is instance-specific or changes frequently.
60
+
61
+ === Frozen Constants
62
+
63
+ Define immutable data as frozen constants to avoid reallocations.
64
+
65
+ *Example:* `GLOBAL_PROPERTIES` constant in `AllowedElementsRequirement`:
66
+
67
+ [source,ruby]
68
+ ----
69
+ GLOBAL_PROPERTIES = %w[
70
+ about base baseprofile d break class content cx cy
71
+ # ... full list of properties
72
+ ].freeze
73
+ ----
74
+
75
+ *When to use:* For lookup tables, configuration data, and other immutable collections.
76
+
77
+ *When to avoid:* When the data needs to be modified at runtime.
78
+
79
+ === Hash Indexes for O(1) Lookups
80
+
81
+ Pre-build hash indexes for fast lookups instead of linear array searches.
82
+
83
+ *Example:* Element configuration index in `AllowedElementsRequirement`:
84
+
85
+ [source,ruby]
86
+ ----
87
+ def after_initialize
88
+ build_element_config_index if element_configs&.any?
89
+ end
90
+
91
+ def build_element_config_index
92
+ @element_config_index = {}
93
+ element_configs.each do |config|
94
+ @element_config_index[config.tag] = config
95
+ end
96
+ end
97
+
98
+ # Usage - O(1) instead of O(n)
99
+ element_config = @element_config_index&.dig(element_name)
100
+ ----
101
+
102
+ *When to use:* When frequently looking up items by key in a collection.
103
+
104
+ *When to avoid:* When the collection is small or accessed sequentially.
105
+
106
+ === Lazy Initialization with `||=`
107
+
108
+ Use the "or-equals" operator for lazy initialization.
109
+
110
+ *Example:* Cached attributes array in `ElementProxy`:
111
+
112
+ [source,ruby]
113
+ ----
114
+ def attributes
115
+ @cached_attributes ||= @raw_attributes.map { |name, value| SaxAttribute.new(name, value) }
116
+ end
117
+ ----
118
+
119
+ *When to use:* When a value is expensive to compute and may not be needed.
120
+
121
+ *When to avoid:* When `nil` or `false` are valid cached values (use explicit nil check instead).
122
+
123
+ === Batch Processing Optimization
124
+
125
+ Amortize fixed costs across multiple operations.
126
+
127
+ *Example:* Batch file validation in `Validator`:
128
+
129
+ [source,ruby]
130
+ ----
131
+ def validate_files(file_paths, profile: :svg_1_2_rfc, **options)
132
+ # Load profile once
133
+ profile_obj = resolve_profile(profile)
134
+
135
+ file_paths.each do |file_path|
136
+ validate_file_with_profile(file_path, profile_obj, **options)
137
+ end
138
+ end
139
+ ----
140
+
141
+ *When to use:* When processing multiple items with shared setup costs.
142
+
143
+ *When to avoid:* When processing single items or when items have different configurations.
144
+
145
+ === Factory Pattern for Complex Initialization
146
+
147
+ Centralize object creation for cleaner initialization.
148
+
149
+ *Example:* `TrackerFactory` for creating validation trackers:
150
+
151
+ [source,ruby]
152
+ ----
153
+ module Validation
154
+ module TrackerFactory
155
+ def self.create_all_trackers(document)
156
+ node_id_manager = create_node_id_manager(document)
157
+
158
+ {
159
+ error_tracker: create_error_tracker,
160
+ node_id_manager: node_id_manager,
161
+ # ... other trackers
162
+ }
163
+ end
164
+ end
165
+ end
166
+ ----
167
+
168
+ *When to use:* When initialization involves multiple related objects or complex logic.
169
+
170
+ *When to avoid:* For simple object creation that doesn't benefit from centralization.
171
+
172
+ == Anti-Patterns to Avoid
173
+
174
+ === Premature Optimization
175
+
176
+ Don't optimize without benchmarks. Measure first, then optimize.
177
+
178
+ === Global Mutable State
179
+
180
+ Avoid mutable global state. Use class-level caches with thread-safe access.
181
+
182
+ === Over-Abstraction
183
+
184
+ Don't create abstractions just for the sake of it. Each abstraction should pull its weight.
185
+
186
+ === Optimizing Uncommon Paths
187
+
188
+ Focus on hot paths. Don't optimize code that rarely runs.
189
+
190
+ === Sacrificing Readability
191
+
192
+ Performance gains should not come at the cost of code clarity.
193
+
194
+ == Performance Metrics
195
+
196
+ The `benchmark/performance_comparison.rb` script measures:
197
+
198
+ * Single validation speed (average time per document)
199
+ * Batch validation speed (average time per file)
200
+ * Memory usage per validation
201
+
202
+ Run benchmarks before and after optimizations to measure impact:
203
+
204
+ [source,shell]
205
+ ----
206
+ ruby benchmark/performance_comparison.rb
207
+ ----
208
+
209
+ == Architecture Considerations
210
+
211
+ === SAX vs DOM Validation
212
+
213
+ svg_conform uses SAX validation by default for better performance:
214
+
215
+ * *SAX mode*: Constant memory, streaming parser, handles large files
216
+ * *DOM mode*: Loads full document, uses more memory, slower for large files
217
+
218
+ Always prefer SAX for validation. Only use DOM when applying remediations.
219
+
220
+ === Caching Strategy
221
+
222
+ * *Instance-level cache*: Use for data specific to a validation run
223
+ * *Class-level cache*: Use for data shared across all instances
224
+ * *Thread safety*: Always use Mutex for class-level shared state
225
+
226
+ == Future Optimization Opportunities
227
+
228
+ Areas that may benefit from future optimization:
229
+
230
+ * Parallel batch validation for multi-core systems
231
+ * Compiled profiles with pre-computed lookup tables
232
+ * Streaming remediation to avoid full DOM load
233
+ * Attribute name interning for reduced memory
234
+
235
+ == Further Reading
236
+
237
+ * `PERFORMANCE_OPTIMIZATION_PLAN.md` - Detailed optimization plan
238
+ * `CLAUDE.md` - Architecture overview
239
+ * `benchmark/performance_comparison.rb` - Performance benchmarks
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SvgConform
4
+ # Cache for requirement classification results
5
+ # Instance-level (per SaxValidationHandler) to avoid shared mutable state
6
+ # No mutexes needed - each handler has its own cache
7
+ class ClassificationCache
8
+ def initialize
9
+ @cache = {}
10
+ end
11
+
12
+ # Fetch from cache or compute the value
13
+ def fetch(key)
14
+ @cache[key] ||= yield
15
+ end
16
+
17
+ # Clear specific key or entire cache
18
+ def clear(key = nil)
19
+ if key
20
+ @cache.delete(key)
21
+ else
22
+ @cache.clear
23
+ end
24
+ end
25
+
26
+ # Return number of cached entries
27
+ def size
28
+ @cache.size
29
+ end
30
+
31
+ # Check if key exists in cache
32
+ def key?(key)
33
+ @cache.key?(key)
34
+ end
35
+ end
36
+ end
@@ -32,15 +32,20 @@ module SvgConform
32
32
  @child_counters = {} # Track child element positions
33
33
  end
34
34
 
35
- # Build full path ID for this element
35
+ # Build full path ID for this element (memoized for performance)
36
36
  def path_id
37
- parts = @path + ["#{@name}[#{@position}]"]
38
- "/#{parts.join('/')}"
37
+ @path_id ||= begin
38
+ parts = @path + ["#{@name}[#{@position}]"]
39
+ "/#{parts.join('/')}"
40
+ end
39
41
  end
40
42
 
41
- # Return attributes as array of SaxAttribute objects (for compatibility)
43
+ # Return attributes as array of SaxAttribute objects (memoized for performance)
44
+ # Cached to avoid repeated object allocations during SAX parsing
42
45
  def attributes
43
- @raw_attributes.map { |name, value| SaxAttribute.new(name, value) }
46
+ @attributes ||= @raw_attributes.map do |name, value|
47
+ SaxAttribute.new(name, value)
48
+ end
44
49
  end
45
50
 
46
51
  # Check if this element has a specific attribute
@@ -75,8 +80,8 @@ module SvgConform
75
80
  # Boolean check
76
81
  has_attribute?(method.to_s.chomp("?"))
77
82
  else
78
- # Attribute access
79
- @attributes[method.to_s] || @attributes[method.to_sym]
83
+ # Attribute access - use raw_attributes hash, not cached array
84
+ @raw_attributes[method.to_s] || @raw_attributes[method.to_sym]
80
85
  end
81
86
  end
82
87
 
@@ -6,7 +6,12 @@ require_relative "remediations"
6
6
 
7
7
  module SvgConform
8
8
  # Base class for SVG validation profiles using lutaml-model serialization
9
+ #
10
+ # Profile acts as a factory for requirements and remediations.
11
+ # Each validation creates fresh instances to avoid state pollution.
9
12
  class Profile < Lutaml::Model::Serializable
13
+ PROFILES_DIR = File.expand_path("../../config/profiles", __dir__)
14
+
10
15
  attribute :name, :string
11
16
  attribute :description, :string
12
17
  attribute :import, :string
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "profile"
4
+
5
+ module SvgConform
6
+ # Compiles and prepares SVG validation profiles
7
+ # Centralizes profile loading logic for better separation of concerns
8
+ class ProfileCompiler
9
+ @compile_cache = {}
10
+ @compile_mutex = Mutex.new
11
+
12
+ class << self
13
+ attr_reader :compile_cache, :compile_mutex
14
+
15
+ # Compile a profile by name
16
+ # Returns a cached profile if already compiled
17
+ def compile(profile_id)
18
+ profile_key = profile_id.to_s
19
+
20
+ # Check cache (thread-safe)
21
+ compile_mutex.synchronize do
22
+ return compile_cache[profile_key] if compile_cache[profile_key]
23
+ end
24
+
25
+ # Load and compile profile
26
+ profile = load_profile(profile_key)
27
+ compiled_profile = prepare_profile(profile)
28
+
29
+ # Cache result (thread-safe)
30
+ compile_mutex.synchronize do
31
+ compile_cache[profile_key] = compiled_profile
32
+ end
33
+
34
+ compiled_profile
35
+ end
36
+
37
+ # Compile from YAML content
38
+ def compile_from_yaml(yaml_content)
39
+ profile = Profile.from_yaml(yaml_content)
40
+ prepare_profile(profile)
41
+ end
42
+
43
+ # Compile from file path
44
+ def compile_from_file(file_path)
45
+ profile = Profile.load_from_file(file_path)
46
+ prepare_profile(profile)
47
+ end
48
+
49
+ # Clear compilation cache
50
+ def clear_cache!
51
+ compile_mutex.synchronize do
52
+ compile_cache.clear
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Load profile from profiles directory
59
+ def load_profile(profile_name)
60
+ profile_file = File.join(Profile::PROFILES_DIR, "#{profile_name}.yml")
61
+
62
+ unless File.exist?(profile_file)
63
+ raise ProfileError,
64
+ "Profile not found: #{profile_name} (expected: #{profile_file})"
65
+ end
66
+
67
+ Profile.load_from_file(profile_file)
68
+ end
69
+
70
+ # Prepare profile for use (validation and preprocessing)
71
+ def prepare_profile(profile)
72
+ # Validate profile has required fields
73
+ validate_profile(profile)
74
+
75
+ # Pre-compute requirement classifications for SAX validation
76
+ precompute_requirement_metadata(profile)
77
+
78
+ profile
79
+ end
80
+
81
+ # Validate profile structure
82
+ def validate_profile(profile)
83
+ if profile.name.nil? || profile.name.empty?
84
+ raise ProfileError,
85
+ "Profile must have a name"
86
+ end
87
+ end
88
+
89
+ # Pre-compute metadata for faster validation
90
+ def precompute_requirement_metadata(profile)
91
+ # Trigger requirement classification to populate caches
92
+ profile.requirements.each do |req|
93
+ # Access classification method to populate SaxValidationHandler cache
94
+ if req.respond_to?(:needs_deferred_validation?)
95
+ req.needs_deferred_validation?
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end