@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.
- 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,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.
|
|
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",
|