resolvekit 0.0.1__py3-none-any.whl

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 (70) hide show
  1. resolvekit/README.md +134 -0
  2. resolvekit/__init__.py +67 -0
  3. resolvekit/api/README.md +165 -0
  4. resolvekit/api/__init__.py +10 -0
  5. resolvekit/api/convenience.py +53 -0
  6. resolvekit/api/resolver.py +457 -0
  7. resolvekit/builders/README.md +173 -0
  8. resolvekit/builders/__init__.py +0 -0
  9. resolvekit/calibration/README.md +351 -0
  10. resolvekit/calibration/__init__.py +12 -0
  11. resolvekit/calibration/calibrator.py +184 -0
  12. resolvekit/calibration/features.py +139 -0
  13. resolvekit/calibration/models.py +78 -0
  14. resolvekit/cli/README.md +215 -0
  15. resolvekit/cli/__init__.py +0 -0
  16. resolvekit/cli/main.py +18 -0
  17. resolvekit/config.py +128 -0
  18. resolvekit/constants.py +252 -0
  19. resolvekit/constraints/README.md +102 -0
  20. resolvekit/constraints/__init__.py +17 -0
  21. resolvekit/constraints/constraint_engine.py +111 -0
  22. resolvekit/constraints/hierarchy_validator.py +148 -0
  23. resolvekit/constraints/membership_validator.py +60 -0
  24. resolvekit/constraints/protocols.py +33 -0
  25. resolvekit/constraints/temporal_validator.py +43 -0
  26. resolvekit/constraints/type_validator.py +42 -0
  27. resolvekit/data/README.md +165 -0
  28. resolvekit/data/__init__.py +14 -0
  29. resolvekit/data/alias_repository.py +206 -0
  30. resolvekit/data/code_repository.py +85 -0
  31. resolvekit/data/context_filters.py +49 -0
  32. resolvekit/data/db_manager.py +196 -0
  33. resolvekit/data/entity_repository.py +466 -0
  34. resolvekit/data/membership_repository.py +107 -0
  35. resolvekit/data/query_builder.py +177 -0
  36. resolvekit/data/schema.py +122 -0
  37. resolvekit/disambiguation/README.md +72 -0
  38. resolvekit/disambiguation/__init__.py +0 -0
  39. resolvekit/extraction/README.md +204 -0
  40. resolvekit/extraction/__init__.py +0 -0
  41. resolvekit/matchers/README.md +77 -0
  42. resolvekit/matchers/__init__.py +65 -0
  43. resolvekit/matchers/alias_exact.py +65 -0
  44. resolvekit/matchers/canonical_name.py +62 -0
  45. resolvekit/matchers/cascade.py +127 -0
  46. resolvekit/matchers/code_validators.py +250 -0
  47. resolvekit/matchers/exact_code.py +177 -0
  48. resolvekit/matchers/fts_matcher.py +106 -0
  49. resolvekit/matchers/fuzzy_matcher.py +142 -0
  50. resolvekit/matchers/priorities.py +174 -0
  51. resolvekit/matchers/protocols.py +75 -0
  52. resolvekit/normalization/README.md +192 -0
  53. resolvekit/normalization/__init__.py +8 -0
  54. resolvekit/normalization/normalizer.py +164 -0
  55. resolvekit/overlays/README.md +226 -0
  56. resolvekit/overlays/__init__.py +0 -0
  57. resolvekit/types.py +534 -0
  58. resolvekit/utils/README.md +188 -0
  59. resolvekit/utils/__init__.py +48 -0
  60. resolvekit/utils/cache.py +109 -0
  61. resolvekit/utils/dates.py +339 -0
  62. resolvekit/utils/errors.py +145 -0
  63. resolvekit/utils/files.py +366 -0
  64. resolvekit/utils/logging.py +219 -0
  65. resolvekit/utils/text.py +475 -0
  66. resolvekit/utils/validation.py +301 -0
  67. resolvekit-0.0.1.dist-info/METADATA +36 -0
  68. resolvekit-0.0.1.dist-info/RECORD +70 -0
  69. resolvekit-0.0.1.dist-info/WHEEL +4 -0
  70. resolvekit-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,192 @@
1
+ # Normalization Module
2
+
3
+ ## Purpose
4
+
5
+ The normalization module handles text preprocessing to ensure consistent string matching across different input formats and languages. It provides three preset levels optimized for different use cases.
6
+
7
+ ## Implementation Status
8
+
9
+ ✅ **Implemented** - Phase A complete with 94% test coverage
10
+
11
+ ## Features
12
+
13
+ - **Three Preset Levels**: strict, standard, aggressive
14
+ - **Performance-First**: LRU caching for repeated queries (10,000 entry cache)
15
+ - **Unicode Handling**: NFKD normalization for consistent representation
16
+ - **Diacritic Removal**: Configurable diacritical mark handling
17
+ - **Batch Operations**: Efficient processing of multiple texts
18
+ - **Graceful Error Handling**: Handles edge cases (empty strings, None, etc.)
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from resolvekit.normalization import TextNormalizer
24
+
25
+ # Create normalizer (stateless, reusable)
26
+ normalizer = TextNormalizer()
27
+
28
+ # Basic usage (default: standard level)
29
+ result = normalizer.normalize("Côte d'Ivoire")
30
+ print(result) # Output: "cote d'ivoire"
31
+
32
+ # Batch processing
33
+ countries = ["France", "Côte d'Ivoire", "Germany"]
34
+ normalized = normalizer.normalize_batch(countries)
35
+ print(normalized) # ['france', "cote d'ivoire", 'germany']
36
+ ```
37
+
38
+ ## Normalization Levels
39
+
40
+ ### Strict
41
+ **Use for**: Exact matching where case and diacritics matter (e.g., ISO codes)
42
+
43
+ ```python
44
+ normalizer.normalize("USA", level="strict") # "USA"
45
+ normalizer.normalize("Côte d'Ivoire", level="strict") # "Côte d'Ivoire" (decomposed unicode)
46
+ ```
47
+
48
+ - ✅ Unicode normalization (NFKD)
49
+ - ✅ Whitespace normalization
50
+ - ❌ Diacritic removal
51
+ - ❌ Case folding
52
+ - ❌ Punctuation removal
53
+
54
+ ### Standard (Default)
55
+ **Use for**: General entity resolution and fuzzy matching
56
+
57
+ ```python
58
+ normalizer.normalize("Côte d'Ivoire") # "cote d'ivoire"
59
+ normalizer.normalize("U.S.A.") # "u.s.a."
60
+ ```
61
+
62
+ - ✅ Unicode normalization (NFKD)
63
+ - ✅ Diacritic removal
64
+ - ✅ Case folding
65
+ - ✅ Whitespace normalization
66
+ - ❌ Punctuation removal
67
+
68
+ ### Aggressive
69
+ **Use for**: Noisy data, user input with typos, low-quality sources
70
+
71
+ ```python
72
+ normalizer.normalize("U.S.A.", level="aggressive") # "usa"
73
+ normalizer.normalize("Côte-d'Ivoire!!!", level="aggressive") # "cote divoire"
74
+ ```
75
+
76
+ - ✅ Unicode normalization (NFKD)
77
+ - ✅ Diacritic removal
78
+ - ✅ Case folding
79
+ - ✅ Whitespace normalization
80
+ - ✅ Punctuation removal
81
+
82
+ ## API Reference
83
+
84
+ ### TextNormalizer
85
+
86
+ ```python
87
+ class TextNormalizer:
88
+ def normalize(text: str, level: str = "standard") -> str:
89
+ """Normalize single text with LRU caching."""
90
+
91
+ def normalize_batch(texts: list[str], level: str = "standard") -> list[str]:
92
+ """Normalize multiple texts efficiently."""
93
+
94
+ def get_level_config(level: str) -> NormalizationLevel:
95
+ """Get configuration for a level (for introspection)."""
96
+ ```
97
+
98
+ ### Examples
99
+
100
+ ```python
101
+ from resolvekit.normalization import TextNormalizer
102
+
103
+ normalizer = TextNormalizer()
104
+
105
+ # Different levels
106
+ normalizer.normalize("Türkiye", level="strict") # "Türkiye"
107
+ normalizer.normalize("Türkiye", level="standard") # "turkiye"
108
+ normalizer.normalize("Türkiye", level="aggressive") # "turkiye"
109
+
110
+ # Batch processing
111
+ texts = ["France", "Germany", "Italy"]
112
+ normalized = normalizer.normalize_batch(texts) # ["france", "germany", "italy"]
113
+
114
+ # Edge cases (handled gracefully)
115
+ normalizer.normalize("") # ""
116
+ normalizer.normalize(" ") # ""
117
+ normalizer.normalize(None) # ""
118
+
119
+ # Introspection
120
+ config = normalizer.get_level_config("standard")
121
+ print(config.remove_diacritics) # True
122
+ ```
123
+
124
+ ## Performance
125
+
126
+ The normalization module is optimized for high-throughput entity resolution:
127
+
128
+ - **Caching**: 10,000-entry LRU cache for repeated queries
129
+ - **Speed**: < 0.001ms per cached normalization
130
+ - **Batch**: Efficient config reuse for multiple texts
131
+ - **Coverage**: 94% test coverage with performance benchmarks
132
+
133
+ ### Performance Examples
134
+
135
+ ```python
136
+ # Cached calls are 10-100x faster
137
+ normalizer.normalize("France") # First call: ~0.01ms
138
+ normalizer.normalize("France") # Cached: ~0.0001ms
139
+
140
+ # Batch processing is efficient
141
+ texts = ["France"] * 1000
142
+ normalizer.normalize_batch(texts) # Config reused for all texts
143
+ ```
144
+
145
+ ## Design Principles
146
+
147
+ 1. **Thin Orchestrator**: Wraps existing `utils.text` functions
148
+ 2. **Preset Levels**: Simple API with sensible defaults
149
+ 3. **Performance-First**: LRU caching from day one
150
+ 4. **Graceful Degradation**: Handles edge cases without crashing
151
+ 5. **Testable**: 19 comprehensive tests with 94% coverage
152
+
153
+ ## Integration
154
+
155
+ The normalizer integrates with the matcher cascade:
156
+
157
+ | Matcher | Level | Rationale |
158
+ |---------|-------|-----------|
159
+ | ExactCodeMatcher | strict | Codes are case-sensitive |
160
+ | CanonicalNameMatcher | standard | Canonical names need normalization |
161
+ | AliasExactMatcher | standard | Aliases need diacritic/case handling |
162
+ | FuzzyMatchers | standard/aggressive | Depends on data quality |
163
+
164
+ ## Future Enhancements
165
+
166
+ - **Transliteration**: Cyrillic→Latin, Arabic→Latin (deferred to Phase B+)
167
+ - **Custom levels**: User-defined normalization configs
168
+ - **Language hints**: Language-aware normalization rules
169
+ - **Normalization metadata**: Track applied transformations for debugging
170
+
171
+ ## Testing
172
+
173
+ Run normalization tests:
174
+
175
+ ```bash
176
+ # All normalization tests
177
+ uv run pytest tests/test_normalization.py -v
178
+
179
+ # Performance tests only
180
+ uv run pytest tests/test_normalization.py::TestPerformance -v
181
+
182
+ # With coverage
183
+ uv run pytest tests/test_normalization.py --cov=src/resolvekit/normalization
184
+ ```
185
+
186
+ ## Files
187
+
188
+ ```
189
+ src/resolvekit/normalization/
190
+ ├── __init__.py # Exports TextNormalizer, NormalizationLevel
191
+ └── normalizer.py # Implementation (40 SLOC, 94% coverage)
192
+ ```
@@ -0,0 +1,8 @@
1
+ """Normalization module for text preprocessing."""
2
+
3
+ from resolvekit.normalization.normalizer import NormalizationLevel, TextNormalizer
4
+
5
+ __all__ = [
6
+ "NormalizationLevel",
7
+ "TextNormalizer",
8
+ ]
@@ -0,0 +1,164 @@
1
+ """Text normalization module for resolvekit."""
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from typing import ClassVar
6
+
7
+ from resolvekit.utils.cache import lru_cache
8
+ from resolvekit.utils.text import (
9
+ case_fold,
10
+ normalize_unicode,
11
+ normalize_whitespace,
12
+ remove_diacritics,
13
+ )
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class NormalizationLevel:
18
+ """Configuration for a normalization level."""
19
+
20
+ name: str
21
+ unicode_norm: bool
22
+ remove_diacritics: bool
23
+ case_fold: bool
24
+ normalize_whitespace: bool
25
+ remove_punctuation: bool
26
+
27
+
28
+ class TextNormalizer:
29
+ """Text normalizer with preset levels and LRU caching."""
30
+
31
+ LEVELS: ClassVar[dict[str, NormalizationLevel]] = {
32
+ "strict": NormalizationLevel(
33
+ name="strict",
34
+ unicode_norm=True,
35
+ remove_diacritics=False,
36
+ case_fold=False,
37
+ normalize_whitespace=True,
38
+ remove_punctuation=False,
39
+ ),
40
+ "standard": NormalizationLevel(
41
+ name="standard",
42
+ unicode_norm=True,
43
+ remove_diacritics=True,
44
+ case_fold=True,
45
+ normalize_whitespace=True,
46
+ remove_punctuation=False,
47
+ ),
48
+ "aggressive": NormalizationLevel(
49
+ name="aggressive",
50
+ unicode_norm=True,
51
+ remove_diacritics=True,
52
+ case_fold=True,
53
+ normalize_whitespace=True,
54
+ remove_punctuation=True,
55
+ ),
56
+ }
57
+
58
+ @lru_cache(maxsize=10000)
59
+ def normalize(self, text: str, level: str = "standard") -> str:
60
+ """
61
+ Normalize text using preset level.
62
+
63
+ Args:
64
+ text: Text to normalize
65
+ level: Normalization level (strict, standard, aggressive)
66
+
67
+ Returns:
68
+ Normalized text
69
+
70
+ Examples:
71
+ >>> normalizer = TextNormalizer()
72
+ >>> normalizer.normalize("Côte d'Ivoire")
73
+ "cote d'ivoire"
74
+ """
75
+ # Handle empty/whitespace-only strings
76
+ if not text or not text.strip():
77
+ return ""
78
+
79
+ # Get level config
80
+ level_config = self.LEVELS[level]
81
+
82
+ # Apply normalizations
83
+ return self._apply_normalizations(text, level_config)
84
+
85
+ def _apply_normalizations(self, text: str, config: NormalizationLevel) -> str:
86
+ """
87
+ Apply normalizations based on configuration.
88
+
89
+ Args:
90
+ text: Text to normalize
91
+ config: Normalization level configuration
92
+
93
+ Returns:
94
+ Normalized text
95
+ """
96
+ # Unicode normalization
97
+ if config.unicode_norm:
98
+ text = normalize_unicode(text)
99
+
100
+ # Remove diacritics
101
+ if config.remove_diacritics:
102
+ text = remove_diacritics(text)
103
+
104
+ # Case folding
105
+ if config.case_fold:
106
+ text = case_fold(text)
107
+
108
+ # Normalize whitespace
109
+ if config.normalize_whitespace:
110
+ text = normalize_whitespace(text)
111
+
112
+ # Remove punctuation
113
+ if config.remove_punctuation:
114
+ text = self._remove_punctuation(text)
115
+
116
+ return text
117
+
118
+ @staticmethod
119
+ def _remove_punctuation(text: str) -> str:
120
+ """
121
+ Remove punctuation from text.
122
+
123
+ Args:
124
+ text: Input text
125
+
126
+ Returns:
127
+ Text without punctuation
128
+ """
129
+ # Remove common punctuation but keep spaces and alphanumerics
130
+ return re.sub(r"[^\w\s]", "", text)
131
+
132
+ def normalize_batch(self, texts: list[str], level: str = "standard") -> list[str]:
133
+ """
134
+ Normalize multiple texts efficiently.
135
+
136
+ Args:
137
+ texts: List of texts to normalize
138
+ level: Normalization level (strict, standard, aggressive)
139
+
140
+ Returns:
141
+ List of normalized texts
142
+
143
+ Examples:
144
+ >>> normalizer = TextNormalizer()
145
+ >>> normalizer.normalize_batch(["France", "Germany"])
146
+ ["france", "germany"]
147
+ """
148
+ # Delegate to normalize() to reuse guard logic and benefit from cache
149
+ return [self.normalize(text, level) for text in texts]
150
+
151
+ def get_level_config(self, level: str) -> NormalizationLevel:
152
+ """
153
+ Get configuration for a normalization level.
154
+
155
+ Args:
156
+ level: Normalization level name
157
+
158
+ Returns:
159
+ NormalizationLevel configuration
160
+
161
+ Raises:
162
+ KeyError: If level name is invalid
163
+ """
164
+ return self.LEVELS[level]
@@ -0,0 +1,226 @@
1
+ # Overlays Module
2
+
3
+ ## Purpose
4
+
5
+ The overlays module manages custom data packs that extend or override the base data pack. Overlays enable users to install domain-specific packs (e.g., humanitarian-orgs-pack, schools-pack, corporate-entities-pack) or create their own custom aliases and entities.
6
+
7
+ ## Components
8
+
9
+ ### Core Components
10
+
11
+ 1. **Overlay Manager** (`overlay_manager.py`)
12
+ - Load and attach overlay databases
13
+ - Manage precedence rules
14
+ - Handle overlay conflicts
15
+
16
+ 2. **Overlay Writer** (`overlay_writer.py`)
17
+ - Create new overlay packs
18
+ - Add/update aliases, entities, memberships
19
+ - Build FTS indexes for overlays
20
+
21
+ 3. **Overlay Validator** (`validator.py`)
22
+ - Validate overlay schemas
23
+ - Check for conflicts with base pack
24
+ - Verify referential integrity
25
+
26
+ 4. **Overlay Merger** (`merger.py`)
27
+ - Merge queries across base + overlays
28
+ - Apply precedence rules
29
+ - Deduplicate results
30
+
31
+ ### Data Models
32
+
33
+ - `overlay_config.py`: Overlay configuration and metadata
34
+ - `precedence.py`: Precedence rule engine
35
+
36
+ ## Overlay Structure
37
+
38
+ Each overlay is a SQLite database with the same schema as base pack:
39
+
40
+ ```
41
+ my-overlay.sqlite
42
+ ├── entities (custom entities only)
43
+ ├── aliases (custom aliases + overrides)
44
+ ├── aliases_fts (FTS index)
45
+ ├── codes (custom codes)
46
+ ├── memberships (custom memberships)
47
+ └── manifest (overlay metadata)
48
+ ```
49
+
50
+ ## Overlay Manifest
51
+
52
+ ```json
53
+ {
54
+ "pack_name": "humanitarian-orgs.overlay",
55
+ "version": "0.3.0",
56
+ "base_compat": ">=1.2.0",
57
+ "components": {
58
+ "overlay_sqlite": "humanitarian-orgs.overlay.sqlite",
59
+ "fts_built": true,
60
+ "calibration_delta": null
61
+ },
62
+ "description": "Humanitarian organization names and aliases",
63
+ "author": "OCHA",
64
+ "created": "2025-10-20T10:00:00Z"
65
+ }
66
+ ```
67
+
68
+ **Note:** Precedence is not stored in the manifest. It's determined by the order in which overlays are loaded (earlier in list = higher precedence).
69
+
70
+ ## Precedence Rules
71
+
72
+ Precedence is determined by the order in the overlays list:
73
+
74
+ - **First overlay in list**: Highest precedence (100)
75
+ - **Second overlay**: Precedence 99
76
+ - **Third overlay**: Precedence 98
77
+ - **...and so on** (max 5 overlays)
78
+ - **Base pack**: Precedence 0 (lowest)
79
+
80
+ When the same alias maps to different entities:
81
+ 1. Use the highest precedence mapping (earliest in the list)
82
+ 2. Log warning about conflict
83
+ 3. Optional: Surface conflict to user for resolution
84
+
85
+ **Example:**
86
+ ```python
87
+ resolver = Resolver(overlays=["personal.sqlite", "humanitarian.sqlite", "schools.sqlite"])
88
+ # personal.sqlite: precedence 100 (highest)
89
+ # humanitarian.sqlite: precedence 99
90
+ # schools.sqlite: precedence 98
91
+ # base pack: precedence 0 (lowest)
92
+ ```
93
+
94
+ ## Creating Overlays
95
+
96
+ ### From CSV
97
+
98
+ ```python
99
+ from resolvekit.overlays import OverlayWriter
100
+
101
+ # Create overlay from CSV
102
+ writer = OverlayWriter("my-overlay")
103
+
104
+ # Add aliases
105
+ writer.add_aliases_from_csv("custom_aliases.csv")
106
+ # CSV columns: dcid, alias_text, alias_type, language
107
+
108
+ # Add custom entities
109
+ writer.add_entities_from_csv("custom_entities.csv")
110
+ # CSV columns: dcid, canonical_name, entity_type, parent_dcid
111
+
112
+ # Add memberships
113
+ writer.add_memberships_from_csv("memberships.csv")
114
+ # CSV columns: entity_dcid, group_dcid, valid_from, valid_until
115
+
116
+ # Build and save
117
+ writer.build("my-overlay.sqlite")
118
+ ```
119
+
120
+ ### From YAML
121
+
122
+ ```yaml
123
+ # custom_data.yaml
124
+ entities:
125
+ - dcid: "custom/MyOrg"
126
+ name: "My Organization"
127
+ type: "organization"
128
+ aliases:
129
+ - "MyOrg"
130
+ - "The Organization"
131
+ codes:
132
+ internal_id: "ORG-001"
133
+
134
+ - dcid: "country/COD" # Extend existing entity
135
+ custom_aliases:
136
+ - "Congo-Kinshasa"
137
+ - "DROC" # Internal shorthand
138
+
139
+ groups:
140
+ - dcid: "custom/OurPartners"
141
+ name: "Our Partner Countries"
142
+ members:
143
+ - country/KEN
144
+ - country/TZA
145
+ - country/UGA
146
+ ```
147
+
148
+ ### Via API
149
+
150
+ ```python
151
+ from resolvekit.overlays import create_overlay
152
+
153
+ overlay = create_overlay(
154
+ name="my-custom-overlay",
155
+ base_pack_version="1.2.0"
156
+ )
157
+
158
+ # Add custom alias
159
+ overlay.add_alias(
160
+ entity_dcid="country/COD",
161
+ alias_text="Congo-Kinshasa",
162
+ alias_type="exonym",
163
+ language="en"
164
+ )
165
+
166
+ # Add custom entity
167
+ overlay.add_entity(
168
+ dcid="custom/MyEntity",
169
+ canonical_name="My Custom Entity",
170
+ entity_type="organization"
171
+ )
172
+
173
+ # Save overlay
174
+ overlay.save("my-custom.overlay.sqlite")
175
+ ```
176
+
177
+ **Note:** Precedence is determined when loading overlays into the Resolver, not when creating them.
178
+
179
+ ## Loading Overlays
180
+
181
+ ```python
182
+ from resolvekit.api import Resolver
183
+
184
+ # Load resolver with overlays (order = precedence)
185
+ resolver = Resolver(
186
+ overlays=[
187
+ "path/to/personal-customizations.sqlite", # Highest precedence (100)
188
+ "path/to/humanitarian-orgs.sqlite", # Precedence 99
189
+ "path/to/schools-pack.sqlite" # Precedence 98
190
+ ]
191
+ )
192
+
193
+ # Overlays are automatically applied to all queries
194
+ result = resolver.resolve("DROC") # Matches custom alias from highest-precedence overlay
195
+
196
+ # Maximum 5 overlays supported
197
+ # Order in the list determines precedence (first = highest)
198
+ ```
199
+
200
+ ## Conflict Resolution
201
+
202
+ When conflicts detected:
203
+
204
+ ```python
205
+ # Log warning
206
+ logger.warning(
207
+ "Alias 'Georgia' maps to both country/GEO (precedence: 0) "
208
+ "and geoId/13 (precedence: 50). Using geoId/13."
209
+ )
210
+
211
+ # Optionally return conflict info
212
+ result = resolver.resolve("Georgia", return_conflicts=True)
213
+ if result.conflicts:
214
+ print(f"Conflicts: {result.conflicts}")
215
+ ```
216
+
217
+ ## Design Principles
218
+
219
+ 1. **Non-destructive**: Overlays don't modify base pack
220
+ 2. **Composable**: Multiple overlays can coexist
221
+ 3. **Validated**: Schema validation on overlay creation
222
+ 4. **Transparent**: Clear precedence rules, conflict reporting
223
+
224
+ ## Implementation Priority
225
+
226
+ **Phase C** - Overlay system and builders
File without changes