···39394040---
41414242+### [Beaver's Big System](./beavers-big-system/)
4343+4444+A sequel trilogy about building maintainable maintenance software.
4545+4646+| Part | Title | Concepts | Status |
4747+|------|----------------------------------------------------------------------------|--------------------------------|-----------|
4848+| 1 | [The Mixup](./beavers-big-system/01-the-mixup.md) | Branded Types + Typestate | Available |
4949+| 2 | [The Categorization](./beavers-big-system/02-the-categorization.md) | ADTs + Pattern Matching | Available |
5050+| 3 | [The Queue](./beavers-big-system/03-the-queue.md) | Tracked Arrays + Refinements | Available |
5151+5252+**Setup:** After the election, Beaver builds "The System" — a work order tracker. But strings everywhere lead to chaos: mixed-up IDs, invalid states, unsorted queues. Owl guides the rebuild.
5353+5454+**Same characters** continue their journey, learning compile-time safety through infrastructure problems.
5555+5656+---
5757+4258## See Also
43594460- [Tutorial](../tutorial/) — Step-by-step guide building a complete application
···11+# The Mixup
22+33+*Part 1 of Beaver's Big System*
44+55+> In which the animals learn that not all strings are created equal,
66+> and some transitions should be impossible.
77+88+---
99+1010+The forest had never been busier. After the election, President Deer announced an ambitious infrastructure program: repair the old dam, reinforce the eastern bridge, clear the overgrown paths. The animals needed a way to track it all.
1111+1212+"I'll build a system!" Beaver declared at the town meeting. "I'm good at building things."
1313+1414+And Beaver was. Within three days, the Work Order Tracker was ready. A beautiful wooden board with slots for leaf-cards, each representing a job to be done.
1515+1616+"It's simple," Beaver explained proudly. "Each work order has an ID, a target — that's what we're fixing — and a status. When work starts, move the card to 'In Progress'. When it's done, move it to 'Done'."
1717+1818+Fox examined a card. "What's `dam-1` mean?"
1919+2020+"That's the ID. I just number them as they come in."
2121+2222+"And `dam-north`?"
2323+2424+"That's the target — the north dam."
2525+2626+"They're both strings that start with 'dam'," Fox observed. "That seems... fragile."
2727+2828+Beaver waved a paw dismissively. "It's fine. We all know what's what."
2929+3030+---
3131+3232+It was not fine.
3333+3434+The first incident happened on a Thursday. Fox, tired after a long day, grabbed the wrong card. He moved `dam-north` to the "Done" column — but he'd meant to update work order `dam-1`, which was about reinforcing the *south* dam.
3535+3636+"Why does this say the north dam is fixed?" Badger asked the next morning. "I was just there. The spillway is still cracked."
3737+3838+Beaver checked the board. "Oh no. Fox marked the wrong thing."
3939+4040+"They're labeled almost identically!" Fox protested. "dam-north, dam-1 — how was I supposed to know one's a *location* and one's an *order ID*?"
4141+4242+The second incident was worse. Rabbit, updating her burrow inspection report, typed the status as `in_progerss`. Nobody noticed for two days, because the system accepted any string at all.
4343+4444+"We have three work orders stuck in a status that doesn't exist," Owl observed, studying the board. "The system thinks they're valid."
4545+4646+"Can't we just check for typos?" Beaver asked.
4747+4848+"We could," said Owl. "But what about `In-Progress`, `in progress`, `InProgress`? How many variations do we check against?"
4949+5050+The third incident was the strangest. Badger found a work order marked "Done" with a completion time — but no start time. Someone had dragged it straight from "Pending" to "Done", skipping "In Progress" entirely.
5151+5252+"How is that possible?" Rabbit wrung her paws. "You can't finish work that never started!"
5353+5454+"The system doesn't know that," Beaver said quietly. "It lets you put any status anywhere."
5555+5656+---
5757+5858+That evening, Owl called a meeting.
5959+6060+"I've been studying the system," she said. "The problem isn't the design — it's the *types*. Everything is a string."
6161+6262+She scratched marks in the dirt:
6363+6464+```typescript
6565+// Beaver's original system
6666+type WorkOrder = {
6767+ id: string // "dam-1", "wo-47"
6868+ targetId: string // "dam-north", "bridge-east"
6969+ status: string // "pending", "in-progress", "done"
7070+}
7171+```
7272+7373+"Three different kinds of values, all represented the same way. The compiler can't tell them apart. So when Fox swaps an order ID with a target ID—"
7474+7575+"The system just accepts it," Fox finished. "Like it's perfectly normal."
7676+7777+"Exactly."
7878+7979+---
8080+8181+"Here's the first fix." Owl made a fresh leaf. "We create *distinct* types — even though they're still strings underneath."
8282+8383+```typescript
8484+import { type Branded, brand } from "purus-ts"
8585+8686+// Each ID type is distinct — you can't mix them up
8787+type WorkOrderId = Branded<string, "WorkOrderId">
8888+type DamId = Branded<string, "DamId">
8989+type BridgeId = Branded<string, "BridgeId">
9090+type PathId = Branded<string, "PathId">
9191+type BurrowId = Branded<string, "BurrowId">
9292+```
9393+9494+"They're called *branded types*," Owl explained. "A `DamId` is a string with a brand. A `BridgeId` is a string with a *different* brand. The compiler treats them as completely different types."
9595+9696+"Show me," said Fox, still skeptical.
9797+9898+Owl wrote more:
9999+100100+```typescript
101101+// Creating branded IDs
102102+const damNorth: DamId = brand("dam-north")
103103+const workOrder1: WorkOrderId = brand("wo-1")
104104+105105+// This compiles — same types
106106+const validAssignment: DamId = damNorth
107107+108108+// This FAILS to compile — different brands!
109109+// const invalidAssignment: DamId = workOrder1
110110+// Error: Type 'WorkOrderId' is not assignable to type 'DamId'
111111+```
112112+113113+"So my mistake last Thursday—" Fox began.
114114+115115+"Would have been caught immediately. The compiler would refuse to accept a `WorkOrderId` where a `DamId` was expected."
116116+117117+Beaver's eyes widened. "We can make it impossible to mix them up?"
118118+119119+"At compile time. Before the code ever runs."
120120+121121+---
122122+123123+"But that's only half the problem," said Rabbit. "What about the status? The typos? The impossible transitions?"
124124+125125+Owl nodded. "For that, we need *typestate*."
126126+127127+She drew a diagram:
128128+129129+```
130130+ start() complete()
131131+ Pending ─────────► InProgress ─────────► Completed
132132+ │ │
133133+ └──────────────── cannot skip ─────────────────┘
134134+```
135135+136136+"A work order isn't just 'pending' or 'done'. It's *in a state*. And certain operations are only valid in certain states."
137137+138138+```typescript
139139+// Each state is a distinct type
140140+type PendingWork = {
141141+ readonly _state: "Pending"
142142+ readonly id: WorkOrderId
143143+ readonly targetId: DamId | BridgeId | PathId | BurrowId
144144+ readonly createdAt: number
145145+}
146146+147147+type InProgressWork = {
148148+ readonly _state: "InProgress"
149149+ readonly id: WorkOrderId
150150+ readonly targetId: DamId | BridgeId | PathId | BurrowId
151151+ readonly createdAt: number
152152+ readonly startedAt: number // Must have a start time
153153+}
154154+155155+type CompletedWork = {
156156+ readonly _state: "Completed"
157157+ readonly id: WorkOrderId
158158+ readonly targetId: DamId | BridgeId | PathId | BurrowId
159159+ readonly createdAt: number
160160+ readonly startedAt: number
161161+ readonly completedAt: number // Must have a completion time
162162+}
163163+164164+// The union of all possible states
165165+type WorkOrder = PendingWork | InProgressWork | CompletedWork
166166+```
167167+168168+"Each state type has exactly the fields that make sense for that state," Owl continued. "Pending work doesn't have a `startedAt`. Completed work must have both times."
169169+170170+---
171171+172172+"Now watch what happens with the transitions." Owl wrote the functions:
173173+174174+```typescript
175175+// Can only start work that's pending
176176+const startWork = (work: PendingWork): InProgressWork => ({
177177+ ...work,
178178+ _state: "InProgress",
179179+ startedAt: Date.now(),
180180+})
181181+182182+// Can only complete work that's in progress
183183+const completeWork = (work: InProgressWork): CompletedWork => ({
184184+ ...work,
185185+ _state: "Completed",
186186+ completedAt: Date.now(),
187187+})
188188+```
189189+190190+"The types enforce the rules. You *can't* call `completeWork` on `PendingWork`—"
191191+192192+"Because it expects `InProgressWork`," Beaver said, catching on.
193193+194194+"So Badger's impossible work order—" Rabbit started.
195195+196196+"Could never exist. The only way to get a `CompletedWork` is through `completeWork`, which requires `InProgressWork`, which requires `startWork` on `PendingWork`. The chain is unbreakable."
197197+198198+---
199199+200200+Fox leaned forward. "What about the typos? The `in_progerss` problem?"
201201+202202+"Gone." Owl smiled. "There is no `status: string` anymore. The state is encoded in the *type*. You can't misspell a type."
203203+204204+```typescript
205205+// This compiles - proper state
206206+const pending: PendingWork = {
207207+ _state: "Pending",
208208+ id: brand("wo-1"),
209209+ targetId: brand("dam-north"),
210210+ createdAt: Date.now(),
211211+}
212212+213213+// This FAILS - not a valid state value
214214+// const invalid: PendingWork = {
215215+// _state: "Pendnig", // Typo!
216216+// ...
217217+// }
218218+// Error: Type '"Pendnig"' is not assignable to type '"Pending"'
219219+```
220220+221221+"The compiler checks the literal string against the expected value. `"Pendnig"` is not `"Pending"`, so it's rejected."
222222+223223+---
224224+225225+Beaver had been quiet, thinking. "Let me rebuild the system."
226226+227227+```typescript
228228+import { type Branded, brand, match, pipe } from "purus-ts"
229229+230230+// =================================================================
231231+// Branded IDs — Each infrastructure type has its own ID type
232232+// =================================================================
233233+234234+type WorkOrderId = Branded<string, "WorkOrderId">
235235+type DamId = Branded<string, "DamId">
236236+type BridgeId = Branded<string, "BridgeId">
237237+238238+const workOrderId = (id: string): WorkOrderId => brand(id)
239239+const damId = (id: string): DamId => brand(id)
240240+const bridgeId = (id: string): BridgeId => brand(id)
241241+242242+// =================================================================
243243+// Typestate — Work orders progress through defined states
244244+// =================================================================
245245+246246+type TargetId = DamId | BridgeId
247247+248248+type PendingWork = {
249249+ readonly _state: "Pending"
250250+ readonly id: WorkOrderId
251251+ readonly targetId: TargetId
252252+ readonly description: string
253253+ readonly createdAt: number
254254+}
255255+256256+type InProgressWork = {
257257+ readonly _state: "InProgress"
258258+ readonly id: WorkOrderId
259259+ readonly targetId: TargetId
260260+ readonly description: string
261261+ readonly createdAt: number
262262+ readonly startedAt: number
263263+ readonly assignee: string
264264+}
265265+266266+type CompletedWork = {
267267+ readonly _state: "Completed"
268268+ readonly id: WorkOrderId
269269+ readonly targetId: TargetId
270270+ readonly description: string
271271+ readonly createdAt: number
272272+ readonly startedAt: number
273273+ readonly completedAt: number
274274+ readonly assignee: string
275275+ readonly notes: string
276276+}
277277+278278+type WorkOrder = PendingWork | InProgressWork | CompletedWork
279279+280280+// =================================================================
281281+// State Transitions — Each function enforces valid progression
282282+// =================================================================
283283+284284+const createWorkOrder = (
285285+ id: WorkOrderId,
286286+ targetId: TargetId,
287287+ description: string,
288288+): PendingWork => ({
289289+ _state: "Pending",
290290+ id,
291291+ targetId,
292292+ description,
293293+ createdAt: Date.now(),
294294+})
295295+296296+const startWork = (
297297+ work: PendingWork,
298298+ assignee: string,
299299+): InProgressWork => ({
300300+ ...work,
301301+ _state: "InProgress",
302302+ startedAt: Date.now(),
303303+ assignee,
304304+})
305305+306306+const completeWork = (
307307+ work: InProgressWork,
308308+ notes: string,
309309+): CompletedWork => ({
310310+ ...work,
311311+ _state: "Completed",
312312+ completedAt: Date.now(),
313313+ notes,
314314+})
315315+```
316316+317317+"Beautiful," said Owl. "Now let's see it catch errors."
318318+319319+---
320320+321321+Beaver demonstrated:
322322+323323+```typescript
324324+// Create some work orders
325325+const damWork = createWorkOrder(
326326+ workOrderId("wo-1"),
327327+ damId("dam-north"),
328328+ "Repair spillway crack",
329329+)
330330+331331+const bridgeWork = createWorkOrder(
332332+ workOrderId("wo-2"),
333333+ bridgeId("bridge-east"),
334334+ "Reinforce support beams",
335335+)
336336+337337+// This WORKS - proper progression
338338+const started = startWork(damWork, "Beaver")
339339+const completed = completeWork(started, "Spillway sealed with river clay")
340340+341341+// This FAILS - can't skip states!
342342+// const badComplete = completeWork(damWork, "Done!")
343343+// Error: Argument of type 'PendingWork' is not assignable
344344+// to parameter of type 'InProgressWork'
345345+346346+// This FAILS - can't mix ID types!
347347+// const confused = createWorkOrder(
348348+// damId("dam-north"), // Oops, used DamId instead of WorkOrderId
349349+// damId("dam-north"),
350350+// "Fix something",
351351+// )
352352+// Error: Argument of type 'DamId' is not assignable
353353+// to parameter of type 'WorkOrderId'
354354+```
355355+356356+"Every mistake I made last month," said Fox slowly, "would be caught before the code even runs."
357357+358358+"That's the power of the type system," said Owl. "It's not about more checking. It's about making invalid states *unrepresentable*."
359359+360360+---
361361+362362+The moon was rising over the forest. The animals gathered their leaves and prepared to leave.
363363+364364+"This is good," said Badger. "But I notice we're treating all work orders the same. Dam repairs and bridge work and path clearing — they're all just `WorkOrder`."
365365+366366+"Is that a problem?" asked Beaver.
367367+368368+"Dam repairs need water level checks," Badger said. "Bridge work needs weight capacity assessments. Path clearing needs different equipment. They're not really the same thing at all."
369369+370370+Owl gathered her notes. "Then tomorrow, we'll talk about *discriminated unions*. Different types of work, each with their own requirements — all handled safely."
371371+372372+"More types," Fox muttered, but there was no complaint in his voice. "I'm starting to see why."
373373+374374+Rabbit had stopped wringing her paws. For the first time in days, she looked calm.
375375+376376+"No more mystery statuses," she said. "No more impossible transitions. The system knows what's valid."
377377+378378+Beaver looked at the wooden board with its leaf-cards. Tomorrow, they'd build something better. Something that couldn't fail in the ways the old system had failed.
379379+380380+"Same time tomorrow?" Beaver asked.
381381+382382+The animals nodded and dispersed into the moonlit forest.
383383+384384+---
385385+386386+## What We Learned
387387+388388+1. **Branded types create distinct types from primitives** — `DamId` and `WorkOrderId` are both strings, but the compiler treats them as incompatible. Use `brand()` to create values, and the type system prevents mixing.
389389+390390+2. **Typestate encodes valid states as types** — Instead of a `status: string` field, each state is its own type (`PendingWork`, `InProgressWork`, `CompletedWork`). The type itself carries the state.
391391+392392+3. **State transitions become functions with typed signatures** — `startWork(PendingWork) → InProgressWork` makes it impossible to start completed work or complete pending work.
393393+394394+4. **Literal types catch typos at compile time** — `"Pending"` is not a string, it's a specific literal type. `"Pendnig"` won't match.
395395+396396+5. **Invalid states become unrepresentable** — You can't construct a `CompletedWork` without going through the proper transitions. The type system enforces the business rules.
397397+398398+---
399399+400400+## Try It Yourself
401401+402402+The complete code from this story is available as a runnable example:
403403+404404+```bash
405405+bun examples/stories/beavers-big-system/01-branded-typestate.ts
406406+```
407407+408408+Try creating work orders with mismatched ID types, or completing work that was never started. Watch the compiler catch your mistakes.
409409+410410+---
411411+412412+*Next: [The Categorization](./02-the-categorization.md) — where the animals learn to distinguish different kinds of work*
···11+# The Categorization
22+33+*Part 2 of Beaver's Big System*
44+55+> In which the animals learn that different work requires different data,
66+> and the compiler can ensure you handle every case.
77+88+---
99+1010+The morning sun filtered through the canopy as the animals reconvened. Beaver had brought fresh leaves, ready to continue the rebuild.
1111+1212+"Yesterday we fixed the ID confusion and the impossible states," Owl began. "But Badger raised an important point."
1313+1414+"Different work needs different handling," Badger repeated. "My dam repairs need water level readings. You can't schedule work when the river's too high."
1515+1616+"And bridge inspections need weight capacity checks," added Rabbit. "I won't cross a bridge until I know it can hold me."
1717+1818+Beaver pulled out the current type definition:
1919+2020+```typescript
2121+type TargetId = DamId | BridgeId | PathId | BurrowId
2222+2323+type PendingWork = {
2424+ readonly _state: "Pending"
2525+ readonly id: WorkOrderId
2626+ readonly targetId: TargetId
2727+ readonly description: string
2828+ readonly createdAt: number
2929+}
3030+```
3131+3232+"The problem is `description: string`," Owl observed. "It's just... text. Someone writes 'check water level' for dam work, 'assess capacity' for bridges. But nothing *enforces* that structure."
3333+3434+"I added a `type` field," Beaver admitted. "But look what happened."
3535+3636+```typescript
3737+// Beaver's first attempt
3838+type WorkType = string
3939+4040+const work1 = { type: "Dam", ... }
4141+const work2 = { type: "dam", ... }
4242+const work3 = { type: "DAM", ... }
4343+const work4 = { type: "water-structure", ... } // Fox's contribution
4444+```
4545+4646+Fox shrugged. "I thought 'dam' was too informal."
4747+4848+---
4949+5050+"The real issue," said Owl, "is that different kinds of work have different *shapes*. Dam repairs need a water level. Bridge work needs a weight capacity. Path clearing needs equipment type. They're not the same structure at all."
5151+5252+She sketched in the dirt:
5353+5454+```typescript
5555+// What we actually need
5656+5757+Dam Repair:
5858+ - damId: DamId
5959+ - waterLevel: number
6060+ - repairType: "structural" | "spillway" | "foundation"
6161+6262+Bridge Work:
6363+ - bridgeId: BridgeId
6464+ - weightCapacity: number
6565+ - inspectionRequired: boolean
6666+6767+Path Clearing:
6868+ - pathId: PathId
6969+ - equipment: "saw" | "shovel" | "rake"
7070+ - estimatedLength: number
7171+7272+Burrow Inspection:
7373+ - burrowId: BurrowId
7474+ - size: "small" | "medium" | "large"
7575+ - occupant: string
7676+```
7777+7878+"Four different structures," said Beaver. "How do we represent that in one system?"
7979+8080+"With a *discriminated union*," said Owl. "Also called an algebraic data type, or ADT."
8181+8282+---
8383+8484+"Watch carefully." Owl wrote new types:
8585+8686+```typescript
8787+type DamRepair = {
8888+ readonly _tag: "DamRepair"
8989+ readonly damId: DamId
9090+ readonly waterLevel: number
9191+ readonly repairType: "structural" | "spillway" | "foundation"
9292+}
9393+9494+type BridgeWork = {
9595+ readonly _tag: "BridgeWork"
9696+ readonly bridgeId: BridgeId
9797+ readonly weightCapacity: number
9898+ readonly inspectionRequired: boolean
9999+}
100100+101101+type PathClearing = {
102102+ readonly _tag: "PathClearing"
103103+ readonly pathId: PathId
104104+ readonly equipment: "saw" | "shovel" | "rake"
105105+ readonly estimatedLength: number
106106+}
107107+108108+type BurrowInspection = {
109109+ readonly _tag: "BurrowInspection"
110110+ readonly burrowId: BurrowId
111111+ readonly size: "small" | "medium" | "large"
112112+ readonly occupant: string
113113+}
114114+115115+// The union of all work types
116116+type WorkDetails =
117117+ | DamRepair
118118+ | BridgeWork
119119+ | PathClearing
120120+ | BurrowInspection
121121+```
122122+123123+"Each variant has a `_tag` field," Owl explained. "That's the *discriminant*. When you have a `WorkDetails` value, you can check the `_tag` to know which variant you're dealing with."
124124+125125+"Like a label," said Beaver.
126126+127127+"Exactly. But unlike your string `type` field, these are the *only* valid values. There's no `"dam"` vs `"Dam"` confusion. Either it's `"DamRepair"` or the code doesn't compile."
128128+129129+---
130130+131131+"Now here's where it gets powerful." Owl pulled out a fresh leaf. "What if I need to estimate how long a job will take?"
132132+133133+```typescript
134134+// Bad approach — manual checking, easy to forget cases
135135+const estimateTime = (work: WorkDetails): number => {
136136+ if (work._tag === "DamRepair") {
137137+ return work.waterLevel > 5 ? 8 : 4 // High water = longer job
138138+ }
139139+ if (work._tag === "BridgeWork") {
140140+ return work.inspectionRequired ? 6 : 3
141141+ }
142142+ // Oops, forgot PathClearing and BurrowInspection!
143143+ return 2 // Default... but is this right?
144144+}
145145+```
146146+147147+"This compiles," Owl said. "TypeScript doesn't complain. But when someone adds a `PathClearing` work order, it silently gets the wrong estimate."
148148+149149+Rabbit's ears flattened. "That's the kind of bug that hides for weeks."
150150+151151+"Now watch this." Owl rewrote the function:
152152+153153+```typescript
154154+import { match } from "purus-ts"
155155+156156+const estimateTime = (work: WorkDetails): number =>
157157+ match(work)({
158158+ DamRepair: ({ waterLevel }) => waterLevel > 5 ? 8 : 4,
159159+ BridgeWork: ({ inspectionRequired }) => inspectionRequired ? 6 : 3,
160160+ PathClearing: ({ estimatedLength }) => Math.ceil(estimatedLength / 100),
161161+ BurrowInspection: ({ size }) =>
162162+ size === "large" ? 3 : size === "medium" ? 2 : 1,
163163+ })
164164+```
165165+166166+"This is *exhaustive* pattern matching. The `match` function requires a handler for *every* variant. If you forget one—"
167167+168168+She erased the `BurrowInspection` line.
169169+170170+```typescript
171171+// Missing BurrowInspection handler
172172+const estimateTime = (work: WorkDetails): number =>
173173+ match(work)({
174174+ DamRepair: ({ waterLevel }) => waterLevel > 5 ? 8 : 4,
175175+ BridgeWork: ({ inspectionRequired }) => inspectionRequired ? 6 : 3,
176176+ PathClearing: ({ estimatedLength }) => Math.ceil(estimatedLength / 100),
177177+ // Error: Property 'BurrowInspection' is missing in type...
178178+ })
179179+```
180180+181181+"The compiler catches it immediately!"
182182+183183+---
184184+185185+Fox had been studying the examples. "What happens when we add a new type of work? Like... tree trimming?"
186186+187187+"That's the best part." Owl grinned — as much as an owl can grin. "Let's add it."
188188+189189+```typescript
190190+type TreeTrimming = {
191191+ readonly _tag: "TreeTrimming"
192192+ readonly treeId: TreeId
193193+ readonly height: number
194194+ readonly species: string
195195+}
196196+197197+// Updated union
198198+type WorkDetails =
199199+ | DamRepair
200200+ | BridgeWork
201201+ | PathClearing
202202+ | BurrowInspection
203203+ | TreeTrimming // New!
204204+```
205205+206206+"Now every `match` in the codebase breaks."
207207+208208+```typescript
209209+const estimateTime = (work: WorkDetails): number =>
210210+ match(work)({
211211+ DamRepair: ({ waterLevel }) => waterLevel > 5 ? 8 : 4,
212212+ BridgeWork: ({ inspectionRequired }) => inspectionRequired ? 6 : 3,
213213+ PathClearing: ({ estimatedLength }) => Math.ceil(estimatedLength / 100),
214214+ BurrowInspection: ({ size }) =>
215215+ size === "large" ? 3 : size === "medium" ? 2 : 1,
216216+ // Error: Property 'TreeTrimming' is missing in type...
217217+ })
218218+```
219219+220220+"The compiler tells you exactly where to add handling. You can't forget."
221221+222222+"That's..." Fox paused. "That's actually brilliant."
223223+224224+---
225225+226226+Beaver started rebuilding the system with the new concepts:
227227+228228+```typescript
229229+import { type Branded, brand, match, pipe } from "purus-ts"
230230+231231+// =================================================================
232232+// Branded IDs
233233+// =================================================================
234234+235235+type WorkOrderId = Branded<string, "WorkOrderId">
236236+type DamId = Branded<string, "DamId">
237237+type BridgeId = Branded<string, "BridgeId">
238238+type PathId = Branded<string, "PathId">
239239+type BurrowId = Branded<string, "BurrowId">
240240+241241+// =================================================================
242242+// Work Detail Types — Each kind of work has its own shape
243243+// =================================================================
244244+245245+type DamRepair = {
246246+ readonly _tag: "DamRepair"
247247+ readonly damId: DamId
248248+ readonly waterLevel: number
249249+ readonly repairType: "structural" | "spillway" | "foundation"
250250+}
251251+252252+type BridgeWork = {
253253+ readonly _tag: "BridgeWork"
254254+ readonly bridgeId: BridgeId
255255+ readonly weightCapacity: number
256256+ readonly inspectionRequired: boolean
257257+}
258258+259259+type PathClearing = {
260260+ readonly _tag: "PathClearing"
261261+ readonly pathId: PathId
262262+ readonly equipment: "saw" | "shovel" | "rake"
263263+ readonly estimatedLength: number
264264+}
265265+266266+type BurrowInspection = {
267267+ readonly _tag: "BurrowInspection"
268268+ readonly burrowId: BurrowId
269269+ readonly size: "small" | "medium" | "large"
270270+ readonly occupant: string
271271+}
272272+273273+type WorkDetails =
274274+ | DamRepair
275275+ | BridgeWork
276276+ | PathClearing
277277+ | BurrowInspection
278278+279279+// =================================================================
280280+// Work Order with Details
281281+// =================================================================
282282+283283+type WorkOrder<S extends string, D extends WorkDetails> = {
284284+ readonly _state: S
285285+ readonly id: WorkOrderId
286286+ readonly details: D
287287+ readonly createdAt: number
288288+}
289289+290290+type PendingWork<D extends WorkDetails> = WorkOrder<"Pending", D>
291291+type InProgressWork<D extends WorkDetails> = WorkOrder<"InProgress", D> & {
292292+ readonly startedAt: number
293293+ readonly assignee: string
294294+}
295295+type CompletedWork<D extends WorkDetails> = WorkOrder<"Completed", D> & {
296296+ readonly startedAt: number
297297+ readonly completedAt: number
298298+ readonly assignee: string
299299+}
300300+```
301301+302302+"I combined the typestate from yesterday with the discriminated union," Beaver explained. "Each work order has *both* a state and a details type."
303303+304304+---
305305+306306+"Let's write some utility functions," Owl suggested.
307307+308308+```typescript
309309+// Estimate time based on work type
310310+const estimateHours = (details: WorkDetails): number =>
311311+ match(details)({
312312+ DamRepair: ({ waterLevel, repairType }) =>
313313+ repairType === "foundation" ? 12 :
314314+ waterLevel > 5 ? 8 : 4,
315315+ BridgeWork: ({ inspectionRequired, weightCapacity }) =>
316316+ inspectionRequired ? 6 :
317317+ weightCapacity < 1000 ? 2 : 4,
318318+ PathClearing: ({ estimatedLength, equipment }) =>
319319+ equipment === "saw" ? Math.ceil(estimatedLength / 50) :
320320+ Math.ceil(estimatedLength / 100),
321321+ BurrowInspection: ({ size }) =>
322322+ size === "large" ? 3 : size === "medium" ? 2 : 1,
323323+ })
324324+325325+// Get a human-readable description
326326+const describeWork = (details: WorkDetails): string =>
327327+ match(details)({
328328+ DamRepair: ({ repairType }) =>
329329+ `Dam ${repairType} repair`,
330330+ BridgeWork: ({ inspectionRequired }) =>
331331+ inspectionRequired ? "Bridge inspection and repair" : "Bridge maintenance",
332332+ PathClearing: ({ equipment, estimatedLength }) =>
333333+ `Clear ${estimatedLength}m of path using ${equipment}`,
334334+ BurrowInspection: ({ occupant, size }) =>
335335+ `Inspect ${size} burrow (${occupant}'s home)`,
336336+ })
337337+338338+// Check if work requires special equipment
339339+const requiresEquipment = (details: WorkDetails): boolean =>
340340+ match(details)({
341341+ DamRepair: () => true, // Always need tools
342342+ BridgeWork: () => true,
343343+ PathClearing: () => true,
344344+ BurrowInspection: () => false, // Just looking
345345+ })
346346+```
347347+348348+"Every function that operates on `WorkDetails` is forced to consider all cases," Owl noted. "You can't accidentally treat bridge work like dam repair."
349349+350350+---
351351+352352+Rabbit had been thinking. "What if I only want to handle some cases? Like, I only care about burrow inspections."
353353+354354+Owl nodded. "There's a variant for that — `matchOr`. You provide handlers for the cases you care about, and a default for the rest."
355355+356356+```typescript
357357+import { matchOr } from "purus-ts"
358358+359359+// Only Rabbit cares about burrows
360360+const isRabbitRelevant = (details: WorkDetails): boolean =>
361361+ matchOr(false)(details)({
362362+ BurrowInspection: () => true,
363363+ })
364364+```
365365+366366+"But use it carefully," Owl warned. "If you add a new work type that Rabbit *should* care about, the default will swallow it silently. Exhaustive matching is safer."
367367+368368+"When in doubt, handle every case," said Fox. He'd been won over.
369369+370370+---
371371+372372+Beaver demonstrated the complete system:
373373+374374+```typescript
375375+// Create some work orders
376376+const damWork: PendingWork<DamRepair> = {
377377+ _state: "Pending",
378378+ id: brand("wo-1"),
379379+ details: {
380380+ _tag: "DamRepair",
381381+ damId: brand("dam-north"),
382382+ waterLevel: 3.2,
383383+ repairType: "spillway",
384384+ },
385385+ createdAt: Date.now(),
386386+}
387387+388388+const burrowWork: PendingWork<BurrowInspection> = {
389389+ _state: "Pending",
390390+ id: brand("wo-2"),
391391+ details: {
392392+ _tag: "BurrowInspection",
393393+ burrowId: brand("burrow-rabbit-1"),
394394+ size: "medium",
395395+ occupant: "Rabbit",
396396+ },
397397+ createdAt: Date.now(),
398398+}
399399+400400+// Process work orders
401401+console.log(describeWork(damWork.details))
402402+// "Dam spillway repair"
403403+404404+console.log(describeWork(burrowWork.details))
405405+// "Inspect medium burrow (Rabbit's home)"
406406+407407+console.log(`Dam work estimate: ${estimateHours(damWork.details)} hours`)
408408+// "Dam work estimate: 4 hours"
409409+410410+console.log(`Burrow work estimate: ${estimateHours(burrowWork.details)} hours`)
411411+// "Burrow work estimate: 2 hours"
412412+```
413413+414414+"Each work order knows exactly what kind of work it is," Beaver said proudly. "And the compiler knows too."
415415+416416+---
417417+418418+"Add bridge work to the mix," Fox suggested.
419419+420420+```typescript
421421+const bridgeWork: PendingWork<BridgeWork> = {
422422+ _state: "Pending",
423423+ id: brand("wo-3"),
424424+ details: {
425425+ _tag: "BridgeWork",
426426+ bridgeId: brand("bridge-east"),
427427+ weightCapacity: 500,
428428+ inspectionRequired: true,
429429+ },
430430+ createdAt: Date.now(),
431431+}
432432+433433+// Works seamlessly
434434+console.log(describeWork(bridgeWork.details))
435435+// "Bridge inspection and repair"
436436+437437+console.log(`Estimate: ${estimateHours(bridgeWork.details)} hours`)
438438+// "Estimate: 6 hours"
439439+```
440440+441441+"And if we add tree trimming next month—" Fox started.
442442+443443+"Every function with `match` will immediately show where we need to add handling," Owl finished. "The compiler guides you to completeness."
444444+445445+---
446446+447447+The afternoon sun was warm. The animals looked at their new system with satisfaction.
448448+449449+"We have distinct ID types," Beaver summarized. "Typestate for work progression. And now discriminated unions for different kinds of work."
450450+451451+"Each piece of work carries exactly the data it needs," said Owl. "No more guessing what fields apply to what type."
452452+453453+Badger cleared his throat. "This is good. But I have another concern."
454454+455455+The animals turned to him.
456456+457457+"We have dozens of work orders now. They need to be prioritized. Someone built a queue, but..." He shook his head. "Last night it crashed. Called `getNext()` on an empty queue."
458458+459459+"And the 'sorted by priority' list," Rabbit added, "wasn't actually sorted. Someone appended a new item and forgot to re-sort."
460460+461461+"Can the type system help with *that*?" asked Fox, genuinely curious now.
462462+463463+Owl gathered her leaves. "It can. Tomorrow, we'll talk about *tracked arrays*. Properties like 'non-empty' and 'sorted' — encoded in the type itself."
464464+465465+Beaver was already sketching notes for tomorrow. The system was taking shape, one type at a time.
466466+467467+---
468468+469469+## What We Learned
470470+471471+1. **Discriminated unions group related variants** — `WorkDetails` is one of four specific types, each with its own structure. The `_tag` field identifies which variant you have.
472472+473473+2. **Exhaustive matching with `match()`** — The `match` function requires handlers for every variant. Forget one, and the compiler tells you.
474474+475475+3. **Adding a variant breaks all incomplete matches** — When you add `TreeTrimming` to the union, every `match` that doesn't handle it becomes a compile error. You can't forget to update handlers.
476476+477477+4. **Each variant has the right data** — `DamRepair` has `waterLevel`, `BridgeWork` has `weightCapacity`. No optional fields, no runtime checks for "does this field exist?"
478478+479479+5. **Use `matchOr` for partial matching** — When you only care about some variants, `matchOr(defaultValue)(...)` handles the rest. But prefer exhaustive matching when possible.
480480+481481+---
482482+483483+## Try It Yourself
484484+485485+The complete code from this story is available as a runnable example:
486486+487487+```bash
488488+bun examples/stories/beavers-big-system/02-adt-matching.ts
489489+```
490490+491491+Try adding a new work type like `TreeTrimming` or `NestRepair`. Watch the compiler guide you to every place that needs updating.
492492+493493+---
494494+495495+*Next: [The Queue](./03-the-queue.md) — where the animals learn that arrays can track their own properties*
···11+# The Queue
22+33+*Part 3 of Beaver's Big System*
44+55+> In which the animals learn that arrays can remember their properties,
66+> and valid values should prove themselves.
77+88+---
99+1010+It was past midnight when the crash happened.
1111+1212+Beaver had built the priority queue three days ago — a simple system to order work by urgency. High-priority items at the front, low-priority at the back. Every morning, the crew would check the queue and grab the next job.
1313+1414+But tonight, the night crew found an empty queue and called `getNext()` anyway.
1515+1616+The system exploded.
1717+1818+"Array index out of bounds," Fox read from the error log. "At line 47: `return queue[0]`."
1919+2020+"But there should always be work in the queue," Beaver protested. "I checked before leaving."
2121+2222+"Apparently not." Badger rubbed his eyes. "The bridge crew finished early and cleared everything. Then the dam crew came in, saw the queue, and..."
2323+2424+"Boom," said Fox.
2525+2626+---
2727+2828+Morning came too soon. The animals gathered, bleary-eyed, around Owl's teaching stump.
2929+3030+"The queue had three problems," Owl began, drawing in the dirt. "First, the crash we saw last night — accessing an empty array."
3131+3232+```typescript
3333+// Beaver's original queue
3434+const getNext = (queue: WorkOrder[]): WorkOrder =>
3535+ queue[0] // Crashes if queue is empty!
3636+```
3737+3838+"Second," Owl continued, "the queue was supposed to be sorted by priority. But look at this."
3939+4040+```typescript
4141+const addWork = (queue: WorkOrder[], work: WorkOrder): WorkOrder[] =>
4242+ [...queue, work] // Just appends — doesn't maintain sort order!
4343+```
4444+4545+"Someone adds a high-priority item, and it goes to the *end* of the queue. The sorted property is gone."
4646+4747+Rabbit raised a paw. "And the third problem?"
4848+4949+Owl pointed to a leaf-card. "Priority values. This one has priority `-1`. Someone thought negative meant 'urgent'. This one has priority `999999`. Someone thought bigger was better."
5050+5151+"So priorities are just... whatever anyone types?" Fox asked.
5252+5353+"Exactly. No validation. No constraints."
5454+5555+---
5656+5757+"Let's fix the priority first," said Owl. "It's the foundation everything else builds on."
5858+5959+She wrote:
6060+6161+```typescript
6262+import { type Branded, brand, type Option, some, none } from "purus-ts"
6363+6464+// A priority is a non-negative integer
6565+type Priority = Branded<number, "Priority">
6666+6767+// Creating a valid priority — returns Option because it might fail
6868+const priority = (n: number): Option<Priority> =>
6969+ Number.isInteger(n) && n >= 0
7070+ ? some(brand(n))
7171+ : none
7272+```
7373+7474+"This is a *refinement*," Owl explained. "A `Priority` isn't just any number — it's a number that's passed validation. Non-negative, integer, branded."
7575+7676+"But it returns an `Option`," Beaver noted.
7777+7878+"Because validation can fail. If someone passes `-1`, they get `none`. The type system forces them to handle that case."
7979+8080+```typescript
8181+import { match } from "purus-ts"
8282+8383+const result = priority(-1)
8484+8585+match(result)({
8686+ Some: ({ value }) => console.log(`Valid priority: ${value}`),
8787+ None: () => console.log("Invalid priority — must be non-negative integer"),
8888+})
8989+// Output: "Invalid priority — must be non-negative integer"
9090+```
9191+9292+"No more mystery `-1` priorities," said Rabbit, relieved.
9393+9494+---
9595+9696+"Now for the array problems." Owl cleared a fresh space. "We need arrays that *remember* their properties."
9797+9898+```typescript
9999+import { type Arr, type NonEmpty, type Sorted, arr, nonEmpty, sortBy } from "purus-ts"
100100+101101+// A regular array — we know nothing about it
102102+type WorkQueue = Arr<WorkOrder>
103103+104104+// A non-empty array — guaranteed to have at least one element
105105+type NonEmptyQueue = Arr<WorkOrder, NonEmpty>
106106+107107+// A sorted array — guaranteed to be in order
108108+type SortedQueue = Arr<WorkOrder, Sorted>
109109+110110+// Both properties at once!
111111+type ReadyQueue = Arr<WorkOrder, NonEmpty | Sorted>
112112+```
113113+114114+"These are called *phantom types*," Owl said. "The `NonEmpty` and `Sorted` markers don't exist at runtime — they're purely compile-time information. But the type system tracks them."
115115+116116+Fox squinted. "So an `Arr<WorkOrder, NonEmpty>` is just a regular array underneath?"
117117+118118+"Yes. But the compiler treats it differently. Functions can *require* certain properties."
119119+120120+---
121121+122122+"Watch how this prevents last night's crash." Owl wrote the safe version:
123123+124124+```typescript
125125+import { head } from "purus-ts"
126126+127127+// head() REQUIRES a NonEmpty array — won't compile otherwise
128128+const getNext = (queue: Arr<WorkOrder, NonEmpty>): WorkOrder =>
129129+ head(queue) // Safe! We KNOW there's at least one element
130130+```
131131+132132+"If you try to pass a regular array—"
133133+134134+```typescript
135135+const emptyQueue: Arr<WorkOrder> = arr([])
136136+137137+// This FAILS to compile!
138138+// const work = getNext(emptyQueue)
139139+// Error: Argument of type 'Arr<WorkOrder>' is not assignable
140140+// to parameter of type 'Arr<WorkOrder, NonEmpty>'
141141+```
142142+143143+"The compiler stops you *before* the crash happens."
144144+145145+Beaver's eyes went wide. "So we can't call `getNext` on an empty queue at all?"
146146+147147+"Not unless you prove it's non-empty first."
148148+149149+---
150150+151151+"How do you prove it?" asked Rabbit.
152152+153153+Owl wrote:
154154+155155+```typescript
156156+const queue: Arr<WorkOrder> = arr([...]) // Regular array, unknown if empty
157157+158158+const result = nonEmpty(queue) // Returns Option<Arr<WorkOrder, NonEmpty>>
159159+160160+match(result)({
161161+ Some: ({ value }) => {
162162+ // Inside here, 'value' is Arr<WorkOrder, NonEmpty>
163163+ const next = getNext(value) // Safe!
164164+ console.log(`Next work: ${next.id}`)
165165+ },
166166+ None: () => {
167167+ console.log("No work in queue")
168168+ },
169169+})
170170+```
171171+172172+"The `nonEmpty` function checks at runtime and returns an `Option`. If the array is non-empty, you get `Some` with the array *rebranded* as `NonEmpty`. If it's empty, you get `None`."
173173+174174+"So you check once, and then the type remembers," Fox said slowly.
175175+176176+"Exactly. Check at the boundary, trust the types inside."
177177+178178+---
179179+180180+"What about sorting?" Beaver asked. "The queue kept getting unsorted."
181181+182182+Owl nodded. "Sorting works the same way. The `sortBy` function takes any array and returns one marked `Sorted`."
183183+184184+```typescript
185185+// Sort by priority (ascending — lower number = higher priority)
186186+const sortByPriority = sortBy<WorkOrder, never>(
187187+ (work) => work.priority
188188+)
189189+190190+const unsorted: Arr<WorkOrder> = arr([
191191+ { id: "wo-3", priority: brand(5) },
192192+ { id: "wo-1", priority: brand(1) },
193193+ { id: "wo-2", priority: brand(3) },
194194+])
195195+196196+const sorted: Arr<WorkOrder, Sorted> = sortByPriority(unsorted)
197197+// Now the type KNOWS it's sorted
198198+```
199199+200200+"But here's the key." Owl wrote more:
201201+202202+```typescript
203203+import { map, filter } from "purus-ts"
204204+205205+// Mapping preserves NonEmpty but REMOVES Sorted
206206+// (because the mapped values might have different sort keys)
207207+const descriptions = pipe(
208208+ sorted,
209209+ map((w) => w.description)
210210+)
211211+// Type: Arr<string, never> — Sorted property is gone!
212212+213213+// Filtering REMOVES NonEmpty
214214+// (because filter might remove all elements)
215215+const urgent = pipe(
216216+ sorted,
217217+ filter((w) => w.priority < brand(3))
218218+)
219219+// Type: Arr<WorkOrder, never> — NonEmpty is gone!
220220+```
221221+222222+"The type system tracks which operations preserve which properties. You can't accidentally assume a filtered array is still non-empty."
223223+224224+---
225225+226226+"Let me rebuild the queue properly," said Beaver.
227227+228228+```typescript
229229+import {
230230+ type Arr,
231231+ type NonEmpty,
232232+ type Sorted,
233233+ type Branded,
234234+ type Option,
235235+ arr,
236236+ brand,
237237+ nonEmpty,
238238+ sortBy,
239239+ head,
240240+ match,
241241+ pipe,
242242+ some,
243243+ none,
244244+} from "purus-ts"
245245+246246+// =================================================================
247247+// Priority — Validated non-negative integer
248248+// =================================================================
249249+250250+type Priority = Branded<number, "Priority">
251251+252252+const priority = (n: number): Option<Priority> =>
253253+ Number.isInteger(n) && n >= 0
254254+ ? some(brand(n))
255255+ : none
256256+257257+// =================================================================
258258+// Work Order (simplified for this example)
259259+// =================================================================
260260+261261+type WorkOrderId = Branded<string, "WorkOrderId">
262262+263263+type WorkOrder = {
264264+ readonly id: WorkOrderId
265265+ readonly description: string
266266+ readonly priority: Priority
267267+}
268268+269269+const workOrder = (
270270+ id: string,
271271+ description: string,
272272+ p: Priority,
273273+): WorkOrder => ({
274274+ id: brand(id),
275275+ description,
276276+ priority: p,
277277+})
278278+279279+// =================================================================
280280+// Queue Operations
281281+// =================================================================
282282+283283+type WorkQueue = Arr<WorkOrder>
284284+type ReadyQueue = Arr<WorkOrder, NonEmpty | Sorted>
285285+286286+// Sort by priority (lower = more urgent)
287287+const sortByPriority = sortBy<WorkOrder, never>(
288288+ (work) => work.priority as number,
289289+)
290290+291291+// Add work and re-sort to maintain ordering
292292+const addToQueue = (
293293+ queue: Arr<WorkOrder, Sorted>,
294294+ work: WorkOrder,
295295+): Arr<WorkOrder, NonEmpty | Sorted> =>
296296+ sortByPriority(arr([...queue, work])) as Arr<WorkOrder, NonEmpty | Sorted>
297297+298298+// Get next work — REQUIRES non-empty queue
299299+const getNext = (queue: Arr<WorkOrder, NonEmpty>): WorkOrder =>
300300+ head(queue)
301301+302302+// Try to get next work — handles empty case safely
303303+const tryGetNext = (queue: WorkQueue): Option<WorkOrder> =>
304304+ pipe(
305305+ nonEmpty(queue),
306306+ (opt) => match(opt)({
307307+ Some: ({ value }) => some(head(value)),
308308+ None: () => none,
309309+ }),
310310+ )
311311+```
312312+313313+---
314314+315315+"Let's trace through the whole flow," said Owl.
316316+317317+```typescript
318318+// Start with an empty queue
319319+const emptyQueue: Arr<WorkOrder, Sorted> = arr([]) as Arr<WorkOrder, Sorted>
320320+321321+// Create valid priorities
322322+const p1 = priority(1) // Option<Priority>
323323+const p5 = priority(5)
324324+const p3 = priority(3)
325325+const pBad = priority(-1) // This will be None!
326326+327327+// Build work orders (only with valid priorities)
328328+const processValidPriority = (
329329+ id: string,
330330+ desc: string,
331331+ maybePriority: Option<Priority>,
332332+): Option<WorkOrder> =>
333333+ match(maybePriority)({
334334+ Some: ({ value }) => some(workOrder(id, desc, value)),
335335+ None: () => none,
336336+ })
337337+338338+// Add work to queue
339339+match(processValidPriority("wo-1", "Fix spillway", p1))({
340340+ Some: ({ value: work }) => {
341341+ const queue1 = addToQueue(emptyQueue, work)
342342+ // queue1 is now NonEmpty | Sorted
343343+344344+ match(processValidPriority("wo-2", "Inspect bridge", p5))({
345345+ Some: ({ value: work2 }) => {
346346+ const queue2 = addToQueue(queue1, work2)
347347+348348+ match(processValidPriority("wo-3", "Clear path", p3))({
349349+ Some: ({ value: work3 }) => {
350350+ const queue3 = addToQueue(queue2, work3)
351351+352352+ // Queue is guaranteed NonEmpty and Sorted
353353+ const next = getNext(queue3)
354354+ console.log(`Next job: ${next.description}`)
355355+ // Output: "Next job: Fix spillway" (priority 1)
356356+ },
357357+ None: () => console.log("Invalid priority for wo-3"),
358358+ })
359359+ },
360360+ None: () => console.log("Invalid priority for wo-2"),
361361+ })
362362+ },
363363+ None: () => console.log("Invalid priority for wo-1"),
364364+})
365365+366366+// Invalid priority is caught
367367+match(processValidPriority("wo-bad", "Urgent!!!", pBad))({
368368+ Some: () => console.log("This won't print"),
369369+ None: () => console.log("Rejected: -1 is not a valid priority"),
370370+})
371371+// Output: "Rejected: -1 is not a valid priority"
372372+```
373373+374374+---
375375+376376+Fox leaned back. "So let me make sure I understand. The `Priority` type guarantees the value is valid. The `NonEmpty` property guarantees we can call `head`. The `Sorted` property guarantees the order."
377377+378378+"And the type system tracks all of it," Owl confirmed. "You don't need runtime checks scattered everywhere — you check once at the boundary, and the types carry the proof."
379379+380380+"Last night's crash..." Beaver said slowly.
381381+382382+"Would have been a compile error, not a runtime crash. The code wouldn't even build unless you handled the empty case."
383383+384384+---
385385+386386+Badger had been quiet, watching. Now he smiled.
387387+388388+"You know," he said, "I remember when forest maintenance was simpler. Leaves in a box. Rocks for priority. Shouting to coordinate."
389389+390390+The animals chuckled.
391391+392392+"But we were also smaller then," Badger continued. "Fewer dams, fewer bridges, fewer animals. The old ways worked when you could hold everything in your head."
393393+394394+He looked at the leaves covered in types and functions.
395395+396396+"This forest elected a president using validated ballots, counted with proper error handling, announced with reliable effects. Now it tracks maintenance with branded IDs, typestate transitions, discriminated work types, and tracked arrays."
397397+398398+"Is that good?" Rabbit asked.
399399+400400+Badger nodded slowly. "It's necessary. When systems grow, you can't rely on everyone remembering the rules. You need the rules *encoded*. So the system itself prevents mistakes."
401401+402402+He stood to leave.
403403+404404+"This is the same forest that once counted votes with leaves in a box," Badger said. "Look how far we've come."
405405+406406+---
407407+408408+The sun was setting. The animals gathered their notes, their leaves, their diagrams.
409409+410410+Beaver looked at the queue implementation — sorted, non-empty, priority-validated. No more midnight crashes. No more negative priorities. No more unsorted surprises.
411411+412412+"Tomorrow," Beaver said, "we put it all together. Branded IDs. Typestate. ADTs. Tracked arrays. The real system."
413413+414414+Fox stretched. "I'll admit, I was skeptical. 'Too many types,' I kept saying."
415415+416416+"And now?" asked Owl.
417417+418418+Fox paused. "Now I think... it's not about more types. It's about *better* types. Types that mean something. Types that prevent bugs instead of just describing data."
419419+420420+Rabbit nodded, her worries finally settled. Every anxiety she'd voiced — mixed-up IDs, impossible states, missing cases, empty queues — had become a compile-time check.
421421+422422+"Thank you, Owl," she said quietly.
423423+424424+Owl ruffled her feathers. "Thank yourselves. You built it. I just showed you the patterns."
425425+426426+The animals dispersed into the twilight forest. Behind them, the Work Order Tracker hummed quietly, processing jobs with types that couldn't lie.
427427+428428+---
429429+430430+## What We Learned
431431+432432+1. **Refinements validate values at creation** — A `Priority` isn't just a number; it's a number that passed validation. The `Option` return type forces callers to handle invalid input.
433433+434434+2. **Phantom types track array properties** — `Arr<T, NonEmpty>` and `Arr<T, Sorted>` carry compile-time information about the array's state. The markers don't exist at runtime.
435435+436436+3. **Functions can require properties** — `head(queue: Arr<T, NonEmpty>)` won't accept a regular array. The type system enforces the precondition.
437437+438438+4. **Operations affect properties correctly** — `map` preserves `NonEmpty` but removes `Sorted`. `filter` removes `NonEmpty`. The types track what's preserved.
439439+440440+5. **Check at boundaries, trust inside** — Use `nonEmpty()` once to prove an array isn't empty, then work with the `NonEmpty` type freely. No repeated runtime checks.
441441+442442+---
443443+444444+## Try It Yourself
445445+446446+The complete code from this story is available as a runnable example:
447447+448448+```bash
449449+bun examples/stories/beavers-big-system/03-tracked-arrays.ts
450450+```
451451+452452+Try creating priorities with invalid values, or calling `head` on an array that hasn't been checked with `nonEmpty`. Watch the compiler guide you to safety.
453453+454454+---
455455+456456+## The Complete Trilogy
457457+458458+| Part | Story | Concepts |
459459+|------|-------|----------|
460460+| 1 | [The Mixup](./01-the-mixup.md) | Branded Types + Typestate |
461461+| 2 | [The Categorization](./02-the-categorization.md) | ADTs + Pattern Matching |
462462+| 3 | The Queue (this story) | Tracked Arrays + Refinements |
463463+464464+Together, these patterns form a toolkit for *making invalid states unrepresentable*. The type system becomes your first line of defense against bugs — catching errors at compile time instead of runtime.
465465+466466+---
467467+468468+*Return to [Beaver's Big System overview](./) or explore [The Forest Election](../forest-election/) trilogy*
+95
docs/guides/stories/beavers-big-system/README.md
···11+# Beaver's Big System
22+33+A story in three parts about building maintainable maintenance software.
44+55+---
66+77+## The Story
88+99+After the successful forest election, the animals turn to infrastructure. Beaver, eager to help, builds "The System" — a grand work order tracker. But Beaver used strings everywhere: `"dam-1"`, `"started"`, `"in_progress"`. Soon:
1010+- Work orders for dams are accidentally applied to bridges
1111+- Things marked "done" were never "started"
1212+- Nobody knows which status values are even valid
1313+1414+Owl helps rebuild it properly, teaching patterns that prevent these bugs at compile time.
1515+1616+---
1717+1818+## Characters
1919+2020+| Character | Personality | Role in the Code |
2121+|-----------|-------------|------------------|
2222+| **Owl** | Methodical, patient | Guides the refactoring with types |
2323+| **Beaver** | Eager, practical | Built "The System" with good intentions, now sees it crumbling |
2424+| **Fox** | Skeptical, pragmatic | "I told you this was over-engineered" (but becomes a convert) |
2525+| **Rabbit** | Anxious, detail-oriented | Her worries become the error cases |
2626+| **Badger** | Wise, experienced | Remembers when forest maintenance was simpler |
2727+2828+---
2929+3030+## Parts
3131+3232+### Part 1: [The Mixup](./01-the-mixup.md)
3333+3434+Beaver's system uses plain strings for IDs and status. Fox accidentally marks `bridge-east` work as done using `dam-north`'s ID. Rabbit finds a work order with status `"in_progerss"` (typo). Badger discovers work marked `"done"` that was never `"started"`.
3535+3636+**Concepts:** Branded Types + Typestate
3737+3838+**You'll learn:**
3939+- Why plain strings let bugs slip through
4040+- Creating distinct ID types with `Branded<T, B>`
4141+- Encoding valid state transitions with typestate
4242+- Making impossible states unrepresentable
4343+4444+---
4545+4646+### Part 2: [The Categorization](./02-the-categorization.md)
4747+4848+All work orders are treated identically, but different infrastructure needs different handling. Dam repairs need water level checks. Bridge work needs weight capacity assessments. Beaver's `type: string` field leads to `"Dam"`, `"dam"`, and `"water-structure"` chaos.
4949+5050+**Concepts:** ADTs (Discriminated Unions) + Pattern Matching
5151+5252+**You'll learn:**
5353+- Modeling variants with discriminated unions
5454+- Exhaustive pattern matching with `match()`
5555+- Compiler-enforced completeness when adding new types
5656+- Using `_tag` for type-safe dispatch
5757+5858+---
5959+6060+### Part 3: [The Queue](./03-the-queue.md)
6161+6262+Work orders need prioritization. Someone calls `getNext()` on an empty queue — runtime crash. The "sorted by priority" list isn't actually sorted after appending. Negative priority values sneak in.
6363+6464+**Concepts:** Tracked Arrays + Refinements
6565+6666+**You'll learn:**
6767+- Phantom types that track array properties
6868+- `NonEmpty` arrays that can't crash on `head()`
6969+- `Sorted` arrays with guaranteed ordering
7070+- Refinement types for validated values
7171+7272+---
7373+7474+## Running the Code
7575+7676+Each part has a matching example file:
7777+7878+```bash
7979+# Part 1: Branded Types + Typestate
8080+bun examples/stories/beavers-big-system/01-branded-typestate.ts
8181+8282+# Part 2: ADTs + Pattern Matching
8383+bun examples/stories/beavers-big-system/02-adt-matching.ts
8484+8585+# Part 3: Tracked Arrays + Refinements
8686+bun examples/stories/beavers-big-system/03-tracked-arrays.ts
8787+```
8888+8989+---
9090+9191+## See Also
9292+9393+- [Stories overview](../) — Other available stories
9494+- [The Forest Election](../forest-election/) — The prequel trilogy
9595+- [Branded Types and Refinements](../../concepts/01-branded-types-and-refinements.md) — Technical deep-dive