@rmrdeveloper/sideroom-pi 5.0.0 → 6.0.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 (33) hide show
  1. package/README.md +108 -79
  2. package/{src/assets → assets}/artifacts/GUIDELINES_TEMPLATE.md +40 -11
  3. package/extensions/ask/execute.ts +56 -0
  4. package/extensions/ask/index.ts +77 -0
  5. package/extensions/ask/model.ts +196 -0
  6. package/extensions/ask/ui.ts +445 -0
  7. package/extensions/modified-files/index.ts +45 -0
  8. package/extensions/modified-files/model.ts +74 -0
  9. package/extensions/modified-files/session.ts +200 -0
  10. package/extensions/modified-files/ui.ts +207 -0
  11. package/extensions/todo/execute.ts +69 -0
  12. package/extensions/todo/guards.ts +107 -0
  13. package/extensions/todo/index.ts +91 -0
  14. package/extensions/todo/model.ts +327 -0
  15. package/extensions/todo/session.ts +166 -0
  16. package/extensions/todo/ui.ts +41 -0
  17. package/package.json +26 -27
  18. package/dist/pi-extension.js +0 -1175
  19. package/skills/sideroom-critic/SKILL.md +0 -32
  20. package/skills/sideroom-grilling/SKILL.md +0 -81
  21. package/skills/sideroom-spec/SKILL.md +0 -38
  22. package/skills/sideroom-transcribe-audio/SKILL.md +0 -38
  23. package/skills/sideroom-transcribe-audio/scripts/transcribe.py +0 -40
  24. package/src/assets/agents/sideroom-code-reviewer.md +0 -30
  25. package/src/assets/agents/sideroom-fixer.md +0 -37
  26. package/src/assets/agents/sideroom-implementer.md +0 -39
  27. package/src/assets/agents/sideroom-planner.md +0 -31
  28. package/src/assets/agents/sideroom-verifier.md +0 -32
  29. package/src/assets/artifacts/guidelines/java.md +0 -211
  30. package/src/assets/artifacts/guidelines/javascript.md +0 -235
  31. package/src/assets/artifacts/guidelines/php-laravel.md +0 -210
  32. package/src/assets/artifacts/guidelines/python.md +0 -227
  33. package/src/assets/artifacts/guidelines/typescript.md +0 -231
@@ -1,32 +0,0 @@
1
- ---
2
- name: sideroom-critic
3
- description: Adversarially stress-test a plan or diff for concrete logical, architectural, and scope failures.
4
- license: MIT
5
- ---
6
-
7
- # Adversarial critic
8
-
9
- Use this skill only when the user explicitly requests a red-team review of a
10
- non-trivial plan or implementation. It is not an automatic replacement for the
11
- pipeline reviewer or verifier.
12
-
13
- Read repository instructions, relevant domain context, the plan or diff, and
14
- the affected code before drawing conclusions. Verify facts with the available
15
- tools; do not report speculative risks.
16
-
17
- Attack only concrete concerns that ordinary implementation review can miss:
18
-
19
- - a reproducible edge case, invalid state, race, or broken invariant;
20
- - an unsupported business assumption, with the missing evidence identified;
21
- - a real dependency-direction, Law of Demeter, CQS, composition, or
22
- validate-once violation, including the concrete call or import chain;
23
- - scope mismatch, YAGNI, or a required case that is unhandled.
24
-
25
- For each finding, give a counterexample or precise consequence. Do not report
26
- formatting, naming, or generic style preferences. Do not edit code.
27
-
28
- Return either `No findings` or one record per issue:
29
-
30
- ```
31
- FINDING <N> | <Critical|High|Medium|Low> | <file:line> | <rule> | <description>
32
- ```
@@ -1,81 +0,0 @@
1
- ---
2
- name: sideroom-grilling
3
- description: Clarify a non-trivial feature, plan, or decision through dependency-aware question rounds before implementation.
4
- license: MIT
5
- ---
6
-
7
- # Grilling
8
-
9
- Reach a shared understanding before a non-trivial change is planned or built.
10
- Model the work as a **design tree**: each decision can unlock further decisions.
11
- Do not silently choose a product, scope, or trade-off decision for the user.
12
-
13
- ## Read facts; ask for decisions
14
-
15
- Read existing repository instructions, source, tests, package scripts, and any
16
- `CONTEXT.md` or `CONTEXT-MAP.md` before the first round. Facts such as current
17
- behavior, available commands, and domain vocabulary are yours to verify. Never
18
- ask the user for a fact you can inspect.
19
-
20
- When a context file exists, use its vocabulary exactly. A context term is a
21
- domain concept, not a generic technical word. Prefer one canonical term; use
22
- `Avoid:` terms to prevent synonyms from blurring a decision. If the repository
23
- has multiple contexts, use its map to identify the relevant one and ask only
24
- when the relationship remains unclear. Do not create context files or any
25
- project-local Sideroom state.
26
-
27
- The user owns decisions: desired outcome, scope, compatibility, user-visible
28
- behavior, priorities, and accepted trade-offs.
29
-
30
- ## Rounds and frontier
31
-
32
- The **frontier** contains decisions whose prerequisites are already settled.
33
- Ask no more than three independent frontier questions in one round. A question
34
- that depends on another unanswered question belongs to a later round.
35
-
36
- For every question:
37
-
38
- - Explain the concrete decision and its observable consequence.
39
- - Offer a recommended answer and why it best fits the known facts.
40
- - Keep alternatives mutually exclusive and avoid asking a disguised fact lookup.
41
- - Number questions continuously across rounds.
42
-
43
- After each answer, update the design tree, record the decision, and recompute
44
- the frontier. If the user accepts a recommendation, record the recommendation
45
- as the decision. If a fact lookup is still pending, ask every independent
46
- question that does not depend on it; do not block the entire round.
47
-
48
- In a conversational interface, format a fallback round as:
49
-
50
- ```md
51
- **Q1 — <short decision title>**
52
- <question, alternatives, and relevant facts>
53
-
54
- Recommended: <one answer and its trade-off>
55
- ```
56
-
57
- Use a native question UI when the host provides one. Otherwise use the fallback
58
- format and wait for the user's answer; never proceed because silence is
59
- convenient.
60
-
61
- ## Completion and handoff
62
-
63
- The grilling phase is complete only when the frontier is empty: each branch is
64
- settled or explicitly out of scope. Then produce a compact **Settled
65
- understanding** containing decisions, non-goals, repository constraints, and
66
- risks the user accepted. Do not implement or plan until this is confirmed.
67
-
68
- Hand the confirmed understanding to the planner. It constrains the plan; it is
69
- not an implementation task.
70
-
71
- ## Sideroom pipeline mode
72
-
73
- When invoked by Sideroom with a requested JSON schema, perform exactly one
74
- round. Return only the requested JSON value:
75
-
76
- - `status: "questions"` with one to three frontier questions, each with `id`,
77
- `title`, `question`, and `recommendation`; or
78
- - `status: "settled"` with a concise `summary` suitable for the planner.
79
-
80
- The caller supplies prior answers in the task context. Do not repeat settled
81
- questions or make changes to the working tree.
@@ -1,38 +0,0 @@
1
- ---
2
- name: sideroom-spec
3
- description: Draft a lightweight, implementation-ready feature specification without creating Sideroom project state.
4
- license: MIT
5
- ---
6
-
7
- # Specification writer
8
-
9
- Use this skill when the user asks for a standalone specification rather than an
10
- implementation. Read repository instructions and relevant domain context first.
11
- If scope or product decisions remain open, run `sideroom-grilling` in the current
12
- conversation and wait for a settled understanding.
13
-
14
- Draft one English Markdown document with these sections, in this order:
15
-
16
- ```md
17
- # <Title>
18
-
19
- ## Context
20
- ## Goal
21
- ## Non-goals
22
- ## Requirements
23
- ## Acceptance Criteria
24
- ## Constraints
25
- ## Open Questions
26
- ```
27
-
28
- Requirements are concise bullets. Acceptance criteria use observable
29
- Given/When/Then scenarios. Do not invent answers to open decisions; place them
30
- under **Open Questions**.
31
-
32
- Show the complete draft in the conversation. Only write it when the user names
33
- an explicit destination and confirms that write. Never initialize
34
- project-local Sideroom state, create task state, or write a specification merely as a side
35
- effect of running this skill.
36
-
37
- After confirmation, hand the settled document to the planner; do not implement
38
- the feature yourself.
@@ -1,38 +0,0 @@
1
- ---
2
- name: sideroom-transcribe-audio
3
- description: Transcribe a local mp3, wav, m4a, ogg, or opus recording to JSON text.
4
- license: MIT
5
- compatibility: Requires ffmpeg on PATH, Python 3, uv, and a first-run Whisper large-v3-turbo download.
6
- ---
7
-
8
- # Audio transcription
9
-
10
- Use this skill only when the user supplies a local audio-file path and asks for
11
- a transcript. Do not invent or summarize a transcript without running the
12
- companion script.
13
-
14
- Supported extensions are `mp3`, `wav`, `m4a`, `ogg`, and `opus`. Resolve the
15
- script relative to this skill directory, quote the audio path, and run:
16
-
17
- ```sh
18
- uv run --with faster-whisper python3 scripts/transcribe.py "<audio_path>"
19
- ```
20
-
21
- `uv run --with faster-whisper` creates the isolated dependency environment;
22
- do not separately install `faster-whisper` or create a virtual environment.
23
- The first run downloads the roughly 809 MB `large-v3-turbo` model and later
24
- runs use its cache.
25
-
26
- Check for `ffmpeg` and `uv` first. On Ubuntu or WSL, `ffmpeg` can be installed
27
- with `sudo apt install ffmpeg`; ask for explicit confirmation before installing
28
- anything. The same applies if `uv` is missing. Keep the recommended workload
29
- under roughly 25 MB or 30 minutes of audio on CPU-constrained hosts.
30
-
31
- The script writes only this JSON object to stdout:
32
-
33
- ```json
34
- {"language":"<detected-code>","text":"<transcript>"}
35
- ```
36
-
37
- Treat non-zero exit status, non-JSON output, a missing file, an unsupported
38
- extension, or a missing dependency as an error and report its stderr message.
@@ -1,40 +0,0 @@
1
- #!/usr/bin/env python3
2
- import json
3
- import shutil
4
- import sys
5
- from pathlib import Path
6
-
7
- ALLOWED_EXTENSIONS = {".mp3", ".wav", ".m4a", ".ogg", ".opus"}
8
-
9
-
10
- def fail(message):
11
- print(message, file=sys.stderr)
12
- sys.exit(1)
13
-
14
-
15
- if len(sys.argv) < 2:
16
- fail("usage: uv run --with faster-whisper python3 transcribe.py <audio_path>")
17
-
18
- audio_path = Path(sys.argv[1].strip('"').strip("'")).expanduser().resolve()
19
- if not audio_path.is_file():
20
- fail(f"not a file: {audio_path}")
21
-
22
- suffix = audio_path.suffix.lower()
23
- if suffix not in ALLOWED_EXTENSIONS:
24
- allowed = ", ".join(sorted(ALLOWED_EXTENSIONS))
25
- fail(f"unsupported extension {suffix!r}; allowed: {allowed}")
26
-
27
- if shutil.which("ffmpeg") is None:
28
- fail("ffmpeg is not on PATH; install the OS binary: sudo apt install ffmpeg")
29
-
30
- try:
31
- from faster_whisper import WhisperModel # type: ignore[import-not-found]
32
- except ImportError: # pragma: no cover
33
- fail(
34
- "invoke with: uv run --with faster-whisper python3 transcribe.py <audio_path>"
35
- )
36
-
37
- model = WhisperModel("large-v3-turbo", device="cpu", compute_type="int8")
38
- segments, transcription_info = model.transcribe(str(audio_path))
39
- text = "".join(segment.text for segment in segments).strip()
40
- print(json.dumps({"language": transcription_info.language, "text": text}, ensure_ascii=False))
@@ -1,30 +0,0 @@
1
- ---
2
- name: sideroom-code-reviewer
3
- description: Reviews an implementation for correctness and maintainability.
4
- readonly: true
5
- ---
6
-
7
- # Code reviewer
8
-
9
- Review the implementation against its plan, the repository's existing behavior,
10
- and the supplied language coding guidelines. Do not edit files or run mutating
11
- commands.
12
-
13
- ## Read-first
14
-
15
- Read the relevant instructions, changed source, nearby tests, and diff before
16
- forming conclusions. Validate findings against code paths that can actually occur;
17
- do not report speculative style preferences or issues outside the change scope.
18
-
19
- ## Review focus
20
-
21
- - Report correctness, security, compatibility, regression, and test-coverage risks.
22
- - Apply SOLID, simple design, validation, error-handling, and naming guidance only
23
- where a concrete issue follows.
24
- - Prefer a small number of precise, actionable findings over exhaustive narration.
25
- - State `No findings` when no issue warrants action.
26
-
27
- ## Response
28
-
29
- Return only the JSON string requested by the caller: either `"No findings"` or
30
- newline-separated `FINDING` records in the requested format.
@@ -1,37 +0,0 @@
1
- ---
2
- name: sideroom-fixer
3
- description: Fixes confirmed blocking findings after review and verification.
4
- readonly: false
5
- ---
6
-
7
- # Fixer
8
-
9
- Address only the confirmed blocking findings that accompany the implementation.
10
- Do not redesign unrelated code or reopen settled planning decisions.
11
-
12
- ## Read-first
13
-
14
- Read the repository instructions, the reported findings, the relevant changed code,
15
- and affected tests before modifying anything. Confirm each finding against the
16
- current tree and apply the supplied language coding guidelines.
17
-
18
- ## Mandatory write gate
19
-
20
- The complete **Shared engineering policy** and selected **Language policy** are
21
- already present in this session. Before **every** code-writing tool call
22
- (`edit`, `write`, or a shell command that changes source), re-read the relevant
23
- guideline sections and confirm the repair follows them. Never make a repair
24
- first and inspect its guidelines afterwards.
25
-
26
- ## Fix precisely
27
-
28
- - Make the smallest safe change that resolves a confirmed issue.
29
- - Preserve prior behavior outside the reported defect.
30
- - Update focused tests when a fix changes behavior.
31
- - Run relevant existing checks when practical; describe anything that could not run.
32
- - If a finding is not valid, leave the code intact and explain why in the summary.
33
-
34
- ## Response
35
-
36
- Return only the JSON object requested by the caller, with a concise factual
37
- `summary` of resolved findings, tests run, and any remaining limitation.
@@ -1,39 +0,0 @@
1
- ---
2
- name: sideroom-implementer
3
- description: Implements one approved plan task in the working tree.
4
- readonly: false
5
- ---
6
-
7
- # Implementer
8
-
9
- Implement the assigned task completely and keep the change narrowly scoped.
10
-
11
- ## Read-first
12
-
13
- Before editing, inspect the repository instructions, the plan task, the related
14
- source, tests, and the existing verification commands. Follow the repository's
15
- established patterns and the supplied language coding guidelines. Do not assume
16
- tools, files, package managers, or frameworks that are not present.
17
-
18
- ## Mandatory write gate
19
-
20
- The complete **Shared engineering policy** and selected **Language policy** are
21
- already present in this session. Before **every** code-writing tool call
22
- (`edit`, `write`, or a shell command that changes source), re-read the relevant
23
- guideline sections and confirm the change follows them. Do not write first and
24
- review the guidelines afterwards. If the task moves to a different concern,
25
- read the relevant policy again before its next write.
26
-
27
- ## Work carefully
28
-
29
- - Change only the files needed for the assigned task.
30
- - Keep interfaces compatible unless the task explicitly changes them.
31
- - Add or update focused tests when behavior changes.
32
- - Run the relevant existing checks when practical and report failures honestly.
33
- - Do not modify generated files, credentials, lockfiles, or unrelated formatting
34
- unless the task requires it.
35
-
36
- ## Response
37
-
38
- Return only the JSON object requested by the caller. List the files actually
39
- changed and give a concise factual summary of the implementation and verification.
@@ -1,31 +0,0 @@
1
- ---
2
- name: sideroom-planner
3
- description: Plans a requested code change before implementation.
4
- readonly: true
5
- ---
6
-
7
- # Planner
8
-
9
- Turn the request into a small, executable plan. Do not edit files, run mutating
10
- commands, or delegate work.
11
-
12
- ## Read-first
13
-
14
- Inspect the repository before planning. Read the applicable project instructions,
15
- the relevant source and tests, and the package scripts or build configuration when
16
- they affect the request. Base the plan on what is actually present; never invent
17
- files, commands, or framework conventions.
18
-
19
- ## Plan well
20
-
21
- - Split work into independently actionable tasks, in dependency order.
22
- - Name the files or areas that each task is likely to affect when evidence supports it.
23
- - Preserve public behavior unless the request explicitly changes it.
24
- - Include focused verification commands that the repository provides.
25
- - Apply the supplied language coding guidelines to every task.
26
-
27
- ## Response
28
-
29
- Return only the JSON object requested by the caller. Its `summary` must describe
30
- the intended outcome, `tasks` must contain stable ids and clear titles, and
31
- `verification` must be an array of concrete checks.
@@ -1,32 +0,0 @@
1
- ---
2
- name: sideroom-verifier
3
- description: Independently verifies an implementation against its acceptance criteria.
4
- readonly: true
5
- ---
6
-
7
- # Verifier
8
-
9
- Independently validate whether the implementation satisfies the plan and request.
10
- Do not edit files or run mutating commands.
11
-
12
- ## Read-first
13
-
14
- Inspect repository instructions, the assigned plan task, changed code, relevant
15
- tests, and available verification scripts. Use the supplied language coding
16
- guidelines as policy, but evaluate the code and tests rather than merely checking
17
- for words or patterns.
18
-
19
- ## Verification focus
20
-
21
- - Check the acceptance criteria and observable behavior.
22
- - Identify missing coverage, broken edge cases, and commands that should be run.
23
- - Report only reproducible or well-supported concerns in the requested finding
24
- format.
25
- - If the task meets its criteria, return no findings and explain the checks used.
26
-
27
- ## Response
28
-
29
- Return only the JSON object requested by the caller. Its `findings` field must
30
- be a string: exactly `No findings` or newline-separated `FINDING` records in
31
- the requested format — never an array, object, or `null`. Include a concise,
32
- non-empty `summary`.
@@ -1,211 +0,0 @@
1
- # Java Coding Guidelines
2
-
3
- This is the Java version of `GUIDELINES_TEMPLATE.md`. It preserves the same
4
- principles and example scenarios using explicit Java types, immutable values,
5
- and layered application boundaries. The shared baseline remains mandatory.
6
-
7
- ## Quick reference
8
-
9
- | Do | Don't |
10
- | --- | --- |
11
- | Guard invalid input early | Bury happy paths in nested `if/else` branches |
12
- | Throw a precise exception now | Substitute a default and hide invalid state |
13
- | Give a class one responsibility | Validate, transform, persist, and notify together |
14
- | Extract repeated domain behavior | Add a hierarchy for a one-off method |
15
- | Build only current behavior | Add speculative flags and configuration |
16
- | Prefer immutable records/value objects | Mutate data passed by a caller |
17
- | Validate once at an adapter | Re-validate the same command in every layer |
18
- | Depend on ports at real seams | Construct database/HTTP clients in domain code |
19
-
20
- ## Principles
21
-
22
- ### Guard clauses
23
-
24
- The discount example keeps the happy path shallow.
25
-
26
- ```java
27
- double getDiscount(User user) {
28
- if (user == null) return 0.0;
29
- if (!user.active()) return 0.0;
30
- if (!user.hasSubscription()) return 0.0;
31
- return 0.2;
32
- }
33
- ```
34
-
35
- ### Fail fast
36
-
37
- An unknown shipping zone is invalid state, not a reason to use a default.
38
-
39
- ```java
40
- int shippingCost(String zoneId, Map<String, Zone> zones) {
41
- var zone = zones.get(zoneId);
42
- if (zone == null) throw new IllegalArgumentException("Unknown zoneId: " + zoneId);
43
- return zone.baseCost();
44
- }
45
- ```
46
-
47
- ### SRP
48
-
49
- The user flow separates normalization, repository work, and notification.
50
-
51
- ```java
52
- final class CreateUser {
53
- private final UserRepository users;
54
- private final Mailer mailer;
55
- User create(String email) {
56
- if (email.isBlank()) throw new IllegalArgumentException("email required");
57
- var user = users.save(new User(normalizeEmail(email)));
58
- mailer.send(user.email(), "welcome");
59
- return user;
60
- }
61
- private String normalizeEmail(String email) { return email.trim().toLowerCase(Locale.ROOT); }
62
- }
63
- ```
64
-
65
- ### DRY
66
-
67
- Extract the third repeated ticket-status normalization, not a premature helper.
68
-
69
- ```java
70
- enum TicketStatus { OPEN, CLOSED }
71
- TicketStatus normalizeTicketStatus(String value) { return TicketStatus.valueOf(value.toUpperCase(Locale.ROOT)); }
72
- ```
73
-
74
- ### KISS
75
-
76
- Keep price and invoice examples limited to today's scope.
77
-
78
- ```java
79
- String formatPrice(BigDecimal value) { return "$" + value.setScale(2); }
80
- ```
81
-
82
- ### YAGNI (You Aren't Gonna Need It)
83
-
84
- Build the invoice requirement that exists today. Do not add currency strategies,
85
- recurring branches, event listeners, or cache layers without a real requirement.
86
-
87
- ```java
88
- Invoice createInvoice(Order order) { return new Invoice(order.total(), order.items()); }
89
- ```
90
-
91
- ### Composition over inheritance
92
-
93
- Compose behavior rather than inherit an irrelevant `fly()` operation.
94
-
95
- ```java
96
- record Dog(BarkBehavior bark) { String makeSound() { return bark.sound(); } }
97
- interface BarkBehavior { String sound(); }
98
- ```
99
-
100
- ### Law of Demeter
101
-
102
- Ask the user for its city, not its address's city's name.
103
-
104
- ```java
105
- String cityName(User user) { return user.cityName(); }
106
- ```
107
-
108
- ### Command Query Separation
109
-
110
- Peeking and incrementing a counter are separate methods.
111
-
112
- ```java
113
- int peekNextId(Counter counter) { return counter.value() + 1; }
114
- void incrementCounter(Counter counter) { counter.increment(); }
115
- ```
116
-
117
- ### Explicit error handling
118
-
119
- Retain a cause and add user-specific context; do not catch and return null.
120
-
121
- ```java
122
- User loadUser(String id, UserApi api) {
123
- try { return api.get(id); }
124
- catch (ApiException error) { throw new IllegalStateException("Failed to load user " + id, error); }
125
- }
126
- ```
127
-
128
- ### Immutability by default
129
-
130
- The cart scenario returns a new record.
131
-
132
- ```java
133
- record Cart(List<Item> items) {}
134
- Cart addItem(Cart cart, Item item) {
135
- var items = new ArrayList<>(cart.items());
136
- items.add(item);
137
- return new Cart(List.copyOf(items));
138
- }
139
- ```
140
-
141
- ### Null handling
142
-
143
- Prefer explicit absence with `Optional` at boundaries and preserve a zero
144
- discount as a real value.
145
-
146
- ```java
147
- double getDiscount(Optional<User> user) {
148
- return user.flatMap(User::plan).map(Plan::discount).orElse(0.0);
149
- }
150
- ```
151
-
152
- ### Testability as a design constraint
153
-
154
- Inject `Clock` instead of asking the system clock inside domain behavior.
155
-
156
- ```java
157
- boolean isSubscriptionExpired(Subscription subscription, Clock clock) {
158
- return subscription.expiresAt().isBefore(Instant.now(clock));
159
- }
160
- ```
161
-
162
- ### Dependency direction
163
-
164
- Invoice calculation receives an invoice rather than an SQL/JPA dependency.
165
-
166
- ```java
167
- int calculateInvoiceTotal(Invoice invoice) {
168
- return invoice.items().stream().mapToInt(InvoiceItem::price).sum();
169
- }
170
- ```
171
-
172
- ### Clear names
173
-
174
- Use `activeSubscriptions`, `remainingDays`, and `externalId`, never `data`,
175
- `temp`, or `obj`.
176
-
177
- ### Comments
178
-
179
- Comments explain a delivery trade-off or an idempotency hazard, not a statement
180
- whose name already explains it.
181
-
182
- ### No magic strings
183
-
184
- Model order states as an enum.
185
-
186
- ```java
187
- enum OrderStatus { PENDING_PAYMENT, PAID }
188
- if (order.status() == OrderStatus.PENDING_PAYMENT) payments.request(order);
189
- ```
190
-
191
- ### SOLID
192
-
193
- The welcome use case depends on a mailer port, not SMTP.
194
-
195
- ```java
196
- interface Mailer { void send(String email, String template); }
197
- final class SendWelcomeEmail { private final Mailer mailer; SendWelcomeEmail(Mailer mailer) { this.mailer = mailer; } }
198
- ```
199
-
200
- ### Validate once
201
-
202
- An HTTP/message adapter builds a validated command. The application service
203
- trusts it and never repeats the same null, type, and range checks.
204
-
205
- ```java
206
- record UpdateProfile(String email) {}
207
- void updateProfile(UpdateProfile command, UserRepository users) { users.update(command); }
208
- ```
209
-
210
- Use one validation contract per body/query. When in doubt: **fail fast, keep it
211
- flat, keep it small.**