@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.
- package/CONTRIBUTING.md +122 -0
- package/README.md +20 -1
- 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/SKILL.md +199 -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/ref-01-pythonic-thinking.md +202 -0
- package/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/effective-python/ref-03-functions.md +186 -0
- package/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/effective-python/ref-05-classes-interfaces.md +188 -0
- package/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/effective-python/ref-07-concurrency.md +213 -0
- package/effective-python/ref-08-robustness-performance.md +248 -0
- package/effective-python/ref-09-testing-debugging.md +253 -0
- package/effective-python/ref-10-collaboration.md +175 -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 +1 -1
- 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
|
@@ -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
|
@@ -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
|
+
}
|