Skip to content

Commit 0ab5970

Browse files
authored
chore(skills): prevent string-derived error semantics (#376)
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
1 parent 1e29970 commit 0ab5970

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

  • .agents/skills/type-safe-error-handling
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
name: type-safe-error-handling
3+
description: Replace string-matched error classification with typed domain errors, value objects, and explicit boundary mappings. Use when code derives behavior from error messages, substrings, magic prefixes, loosely typed status strings, denial reasons, protocol categories, validation messages, or other stringly typed logic that should be represented at the data type level.
4+
allowed-tools:
5+
- Bash
6+
- Read
7+
- Edit
8+
- Grep
9+
---
10+
11+
# Type-Safe Error Handling
12+
13+
Use this skill when code lets human-readable text carry domain meaning. The
14+
agent should learn the shape of the domain, then make the semantic decision live
15+
in a type instead of in wording.
16+
17+
## Mental Model
18+
19+
A message can describe a fact, but once behavior depends on that message the
20+
message has become an accidental API. If changing a sentence can change an auth
21+
decision, retry policy, denial category, telemetry status, or protocol response,
22+
the code is missing a type.
23+
24+
Think in terms of information flow:
25+
26+
- Where is the domain fact first known?
27+
- Where is it collapsed into a `String`, status label, magic prefix, or loosely
28+
typed field?
29+
- Which later decision is trying to reconstruct that fact?
30+
- Which existing enum, value object, source error, or wire type should carry the
31+
fact instead?
32+
33+
## Principles
34+
35+
- Name the semantic fact at the source. `UnknownAccount`, `InvalidCredentials`,
36+
`MissingCredentialMaterial`, or `VerifierUnavailable` are better decision
37+
inputs than messages containing those words.
38+
- Preserve meaning across layers. A boundary conversion should be
39+
variant-to-variant or field-to-field, not format-and-parse.
40+
- Keep display text at presentation edges. Logs, diagnostics, and user-facing
41+
messages may stay stable, but they should explain typed decisions rather than
42+
drive them.
43+
- Preserve source chains when they explain cause. Do not choose between correct
44+
classification and observability if the error model can represent both.
45+
- Let wire compatibility and domain modeling coexist. Keep external strings,
46+
codes, or protobuf fields stable when required, but convert to richer domain
47+
types inside the owning package.
48+
- Respect local architecture. In Rust, keep value objects inside the owning
49+
crate unless ADR 0002 justifies a package boundary. For wire and persistence
50+
contracts, follow ADR 0009.
51+
52+
## Key Examples
53+
54+
Typed boundary mapping:
55+
56+
```rust
57+
impl From<ApiKeyError> for AuthCalloutError {
58+
fn from(error: ApiKeyError) -> Self {
59+
match error {
60+
ApiKeyError::Empty => CredentialError::InvalidRequest(...).into(),
61+
ApiKeyError::Unknown => CredentialError::InvalidCredentials(...).into(),
62+
ApiKeyError::AudienceMismatch { .. } => CredentialError::InvalidRequest(...).into(),
63+
ApiKeyError::CallerIdDerivation(_) => CredentialError::InvalidCredentials(...).into(),
64+
}
65+
}
66+
}
67+
```
68+
69+
String-derived behavior:
70+
71+
```rust
72+
let category = if error.to_string().contains("not found") {
73+
DenialCategory::InvalidCredentials
74+
} else {
75+
DenialCategory::InternalError
76+
};
77+
```
78+
79+
The first shape carries meaning through variants. The second makes wording part
80+
of control flow.
81+
82+
Local examples to learn from:
83+
84+
- `rsworkspace/crates/a2a-auth-callout/src/denial_category.rs` classifies
85+
`CredentialVerification(String)` with `msg.contains(...)`. The category
86+
decision belongs in typed credential or domain errors that map directly to
87+
`DenialCategory`.
88+
- `rsworkspace/crates/a2a-auth-callout/src/account_resolver.rs` converts
89+
`AccountResolverError` through `value.to_string()`. The resolver already knows
90+
whether the problem is `EmptyRequest` or `Unknown`; that distinction should
91+
survive until category mapping.
92+
- `ApiKeyError`-style mappings should stay variant-to-variant. Empty input,
93+
unknown key, caller-id derivation failure, and audience mismatch are different
94+
facts even if their display messages all end up near credential verification.
95+
96+
## Smells
97+
98+
Pause and reason from the principles when you see:
99+
100+
- `contains`, `starts_with`, regexes, or equality checks against error messages.
101+
- `map_err(|e| SomeError(e.to_string()))` before another layer classifies the
102+
result.
103+
- catch-all `String` variants used for multiple semantic cases.
104+
- tests proving semantic behavior through substrings.
105+
- protocol categories inferred from local wording instead of typed tags.
106+
107+
## Verification
108+
109+
Confidence comes from tests that prove the type-level contract:
110+
111+
- each source variant maps to the intended typed category.
112+
- wire-visible strings or codes remain compatible when compatibility matters.
113+
- wrapped sources remain available through `source()` where supported.
114+
- unknown or internal errors do not accidentally become user-correctable errors.
115+
116+
String assertions are allowed for stable display text, but not as the only proof
117+
of semantic behavior.
118+
119+
## Done
120+
121+
The task is done when a future wording change cannot change behavior, and a
122+
reviewer can point to the type that owns each semantic decision.

0 commit comments

Comments
 (0)