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.
- package/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/agents/engineering/dotnet-engineer.md +78 -0
- package/docs/SKILLS-CATALOG.md +26 -3
- package/package.json +1 -1
- package/profiles/backend-dotnet.yaml +54 -0
- package/profiles/job-hunt.yaml +35 -0
- package/profiles/unity-design.yaml +1 -0
- package/profiles/unity-dev.yaml +1 -0
- package/rules/dotnet-code.md +64 -0
- package/skills/cold-outreach/SKILL.md +65 -0
- package/skills/cold-outreach/references/recruiter-playbook.md +65 -0
- package/skills/cover-letter/SKILL.md +66 -0
- package/skills/cv-ats-export/SKILL.md +64 -0
- package/skills/cv-ats-export/scripts/cv_export.py +306 -0
- package/skills/cv-tailor/SKILL.md +70 -0
- package/skills/cv-tailor/references/ats-keywords.md +46 -0
- package/skills/dotnet-architecture/SKILL.md +66 -0
- package/skills/dotnet-architecture/references/anti-patterns.md +12 -0
- package/skills/dotnet-architecture/references/checklist.md +19 -0
- package/skills/dotnet-architecture/references/patterns.md +118 -0
- package/skills/dotnet-architecture/references/project-structure.md +78 -0
- package/skills/dotnet-best-practices/SKILL.md +76 -0
- package/skills/dotnet-best-practices/references/api-design.md +75 -0
- package/skills/dotnet-best-practices/references/architecture.md +62 -0
- package/skills/dotnet-best-practices/references/async.md +62 -0
- package/skills/dotnet-best-practices/references/database.md +69 -0
- package/skills/dotnet-best-practices/references/dependency-injection.md +73 -0
- package/skills/dotnet-best-practices/references/devops.md +76 -0
- package/skills/dotnet-best-practices/references/error-handling.md +72 -0
- package/skills/dotnet-best-practices/references/performance.md +63 -0
- package/skills/dotnet-best-practices/references/security.md +73 -0
- package/skills/dotnet-best-practices/references/testing.md +76 -0
- package/skills/dotnet-scaffold/SKILL.md +99 -0
- package/skills/install-mcp/SKILL.md +107 -0
- package/skills/install-mcp/references/manual-setup.md +92 -0
- package/skills/interview-prep/SKILL.md +69 -0
- package/skills/interview-prep/references/star-framework.md +42 -0
- package/skills/job-hunt/SKILL.md +92 -0
- package/skills/job-hunt/references/templates/Aplicacion.md +48 -0
- package/skills/job-hunt/references/templates/CV Custom.md +53 -0
- package/skills/job-hunt/references/templates/Contacto.md +30 -0
- package/skills/job-hunt/references/templates/Dashboard.md +45 -0
- package/skills/job-hunt/references/templates/Empresa.md +36 -0
- package/skills/job-hunt/references/templates/Entrevista.md +44 -0
- package/skills/job-hunt/references/templates/Perfil.md +38 -0
- package/skills/job-search/SKILL.md +83 -0
- package/skills/job-search/references/scoring-rubric.md +43 -0
- package/skills/linkedin-optimize/SKILL.md +79 -0
- package/skills/master-profile/SKILL.md +69 -0
- package/skills/network-map/SKILL.md +61 -0
- package/skills/network-map/scripts/network_map.py +109 -0
- package/skills/personal-brand/SKILL.md +54 -0
- package/skills/personal-brand/references/post-pillars.md +66 -0
- 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_
|