@coralai/sps-cli 0.42.0 → 0.44.0

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.
Files changed (147) hide show
  1. package/README.md +59 -4
  2. package/dist/commands/consoleCommand.d.ts +2 -0
  3. package/dist/commands/consoleCommand.d.ts.map +1 -0
  4. package/dist/commands/consoleCommand.js +129 -0
  5. package/dist/commands/consoleCommand.js.map +1 -0
  6. package/dist/commands/projectInit.d.ts.map +1 -1
  7. package/dist/commands/projectInit.js +40 -53
  8. package/dist/commands/projectInit.js.map +1 -1
  9. package/dist/commands/setup.d.ts.map +1 -1
  10. package/dist/commands/setup.js +14 -2
  11. package/dist/commands/setup.js.map +1 -1
  12. package/dist/commands/skillCommand.d.ts +2 -0
  13. package/dist/commands/skillCommand.d.ts.map +1 -0
  14. package/dist/commands/skillCommand.js +235 -0
  15. package/dist/commands/skillCommand.js.map +1 -0
  16. package/dist/console-assets/assets/index-Bhd2f9AP.js +125 -0
  17. package/dist/console-assets/assets/index-bsAN2a12.css +1 -0
  18. package/dist/console-assets/index.html +16 -0
  19. package/dist/console-server/index.d.ts +29 -0
  20. package/dist/console-server/index.d.ts.map +1 -0
  21. package/dist/console-server/index.js +145 -0
  22. package/dist/console-server/index.js.map +1 -0
  23. package/dist/console-server/lib/lockFile.d.ts +17 -0
  24. package/dist/console-server/lib/lockFile.d.ts.map +1 -0
  25. package/dist/console-server/lib/lockFile.js +61 -0
  26. package/dist/console-server/lib/lockFile.js.map +1 -0
  27. package/dist/console-server/lib/portPicker.d.ts +3 -0
  28. package/dist/console-server/lib/portPicker.d.ts.map +1 -0
  29. package/dist/console-server/lib/portPicker.js +25 -0
  30. package/dist/console-server/lib/portPicker.js.map +1 -0
  31. package/dist/console-server/routes/projects.d.ts +11 -0
  32. package/dist/console-server/routes/projects.d.ts.map +1 -0
  33. package/dist/console-server/routes/projects.js +149 -0
  34. package/dist/console-server/routes/projects.js.map +1 -0
  35. package/dist/console-server/routes/system.d.ts +7 -0
  36. package/dist/console-server/routes/system.d.ts.map +1 -0
  37. package/dist/console-server/routes/system.js +19 -0
  38. package/dist/console-server/routes/system.js.map +1 -0
  39. package/dist/console-server/sse/eventBus.d.ts +25 -0
  40. package/dist/console-server/sse/eventBus.d.ts.map +1 -0
  41. package/dist/console-server/sse/eventBus.js +32 -0
  42. package/dist/console-server/sse/eventBus.js.map +1 -0
  43. package/dist/console-server/watchers/cardWatcher.d.ts +9 -0
  44. package/dist/console-server/watchers/cardWatcher.d.ts.map +1 -0
  45. package/dist/console-server/watchers/cardWatcher.js +42 -0
  46. package/dist/console-server/watchers/cardWatcher.js.map +1 -0
  47. package/dist/core/skillStore.d.ts +46 -0
  48. package/dist/core/skillStore.d.ts.map +1 -0
  49. package/dist/core/skillStore.js +210 -0
  50. package/dist/core/skillStore.js.map +1 -0
  51. package/dist/core/skillStore.test.d.ts +2 -0
  52. package/dist/core/skillStore.test.d.ts.map +1 -0
  53. package/dist/core/skillStore.test.js +203 -0
  54. package/dist/core/skillStore.test.js.map +1 -0
  55. package/dist/main.js +27 -17
  56. package/dist/main.js.map +1 -1
  57. package/package.json +8 -2
  58. package/skills/architecture-decision-records/SKILL.md +207 -0
  59. package/skills/backend/SKILL.md +62 -0
  60. package/skills/backend/references/api-design.md +168 -0
  61. package/skills/backend/references/caching.md +181 -0
  62. package/skills/backend/references/data-access.md +173 -0
  63. package/skills/backend/references/layering.md +181 -0
  64. package/skills/backend/references/observability.md +190 -0
  65. package/skills/backend/references/resilience.md +201 -0
  66. package/skills/backend/references/security.md +186 -0
  67. package/skills/backend-architect/SKILL.md +119 -0
  68. package/skills/code-reviewer/SKILL.md +143 -0
  69. package/skills/coding-standards/SKILL.md +60 -0
  70. package/skills/coding-standards/references/clean-code.md +258 -0
  71. package/skills/coding-standards/references/code-review.md +192 -0
  72. package/skills/coding-standards/references/commits-and-prs.md +226 -0
  73. package/skills/coding-standards/references/error-strategy.md +193 -0
  74. package/skills/coding-standards/references/naming.md +185 -0
  75. package/skills/coding-standards/references/tdd.md +171 -0
  76. package/skills/database/SKILL.md +53 -0
  77. package/skills/database/references/indexing.md +190 -0
  78. package/skills/database/references/migrations.md +199 -0
  79. package/skills/database/references/nosql.md +185 -0
  80. package/skills/database/references/queries.md +295 -0
  81. package/skills/database/references/scaling.md +203 -0
  82. package/skills/database/references/schema.md +191 -0
  83. package/skills/database-optimizer/SKILL.md +168 -0
  84. package/skills/debugging-workflow/SKILL.md +244 -0
  85. package/skills/devops/SKILL.md +55 -0
  86. package/skills/devops/references/ci-cd.md +204 -0
  87. package/skills/devops/references/containers.md +272 -0
  88. package/skills/devops/references/deploy.md +201 -0
  89. package/skills/devops/references/iac.md +252 -0
  90. package/skills/devops/references/observability.md +228 -0
  91. package/skills/devops/references/secrets.md +178 -0
  92. package/skills/devops-automator/SKILL.md +164 -0
  93. package/skills/frontend/SKILL.md +52 -0
  94. package/skills/frontend/references/accessibility.md +222 -0
  95. package/skills/frontend/references/components.md +206 -0
  96. package/skills/frontend/references/performance.md +219 -0
  97. package/skills/frontend/references/routing.md +209 -0
  98. package/skills/frontend/references/state.md +190 -0
  99. package/skills/frontend/references/testing.md +216 -0
  100. package/skills/frontend-developer/SKILL.md +115 -0
  101. package/skills/git-workflow/SKILL.md +355 -0
  102. package/skills/golang/SKILL.md +49 -0
  103. package/skills/golang/references/concurrency.md +284 -0
  104. package/skills/golang/references/errors.md +241 -0
  105. package/skills/golang/references/idioms.md +285 -0
  106. package/skills/golang/references/testing.md +238 -0
  107. package/skills/java/SKILL.md +50 -0
  108. package/skills/java/references/concurrency.md +194 -0
  109. package/skills/java/references/idioms.md +283 -0
  110. package/skills/java/references/testing.md +228 -0
  111. package/skills/kotlin/SKILL.md +47 -0
  112. package/skills/kotlin/references/coroutines.md +240 -0
  113. package/skills/kotlin/references/idioms.md +268 -0
  114. package/skills/kotlin/references/testing.md +219 -0
  115. package/skills/mobile/SKILL.md +50 -0
  116. package/skills/mobile/references/architecture.md +204 -0
  117. package/skills/mobile/references/navigation.md +158 -0
  118. package/skills/mobile/references/performance.md +152 -0
  119. package/skills/mobile/references/platform.md +166 -0
  120. package/skills/mobile/references/state-and-data.md +174 -0
  121. package/skills/python/SKILL.md +51 -0
  122. package/skills/python/THIRD_PARTY.md +14 -0
  123. package/skills/python/references/async.md +218 -0
  124. package/skills/python/references/error-handling.md +254 -0
  125. package/skills/python/references/idioms.md +279 -0
  126. package/skills/python/references/packaging.md +233 -0
  127. package/skills/python/references/testing.md +269 -0
  128. package/skills/python/references/typing.md +292 -0
  129. package/skills/qa-tester/SKILL.md +186 -0
  130. package/skills/rust/SKILL.md +50 -0
  131. package/skills/rust/references/async.md +224 -0
  132. package/skills/rust/references/errors.md +240 -0
  133. package/skills/rust/references/ownership.md +263 -0
  134. package/skills/rust/references/testing.md +274 -0
  135. package/skills/rust/references/traits.md +250 -0
  136. package/skills/security-engineer/SKILL.md +157 -0
  137. package/skills/swift/SKILL.md +48 -0
  138. package/skills/swift/references/concurrency.md +280 -0
  139. package/skills/swift/references/idioms.md +334 -0
  140. package/skills/swift/references/testing.md +229 -0
  141. package/skills/typescript/SKILL.md +51 -0
  142. package/skills/typescript/references/async.md +241 -0
  143. package/skills/typescript/references/errors.md +208 -0
  144. package/skills/typescript/references/idioms.md +246 -0
  145. package/skills/typescript/references/testing.md +225 -0
  146. package/skills/typescript/references/tooling.md +208 -0
  147. package/skills/typescript/references/types.md +259 -0
@@ -0,0 +1,218 @@
1
+ # Python Async / Concurrency
2
+
3
+ `asyncio` patterns for I/O-bound concurrency. For CPU-bound work, use processes, not async.
4
+
5
+ ## When to use what
6
+
7
+ | Workload | Tool | Why |
8
+ |---|---|---|
9
+ | I/O-bound (network, files, DB) | `asyncio` | One thread, thousands of connections |
10
+ | CPU-bound (crunching, crypto) | `multiprocessing` / `concurrent.futures.ProcessPoolExecutor` | GIL blocks threads |
11
+ | Blocking library you can't async-ify | `asyncio.to_thread()` | Offload to the default thread pool |
12
+ | Parallelizing blocking syscalls | `concurrent.futures.ThreadPoolExecutor` | Simple, no asyncio required |
13
+
14
+ `asyncio` without I/O is pointless — a `async def` that only does CPU work gives you no concurrency.
15
+
16
+ ## `async def` / `await` basics
17
+
18
+ ```python
19
+ import asyncio
20
+ import httpx
21
+
22
+ async def fetch(url: str) -> str:
23
+ async with httpx.AsyncClient() as client:
24
+ r = await client.get(url)
25
+ return r.text
26
+
27
+ async def main() -> None:
28
+ html = await fetch("https://example.com")
29
+ print(len(html))
30
+
31
+ asyncio.run(main())
32
+ ```
33
+
34
+ Rule: never call `asyncio.run()` from inside already-running async code. It's the entry point, not a utility.
35
+
36
+ ## Concurrency — run coroutines in parallel
37
+
38
+ ### `asyncio.gather` (legacy, still common)
39
+
40
+ ```python
41
+ async def fetch_all(urls: list[str]) -> list[str]:
42
+ return await asyncio.gather(*(fetch(u) for u in urls))
43
+
44
+ # With return_exceptions — failures don't cancel siblings
45
+ results = await asyncio.gather(*tasks, return_exceptions=True)
46
+ for r in results:
47
+ if isinstance(r, Exception):
48
+ log.warning("one failed: %s", r)
49
+ ```
50
+
51
+ ### `asyncio.TaskGroup` (Python 3.11+) — preferred
52
+
53
+ Structured concurrency: if any task raises, siblings are cancelled and errors are aggregated into an `ExceptionGroup`.
54
+
55
+ ```python
56
+ async def fetch_all(urls: list[str]) -> list[str]:
57
+ async with asyncio.TaskGroup() as tg:
58
+ tasks = [tg.create_task(fetch(u)) for u in urls]
59
+ return [t.result() for t in tasks]
60
+
61
+ # Error handling
62
+ try:
63
+ await fetch_all(urls)
64
+ except* httpx.ConnectError as eg:
65
+ log.warning("network failures: %d", len(eg.exceptions))
66
+ except* httpx.HTTPStatusError as eg:
67
+ log.error("HTTP errors: %s", [e.response.status_code for e in eg.exceptions])
68
+ ```
69
+
70
+ Prefer `TaskGroup` over `gather` in new code — cancellation is correct by default.
71
+
72
+ ## Timeouts
73
+
74
+ ### Python 3.11+: `asyncio.timeout`
75
+
76
+ ```python
77
+ async def with_deadline():
78
+ async with asyncio.timeout(5.0):
79
+ return await slow_operation()
80
+
81
+ # Nested / reschedulable
82
+ async with asyncio.timeout(None) as cm:
83
+ cm.reschedule(asyncio.get_running_loop().time() + 10)
84
+ ...
85
+ ```
86
+
87
+ ### Older: `asyncio.wait_for`
88
+
89
+ ```python
90
+ result = await asyncio.wait_for(slow_operation(), timeout=5.0)
91
+ ```
92
+
93
+ ## Cancellation
94
+
95
+ Cancellation is a `CancelledError` injected at the next `await`. Rules:
96
+
97
+ - **Never swallow `CancelledError`** except to run cleanup; always re-raise.
98
+ - Cleanup in `finally` must itself be fast and cancellation-safe.
99
+ - `asyncio.shield()` protects a critical section from outer cancellation.
100
+
101
+ ```python
102
+ async def worker():
103
+ try:
104
+ while True:
105
+ await do_work()
106
+ except asyncio.CancelledError:
107
+ await cleanup() # fast, idempotent
108
+ raise # REQUIRED — don't swallow
109
+
110
+ async def critical_write(path, data):
111
+ # Outer cancel won't interrupt the write
112
+ await asyncio.shield(write_file(path, data))
113
+ ```
114
+
115
+ ## Offloading blocking code
116
+
117
+ Never call blocking code from an async function — it stalls the event loop for everyone.
118
+
119
+ ```python
120
+ # Wrong: blocks the event loop
121
+ async def handler(req):
122
+ data = requests.get(req.url).text # ❌ blocking library in async code
123
+ return data
124
+
125
+ # Right: offload to thread pool
126
+ async def handler(req):
127
+ data = await asyncio.to_thread(requests.get, req.url)
128
+ return data.text
129
+
130
+ # Or use an async-native library
131
+ async def handler(req):
132
+ async with httpx.AsyncClient() as client:
133
+ r = await client.get(req.url)
134
+ return r.text
135
+ ```
136
+
137
+ `time.sleep()` in async code is always a bug — use `await asyncio.sleep()`.
138
+
139
+ ## Async iteration & generators
140
+
141
+ ```python
142
+ # Async iterator
143
+ async def stream_lines(url: str) -> AsyncIterator[str]:
144
+ async with httpx.AsyncClient() as client:
145
+ async with client.stream("GET", url) as resp:
146
+ async for line in resp.aiter_lines():
147
+ yield line
148
+
149
+ async for line in stream_lines(url):
150
+ process(line)
151
+
152
+ # Async comprehension
153
+ urls = [u async for u in stream_urls() if u.startswith("https://")]
154
+ ```
155
+
156
+ ## Async context managers
157
+
158
+ ```python
159
+ from contextlib import asynccontextmanager
160
+
161
+ @asynccontextmanager
162
+ async def db_transaction(conn):
163
+ tx = await conn.begin()
164
+ try:
165
+ yield tx
166
+ except Exception:
167
+ await tx.rollback()
168
+ raise
169
+ else:
170
+ await tx.commit()
171
+
172
+ async with db_transaction(conn) as tx:
173
+ await tx.execute(...)
174
+ ```
175
+
176
+ ## Backpressure with semaphores
177
+
178
+ Limit in-flight concurrency when the downstream can't take unlimited load.
179
+
180
+ ```python
181
+ async def fetch_bounded(urls: list[str], limit: int = 10) -> list[str]:
182
+ sem = asyncio.Semaphore(limit)
183
+
184
+ async def one(u):
185
+ async with sem:
186
+ return await fetch(u)
187
+
188
+ async with asyncio.TaskGroup() as tg:
189
+ tasks = [tg.create_task(one(u)) for u in urls]
190
+ return [t.result() for t in tasks]
191
+ ```
192
+
193
+ ## Anti-patterns
194
+
195
+ | Anti-pattern | Fix |
196
+ |---|---|
197
+ | `asyncio.run()` inside async code | Pass awaitables up; `asyncio.run()` is the entry point only |
198
+ | `time.sleep()` in async | `await asyncio.sleep()` |
199
+ | `requests` / blocking HTTP in async | Use `httpx.AsyncClient` or `asyncio.to_thread` |
200
+ | Swallowing `CancelledError` | Always re-raise after cleanup |
201
+ | Fire-and-forget `asyncio.create_task(x)` without keeping a reference | Reference dropped → task can be garbage-collected mid-flight |
202
+ | Mixing threads and asyncio naively | Use `asyncio.to_thread` / `loop.run_in_executor`, not raw `threading` |
203
+ | Using async just because "it's modern" | Async has real overhead; pure CPU or simple scripts don't need it |
204
+
205
+ ## Running background tasks correctly
206
+
207
+ ```python
208
+ # Wrong: task may be GC'd before it runs
209
+ asyncio.create_task(background())
210
+
211
+ # Right: keep a reference
212
+ _background_tasks: set[asyncio.Task] = set()
213
+
214
+ def schedule(coro):
215
+ t = asyncio.create_task(coro)
216
+ _background_tasks.add(t)
217
+ t.add_done_callback(_background_tasks.discard)
218
+ ```
@@ -0,0 +1,254 @@
1
+ # Python Error Handling
2
+
3
+ Exception design and handling patterns.
4
+
5
+ ## Specific exceptions only
6
+
7
+ Never `except:` (bare). Never `except Exception:` except at the top of a daemon/server loop.
8
+
9
+ ```python
10
+ # Good: specific exceptions, each with its own response
11
+ def load_config(path: str) -> Config:
12
+ try:
13
+ with open(path) as f:
14
+ return Config.from_json(f.read())
15
+ except FileNotFoundError as e:
16
+ raise ConfigError(f"Config not found: {path}") from e
17
+ except json.JSONDecodeError as e:
18
+ raise ConfigError(f"Invalid JSON in {path}: {e}") from e
19
+ except PermissionError as e:
20
+ raise ConfigError(f"No read permission: {path}") from e
21
+
22
+ # Bad: silent catch-all
23
+ def load_config(path: str) -> Config:
24
+ try:
25
+ with open(path) as f:
26
+ return Config.from_json(f.read())
27
+ except:
28
+ return None # caller has no idea what went wrong
29
+ ```
30
+
31
+ ## Exception chaining (`from`)
32
+
33
+ Always preserve the original traceback when re-raising.
34
+
35
+ ```python
36
+ def process(data: str) -> Result:
37
+ try:
38
+ parsed = json.loads(data)
39
+ except json.JSONDecodeError as e:
40
+ # `from e` attaches the original exception
41
+ raise ValueError(f"Bad data: {data!r}") from e
42
+
43
+ # Output traceback shows both:
44
+ # ValueError: Bad data: '...'
45
+ # The above exception was the direct cause of the following:
46
+ # json.JSONDecodeError: ...
47
+ ```
48
+
49
+ Use `raise ... from None` to **hide** the chain (rare — only when the chain is noise).
50
+
51
+ ## Custom exception hierarchy
52
+
53
+ One base class per module/service. Subclasses for specific failures.
54
+
55
+ ```python
56
+ # errors.py
57
+ class AppError(Exception):
58
+ """Base exception for this application."""
59
+
60
+ class ValidationError(AppError):
61
+ """Input validation failed."""
62
+
63
+ class NotFoundError(AppError):
64
+ """Resource not found."""
65
+
66
+ class AuthError(AppError):
67
+ """Authentication or authorization failed."""
68
+
69
+ class ExternalServiceError(AppError):
70
+ """Upstream dependency failed."""
71
+
72
+ # Usage
73
+ def get_user(user_id: str) -> User:
74
+ user = db.find_user(user_id)
75
+ if not user:
76
+ raise NotFoundError(f"User {user_id}")
77
+ return user
78
+
79
+ # Callers catch at the right level
80
+ try:
81
+ user = get_user(uid)
82
+ except NotFoundError:
83
+ return 404
84
+ except AppError:
85
+ return 500 # any other app error
86
+ except Exception:
87
+ logger.exception("unexpected")
88
+ return 500
89
+ ```
90
+
91
+ ## Rules
92
+
93
+ | Rule | Why |
94
+ |---|---|
95
+ | Catch only exceptions you can handle | If you can't recover, let it propagate |
96
+ | Never catch `BaseException` | That includes `KeyboardInterrupt` and `SystemExit` |
97
+ | Log before re-raising | Otherwise the log line says "unexpected" when you actually expected it |
98
+ | Specific exception types | Callers can pattern-match; `Exception` forces logs to be the only debugging aid |
99
+ | Use `try/except/else` for 2-phase operations | `else` runs only if no exception — clearer than nesting |
100
+
101
+ ## `try/except/else/finally`
102
+
103
+ ```python
104
+ def fetch(url: str) -> Response:
105
+ try:
106
+ response = http.get(url)
107
+ except TimeoutError:
108
+ return fallback_response()
109
+ except HTTPError as e:
110
+ logger.error("http error: %s", e)
111
+ raise
112
+ else:
113
+ # runs only on success — cleaner than putting this in try
114
+ cache.set(url, response)
115
+ return response
116
+ finally:
117
+ # always runs
118
+ release_connection()
119
+ ```
120
+
121
+ ## Context manager for error handling
122
+
123
+ For repeated try/except patterns, wrap in a context manager.
124
+
125
+ ```python
126
+ from contextlib import contextmanager
127
+
128
+ @contextmanager
129
+ def as_domain_error(target_type: type[Exception], msg: str):
130
+ """Re-raise any exception as target_type(msg)."""
131
+ try:
132
+ yield
133
+ except Exception as e:
134
+ raise target_type(msg) from e
135
+
136
+ # Usage
137
+ with as_domain_error(ConfigError, "Failed to load config"):
138
+ with open('config.json') as f:
139
+ return json.load(f)
140
+ ```
141
+
142
+ ## Validation vs exceptions
143
+
144
+ For user-input validation, prefer returning a result object over raising.
145
+
146
+ ```python
147
+ from dataclasses import dataclass
148
+
149
+ @dataclass
150
+ class ValidationResult:
151
+ valid: bool
152
+ errors: list[str]
153
+
154
+ def validate_user(data: dict) -> ValidationResult:
155
+ errors = []
156
+ if not data.get('email'):
157
+ errors.append('email required')
158
+ if 'age' in data and not 0 <= data['age'] <= 150:
159
+ errors.append('age must be 0-150')
160
+ return ValidationResult(valid=not errors, errors=errors)
161
+
162
+ # Caller
163
+ result = validate_user(input)
164
+ if not result.valid:
165
+ return {"errors": result.errors}, 400
166
+ ```
167
+
168
+ Raise exceptions for **exceptional** conditions. Validation failure is **expected**.
169
+
170
+ ## Exception groups (Python 3.11+)
171
+
172
+ For reporting multiple failures at once — concurrent tasks, batch operations, validation aggregates. Use `ExceptionGroup` and `except*`.
173
+
174
+ ```python
175
+ def import_batch(records: list[dict]) -> None:
176
+ errors = []
177
+ for r in records:
178
+ try:
179
+ import_one(r)
180
+ except (ValidationError, DBError) as e:
181
+ errors.append(e)
182
+ if errors:
183
+ raise ExceptionGroup("batch import failed", errors)
184
+
185
+ # Caller: except* matches by type across the group
186
+ try:
187
+ import_batch(data)
188
+ except* ValidationError as eg:
189
+ for e in eg.exceptions:
190
+ log.warning("invalid: %s", e)
191
+ except* DBError as eg:
192
+ for e in eg.exceptions:
193
+ log.error("db failure: %s", e)
194
+ ```
195
+
196
+ `asyncio.TaskGroup` (3.11+) raises `ExceptionGroup` natively when multiple child tasks fail.
197
+
198
+ ## Logging exceptions
199
+
200
+ Use `logger.exception()` inside an except block — it auto-includes the traceback.
201
+
202
+ ```python
203
+ import logging
204
+ logger = logging.getLogger(__name__)
205
+
206
+ try:
207
+ risky_operation()
208
+ except ExternalServiceError:
209
+ # logger.exception is equivalent to logger.error(exc_info=True)
210
+ logger.exception("External service failed")
211
+ # Re-raise or handle
212
+ raise
213
+ ```
214
+
215
+ **Never** use `print(e)` in production — print goes to stdout, not your log pipeline, and loses the traceback.
216
+
217
+ ## Common mistakes
218
+
219
+ ```python
220
+ # ❌ Mutating `except` clause without `from`
221
+ try: ...
222
+ except ValueError:
223
+ raise TypeError("wrong type") # original traceback lost
224
+
225
+ # ✅
226
+ try: ...
227
+ except ValueError as e:
228
+ raise TypeError("wrong type") from e
229
+
230
+
231
+ # ❌ Catching to "convert to None"
232
+ try:
233
+ return lookup(key)
234
+ except KeyError:
235
+ return None # if this is your pattern, use dict.get(key) — simpler
236
+
237
+ # ✅
238
+ return mapping.get(key)
239
+
240
+
241
+ # ❌ Overcatching hides bugs
242
+ try:
243
+ data = get_user(uid)
244
+ data.name = new_name # if this raises, you misclassify it as "user not found"
245
+ except Exception:
246
+ return 404
247
+
248
+ # ✅
249
+ try:
250
+ data = get_user(uid)
251
+ except NotFoundError:
252
+ return 404
253
+ data.name = new_name # let real bugs propagate
254
+ ```