Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agent/keyrack.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
org: ehmpathy
extends:
- .agent/repo=ehmpathy/role=mechanic/keyrack.yml
env.prod: null
env.prep: null
env.test: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
branch: vlad/fix-constraints-vs-malfunctions
bound_by: init.behavior skill
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
emit your response to the feedback into
- .behavior/v2026_03_12.fix-constraints-vs-malfunctions/$BEHAVIOR_REF_NAME.[feedback].v$FEEDBACK_VERSION.[taken].by_robot.md

1. emit your response checklist
2. exec your response plan
3. emit your response checkoffs into the checklist

---

first, bootup your mechanics briefs again

npx rhachet roles boot --repo ehmpathy --role mechanic

---
---
---


# blocker.1

---

# nitpick.2

---

# blocker.3
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
branch: vlad/fix-constraints-vs-malfunctions
bound_by: route.bind skill
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"protections": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# ignore all except passage.jsonl and .bind flags
*
!.gitignore
!passage.jsonl
!.bind.*
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{"stone":"1.vision","status":"blocked","blocker":"review.self","reason":"review.self required: has-questioned-requirements"}
{"stone":"1.vision","status":"blocked","blocker":"review.self","reason":"review.self required: has-questioned-questions"}
{"stone":"1.vision","status":"blocked","blocker":"approval","reason":"wait for human approval"}
{"stone":"1.vision","status":"approved"}
{"stone":"1.vision","status":"passed"}
{"stone":"2.1.criteria.blackbox","status":"passed"}
{"stone":"2.2.criteria.blackbox.matrix","status":"passed"}
{"stone":"3.1.3.research.internal.product.code.prod._.v1","status":"passed"}
{"stone":"3.1.3.research.internal.product.code.test._.v1","status":"passed"}
{"stone":"3.2.distill.domain._.v1","status":"passed"}
{"stone":"3.2.distill.repros.experience._.v1","status":"passed"}
{"stone":"3.3.1.blueprint.product.v1","status":"blocked","blocker":"review.self","reason":"review.self required: has-questioned-deletables"}
{"stone":"3.3.1.blueprint.product.v1","status":"blocked","blocker":"approval","reason":"wait for human approval"}
{"stone":"3.3.1.blueprint.product.v1","status":"approved"}
{"stone":"3.3.1.blueprint.product.v1","status":"passed"}
{"stone":"4.1.roadmap.v1","status":"passed"}
{"stone":"5.1.execution.phase0_to_phaseN.v1","status":"blocked","blocker":"review.self","reason":"review.self required: has-pruned-yagni"}
{"stone":"5.1.execution.phase0_to_phaseN.v1","status":"passed"}
{"stone":"5.3.verification.v1","status":"blocked","blocker":"review.self","reason":"review.self required: has-behavior-coverage"}
{"stone":"5.5.playtest.v1","status":"blocked","blocker":"review.self","reason":"review.self required: has-clear-instructions"}
{"stone":"5.5.playtest.v1","status":"blocked","blocker":"approval","reason":"wait for human approval"}
{"stone":"5.3.verification.v1","status":"passed"}
23 changes: 23 additions & 0 deletions .behavior/v2026_03_12.fix-constraints-vs-malfunctions/0.wish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
wish =

lets release a ConstraintError and MalfunctionError to better clarify and symmetrize the fundamental message behind the two classes of errors

specifically

ConstraintError = BadRequestError
- its when a system halts the caller and throws an expected error that says "hey, you cant do that" or "hey, that doesn't make sense"
- exit.code = 2
- http.code = 4xx
- emoji = βœ‹

lets have it extend BadRequestError, so consumers who depend on the already established BadRequestError know that ConstrainError is just a more modern synonym for it

---

MalfunctionError = UnexpectedCodePathError
- its when the system itself has a defect; hits an unexpected code path; hits some internal issue that it wasnt prepared for
- exit.code = 1 (or anything other than 2, on reads)
- http.code = 5xx
- emoji = πŸ’₯

lets have it extend UnexpectedCodePathError, for the same reason
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# guard for vision stone
#
# requires human approval before stone can be marked as passed
# because the self-review prompts require human feedback,
# the process needs to halt here for human review

judges:
- npx rhachet run --repo bhrain --skill route.stone.judge --mechanism approved? --stone $stone --route $route

reviews:
self:
- slug: has-questioned-requirements
say: |
a junior recently modified files in this repo. we need to carefully
review the vision due to this.

are there any requirements that should be questioned?

for each requirement, ask:
- who said this was needed? when? why?
- what evidence supports this requirement?
- what if we didn't do this β€” what would happen?
- is the scope too large, too small, or misdirected?
- could we achieve the goal in a simpler way?

challenge each requirement and justify why it belongs.

- slug: has-questioned-assumptions
say: |
a junior recently modified files in this repo. we need to carefully
review the vision due to this.

are there any hidden assumptions the junior took as requirements?

for each assumption, ask:
- what do we assume here without evidence?
- what evidence supports this assumption?
- what if the opposite were true?
- did the wisher actually say this, or did we infer it?
- what exceptions or counterexamples exist?

surface all hidden assumptions and question each one.

- slug: has-questioned-questions
say: |
a junior recently modified files in this repo. we need to carefully
review the vision due to this.

are there any open questions? triage them:

for each question, ask:
- can this be answered via websearch or logic? if so, answer it now.
- can this be answered via extant docs or code? if so, answer it now.
- does only the wisher know the answer? if so, ask the wisher.

self-answer all questions that can be researched or reasoned.
only escalate questions that truly require the wisher's input.
240 changes: 240 additions & 0 deletions .behavior/v2026_03_12.fix-constraints-vs-malfunctions/1.vision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# vision: ConstraintError & MalfunctionError

## the outcome world

### day-in-the-life

a developer is building an api endpoint. they need to handle two fundamentally different failure modes:

1. **the caller broke a rule** β€” they sent invalid input, asked for something forbidden, or violated business logic
2. **the system broke itself** β€” a bug, unexpected state, or internal failure occurred

today, they reach for `BadRequestError` and `UnexpectedCodePathError`. these names work, but they carry http-centric baggage and don't immediately convey the semantic split.

with `ConstraintError` and `MalfunctionError`, the intent is crystal clear at the call site:

```ts
// βœ‹ the caller violated a constraint
if (!input.email.includes('@'))
throw new ConstraintError('email must be valid', { email: input.email });

// πŸ’₯ the system malfunctioned
if (!dbConnection)
throw new MalfunctionError('database connection missing', { stage });
```

### before / after

| before | after |
|--------|-------|
| `BadRequestError` β€” name implies http, feels api-specific | `ConstraintError` β€” universal, works in cli, libs, anywhere |
| `UnexpectedCodePathError` β€” long, describes symptom not cause | `MalfunctionError` β€” short, describes what happened |
| mental model requires knowing http codes | mental model is intuitive: constraint vs malfunction |
| error class names are asymmetric in length and style | error class names are symmetric and parallel |

### the "aha" moment

the value clicks when a developer sees this in their logs:

```
βœ‹ ConstraintError: customer must have a phone number
πŸ’₯ MalfunctionError: payment processor returned unexpected shape
```

immediately, without context, they know:
- βœ‹ means "fix the input" β€” it's a caller issue
- πŸ’₯ means "fix the code" β€” it's our bug

the emoji + term combo creates instant recognition.

---

## user experience

### usecases & goals

| usecase | goal | which error |
|---------|------|-------------|
| validate user input | reject bad requests early | `ConstraintError` |
| enforce business rules | halt operations that violate invariants | `ConstraintError` |
| guard impossible states | catch bugs before they propagate | `MalfunctionError` |
| wrap external service 5xx | surface internal issues clearly | `MalfunctionError` |
| propagate external service 4xx | surface constraint from upstream | `ConstraintError` |

### contract inputs & outputs

```ts
// ConstraintError β€” extends BadRequestError
new ConstraintError(message: string, metadata?: { ... })
ConstraintError.throw(message, metadata) // never returns
ConstraintError.wrap(fn, { message, metadata }) // wraps async fn

// static properties
ConstraintError.code.http // 400
ConstraintError.code.exit // 2
ConstraintError.emoji // 'βœ‹' β€” for log utilities to use

// MalfunctionError β€” extends UnexpectedCodePathError
new MalfunctionError(message: string, metadata?: { ... })
MalfunctionError.throw(message, metadata)
MalfunctionError.wrap(fn, { message, metadata })

// static properties
MalfunctionError.code.http // 500
MalfunctionError.code.exit // 1
MalfunctionError.emoji // 'πŸ’₯' β€” for log utilities to use
```

### usage examples

```ts
import { ConstraintError, MalfunctionError } from 'helpful-errors';

// guard clauses
const phone = customer.phone ?? ConstraintError.throw('customer must have phone');
const config = process.env.CONFIG ?? MalfunctionError.throw('config not loaded');

// validation
if (amount <= 0) throw new ConstraintError('amount must be positive', { amount });

// impossible state detection
switch (status) {
case 'active': return handleActive();
case 'inactive': return handleInactive();
default: throw new MalfunctionError('unknown status', { status });
}

// wrapping external calls
const fetchUser = MalfunctionError.wrap(
async (id: string) => api.getUser(id),
{ message: 'failed to fetch user', metadata: { service: 'user-api' } }
);
```

### timeline

1. **immediate** β€” drop-in alongside current errors, no migration required
2. **gradual** β€” teams adopt new names for new code
3. **eventual** β€” `BadRequestError` and `UnexpectedCodePathError` remain as base classes, never deprecated

---

## mental model

### how users describe it to a friend

> "we have two error types: constraints and malfunctions. constraints are when the caller screwed up β€” bad input, forbidden action. malfunctions are when our code screwed up β€” bugs, unexpected state. the names make it obvious which is which."

### analogies & metaphors

| error type | analogy |
|------------|---------|
| `ConstraintError` | a bouncer at a club: "βœ‹ you can't come in dressed like that" |
| `MalfunctionError` | a machine breaking: "πŸ’₯ the engine exploded" |

the constraint is external pressure being rejected.
the malfunction is internal failure being surfaced.

### terms: theirs vs ours

| user might say | we call it |
|----------------|------------|
| "bad input" | `ConstraintError` |
| "validation error" | `ConstraintError` |
| "forbidden" | `ConstraintError` |
| "bug" | `MalfunctionError` |
| "unexpected" | `MalfunctionError` |
| "internal error" | `MalfunctionError` |

---

## evaluation

### how well does it solve the goals?

| goal | score | notes |
|------|-------|-------|
| clearer semantics | βœ… | names are self-explanatory |
| symmetrical design | βœ… | parallel structure, similar length |
| backwards compatible | βœ… | extends extant classes |
| universal applicability | βœ… | not http-centric |
| pit of success | βœ… | hard to misuse |

### pros

- **intuitive names** β€” no http knowledge required
- **symmetric design** β€” `Constraint` vs `Malfunction` mirrors the fundamental split
- **emoji integration** β€” βœ‹ and πŸ’₯ provide instant visual recognition
- **exit codes** β€” `2` for constraints, `1` for malfunctions aligns with unix conventions
- **backwards compat** β€” `instanceof BadRequestError` still works for `ConstraintError`
- **no migration** β€” current code continues to work

### cons

- **two names for same thing** β€” `ConstraintError` and `BadRequestError` both exist
- **learning curve** β€” users must learn which to use (though names help)
- **potential confusion** β€” "which one should I use?" (answer: use the new ones)

### edgecases & pit of success

| edgecase | how we handle it |
|----------|------------------|
| user throws wrong error type | names are so clear it's hard to confuse |
| mixing old and new errors | inheritance ensures `instanceof` works |
| error serialization | inherits from `HelpfulError`, same behavior |
| cli vs api vs library | exit codes + http codes cover all contexts |

---

## open questions & assumptions

### assumptions

1. **exit code 2 for constraints** β€” convention: exit 2 = "usage error" in unix (like `grep` with no match)
2. **exit code 1 for malfunctions** β€” convention: exit 1 = "general error"
3. **extending extant classes** β€” not replacing them, adding synonyms
4. **emoji in logs** β€” βœ‹ and πŸ’₯ are universally supported and unambiguous

### self-answered questions

all questions were answered via logic and first principles:

1. **code.exit**: βœ… YES β€” add `exit` field to `HelpfulErrorCode` type. the wish explicitly mentions exit codes, and this follows the extant pattern.

2. **emoji placement**: βœ… add as `static emoji = 'βœ‹'` property, NOT baked into message prefix. the wish specifies WHICH emoji, not WHERE. static property gives flexibility; message stays clean for all contexts.

3. **utility functions**: ❌ NO β€” defer `isConstraintError()` etc. until a concrete use case emerges. YAGNI. no such utilities exist for extant error classes.

4. **dynamic prefix**: βœ… YES β€” modify base classes to use `this.constructor.name`. this is a BENEFICIAL change: subclasses SHOULD get their own prefix. current behavior (parent prefix for subclasses) is actually a bug, not a feature.

---

## what is awkward?

### what feels off?

- **two names coexist** β€” `BadRequestError` and `ConstraintError` are synonyms. some may find this confusing. mitigation: docs clearly state the relationship.

- **"malfunction" implies hardware** β€” some may think of physical devices. however, the term applies equally to software: "the program malfunctioned."

- **message prefix differs from parent** β€” `ConstraintError` produces `ConstraintError: ...` messages, not `BadRequestError: ...`. consumers who parse error messages via string matching may need to update. mitigation: use `instanceof` checks instead of string parsing.

### where does the design fight the mental model?

- **http-first users** β€” developers who think in http codes may prefer `BadRequestError`. solution: both coexist, use what fits your context.

### uncomfortable tradeoffs

- **more exports** β€” the package now exports 6 error-related items instead of 4. acceptable for clarity.
- **documentation burden** β€” must explain the relationship between old and new names. acceptable for long-term clarity.

---

## summary

`ConstraintError` and `MalfunctionError` provide:

- βœ‹ **constraints**: "you can't do that" β€” caller's fault β€” http 4xx β€” exit 2
- πŸ’₯ **malfunctions**: "we broke" β€” our fault β€” http 5xx β€” exit 1

symmetric, intuitive, backwards-compatible. the fundamental distinction between "bad request" and "internal error" is now crystal clear at the call site.
Loading
Loading