@booklib/skills 1.0.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.
Files changed (100) hide show
  1. package/CONTRIBUTING.md +122 -0
  2. package/README.md +20 -1
  3. package/ROADMAP.md +36 -0
  4. package/animation-at-work/evals/evals.json +44 -0
  5. package/animation-at-work/examples/after.md +64 -0
  6. package/animation-at-work/examples/before.md +35 -0
  7. package/animation-at-work/scripts/audit_animations.py +295 -0
  8. package/bin/skills.js +552 -42
  9. package/clean-code-reviewer/SKILL.md +109 -1
  10. package/clean-code-reviewer/evals/evals.json +121 -3
  11. package/clean-code-reviewer/examples/after.md +48 -0
  12. package/clean-code-reviewer/examples/before.md +33 -0
  13. package/clean-code-reviewer/references/api_reference.md +158 -0
  14. package/clean-code-reviewer/references/practices-catalog.md +282 -0
  15. package/clean-code-reviewer/references/review-checklist.md +254 -0
  16. package/clean-code-reviewer/scripts/pre-review.py +206 -0
  17. package/data-intensive-patterns/evals/evals.json +43 -0
  18. package/data-intensive-patterns/examples/after.md +61 -0
  19. package/data-intensive-patterns/examples/before.md +38 -0
  20. package/data-intensive-patterns/scripts/adr.py +213 -0
  21. package/data-pipelines/evals/evals.json +45 -0
  22. package/data-pipelines/examples/after.md +97 -0
  23. package/data-pipelines/examples/before.md +37 -0
  24. package/data-pipelines/scripts/new_pipeline.py +444 -0
  25. package/design-patterns/evals/evals.json +46 -0
  26. package/design-patterns/examples/after.md +52 -0
  27. package/design-patterns/examples/before.md +29 -0
  28. package/design-patterns/scripts/scaffold.py +807 -0
  29. package/domain-driven-design/SKILL.md +120 -0
  30. package/domain-driven-design/evals/evals.json +48 -0
  31. package/domain-driven-design/examples/after.md +80 -0
  32. package/domain-driven-design/examples/before.md +43 -0
  33. package/domain-driven-design/scripts/scaffold.py +421 -0
  34. package/effective-java/evals/evals.json +46 -0
  35. package/effective-java/examples/after.md +83 -0
  36. package/effective-java/examples/before.md +37 -0
  37. package/effective-java/scripts/checkstyle_setup.py +211 -0
  38. package/effective-kotlin/evals/evals.json +45 -0
  39. package/effective-kotlin/examples/after.md +36 -0
  40. package/effective-kotlin/examples/before.md +38 -0
  41. package/effective-python/SKILL.md +199 -0
  42. package/effective-python/evals/evals.json +44 -0
  43. package/effective-python/examples/after.md +56 -0
  44. package/effective-python/examples/before.md +40 -0
  45. package/effective-python/ref-01-pythonic-thinking.md +202 -0
  46. package/effective-python/ref-02-lists-and-dicts.md +146 -0
  47. package/effective-python/ref-03-functions.md +186 -0
  48. package/effective-python/ref-04-comprehensions-generators.md +211 -0
  49. package/effective-python/ref-05-classes-interfaces.md +188 -0
  50. package/effective-python/ref-06-metaclasses-attributes.md +209 -0
  51. package/effective-python/ref-07-concurrency.md +213 -0
  52. package/effective-python/ref-08-robustness-performance.md +248 -0
  53. package/effective-python/ref-09-testing-debugging.md +253 -0
  54. package/effective-python/ref-10-collaboration.md +175 -0
  55. package/effective-python/references/api_reference.md +218 -0
  56. package/effective-python/references/practices-catalog.md +483 -0
  57. package/effective-python/references/review-checklist.md +190 -0
  58. package/effective-python/scripts/lint.py +173 -0
  59. package/kotlin-in-action/evals/evals.json +43 -0
  60. package/kotlin-in-action/examples/after.md +53 -0
  61. package/kotlin-in-action/examples/before.md +39 -0
  62. package/kotlin-in-action/scripts/setup_detekt.py +224 -0
  63. package/lean-startup/evals/evals.json +43 -0
  64. package/lean-startup/examples/after.md +80 -0
  65. package/lean-startup/examples/before.md +34 -0
  66. package/lean-startup/scripts/new_experiment.py +286 -0
  67. package/microservices-patterns/SKILL.md +140 -0
  68. package/microservices-patterns/evals/evals.json +45 -0
  69. package/microservices-patterns/examples/after.md +69 -0
  70. package/microservices-patterns/examples/before.md +40 -0
  71. package/microservices-patterns/scripts/new_service.py +583 -0
  72. package/package.json +1 -1
  73. package/refactoring-ui/evals/evals.json +45 -0
  74. package/refactoring-ui/examples/after.md +85 -0
  75. package/refactoring-ui/examples/before.md +58 -0
  76. package/refactoring-ui/scripts/audit_css.py +250 -0
  77. package/skill-router/SKILL.md +142 -0
  78. package/skill-router/evals/evals.json +38 -0
  79. package/skill-router/examples/after.md +63 -0
  80. package/skill-router/examples/before.md +39 -0
  81. package/skill-router/references/api_reference.md +24 -0
  82. package/skill-router/references/routing-heuristics.md +89 -0
  83. package/skill-router/references/skill-catalog.md +156 -0
  84. package/skill-router/scripts/route.py +266 -0
  85. package/storytelling-with-data/evals/evals.json +47 -0
  86. package/storytelling-with-data/examples/after.md +50 -0
  87. package/storytelling-with-data/examples/before.md +33 -0
  88. package/storytelling-with-data/scripts/chart_review.py +301 -0
  89. package/system-design-interview/evals/evals.json +45 -0
  90. package/system-design-interview/examples/after.md +94 -0
  91. package/system-design-interview/examples/before.md +27 -0
  92. package/system-design-interview/scripts/new_design.py +421 -0
  93. package/using-asyncio-python/evals/evals.json +43 -0
  94. package/using-asyncio-python/examples/after.md +68 -0
  95. package/using-asyncio-python/examples/before.md +39 -0
  96. package/using-asyncio-python/scripts/check_blocking.py +270 -0
  97. package/web-scraping-python/evals/evals.json +46 -0
  98. package/web-scraping-python/examples/after.md +109 -0
  99. package/web-scraping-python/examples/before.md +40 -0
  100. package/web-scraping-python/scripts/new_scraper.py +231 -0
@@ -0,0 +1,40 @@
1
+ # Before
2
+
3
+ An `InventoryService` that directly queries the `orders` database table owned by another service, creating tight coupling and a shared-database anti-pattern.
4
+
5
+ ```java
6
+ @RestController
7
+ @RequestMapping("/inventory")
8
+ public class InventoryController {
9
+
10
+ @Autowired
11
+ private DataSource sharedDataSource; // connected to the orders DB
12
+
13
+ @GetMapping("/reorder-candidates")
14
+ public List<ReorderItem> getReorderCandidates() {
15
+ List<ReorderItem> candidates = new ArrayList<>();
16
+
17
+ // Directly querying the Order Service's database table
18
+ try (Connection conn = sharedDataSource.getConnection();
19
+ PreparedStatement ps = conn.prepareStatement(
20
+ "SELECT product_id, SUM(quantity) as sold_qty " +
21
+ "FROM orders.order_lines " +
22
+ "WHERE created_at > NOW() - INTERVAL 7 DAY " +
23
+ "GROUP BY product_id")) {
24
+
25
+ ResultSet rs = ps.executeQuery();
26
+ while (rs.next()) {
27
+ String productId = rs.getString("product_id");
28
+ int soldQty = rs.getInt("sold_qty");
29
+ int stockLevel = getStockLevel(productId);
30
+ if (stockLevel < soldQty * 2) {
31
+ candidates.add(new ReorderItem(productId, soldQty * 3 - stockLevel));
32
+ }
33
+ }
34
+ } catch (SQLException e) {
35
+ throw new RuntimeException(e);
36
+ }
37
+ return candidates;
38
+ }
39
+ }
40
+ ```
@@ -0,0 +1,583 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Microservice Scaffold — generates a new microservice skeleton with proper boundaries.
4
+
5
+ Usage: python new_service.py <ServiceName> [--lang python|java|kotlin] [--output-dir ./]
6
+
7
+ Generates a service with:
8
+ - Its own domain model (no shared DB)
9
+ - Event publishing stub
10
+ - Health endpoint
11
+ - README with responsibility and run instructions
12
+ """
13
+
14
+ import argparse
15
+ import re
16
+ import sys
17
+ from pathlib import Path
18
+ from string import Template
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Name helpers
22
+ # ---------------------------------------------------------------------------
23
+
24
+ def to_snake(name: str) -> str:
25
+ """PascalCase -> snake_case, e.g. OrderService -> order_service"""
26
+ s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
27
+ s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s)
28
+ return s.lower()
29
+
30
+
31
+ def to_kebab(name: str) -> str:
32
+ return to_snake(name).replace("_", "-")
33
+
34
+
35
+ def strip_service(name: str) -> str:
36
+ """Remove trailing 'Service' suffix for entity naming."""
37
+ return re.sub(r"Service$", "", name, flags=re.IGNORECASE)
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Python templates
42
+ # ---------------------------------------------------------------------------
43
+
44
+ PY_MAIN = Template("""\
45
+ #!/usr/bin/env python3
46
+ \"\"\"Entry point for the $service_name microservice.\"\"\"
47
+ import uvicorn
48
+
49
+ if __name__ == "__main__":
50
+ uvicorn.run("app.api.routes:app", host="0.0.0.0", port=8000, reload=False)
51
+ """)
52
+
53
+ PY_ROUTES = Template("""\
54
+ \"\"\"FastAPI routes for $service_name.\"\"\"
55
+ from fastapi import FastAPI, HTTPException
56
+ from pydantic import BaseModel
57
+ from ..domain.${entity_snake} import ${Entity}, ${Entity}Id
58
+ from ..domain.${entity_snake}_repository import ${Entity}Repository
59
+ from ..infrastructure.in_memory_repository import InMemory${Entity}Repository
60
+ from ..infrastructure.event_publisher import EventPublisher
61
+
62
+ app = FastAPI(title="$service_name", version="0.1.0")
63
+
64
+ # In production replace with real DI / IOC.
65
+ _repo: ${Entity}Repository = InMemory${Entity}Repository()
66
+ _publisher = EventPublisher()
67
+
68
+
69
+ class Create${Entity}Request(BaseModel):
70
+ name: str
71
+
72
+
73
+ class ${Entity}Response(BaseModel):
74
+ id: str
75
+ name: str
76
+
77
+
78
+ @app.get("/healthz")
79
+ def health() -> dict:
80
+ return {"status": "ok", "service": "$service_name"}
81
+
82
+
83
+ @app.post("/${entity_kebab}s", response_model=${Entity}Response, status_code=201)
84
+ def create_${entity_snake}(req: Create${Entity}Request) -> ${Entity}Response:
85
+ entity = ${Entity}.create(name=req.name)
86
+ _repo.save(entity)
87
+ for event in entity.pull_events():
88
+ _publisher.publish(event)
89
+ return ${Entity}Response(id=str(entity.id), name=entity.name)
90
+
91
+
92
+ @app.get("/${entity_kebab}s/{id}", response_model=${Entity}Response)
93
+ def get_${entity_snake}(id: str) -> ${Entity}Response:
94
+ entity = _repo.find_by_id(${Entity}Id(id))
95
+ if entity is None:
96
+ raise HTTPException(status_code=404, detail="${Entity} not found")
97
+ return ${Entity}Response(id=str(entity.id), name=entity.name)
98
+ """)
99
+
100
+ PY_ENTITY = Template("""\
101
+ \"\"\"Domain entity for $Entity — $service_name aggregate root.\"\"\"
102
+ from __future__ import annotations
103
+ from dataclasses import dataclass, field
104
+ from typing import List
105
+ import uuid
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class ${Entity}Id:
110
+ value: str
111
+
112
+ def __post_init__(self) -> None:
113
+ if not self.value:
114
+ raise ValueError("${Entity}Id must not be blank.")
115
+
116
+ @classmethod
117
+ def generate(cls) -> "${Entity}Id":
118
+ return cls(str(uuid.uuid4()))
119
+
120
+ def __str__(self) -> str:
121
+ return self.value
122
+
123
+
124
+ @dataclass
125
+ class ${Entity}Event:
126
+ event_type: str
127
+ payload: dict
128
+
129
+
130
+ @dataclass
131
+ class ${Entity}:
132
+ id: ${Entity}Id
133
+ name: str
134
+ _events: List[${Entity}Event] = field(default_factory=list, init=False, repr=False)
135
+
136
+ @classmethod
137
+ def create(cls, name: str) -> "${Entity}":
138
+ if not name or not name.strip():
139
+ raise ValueError("name must not be blank.")
140
+ entity = cls(id=${Entity}Id.generate(), name=name)
141
+ entity._events.append(${Entity}Event("${Entity}Created", {"id": str(entity.id), "name": name}))
142
+ return entity
143
+
144
+ def pull_events(self) -> List[${Entity}Event]:
145
+ events, self._events = self._events, []
146
+ return events
147
+ """)
148
+
149
+ PY_REPOSITORY = Template("""\
150
+ \"\"\"Repository interface (port) for ${Entity}.\"\"\"
151
+ from abc import ABC, abstractmethod
152
+ from typing import Optional
153
+ from .${entity_snake} import ${Entity}, ${Entity}Id
154
+
155
+
156
+ class ${Entity}Repository(ABC):
157
+ @abstractmethod
158
+ def find_by_id(self, id: ${Entity}Id) -> Optional[${Entity}]: ...
159
+
160
+ @abstractmethod
161
+ def save(self, entity: ${Entity}) -> None: ...
162
+
163
+ @abstractmethod
164
+ def delete(self, id: ${Entity}Id) -> None: ...
165
+ """)
166
+
167
+ PY_IN_MEMORY_REPO = Template("""\
168
+ \"\"\"In-memory repository for development and testing.\"\"\"
169
+ from typing import Dict, Optional
170
+ from ..domain.${entity_snake} import ${Entity}, ${Entity}Id
171
+ from ..domain.${entity_snake}_repository import ${Entity}Repository
172
+
173
+
174
+ class InMemory${Entity}Repository(${Entity}Repository):
175
+ def __init__(self) -> None:
176
+ self._store: Dict[str, ${Entity}] = {}
177
+
178
+ def find_by_id(self, id: ${Entity}Id) -> Optional[${Entity}]:
179
+ return self._store.get(id.value)
180
+
181
+ def save(self, entity: ${Entity}) -> None:
182
+ self._store[entity.id.value] = entity
183
+
184
+ def delete(self, id: ${Entity}Id) -> None:
185
+ self._store.pop(id.value, None)
186
+ """)
187
+
188
+ PY_EVENT_PUBLISHER = Template("""\
189
+ \"\"\"Event publisher stub — replace with real broker integration (Kafka, SNS, etc.).\"\"\"
190
+ import json
191
+ import logging
192
+ from typing import Any
193
+
194
+ logger = logging.getLogger(__name__)
195
+
196
+
197
+ class EventPublisher:
198
+ \"\"\"Publishes domain events to a message broker.
199
+
200
+ In production, swap this stub with a Kafka producer, AWS SNS client,
201
+ RabbitMQ publisher, or similar. The interface intentionally stays simple.
202
+ \"\"\"
203
+
204
+ def publish(self, event: Any) -> None:
205
+ payload = {
206
+ "event_type": getattr(event, "event_type", type(event).__name__),
207
+ "payload": getattr(event, "payload", {}),
208
+ }
209
+ logger.info("EVENT PUBLISHED: %s", json.dumps(payload))
210
+ # TODO: replace with real broker call, e.g.:
211
+ # self._producer.produce(topic="$service_kebab-events", value=json.dumps(payload))
212
+ """)
213
+
214
+ PY_REQUIREMENTS = Template("""\
215
+ fastapi>=0.110.0
216
+ uvicorn[standard]>=0.29.0
217
+ pydantic>=2.0.0
218
+ """)
219
+
220
+ PY_DOCKERFILE = Template("""\
221
+ FROM python:3.12-slim
222
+
223
+ WORKDIR /app
224
+
225
+ COPY requirements.txt .
226
+ RUN pip install --no-cache-dir -r requirements.txt
227
+
228
+ COPY . .
229
+
230
+ EXPOSE 8000
231
+
232
+ CMD ["python", "main.py"]
233
+ """)
234
+
235
+ PY_INIT = """\
236
+ """
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # Java templates (single-file for brevity; split in real project)
240
+ # ---------------------------------------------------------------------------
241
+
242
+ JAVA_ENTITY = Template("""\
243
+ package com.example.${entity_lower};
244
+
245
+ import java.util.ArrayList;
246
+ import java.util.List;
247
+ import java.util.UUID;
248
+
249
+ public final class ${Entity} {
250
+ private final ${Entity}Id id;
251
+ private String name;
252
+ private final List<${Entity}Event> events = new ArrayList<>();
253
+
254
+ private ${Entity}(${Entity}Id id, String name) {
255
+ this.id = id;
256
+ this.name = name;
257
+ }
258
+
259
+ public static ${Entity} create(String name) {
260
+ if (name == null || name.isBlank()) throw new IllegalArgumentException("name must not be blank");
261
+ var e = new ${Entity}(${Entity}Id.generate(), name);
262
+ e.events.add(new ${Entity}Event("${Entity}Created", java.util.Map.of("id", e.id.value(), "name", name)));
263
+ return e;
264
+ }
265
+
266
+ public ${Entity}Id getId() { return id; }
267
+ public String getName() { return name; }
268
+
269
+ public List<${Entity}Event> pullEvents() {
270
+ var copy = List.copyOf(events);
271
+ events.clear();
272
+ return copy;
273
+ }
274
+ }
275
+ """)
276
+
277
+ JAVA_ID = Template("""\
278
+ package com.example.${entity_lower};
279
+ import java.util.UUID;
280
+
281
+ public record ${Entity}Id(String value) {
282
+ public ${Entity}Id { if (value == null || value.isBlank()) throw new IllegalArgumentException("blank id"); }
283
+ public static ${Entity}Id generate() { return new ${Entity}Id(UUID.randomUUID().toString()); }
284
+ public static ${Entity}Id of(String s) { return new ${Entity}Id(s); }
285
+ @Override public String toString() { return value; }
286
+ }
287
+ """)
288
+
289
+ JAVA_EVENT = Template("""\
290
+ package com.example.${entity_lower};
291
+ import java.util.Map;
292
+
293
+ public record ${Entity}Event(String eventType, Map<String, Object> payload) {}
294
+ """)
295
+
296
+ JAVA_REPOSITORY = Template("""\
297
+ package com.example.${entity_lower};
298
+ import java.util.Optional;
299
+
300
+ public interface ${Entity}Repository {
301
+ Optional<${Entity}> findById(${Entity}Id id);
302
+ void save(${Entity} entity);
303
+ void delete(${Entity}Id id);
304
+ }
305
+ """)
306
+
307
+ JAVA_PUBLISHER = Template("""\
308
+ package com.example.${entity_lower};
309
+
310
+ public class EventPublisher {
311
+ /** Replace with Kafka / SNS / RabbitMQ integration in production. */
312
+ public void publish(${Entity}Event event) {
313
+ System.out.printf("[EVENT] type=%s payload=%s%n", event.eventType(), event.payload());
314
+ }
315
+ }
316
+ """)
317
+
318
+ JAVA_MAIN = Template("""\
319
+ package com.example.${entity_lower};
320
+
321
+ import java.util.HashMap;
322
+ import java.util.Map;
323
+ import java.util.Optional;
324
+
325
+ /**
326
+ * Minimal HTTP entry point — wire up Spring Boot / Micronaut / Quarkus in production.
327
+ * This stub demonstrates domain wiring only.
328
+ */
329
+ public class Main {
330
+
331
+ // In-memory repo for the stub
332
+ static Map<String, ${Entity}> store = new HashMap<>();
333
+ static ${Entity}Repository repo = new ${Entity}Repository() {
334
+ public Optional<${Entity}> findById(${Entity}Id id) { return Optional.ofNullable(store.get(id.value())); }
335
+ public void save(${Entity} e) { store.put(e.getId().value(), e); }
336
+ public void delete(${Entity}Id id) { store.remove(id.value()); }
337
+ };
338
+ static EventPublisher publisher = new EventPublisher();
339
+
340
+ public static void main(String[] args) {
341
+ var entity = ${Entity}.create("Example");
342
+ repo.save(entity);
343
+ entity.pullEvents().forEach(publisher::publish);
344
+ System.out.println("Created: " + entity.getId());
345
+
346
+ var found = repo.findById(entity.getId());
347
+ found.ifPresent(e -> System.out.println("Found: " + e.getName()));
348
+ }
349
+ }
350
+ """)
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # Kotlin templates
354
+ # ---------------------------------------------------------------------------
355
+
356
+ KT_ENTITY = Template("""\
357
+ package com.example.${entity_lower}
358
+
359
+ import java.util.UUID
360
+
361
+ data class ${Entity}Id(val value: String) {
362
+ init { require(value.isNotBlank()) { "blank id" } }
363
+ companion object {
364
+ fun generate() = ${Entity}Id(UUID.randomUUID().toString())
365
+ fun of(s: String) = ${Entity}Id(s)
366
+ }
367
+ override fun toString() = value
368
+ }
369
+
370
+ data class ${Entity}Event(val eventType: String, val payload: Map<String, Any>)
371
+
372
+ class ${Entity} private constructor(val id: ${Entity}Id, val name: String) {
373
+ private val _events = mutableListOf<${Entity}Event>()
374
+
375
+ companion object {
376
+ fun create(name: String): ${Entity} {
377
+ require(name.isNotBlank()) { "name must not be blank" }
378
+ val e = ${Entity}(${Entity}Id.generate(), name)
379
+ e._events.add(${Entity}Event("${Entity}Created", mapOf("id" to e.id.value, "name" to name)))
380
+ return e
381
+ }
382
+ }
383
+
384
+ fun pullEvents(): List<${Entity}Event> {
385
+ val copy = _events.toList(); _events.clear(); return copy
386
+ }
387
+ }
388
+ """)
389
+
390
+ KT_REPOSITORY = Template("""\
391
+ package com.example.${entity_lower}
392
+
393
+ interface ${Entity}Repository {
394
+ fun findById(id: ${Entity}Id): ${Entity}?
395
+ fun save(entity: ${Entity})
396
+ fun delete(id: ${Entity}Id)
397
+ }
398
+
399
+ class InMemory${Entity}Repository : ${Entity}Repository {
400
+ private val store = mutableMapOf<String, ${Entity}>()
401
+ override fun findById(id: ${Entity}Id) = store[id.value]
402
+ override fun save(entity: ${Entity}) { store[entity.id.value] = entity }
403
+ override fun delete(id: ${Entity}Id) { store.remove(id.value) }
404
+ }
405
+ """)
406
+
407
+ KT_PUBLISHER = Template("""\
408
+ package com.example.${entity_lower}
409
+
410
+ class EventPublisher {
411
+ /** Replace with Kafka / SNS / RabbitMQ integration in production. */
412
+ fun publish(event: ${Entity}Event) {
413
+ println("[EVENT] type=${dollar}{event.eventType} payload=${dollar}{event.payload}")
414
+ }
415
+ }
416
+ """)
417
+
418
+ KT_MAIN = Template("""\
419
+ package com.example.${entity_lower}
420
+
421
+ fun main() {
422
+ val repo: ${Entity}Repository = InMemory${Entity}Repository()
423
+ val publisher = EventPublisher()
424
+
425
+ val entity = ${Entity}.create("Example")
426
+ repo.save(entity)
427
+ entity.pullEvents().forEach { publisher.publish(it) }
428
+
429
+ println("Created: ${dollar}{entity.id}")
430
+ println("Found: ${dollar}{repo.findById(entity.id)?.name}")
431
+ }
432
+ """)
433
+
434
+ README = Template("""\
435
+ # $service_name
436
+
437
+ ## Responsibility
438
+
439
+ > **$service_name** is responsible for managing the lifecycle of `$Entity` resources.
440
+ > It owns its own data store and publishes domain events when state changes occur.
441
+
442
+ This service follows the microservices pattern of **one service, one bounded context**.
443
+ It does **not** share a database with any other service.
444
+
445
+ ## Structure
446
+
447
+ ```
448
+ $service_dir/
449
+ ├── app/
450
+ │ ├── api/ # HTTP layer (routes, request/response models)
451
+ │ ├── domain/ # Entities, value objects, repository interfaces
452
+ │ └── infrastructure/ # Concrete adapters (DB, cache, event broker)
453
+ ├── main.py # Entry point
454
+ ├── requirements.txt
455
+ ├── Dockerfile
456
+ └── README.md
457
+ ```
458
+
459
+ ## Running Locally
460
+
461
+ ```bash
462
+ pip install -r requirements.txt
463
+ python main.py
464
+ # or:
465
+ uvicorn app.api.routes:app --reload
466
+ ```
467
+
468
+ Open http://localhost:8000/docs for the interactive API documentation.
469
+
470
+ ## Events Published
471
+
472
+ | Event | Trigger | Payload |
473
+ |-------|---------|---------|
474
+ | `${Entity}Created` | POST /${entity_kebab}s | `{id, name}` |
475
+
476
+ ## Configuration
477
+
478
+ | Variable | Default | Description |
479
+ |----------|---------|-------------|
480
+ | `PORT` | `8000` | HTTP listen port |
481
+ | `DATABASE_URL` | in-memory | Connection string for the DB adapter |
482
+ | `BROKER_URL` | stdout | Event broker endpoint |
483
+
484
+ ## Design Decisions
485
+
486
+ - **No shared database**: Other services must call this service's API or consume its events.
487
+ - **Event publishing**: Every state change emits a domain event for downstream consumers.
488
+ - **Repository pattern**: The domain layer depends on an interface; the infrastructure layer provides the adapter.
489
+ """)
490
+
491
+
492
+ # ---------------------------------------------------------------------------
493
+ # Writer
494
+ # ---------------------------------------------------------------------------
495
+
496
+ def write(path: Path, content: str) -> None:
497
+ path.parent.mkdir(parents=True, exist_ok=True)
498
+ path.write_text(content)
499
+ print(f" Created: {path}")
500
+
501
+
502
+ def scaffold_python(service_dir: Path, ctx: dict) -> None:
503
+ base = service_dir
504
+ write(base / "main.py", PY_MAIN.substitute(ctx))
505
+ write(base / "requirements.txt", PY_REQUIREMENTS.substitute(ctx))
506
+ write(base / "Dockerfile", PY_DOCKERFILE.substitute(ctx))
507
+ write(base / "app" / "__init__.py", PY_INIT)
508
+ write(base / "app" / "api" / "__init__.py", PY_INIT)
509
+ write(base / "app" / "api" / "routes.py", PY_ROUTES.substitute(ctx))
510
+ write(base / "app" / "domain" / "__init__.py", PY_INIT)
511
+ write(base / "app" / "domain" / f"{ctx['entity_snake']}.py", PY_ENTITY.substitute(ctx))
512
+ write(base / "app" / "domain" / f"{ctx['entity_snake']}_repository.py", PY_REPOSITORY.substitute(ctx))
513
+ write(base / "app" / "infrastructure" / "__init__.py", PY_INIT)
514
+ write(base / "app" / "infrastructure" / "in_memory_repository.py", PY_IN_MEMORY_REPO.substitute(ctx))
515
+ write(base / "app" / "infrastructure" / "event_publisher.py", PY_EVENT_PUBLISHER.substitute(ctx))
516
+
517
+
518
+ def scaffold_java(service_dir: Path, ctx: dict) -> None:
519
+ pkg = service_dir / "src" / "main" / "java" / "com" / "example" / ctx["entity_lower"]
520
+ write(pkg / f"{ctx['Entity']}.java", JAVA_ENTITY.substitute(ctx))
521
+ write(pkg / f"{ctx['Entity']}Id.java", JAVA_ID.substitute(ctx))
522
+ write(pkg / f"{ctx['Entity']}Event.java", JAVA_EVENT.substitute(ctx))
523
+ write(pkg / f"{ctx['Entity']}Repository.java", JAVA_REPOSITORY.substitute(ctx))
524
+ write(pkg / "EventPublisher.java", JAVA_PUBLISHER.substitute(ctx))
525
+ write(pkg / "Main.java", JAVA_MAIN.substitute(ctx))
526
+
527
+
528
+ def scaffold_kotlin(service_dir: Path, ctx: dict) -> None:
529
+ pkg = service_dir / "src" / "main" / "kotlin" / "com" / "example" / ctx["entity_lower"]
530
+ write(pkg / f"{ctx['Entity']}.kt", KT_ENTITY.substitute(ctx))
531
+ write(pkg / f"{ctx['Entity']}Repository.kt", KT_REPOSITORY.substitute(ctx))
532
+ write(pkg / "EventPublisher.kt", KT_PUBLISHER.substitute(ctx))
533
+ write(pkg / "Main.kt", KT_MAIN.substitute(ctx))
534
+
535
+
536
+ SCAFFOLDERS = {"python": scaffold_python, "java": scaffold_java, "kotlin": scaffold_kotlin}
537
+
538
+
539
+ def main() -> None:
540
+ parser = argparse.ArgumentParser(description="Scaffold a microservice skeleton.")
541
+ parser.add_argument("service_name", metavar="ServiceName",
542
+ help="Service name in PascalCase, e.g. OrderService")
543
+ parser.add_argument("--lang", choices=["python", "java", "kotlin"], default="python")
544
+ parser.add_argument("--output-dir", default=".", type=Path)
545
+ args = parser.parse_args()
546
+
547
+ name = args.service_name
548
+ entity = strip_service(name)
549
+ service_dir = args.output_dir / to_kebab(name)
550
+
551
+ ctx = {
552
+ "service_name": name,
553
+ "service_kebab": to_kebab(name),
554
+ "service_dir": to_kebab(name),
555
+ "Entity": entity,
556
+ "entity_snake": to_snake(entity),
557
+ "entity_lower": entity.lower(),
558
+ "entity_kebab": to_kebab(entity),
559
+ "dollar": "$",
560
+ }
561
+
562
+ print(f"\nScaffolding microservice '{name}' ({args.lang}) in {service_dir}/\n")
563
+ SCAFFOLDERS[args.lang](service_dir, ctx)
564
+ write(service_dir / "README.md", README.substitute(ctx))
565
+
566
+ print(f"\nDone. Next steps:")
567
+ if args.lang == "python":
568
+ print(f" cd {service_dir}")
569
+ print(f" pip install -r requirements.txt")
570
+ print(f" python main.py")
571
+ print(f" # API docs: http://localhost:8000/docs")
572
+ elif args.lang == "java":
573
+ print(f" cd {service_dir}")
574
+ print(f" # Add to a Maven/Gradle project, then: mvn compile exec:java -Dexec.mainClass=com.example.{entity.lower()}.Main")
575
+ elif args.lang == "kotlin":
576
+ print(f" cd {service_dir}")
577
+ print(f" # Add to a Gradle project, then: ./gradlew run")
578
+ print(f"\n Replace InMemory{entity}Repository with a real DB adapter.")
579
+ print(f" Replace EventPublisher stub with a Kafka/SNS producer.\n")
580
+
581
+
582
+ if __name__ == "__main__":
583
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@booklib/skills",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "description": "Book knowledge distilled into structured AI skills for Claude Code and other AI assistants",
5
5
  "bin": {
6
6
  "skills": "./bin/skills.js"
@@ -0,0 +1,45 @@
1
+ {
2
+ "evals": [
3
+ {
4
+ "id": "eval-01-poor-visual-hierarchy",
5
+ "prompt": "Review this CSS for a product card component:\n\n```css\n.product-card {\n padding: 20px;\n border: 1px solid #cccccc;\n border-radius: 4px;\n background: white;\n}\n\n.product-card .category-label {\n font-size: 16px;\n font-weight: 400;\n color: #333333;\n margin-bottom: 8px;\n}\n\n.product-card .product-name {\n font-size: 18px;\n font-weight: 400;\n color: #333333;\n margin-bottom: 8px;\n}\n\n.product-card .product-price {\n font-size: 16px;\n font-weight: 400;\n color: #333333;\n margin-bottom: 8px;\n}\n\n.product-card .product-description {\n font-size: 15px;\n font-weight: 400;\n color: #333333;\n margin-bottom: 12px;\n}\n\n.product-card .stock-status {\n font-size: 14px;\n font-weight: 400;\n color: #333333;\n}\n\n.product-card .add-to-cart-btn {\n font-size: 16px;\n font-weight: 400;\n background: #333333;\n color: white;\n padding: 10px 20px;\n border: none;\n border-radius: 4px;\n margin-top: 12px;\n}\n```",
6
+ "expectations": [
7
+ "Identifies the core problem: visual hierarchy is flat — nearly every element uses the same font-size (15-18px) and font-weight (400), making it impossible to identify what matters most at a glance",
8
+ "Flags that the product name should be the most visually prominent element — it should use a larger size (24-30px) and heavier weight (600-700) to create a clear entry point",
9
+ "Flags that the price is the second most important element — it should be visually distinct, not the same weight as the description",
10
+ "Points out that category-label should be de-emphasized (smaller, lighter color like hsl(0,0%,55%)) — it is supporting context, not primary information",
11
+ "Notes that relying on font-size alone for hierarchy is addressed in Chapter 2 of Refactoring UI: use weight and color as primary levers, size as secondary",
12
+ "Flags that all elements use the same color (#333333) — tertiary elements like category-label and stock-status should use a lighter grey to create visual separation",
13
+ "Notes the add-to-cart button has font-weight: 400 — a primary action button should use font-weight 500-600 for emphasis",
14
+ "Recommends a concrete hierarchy: product-name (24px, 700 weight, dark), price (20px, 600 weight, dark), description (15px, 400 weight, medium grey #6b7280), category-label (12px, 500 weight, uppercase, light grey)"
15
+ ]
16
+ },
17
+ {
18
+ "id": "eval-02-too-many-colors-no-spacing-system",
19
+ "prompt": "Review this CSS for a dashboard sidebar:\n\n```css\n.sidebar {\n background: #1a237e;\n padding: 15px;\n width: 240px;\n}\n\n.sidebar-header {\n color: #ffffff;\n font-size: 22px;\n margin-bottom: 7px;\n padding-bottom: 11px;\n border-bottom: 2px solid #3949ab;\n}\n\n.nav-item {\n color: #b0bec5;\n font-size: 14px;\n padding: 9px 13px;\n margin-bottom: 3px;\n border-radius: 6px;\n}\n\n.nav-item.active {\n background: #ff6f00;\n color: #ffffff;\n}\n\n.nav-item:hover {\n background: #283593;\n color: #e8eaf6;\n}\n\n.nav-section-label {\n color: #ffd54f;\n font-size: 11px;\n text-transform: uppercase;\n margin-top: 18px;\n margin-bottom: 6px;\n letter-spacing: 1px;\n}\n\n.badge {\n background: #e53935;\n color: white;\n font-size: 10px;\n padding: 2px 7px;\n border-radius: 10px;\n}\n\n.sidebar-footer {\n margin-top: 23px;\n padding-top: 14px;\n border-top: 1px solid #303f9f;\n color: #90a4ae;\n font-size: 13px;\n}\n```",
20
+ "expectations": [
21
+ "Identifies the color proliferation problem: #1a237e, #3949ab, #ff6f00, #b0bec5, #283593, #e8eaf6, #ffd54f, #e53935, #303f9f, #90a4ae — 10+ ad-hoc hex colors with no systematic relationship",
22
+ "Flags that the active state uses #ff6f00 (deep orange) which is unrelated to the #1a237e (indigo) background — the accent color should be a lighter tint of the primary palette or a purposeful brand color",
23
+ "Flags that nav-section-label uses #ffd54f (amber/yellow) as a color — this creates a third unrelated hue in the sidebar; section labels should use a desaturated light color consistent with the palette",
24
+ "Flags the arbitrary spacing values: 7px, 11px, 9px, 13px, 3px, 18px, 6px, 23px, 14px — none of these follow a spacing scale; Refactoring UI Ch 3 prescribes a constrained scale (4, 8, 12, 16, 24, 32px)",
25
+ "Notes that grey-on-deep-blue text (#b0bec5 on #1a237e) needs a contrast check — and recommends using a color that is hue-matched to the background rather than a neutral grey",
26
+ "Recommends defining a design token set: 2-3 shades of indigo for the sidebar tones, one accent color for the active state, one semantic red for the badge — not 10 unrelated hex values",
27
+ "Notes that the badge using an unrelated red (#e53935) is fine for semantic meaning but should be defined as a token (--color-danger) rather than a hard-coded hex"
28
+ ]
29
+ },
30
+ {
31
+ "id": "eval-03-clean-hierarchy-design",
32
+ "prompt": "Review this CSS for a pricing card component:\n\n```css\n:root {\n --space-1: 4px;\n --space-2: 8px;\n --space-3: 12px;\n --space-4: 16px;\n --space-6: 24px;\n --space-8: 32px;\n --space-12: 48px;\n\n --text-sm: 13px;\n --text-base: 16px;\n --text-lg: 18px;\n --text-xl: 20px;\n --text-3xl: 30px;\n\n --color-text-primary: hsl(222, 47%, 11%);\n --color-text-secondary: hsl(215, 20%, 40%);\n --color-text-tertiary: hsl(215, 16%, 60%);\n --color-accent: hsl(245, 75%, 60%);\n --color-accent-light: hsl(245, 75%, 96%);\n}\n\n.pricing-card {\n background: white;\n border-radius: 12px;\n padding: var(--space-8);\n box-shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);\n max-width: 320px;\n}\n\n.plan-name {\n font-size: var(--text-sm);\n font-weight: 600;\n color: var(--color-accent);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n margin-bottom: var(--space-3);\n}\n\n.plan-price {\n font-size: var(--text-3xl);\n font-weight: 700;\n color: var(--color-text-primary);\n margin-bottom: var(--space-1);\n}\n\n.price-period {\n font-size: var(--text-sm);\n color: var(--color-text-tertiary);\n margin-bottom: var(--space-6);\n}\n\n.feature-list {\n list-style: none;\n padding: 0;\n margin: 0 0 var(--space-8) 0;\n}\n\n.feature-item {\n font-size: var(--text-base);\n color: var(--color-text-secondary);\n padding: var(--space-2) 0;\n display: flex;\n align-items: center;\n gap: var(--space-3);\n}\n\n.cta-button {\n display: block;\n width: 100%;\n padding: var(--space-3) var(--space-6);\n background: var(--color-accent);\n color: white;\n font-size: var(--text-base);\n font-weight: 600;\n border: none;\n border-radius: 8px;\n cursor: pointer;\n text-align: center;\n}\n```",
33
+ "expectations": [
34
+ "Recognizes this as a well-designed component with proper design system foundations and says so explicitly",
35
+ "Praises the CSS custom property (design token) system: spacing scale (--space-1 through --space-12 in steps of 4px), type scale (--text-sm through --text-3xl), and color tokens all follow Refactoring UI principles",
36
+ "Praises the three-level color hierarchy for text: --color-text-primary (dark), --color-text-secondary (medium), --color-text-tertiary (light) — directly implementing the Refactoring UI color-as-hierarchy approach",
37
+ "Praises the visual hierarchy of the price card: plan name is small+uppercase+accent (supporting), price is 30px+700 weight (primary), period is small+tertiary (de-emphasized) — excellent three-tier hierarchy",
38
+ "Praises the shadow: uses a two-layer shadow (diffuse + tight) following the Refactoring UI recommendation for realistic depth",
39
+ "Praises that plan-name uses color (--color-accent) to create emphasis at a small size rather than making it large — using color+weight as levers, not font-size alone",
40
+ "Does NOT manufacture fake issues just to have something to say",
41
+ "May offer optional improvements (hover state for cta-button, dark mode token variants, focus ring for accessibility) but clearly frames them as enhancements"
42
+ ]
43
+ }
44
+ ]
45
+ }