agentme 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/agents/speckit.analyze.agent.md +184 -0
- package/.github/agents/speckit.checklist.agent.md +295 -0
- package/.github/agents/speckit.clarify.agent.md +181 -0
- package/.github/agents/speckit.constitution.agent.md +84 -0
- package/.github/agents/speckit.implement.agent.md +198 -0
- package/.github/agents/speckit.plan.agent.md +90 -0
- package/.github/agents/speckit.specify.agent.md +237 -0
- package/.github/agents/speckit.tasks.agent.md +200 -0
- package/.github/agents/speckit.taskstoissues.agent.md +30 -0
- package/.github/prompts/speckit.analyze.prompt.md +3 -0
- package/.github/prompts/speckit.checklist.prompt.md +3 -0
- package/.github/prompts/speckit.clarify.prompt.md +3 -0
- package/.github/prompts/speckit.constitution.prompt.md +3 -0
- package/.github/prompts/speckit.implement.prompt.md +3 -0
- package/.github/prompts/speckit.plan.prompt.md +3 -0
- package/.github/prompts/speckit.specify.prompt.md +3 -0
- package/.github/prompts/speckit.tasks.prompt.md +3 -0
- package/.github/prompts/speckit.taskstoissues.prompt.md +3 -0
- package/.specify/memory/constitution.md +119 -0
- package/.specify/scripts/bash/check-prerequisites.sh +190 -0
- package/.specify/scripts/bash/common.sh +253 -0
- package/.specify/scripts/bash/create-new-feature.sh +333 -0
- package/.specify/scripts/bash/setup-plan.sh +73 -0
- package/.specify/scripts/bash/update-agent-context.sh +808 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +40 -0
- package/.specify/templates/constitution-template.md +50 -0
- package/.specify/templates/plan-template.md +110 -0
- package/.specify/templates/spec-template.md +115 -0
- package/.specify/templates/tasks-template.md +251 -0
- package/.vscode/settings.json +14 -0
- package/.xdrs/agentme/edrs/application/003-javascript-project-tooling.md +89 -0
- package/.xdrs/agentme/edrs/application/010-golang-project-tooling.md +141 -0
- package/.xdrs/agentme/edrs/application/skills/001-create-javascript-project/SKILL.md +360 -0
- package/.xdrs/agentme/edrs/application/skills/003-create-golang-project/SKILL.md +311 -0
- package/.xdrs/agentme/edrs/devops/005-monorepo-structure.md +104 -0
- package/.xdrs/agentme/edrs/devops/006-github-pipelines.md +170 -0
- package/.xdrs/agentme/edrs/devops/008-common-targets.md +207 -0
- package/.xdrs/agentme/edrs/devops/skills/002-monorepo-setup/SKILL.md +270 -0
- package/.xdrs/agentme/edrs/index.md +41 -0
- package/.xdrs/agentme/edrs/observability/011-service-health-check-endpoint.md +78 -0
- package/.xdrs/agentme/edrs/principles/002-coding-best-practices.md +110 -0
- package/.xdrs/agentme/edrs/principles/004-unit-test-requirements.md +97 -0
- package/.xdrs/agentme/edrs/principles/007-project-quality-standards.md +156 -0
- package/.xdrs/agentme/edrs/principles/009-error-handling.md +327 -0
- package/.xdrs/index.md +32 -0
- package/README.md +119 -0
- package/bin/npmdata.js +3 -0
- package/package.json +102 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# agentme-edr-002: Coding best practices
|
|
2
|
+
|
|
3
|
+
## Context and Problem Statement
|
|
4
|
+
|
|
5
|
+
Without consistent coding standards, codebases tend to accumulate large, hard-to-navigate files, tangled logic, and documentation that drifts out of sync with the implementation. This leads to slower onboarding, harder maintenance, and higher defect rates.
|
|
6
|
+
|
|
7
|
+
What coding practices should be followed across all languages and projects to keep code readable, maintainable, and well-structured?
|
|
8
|
+
|
|
9
|
+
## Decision Outcome
|
|
10
|
+
|
|
11
|
+
**Apply a set of language-agnostic structural and organizational practices that keep files small, logic decomposed, types co-located, tests co-located, and documentation always in sync.**
|
|
12
|
+
|
|
13
|
+
### Implementation Details
|
|
14
|
+
|
|
15
|
+
#### 1. Keep files short — split at 400 lines
|
|
16
|
+
|
|
17
|
+
A file must not exceed **400 lines**. When a file grows beyond this limit, split related functions or types into separate, focused modules.
|
|
18
|
+
|
|
19
|
+
One exception are test files, which normally are bigger than the tested resources.
|
|
20
|
+
|
|
21
|
+
*Why:* Large files make navigation slow, increase merge conflicts, and obscure the single-responsibility principle.
|
|
22
|
+
|
|
23
|
+
**Example (TypeScript):**
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
# before — one bloated file
|
|
27
|
+
src/
|
|
28
|
+
orders.ts # 650 lines: validation, pricing, persistence, notifications
|
|
29
|
+
|
|
30
|
+
# after — split by responsibility
|
|
31
|
+
src/
|
|
32
|
+
orders/
|
|
33
|
+
validation.ts # 120 lines
|
|
34
|
+
pricing.ts # 95 lines
|
|
35
|
+
persistence.ts # 110 lines
|
|
36
|
+
notifications.ts # 80 lines
|
|
37
|
+
index.ts # 30 lines (re-exports the public API)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
#### 2. Apply the Template Method pattern for large multi-section functions
|
|
43
|
+
|
|
44
|
+
When a function's main logic contains well-defined sections and **any individual section exceeds ~20 lines**, extract each section into its own named function. The outer function becomes an orchestrator that calls the extracted helpers in sequence.
|
|
45
|
+
|
|
46
|
+
*Why:* Named sub-functions serve as inline documentation, are independently testable, and reduce cognitive load.
|
|
47
|
+
|
|
48
|
+
**Example (Python):**
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# before — one long function with implicit sections
|
|
52
|
+
def process_order(order):
|
|
53
|
+
# --- validate --- (~25 lines)
|
|
54
|
+
if not order.items:
|
|
55
|
+
raise ValueError("empty order")
|
|
56
|
+
# ... more validation ...
|
|
57
|
+
|
|
58
|
+
# --- calculate price --- (~30 lines)
|
|
59
|
+
subtotal = sum(i.price * i.qty for i in order.items)
|
|
60
|
+
# ... discounts, taxes ...
|
|
61
|
+
|
|
62
|
+
# --- persist --- (~22 lines)
|
|
63
|
+
db.save(order)
|
|
64
|
+
# ... audit log ...
|
|
65
|
+
|
|
66
|
+
# after — template method style
|
|
67
|
+
def process_order(order):
|
|
68
|
+
_validate_order(order)
|
|
69
|
+
total = _calculate_price(order)
|
|
70
|
+
_persist_order(order, total)
|
|
71
|
+
|
|
72
|
+
def _validate_order(order): ...
|
|
73
|
+
def _calculate_price(order) -> Decimal: ...
|
|
74
|
+
def _persist_order(order, total): ...
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
#### 3. Keep README, tests, and examples in sync with implementation
|
|
80
|
+
|
|
81
|
+
Every change to a public interface, behavior, or configuration option must be reflected in:
|
|
82
|
+
|
|
83
|
+
- `README.md` — update usage examples, option tables, and feature descriptions.
|
|
84
|
+
- Unit/integration tests — update or add tests that cover the changed behavior.
|
|
85
|
+
- `examples/` resources — update runnable examples so they continue to work.
|
|
86
|
+
|
|
87
|
+
*Why:* Stale documentation and broken examples erode trust and waste time for consumers of the code.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
#### 4. Declare types in the file where they are used — unless shared
|
|
92
|
+
|
|
93
|
+
If a type (struct, interface, class, typedef, etc.) is used in only **one** file, declare it in that same file. Move a type to a shared module only when it is referenced in two or more files.
|
|
94
|
+
|
|
95
|
+
*Why:* Co-locating a type with its sole consumer removes the need to navigate to a separate types file and makes the type's purpose immediately obvious from context.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
#### 5. Keep test files next to the files they test
|
|
100
|
+
|
|
101
|
+
Where the language ecosystem supports it (e.g. JavaScript/TypeScript, Go, Rust), place test files **beside** the source file they cover and use a consistent naming convention rather than mirroring the source tree in a separate `tests/` folder.
|
|
102
|
+
|
|
103
|
+
**Recommended naming conventions:**
|
|
104
|
+
|
|
105
|
+
| Language / ecosystem | Source file | Test file |
|
|
106
|
+
|----------------------|------------------|------------------------|
|
|
107
|
+
| TypeScript / JS | `app.ts` | `app.test.ts` |
|
|
108
|
+
| Go | `handler.go` | `handler_test.go` |
|
|
109
|
+
| Rust | `parser.rs` | inline `#[cfg(test)]` |
|
|
110
|
+
| Python | `service.py` | `service_test.py` (same directory, or `tests/` when the ecosystem convention dictates otherwise) |
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# agentme-edr-004: Unit test requirements
|
|
2
|
+
|
|
3
|
+
## Context and Problem Statement
|
|
4
|
+
|
|
5
|
+
Without clear unit testing standards, test suites become inconsistent — tests lack assertions, coverage is spotty, setup code is duplicated, and mocks bypass real logic.
|
|
6
|
+
|
|
7
|
+
What unit testing practices should be followed to ensure tests are meaningful, reliable, and maintainable?
|
|
8
|
+
|
|
9
|
+
## Decision Outcome
|
|
10
|
+
|
|
11
|
+
**Every test must assert behavior, run offline without external dependencies, enforce 80% coverage, centralize shared setup, and prefer real code over mocks.**
|
|
12
|
+
|
|
13
|
+
### Implementation Details
|
|
14
|
+
|
|
15
|
+
#### 1. MUST have at least one assertion per test
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// bad — no assertion; passes even when code is broken
|
|
19
|
+
it("processes the order", () => { processOrder(mockOrder); });
|
|
20
|
+
|
|
21
|
+
// good
|
|
22
|
+
it("processes the order and returns a confirmation id", () => {
|
|
23
|
+
const result = processOrder(mockOrder);
|
|
24
|
+
expect(result.confirmationId).toBeDefined();
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
#### 2. MUST run offline — no external resource dependencies
|
|
31
|
+
|
|
32
|
+
Unit tests must not depend on any external resources: no network calls, no running databases, no external APIs, no file system paths outside the repo. Tests must pass with only static code available.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// bad — hits a real HTTP endpoint
|
|
36
|
+
it("fetches user", async () => {
|
|
37
|
+
const user = await fetch("https://api.example.com/users/1").then(r => r.json());
|
|
38
|
+
expect(user.id).toBe(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// good — uses a fake/in-memory implementation
|
|
42
|
+
it("fetches user", async () => {
|
|
43
|
+
const client = new UserClient({ transport: new InMemoryTransport(fixtures.users) });
|
|
44
|
+
const user = await client.getUser(1);
|
|
45
|
+
expect(user.id).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
#### 3. MUST maintain at least 80% line/branch coverage, enforced in CI
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// vitest.config.ts
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
test: { coverage: { provider: "v8", thresholds: { lines: 80, branches: 80 } } },
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Builds that miss the threshold must not be merged.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
#### 4. SHOULD extract shared setup into a test utility module
|
|
65
|
+
|
|
66
|
+
When setup logic is repeated across two or more test files, centralize it (`src/test-utils/`, `internal/testutil/`, `tests/conftest.py`).
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// src/test-utils/order-factory.ts
|
|
70
|
+
export function makeOrder(overrides: Partial<Order> = {}): Order {
|
|
71
|
+
return { id: "ord-1", items: [{ sku: "A", qty: 1, price: 10 }], status: "pending", ...overrides };
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
#### 5. SHOULD avoid mocks — prefer real code execution
|
|
78
|
+
|
|
79
|
+
Use the lowest-cost alternative that exercises real behavior:
|
|
80
|
+
|
|
81
|
+
1. **Real implementation** — always prefer this
|
|
82
|
+
2. **In-memory / lightweight fake** — e.g. in-memory DB, stub HTTP server
|
|
83
|
+
3. **Recorded fixture** — replay captured real responses
|
|
84
|
+
4. **Mock / stub** — only for external APIs, irreversible operations, or hardware I/O
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// bad — mocks internal logic; passes even when pricing is broken
|
|
88
|
+
jest.mock("../pricing", () => ({ calculateTotal: () => 99 }));
|
|
89
|
+
|
|
90
|
+
// good — exercises the real pricing module
|
|
91
|
+
it("charges the correct amount", () => {
|
|
92
|
+
const order = makeOrder({ items: [{ sku: "A", qty: 1, price: 99 }] });
|
|
93
|
+
expect(checkout(order)).toBe(99);
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
When a mock is unavoidable, keep it narrow (one boundary point) and add a comment explaining why.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# agentme-edr-007: Project quality standards
|
|
2
|
+
|
|
3
|
+
## Context and Problem Statement
|
|
4
|
+
|
|
5
|
+
Without a baseline quality bar, projects within the same organization can diverge significantly in documentation completeness, test coverage, linting discipline, and structural clarity. New developers encounter confusion, quality regressions slip through, and standards drift over time.
|
|
6
|
+
|
|
7
|
+
What minimum quality standards must every project in the organization meet to ensure it is understandable, maintainable, and consistently verifiable?
|
|
8
|
+
|
|
9
|
+
## Decision Outcome
|
|
10
|
+
|
|
11
|
+
Every project must meet six minimum quality standards: a Getting Started section in its README, unit tests that run on every release, compliance with workspace XDRs, active linting enforcement, a structure that is clear to new developers, and — for libraries and utilities — a runnable examples folder verified on every test run.
|
|
12
|
+
|
|
13
|
+
These standards form a non-negotiable baseline. Individual projects may raise the bar but must never fall below it.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### 1. README MUST have a Getting Started section
|
|
18
|
+
|
|
19
|
+
`README.md` must include a **Getting Started** section in the first 20 lines with the minimal steps to install and use the project.
|
|
20
|
+
|
|
21
|
+
**Required content:**
|
|
22
|
+
- Installation or setup command(s)
|
|
23
|
+
- At least one runnable usage example (code snippet, CLI command, or API call)
|
|
24
|
+
|
|
25
|
+
**Required README structure:**
|
|
26
|
+
|
|
27
|
+
```markdown
|
|
28
|
+
# Project Name
|
|
29
|
+
|
|
30
|
+
One-line description.
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install my-package
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { myFunction } from "my-package";
|
|
40
|
+
myFunction({ input: "value" });
|
|
41
|
+
```
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 2. Unit tests MUST run on every release
|
|
47
|
+
|
|
48
|
+
A unit test suite must run automatically before every release. Failing tests must block the release — no silent skips or overrides.
|
|
49
|
+
|
|
50
|
+
**Requirements:**
|
|
51
|
+
- A `make test` target must exist and run the full suite
|
|
52
|
+
- CI/CD must invoke it before publish/deploy
|
|
53
|
+
- Test failures block the release
|
|
54
|
+
|
|
55
|
+
**Exception:** Projects with fewer than 100 lines of code, or whose `README.md` prominently marks them as a **Spike** or **Experiment**, are exempt from this requirement. Such projects must never be deployed to production.
|
|
56
|
+
|
|
57
|
+
**Reference:** [agentme-edr-004](004-unit-test-requirements.md) for detailed unit test requirements.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### 3. The project MUST comply with all applicable workspace XDRs
|
|
62
|
+
|
|
63
|
+
All XDRs that apply to the project's scope (as listed in [.xdrs/index.md](../../../../index.md)) must be followed. A deviation requires a project-local XDR documenting the override.
|
|
64
|
+
|
|
65
|
+
**Requirements:**
|
|
66
|
+
- Review applicable XDRs before any significant implementation
|
|
67
|
+
- If an XDR conflicts with project needs, create a `_local` XDR documenting the deviation
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### 4. The project MUST have linting enforcing code style, formatting, and best practices
|
|
72
|
+
|
|
73
|
+
Projects larger than 10 files or 200 lines of code must have a linter configured and actively enforced. Lint failures block CI builds.
|
|
74
|
+
|
|
75
|
+
**Requirements:**
|
|
76
|
+
- `make lint` runs the linter with zero-warning tolerance
|
|
77
|
+
- `make lint-fix` auto-fixes fixable issues
|
|
78
|
+
- Linter config is checked in (e.g., `.eslintrc.js`, `pyproject.toml`, `.golangci.yml`)
|
|
79
|
+
- CI runs `make lint` before merging or releasing
|
|
80
|
+
|
|
81
|
+
**Exception:** Projects with fewer than 100 lines of code, or whose `README.md` prominently marks them as a **Spike** or **Experiment**, are exempt from this requirement. Such projects must never be deployed to production.
|
|
82
|
+
|
|
83
|
+
**Reference:** [agentme-edr-003](003-javascript-project-tooling.md) for JavaScript-specific tooling.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### 5. The project structure MUST be easily understood by new developers
|
|
88
|
+
|
|
89
|
+
Directory and file layout must be self-explanatory: source code, tests, configuration, and examples must be clearly separated and named.
|
|
90
|
+
|
|
91
|
+
**Requirements:**
|
|
92
|
+
- Directory names must reflect their purpose (`src/`, `lib/`, `tests/`, `examples/`, `docs/`)
|
|
93
|
+
- README must describe the top-level layout if non-obvious
|
|
94
|
+
- No orphaned or unexplained directories or files at the project root
|
|
95
|
+
|
|
96
|
+
**Example layout (TypeScript project):**
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
/
|
|
100
|
+
├── README.md
|
|
101
|
+
├── Makefile
|
|
102
|
+
├── lib/
|
|
103
|
+
│ └── src/
|
|
104
|
+
│ ├── index.ts
|
|
105
|
+
│ └── *.test.ts
|
|
106
|
+
└── examples/
|
|
107
|
+
└── basic-usage/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### 6. Libraries and utilities MUST have a runnable examples folder verified on every test run
|
|
113
|
+
|
|
114
|
+
Projects that are libraries or shared utilities must include an `examples/` directory. Each subdirectory represents a usage scenario and must be independently runnable. Examples are executed as part of `make test`.
|
|
115
|
+
|
|
116
|
+
**Requirements:**
|
|
117
|
+
- `examples/` must contain at least one subdirectory per major usage scenario
|
|
118
|
+
- Each scenario subdirectory must have a `Makefile` with a `run` target
|
|
119
|
+
- Examples must import the library as an external consumer (not via relative `../src` imports)
|
|
120
|
+
- `make test` in the root must run all examples; failures block CI and releases
|
|
121
|
+
|
|
122
|
+
**Directory layout:**
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
/
|
|
126
|
+
├── Makefile
|
|
127
|
+
├── lib/src/
|
|
128
|
+
└── examples/
|
|
129
|
+
├── Makefile
|
|
130
|
+
├── basic-usage/
|
|
131
|
+
│ ├── Makefile # targets: run
|
|
132
|
+
│ └── main.ts
|
|
133
|
+
└── advanced-usage/
|
|
134
|
+
├── Makefile # targets: run
|
|
135
|
+
└── main.ts
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Root Makefile:**
|
|
139
|
+
|
|
140
|
+
```makefile
|
|
141
|
+
test: test-unit test-examples
|
|
142
|
+
|
|
143
|
+
test-unit:
|
|
144
|
+
$(MAKE) -C lib test
|
|
145
|
+
|
|
146
|
+
test-examples:
|
|
147
|
+
$(MAKE) -C examples
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Examples Makefile:**
|
|
151
|
+
|
|
152
|
+
```makefile
|
|
153
|
+
all:
|
|
154
|
+
$(MAKE) -C basic-usage run
|
|
155
|
+
$(MAKE) -C advanced-usage run
|
|
156
|
+
```
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# agentme-edr-009: Error handling
|
|
2
|
+
|
|
3
|
+
## Context and Problem Statement
|
|
4
|
+
|
|
5
|
+
Poor error handling is one of the most common sources of production incidents. Errors that are silently swallowed hide bugs; exceptions leaked through public interfaces force callers to know internal implementation details; scattered `catch` blocks spread fragile logic across the codebase; processes that exit with code `0` despite failure mislead orchestrators; and untested error paths mean breakages are only discovered in production.
|
|
6
|
+
|
|
7
|
+
What error handling practices should be followed across all languages and projects to produce systems that fail loudly, communicate failure clearly, and remain easy to reason about?
|
|
8
|
+
|
|
9
|
+
## Decision Outcome
|
|
10
|
+
|
|
11
|
+
**Follow a set of consistent error handling practices: catch only where you can handle, return errors as values at interfaces, centralize repetitive catch logic, communicate failure clearly at process and service boundaries, and exercise error paths with dedicated tests.**
|
|
12
|
+
|
|
13
|
+
### Implementation Details
|
|
14
|
+
|
|
15
|
+
#### 1. Catch exceptions only where they can be properly handled
|
|
16
|
+
|
|
17
|
+
Never catch an exception unless the catching site can genuinely recover from it, translate it into a meaningful domain error, or enrich it with context before re-throwing. Do **not** swallow exceptions silently. When suppressing an exception is intentional, always add a comment explaining exactly why, or log it at an appropriate level.
|
|
18
|
+
|
|
19
|
+
*Why:* Swallowed exceptions hide bugs and make incidents impossible to diagnose. Every silent `catch` is a future mystery.
|
|
20
|
+
|
|
21
|
+
**Examples:**
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// bad — swallowed silently
|
|
25
|
+
try {
|
|
26
|
+
await saveOrder(order);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// nothing here
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// bad — caught but not handled or re-thrown
|
|
32
|
+
try {
|
|
33
|
+
await saveOrder(order);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.log("error"); // no context, no rethrow, no recovery
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// good — caught, enriched, re-thrown
|
|
39
|
+
try {
|
|
40
|
+
await saveOrder(order);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
throw new OrderPersistenceError(`Failed to save order ${order.id}`, { cause: e });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// good — intentional suppression with explanation
|
|
46
|
+
try {
|
|
47
|
+
await evictCache(key);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// Cache eviction is best-effort; a failure here does not affect correctness.
|
|
50
|
+
logger.warn({ err: e, key }, "cache eviction failed, continuing");
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# bad
|
|
56
|
+
try:
|
|
57
|
+
save_order(order)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass # no explanation
|
|
60
|
+
|
|
61
|
+
# good — intentional suppression documented
|
|
62
|
+
try:
|
|
63
|
+
evict_cache(key)
|
|
64
|
+
except CacheError:
|
|
65
|
+
# Cache eviction is best-effort; failure does not affect correctness.
|
|
66
|
+
logger.warning("cache eviction failed for key=%s", key, exc_info=True)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
#### 2. Avoid exposing exceptions as part of public interfaces — return error values instead
|
|
72
|
+
|
|
73
|
+
At module and service boundaries, prefer returning a value that signals success or failure (e.g., a result type, a discriminated union, or a `(value, error)` tuple as in Go) over throwing exceptions. This forces callers to explicitly acknowledge and handle the error case before using the result.
|
|
74
|
+
|
|
75
|
+
*Why:* Exceptions are invisible in signatures. A caller who doesn't know an exception can be thrown will never write a handler. Explicit error return values make the contract visible and encourage handling at the call site.
|
|
76
|
+
|
|
77
|
+
**Examples:**
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// bad — exception leaks from a public function
|
|
81
|
+
async function fetchUser(id: string): Promise<User> {
|
|
82
|
+
const row = await db.query("SELECT * FROM users WHERE id = $1", [id]);
|
|
83
|
+
if (!row) throw new Error("user not found"); // caller must know this can throw
|
|
84
|
+
return row;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// good — result type makes the error case explicit
|
|
88
|
+
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
|
89
|
+
|
|
90
|
+
async function fetchUser(id: string): Promise<Result<User, "not-found" | "db-error">> {
|
|
91
|
+
try {
|
|
92
|
+
const row = await db.query("SELECT * FROM users WHERE id = $1", [id]);
|
|
93
|
+
if (!row) return { ok: false, error: "not-found" };
|
|
94
|
+
return { ok: true, value: row };
|
|
95
|
+
} catch (e) {
|
|
96
|
+
logger.error({ err: e, userId: id }, "db query failed");
|
|
97
|
+
return { ok: false, error: "db-error" };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// caller is forced to check
|
|
102
|
+
const result = await fetchUser(id);
|
|
103
|
+
if (!result.ok) {
|
|
104
|
+
// handle not-found or db-error
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
use(result.value);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```go
|
|
111
|
+
// Go — idiomatic error return
|
|
112
|
+
func FetchUser(id string) (*User, error) {
|
|
113
|
+
row, err := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
|
|
114
|
+
if err != nil {
|
|
115
|
+
return nil, fmt.Errorf("fetchUser %s: %w", id, err)
|
|
116
|
+
}
|
|
117
|
+
return row, nil
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// caller must inspect error before using value
|
|
121
|
+
user, err := FetchUser(id)
|
|
122
|
+
if err != nil {
|
|
123
|
+
return err
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# good — use a result-like pattern or explicit sentinel returns
|
|
129
|
+
from dataclasses import dataclass
|
|
130
|
+
from typing import Generic, TypeVar
|
|
131
|
+
|
|
132
|
+
T = TypeVar("T")
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class Ok(Generic[T]):
|
|
136
|
+
value: T
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class Err:
|
|
140
|
+
error: str
|
|
141
|
+
|
|
142
|
+
def fetch_user(user_id: str) -> Ok[User] | Err:
|
|
143
|
+
try:
|
|
144
|
+
row = db.query("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
145
|
+
if row is None:
|
|
146
|
+
return Err("not-found")
|
|
147
|
+
return Ok(row)
|
|
148
|
+
except DBError as e:
|
|
149
|
+
logger.error("db query failed", exc_info=True, extra={"user_id": user_id})
|
|
150
|
+
return Err("db-error")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
#### 3. Centralise repetitive catch logic into utility helpers
|
|
156
|
+
|
|
157
|
+
If the same `try/catch` pattern (e.g., logging, classifying HTTP errors, wrapping exceptions) appears in multiple places, extract it into a shared utility. Do not copy-paste catch blocks across the codebase.
|
|
158
|
+
|
|
159
|
+
*Why:* Scattered catch blocks drift out of sync — one gets updated, the others don't. A central utility is tested once and applied everywhere consistently.
|
|
160
|
+
|
|
161
|
+
**Examples:**
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// bad — copy-pasted http error handling across many call sites
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(url);
|
|
167
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
logger.error({ err: e }, "request failed");
|
|
170
|
+
throw e;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// good — one utility used everywhere
|
|
174
|
+
async function httpGet<T>(url: string): Promise<Result<T, HttpError>> {
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch(url);
|
|
177
|
+
if (!res.ok) return { ok: false, error: new HttpError(res.status, url) };
|
|
178
|
+
return { ok: true, value: (await res.json()) as T };
|
|
179
|
+
} catch (e) {
|
|
180
|
+
logger.error({ err: e, url }, "http request failed");
|
|
181
|
+
return { ok: false, error: new HttpError(0, url, e as Error) };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# good — a decorator that handles and logs DB errors uniformly
|
|
188
|
+
def handle_db_errors(fn):
|
|
189
|
+
@functools.wraps(fn)
|
|
190
|
+
def wrapper(*args, **kwargs):
|
|
191
|
+
try:
|
|
192
|
+
return fn(*args, **kwargs)
|
|
193
|
+
except DBError as e:
|
|
194
|
+
logger.error("db error in %s", fn.__name__, exc_info=True)
|
|
195
|
+
return Err("db-error")
|
|
196
|
+
return wrapper
|
|
197
|
+
|
|
198
|
+
@handle_db_errors
|
|
199
|
+
def fetch_user(user_id: str): ...
|
|
200
|
+
|
|
201
|
+
@handle_db_errors
|
|
202
|
+
def save_order(order: Order): ...
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
#### 4. Communicate failure clearly at process and service boundaries
|
|
208
|
+
|
|
209
|
+
Every system boundary must signal failure explicitly:
|
|
210
|
+
|
|
211
|
+
- **OS processes** must exit with a **non-zero exit code** when something went wrong. Exit code `0` means success.
|
|
212
|
+
- **HTTP services** must return a **non-2xx/3xx status code** on error, accompanied by a response body that describes the problem without exposing internal system details (stack traces, SQL queries, internal paths, etc.).
|
|
213
|
+
- **All error responses** should be logged to the console/structured logger, especially system-level or unexpected errors. Operational teams must be able to find the cause from logs alone.
|
|
214
|
+
|
|
215
|
+
*Why:* Orchestrators, CI runners, load balancers, and callers all rely on these signals to detect failures automatically. A process or service that reports success on failure leads to silent data corruption and missed alerts.
|
|
216
|
+
|
|
217
|
+
**Examples:**
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
#!/usr/bin/env bash
|
|
221
|
+
set -euo pipefail # exit immediately on error, unset variable, or pipe failure
|
|
222
|
+
|
|
223
|
+
run_migration() {
|
|
224
|
+
./bin/migrate up || { echo "Migration failed" >&2; exit 1; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
run_migration
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// HTTP handler — bad: always 200, internals leaked
|
|
232
|
+
app.post("/orders", async (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const order = await createOrder(req.body);
|
|
235
|
+
res.json(order);
|
|
236
|
+
} catch (e) {
|
|
237
|
+
res.status(200).json({ error: e.stack }); // wrong status, stack leaked
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// HTTP handler — good: correct status, safe message, logged internally
|
|
242
|
+
app.post("/orders", async (req, res) => {
|
|
243
|
+
const result = await createOrder(req.body);
|
|
244
|
+
if (!result.ok) {
|
|
245
|
+
if (result.error === "validation") {
|
|
246
|
+
return res.status(400).json({ error: "Invalid order data" });
|
|
247
|
+
}
|
|
248
|
+
logger.error({ error: result.error }, "unexpected error creating order");
|
|
249
|
+
return res.status(500).json({ error: "Internal server error" });
|
|
250
|
+
}
|
|
251
|
+
res.status(201).json(result.value);
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
# FastAPI — good error response
|
|
257
|
+
@app.post("/orders", status_code=201)
|
|
258
|
+
def create_order_endpoint(payload: OrderRequest):
|
|
259
|
+
result = create_order(payload)
|
|
260
|
+
if isinstance(result, Err):
|
|
261
|
+
if result.error == "validation":
|
|
262
|
+
raise HTTPException(status_code=400, detail="Invalid order data")
|
|
263
|
+
logger.error("unexpected error creating order: %s", result.error)
|
|
264
|
+
raise HTTPException(status_code=500, detail="Internal server error")
|
|
265
|
+
return result.value
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
#### 5. Write test cases for error scenarios
|
|
271
|
+
|
|
272
|
+
Every module that handles errors must have dedicated test cases that verify the error paths. Do not only test the happy path.
|
|
273
|
+
|
|
274
|
+
*Why:* Error handling code is the code most likely to be broken and the code least likely to be exercised in manual testing. Without automated tests, regressions in error paths go undetected until production.
|
|
275
|
+
|
|
276
|
+
Typical error scenarios to cover:
|
|
277
|
+
|
|
278
|
+
- The dependency (DB, HTTP service, file system) is unavailable or times out.
|
|
279
|
+
- The input is invalid, missing, or out of range.
|
|
280
|
+
- A partial failure occurs (some items processed, some not).
|
|
281
|
+
- The system is in an unexpected state (e.g., record not found, duplicate key).
|
|
282
|
+
|
|
283
|
+
**Examples:**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// good — dedicated tests for error scenarios
|
|
287
|
+
describe("fetchUser", () => {
|
|
288
|
+
it("returns ok:true with the user when found", async () => {
|
|
289
|
+
db.query.mockResolvedValue(mockUser);
|
|
290
|
+
const result = await fetchUser("123");
|
|
291
|
+
expect(result).toEqual({ ok: true, value: mockUser });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("returns ok:false with 'not-found' when the user does not exist", async () => {
|
|
295
|
+
db.query.mockResolvedValue(null);
|
|
296
|
+
const result = await fetchUser("999");
|
|
297
|
+
expect(result).toEqual({ ok: false, error: "not-found" });
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("returns ok:false with 'db-error' when the db throws", async () => {
|
|
301
|
+
db.query.mockRejectedValue(new Error("connection refused"));
|
|
302
|
+
const result = await fetchUser("123");
|
|
303
|
+
expect(result).toEqual({ ok: false, error: "db-error" });
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# good
|
|
310
|
+
def test_fetch_user_found(mock_db):
|
|
311
|
+
mock_db.query.return_value = sample_user
|
|
312
|
+
result = fetch_user("123")
|
|
313
|
+
assert isinstance(result, Ok)
|
|
314
|
+
assert result.value == sample_user
|
|
315
|
+
|
|
316
|
+
def test_fetch_user_not_found(mock_db):
|
|
317
|
+
mock_db.query.return_value = None
|
|
318
|
+
result = fetch_user("999")
|
|
319
|
+
assert isinstance(result, Err)
|
|
320
|
+
assert result.error == "not-found"
|
|
321
|
+
|
|
322
|
+
def test_fetch_user_db_error(mock_db):
|
|
323
|
+
mock_db.query.side_effect = DBError("connection refused")
|
|
324
|
+
result = fetch_user("123")
|
|
325
|
+
assert isinstance(result, Err)
|
|
326
|
+
assert result.error == "db-error"
|
|
327
|
+
```
|