@booklib/skills 1.2.0 → 1.3.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.
- package/CONTRIBUTING.md +122 -0
- package/README.md +20 -2
- package/ROADMAP.md +36 -0
- package/animation-at-work/evals/evals.json +44 -0
- package/animation-at-work/examples/after.md +64 -0
- package/animation-at-work/examples/before.md +35 -0
- package/animation-at-work/scripts/audit_animations.py +295 -0
- package/bin/skills.js +552 -42
- package/clean-code-reviewer/SKILL.md +109 -1
- package/clean-code-reviewer/evals/evals.json +121 -3
- package/clean-code-reviewer/examples/after.md +48 -0
- package/clean-code-reviewer/examples/before.md +33 -0
- package/clean-code-reviewer/references/api_reference.md +158 -0
- package/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/clean-code-reviewer/references/review-checklist.md +254 -0
- package/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/data-intensive-patterns/evals/evals.json +43 -0
- package/data-intensive-patterns/examples/after.md +61 -0
- package/data-intensive-patterns/examples/before.md +38 -0
- package/data-intensive-patterns/scripts/adr.py +213 -0
- package/data-pipelines/evals/evals.json +45 -0
- package/data-pipelines/examples/after.md +97 -0
- package/data-pipelines/examples/before.md +37 -0
- package/data-pipelines/scripts/new_pipeline.py +444 -0
- package/design-patterns/evals/evals.json +46 -0
- package/design-patterns/examples/after.md +52 -0
- package/design-patterns/examples/before.md +29 -0
- package/design-patterns/scripts/scaffold.py +807 -0
- package/domain-driven-design/SKILL.md +120 -0
- package/domain-driven-design/evals/evals.json +48 -0
- package/domain-driven-design/examples/after.md +80 -0
- package/domain-driven-design/examples/before.md +43 -0
- package/domain-driven-design/scripts/scaffold.py +421 -0
- package/effective-java/evals/evals.json +46 -0
- package/effective-java/examples/after.md +83 -0
- package/effective-java/examples/before.md +37 -0
- package/effective-java/scripts/checkstyle_setup.py +211 -0
- package/effective-kotlin/evals/evals.json +45 -0
- package/effective-kotlin/examples/after.md +36 -0
- package/effective-kotlin/examples/before.md +38 -0
- package/effective-python/evals/evals.json +44 -0
- package/effective-python/examples/after.md +56 -0
- package/effective-python/examples/before.md +40 -0
- package/effective-python/references/api_reference.md +218 -0
- package/effective-python/references/practices-catalog.md +483 -0
- package/effective-python/references/review-checklist.md +190 -0
- package/effective-python/scripts/lint.py +173 -0
- package/kotlin-in-action/evals/evals.json +43 -0
- package/kotlin-in-action/examples/after.md +53 -0
- package/kotlin-in-action/examples/before.md +39 -0
- package/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/lean-startup/evals/evals.json +43 -0
- package/lean-startup/examples/after.md +80 -0
- package/lean-startup/examples/before.md +34 -0
- package/lean-startup/scripts/new_experiment.py +286 -0
- package/microservices-patterns/SKILL.md +140 -0
- package/microservices-patterns/evals/evals.json +45 -0
- package/microservices-patterns/examples/after.md +69 -0
- package/microservices-patterns/examples/before.md +40 -0
- package/microservices-patterns/scripts/new_service.py +583 -0
- package/package.json +2 -8
- package/refactoring-ui/evals/evals.json +45 -0
- package/refactoring-ui/examples/after.md +85 -0
- package/refactoring-ui/examples/before.md +58 -0
- package/refactoring-ui/scripts/audit_css.py +250 -0
- package/skill-router/SKILL.md +142 -0
- package/skill-router/evals/evals.json +38 -0
- package/skill-router/examples/after.md +63 -0
- package/skill-router/examples/before.md +39 -0
- package/skill-router/references/api_reference.md +24 -0
- package/skill-router/references/routing-heuristics.md +89 -0
- package/skill-router/references/skill-catalog.md +156 -0
- package/skill-router/scripts/route.py +266 -0
- package/storytelling-with-data/evals/evals.json +47 -0
- package/storytelling-with-data/examples/after.md +50 -0
- package/storytelling-with-data/examples/before.md +33 -0
- package/storytelling-with-data/scripts/chart_review.py +301 -0
- package/system-design-interview/evals/evals.json +45 -0
- package/system-design-interview/examples/after.md +94 -0
- package/system-design-interview/examples/before.md +27 -0
- package/system-design-interview/scripts/new_design.py +421 -0
- package/using-asyncio-python/evals/evals.json +43 -0
- package/using-asyncio-python/examples/after.md +68 -0
- package/using-asyncio-python/examples/before.md +39 -0
- package/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/web-scraping-python/evals/evals.json +46 -0
- package/web-scraping-python/examples/after.md +109 -0
- package/web-scraping-python/examples/before.md +40 -0
- package/web-scraping-python/scripts/new_scraper.py +231 -0
- /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
- /package/{effective-python-skill → effective-python}/ref-10-collaboration.md +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
pre-review.py — Pre-analysis script for Clean Code reviews.
|
|
4
|
+
Usage: python pre-review.py <file>
|
|
5
|
+
|
|
6
|
+
Produces a structured report covering file stats, long functions, deep nesting,
|
|
7
|
+
argument count violations, and linter output — ready to feed an agent as context.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def detect_language(path: Path) -> str:
|
|
18
|
+
return {
|
|
19
|
+
".py": "python",
|
|
20
|
+
".js": "javascript",
|
|
21
|
+
".ts": "typescript",
|
|
22
|
+
".java": "java",
|
|
23
|
+
".go": "go",
|
|
24
|
+
".rb": "ruby",
|
|
25
|
+
".rs": "rust",
|
|
26
|
+
}.get(path.suffix.lower(), "unknown")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def count_lines(source: str) -> int:
|
|
30
|
+
return len(source.splitlines())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def measure_nesting_depth(node: ast.AST, depth: int = 0) -> int:
|
|
34
|
+
nesting_nodes = (
|
|
35
|
+
ast.If, ast.For, ast.While, ast.With, ast.Try,
|
|
36
|
+
ast.ExceptHandler, ast.AsyncFor, ast.AsyncWith,
|
|
37
|
+
)
|
|
38
|
+
max_depth = depth
|
|
39
|
+
for child in ast.iter_child_nodes(node):
|
|
40
|
+
if isinstance(child, nesting_nodes):
|
|
41
|
+
max_depth = max(max_depth, measure_nesting_depth(child, depth + 1))
|
|
42
|
+
else:
|
|
43
|
+
max_depth = max(max_depth, measure_nesting_depth(child, depth))
|
|
44
|
+
return max_depth
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def analyze_python_ast(source: str):
|
|
48
|
+
"""Return function/class stats using AST. Returns list of dicts."""
|
|
49
|
+
try:
|
|
50
|
+
tree = ast.parse(source)
|
|
51
|
+
except SyntaxError as exc:
|
|
52
|
+
return None, f"AST parse failed: {exc}"
|
|
53
|
+
|
|
54
|
+
lines = source.splitlines()
|
|
55
|
+
results = []
|
|
56
|
+
|
|
57
|
+
for node in ast.walk(tree):
|
|
58
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
kind = "class" if isinstance(node, ast.ClassDef) else "function"
|
|
62
|
+
start = node.lineno
|
|
63
|
+
end = node.end_lineno if hasattr(node, "end_lineno") else start
|
|
64
|
+
length = end - start + 1
|
|
65
|
+
|
|
66
|
+
arg_count = 0
|
|
67
|
+
nesting = 0
|
|
68
|
+
if kind == "function":
|
|
69
|
+
args = node.args
|
|
70
|
+
arg_count = (
|
|
71
|
+
len(args.args)
|
|
72
|
+
+ len(args.posonlyargs)
|
|
73
|
+
+ len(args.kwonlyargs)
|
|
74
|
+
+ (1 if args.vararg else 0)
|
|
75
|
+
+ (1 if args.kwarg else 0)
|
|
76
|
+
)
|
|
77
|
+
# Don't count 'self' / 'cls'
|
|
78
|
+
first = args.posonlyargs[0].arg if args.posonlyargs else (args.args[0].arg if args.args else None)
|
|
79
|
+
if first in ("self", "cls"):
|
|
80
|
+
arg_count = max(0, arg_count - 1)
|
|
81
|
+
nesting = measure_nesting_depth(node)
|
|
82
|
+
|
|
83
|
+
results.append({
|
|
84
|
+
"kind": kind,
|
|
85
|
+
"name": node.name,
|
|
86
|
+
"start": start,
|
|
87
|
+
"end": end,
|
|
88
|
+
"length": length,
|
|
89
|
+
"arg_count": arg_count,
|
|
90
|
+
"nesting": nesting,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return results, None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def run_ruff(filepath: Path):
|
|
97
|
+
"""Run ruff on the file; return (output_lines, error_message)."""
|
|
98
|
+
try:
|
|
99
|
+
result = subprocess.run(
|
|
100
|
+
["ruff", "check", "--output-format", "concise", str(filepath)],
|
|
101
|
+
capture_output=True, text=True, timeout=30,
|
|
102
|
+
)
|
|
103
|
+
output = (result.stdout + result.stderr).strip()
|
|
104
|
+
return output.splitlines() if output else [], None
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
return [], "ruff not installed (pip install ruff)"
|
|
107
|
+
except subprocess.TimeoutExpired:
|
|
108
|
+
return [], "ruff timed out"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def separator(char="-", width=70):
|
|
112
|
+
return char * width
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main():
|
|
116
|
+
if len(sys.argv) < 2:
|
|
117
|
+
print("Usage: python pre-review.py <file>")
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
filepath = Path(sys.argv[1])
|
|
121
|
+
if not filepath.exists():
|
|
122
|
+
print(f"Error: file not found: {filepath}")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
source = filepath.read_text(encoding="utf-8", errors="replace")
|
|
126
|
+
language = detect_language(filepath)
|
|
127
|
+
total_lines = count_lines(source)
|
|
128
|
+
file_size = filepath.stat().st_size
|
|
129
|
+
|
|
130
|
+
print(separator("="))
|
|
131
|
+
print(f"CLEAN CODE PRE-REVIEW REPORT")
|
|
132
|
+
print(separator("="))
|
|
133
|
+
print(f"File : {filepath}")
|
|
134
|
+
print(f"Language : {language}")
|
|
135
|
+
print(f"Size : {file_size:,} bytes | {total_lines} lines")
|
|
136
|
+
print()
|
|
137
|
+
|
|
138
|
+
# --- AST analysis (Python only) ---
|
|
139
|
+
if language == "python":
|
|
140
|
+
print(separator())
|
|
141
|
+
print("FUNCTION / CLASS ANALYSIS (AST)")
|
|
142
|
+
print(separator())
|
|
143
|
+
items, err = analyze_python_ast(source)
|
|
144
|
+
if err:
|
|
145
|
+
print(f" Warning: {err}")
|
|
146
|
+
elif items:
|
|
147
|
+
long_fns = [i for i in items if i["kind"] == "function" and i["length"] > 20]
|
|
148
|
+
deep_fns = [i for i in items if i["kind"] == "function" and i["nesting"] >= 3]
|
|
149
|
+
many_args = [i for i in items if i["kind"] == "function" and i["arg_count"] > 3]
|
|
150
|
+
|
|
151
|
+
print(f" Total functions : {sum(1 for i in items if i['kind'] == 'function')}")
|
|
152
|
+
print(f" Total classes : {sum(1 for i in items if i['kind'] == 'class')}")
|
|
153
|
+
print()
|
|
154
|
+
|
|
155
|
+
if long_fns:
|
|
156
|
+
print(f" [!] LONG FUNCTIONS (>20 lines) — Clean Code: functions should do one thing")
|
|
157
|
+
for fn in long_fns:
|
|
158
|
+
print(f" {fn['name']}() lines {fn['start']}-{fn['end']} ({fn['length']} lines)")
|
|
159
|
+
else:
|
|
160
|
+
print(" [OK] No functions exceed 20 lines.")
|
|
161
|
+
|
|
162
|
+
print()
|
|
163
|
+
if deep_fns:
|
|
164
|
+
print(f" [!] DEEP NESTING (>=3 levels) — consider early returns or extraction")
|
|
165
|
+
for fn in deep_fns:
|
|
166
|
+
print(f" {fn['name']}() line {fn['start']} (max nesting: {fn['nesting']})")
|
|
167
|
+
else:
|
|
168
|
+
print(" [OK] No functions have excessive nesting depth.")
|
|
169
|
+
|
|
170
|
+
print()
|
|
171
|
+
if many_args:
|
|
172
|
+
print(f" [!] TOO MANY ARGUMENTS (>3) — Clean Code: prefer parameter objects")
|
|
173
|
+
for fn in many_args:
|
|
174
|
+
print(f" {fn['name']}() line {fn['start']} ({fn['arg_count']} args)")
|
|
175
|
+
else:
|
|
176
|
+
print(" [OK] All functions have 3 or fewer arguments.")
|
|
177
|
+
else:
|
|
178
|
+
print(" No functions or classes found.")
|
|
179
|
+
print()
|
|
180
|
+
|
|
181
|
+
# --- Linter output ---
|
|
182
|
+
print(separator())
|
|
183
|
+
if language == "python":
|
|
184
|
+
print("RUFF LINTER OUTPUT")
|
|
185
|
+
print(separator())
|
|
186
|
+
ruff_lines, ruff_err = run_ruff(filepath)
|
|
187
|
+
if ruff_err:
|
|
188
|
+
print(f" Note: {ruff_err}")
|
|
189
|
+
elif ruff_lines:
|
|
190
|
+
for line in ruff_lines:
|
|
191
|
+
print(f" {line}")
|
|
192
|
+
else:
|
|
193
|
+
print(" [OK] ruff found no issues.")
|
|
194
|
+
else:
|
|
195
|
+
print(f"LINTER")
|
|
196
|
+
print(separator())
|
|
197
|
+
print(f" Automated linting not configured for '{language}'. Run language-specific tools manually.")
|
|
198
|
+
|
|
199
|
+
print()
|
|
200
|
+
print(separator("="))
|
|
201
|
+
print("END OF PRE-REVIEW REPORT")
|
|
202
|
+
print(separator("="))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
if __name__ == "__main__":
|
|
206
|
+
main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"evals": [
|
|
3
|
+
{
|
|
4
|
+
"id": "eval-01-synchronous-rest-no-event-log",
|
|
5
|
+
"prompt": "Review this microservices architecture description and pseudo-code:\n\n```\nSystem: E-commerce order processing\nServices: OrderService, InventoryService, PaymentService, NotificationService\n\nFlow (all synchronous REST over HTTP):\n\n# OrderService.place_order()\nPOST /orders → validates cart\n → GET /inventory/{sku} (blocks on InventoryService response)\n → POST /inventory/reserve (blocks, decrements stock)\n → POST /payments/charge (blocks on PaymentService)\n → POST /notifications/send (blocks on NotificationService)\n → INSERT orders (status='CONFIRMED') into orders_db\n → return 201\n\n# If PaymentService times out:\n# - inventory already reserved, order not yet written\n# - caller gets 504, retries POST /orders\n# - inventory reserved again (double reservation)\n\n# InventoryService\nGET /inventory/{sku} → SELECT stock FROM inventory WHERE sku=?\nPOST /inventory/reserve → UPDATE inventory SET stock=stock-qty WHERE sku=?\n\n# Shared database: orders_db is directly accessed by both OrderService\n# and InventoryService for reporting queries\n```",
|
|
6
|
+
"expectations": [
|
|
7
|
+
"Flags the fully synchronous REST chain as a distributed systems anti-pattern: a failure or timeout in any downstream service leaves the system in an inconsistent state (Ch 8: partial failures, timeouts, retries)",
|
|
8
|
+
"Identifies the double-reservation bug as a direct consequence of no idempotency on POST /inventory/reserve; recommends idempotency keys on all mutating endpoints (Ch 8: idempotent operations everywhere)",
|
|
9
|
+
"Flags the absence of an event log or write-ahead log: there is no source of truth to replay from if a service crashes mid-flow (Ch 11: event sourcing, log as source of truth)",
|
|
10
|
+
"Flags shared database access between OrderService and InventoryService as shared mutable state across services, violating service autonomy (Ch 12: shared mutable state across services anti-pattern)",
|
|
11
|
+
"Recommends replacing the synchronous chain with an event-driven approach: publish an OrderPlaced event, have downstream services react asynchronously (Ch 11: stream processing, event-driven)",
|
|
12
|
+
"Recommends the transactional outbox pattern to atomically write the order and publish the event in one local transaction (Ch 11: CDC, transactional outbox)",
|
|
13
|
+
"Notes that NotificationService is particularly ill-suited for the synchronous chain since a notification failure should not roll back a payment"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "eval-02-schema-without-access-patterns",
|
|
18
|
+
"prompt": "Review this database schema design:\n\n```sql\n-- Proposed schema for a social media analytics platform\n-- Requirements: show user activity feeds, compute engagement scores,\n-- support time-range queries on posts, and generate daily digest emails\n\nCREATE TABLE users (\n id BIGINT PRIMARY KEY,\n username VARCHAR(50),\n email VARCHAR(255),\n created_at TIMESTAMP\n);\n\nCREATE TABLE posts (\n id BIGINT PRIMARY KEY,\n user_id BIGINT REFERENCES users(id),\n content TEXT,\n created_at TIMESTAMP\n);\n\nCREATE TABLE events (\n id BIGINT PRIMARY KEY,\n post_id BIGINT REFERENCES posts(id),\n user_id BIGINT REFERENCES users(id),\n event_type VARCHAR(20), -- 'like', 'comment', 'share', 'view'\n occurred_at TIMESTAMP\n);\n\n-- All analytics queries will be run against these three tables via JOINs\n-- Indexes: only the primary keys above\n```",
|
|
19
|
+
"expectations": [
|
|
20
|
+
"Flags the absence of secondary indexes for the primary access patterns: time-range queries on posts (created_at) and per-user activity (user_id + created_at) will require full table scans (Ch 3: indexing strategies, B-tree vs LSM-tree, DDIA Chapter 3)",
|
|
21
|
+
"Identifies the schema is designed around normalization, not around read patterns; for analytics (read-heavy) workloads, this forces expensive multi-table JOINs on every query (Ch 2: document model vs relational model, denormalization for read-heavy workloads)",
|
|
22
|
+
"Flags that the `events` table mixing four different event types in one table with no partitioning will become a hot write target and a slow scan table as it grows (Ch 6: partitioning to spread load)",
|
|
23
|
+
"Notes there is no derived/materialized view for engagement scores, meaning every score computation re-scans all events; recommends pre-computed aggregates or a materialized view updated via CDC (Ch 11: derived data, CQRS)",
|
|
24
|
+
"Flags that the schema has no time-based partitioning on the `events` table despite time-range queries being a stated requirement (Ch 6: partitioning by key range for range scan efficiency)",
|
|
25
|
+
"Recommends separating the OLTP write path from the OLAP analytics read path, using CDC or batch export to feed an analytics store (Ch 10: batch processing, OLTP vs OLAP separation)"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"id": "eval-03-well-designed-event-sourced-system",
|
|
30
|
+
"prompt": "Review this event-sourced order system design:\n\n```python\n# Event definitions\n@dataclass(frozen=True)\nclass OrderPlaced:\n order_id: str\n customer_id: str\n items: tuple # immutable list of (sku, qty, price)\n occurred_at: datetime\n event_id: str = field(default_factory=lambda: str(uuid4()))\n\n@dataclass(frozen=True)\nclass OrderShipped:\n order_id: str\n tracking_number: str\n occurred_at: datetime\n event_id: str = field(default_factory=lambda: str(uuid4()))\n\n@dataclass(frozen=True)\nclass OrderCancelled:\n order_id: str\n reason: str\n occurred_at: datetime\n event_id: str = field(default_factory=lambda: str(uuid4()))\n\n# Append-only event store\nclass EventStore:\n def append(self, stream_id: str, events: list, expected_version: int) -> None:\n \"\"\"Append events with optimistic concurrency check.\"\"\"\n ...\n\n def load(self, stream_id: str) -> list:\n \"\"\"Load all events for a stream in order.\"\"\"\n ...\n\n# Aggregate rebuilt from events\nclass Order:\n def __init__(self):\n self.status = None\n self.items = []\n self._version = 0\n\n @classmethod\n def from_events(cls, events: list) -> 'Order':\n order = cls()\n for event in events:\n order._apply(event)\n return order\n\n def _apply(self, event):\n match event:\n case OrderPlaced(items=items):\n self.status = 'placed'\n self.items = list(items)\n case OrderShipped():\n self.status = 'shipped'\n case OrderCancelled():\n self.status = 'cancelled'\n self._version += 1\n\n# Idempotent consumer for search index projection\nclass SearchIndexProjection:\n def handle(self, event, event_id: str) -> None:\n if self._already_processed(event_id):\n return\n # update search index\n self._mark_processed(event_id)\n```",
|
|
31
|
+
"expectations": [
|
|
32
|
+
"Recognizes this is a well-designed event-sourced system and says so explicitly",
|
|
33
|
+
"Praises the append-only event store with optimistic concurrency control via `expected_version` preventing lost updates (Ch 7: transaction isolation, write conflicts)",
|
|
34
|
+
"Praises rebuilding aggregate state from events via `from_events` — the event log is the source of truth (Ch 11: event sourcing, log-centric architecture)",
|
|
35
|
+
"Praises frozen dataclasses for events ensuring immutability, which is correct for an append-only log (Ch 11: immutable events)",
|
|
36
|
+
"Praises the idempotent consumer with deduplication by `event_id` in `SearchIndexProjection` making the projection safe to replay (Ch 11: idempotent consumers, exactly-once semantics)",
|
|
37
|
+
"Praises the separation of the write model (Order aggregate) from the read model (SearchIndexProjection) as CQRS (Ch 11: CQRS, derived data)",
|
|
38
|
+
"Does NOT manufacture issues to appear thorough; any suggestions are explicitly framed as minor optional improvements",
|
|
39
|
+
"May suggest snapshotting for long-lived streams as a performance optimization, but frames it as a future concern, not a current violation"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# After
|
|
2
|
+
|
|
3
|
+
An event-driven architecture where writes go through a message log, read models are derived via CDC, read replicas serve analytics, and the command/query paths are separated.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
ARCHITECTURE: E-Commerce Platform (Event-Driven + CQRS)
|
|
7
|
+
|
|
8
|
+
WRITE PATH
|
|
9
|
+
──────────
|
|
10
|
+
[Mobile App / Web Browser]
|
|
11
|
+
│ REST (commands only: place order, update product)
|
|
12
|
+
v
|
|
13
|
+
[API Gateway] ──> [Order Command Service] ──> [Orders DB - Postgres]
|
|
14
|
+
│
|
|
15
|
+
[Debezium CDC connector]
|
|
16
|
+
│
|
|
17
|
+
v
|
|
18
|
+
[Kafka: order.events topic]
|
|
19
|
+
(append-only event log,
|
|
20
|
+
partitioned by order_id)
|
|
21
|
+
|
|
22
|
+
READ PATH (derived models — rebuilt from event log, no dual-writes)
|
|
23
|
+
──────────────────────────────────────────────────────────────────
|
|
24
|
+
[Kafka: order.events]
|
|
25
|
+
│
|
|
26
|
+
├──> [Inventory Consumer] ──> [Inventory Read DB - Postgres replica]
|
|
27
|
+
│ (product weekly sales view)
|
|
28
|
+
│
|
|
29
|
+
├──> [Search Consumer] ──> [Elasticsearch Index]
|
|
30
|
+
│ (product search, updated async)
|
|
31
|
+
│
|
|
32
|
+
└──> [Analytics Consumer] ──> [BigQuery Streaming Insert]
|
|
33
|
+
(append-only fact table,
|
|
34
|
+
no load on production DB)
|
|
35
|
+
|
|
36
|
+
QUERY ENDPOINTS (served from read models, not production write DB)
|
|
37
|
+
──────────────────────────────────────────────────────────────────
|
|
38
|
+
GET /inventory/reorder-candidates → Inventory Read DB
|
|
39
|
+
GET /search/products?q=... → Elasticsearch
|
|
40
|
+
GET /reports/revenue?period=... → BigQuery
|
|
41
|
+
|
|
42
|
+
ASYNC COORDINATION (replaces synchronous call chain)
|
|
43
|
+
────────────────────────────────────────────────────
|
|
44
|
+
Place order → write Orders DB → CDC → Kafka
|
|
45
|
+
→ Payment Service consumes event → publishes PaymentAuthorized
|
|
46
|
+
→ Notification Service consumes PaymentAuthorized → sends email
|
|
47
|
+
(no synchronous chain; each step is independently retried)
|
|
48
|
+
|
|
49
|
+
CONSISTENCY MODEL
|
|
50
|
+
─────────────────
|
|
51
|
+
Orders DB → strongly consistent (single Postgres primary)
|
|
52
|
+
Read models → eventually consistent (seconds of lag, acceptable for reads)
|
|
53
|
+
Analytics → eventually consistent (minutes of lag, acceptable for reports)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Key improvements:
|
|
57
|
+
- Append-only event log (Kafka) is the single source of truth — derived views are rebuilt from it, never maintained by dual-writes (Ch 11: derived data vs. system of record)
|
|
58
|
+
- CDC via Debezium captures changes from the Orders DB atomically — no risk of writing to DB and failing to publish the event (Ch 11: Change Data Capture)
|
|
59
|
+
- Analytics consumers write to BigQuery directly from Kafka — no SELECT queries on the production Orders DB (Ch 10: separation of OLTP and OLAP)
|
|
60
|
+
- CQRS separates command endpoints (write path) from query endpoints (read path) — each can scale independently
|
|
61
|
+
- The synchronous call chain (place order → payment → notification) is replaced by event-driven coordination — failure of one consumer does not block the order write
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Before
|
|
2
|
+
|
|
3
|
+
A text architecture diagram showing an e-commerce platform where every component communicates synchronously via REST with no event log, no read replicas, and no separation of read/write paths.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
ARCHITECTURE: E-Commerce Platform (Synchronous REST Only)
|
|
7
|
+
|
|
8
|
+
[Mobile App] ──REST──> [API Server]
|
|
9
|
+
[Web Browser] ──REST──> [API Server]
|
|
10
|
+
│
|
|
11
|
+
┌───────────────┼──────────────────┐
|
|
12
|
+
│ │ │
|
|
13
|
+
v v v
|
|
14
|
+
[Order DB] [Product DB] [User DB]
|
|
15
|
+
(Postgres) (Postgres) (Postgres)
|
|
16
|
+
│ │
|
|
17
|
+
v v
|
|
18
|
+
[Analytics REST] [Search REST] (both query
|
|
19
|
+
calls Order DB calls Product production
|
|
20
|
+
directly via DB directly DBs live)
|
|
21
|
+
SQL over HTTP
|
|
22
|
+
|
|
23
|
+
FLOWS:
|
|
24
|
+
Place order → API → write Order DB → REST call to
|
|
25
|
+
Inventory Service → REST call to
|
|
26
|
+
Payment Service → REST call to
|
|
27
|
+
Notification Service
|
|
28
|
+
(all synchronous, chain fails if any step fails)
|
|
29
|
+
|
|
30
|
+
Dashboard → API → query Order DB, Product DB, User DB
|
|
31
|
+
in sequence (3 serial DB queries on write path)
|
|
32
|
+
|
|
33
|
+
Search → API → query Product DB directly
|
|
34
|
+
(full table scans, no index service)
|
|
35
|
+
|
|
36
|
+
Reports → Analytics service polls Order DB every 5 min
|
|
37
|
+
via REST (puts load on production DB)
|
|
38
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
adr.py - Architecture Decision Record generator for data-intensive systems.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python adr.py <decision-title>
|
|
7
|
+
python adr.py # interactive mode
|
|
8
|
+
|
|
9
|
+
Generates:
|
|
10
|
+
adr-NNN-<slug>.md - Numbered ADR file with data-intensive-specific sections
|
|
11
|
+
ADR-INDEX.md - Running index of all ADRs (appended to)
|
|
12
|
+
|
|
13
|
+
The ADR includes standard sections plus four data-intensive-specific sections:
|
|
14
|
+
- Consistency model
|
|
15
|
+
- Failure mode
|
|
16
|
+
- Scalability impact
|
|
17
|
+
- Operability
|
|
18
|
+
|
|
19
|
+
Based on patterns from "Designing Data-Intensive Applications" by Martin Kleppmann.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import datetime
|
|
24
|
+
import pathlib
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def slugify(title: str) -> str:
|
|
30
|
+
slug = title.lower()
|
|
31
|
+
slug = re.sub(r"[^a-z0-9]+", "-", slug)
|
|
32
|
+
slug = slug.strip("-")
|
|
33
|
+
return slug
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def next_adr_number(adr_dir: pathlib.Path) -> int:
|
|
37
|
+
existing = list(adr_dir.glob("adr-[0-9][0-9][0-9]-*.md"))
|
|
38
|
+
if not existing:
|
|
39
|
+
return 1
|
|
40
|
+
numbers = []
|
|
41
|
+
for p in existing:
|
|
42
|
+
m = re.match(r"adr-(\d{3})-", p.name)
|
|
43
|
+
if m:
|
|
44
|
+
numbers.append(int(m.group(1)))
|
|
45
|
+
return max(numbers) + 1 if numbers else 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def prompt(question: str, default: str = "") -> str:
|
|
49
|
+
suffix = f" [{default}]" if default else ""
|
|
50
|
+
try:
|
|
51
|
+
answer = input(f"{question}{suffix}: ").strip()
|
|
52
|
+
except (EOFError, KeyboardInterrupt):
|
|
53
|
+
print()
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
return answer if answer else default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def collect_options() -> list[str]:
|
|
59
|
+
options = []
|
|
60
|
+
print("Enter up to 4 considered options (leave blank to stop):")
|
|
61
|
+
for i in range(1, 5):
|
|
62
|
+
opt = prompt(f" Option {i}")
|
|
63
|
+
if not opt:
|
|
64
|
+
break
|
|
65
|
+
options.append(opt)
|
|
66
|
+
return options
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def render_adr(
|
|
70
|
+
number: int,
|
|
71
|
+
title: str,
|
|
72
|
+
context: str,
|
|
73
|
+
options: list[str],
|
|
74
|
+
chosen: str,
|
|
75
|
+
consequences: str,
|
|
76
|
+
consistency_model: str,
|
|
77
|
+
failure_mode: str,
|
|
78
|
+
scalability_impact: str,
|
|
79
|
+
operability: str,
|
|
80
|
+
date: str,
|
|
81
|
+
) -> str:
|
|
82
|
+
options_text = "\n".join(f"- {opt}" for opt in options) if options else "- (none listed)"
|
|
83
|
+
return f"""\
|
|
84
|
+
# ADR-{number:03d}: {title}
|
|
85
|
+
|
|
86
|
+
**Date:** {date}
|
|
87
|
+
**Status:** Proposed
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Context
|
|
92
|
+
|
|
93
|
+
{context}
|
|
94
|
+
|
|
95
|
+
## Considered Options
|
|
96
|
+
|
|
97
|
+
{options_text}
|
|
98
|
+
|
|
99
|
+
## Decision
|
|
100
|
+
|
|
101
|
+
{chosen}
|
|
102
|
+
|
|
103
|
+
## Consequences
|
|
104
|
+
|
|
105
|
+
{consequences}
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Data-Intensive Considerations
|
|
110
|
+
|
|
111
|
+
### Consistency Model
|
|
112
|
+
|
|
113
|
+
> What consistency guarantees does this choice provide?
|
|
114
|
+
|
|
115
|
+
{consistency_model}
|
|
116
|
+
|
|
117
|
+
### Failure Mode
|
|
118
|
+
|
|
119
|
+
> What happens when this component fails?
|
|
120
|
+
|
|
121
|
+
{failure_mode}
|
|
122
|
+
|
|
123
|
+
### Scalability Impact
|
|
124
|
+
|
|
125
|
+
> How does this scale with data volume?
|
|
126
|
+
|
|
127
|
+
{scalability_impact}
|
|
128
|
+
|
|
129
|
+
### Operability
|
|
130
|
+
|
|
131
|
+
> How observable and maintainable is this choice?
|
|
132
|
+
|
|
133
|
+
{operability}
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def append_to_index(index_path: pathlib.Path, number: int, title: str, filename: str, date: str) -> None:
|
|
138
|
+
header = "# ADR Index\n\n| # | Title | Date | File |\n|---|-------|------|------|\n"
|
|
139
|
+
entry = f"| {number:03d} | {title} | {date} | [{filename}]({filename}) |\n"
|
|
140
|
+
if not index_path.exists():
|
|
141
|
+
index_path.write_text(header + entry, encoding="utf-8")
|
|
142
|
+
print(f"Created: {index_path}")
|
|
143
|
+
else:
|
|
144
|
+
content = index_path.read_text(encoding="utf-8")
|
|
145
|
+
index_path.write_text(content + entry, encoding="utf-8")
|
|
146
|
+
print(f"Updated: {index_path}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main() -> None:
|
|
150
|
+
parser = argparse.ArgumentParser(
|
|
151
|
+
description="Generate an ADR for data-intensive systems."
|
|
152
|
+
)
|
|
153
|
+
parser.add_argument(
|
|
154
|
+
"title",
|
|
155
|
+
nargs="?",
|
|
156
|
+
default="",
|
|
157
|
+
help="Decision title (will prompt if omitted)",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--output-dir",
|
|
161
|
+
default=".",
|
|
162
|
+
help="Directory to write ADR files (default: ./)",
|
|
163
|
+
)
|
|
164
|
+
args = parser.parse_args()
|
|
165
|
+
|
|
166
|
+
output_dir = pathlib.Path(args.output_dir).resolve()
|
|
167
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
168
|
+
|
|
169
|
+
title = args.title.strip() or prompt("Decision title")
|
|
170
|
+
if not title:
|
|
171
|
+
print("ERROR: A title is required.")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
print()
|
|
175
|
+
context = prompt("Context (why is this decision needed?)", default="Describe the situation and forces at play.")
|
|
176
|
+
options = collect_options()
|
|
177
|
+
chosen = prompt("Chosen option")
|
|
178
|
+
consequences = prompt("Consequences (trade-offs, risks, next steps)", default="To be determined.")
|
|
179
|
+
print()
|
|
180
|
+
print("-- Data-intensive sections --")
|
|
181
|
+
consistency_model = prompt("Consistency model", default="To be defined.")
|
|
182
|
+
failure_mode = prompt("Failure mode", default="To be defined.")
|
|
183
|
+
scalability_impact = prompt("Scalability impact", default="To be defined.")
|
|
184
|
+
operability = prompt("Operability", default="To be defined.")
|
|
185
|
+
|
|
186
|
+
number = next_adr_number(output_dir)
|
|
187
|
+
date = datetime.date.today().isoformat()
|
|
188
|
+
filename = f"adr-{number:03d}-{slugify(title)}.md"
|
|
189
|
+
adr_path = output_dir / filename
|
|
190
|
+
|
|
191
|
+
content = render_adr(
|
|
192
|
+
number=number,
|
|
193
|
+
title=title,
|
|
194
|
+
context=context,
|
|
195
|
+
options=options,
|
|
196
|
+
chosen=chosen,
|
|
197
|
+
consequences=consequences,
|
|
198
|
+
consistency_model=consistency_model,
|
|
199
|
+
failure_mode=failure_mode,
|
|
200
|
+
scalability_impact=scalability_impact,
|
|
201
|
+
operability=operability,
|
|
202
|
+
date=date,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
adr_path.write_text(content, encoding="utf-8")
|
|
206
|
+
print(f"\nWrote: {adr_path}")
|
|
207
|
+
|
|
208
|
+
append_to_index(output_dir / "ADR-INDEX.md", number, title, filename, date)
|
|
209
|
+
print("\nDone.")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"evals": [
|
|
3
|
+
{
|
|
4
|
+
"id": "eval-01-etl-no-error-handling-no-idempotency",
|
|
5
|
+
"prompt": "Review this ETL script:\n\n```python\nimport psycopg2\nimport requests\n\nSOURCE_DB = 'postgresql://user:pass@source-host/prod'\nDEST_DB = 'postgresql://user:pass@warehouse-host/warehouse'\n\ndef run():\n src = psycopg2.connect(SOURCE_DB)\n dst = psycopg2.connect(DEST_DB)\n\n rows = src.cursor().execute(\n 'SELECT id, customer_id, amount, created_at FROM orders'\n ).fetchall()\n\n for row in rows:\n order_id, customer_id, amount, created_at = row\n resp = requests.get(f'https://api.exchange.io/rate?currency=EUR')\n rate = resp.json()['rate']\n amount_eur = amount * rate\n\n dst.cursor().execute(\n 'INSERT INTO orders_eur VALUES (%s, %s, %s, %s)',\n (order_id, customer_id, amount_eur, created_at)\n )\n\n dst.commit()\n src.close()\n dst.close()\n\nif __name__ == '__main__':\n run()\n```",
|
|
6
|
+
"expectations": [
|
|
7
|
+
"Flags the full-table extraction `SELECT * FROM orders` with no timestamp filter as a non-incremental load that will re-process the entire table on every run; recommends incremental extraction using a watermark (Ch 3-4: incremental over full extraction)",
|
|
8
|
+
"Flags the absence of idempotency: re-running the script will insert duplicate rows into `orders_eur`; recommends an INSERT ... ON CONFLICT DO NOTHING or MERGE pattern (Ch 13: idempotency is non-negotiable)",
|
|
9
|
+
"Flags the external API call `requests.get` inside the per-row loop, which issues one HTTP request per order row — an N+1 pattern causing severe performance and rate-limit issues; recommends fetching the exchange rate once before the loop",
|
|
10
|
+
"Flags no error handling anywhere: if the API call fails, the loop crashes mid-run leaving the destination in a partially loaded state with no indication of progress (Ch 13: error handling and retry strategies)",
|
|
11
|
+
"Flags hardcoded credentials in source strings; recommends environment variables or a secrets manager (Ch 13: never hardcode credentials)",
|
|
12
|
+
"Flags no logging of rows processed, errors encountered, or run duration (Ch 12: monitoring and observability)",
|
|
13
|
+
"Flags the absence of a staging table: data is written directly to the production `orders_eur` table without validation (Ch 8: always load to staging first)"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "eval-02-mixed-transform-and-load",
|
|
18
|
+
"prompt": "Review this data pipeline script:\n\n```python\nimport pandas as pd\nimport sqlalchemy\n\ndef process_and_load(csv_path: str, db_url: str, table: str):\n df = pd.read_csv(csv_path)\n\n # Clean and transform\n df['email'] = df['email'].str.lower().str.strip()\n df['revenue'] = df['revenue'].fillna(0)\n df['signup_date'] = pd.to_datetime(df['signup_date'])\n df = df[df['revenue'] >= 0]\n df['revenue_category'] = df['revenue'].apply(\n lambda x: 'high' if x > 1000 else 'low'\n )\n df['country'] = df['country'].str.upper()\n\n # Enrich with another file\n regions = pd.read_csv('regions.csv') # hardcoded path\n df = df.merge(regions, on='country', how='left')\n\n # Load directly into the final table\n engine = sqlalchemy.create_engine(db_url)\n df.to_sql(table, engine, if_exists='append', index=False)\n print(f'Loaded {len(df)} rows')\n```",
|
|
19
|
+
"expectations": [
|
|
20
|
+
"Flags that transformation logic and loading logic are combined in a single function, violating separation of concerns; recommends splitting into separate extract, transform, and load functions (Ch 3: ETL pattern design, Ch 11: DAG-based task granularity)",
|
|
21
|
+
"Flags the hardcoded path `'regions.csv'` as a non-configurable dependency that breaks when the file moves; recommends externalizing all paths and inputs as parameters or config (Ch 13: configurable pipelines)",
|
|
22
|
+
"Flags `if_exists='append'` with no deduplication: re-running appends duplicate rows; recommends staging table + MERGE or using a unique constraint with INSERT OR IGNORE (Ch 13: idempotency)",
|
|
23
|
+
"Flags no data validation before loading: there is no check that the merge did not produce unexpected nulls in the region column or that row counts match expectations (Ch 10: validate at boundaries)",
|
|
24
|
+
"Flags no logging beyond a single print statement: recommends structured logging of row counts at each stage, null rates, and merge match rate (Ch 12: monitoring and observability)",
|
|
25
|
+
"Flags absence of data lineage tracking: no pipeline_run_id or audit column to identify which pipeline run produced each row, making debugging and reruns harder to trace (Ch 13: data lineage)",
|
|
26
|
+
"Recommends adding a schema validation step after reading the CSV to catch missing or mistyped columns before transformations run (Ch 10: schema validation at ingestion)"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "eval-03-clean-pipeline-with-retry-logging-separation",
|
|
31
|
+
"prompt": "Review this data pipeline implementation:\n\n```python\nimport logging\nimport time\nfrom datetime import datetime, timedelta\nfrom typing import Iterator\nimport psycopg2\nimport psycopg2.extras\n\nlogger = logging.getLogger(__name__)\n\nBATCH_SIZE = 1000\nMAX_RETRIES = 3\nBACKOFF_BASE = 2\n\n\ndef extract(conn, watermark: datetime) -> Iterator[list]:\n \"\"\"Yield batches of new orders since the watermark.\"\"\"\n with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:\n cur.execute(\n 'SELECT id, customer_id, amount, created_at '\n 'FROM orders WHERE created_at > %s ORDER BY created_at',\n (watermark,)\n )\n while True:\n rows = cur.fetchmany(BATCH_SIZE)\n if not rows:\n break\n logger.info('Extracted batch of %d rows', len(rows))\n yield [dict(r) for r in rows]\n\n\ndef transform(batch: list[dict]) -> list[dict]:\n \"\"\"Apply business rules: normalize amounts, tag high-value orders.\"\"\"\n result = []\n for row in batch:\n row['amount'] = round(float(row['amount']), 2)\n row['is_high_value'] = row['amount'] > 500\n result.append(row)\n return result\n\n\ndef load(conn, rows: list[dict], run_id: str) -> int:\n \"\"\"Upsert rows into orders_warehouse; return count of rows loaded.\"\"\"\n with conn.cursor() as cur:\n psycopg2.extras.execute_values(\n cur,\n '''\n INSERT INTO orders_warehouse (id, customer_id, amount, is_high_value, created_at, pipeline_run_id)\n VALUES %s\n ON CONFLICT (id) DO UPDATE SET\n amount = EXCLUDED.amount,\n is_high_value = EXCLUDED.is_high_value,\n pipeline_run_id = EXCLUDED.pipeline_run_id\n ''',\n [(r['id'], r['customer_id'], r['amount'], r['is_high_value'],\n r['created_at'], run_id) for r in rows]\n )\n conn.commit()\n return len(rows)\n\n\ndef run_with_retry(fn, *args, **kwargs):\n \"\"\"Retry a function with exponential backoff on transient errors.\"\"\"\n for attempt in range(1, MAX_RETRIES + 1):\n try:\n return fn(*args, **kwargs)\n except psycopg2.OperationalError as e:\n if attempt == MAX_RETRIES:\n raise\n delay = BACKOFF_BASE ** attempt\n logger.warning('Attempt %d failed: %s. Retrying in %ds', attempt, e, delay)\n time.sleep(delay)\n```",
|
|
32
|
+
"expectations": [
|
|
33
|
+
"Recognizes this is a well-designed pipeline and says so explicitly",
|
|
34
|
+
"Praises the clear separation of `extract`, `transform`, and `load` into distinct functions with single responsibilities (Ch 3: ETL pattern, Ch 11: task granularity)",
|
|
35
|
+
"Praises the watermark-based incremental extraction that avoids full-table scans on reruns (Ch 3-4: incremental extraction)",
|
|
36
|
+
"Praises the `ON CONFLICT DO UPDATE` upsert ensuring the pipeline is idempotent and safe to re-run (Ch 13: idempotency is non-negotiable)",
|
|
37
|
+
"Praises the generator-based `extract` function that yields batches, avoiding loading the full result set into memory (Ch 4: streaming extraction, memory efficiency)",
|
|
38
|
+
"Praises the `run_with_retry` wrapper with exponential backoff for transient database errors (Ch 13: error handling and retry strategies)",
|
|
39
|
+
"Praises structured logging at the batch level with row counts for observability (Ch 12: monitoring)",
|
|
40
|
+
"Praises the `pipeline_run_id` column in the load, enabling lineage tracking and debugging of which run produced which rows (Ch 13: data lineage)",
|
|
41
|
+
"Does NOT manufacture issues to appear thorough; any suggestions are framed as minor optional improvements"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|