|
| 1 | +# Foundation Injection Enhancement Plan |
| 2 | + |
| 3 | +**Date**: 2025-11-04 |
| 4 | +**Context**: Enhancing foundation injection to use immutable exceptions and include common dependencies |
| 5 | + |
| 6 | +## Background |
| 7 | + |
| 8 | +We want to update the injected `Omniexception` base class to inherit from `__.immut.exceptions.Omniexception` (provided by the `frigid` package) to enforce class and instance immutability. However, this creates a bootstrapping problem for foundation packages like `frigid`, `absence`, and `classcore` that cannot depend on themselves or their dependents. |
| 9 | + |
| 10 | +### Key Insight |
| 11 | + |
| 12 | +Merge conflicts with injected exceptions have not been a problem in practice. Most customization of `exceptions.py` is **additive** (adding project-specific exception subclasses), which doesn't conflict with template updates to the base `Omniexception`/`Omnierror` classes. |
| 13 | + |
| 14 | +### Current State |
| 15 | + |
| 16 | +- `inject_foundations`: Boolean flag that gates `inject_exceptions` |
| 17 | +- `inject_exceptions`: When enabled, creates `exceptions.py` with plain `Omniexception` and `Omnierror` classes |
| 18 | +- Currently NOT used in any template files directly, only in `copier.yaml` as a conditional |
| 19 | + |
| 20 | +## Proposed Approach |
| 21 | + |
| 22 | +### 1. Update Foundation Injection Behavior |
| 23 | + |
| 24 | +When `inject_foundations: true`: |
| 25 | +- **Add default dependencies**: `frigid`, `absence`, `dynadoc` |
| 26 | +- **Inject immutable exceptions**: Subclass `Omniexception` from `__.immut.exceptions.Omniexception` |
| 27 | + |
| 28 | +### 2. Disable Foundation Injection for Foundation Packages |
| 29 | + |
| 30 | +Foundation packages that cannot have these dependencies: |
| 31 | +- `classcore` - Provides base object system (already has `inject_foundations: false`) |
| 32 | +- `frigid` - Provides immutability (currently has `inject_foundations: true` - needs update) |
| 33 | +- `absence` - Provides absence sentinel (needs verification and possible update) |
| 34 | +- `accretive` - Sister package to frigid (needs verification and possible update) |
| 35 | + |
| 36 | +### 3. Rationale for This Approach |
| 37 | + |
| 38 | +**Why this is pragmatic**: |
| 39 | +- Only 3-4 packages need special treatment (foundation layer) |
| 40 | +- The vast majority of projects are application-level and benefit from immutable exceptions |
| 41 | +- We already manually add `frigid`, `absence`, and `dynadoc` to most projects—automating reduces toil |
| 42 | +- Merge conflicts have not been a problem in practice |
| 43 | +- Simple implementation with clear mental model |
| 44 | + |
| 45 | +**When this works well**: |
| 46 | +- ✅ Most projects (>80%) are application-level and use these dependencies |
| 47 | +- ✅ Base exception classes in template rarely change (merge conflicts rare) |
| 48 | +- ✅ New foundation packages created infrequently |
| 49 | +- ✅ Template primarily for our ecosystem |
| 50 | + |
| 51 | +## Implementation Tasks |
| 52 | + |
| 53 | +### Task 1: Update copier.yaml |
| 54 | + |
| 55 | +#### Update inject_foundations |
| 56 | + |
| 57 | +```yaml |
| 58 | +inject_foundations: |
| 59 | + type: bool |
| 60 | + help: | |
| 61 | + Include foundational constructs (base exceptions using immutable patterns). |
| 62 | +
|
| 63 | + Dependencies added: frigid, absence, dynadoc |
| 64 | +
|
| 65 | + Set to FALSE for foundation packages that cannot depend on these: |
| 66 | + - classcore (provides base object system) |
| 67 | + - frigid (provides immutability) |
| 68 | + - absence (provides absence sentinel) |
| 69 | + - accretive (sister package to frigid) |
| 70 | + default: false |
| 71 | +``` |
| 72 | +
|
| 73 | +#### Update inject_exceptions (keep as-is) |
| 74 | +
|
| 75 | +```yaml |
| 76 | +inject_exceptions: |
| 77 | + type: bool |
| 78 | + help: 'Include base exceptions for package?' |
| 79 | + when: "{{ inject_foundations }}" |
| 80 | + default: false |
| 81 | +``` |
| 82 | +
|
| 83 | +### Task 2: Update pyproject.toml.jinja |
| 84 | +
|
| 85 | +Add conditional dependencies when `inject_foundations: true`: |
| 86 | + |
| 87 | +```toml |
| 88 | +dependencies = [ |
| 89 | + "python >= 3.10", |
| 90 | + {% if inject_foundations %} |
| 91 | + "absence >= 0.3", # Absence sentinel |
| 92 | + "dynadoc >= 0.3", # Dynamic documentation |
| 93 | + "frigid >= 0.15", # Immutable data structures |
| 94 | + {% endif %} |
| 95 | + # ... other dependencies |
| 96 | +] |
| 97 | +``` |
| 98 | + |
| 99 | +**Note**: Verify current minimum versions for these packages. |
| 100 | + |
| 101 | +### Task 3: Update sources/{{ package_name }}/__/imports.py.jinja |
| 102 | + |
| 103 | +Ensure frigid is aliased to `immut` when foundations are injected: |
| 104 | + |
| 105 | +```python |
| 106 | +{% if inject_foundations %} |
| 107 | +import frigid as immut |
| 108 | +{% endif %} |
| 109 | +``` |
| 110 | + |
| 111 | +**Note**: Verify current import structure and integrate appropriately with existing import cascade pattern. |
| 112 | + |
| 113 | +### Task 4: Update exceptions.py.jinja |
| 114 | + |
| 115 | +Update to inherit from immutable base: |
| 116 | + |
| 117 | +```python |
| 118 | +# template/sources/{{ package_name }}/{% if inject_exceptions %}exceptions.py{% endif %}.jinja |
| 119 | +
|
| 120 | +''' Family of exceptions for package API. ''' |
| 121 | +
|
| 122 | +from . import __ |
| 123 | +
|
| 124 | +
|
| 125 | +class Omniexception(__.immut.exceptions.Omniexception): |
| 126 | + ''' Base for all exceptions raised by package API. ''' |
| 127 | +
|
| 128 | +
|
| 129 | +class Omnierror(Omniexception, Exception): |
| 130 | + ''' Base for error exceptions raised by package API. ''' |
| 131 | +
|
| 132 | + def render_as_markdown(self) -> tuple[str, ...]: |
| 133 | + ''' Renders exception as Markdown lines for display. ''' |
| 134 | + return (f"❌ {self}",) |
| 135 | +``` |
| 136 | + |
| 137 | +**Note**: Review if `render_as_markdown()` should be included in base or if it's project-specific. |
| 138 | + |
| 139 | +### Task 5: Update Foundation Packages |
| 140 | + |
| 141 | +For packages that must disable foundation injection: |
| 142 | + |
| 143 | +#### frigid |
| 144 | +- Update `.auxiliary/configuration/copier-answers.yaml` |
| 145 | +- Set `inject_foundations: false` |
| 146 | +- Set `inject_exceptions: false` |
| 147 | +- Keep existing hand-crafted `exceptions.py` |
| 148 | + |
| 149 | +#### absence |
| 150 | +- Verify current copier answers |
| 151 | +- Update if needed: `inject_foundations: false`, `inject_exceptions: false` |
| 152 | + |
| 153 | +#### accretive |
| 154 | +- Verify current copier answers |
| 155 | +- Update if needed: `inject_foundations: false`, `inject_exceptions: false` |
| 156 | + |
| 157 | +#### classcore |
| 158 | +- Already has `inject_foundations: false` ✅ |
| 159 | +- No changes needed |
| 160 | + |
| 161 | +### Task 6: Documentation |
| 162 | + |
| 163 | +Update template README to explain foundation injection: |
| 164 | + |
| 165 | +```markdown |
| 166 | +## Foundation Injection |
| 167 | +
|
| 168 | +The template can inject foundational constructs including: |
| 169 | +- Base exception classes (`Omniexception`, `Omnierror`) with immutability |
| 170 | +- Common dependencies: `frigid`, `absence`, `dynadoc` |
| 171 | + |
| 172 | +**For most projects**: Enable `inject_foundations: true` and `inject_exceptions: true` |
| 173 | + |
| 174 | +**For foundation packages** (classcore, frigid, absence, accretive): |
| 175 | +- Set `inject_foundations: false` |
| 176 | +- These packages provide foundational constructs and cannot depend on themselves |
| 177 | +``` |
| 178 | +
|
| 179 | +## Verification Steps |
| 180 | +
|
| 181 | +After implementation: |
| 182 | +
|
| 183 | +1. **Test new project generation**: |
| 184 | + - Generate test project with `inject_foundations: true` |
| 185 | + - Verify dependencies added to `pyproject.toml` |
| 186 | + - Verify `exceptions.py` uses `__.immut.exceptions.Omniexception` |
| 187 | + - Verify imports in `__/imports.py` include `frigid as immut` |
| 188 | + |
| 189 | +2. **Test foundation package updates**: |
| 190 | + - Run `copier update` on frigid with `inject_foundations: false` |
| 191 | + - Verify no conflicts, no unwanted changes |
| 192 | + - Repeat for absence, accretive if applicable |
| 193 | + |
| 194 | +3. **Test existing project updates**: |
| 195 | + - Run `copier update` on an existing project like agentsmgr |
| 196 | + - Verify smooth merge or identify expected conflicts |
| 197 | + - Document any manual migration steps needed |
| 198 | + |
| 199 | +## Migration Notes |
| 200 | + |
| 201 | +### For Existing Projects |
| 202 | + |
| 203 | +When existing projects update to the new template version: |
| 204 | + |
| 205 | +**If they had `inject_exceptions: true`**: |
| 206 | +- Will need to update `exceptions.py` base class inheritance |
| 207 | +- Will need to ensure `frigid as immut` import exists |
| 208 | +- Dependencies will be automatically added |
| 209 | + |
| 210 | +**Migration steps**: |
| 211 | +1. Accept template updates to `pyproject.toml` (adds dependencies) |
| 212 | +2. Accept template updates to `__/imports.py` (adds frigid import) |
| 213 | +3. Review `exceptions.py` changes: |
| 214 | + - Base class changes from plain to `__.immut.exceptions.Omniexception` |
| 215 | + - Custom exception subclasses should merge cleanly (additive changes) |
| 216 | + |
| 217 | +**If they had `inject_exceptions: false`**: |
| 218 | +- No changes needed |
| 219 | +- Can optionally enable foundations if desired |
| 220 | + |
| 221 | +### For Foundation Packages |
| 222 | + |
| 223 | +1. **Before template update**: Set `inject_foundations: false` in copier answers |
| 224 | +2. **Run template update**: Should see no changes to exceptions or dependencies |
| 225 | +3. **Verify**: Existing hand-crafted code remains untouched |
| 226 | + |
| 227 | +## Open Questions |
| 228 | + |
| 229 | +1. **Dependency versions**: What are the current minimum versions for `frigid`, `absence`, `dynadoc`? |
| 230 | + |
| 231 | +2. **Import cascade structure**: What is the current pattern in `__/imports.py`? How should frigid import be integrated? |
| 232 | + |
| 233 | +3. **render_as_markdown()**: Should this method be in the base `Omnierror` template, or is it project-specific? |
| 234 | + |
| 235 | +4. **accretive and absence status**: Do these packages currently have foundation injection enabled? Need to verify their copier answers. |
| 236 | + |
| 237 | +5. **Other common dependencies**: Are there other packages besides `frigid`, `absence`, `dynadoc` that should be added as defaults? |
| 238 | + |
| 239 | +## Success Criteria |
| 240 | + |
| 241 | +- ✅ New projects with `inject_foundations: true` get immutable exceptions and common dependencies |
| 242 | +- ✅ Foundation packages (frigid, absence, accretive, classcore) can disable injection cleanly |
| 243 | +- ✅ Existing projects can update with minimal conflicts |
| 244 | +- ✅ Documentation clearly explains when to enable/disable foundations |
| 245 | +- ✅ All verification tests pass |
| 246 | + |
| 247 | +## Future Considerations |
| 248 | + |
| 249 | +- Monitor merge conflicts over time to validate assumption they remain rare |
| 250 | +- If conflicts become problematic, consider adding example-based escape hatch |
| 251 | +- Consider whether other foundational constructs should be injected in future |
0 commit comments