@booklib/skills 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CONTRIBUTING.md +122 -0
  2. package/README.md +20 -2
  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/evals/evals.json +44 -0
  42. package/effective-python/examples/after.md +56 -0
  43. package/effective-python/examples/before.md +40 -0
  44. package/effective-python/references/api_reference.md +218 -0
  45. package/effective-python/references/practices-catalog.md +483 -0
  46. package/effective-python/references/review-checklist.md +190 -0
  47. package/effective-python/scripts/lint.py +173 -0
  48. package/kotlin-in-action/evals/evals.json +43 -0
  49. package/kotlin-in-action/examples/after.md +53 -0
  50. package/kotlin-in-action/examples/before.md +39 -0
  51. package/kotlin-in-action/scripts/setup_detekt.py +224 -0
  52. package/lean-startup/evals/evals.json +43 -0
  53. package/lean-startup/examples/after.md +80 -0
  54. package/lean-startup/examples/before.md +34 -0
  55. package/lean-startup/scripts/new_experiment.py +286 -0
  56. package/microservices-patterns/SKILL.md +140 -0
  57. package/microservices-patterns/evals/evals.json +45 -0
  58. package/microservices-patterns/examples/after.md +69 -0
  59. package/microservices-patterns/examples/before.md +40 -0
  60. package/microservices-patterns/scripts/new_service.py +583 -0
  61. package/package.json +2 -8
  62. package/refactoring-ui/evals/evals.json +45 -0
  63. package/refactoring-ui/examples/after.md +85 -0
  64. package/refactoring-ui/examples/before.md +58 -0
  65. package/refactoring-ui/scripts/audit_css.py +250 -0
  66. package/skill-router/SKILL.md +142 -0
  67. package/skill-router/evals/evals.json +38 -0
  68. package/skill-router/examples/after.md +63 -0
  69. package/skill-router/examples/before.md +39 -0
  70. package/skill-router/references/api_reference.md +24 -0
  71. package/skill-router/references/routing-heuristics.md +89 -0
  72. package/skill-router/references/skill-catalog.md +156 -0
  73. package/skill-router/scripts/route.py +266 -0
  74. package/storytelling-with-data/evals/evals.json +47 -0
  75. package/storytelling-with-data/examples/after.md +50 -0
  76. package/storytelling-with-data/examples/before.md +33 -0
  77. package/storytelling-with-data/scripts/chart_review.py +301 -0
  78. package/system-design-interview/evals/evals.json +45 -0
  79. package/system-design-interview/examples/after.md +94 -0
  80. package/system-design-interview/examples/before.md +27 -0
  81. package/system-design-interview/scripts/new_design.py +421 -0
  82. package/using-asyncio-python/evals/evals.json +43 -0
  83. package/using-asyncio-python/examples/after.md +68 -0
  84. package/using-asyncio-python/examples/before.md +39 -0
  85. package/using-asyncio-python/scripts/check_blocking.py +270 -0
  86. package/web-scraping-python/evals/evals.json +46 -0
  87. package/web-scraping-python/examples/after.md +109 -0
  88. package/web-scraping-python/examples/before.md +40 -0
  89. package/web-scraping-python/scripts/new_scraper.py +231 -0
  90. /package/{effective-python-skill → effective-python}/SKILL.md +0 -0
  91. /package/{effective-python-skill → effective-python}/ref-01-pythonic-thinking.md +0 -0
  92. /package/{effective-python-skill → effective-python}/ref-02-lists-and-dicts.md +0 -0
  93. /package/{effective-python-skill → effective-python}/ref-03-functions.md +0 -0
  94. /package/{effective-python-skill → effective-python}/ref-04-comprehensions-generators.md +0 -0
  95. /package/{effective-python-skill → effective-python}/ref-05-classes-interfaces.md +0 -0
  96. /package/{effective-python-skill → effective-python}/ref-06-metaclasses-attributes.md +0 -0
  97. /package/{effective-python-skill → effective-python}/ref-07-concurrency.md +0 -0
  98. /package/{effective-python-skill → effective-python}/ref-08-robustness-performance.md +0 -0
  99. /package/{effective-python-skill → effective-python}/ref-09-testing-debugging.md +0 -0
  100. /package/{effective-python-skill → effective-python}/ref-10-collaboration.md +0 -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,17 +1,11 @@
1
1
  {
2
2
  "name": "@booklib/skills",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
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"
7
7
  },
8
- "keywords": [
9
- "claude",
10
- "claude-code",
11
- "ai",
12
- "skills",
13
- "agent-skills"
14
- ],
8
+ "keywords": ["claude", "claude-code", "ai", "skills", "agent-skills"],
15
9
  "license": "MIT",
16
10
  "repository": {
17
11
  "type": "git",