mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-07 07:55:13 +02:00
docs: add code-design principles and link from AGENTS.md
Add docs/code-design.md with seven cross-cutting principles for avoiding overengineering (one way to say one thing, behavior follows from inputs, failures must reach a decision-maker, no seams without a second consumer, spec-and-behavior drift, verify the path you fixed, naming asymmetries). Reference it from AGENTS.md with the same weight as in-file MUST/MUST NOT rules, mirroring the docs/terminology.md pattern.
This commit is contained in:
parent
1c7131c6c2
commit
173926dc0e
2 changed files with 102 additions and 0 deletions
11
AGENTS.md
11
AGENTS.md
|
|
@ -145,6 +145,17 @@ file instead of rerunning to apply different filters:
|
|||
pnpm run test 2>&1 | tee /tmp/ktx-test-output.log
|
||||
```
|
||||
|
||||
## Avoiding Overengineering
|
||||
|
||||
For the code-design principles agents must apply when writing or changing
|
||||
behavior — one way to say one thing, behavior follows from inputs (not
|
||||
from which path the caller took), failures must reach a decision-maker,
|
||||
don't build seams without a second piece on the other side, specification
|
||||
and behavior are one artifact, verify the path you claim to have fixed,
|
||||
and naming asymmetries are bugs in waiting — see
|
||||
[`docs/code-design.md`](docs/code-design.md). Treat the `MUST` / `MUST NOT`
|
||||
rules there with the same weight as the ones in this file.
|
||||
|
||||
## TypeScript Standards
|
||||
|
||||
- Use Node 22+ and pnpm workspace commands.
|
||||
|
|
|
|||
91
docs/code-design.md
Normal file
91
docs/code-design.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# ktx Code-Design Principles
|
||||
|
||||
Principles agents must apply when writing or changing behavior in this
|
||||
repository. These rules carry the same weight as the `MUST` / `MUST NOT`
|
||||
rules in `AGENTS.md`.
|
||||
|
||||
Overengineering rarely looks like over-engineering at the line level. It
|
||||
shows up as small, locally-reasonable choices that combine into a system
|
||||
where features fail silently and bug fixes have to be applied N times. The
|
||||
principles below are the lessons; if a piece of code violates one, that is
|
||||
enough reason to fix it even when the local code "works."
|
||||
|
||||
## One way to say one thing
|
||||
|
||||
- **MUST NOT**: Accept two spellings of the same intent — e.g. a magic
|
||||
sentinel value AND absence-of-field both meaning "use the default". Pick
|
||||
one and reject the other.
|
||||
- **MUST NOT**: Maintain two entry points that load/construct/resolve the
|
||||
same thing where one does strictly more work than the other. Callers
|
||||
will pick the wrong one. Unify them, or encode the difference as a
|
||||
required argument on a single entry point.
|
||||
- **MUST NOT**: Let each consumer write its own private wrapper around a
|
||||
shared helper to make it usable. If three callers each prepend the same
|
||||
three lines, those three lines belong in the helper.
|
||||
|
||||
## Behavior follows from inputs, not from which path the caller took
|
||||
|
||||
- **MUST**: A function's result must depend on its arguments, not on
|
||||
which sibling function the caller happened to invoke first. If "did
|
||||
setup step S run?" determines correctness, S belongs INSIDE the function
|
||||
that needs it, or its absence must be a hard error — not a silent
|
||||
degradation.
|
||||
- **SHOULD**: When a value on disk requires runtime resolution (start a
|
||||
process, read state, hit a service), the resolution belongs in ONE
|
||||
place that every consumer goes through. If some consumers get the
|
||||
resolved form and some get the raw form, the abstraction is broken.
|
||||
|
||||
## Failures must reach a decision-maker
|
||||
|
||||
- **MUST NOT**: Catch an error, log it through a logger that may be a
|
||||
no-op, and continue with a null/empty result. The error reaches no one.
|
||||
Either surface the failure to the caller (return type, status field,
|
||||
stderr line), or throw.
|
||||
- **MUST**: A caller that receives "no result" must be able to
|
||||
distinguish "the input legitimately produced nothing" from "a
|
||||
dependency was unavailable" from "the operation was skipped." If those
|
||||
three look the same to the user, the system is hiding bugs — including
|
||||
this one.
|
||||
- **MUST**: When a function returns `T | null` (or a "skipped" status),
|
||||
at least one caller in the codebase must branch on the negative case
|
||||
and surface it. If every caller treats absence as success, the function
|
||||
is laundering errors.
|
||||
|
||||
## Don't build seams without a second piece on the other side
|
||||
|
||||
- **MUST NOT**: Introduce an interface, abstract type, or "port" boundary
|
||||
with exactly one implementation and no concrete plan for a second.
|
||||
Abstractions are paid for with indirection; pay only when you collect.
|
||||
- **MUST NOT**: Add an optional dep-injection slot (`deps.X ?? defaultX`)
|
||||
unless at least one test exercises the production default. If every
|
||||
test injects a fake, the production codepath is type-checked and
|
||||
untested.
|
||||
- **MUST NOT**: Add a wrapper "in case" callers later need to extend it.
|
||||
Add the wrapper when the second caller arrives.
|
||||
|
||||
## Specification and behavior are one artifact
|
||||
|
||||
- **MUST**: When a schema, doc comment, or config description states a
|
||||
default or a meaning, the code MUST enforce it. Drift between
|
||||
"what the field claims" and "what the code does" is a contract bug
|
||||
even if both compile.
|
||||
- **MUST**: When you change behavior, update the schema description, the
|
||||
doc, AND the example in the same change. Not later.
|
||||
|
||||
## Verify the path you claim to have fixed
|
||||
|
||||
- **MUST**: Before claiming a feature works, run a command that actually
|
||||
exercises it end-to-end and observe the side effect — the file
|
||||
written, the process contacted, the row stored. Type-check passing is
|
||||
necessary, not sufficient. A test passing against a fake is not
|
||||
evidence the real path works.
|
||||
- **MUST**: Before declaring a bug fixed, grep for the same shape
|
||||
elsewhere. Bugs of the kind described in this section repeat. Fix the
|
||||
class, not just the instance.
|
||||
|
||||
## Naming asymmetries are bugs in waiting
|
||||
|
||||
- **SHOULD**: When two related identifiers have non-parallel names
|
||||
(`loadX` vs `loadHigherX`, `createY` vs `createDefaultY`, `xClient`
|
||||
vs `xService`), assume callers will pick the wrong one. Unify, or
|
||||
document inline why both must exist.
|
||||
Loading…
Add table
Add a link
Reference in a new issue