claude-code-arcane 1.2.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -0
  3. package/agents/engineering/dotnet-engineer.md +78 -0
  4. package/docs/SKILLS-CATALOG.md +26 -3
  5. package/package.json +1 -1
  6. package/profiles/backend-dotnet.yaml +54 -0
  7. package/profiles/job-hunt.yaml +35 -0
  8. package/profiles/unity-design.yaml +1 -0
  9. package/profiles/unity-dev.yaml +1 -0
  10. package/rules/dotnet-code.md +64 -0
  11. package/skills/cold-outreach/SKILL.md +65 -0
  12. package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
  13. package/skills/cover-letter/SKILL.md +66 -0
  14. package/skills/cv-ats-export/SKILL.md +64 -0
  15. package/skills/cv-ats-export/scripts/cv_export.py +306 -0
  16. package/skills/cv-tailor/SKILL.md +70 -0
  17. package/skills/cv-tailor/references/ats-keywords.md +46 -0
  18. package/skills/dotnet-architecture/SKILL.md +66 -0
  19. package/skills/dotnet-architecture/references/anti-patterns.md +12 -0
  20. package/skills/dotnet-architecture/references/checklist.md +19 -0
  21. package/skills/dotnet-architecture/references/patterns.md +118 -0
  22. package/skills/dotnet-architecture/references/project-structure.md +78 -0
  23. package/skills/dotnet-best-practices/SKILL.md +76 -0
  24. package/skills/dotnet-best-practices/references/api-design.md +75 -0
  25. package/skills/dotnet-best-practices/references/architecture.md +62 -0
  26. package/skills/dotnet-best-practices/references/async.md +62 -0
  27. package/skills/dotnet-best-practices/references/database.md +69 -0
  28. package/skills/dotnet-best-practices/references/dependency-injection.md +73 -0
  29. package/skills/dotnet-best-practices/references/devops.md +76 -0
  30. package/skills/dotnet-best-practices/references/error-handling.md +72 -0
  31. package/skills/dotnet-best-practices/references/performance.md +63 -0
  32. package/skills/dotnet-best-practices/references/security.md +73 -0
  33. package/skills/dotnet-best-practices/references/testing.md +76 -0
  34. package/skills/dotnet-scaffold/SKILL.md +99 -0
  35. package/skills/install-mcp/SKILL.md +107 -0
  36. package/skills/install-mcp/references/manual-setup.md +92 -0
  37. package/skills/interview-prep/SKILL.md +69 -0
  38. package/skills/interview-prep/references/star-framework.md +42 -0
  39. package/skills/job-hunt/SKILL.md +92 -0
  40. package/skills/job-hunt/references/templates/Aplicacion.md +48 -0
  41. package/skills/job-hunt/references/templates/CV Custom.md +53 -0
  42. package/skills/job-hunt/references/templates/Contacto.md +30 -0
  43. package/skills/job-hunt/references/templates/Dashboard.md +45 -0
  44. package/skills/job-hunt/references/templates/Empresa.md +36 -0
  45. package/skills/job-hunt/references/templates/Entrevista.md +44 -0
  46. package/skills/job-hunt/references/templates/Perfil.md +38 -0
  47. package/skills/job-search/SKILL.md +83 -0
  48. package/skills/job-search/references/scoring-rubric.md +43 -0
  49. package/skills/linkedin-optimize/SKILL.md +79 -0
  50. package/skills/master-profile/SKILL.md +69 -0
  51. package/skills/network-map/SKILL.md +61 -0
  52. package/skills/network-map/scripts/network_map.py +109 -0
  53. package/skills/personal-brand/SKILL.md +54 -0
  54. package/skills/personal-brand/references/post-pillars.md +66 -0
  55. package/skills/portfolio-site/SKILL.md +59 -0
@@ -0,0 +1,306 @@
1
+ """
2
+ cv_export.py — Export Markdown CVs to ATS-compliant PDF.
3
+
4
+ Converts a Markdown CV to styled HTML and prints it with headless Chrome/Edge
5
+ (selectable text, single column, A4). No Obsidian, LaTeX or design tool needed.
6
+
7
+ Usage (from anywhere; the script locates the career workspace):
8
+ python cv_export.py # all base CVs (CV - *.md)
9
+ python cv_export.py "CV - Acme - Backend" # a single CV (with/without .md)
10
+ python cv_export.py --workspace ./career-workspace "CV - Acme"
11
+
12
+ Workspace resolution order:
13
+ 1. --workspace <path> flag
14
+ 2. CAREER_WORKSPACE env var
15
+ 3. walk up from the cwd looking for a folder containing "02-CVs" (or "02 - CVs")
16
+ 4. the current working directory
17
+
18
+ Output path:
19
+ - If frontmatter has `archivo_pdf`, the PDF is written to that path
20
+ relative to the CVs folder (<workspace>/02-CVs/<archivo_pdf>).
21
+ - Otherwise it goes to <workspace>/02-CVs/exports/<name>.pdf, where
22
+ <name> is the frontmatter field `archivo_final` if present, else the
23
+ .md filename with the "CV - " prefix stripped.
24
+
25
+ Supported Markdown (what CVs use):
26
+ # ## ### headings · **bold** · *italic* · [text](url) · - bullets · ---
27
+ A single line break inside a paragraph becomes <br> (Obsidian-like).
28
+ """
29
+
30
+ import html
31
+ import os
32
+ import re
33
+ import subprocess
34
+ import sys
35
+ import tempfile
36
+ from pathlib import Path
37
+
38
+ # Possible names for the CVs folder (portable + Obsidian-style with spaces).
39
+ CV_DIR_NAMES = ["02-CVs", "02 - CVs"]
40
+
41
+ BROWSERS = [
42
+ # Windows
43
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
44
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
45
+ r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
46
+ r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
47
+ # macOS
48
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
49
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
50
+ # Linux
51
+ "/usr/bin/google-chrome",
52
+ "/usr/bin/google-chrome-stable",
53
+ "/usr/bin/chromium",
54
+ "/usr/bin/chromium-browser",
55
+ "/usr/bin/microsoft-edge",
56
+ ]
57
+
58
+ # ATS-friendly print style: single column, selectable text, A4.
59
+ CSS = """
60
+ @page { size: A4; margin: 18mm 16mm 18mm 16mm; }
61
+ body {
62
+ font-family: "Calibri", "Arial", "Helvetica", sans-serif;
63
+ font-size: 10.5pt; line-height: 1.35; color: #000; background: #fff;
64
+ margin: 0; padding: 0;
65
+ }
66
+ h1 { font-size: 18pt; font-weight: 700; margin: 0 0 2pt 0; padding: 0; }
67
+ h2 {
68
+ font-size: 12pt; font-weight: 700; text-transform: uppercase;
69
+ letter-spacing: 0.5pt; border-bottom: 0.5pt solid #000;
70
+ padding: 0 0 1pt 0; margin: 10pt 0 5pt 0;
71
+ page-break-after: avoid;
72
+ }
73
+ h3 { font-size: 11pt; font-weight: 700; margin: 6pt 0 2pt 0; padding: 0; page-break-after: avoid; }
74
+ p { margin: 2pt 0; page-break-inside: avoid; }
75
+ ul, ol { margin: 2pt 0 4pt 0; padding-left: 16pt; }
76
+ li { margin: 1pt 0; page-break-inside: avoid; }
77
+ strong { font-weight: 700; }
78
+ a { color: #000; text-decoration: none; }
79
+ hr { border: none; border-top: 0.5pt solid #000; margin: 6pt 0; }
80
+ """
81
+
82
+
83
+ def find_cv_dir(start):
84
+ """Walk up from `start` looking for a directory containing a CVs folder."""
85
+ cur = Path(start).resolve()
86
+ for base in [cur, *cur.parents]:
87
+ for name in CV_DIR_NAMES:
88
+ cand = base / name
89
+ if cand.is_dir():
90
+ return cand
91
+ return None
92
+
93
+
94
+ def resolve_cv_dir(workspace_arg):
95
+ # 1. explicit --workspace
96
+ if workspace_arg:
97
+ ws = Path(workspace_arg).resolve()
98
+ for name in CV_DIR_NAMES:
99
+ if (ws / name).is_dir():
100
+ return ws / name
101
+ # maybe they passed the CVs dir directly
102
+ if ws.name in CV_DIR_NAMES and ws.is_dir():
103
+ return ws
104
+ print(f"[ERROR] No CVs folder ({' / '.join(CV_DIR_NAMES)}) under: {ws}")
105
+ sys.exit(1)
106
+ # 2. env var
107
+ env = os.environ.get("CAREER_WORKSPACE")
108
+ if env:
109
+ for name in CV_DIR_NAMES:
110
+ if (Path(env) / name).is_dir():
111
+ return Path(env).resolve() / name
112
+ # 3. walk up from cwd
113
+ found = find_cv_dir(Path.cwd())
114
+ if found:
115
+ return found
116
+ # 4. cwd fallback
117
+ print("[ERROR] Could not locate a CVs folder. Use --workspace <path>.")
118
+ sys.exit(1)
119
+
120
+
121
+ def strip_frontmatter(text):
122
+ """Return (frontmatter_dict, body). Flat key: value frontmatter."""
123
+ fm = {}
124
+ if text.startswith("---"):
125
+ end = text.find("\n---", 3)
126
+ if end != -1:
127
+ for line in text[3:end].strip().splitlines():
128
+ if ":" in line:
129
+ k, _, v = line.partition(":")
130
+ fm[k.strip()] = v.strip().strip('"')
131
+ text = text[end + 4:]
132
+ return fm, text.lstrip("\n")
133
+
134
+
135
+ def inline(s):
136
+ """Inline markdown -> HTML (escape first, then links/bold/italic)."""
137
+ s = html.escape(s, quote=False)
138
+ s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
139
+ s = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", s)
140
+ s = re.sub(r"(?<!\*)\*([^*\n]+)\*(?!\*)", r"<em>\1</em>", s)
141
+ return s
142
+
143
+
144
+ def md_to_html(md):
145
+ out = []
146
+ paragraph = []
147
+ in_list = False
148
+
149
+ def flush_paragraph():
150
+ if paragraph:
151
+ out.append("<p>" + "<br>".join(inline(l) for l in paragraph) + "</p>")
152
+ paragraph.clear()
153
+
154
+ def close_list():
155
+ nonlocal in_list
156
+ if in_list:
157
+ out.append("</ul>")
158
+ in_list = False
159
+
160
+ for raw in md.splitlines():
161
+ line = raw.rstrip()
162
+ stripped = line.strip()
163
+
164
+ if not stripped:
165
+ flush_paragraph()
166
+ close_list()
167
+ continue
168
+
169
+ m = re.match(r"^(#{1,3})\s+(.*)", stripped)
170
+ if m:
171
+ flush_paragraph()
172
+ close_list()
173
+ level = len(m.group(1))
174
+ out.append(f"<h{level}>{inline(m.group(2))}</h{level}>")
175
+ continue
176
+
177
+ if re.match(r"^(-{3,}|\*{3,})$", stripped):
178
+ flush_paragraph()
179
+ close_list()
180
+ out.append("<hr>")
181
+ continue
182
+
183
+ m = re.match(r"^[-*]\s+(.*)", stripped)
184
+ if m:
185
+ flush_paragraph()
186
+ if not in_list:
187
+ out.append("<ul>")
188
+ in_list = True
189
+ out.append(f"<li>{inline(m.group(1))}</li>")
190
+ continue
191
+
192
+ close_list()
193
+ paragraph.append(stripped)
194
+
195
+ flush_paragraph()
196
+ close_list()
197
+ return "\n".join(out)
198
+
199
+
200
+ def find_browser():
201
+ for path in BROWSERS:
202
+ if Path(path).exists():
203
+ return path
204
+ return None
205
+
206
+
207
+ def export(md_path, browser, cv_dir, export_dir):
208
+ text = md_path.read_text(encoding="utf-8")
209
+ fm, body = strip_frontmatter(text)
210
+
211
+ if fm.get("archivo_pdf"):
212
+ pdf_path = cv_dir / fm["archivo_pdf"]
213
+ else:
214
+ name = fm.get("archivo_final") or md_path.stem.replace("CV - ", "")
215
+ pdf_path = export_dir / f"{name}.pdf"
216
+ pdf_path.parent.mkdir(parents=True, exist_ok=True)
217
+
218
+ doc = (
219
+ "<!DOCTYPE html><html><head><meta charset='utf-8'>"
220
+ f"<title>{html.escape(md_path.stem)}</title>"
221
+ f"<style>{CSS}</style></head><body>"
222
+ + md_to_html(body)
223
+ + "</body></html>"
224
+ )
225
+
226
+ with tempfile.NamedTemporaryFile(
227
+ "w", suffix=".html", delete=False, encoding="utf-8"
228
+ ) as tmp:
229
+ tmp.write(doc)
230
+ tmp_path = Path(tmp.name)
231
+
232
+ try:
233
+ result = subprocess.run(
234
+ [
235
+ browser,
236
+ "--headless=new",
237
+ "--disable-gpu",
238
+ "--no-pdf-header-footer",
239
+ f"--print-to-pdf={pdf_path}",
240
+ tmp_path.as_uri(),
241
+ ],
242
+ capture_output=True,
243
+ text=True,
244
+ timeout=120,
245
+ )
246
+ if result.returncode != 0 or not pdf_path.exists():
247
+ print(f"[ERROR] {md_path.name}: browser exit {result.returncode}")
248
+ print(result.stderr[-500:] if result.stderr else "(no stderr)")
249
+ return False
250
+ finally:
251
+ tmp_path.unlink(missing_ok=True)
252
+
253
+ size_kb = pdf_path.stat().st_size // 1024
254
+ print(f"[OK] {md_path.name} -> {pdf_path} ({size_kb} KB)")
255
+ return True
256
+
257
+
258
+ def parse_args(argv):
259
+ workspace = None
260
+ names = []
261
+ i = 0
262
+ while i < len(argv):
263
+ a = argv[i]
264
+ if a == "--workspace":
265
+ i += 1
266
+ workspace = argv[i] if i < len(argv) else None
267
+ elif a.startswith("--workspace="):
268
+ workspace = a.split("=", 1)[1]
269
+ else:
270
+ names.append(a)
271
+ i += 1
272
+ return workspace, names
273
+
274
+
275
+ def main():
276
+ workspace, names = parse_args(sys.argv[1:])
277
+
278
+ browser = find_browser()
279
+ if not browser:
280
+ print("[ERROR] Chrome/Edge not found in standard paths. Install one or edit BROWSERS.")
281
+ sys.exit(1)
282
+
283
+ cv_dir = resolve_cv_dir(workspace)
284
+ export_dir = cv_dir / "exports"
285
+
286
+ if names:
287
+ targets = []
288
+ for n in names:
289
+ p = cv_dir / (n if n.endswith(".md") else n + ".md")
290
+ if not p.exists():
291
+ print(f"[ERROR] Not found: {p}")
292
+ sys.exit(1)
293
+ targets.append(p)
294
+ else:
295
+ targets = sorted(cv_dir.glob("CV - *.md"))
296
+
297
+ if not targets:
298
+ print(f"[ERROR] No CVs (CV - *.md) found in {cv_dir}.")
299
+ sys.exit(1)
300
+
301
+ ok = all([export(t, browser, cv_dir, export_dir) for t in targets])
302
+ sys.exit(0 if ok else 1)
303
+
304
+
305
+ if __name__ == "__main__":
306
+ main()
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: cv-tailor
3
+ description: "Tailor a CV to a specific job description: extract ATS keywords, decide what to emphasize vs downplay, and rewrite achievement bullets in the offer's vocabulary, starting from a role profile. Triggers: adaptar CV, custom CV, tailor resume, CV para esta oferta, optimizar CV para ATS, matchear CV con job description."
4
+ argument-hint: "[job-url | path-to-jd | application-note]"
5
+ category: "career"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Write, Edit, WebFetch
8
+ ---
9
+
10
+ # CV Tailor — Adaptar el CV a la oferta
11
+
12
+ Tomás un job description y el perfil base del usuario, y producís un **CV custom** que matchea con los filtros automáticos (ATS) y con la lectura humana del hiring manager. No reescribís la verdad — la reorganizás y la traducís al vocabulario de la oferta.
13
+
14
+ ## Inputs
15
+
16
+ - **El job description** (URL → usá WebFetch, o texto pegado, o una nota de `03-Aplicaciones/`).
17
+ - **El perfil base**: `01-Perfiles/<Role>.md` (derivado del maestro vía `/master-profile derive`). Si no existe, sugerir crearlo.
18
+ - **Research del contacto/empresa** si existe en `04-Empresas/` y `05-Contactos/`.
19
+
20
+ ## Proceso
21
+
22
+ ### 1. Analizar la oferta
23
+ - Extraer **keywords literales** del JD: tecnologías, metodologías, responsabilidades, soft skills, seniority. Literal importa — el ATS hace matching de strings (ej: si piden "React.js" no asumas que "React" matchea igual).
24
+ - Detectar **must-have vs nice-to-have**.
25
+ - Detectar **señales de cultura / ángulo** (startup vs enterprise, code-forward, ownership, etc.).
26
+ - Ver `references/ats-keywords.md` para cómo extraer y mapear keywords.
27
+
28
+ ### 2. Mapear contra el perfil
29
+ - Por cada must-have: ¿qué experiencia/proyecto del perfil lo cubre? Marcar gaps honestos.
30
+ - Decidir **qué resaltar** (lo que más alinea, arriba y con más espacio) y **qué bajar de volumen** (lo irrelevante para este rol).
31
+
32
+ ### 3. Reescribir
33
+ - Reordenar experiencia y proyectos hacia el rol.
34
+ - Reescribir bullets de logros usando el vocabulario de la oferta, manteniendo número→contexto→resultado.
35
+ - Inyectar keywords de forma natural (en bullets reales, no en un "keyword stuffing" detectable).
36
+ - Ajustar el headline/summary al rol exacto.
37
+
38
+ ### 4. Score before/after
39
+ - Calcular un **match rate** aproximado: % de keywords/requisitos del JD presentes en el CV, **antes** vs **después** del tailoring.
40
+ - Reportar el delta ("match 48% → 81%") y qué keywords se cubrieron.
41
+ - **Flag de keyword stuffing:** marcar cualquier inserción que a un revisor humano le suene forzada o poco natural — el objetivo es pasar el ATS *y* leer bien para una persona.
42
+
43
+ ### 5. Producir el CV custom
44
+ - Crear/actualizar la nota en `02-CVs/CV - <Empresa> - <Rol>.md` usando el template `CV Custom` (`Templates/CV Custom.md`).
45
+ - Completar frontmatter (`perfil_base`, `aplicacion`, `empresa`, `archivo_pdf`).
46
+ - Documentar las decisiones (keywords, qué se resaltó/bajó) en las secciones del template — sirve para la entrevista y para iterar.
47
+ - El cuerpo del CV en sí debe quedar listo para `/cv-ats-export` (markdown limpio: `# nombre`, `## secciones`, bullets, `---`).
48
+
49
+ ## Formato del CV (ATS-friendly)
50
+
51
+ - **Una columna.** Sin tablas, sin cajas de texto, sin columnas — los ATS las leen mal.
52
+ - Secciones estándar con headers claros: Summary, Experience, Skills, Projects, Education.
53
+ - Fechas consistentes (MM/YYYY).
54
+ - Sin gráficos ni imágenes para el contenido crítico.
55
+ - Verbos de acción + métricas.
56
+
57
+ ## Idioma
58
+
59
+ CV en **inglés** para roles internacionales/remotos globales; **español** si la oferta/empresa es hispanohablante. Ante la duda, preguntar.
60
+
61
+ ## Reglas
62
+
63
+ - Nunca inventar experiencia, fechas o métricas. Tailoring = reorganizar y traducir, no mentir.
64
+ - No keyword-stuffing: cada keyword debe vivir en un bullet verdadero.
65
+ - Un CV custom por postulación — no pisar el perfil base ni el maestro.
66
+ - Después de generar el `.md`, recordar al usuario correr `/cv-ats-export` para el PDF.
67
+
68
+ ## Handoff
69
+
70
+ Pedí aprobación (approval) antes de escribir el CV custom. Cuando el `.md` está READY, el siguiente paso es `/cv-ats-export` para generar el PDF ATS y `/cover-letter` para el mensaje.
@@ -0,0 +1,46 @@
1
+ # ATS Keywords — Cómo extraer y mapear
2
+
3
+ Los Applicant Tracking Systems (Greenhouse, Lever, Workday, Workable, etc.) hacen un primer filtro automático antes de que un humano vea el CV. Entender cómo "leen" cambia el resultado.
4
+
5
+ ## Cómo funciona un ATS (modelo mental)
6
+
7
+ - Parsea el CV a texto plano y lo indexa.
8
+ - Hace matching de **strings** y variantes contra la oferta y/o contra criterios del recruiter.
9
+ - Rankea o filtra candidatos por densidad/relevancia de keywords y por campos estructurados (título, fechas, ubicación).
10
+ - **Lo que rompe el parseo:** tablas, columnas, text boxes, headers/footers con datos, gráficos, iconos en lugar de texto, PDFs escaneados (imagen).
11
+
12
+ ## Extracción de keywords del JD
13
+
14
+ 1. **Hard skills / tecnologías** — lenguajes, frameworks, DBs, cloud, herramientas. Copiá la forma **literal** (React.js ≠ React ≠ ReactJS para un match exacto; cuando puedas, incluí la variante que usa la oferta).
15
+ 2. **Responsabilidades / verbos** — "design REST APIs", "lead a team", "CI/CD pipelines".
16
+ 3. **Metodologías** — Agile, Scrum, TDD, clean architecture.
17
+ 4. **Seniority y años** — "5+ years", "senior", "lead".
18
+ 5. **Soft skills explícitos** — communication, ownership, cross-functional.
19
+ 6. **Señales de cultura / frases gatillo** — ej. "code-forward", "startup mentality", "high autonomy". Suelen indicar qué valoran y dan ángulo para el summary y la cover letter.
20
+
21
+ ## Mapeo contra el perfil
22
+
23
+ | Keyword del JD | ¿Lo tengo? | Dónde lo demuestro (bullet/proyecto) | Acción |
24
+ |---|---|---|---|
25
+ | React.js | Sí | "Built React + TS frontends @ X" | resaltar arriba |
26
+ | Kubernetes | Parcial | exposición en CI/CD | mencionar honesto |
27
+ | Go | No | — | gap; no inventar |
28
+
29
+ - **Must-have que tenés** → arriba, con espacio, con la palabra exacta.
30
+ - **Must-have parcial** → mencionar con honestidad ("exposure to…").
31
+ - **Must-have que no tenés** → gap real; no lo inventes. Decidí si igual aplicás.
32
+
33
+ ## Inyección natural (no stuffing)
34
+
35
+ - Cada keyword vive en un **bullet verdadero** con contexto y resultado.
36
+ - Una sección "Skills" lista las tecnologías (bien para el ATS), pero las importantes también deben aparecer en la experiencia (bien para el humano y para el ranking).
37
+ - Evitá listas de 40 tecnologías sin contexto: lee como relleno y diluye el ranking.
38
+
39
+ ## Checklist de formato ATS-safe
40
+
41
+ - [ ] Una sola columna, sin tablas/text boxes.
42
+ - [ ] Headers de sección estándar (Experience, Skills, Education…).
43
+ - [ ] Fechas consistentes MM/YYYY.
44
+ - [ ] Texto seleccionable (no imagen) — el export de `/cv-ats-export` ya lo garantiza.
45
+ - [ ] Nombre del archivo profesional (`Nombre_Apellido_Empresa.pdf`).
46
+ - [ ] Contacto en el cuerpo, no en el header/footer del documento.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: dotnet-architecture
3
+ description: "Arquitectura de backends .NET: Vertical Slice Architecture (features) y Clean Architecture (capas), con guía de cuándo usar cada una, estructura, patrones (MediatR/Result/outbox) y anti-patterns. Usar al diseñar o revisar la estructura de un proyecto ASP.NET Core."
4
+ category: "backend"
5
+ argument-hint: "[vertical-slice|clean|when-to-use]"
6
+ user-invocable: true
7
+ allowed-tools: Read, Glob, Grep, Bash, Write, Edit, Task
8
+ ---
9
+ # dotnet-architecture — VSA & Clean Architecture para .NET
10
+
11
+ Dos enfoques production-ready para estructurar un backend ASP.NET Core. No son excluyentes: muchos proyectos
12
+ usan Clean Architecture en el borde y slices verticales adentro. Esta skill ayuda a **elegir** y a aplicar
13
+ cada uno correctamente.
14
+
15
+ ## MANDATORY WORKFLOW
16
+
17
+ **No imponer arquitectura por moda. Elegir según complejidad del dominio y vida del proyecto.**
18
+
19
+ ### Step 0: Elegir enfoque (`when-to-use`)
20
+
21
+ | Señal | Recomendación |
22
+ |-------|---------------|
23
+ | CRUD, prototipos, APIs chicas, dominio fino, equipo chico | **Vertical Slice** (o single project) |
24
+ | Dominio complejo, reglas de negocio ricas, long-lived, varios equipos | **Clean Architecture** |
25
+ | Necesitás aislar el dominio de frameworks/DB para test y reemplazo | **Clean Architecture** |
26
+ | Querés velocidad y bajo overhead de indirección | **Vertical Slice** |
27
+
28
+ > Evitá Clean Architecture para CRUD simple: los proyectos extra y la indirección cuestan más de lo que aportan
29
+ > cuando la lógica de negocio es delgada. Empezá con Vertical Slice y migrá si el dominio crece.
30
+
31
+ ### Step 1: Aplicar la estructura
32
+
33
+ - **Vertical Slice:** un folder por feature/use-case; cada slice tiene su request, handler, validator y endpoint juntos. Ver `references/project-structure.md`.
34
+ - **Clean Architecture:** proyectos `Domain → Application → Infrastructure → Api`; las dependencias apuntan **hacia adentro**, nunca al revés. Ver `references/project-structure.md`.
35
+
36
+ ### Step 2: Patrones idiomáticos
37
+
38
+ Leer `references/patterns.md`: MediatR/handlers por use-case, `IApplicationDbContext` (exponer EF Core sin repos
39
+ genéricos), pipeline behaviors (validación/logging/transacción), Result pattern en vez de excepciones para flujo,
40
+ y outbox pattern para consistencia entre DB y eventos.
41
+
42
+ ### Step 3: Revisar contra anti-patterns + checklist
43
+
44
+ - `references/anti-patterns.md` — repos genéricos sobre EF Core, fat controllers, dependencias que apuntan hacia afuera, lógica de dominio en Infrastructure.
45
+ - `references/checklist.md` — verificación de cierre antes de dar la estructura por buena.
46
+
47
+ Antes de aplicar una reestructuración significativa, confirmar el approach con el usuario (Question → Decision → Approval). Si todo el checklist pasa → arquitectura **COMPLIANT**.
48
+
49
+ ## Resumen de enfoques
50
+
51
+ | Aspecto | Vertical Slice | Clean Architecture |
52
+ |---------|----------------|--------------------|
53
+ | Eje de organización | Feature / use-case | Capa técnica |
54
+ | Proyectos | 1 (+ tests) | 4 (Domain/App/Infra/Api) |
55
+ | Acoplamiento entre features | Bajo (slices independientes) | Mediado por Application |
56
+ | Overhead inicial | Bajo | Alto |
57
+ | Mejor para | CRUD, features, velocidad | Dominios complejos, long-lived |
58
+ | Riesgo | Duplicación entre slices | Sobre-ingeniería |
59
+
60
+ ## Próximos pasos
61
+
62
+ Definida la arquitectura → `/dotnet-scaffold` para generar el proyecto, y `/dotnet-best-practices` para revisar el código contra las 40 reglas.
63
+
64
+ ---
65
+
66
+ _Inspirado en [ardalis/CleanArchitecture](https://github.com/ardalis/CleanArchitecture), [nadirbad/VerticalSliceArchitecture](https://github.com/nadirbad/VerticalSliceArchitecture) y la guía de Vertical Slice de [Milan Jovanović](https://www.milanjovanovic.tech/blog/vertical-slice-architecture-dotnet). Adaptado al formato Arcane._
@@ -0,0 +1,12 @@
1
+ # Anti-Patterns
2
+
3
+ - **`IRepository<T>` / `IUnitOfWork` genérico sobre EF Core** — el `DbContext` ya es repositorio (`DbSet<>`) y Unit of Work (`SaveChangesAsync`); envolverlo agrega indirección sin valor y te tapa LINQ/Include/proyecciones.
4
+ - **Fat controllers / endpoints con lógica de negocio** — el endpoint solo traduce HTTP ↔ request y mapea el resultado; las reglas van en el handler/dominio, no en el controlador.
5
+ - **Dependencias apuntando hacia afuera** — `Domain` referenciando `Infrastructure` o EF Core rompe la regla de dependencias: el dominio deja de ser testeable y reemplazable.
6
+ - **Application anémica que solo reenvía a repos** — si el handler no hace más que `return repo.Get(id)`, la capa no aporta nada; movés lógica de negocio real al dominio/handler o eliminás la indirección.
7
+ - **Filtrar entidades EF como respuestas de API** — exponer entidades de persistencia acopla el contrato HTTP al schema de la DB y arrastra lazy-loading/ciclos; devolvé DTOs en el borde.
8
+ - **Forzar Clean Architecture en un CRUD simple** — cuatro proyectos y pipeline behaviors para listar y guardar registros es sobre-ingeniería; empezá con Vertical Slice o single project.
9
+ - **Service locator estático / `BuildServiceProvider()` en `Program.cs`** — construir el provider a mano crea grafos duplicados y dependencias ocultas; registrá todo en el contenedor y dejá que resuelva por constructor.
10
+ - **Un `Services/` y `Controllers/` gigantes** — organizar por tipo técnico dispersa cada feature en cinco carpetas; organizá por feature/slice para que un caso de uso viva junto.
11
+
12
+ _Ref: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-dotnet · https://github.com/ardalis/CleanArchitecture · https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/_
@@ -0,0 +1,19 @@
1
+ # Checklist
2
+
3
+ Verificá antes de dar la estructura por buena.
4
+
5
+ - [ ] Las dependencias apuntan hacia adentro: `Api → Infrastructure → Application → Domain`
6
+ - [ ] `Domain` no tiene `<ProjectReference>` ni paquetes de framework (sin EF, sin ASP.NET)
7
+ - [ ] No hay `IRepository<T>` / `IUnitOfWork` genérico envolviendo EF Core
8
+ - [ ] EF Core se expone vía `IApplicationDbContext` en Application, implementado en Infrastructure
9
+ - [ ] En Vertical Slice, cada slice es autocontenido (request + handler + validator + endpoint)
10
+ - [ ] Los slices no dependen entre sí (acoplamiento solo vía `Common`/`Infrastructure`)
11
+ - [ ] DTOs en el borde: ninguna entidad EF se devuelve como respuesta de API
12
+ - [ ] Cada handler tiene una sola responsabilidad (un caso de uso por archivo)
13
+ - [ ] `Program.cs` es solo composition root (DI + pipeline + map endpoints), sin lógica de negocio
14
+ - [ ] La validación corre en un pipeline behavior, no repetida en cada endpoint
15
+ - [ ] Fallos esperados se modelan con `Result<T>`/`ErrorOr`, no con excepciones de flujo
16
+ - [ ] Las migraciones EF Core están versionadas en git
17
+ - [ ] El enfoque elegido (VSA vs Clean) corresponde a la complejidad real del dominio
18
+
19
+ _Ref: https://github.com/ardalis/CleanArchitecture · https://github.com/nadirbad/VerticalSliceArchitecture · https://www.milanjovanovic.tech/blog/clean-architecture-folder-structure_
@@ -0,0 +1,118 @@
1
+ # Patrones Idiomáticos (.NET 10 / ASP.NET Core)
2
+
3
+ ## Handler por caso de uso
4
+
5
+ Un request (record) + un handler. Con MediatR o con un sender hand-rolled mínimo.
6
+
7
+ ```csharp
8
+ public sealed record CreateOrder(Guid CustomerId, decimal Amount) : IRequest<Result<Guid>>;
9
+
10
+ internal sealed class CreateOrderHandler(IApplicationDbContext db)
11
+ : IRequestHandler<CreateOrder, Result<Guid>>
12
+ {
13
+ public async Task<Result<Guid>> Handle(CreateOrder cmd, CancellationToken ct)
14
+ {
15
+ var order = Order.Create(cmd.CustomerId, cmd.Amount);
16
+ db.Orders.Add(order);
17
+ await db.SaveChangesAsync(ct);
18
+ return order.Id;
19
+ }
20
+ }
21
+ ```
22
+
23
+ ## IApplicationDbContext (EF Core detrás de una interfaz, sin repo genérico)
24
+
25
+ Exponé el `DbContext` a través de una interfaz en `Application`; implementala en
26
+ `Infrastructure`. El `DbContext` ya es Unit of Work + repositorio: no lo envuelvas.
27
+
28
+ ```csharp
29
+ // Application/Common/IApplicationDbContext.cs
30
+ public interface IApplicationDbContext
31
+ {
32
+ DbSet<Order> Orders { get; }
33
+ DbSet<Customer> Customers { get; }
34
+ Task<int> SaveChangesAsync(CancellationToken ct = default);
35
+ }
36
+
37
+ // Infrastructure/Persistence/AppDbContext.cs
38
+ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
39
+ : DbContext(options), IApplicationDbContext
40
+ {
41
+ public DbSet<Order> Orders => Set<Order>();
42
+ public DbSet<Customer> Customers => Set<Customer>();
43
+ }
44
+ ```
45
+
46
+ ## Pipeline behaviors (validación, logging, transacción)
47
+
48
+ Cross-cutting concerns alrededor de cada handler, sin ensuciar la lógica.
49
+
50
+ ```csharp
51
+ public sealed class ValidationBehavior<TReq, TRes>(IEnumerable<IValidator<TReq>> validators)
52
+ : IPipelineBehavior<TReq, TRes> where TReq : notnull
53
+ {
54
+ public async Task<TRes> Handle(TReq req, RequestHandlerDelegate<TRes> next, CancellationToken ct)
55
+ {
56
+ foreach (var v in validators)
57
+ {
58
+ var result = await v.ValidateAsync(req, ct);
59
+ if (!result.IsValid) throw new ValidationException(result.Errors);
60
+ }
61
+ return await next();
62
+ }
63
+ }
64
+ ```
65
+
66
+ Un `TransactionBehavior` similar abre `BeginTransactionAsync` antes de `next()` y
67
+ hace commit/rollback alrededor: el UnitOfWork vive acá, no en repos a mano.
68
+
69
+ ## Result pattern para fallos esperados
70
+
71
+ No tires excepciones para flujo de negocio esperado (not found, validación, conflicto).
72
+ Devolvé `Result<T>` (o `ErrorOr<T>`) y mapealo a HTTP en el endpoint.
73
+
74
+ ```csharp
75
+ public readonly record struct Error(string Code, string Message);
76
+
77
+ public async Task<Result<OrderDto>> Handle(GetOrder q, CancellationToken ct)
78
+ {
79
+ var order = await db.Orders.FindAsync([q.Id], ct);
80
+ return order is null
81
+ ? new Error("Order.NotFound", "Order no existe")
82
+ : order.ToDto();
83
+ }
84
+ ```
85
+
86
+ ## Outbox pattern (consistencia DB + eventos)
87
+
88
+ Para publicar eventos de forma confiable: persistí el evento en una tabla `Outbox`
89
+ **dentro de la misma transacción** que el cambio de datos. Un background worker
90
+ (`BackgroundService`) lee la tabla y publica al broker; así nunca queda el estado
91
+ guardado sin el evento ni el evento sin el estado.
92
+
93
+ ## TypedResults en minimal API endpoints
94
+
95
+ El endpoint es delgado: traduce HTTP ↔ request y mapea el `Result` a status code.
96
+
97
+ ```csharp
98
+ public static class OrderEndpoints
99
+ {
100
+ public static void Map(this IEndpointRouteBuilder app)
101
+ {
102
+ var group = app.MapGroup("/orders").WithTags("Orders");
103
+
104
+ group.MapPost("/", async (CreateOrder cmd, ISender sender, CancellationToken ct) =>
105
+ {
106
+ var result = await sender.Send(cmd, ct);
107
+ return result.IsError
108
+ ? TypedResults.Problem(result.FirstError.Message)
109
+ : TypedResults.Created($"/orders/{result.Value}", result.Value);
110
+ });
111
+ }
112
+ }
113
+ ```
114
+
115
+ `TypedResults` da el tipo de respuesta exacto (útil para OpenAPI y tests) en vez del
116
+ `Results` no tipado.
117
+
118
+ _Ref: https://www.milanjovanovic.tech/blog/internal-vs-public-apis-in-clean-architecture · https://github.com/ardalis/CleanArchitecture · https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/responses_