Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd6f93a
docs(arch): scaffold engineering docs and seed three rules
claude Aug 11, 2026
d5a19cb
docs(arch): add rules 0004 and 0005 — code that defends and reads
claude Aug 11, 2026
fad4017
docs(arch): add rules 0006 and 0007 — assumptions and reading order
claude Aug 11, 2026
a1996ea
docs(arch): add rules 0008-0010 — type discipline layer
claude Aug 11, 2026
fd2a4ac
docs(arch): add rule 0011 — filenames are kebab-case
claude Aug 11, 2026
d7aec3f
docs(arch): enrich rules with examples and cross-references, add INDEX
claude Aug 11, 2026
cafaf9c
docs(arch): add rule 0012 — prefer type over interface
claude Aug 11, 2026
1662fac
docs(arch): enrich rule 0008 with bad/good pairs and a positive example
claude Aug 11, 2026
e5401a2
docs(arch): ground rule 0008 in senior practitioner consensus
claude Aug 11, 2026
e1017de
docs(arch): ground rule 0004 in senior practitioner consensus
claude Aug 11, 2026
00ed5d7
docs(arch): promote trust-the-type as the project slogan
claude Aug 11, 2026
7f5ef4d
docs(arch): refine rule 0005 with three-category diminutive framework
claude Aug 11, 2026
53262d9
docs(arch): add rule 0013 — entity-first naming
claude Aug 11, 2026
79a8ef5
docs(arch): tighten rule 0013 — refuse qualified suffixes too
claude Aug 11, 2026
71f70c2
docs(arch): add rule 0014 — functions over classes for public API
claude Aug 11, 2026
a3f4ffb
docs(arch): add rule 0015 — domain-specific types over primitives
claude Aug 11, 2026
61206d8
docs(arch): add Sources section to every rule
claude Aug 11, 2026
f5eae52
docs(arch): refine rule 0015 example with named union types
claude Aug 11, 2026
9581426
docs(arch): clarify when branded types apply in rule 0015
claude Aug 11, 2026
61da01d
docs(arch): add rule 0016 — no generic verbs
claude Aug 11, 2026
5016fa3
docs(arch): resolve internal inconsistencies across rules 0001-0016
martyy-code Aug 11, 2026
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
5 changes: 5 additions & 0 deletions .changeset/docs-resolve-rule-inconsistencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@deessejs/errors": patch
---

Resolve twelve internal inconsistencies across the architecture rules in `docs/engineering/architecture/rules/` (rules 0001-0016, INDEX.md, README.md). Each fix is a clarification that aligns the text with the doctrine already expressed elsewhere in the ruleset; no new doctrine is introduced and no existing constraint is weakened. Includes: INDEX title and 0015 summary refresh, README length policy softening, threshold disambiguation across 0001/0002/0003/0005, 0016 self-contradiction on `run`/`execute`, 0013/0012/0014 reconciliation, and the 0010/0015 carve-out for environment values. The published runtime is unchanged.
52 changes: 52 additions & 0 deletions docs/engineering/architecture/decisions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Architecture Decisions

This folder collects the Architecture Decision Records (ADRs) for the
DeesseJS Errors repository. Each ADR captures one significant
architectural choice, the context that led to it, and the consequences
that followed.

## Format

Each decision is stored as a Markdown file with the naming convention
`NNNN-short-slug.md`, where `NNNN` is a monotonically increasing
4-digit sequence. For example:

- `0001-staging-first-branching-model.md`
- `0002-npm-trusted-publishing.md`

The sequence numbers are **never reused**, even when an ADR is
superseded — superseded ADRs are linked from the new one but kept
in place for the historical record.

## Status lifecycle

Every ADR carries one of the following statuses, set in its frontmatter
and reflected in the title:

- **Proposed** — under discussion, no commitment yet.
- **Accepted** — adopted by the team; future work must respect it.
- **Superseded** — replaced by a later ADR (cross-link required).
- **Deprecated** — kept on disk for context but no longer applies.

## When to write an ADR

Write one whenever a choice:

- Affects the public API surface (`packages/errors/src/`).
- Changes the release pipeline (`.github/workflows/`, `release.yml`).
- Sets a long-lived convention (branching, commits, dependencies).
- Would surprise a future contributor if it were not written down.

Do **not** write an ADR for one-off implementation details that live
inside a single PR; the PR description is enough.

## Authoring

Use the [`docs/internal/engineering/process/`](../../internal/engineering/process/)
templates if you want a starter, but a minimal ADR only needs:

1. **Context** — what problem we were solving.
2. **Decision** — what we chose to do.
3. **Consequences** — what becomes easier, what becomes harder.

Keep it short. The point is to be readable in 5 minutes a year from now.
233 changes: 233 additions & 0 deletions docs/engineering/architecture/rules/0001-project-mindset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
# 0001 — Project Mindset: Excellence by Default

**Status**: Active (enforced through code review and contributor onboarding).
**Date**: 2026-08-11.

## Rule

Every contribution to this repository must be made **as if it were the
last commit before the project reached its largest possible audience**.
The standard is "would I be comfortable explaining this line to a
contributor joining in two years, in front of a million users, with no
opportunity to revise it first?"

There is no "good enough for now". There is no "we'll fix it later".
The work done today is the work that ships at scale.

## Why

A foundation library reaches a long tail of users. Each shortcut
compounds: a single `as any` raté costs a fraction of a second of
author time today, then costs hours of debugging at scale tomorrow, then
becomes the reason a downstream team migrates to a competitor. The
cost asymmetry is brutal in one direction and trivial in the other.

The same logic applies to **understanding**. A line of code written
without fully grasping its consequences will eventually be the line
that breaks. There is no shortcut around comprehension. Anyone who
finds themselves reaching for one is, by definition, the wrong person
to write that line at that moment.

The codebase is read far more often than it is written. Every
contribution must optimise for the **reader**, not the author.

## The ten invariants

Every contribution must satisfy all of these. They are not
guidelines; they are the floor.

1. **No shortcuts.** A cast that bypasses the type system is a lie to
the audience. Either the return type is what you say it is, or it
is not, and the code should reflect that truthfully.

2. **No conscious debt.** "We'll fix it later" is a promise to a
future that may not exist. The only moment we are paid to do
something well is the moment we are doing it. There is no later
that justifies a shortcut now.

3. **Understand before writing.** If the API being called is not
understood in full, the code is not ready to be written. Reading
the source, asking the maintainer, or waiting for an answer are
all acceptable next steps. Guessing is not.

4. **No speculative abstractions.** An abstraction added "in case"
is a wall the next contributor will have to climb. Abstract only
when three concrete cases exist (Rule of Three). Until then, the
duplication is cheaper than the abstraction. _The threshold for
extracting an abstraction (three) differs from the threshold for
moving a single file (two distinct concerns); see rule 0003 for
the file-level decision._

5. **No `any`.** `unknown` is the safe escape hatch. If a type cannot
be expressed, model it explicitly — through a schema, a discriminated
union, or a generic — rather than closing the eyes.

6. **No silent failures.** A `try`/`catch` that swallows an error is
a betrayal of the user. Either re-raise, transform with explicit
context, or log through a structured channel. Never silently.

7. **No compiler bypass.** `@ts-expect-error`, `as` casts, `// @ts-ignore`,
dynamic `require`, and friends are signals that the code has a
problem. Address the problem; do not silence the alarm.

8. **No dependency without justification.** A new dependency is a
long-term commitment. Before adding it, be able to answer: what is
its license, its release cadence, its bus factor, and why this
one and not its alternatives. If the answer is "it has stars", it
is not ready.

9. **Optimise for the reader.** The next maintainer is the
audience. If a PR is harder to read than to write, it is the
wrong PR. Comments explain _why_, not _what_. Names carry
meaning; comments carry context that names cannot.

10. **Excellence is silent.** No commit message that celebrates. No
PR description that congratulates itself. The work is the
artefact. If it needs explanation to be recognised as good, the
work is not good enough.

## The trust-the-type principle

The single sentence that operationalises the ten invariants:

> "If the type says it's not null, trust the type. If the type is
> wrong, fix the type. Don't add runtime null checks for values
> that can't be null."
>
> — Miguel Pizza, _No Defensive Null Checks_, Maintainable
> TypeScript doctrine.

Every invariant in this rule is a consequence of that principle.
The compiler is the first reviewer (invariant 9); the compiler says
"not null", the runtime says "I trust you" — or, if the compiler
is wrong, the fix is in the type, not in the runtime (rule 0004
operationalises this). No conscious debt (invariant 2) means no
guard that papers over a type we are afraid to fix. No speculative
abstractions (invariant 4) means no abstract `defensive(...)`
helper that catches everything on the assumption that anything
might happen.

The principle is the slogan of the project. A reader who
remembers only one sentence from this rule set should remember
this one.

## Enforcement

- **Code review** is the primary gate. A reviewer who sees any of the
ten invariants violated is expected to block the PR, regardless of
urgency or seniority of the author.
- **Onboarding** documents must include this rule verbatim. New
contributors who arrive through a fast path (open-source
contribution, AI-assisted PR) are pointed here on their first
interaction with the repo.
- **Self-removal**: contributors who consistently violate the
invariants despite feedback are removed from the maintainer list.
This is not a punishment; it is a recognition that the project and
the contributor have different standards, and the project's
standard is the one that ships.

## Examples

Three invariants illustrated as bad/good pairs. The patterns are
generic; they apply to any code that takes the same shape.

**Invariant 1 (no shortcut) — the lie of a cast:**

```ts
// Bad: bypasses the type system because the author did not want to
// model the actual shape.
function loadConfig(path: string): Config {
const raw = readFile(path) as any;
return raw as Config;
}

// Good: the author learned what the file actually contains and
// modelled it. If the file is malformed, the function says so.
function loadConfig(path: string): Config {
const raw = readJson(path);
if (!isConfig(raw)) {
throw new InvalidConfigError(path, raw);
}
return raw;
}
```

**Invariant 5 (no `any`) — escape hatches are modelling failures:**

```ts
// Bad: the author could not express the union, so they shut their eyes.
function handle(event: any) {
if (event.type === 'click') {
/* ... */
}
}

// Good: the discriminated union models the truth. The compiler proves
// every branch is handled.
type Event = { type: 'click'; position: Position } | { type: 'key'; key: string };

function handle(event: Event) {
switch (event.type) {
case 'click':
return; /* ... */
case 'key':
return; /* ... */
}
}
```

**Invariant 6 (no silent failures) — the `catch` that lies:**

```ts
// Bad: the author wrapped the call to be safe and caught "in case".
// Failures vanish. The user never learns.
try {
await sync();
} catch {
/* nothing */
}

// Good: either re-raise with context, transform into a domain error,
// or log through a structured channel. Never silently.
try {
await sync();
} catch (cause) {
throw new SyncError('sync failed', { cause });
}
```

The remaining invariants are expanded in their dedicated rules:
see rule 0004 for invariants 4 and 7 (no speculative defences, no
compiler bypass) and rule 0008 for the type-side discipline
underlying invariant 7.

## See also

- **Rule 0002** — File Separation: the structure this mindset expects.
- **Rule 0003** — File Placement: the discipline that turns the
mindset into a code-shape decision. The Rule of Three named in
invariant 4 above is the _abstraction_ threshold; rule 0003
applies a _second-concern_ threshold for file relocation — the
two are different decisions with different evidence.
- **Rule 0004** — No Speculative Defences: invariant 4 (no
speculative abstractions) and invariant 7 (no compiler bypass)
in operational form.
- **Rule 0007** — Top-Down Composition: the discipline that puts
the reader first.
- **Rule 0008** — No Chained Type Assertions: the type-side
application of invariant 7.

## Exceptions

None. The invariants are absolute. A request for an exception is a
signal that the request should be re-scoped until it no longer
requires one.

## Sources

- **Pizza, Miguel.** _No Defensive Null Checks._ Maintainable
TypeScript doctrine. Cited in rule 0001's trust-the-type epigraph
and again in rule 0004 (where the principle is operationalised
for runtime guards). The "trust the type" quote is the project's
slogan; the rule applies the principle at the level of
contributor mindset.
Loading
Loading