@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,421 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DDD Scaffold — generates aggregate building blocks for a bounded context.
4
+ Usage: python scaffold.py <AggregateName> [--lang python|kotlin|java] [--output-dir ./]
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+ from string import Template
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Templates
14
+ # ---------------------------------------------------------------------------
15
+
16
+ PYTHON_AGGREGATE = Template('''\
17
+ from __future__ import annotations
18
+ from dataclasses import dataclass, field
19
+ from typing import List
20
+ from .${name}Id import ${name}Id
21
+ from .${name}Events import ${name}Created, ${name}Event
22
+
23
+
24
+ @dataclass
25
+ class ${name}:
26
+ """Aggregate root for ${name}."""
27
+
28
+ id: ${name}Id
29
+ _events: List[${name}Event] = field(default_factory=list, init=False, repr=False)
30
+
31
+ # ------------------------------------------------------------------
32
+ # Factory method — the only sanctioned way to create a ${name}.
33
+ # ------------------------------------------------------------------
34
+ @classmethod
35
+ def create(cls, id: ${name}Id, **kwargs) -> "${name}":
36
+ instance = cls(id=id)
37
+ instance._check_invariants()
38
+ instance._record(${name}Created(aggregate_id=id))
39
+ return instance
40
+
41
+ # ------------------------------------------------------------------
42
+ # Commands — each mutates state and records an event.
43
+ # ------------------------------------------------------------------
44
+
45
+ # def rename(self, new_name: str) -> None:
46
+ # if not new_name.strip():
47
+ # raise ValueError("Name must not be blank.")
48
+ # self.name = new_name
49
+ # self._record(${name}Renamed(aggregate_id=self.id, name=new_name))
50
+
51
+ # ------------------------------------------------------------------
52
+ # Domain events
53
+ # ------------------------------------------------------------------
54
+ def pull_events(self) -> List[${name}Event]:
55
+ """Return and clear pending domain events."""
56
+ events, self._events = self._events, []
57
+ return events
58
+
59
+ def _record(self, event: ${name}Event) -> None:
60
+ self._events.append(event)
61
+
62
+ # ------------------------------------------------------------------
63
+ # Invariants
64
+ # ------------------------------------------------------------------
65
+ def _check_invariants(self) -> None:
66
+ """Raise if the aggregate is in an invalid state."""
67
+ if self.id is None:
68
+ raise ValueError("${name} must have an ID.")
69
+ ''')
70
+
71
+ PYTHON_ID = Template('''\
72
+ from __future__ import annotations
73
+ from dataclasses import dataclass
74
+ import uuid
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class ${name}Id:
79
+ """Value Object — identity of a ${name} aggregate."""
80
+
81
+ value: str
82
+
83
+ def __post_init__(self) -> None:
84
+ if not self.value or not self.value.strip():
85
+ raise ValueError("${name}Id must not be blank.")
86
+
87
+ @classmethod
88
+ def generate(cls) -> "${name}Id":
89
+ return cls(value=str(uuid.uuid4()))
90
+
91
+ @classmethod
92
+ def from_string(cls, raw: str) -> "${name}Id":
93
+ return cls(value=raw)
94
+
95
+ def __str__(self) -> str:
96
+ return self.value
97
+ ''')
98
+
99
+ PYTHON_REPOSITORY = Template('''\
100
+ from __future__ import annotations
101
+ from abc import ABC, abstractmethod
102
+ from typing import Optional
103
+ from .${name}Id import ${name}Id
104
+ from .${name} import ${name}
105
+
106
+
107
+ class ${name}Repository(ABC):
108
+ """Port (interface) for ${name} persistence.
109
+
110
+ Concrete adapters live in the infrastructure layer.
111
+ """
112
+
113
+ @abstractmethod
114
+ def find_by_id(self, id: ${name}Id) -> Optional[${name}]:
115
+ """Return the aggregate or None if not found."""
116
+
117
+ @abstractmethod
118
+ def save(self, aggregate: ${name}) -> None:
119
+ """Persist all state changes."""
120
+
121
+ @abstractmethod
122
+ def delete(self, id: ${name}Id) -> None:
123
+ """Remove the aggregate."""
124
+
125
+ @abstractmethod
126
+ def exists(self, id: ${name}Id) -> bool:
127
+ """Check existence without loading the aggregate."""
128
+ ''')
129
+
130
+ PYTHON_EVENTS = Template('''\
131
+ from __future__ import annotations
132
+ from dataclasses import dataclass, field
133
+ from datetime import datetime, timezone
134
+ from typing import ClassVar
135
+ from .${name}Id import ${name}Id
136
+
137
+
138
+ @dataclass(frozen=True)
139
+ class ${name}Event:
140
+ """Base class for all ${name} domain events."""
141
+
142
+ event_type: ClassVar[str]
143
+ aggregate_id: ${name}Id
144
+ occurred_at: datetime = field(
145
+ default_factory=lambda: datetime.now(tz=timezone.utc)
146
+ )
147
+
148
+
149
+ @dataclass(frozen=True)
150
+ class ${name}Created(${name}Event):
151
+ """Raised when a new ${name} is created."""
152
+
153
+ event_type: ClassVar[str] = "${name}Created"
154
+
155
+
156
+ # Add more events below as your domain grows, for example:
157
+ # @dataclass(frozen=True)
158
+ # class ${name}Renamed(${name}Event):
159
+ # event_type: ClassVar[str] = "${name}Renamed"
160
+ # name: str = ""
161
+ ''')
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # Kotlin templates
165
+ # ---------------------------------------------------------------------------
166
+
167
+ KOTLIN_AGGREGATE = Template('''\
168
+ package com.example.${lname}
169
+
170
+ import java.time.Instant
171
+
172
+ class ${name}(val id: ${name}Id) {
173
+
174
+ private val _events: MutableList<${name}Event> = mutableListOf()
175
+ val events: List<${name}Event> get() = _events.toList()
176
+
177
+ companion object {
178
+ /** Factory — the only way to create a valid ${name}. */
179
+ fun create(id: ${name}Id): ${name} {
180
+ val agg = ${name}(id)
181
+ agg.checkInvariants()
182
+ agg.record(${name}Created(aggregateId = id))
183
+ return agg
184
+ }
185
+ }
186
+
187
+ /** Pull and clear pending domain events. */
188
+ fun pullEvents(): List<${name}Event> {
189
+ val copy = _events.toList()
190
+ _events.clear()
191
+ return copy
192
+ }
193
+
194
+ private fun record(event: ${name}Event) { _events.add(event) }
195
+
196
+ private fun checkInvariants() {
197
+ // Add invariant assertions here.
198
+ }
199
+ }
200
+ ''')
201
+
202
+ KOTLIN_ID = Template('''\
203
+ package com.example.${lname}
204
+
205
+ import java.util.UUID
206
+
207
+ @JvmInline
208
+ value class ${name}Id(val value: String) {
209
+ init {
210
+ require(value.isNotBlank()) { "${name}Id must not be blank." }
211
+ }
212
+
213
+ companion object {
214
+ fun generate(): ${name}Id = ${name}Id(UUID.randomUUID().toString())
215
+ fun of(raw: String): ${name}Id = ${name}Id(raw)
216
+ }
217
+
218
+ override fun toString(): String = value
219
+ }
220
+ ''')
221
+
222
+ KOTLIN_REPOSITORY = Template('''\
223
+ package com.example.${lname}
224
+
225
+ interface ${name}Repository {
226
+ fun findById(id: ${name}Id): ${name}?
227
+ fun save(aggregate: ${name})
228
+ fun delete(id: ${name}Id)
229
+ fun exists(id: ${name}Id): Boolean
230
+ }
231
+ ''')
232
+
233
+ KOTLIN_EVENTS = Template('''\
234
+ package com.example.${lname}
235
+
236
+ import java.time.Instant
237
+
238
+ sealed class ${name}Event {
239
+ abstract val aggregateId: ${name}Id
240
+ abstract val occurredAt: Instant
241
+ }
242
+
243
+ data class ${name}Created(
244
+ override val aggregateId: ${name}Id,
245
+ override val occurredAt: Instant = Instant.now()
246
+ ) : ${name}Event()
247
+
248
+ // Add more events as the domain grows:
249
+ // data class ${name}Renamed(
250
+ // override val aggregateId: ${name}Id,
251
+ // val name: String,
252
+ // override val occurredAt: Instant = Instant.now()
253
+ // ) : ${name}Event()
254
+ ''')
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Java templates
258
+ # ---------------------------------------------------------------------------
259
+
260
+ JAVA_AGGREGATE = Template('''\
261
+ package com.example.${lname};
262
+
263
+ import java.util.ArrayList;
264
+ import java.util.Collections;
265
+ import java.util.List;
266
+ import java.util.Objects;
267
+
268
+ public final class ${name} {
269
+
270
+ private final ${name}Id id;
271
+ private final List<${name}Event> events = new ArrayList<>();
272
+
273
+ private ${name}(${name}Id id) {
274
+ this.id = Objects.requireNonNull(id, "id must not be null");
275
+ }
276
+
277
+ /** Factory — the only way to create a valid ${name}. */
278
+ public static ${name} create(${name}Id id) {
279
+ var agg = new ${name}(id);
280
+ agg.checkInvariants();
281
+ agg.record(new ${name}Created(id));
282
+ return agg;
283
+ }
284
+
285
+ public ${name}Id getId() { return id; }
286
+
287
+ /** Pull and clear pending domain events. */
288
+ public List<${name}Event> pullEvents() {
289
+ var copy = List.copyOf(events);
290
+ events.clear();
291
+ return copy;
292
+ }
293
+
294
+ private void record(${name}Event event) { events.add(event); }
295
+
296
+ private void checkInvariants() {
297
+ // Add invariant assertions here.
298
+ }
299
+ }
300
+ ''')
301
+
302
+ JAVA_ID = Template('''\
303
+ package com.example.${lname};
304
+
305
+ import java.util.Objects;
306
+ import java.util.UUID;
307
+
308
+ public record ${name}Id(String value) {
309
+
310
+ public ${name}Id {
311
+ Objects.requireNonNull(value, "value must not be null");
312
+ if (value.isBlank()) throw new IllegalArgumentException("${name}Id must not be blank.");
313
+ }
314
+
315
+ public static ${name}Id generate() { return new ${name}Id(UUID.randomUUID().toString()); }
316
+ public static ${name}Id of(String raw) { return new ${name}Id(raw); }
317
+
318
+ @Override public String toString() { return value; }
319
+ }
320
+ ''')
321
+
322
+ JAVA_REPOSITORY = Template('''\
323
+ package com.example.${lname};
324
+
325
+ import java.util.Optional;
326
+
327
+ public interface ${name}Repository {
328
+ Optional<${name}> findById(${name}Id id);
329
+ void save(${name} aggregate);
330
+ void delete(${name}Id id);
331
+ boolean exists(${name}Id id);
332
+ }
333
+ ''')
334
+
335
+ JAVA_EVENTS = Template('''\
336
+ package com.example.${lname};
337
+
338
+ import java.time.Instant;
339
+
340
+ public sealed interface ${name}Event permits ${name}Created {
341
+ ${name}Id aggregateId();
342
+ Instant occurredAt();
343
+ }
344
+
345
+ record ${name}Created(${name}Id aggregateId, Instant occurredAt) implements ${name}Event {
346
+ ${name}Created(${name}Id aggregateId) { this(aggregateId, Instant.now()); }
347
+ }
348
+
349
+ // Add more events as the domain grows:
350
+ // record ${name}Renamed(${name}Id aggregateId, String name, Instant occurredAt)
351
+ // implements ${name}Event { ... }
352
+ ''')
353
+
354
+ TEMPLATES = {
355
+ "python": {
356
+ "ext": "py",
357
+ "aggregate": PYTHON_AGGREGATE,
358
+ "id": PYTHON_ID,
359
+ "repository": PYTHON_REPOSITORY,
360
+ "events": PYTHON_EVENTS,
361
+ },
362
+ "kotlin": {
363
+ "ext": "kt",
364
+ "aggregate": KOTLIN_AGGREGATE,
365
+ "id": KOTLIN_ID,
366
+ "repository": KOTLIN_REPOSITORY,
367
+ "events": KOTLIN_EVENTS,
368
+ },
369
+ "java": {
370
+ "ext": "java",
371
+ "aggregate": JAVA_AGGREGATE,
372
+ "id": JAVA_ID,
373
+ "repository": JAVA_REPOSITORY,
374
+ "events": JAVA_EVENTS,
375
+ },
376
+ }
377
+
378
+
379
+ def write(path: Path, content: str) -> None:
380
+ path.parent.mkdir(parents=True, exist_ok=True)
381
+ path.write_text(content)
382
+ print(f" Created: {path}")
383
+
384
+
385
+ def scaffold(name: str, lang: str, output_dir: Path) -> None:
386
+ t = TEMPLATES[lang]
387
+ ext = t["ext"]
388
+ ctx = {"name": name, "lname": name.lower()}
389
+ files = {
390
+ f"{name}.{ext}": t["aggregate"],
391
+ f"{name}Id.{ext}": t["id"],
392
+ f"{name}Repository.{ext}": t["repository"],
393
+ f"{name}Events.{ext}": t["events"],
394
+ }
395
+ print(f"\nScaffolding DDD aggregate '{name}' ({lang}) in {output_dir}/\n")
396
+ for filename, tmpl in files.items():
397
+ write(output_dir / filename, tmpl.substitute(ctx))
398
+
399
+ print("\nNext steps:")
400
+ print(f" 1. Implement your domain commands inside {name}.{ext}")
401
+ print(f" 2. Add concrete repository in infrastructure/ (implements {name}Repository)")
402
+ print(f" 3. Publish events from {name}Events.{ext} via a message broker")
403
+ print(f" 4. Keep the aggregate free of infrastructure concerns\n")
404
+
405
+
406
+ def main() -> None:
407
+ parser = argparse.ArgumentParser(description="Scaffold DDD aggregate building blocks.")
408
+ parser.add_argument("name", help="Aggregate name (PascalCase), e.g. Order")
409
+ parser.add_argument("--lang", choices=["python", "kotlin", "java"], default="python")
410
+ parser.add_argument("--output-dir", default=".", type=Path)
411
+ args = parser.parse_args()
412
+
413
+ if not args.name[0].isupper():
414
+ print(f"ERROR: AggregateName should be PascalCase (got '{args.name}').", file=sys.stderr)
415
+ sys.exit(1)
416
+
417
+ scaffold(args.name, args.lang, args.output_dir)
418
+
419
+
420
+ if __name__ == "__main__":
421
+ main()
@@ -0,0 +1,46 @@
1
+ {
2
+ "evals": [
3
+ {
4
+ "id": "eval-01-public-mutable-fields-no-builder",
5
+ "prompt": "Review this Java code:\n\n```java\npublic class HttpRequest {\n public String url;\n public String method;\n public Map<String, String> headers;\n public String body;\n public int timeoutMs;\n public boolean followRedirects;\n public int maxRetries;\n public String authToken;\n public boolean verifySsl;\n public String proxyHost;\n public int proxyPort;\n \n public HttpRequest(String url, String method, Map<String, String> headers,\n String body, int timeoutMs, boolean followRedirects,\n int maxRetries, String authToken, boolean verifySsl,\n String proxyHost, int proxyPort) {\n this.url = url;\n this.method = method;\n this.headers = headers;\n this.body = body;\n this.timeoutMs = timeoutMs;\n this.followRedirects = followRedirects;\n this.maxRetries = maxRetries;\n this.authToken = authToken;\n this.verifySsl = verifySsl;\n this.proxyHost = proxyHost;\n this.proxyPort = proxyPort;\n }\n}\n\n// Typical usage:\nHttpRequest req = new HttpRequest(\n \"https://api.example.com/data\",\n \"POST\",\n new HashMap<>(),\n \"{\\\"key\\\": \\\"value\\\"}\",\n 5000,\n true,\n 3,\n null,\n true,\n null,\n 0\n);\nreq.headers.put(\"Content-Type\", \"application/json\");\n```",
6
+ "expectations": [
7
+ "Flags Item 2 (Builder pattern): 11-parameter constructor is a telescoping constructor — callers cannot tell what the 6th argument (boolean) means at the call site",
8
+ "Flags that all fields are public — violates the 'minimal accessibility' review criterion (Mode 2: Class design — 'Minimal accessibility?'); enables uncontrolled mutation that Item 17 (minimize mutability) and the private final recommendation are meant to prevent",
9
+ "Flags Item 17 (minimize mutability): HttpRequest should be immutable — a request is a value that shouldn't change after construction",
10
+ "Flags Item 50 (defensive copies): headers is a mutable Map passed directly — external code can mutate it after the fact; req.headers.put() after construction is a symptom",
11
+ "Recommends the Builder pattern with a fluent API: HttpRequest.builder(url).method(\"POST\").timeout(5000).build()",
12
+ "Notes that the two boolean parameters (followRedirects, verifySsl) at positions 6 and 9 are especially dangerous — easy to swap without compile-time error",
13
+ "Suggests making all fields private final, constructor private, and providing only getters",
14
+ "May note that modern Java could use a Record for simpler cases, but Builder is more appropriate here due to many optional fields"
15
+ ]
16
+ },
17
+ {
18
+ "id": "eval-02-raw-types-generics",
19
+ "prompt": "Review this Java code:\n\n```java\npublic class EventBus {\n private Map subscribers = new HashMap();\n \n public void subscribe(String eventType, Object handler) {\n List handlers = (List) subscribers.get(eventType);\n if (handlers == null) {\n handlers = new ArrayList();\n subscribers.put(eventType, handlers);\n }\n handlers.add(handler);\n }\n \n public void publish(String eventType, Object event) {\n List handlers = (List) subscribers.get(eventType);\n if (handlers != null) {\n for (Object handler : handlers) {\n try {\n // Cast handler to expected type and call it\n ((java.util.function.Consumer) handler).accept(event);\n } catch (ClassCastException e) {\n System.err.println(\"Handler type mismatch for event: \" + eventType);\n }\n }\n }\n }\n \n public List getHandlersForEvent(String eventType) {\n return (List) subscribers.get(eventType);\n }\n}\n\n// Usage:\nEventBus bus = new EventBus();\nbus.subscribe(\"USER_CREATED\", (Consumer<String>) name -> System.out.println(\"User created: \" + name));\nbus.subscribe(\"USER_CREATED\", (Consumer<Integer>) id -> System.out.println(\"ID: \" + id)); // Wrong type, no compile error\nbus.publish(\"USER_CREATED\", \"Alice\"); // Second handler will ClassCastException at runtime\n```",
20
+ "expectations": [
21
+ "Flags Item 26 (no raw types): Map, HashMap, List, ArrayList, and Consumer are all used as raw types — the entire class loses type safety",
22
+ "Explains the consequence: the second bus.subscribe() with Consumer<Integer> compiles silently but causes ClassCastException at runtime — exactly the bug raw types enable",
23
+ "Notes that catching ClassCastException to handle type mismatches is a design smell that would disappear with proper generics",
24
+ "Recommends a generic EventBus<T> or a type-safe subscriber model using Class<T> as the key instead of String",
25
+ "Suggests a typed design: subscribe(Class<T> eventType, Consumer<T> handler) with Map<Class<?>, List<Consumer<?>>> subscribers",
26
+ "Notes that getHandlersForEvent returns a raw List — callers cannot know the element type at compile time",
27
+ "May mention Item 27 (eliminate unchecked warnings): this code would generate many unchecked cast warnings that are being suppressed or ignored",
28
+ "Notes that String event type keys are fragile — typos cause silent misses; Class<T> as the key is self-documenting and refactoring-safe"
29
+ ]
30
+ },
31
+ {
32
+ "id": "eval-03-immutable-value-class",
33
+ "prompt": "Review this Java code:\n\n```java\npublic final class EmailAddress {\n private final String localPart;\n private final String domain;\n \n private EmailAddress(String localPart, String domain) {\n this.localPart = localPart;\n this.domain = domain;\n }\n \n public static EmailAddress of(String email) {\n Objects.requireNonNull(email, \"email must not be null\");\n String trimmed = email.trim().toLowerCase(Locale.ROOT);\n int atIndex = trimmed.indexOf('@');\n if (atIndex <= 0 || atIndex == trimmed.length() - 1) {\n throw new IllegalArgumentException(\"Invalid email address: \" + email);\n }\n String local = trimmed.substring(0, atIndex);\n String dom = trimmed.substring(atIndex + 1);\n return new EmailAddress(local, dom);\n }\n \n public String getLocalPart() { return localPart; }\n public String getDomain() { return domain; }\n \n public String toCanonicalString() {\n return localPart + \"@\" + domain;\n }\n \n @Override\n public boolean equals(Object o) {\n if (this == o) return true;\n if (!(o instanceof EmailAddress)) return false;\n EmailAddress that = (EmailAddress) o;\n return localPart.equals(that.localPart) && domain.equals(that.domain);\n }\n \n @Override\n public int hashCode() {\n return Objects.hash(localPart, domain);\n }\n \n @Override\n public String toString() {\n return toCanonicalString();\n }\n}\n```",
34
+ "expectations": [
35
+ "Recognizes this as a well-designed immutable value class and says so explicitly",
36
+ "Praises Item 17 (minimize mutability): class is final, all fields are private final, no setters",
37
+ "Praises Item 1 (static factory method): EmailAddress.of() gives a meaningful name, allows validation before construction, and can potentially cache instances",
38
+ "Praises Item 49 (parameter validation): uses Objects.requireNonNull and throws IllegalArgumentException with useful context",
39
+ "Praises Items 10-12 (equals/hashCode/toString contract): properly overridden, equals checks instanceof, hashCode uses Objects.hash, toString returns a useful representation",
40
+ "Praises normalization in the factory: trim() and toLowerCase() ensure canonical representation, meaning EmailAddress.of(\"USER@Example.COM\") equals EmailAddress.of(\"user@example.com\")",
41
+ "Does NOT manufacture fake issues just to have something to say",
42
+ "May offer minor optional suggestions (e.g., more thorough RFC-5321 validation, caching for common domains, a Java Record equivalent in newer Java versions) but clearly frames them as enhancements"
43
+ ]
44
+ }
45
+ ]
46
+ }
@@ -0,0 +1,83 @@
1
+ # After
2
+
3
+ An immutable `ShippingAddress` value class built with a fluent Builder, validated at construction time so that no corrupt instance can ever exist.
4
+
5
+ ```java
6
+ public final class ShippingAddress {
7
+ private final String recipientName;
8
+ private final String streetLine1;
9
+ private final String streetLine2; // nullable — optional field
10
+ private final String city;
11
+ private final String stateCode;
12
+ private final String postalCode;
13
+ private final String countryCode;
14
+ private final boolean residential;
15
+
16
+ private ShippingAddress(Builder builder) {
17
+ this.recipientName = builder.recipientName;
18
+ this.streetLine1 = builder.streetLine1;
19
+ this.streetLine2 = builder.streetLine2;
20
+ this.city = builder.city;
21
+ this.stateCode = builder.stateCode;
22
+ this.postalCode = builder.postalCode;
23
+ this.countryCode = builder.countryCode;
24
+ this.residential = builder.residential;
25
+ }
26
+
27
+ public static Builder builder(String recipientName, String streetLine1,
28
+ String city, String postalCode, String countryCode) {
29
+ return new Builder(recipientName, streetLine1, city, postalCode, countryCode);
30
+ }
31
+
32
+ // Accessors only — no setters
33
+ public String recipientName() { return recipientName; }
34
+ public String streetLine1() { return streetLine1; }
35
+ public Optional<String> streetLine2() { return Optional.ofNullable(streetLine2); }
36
+ public String city() { return city; }
37
+ public String stateCode() { return stateCode; }
38
+ public String postalCode() { return postalCode; }
39
+ public String countryCode() { return countryCode; }
40
+ public boolean isResidential(){ return residential; }
41
+
42
+ public static final class Builder {
43
+ // Required fields set via constructor
44
+ private final String recipientName;
45
+ private final String streetLine1;
46
+ private final String city;
47
+ private final String postalCode;
48
+ private final String countryCode;
49
+ // Optional fields
50
+ private String streetLine2;
51
+ private String stateCode = "";
52
+ private boolean residential = true;
53
+
54
+ private Builder(String recipientName, String streetLine1,
55
+ String city, String postalCode, String countryCode) {
56
+ this.recipientName = Objects.requireNonNull(recipientName, "recipientName");
57
+ this.streetLine1 = Objects.requireNonNull(streetLine1, "streetLine1");
58
+ this.city = Objects.requireNonNull(city, "city");
59
+ this.postalCode = Objects.requireNonNull(postalCode, "postalCode");
60
+ this.countryCode = Objects.requireNonNull(countryCode, "countryCode");
61
+ }
62
+
63
+ public Builder streetLine2(String val) { this.streetLine2 = val; return this; }
64
+ public Builder stateCode(String val) { this.stateCode = val; return this; }
65
+ public Builder commercial() { this.residential = false; return this; }
66
+ public ShippingAddress build() { return new ShippingAddress(this); }
67
+ }
68
+ }
69
+
70
+ // Usage — required fields enforced; optional fields have readable names
71
+ ShippingAddress addr = ShippingAddress
72
+ .builder("Jane Doe", "742 Evergreen Terrace", "Springfield", "62701", "US")
73
+ .stateCode("IL")
74
+ .commercial()
75
+ .build();
76
+ ```
77
+
78
+ Key improvements:
79
+ - Builder pattern eliminates the telescoping constructor problem — required fields are in the builder constructor, optional fields use fluent setters (Item 2: Builder when many parameters)
80
+ - All fields are `private final` — the class is immutable after construction (Item 17: Minimize mutability)
81
+ - `Objects.requireNonNull` validates all required fields at construction time — corrupt objects are impossible to create (Item 49: Validate parameters)
82
+ - `Optional<String>` return type for `streetLine2` explicitly signals to callers that this value may be absent (Item 55: Return Optional judiciously)
83
+ - `final` class prevents subclasses from breaking immutability guarantees (Item 17)
@@ -0,0 +1,37 @@
1
+ # Before
2
+
3
+ A Java class representing a shipping address with public mutable fields, no validation, and no safe construction — making it trivial to create corrupt objects.
4
+
5
+ ```java
6
+ public class ShippingAddress {
7
+ public String recipientName;
8
+ public String streetLine1;
9
+ public String streetLine2;
10
+ public String city;
11
+ public String stateCode;
12
+ public String postalCode;
13
+ public String countryCode;
14
+ public boolean isResidential;
15
+
16
+ public ShippingAddress() {}
17
+
18
+ public ShippingAddress(String recipientName, String streetLine1,
19
+ String streetLine2, String city, String stateCode,
20
+ String postalCode, String countryCode,
21
+ boolean isResidential) {
22
+ this.recipientName = recipientName;
23
+ this.streetLine1 = streetLine1;
24
+ this.streetLine2 = streetLine2;
25
+ this.city = city;
26
+ this.stateCode = stateCode;
27
+ this.postalCode = postalCode;
28
+ this.countryCode = countryCode;
29
+ this.isResidential = isResidential;
30
+ }
31
+ }
32
+
33
+ // Usage — nothing prevents corrupt construction
34
+ ShippingAddress addr = new ShippingAddress();
35
+ addr.city = "Austin";
36
+ // postalCode, countryCode, recipientName all null — silently broken
37
+ ```