|
| 1 | +# brewkit 🍻 |
| 2 | + |
| 3 | +[](https://github.com/AdametherzLab/brewkit/actions) [](https://www.typescriptlang.org/) [](LICENSE) |
| 4 | + |
| 5 | +**Homebrew's secret sauce** - Precision brewing calculations for grain, hops, and water chemistry. Never guess your strike temps or IBUs again! |
| 6 | + |
| 7 | +## 🔥 Features |
| 8 | +- ✅ Mash calculations: Strike water temps, infusion volumes, efficiency |
| 9 | +- 🌡️ IBU formulas: Tinseth vs Rager methods with hop contribution breakdown |
| 10 | +- 🧪 Water chemistry: Mineral adjustments and profile balancing |
| 11 | +- ⚖️ Unit-aware: Metric/imperial conversions built-in |
| 12 | +- 🛠️ Fully typed API with production-grade error handling |
| 13 | + |
| 14 | +## 📦 Installation |
| 15 | +```bash |
| 16 | +npm install @adametherzlab/brewkit |
| 17 | +# or |
| 18 | +bun add @adametherzlab/brewkit |
| 19 | +``` |
| 20 | + |
| 21 | +## 🚀 Quick Start |
| 22 | +```typescript |
| 23 | +// REMOVED external import: import { strikeWaterTemp, calculateIBU, adjustWater } from '@adametherzlab/brewkit'; |
| 24 | +// REMOVED external import: import type { MashConfig, HopAddition } from '@adametherzlab/brewkit'; |
| 25 | + |
| 26 | +// Calculate strike water temperature |
| 27 | +const mashConfig: MashConfig = { |
| 28 | + grainWeight: 5.4, |
| 29 | + waterRatio: 2.8, |
| 30 | + targetTemp: 67, |
| 31 | + grainTemp: 18 |
| 32 | +}; |
| 33 | +console.log(`Heat water to ${strikeWaterTemp(mashConfig)}°C`); // → 72.3°C |
| 34 | + |
| 35 | +// Determine IBUs for recipe |
| 36 | +const hops: HopAddition[] = [ |
| 37 | + { name: 'Amarillo', alphaAcid: 8.5, weight: 50, boilTime: 60 }, |
| 38 | + { name: 'Mosaic', alphaAcid: 12.2, weight: 30, boilTime: 15 } |
| 39 | +]; |
| 40 | +const ibuResult = calculateIBU(hops, 1.055, 5.5, 'TINSETH'); |
| 41 | +console.log(`Total IBU: ${ibuResult.totalIBU.toFixed(1)}`); // → 45.6 IBU |
| 42 | +``` |
| 43 | + |
| 44 | +## 📚 API Reference |
| 45 | + |
| 46 | +### Mash Calculations |
| 47 | +**`strikeWaterTemp(config: MashConfig): number`** |
| 48 | +*Params:* `grainWeight`(kg), `waterRatio`(L/kg), `targetTemp`(°C), `grainTemp`(°C) |
| 49 | +*Returns:* Strike temperature in °C |
| 50 | +```typescript |
| 51 | +strikeWaterTemp({ grainWeight: 4.5, waterRatio: 3, targetTemp: 65, grainTemp: 22 }); // → 70.1 |
| 52 | +``` |
| 53 | + |
| 54 | +**`infusionVolume(currentVolume: number, grainWeight: number, currentTemp: number, targetTemp: number): number`** |
| 55 | +*Params:* Current volume (L), grain weight (kg), current/target temps (°C) |
| 56 | +*Returns:* Liters of boiling water needed |
| 57 | +```typescript |
| 58 | +infusionVolume(15, 5, 63, 68); // → 4.87 L |
| 59 | +``` |
| 60 | + |
| 61 | +### Bitterness Calculations |
| 62 | +**`calculateIBU(additions: HopAddition[], og: number, volumeGallons: number, method: IBUMethod): IBUResult`** |
| 63 | +*Params:* Hop additions array, original gravity (1.XXX), batch volume (gal) |
| 64 | +*Returns:* Object with total IBU and per-hop breakdown |
| 65 | +```typescript |
| 66 | +calculateIBU([{alphaAcid: 6, weight: 30, boilTime: 60}], 1.050, 5, 'RAGER'); |
| 67 | +``` |
| 68 | + |
| 69 | +### Water Chemistry |
| 70 | +**`adjustWater(source: WaterProfile, target: WaterProfile, volumeLiters: number): MineralAddition[]`** |
| 71 | +*Throws if:* Target requires ion reduction (use RO water instead) |
| 72 | +```typescript |
| 73 | +adjustWater( |
| 74 | + {Ca: 50, Mg: 10, SO4: 80, Cl: 40}, |
| 75 | + {Ca: 150, Mg: 20, SO4: 300, Cl: 50}, |
| 76 | + 25 |
| 77 | +); |
| 78 | +// → [{mineral: 'CaSO4', grams: 12.4, volume: 25}] |
| 79 | +``` |
| 80 | + |
| 81 | +## 🧪 Advanced Usage: Full Recipe Setup |
| 82 | +```typescript |
| 83 | +// TypeScript example |
| 84 | +import { |
| 85 | + strikeWaterTemp, |
| 86 | + calculateIBU, |
| 87 | + adjustWater, |
| 88 | + BrewTypes, |
| 89 | + type MashConfig, |
| 90 | + type HopAddition |
| 91 | +} from '@adametherzlab/brewkit'; |
| 92 | + |
| 93 | +// Configure mash for American IPA |
| 94 | +const mashSetup: MashConfig = { |
| 95 | + grainWeight: 6.8, |
| 96 | + waterRatio: 2.6, |
| 97 | + targetTemp: 66, |
| 98 | + grainTemp: 20.5 |
| 99 | +}; |
| 100 | +const strike = strikeWaterTemp(mashSetup); |
| 101 | + |
| 102 | +// Calculate hop schedule |
| 103 | +const hopAdditions: HopAddition[] = [ |
| 104 | + { name: 'Centennial', alphaAcid: 10, weight: 60, boilTime: 60 }, |
| 105 | + { name: 'Simcoe', alphaAcid: 13.5, weight: 40, boilTime: 10 } |
| 106 | +]; |
| 107 | +const ibu = calculateIBU(hopAdditions, 1.065, 6, BrewTypes.IBUMethod.TINSETH); |
| 108 | + |
| 109 | +// Adjust water profile for crisp bitterness |
| 110 | +const additions = adjustWater( |
| 111 | + { Ca: 75, Mg: 8, SO4: 120, Cl: 50 }, |
| 112 | + { Ca: 150, Mg: 15, SO4: 300, Cl: 75 }, |
| 113 | + 25 |
| 114 | +); |
| 115 | +``` |
| 116 | + |
| 117 | +## 🧠 Knowledge Base |
| 118 | + |
| 119 | +### IBU Methods Showdown |
| 120 | +- **Tinseth** |
| 121 | + Formula: `IBU = (AA% * Util * Weight(g) * 7489) / (Vol(gal) * (1 + GA))` |
| 122 | + *GA = (OG - 1.050)/0.2* |
| 123 | + |
| 124 | +- **Rager** |
| 125 | + Formula: `IBU = (Weight(oz) * AA% * Util * 7489) / Vol(gal)` |
| 126 | + |
| 127 | +### Water Chemistry Primer |
| 128 | +| Beer Style | Ca (ppm) | SO4 (ppm) | Cl (ppm) | SO4/Cl Ratio |
| 129 | +|-----------------|----------|-----------|----------|------------- |
| 130 | +| Czech Pilsner | 50-70 | 30-50 | 20-30 | 1.5:1 |
| 131 | +| West Coast IPA | 100-150 | 200-400 | 50-100 | 3:1 |
| 132 | +| NEIPA | 100-150 | 50-100 | 150-200 | 1:2 |
| 133 | +| Dry Stout | 50-100 | 50-100 | 50-100 | 1:1 |
| 134 | + |
| 135 | +## 🚨 Troubleshooting |
| 136 | +**Units Gotchas** |
| 137 | +- All grain weights in **kilograms** |
| 138 | +- Water volumes in **liters** unless specified (IBU uses gallons) |
| 139 | +- Alpha acids as **percentage** (6.5% = 6.5, not 0.065) |
| 140 | +- Temperature always in **Celsius** |
| 141 | + |
| 142 | +**Common Errors** |
| 143 | +- `RangeError: Target temp exceeds boiling` → Check unit conversion (F vs C) |
| 144 | +- `Invalid mineral: NaCl` → Use standardized mineral names (CaSO4, MgCl2) |
| 145 | +- `Negative delta for Ca` → Start with softer water if reducing minerals |
| 146 | + |
| 147 | +## 🤝 Contributing |
| 148 | +See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines. |
| 149 | + |
| 150 | +## 📜 License |
| 151 | +MIT © [AdametherzLab](https://github.com/AdametherzLab) |
0 commit comments