‹ Juan Valentin Alducin Writing
SDDOpenSpecCase study

Anatomy of a change: one real SDD/OpenSpec change, artifact by artifact

2026-09-20 · 9 min read

In Spec-Driven Development: the spec is the source of truth I described the flow. This post shows it running on a real change of this very repository: cv-portafolio-sre-automation, archived on 2026-09-19. Every fragment below is quoted from the artifacts as they sit in openspec/changes/archive/2026-09-19-cv-portafolio-sre-automation/ — nothing was written for the post. The only edits are the <redacted> placeholders where an artifact lists my employer's internal system names: the whole point of decision D2 is that those never reach a public page.

proposal→specs→design→tasks→apply→verify→archive

The change in one line: re-align the CV (ES/EN, one page), the portfolio, llms.txt and the JSON-LD from a generic "Backend + Agentic AI" pitch to what the last year actually looked like — reliability/SRE, automation and applied AI — using honest, evidence-based skill levels and no internal names from my employer.

8capability specs (2 new, 6 modified)
D1–D8design decisions with alternatives
47/47automated checks PASS at close
339.3 mmCV sheet height — 1 page (limit 340)

1. proposal — why does this change exist?

proposal.md ↗

The proposal is written first and it is deliberately opinionated: it names the gap between what the CV says and what the evidence shows.

## Why
El CV y el portafolio venden un perfil "Backend + Agentic AI" genérico, mientras que la evidencia real del
último año en Podemos Progresar (sep 2025 → sep 2026) es otra: **confiabilidad/SRE** (sistema de
post-mortems, metodología de uptime mensual, runbooks), **automatización** (n8n con SDK, pipelines spec-driven,
integraciones Freshdesk/Notion/Asana/Slack), **SQL/ETL crítico en producción** e **IA aplicada** …

What it decided: the scope. It lists the capabilities it will add (stack-por-niveles, principios-de-trabajo) and the ones it will modify (positioning, experience, skills, Fidello, AI-readable layer, CV presentation), plus the impact surface (index.html, cv/*.html → PDFs, llms.txt, CLAUDE.md, docs). No code yet.

2. specs — what must be true when we are done?

specs/ (8 capabilities) ↗

Each capability gets a delta spec with requirements and verifiable scenarios (WHEN / THEN / AND). This is the acceptance test the implementation will be checked against. From specs/proyecto-fidello/spec.md:

#### Scenario: Cifras de ingeniería verificables
- **WHEN** se lee la card de Fidello
- **THEN** muestra, como texto o mini-badges, al menos: 793 pruebas (337 de base de datos contra Postgres real
  + 456 de pantalla), 91 migraciones SQL, 22 tablas, 7 Edge Functions, 5 tareas `pg_cron`, 97 cambios
  OpenSpec (85 archivados), 28 journeys con runbook gemelo, CI con 2 jobs bloqueantes (frontend + BD) …

And the new capability stack-por-niveles fixes the rubric that the skills section must follow — including what must not appear:

| Nivel | Etiqueta (EN, sitio) | Significado |
|---|---|---|
| 1 | **Own**      | Lo construyó de punta a punta y es el dueño operativo |
| 2 | **Build**    | Arma soluciones con ello de forma recurrente |
| 3 | **Operate**  | Lo usa con soltura para diagnosticar y operar; no construye la plataforma |
| 4 | **Learning** | Estudio dirigido; sin profundidad operativa todavía |
…
- **AND** NO aparecen Amazon Kendra ni AWS Kiro en ninguna categoría

What it decided: the contract. Numbers, labels, exclusions. Later, the audit script is literally a translation of these scenarios into PASS/FAIL lines.

3. design — how, with alternatives and trade-offs

design.md ↗

The design records eight numbered decisions (D1–D8). Each one says what was chosen and what was rejected. D1 fixes the exact headline string across every surface:

### D1. Headline híbrido y su forma exacta
- Sitio (`header h3`, `<title>`, OG, JSON-LD `jobTitle`, `whoami`):
  `Senior Backend Engineer · Tech Lead · SRE & Automation · Agentic AI`
- Alternativas: "SRE first" (descartada: saca de búsquedas backend) y mantener el actual (descartada: no
  refleja la evidencia).

D2 is the one I care most about: a mapping from internal system names to public language, so the CV never leaks my employer's internals — and a note that the rule is enforced by a script, not by memory:

### D2. Mapa de lenguaje genérico (interno → público)
| Interno (NO usar) | Público |
| <redacted: internal platform names> | "plataforma de ciclo de crédito / integración con core banking (Step Functions)" |
| <redacted: internal app name>        | "app de campo (Android) para formalización y KYC" |
…
Se implementa una verificación automática (grep) en tasks para que la regla no dependa de memoria.

D7 is the space budget for a one-page CV — and it was corrected during implementation, which is exactly what a design is for (see step 6):

### D7. Estrategia de espacio en el CV (1 página)
… **Techo real calibrado en implementación: `.sheet` ≤ 340 mm** (343.6 mm ya produce 2 páginas: Chrome
mueve el flex `.cols` completo a la página 2). Resultado final: sheet 339.3 mm (izq 298.5 / der 292.2)

What it decided: D1 headline · D2 language map · D3 experience bullets (≤120 chars for the CV) · D4 skills-by-level markup/CSS · D5 where the principles block lives · D6 Fidello as a pilot with real numbers · D7 one-page space budget · D8 the edit order across the five sources of truth.

4. tasks — ordered, ≤2 h blocks, with mandatory steps

tasks.md ↗

Tasks always start with the feature branch and always end with testing, manual verification and documentation — the project has a rule file that makes these steps non-negotiable. Task 0.3 turns the specs into a script before any content is touched:

- [x] 0.3 Crear `…/scripts/audit.py` con las comprobaciones automáticas reutilizables:
      (a) grep de nombres internos prohibidos (D2) en `index.html`, `cv/*.html`, `llms.txt` …;
      (b) headline idéntico en `header h3`, `<title>`, OG, JSON-LD, `whoami`, `.tagline` ES/EN;
      (c) fechas/títulos de los 4 roles iguales en CV ES, CV EN, `index.html`, `llms.txt`;
      (d) ausencia de "Kendra", "Kiro", "Master", "IEU"; (e) conteo de páginas y fuentes de los PDF (pypdf)
## 5. Ejecutar pruebas y verificar estado (OBLIGATORIO) — EL AGENTE EJECUTA
- [x] 5.1 Capturar estado previo: `git rev-parse HEAD`, tamaño y páginas de los PDF actuales, salida de
      `audit.py` antes de los cambios (se espera FAIL en los checks nuevos)
- [x] 5.2 Ejecutar `python …/scripts/audit.py` tras los cambios: todos los checks en PASS

What it decided: the order of work (D8: index.html → llms.txt → CV ES → CV EN → build → docs) and the definition of done: audit green, headless screenshots at 1280/992/768/480, a report file, docs in sync.

5. apply — implement against the contract

scripts/ (audit.py, measure_cv.py, patch_*.py, shots.py) ↗

Implementation is where the AI agent does most of the typing — but against the artifacts, not against a chat prompt. The audit script is the spec made executable. Its core is a list of forbidden patterns and a one-line check():

# Nombres internos del empleador y datos privados (design.md D2)
FORBIDDEN = [ <redacted: regexes for internal system names, incident IDs, database names, people> ]
STALE = [r"Kendra", r"\bKiro\b", r"Master'?s in DevOps", r"Maestr[ií]a en DevOps", r"\bIEU\b", r"Agentic AI Systems"]

def check(name, ok, detail=""):
    results.append((name, ok, detail))
    print(("PASS  " if ok else "FAIL  ") + name + (("  -> " + detail) if detail and not ok else ""))

The edits to index.html and the CV were applied by small idempotent patch scripts (patch_index.py, patch_cv.py) rather than by hand, so a re-run produces the same file. Running the audit before the change gave the expected picture: 18 PASS · 29 FAIL (old headline, internal names, Kendra/Kiro, no levels, no Fidello numbers). That "expected red" is the TDD moment of the flow.

6. verify — the agent runs the checks, then writes the report

reports/2026-09-19-step-5-pruebas-y-verificacion.md ↗

Verification is not delegated to a human "please check it". The report lists the commands, the numbers before/after and the incidents found along the way:

## Resultados de pruebas (audit.py = suite del proyecto, no hay pruebas unitarias)
- Antes de los cambios: 47 checks · 18 PASS · 29 FAIL (esperado …)
- Después: **47 checks · 47 PASS · 0 FAIL** (exit 0)
…
Incidencia de implementación: con el contenido nuevo el CV saltó a 2 páginas (Chrome mueve el flex `.cols`
completo a la página 2 cuando `.sheet` > 340 mm). Se movió Idiomas a la columna izquierda, se acortó el
resumen a 4 líneas y se bajó `line-height` 1.28 → 1.25; regla calibrada documentada en `design.md` D7

The one-page calibration

The interesting bug: with the new bullets the PDF jumped to two pages. Instead of eyeballing it, a tiny script (measure_cv.py) renders the HTML in headless Chrome and reads the sheet and column heights in millimetres:

"""Mide en Chrome headless la altura (mm) del sheet y de cada columna del CV.
Regla empírica calibrada 2026-09-19 (design.md D7): el PDF cabe en 1 página si sheet <= 340 mm
(sheet = 20 mm de padding + header + max(col)); con sheet 343.6 ya salta a 2 páginas."""

Three build iterations later: 339.3 mm (left column 298.5 / right 292.2), one page in ES and EN, only SegoeUI* TrueType fonts embedded, text layer extractable for ATS. The threshold went back into design.md D7 and into the cv-presentacion spec — a bug found in apply updates the design, not just the code.

CheckResult
Horizontal overflow at 480 px (4 views)scrollW == innerW — a pre-existing 12 px overflow in the timeline was found and fixed
Skills by level4 levels, legend visible, 2 columns ≥ 992 px / 1 column ≤ 768 px
Fidello pilot6 metric badges, no Google Wallet presented as active
Error case: forbidden name injectedaudit.py reports FAIL (then the temp file is removed)
PDF text (ATS)name, phone, email, roles, dates present; no internal names

7. archive — promote the deltas into the living specs

openspec/specs/ (living capabilities) ↗

Archiving does two things: the delta specs are merged into openspec/specs/<capability>/spec.md (two new capabilities are created, six are updated), and the change folder moves to openspec/changes/archive/2026-09-19-… with its reports and scripts. The last tasks close the loop in production:

- [x] 8.2 Verificar en vivo: headline híbrido presente, PDF ES/EN HTTP 200 con el mismo tamaño en bytes
      que los locales, `llms.txt` con "How I work"; `gh run list` con los 3 workflows en success
- [x] 8.3 … ejecutar `/opsx:verify` antes de archivar

What it decided: nothing new — and that is the point. By the time we archive, every decision is already written down somewhere that outlives the conversation with the model.

What this buys me

A change is complete when the specs, the code, the docs and the report agree — and the only way to know is to check it, not to remember it.

Browse the whole archived change here: openspec/changes/archive/2026-09-19-cv-portafolio-sre-automation ↗. Other posts: Fidello deep dive · platform-STALD ↗.

Written by Juan Valentin Alducin Vázquez · Get in touch ›