# LeadHunter — Instrucciones para generar JSONs que llenen demos al máximo

> Documento operacional para el equipo del **LeadHunter** (el sistema que caza y enriquece leads).
>
> Cuando el hunter genere un JSON para un lead, debe respetar este contrato.
> El JSON es la **única entrada de contenido** que recibe el provisionador downstream.
>
> Fecha: 2026-05-14
> Versión: v1.1 (basada en validación del primer batch real)

---

## TL;DR — Las 5 reglas

1. **Templates canónicos están en `docs/bewpro2.0/sprint-q2/templates-json/`** (9 archivos, uno por producto). Son la fuente de verdad del shape.
2. **Apply-safe**: solo sobrescribir paths que YA EXISTEN en el template. No inventar keys.
3. **Generar `services[]` específicos del lead** (no genéricos del seed). Este es el cambio que **más mueve la conversión**.
4. **Slugs únicos** dentro de cada lista (services, blog, faqs, etc.). El provisionador hace auto-dedup, pero igual venir limpio.
5. **El bloque `_meta` es del hunter**; el provisionador lo ignora pero lo conserva para trazabilidad.

> ⚠️ **Este doc es TÉCNICO** (shape, validación, pipeline). Para **estrategia de copywriting** (buyer personas, tone of voice, pain points, ejemplos malo vs bueno, system prompt para LLM), leer el doc complementario: [`leadhunter-copywriting-strategy.md`](leadhunter-copywriting-strategy.md).

---

## 1. Mapping producto bewpro → template canónico

Los 9 productos publicados, en orden de tickets-promedio:

| Slug del producto | Template canónico | Demo visual | Módulos activos | Place_types Google sugeridos |
|---|---|---|---|---|
| `restaurant-bar` | `restaurant-bar.json` | demo-restaurant | menu, gallery, blog, faqs, testimonials | `restaurant`, `cafe`, `bar`, `bakery` |
| `real-estate` | `real-estate.json` | demo-real-estate | tokko, services, gallery, blog, faqs, projects, team | `real_estate_agency` |
| `law-firm-digital` | `law-firm-digital.json` | demo-law-firm-2 | services, team, blog, references, faqs | `lawyer` |
| `construction` | `construction.json` | demo-construction | services, projects, gallery, blog, faqs | `general_contractor`, `roofing_contractor` |
| `corporative` | `corporative.json` | demo-marketing-1 | services, gallery, projects, faqs | `marketing_agency`, `consulting`, `accounting` |
| `art-design` | `art-design.json` | demo-architecture-2 | services, gallery, projects, blog, faqs | `architect`, `interior_design`, `art_gallery` |
| `creative-video-editor` | `creative-video-editor.json` | demo-creative-portfolio | gallery, projects, blog, faqs | `movie_theater`, `photographer` (creativos freelance) |
| `foundations-ong` | `foundations-ong.json` | demo-accounting-1 | services, blog, gallery, faqs | `church`, `community_center`, `non_profit_organization` |
| `personal-brand` | `personal-brand.json` | demo-accounting-2 | gallery, blog, projects, faqs | `health_consultant`, `psychologist`, `coach`, `personal_trainer` |

> **El `core` que mandás en el CSV manifest es EXACTAMENTE el slug del producto** (columna izquierda). No inventar variantes.

---

## 2. Shape canónico de cada JSON

Cada template tiene la estructura:

```jsonc
{
  "_meta": {
    "core": "law-firm-digital",
    "demo": "demo-law-firm-2",
    "active_modules": ["services", "team", "blog", "references", "faqs"],
    "generated_at": "...",
    "instructions": "..."
  },
  "site": {                          // Site-level (siempre presente)
    "name": "...",                   // Nombre del estudio/empresa
    "tagline": "...",
    "description": "...",
    "contact": { ... },
    "welcome": { ... },              // Hero del demo. VARÍA POR TEMPLATE — ver sección 4
    "about": { ... },                // Sección about del demo. VARÍA POR TEMPLATE
    "footer": { ... },
    "seo": { ... },
    "og": { ... },                   // Open Graph (FB/LinkedIn/WhatsApp preview)
    "twitter": { ... },              // Twitter card
    "social_media": { ... },         // Redes sociales (8 disponibles, ver sección 5)
    "assets": { ... }                // Logo, hero background, etc.
  },
  "service_categories": [...],       // SOLO si "services" está en active_modules
  "services": [...],                 // ídem
  "team_categories": [...],          // SOLO si "team" está en active_modules
  "team": [...],
  "blog_categories": [...],          // SOLO si "blog" está en active_modules
  "blog": [...],
  "faq_categories": [...],           // SOLO si "faqs" está en active_modules
  "faqs": [...],
  "reference_categories": [...],     // SOLO si "references" está en active_modules
  "references": [...],
  "gallery_categories": [...],       // SOLO si "gallery" está en active_modules
  "gallery_tags": [...],
  "gallery": [...],
  "project_categories": [...],       // SOLO si "projects" está en active_modules
  "project_tags": [...],
  "projects": [...],
  "menu_categories": [...],          // SOLO si "menu" está en active_modules (solo restaurant-bar)
  "menus_meta": [...],
  "menu": [...],
  "testimonial_categories": [...],   // SOLO si "testimonials" está en active_modules (solo restaurant-bar)
  "testimonials": [...]
}
```

**Regla absoluta**: NO incluir secciones que no estén en `active_modules` del template. El provisionador las ignora, pero ensucian el JSON y aumentan riesgo de errores.

---

## 3. Campos UNIVERSALES — rellenar SIEMPRE en todos los templates

Estos paths existen en los 9 templates y deben recibir data del lead:

### 3.1 `site` — top level

| Path | Source data del lead | Ejemplo |
|---|---|---|
| `site.name` | Google Places `name` | "Estudio Jurídico Pérez Hernández" |
| `site.tagline` | Generado: `"{producto_humano} en {ciudad}"` | "Abogados en Rosario" |
| `site.description` | Generado: usar `name`, `city`, prueba social | "Estudio jurídico con 30 años de trayectoria en Rosario, especializado en derecho de familia y sucesorio." |

### 3.2 `site.contact` — siempre presente

| Path | Source | Notas |
|---|---|---|
| `site.contact.address` | Google `formatted_address` | tal cual |
| `site.contact.phone` | Google `formatted_phone_number` | tal cual |
| `site.contact.email` | Scrapeado del sitio o placeholder | si vacío → dejar `""` (el provisionador maneja) |
| `site.contact.city` | Ciudad detectada | "Rosario" |

### 3.3 `site.seo` — siempre presente

| Path | Source |
|---|---|
| `site.seo.keywords` | Combinar: `name` + categoría producto + ciudad + keywords del producto |
| `site.seo.meta_description` | = `site.description` (truncar a 160 chars si es largo) |

### 3.4 `site.og` — Open Graph

| Path | Source |
|---|---|
| `site.og.title` | `name` |
| `site.og.description` | `site.description` (160 chars) |
| `site.og.image` | `og_image` scrapeado del sitio del lead → si no, dejar default del template |
| `site.og.image_alt` | `name` |
| `site.og.site_name` | `name` |
| `site.og.url` | `https://{subdomain}.bewpro.com` |
| `site.og.type` | `"website"` (no tocar) |
| `site.og.locale` | `"es_AR"` (no tocar para LATAM) |

### 3.5 `site.twitter` — Twitter card

| Path | Source |
|---|---|
| `site.twitter.title` | `name` |
| `site.twitter.description` | `site.description` (160 chars) |
| `site.twitter.image` | = `site.og.image` |
| `site.twitter.image_alt` | = `site.og.image_alt` |
| `site.twitter.card` | `"summary_large_image"` (no tocar) |

> ⚠️ **DEPRECATED**: `site.twitter.handle` ya NO existe en el canónico v2026-05-14. NO lo agregues. Si tu template tenía esa key, removela.

### 3.6 `site.assets` — branding

| Path | Source |
|---|---|
| `site.assets.main_logo` | `logo_url` scrapeado → si no, default del template |
| `site.assets.main_logo_alt` | `name` |
| `site.assets.main_logo_sticky` | = `main_logo` |
| `site.assets.footer_logo` | = `main_logo` |
| `site.assets.loader_logo` | = `main_logo` |
| `site.assets.hero_background` | `og_image` scrapeado → si no, default del template |
| `site.assets.favicon` | derivar de `logo_url` si es razonable, sino default |

### 3.7 `site.footer` — pie de página

| Path | Source |
|---|---|
| `site.footer.description` | = `site.description` (versión corta, ~100 chars) |
| `site.footer.copyright_text` | `"© {year} {name}. Todos los derechos reservados."` |
| `site.footer.address` | = `site.contact.address` |
| `site.footer.phone` | = `site.contact.phone` |
| `site.footer.email` | = `site.contact.email` |

---

## 4. Campos `site.welcome` y `site.about` — VARÍAN POR TEMPLATE

Cada demo tiene su propio set de keys para hero (welcome) y about. **Solo rellenar las keys que EXISTEN en el template correspondiente.**

### Cómo descubrir qué keys tiene cada template

```python
import json
template = json.load(open('templates-json/law-firm-digital.json'))
welcome_keys = list(template['site'].get('welcome', {}).keys())
about_keys   = list(template['site'].get('about', {}).keys())
print('welcome:', welcome_keys)
print('about:',   about_keys)
```

### Resumen por template (keys más comunes — chequear cada template para el set exacto)

| Template | Keys en `site.welcome` (típicas) |
|---|---|
| law-firm-digital | `hero_title`, `hero_subtitle`, `hero_description`, `hero_eyebrow`, `hero_cta_label` |
| restaurant-bar | `hero_title`, `hero_subtitle`, `hero_description`, `hero_eyebrow`, `hero_label` |
| real-estate | `hero_title`, `hero_subtitle`, `hero_cta_label`, `hero_search_placeholder` |
| construction | `hero_title`, `hero_subtitle`, `hero_description`, `hero_eyebrow` |
| corporative | `hero_title`, `hero_subtitle`, `hero_description`, `hero_eyebrow` |
| art-design | `hero_title`, `hero_subtitle`, `hero_description`, `hero_eyebrow`, `story_text`, `story_heading` |
| creative-video-editor | `hero_title`, `hero_subtitle`, `hero_description`, `about_text` |
| foundations-ong | `hero_title`, `hero_subtitle`, `hero_description`, `mission_text` |
| personal-brand | `hero_title`, `hero_subtitle`, `about_lead`, `about_description`, `about_author` |

> **Regla**: si el path no existe en el template, NO lo agregues. El demo no lo va a renderear.

### Reglas de contenido por hero

- **`hero_title`**: usar `name` del lead + adjetivo profesional. Max 60 chars.
  - Ej: "Pérez Hernández — Estudio Jurídico"
- **`hero_subtitle`**: claim corto + ciudad. Max 100 chars.
  - Ej: "30 años defendiendo familias en Rosario."
- **`hero_description`**: 1-2 oraciones con `name`, especialidad y prueba social. Max 250 chars.
- **`hero_eyebrow`**: tag corto, 3 palabras max.
  - Ej: "Asesoramiento legal" / "Defensa familiar" / "Especialistas"
- **`hero_cta_label`**: acción específica.
  - Ej: "Agendá tu consulta" / "Conocé los servicios" / "Reservá ahora"

---

## 5. `site.social_media` — 8 redes, activar solo las relevantes

Cada red tiene este shape:

```json
"instagram": {
  "url": "",
  "icon": "fab fa-instagram",
  "title": "Instagram",
  "active": 0
}
```

Las 8 redes con sus icon-class de FontAwesome (no tocar el icon):

| Red | `icon` | `title` |
|---|---|---|
| linkedin | `fab fa-linkedin-in` | `LinkedIn` |
| instagram | `fab fa-instagram` | `Instagram` |
| facebook | `fab fa-facebook-f` | `Facebook` |
| x | `fab fa-x-twitter` | `X` |
| youtube | `fab fa-youtube` | `Youtube` |
| whatsapp | `fab fa-whatsapp` | `Whatsapp` |
| tiktok | `fab fa-tiktok` | `Tiktok` |
| pinterest | `fab fa-pinterest` | `Pinterest` |

### Reglas

1. Si scrapeás `website` del lead y matcheás un dominio de red social → **activar** esa red:
   - `url` ← la URL real
   - `active` ← `1`
2. Si no la encontrás, dejarla con `url: ""` y `active: 0` (no remover la entry — debe existir aunque inactiva).
3. **`whatsapp`** especial: si tenés `phone` formateado, generar `https://wa.me/{phone_sin_+}` y activar.
4. **`active` es INT 0/1** (no boolean `true/false`). El provisionador lo acepta, pero por consistencia: `0/1`.

---

## 6. `services[]` — el cambio MÁS importante para conversión

Hoy el hunter NO genera `services[]` específicos. Pasa array vacío o con copy genérica → el provisionador queda con los 4 services placeholder del seed canónico → todos los demos del mismo core se ven idénticos.

**Pedido**: generar 3-5 services específicos del lead usando `place_types` + `keywords` del producto.

### Shape de cada item

> ⚠️ **IMPORTANTE — schema canónico** (corregido 2026-05-14 tras incidente con campaign 4):
> El comando `bewpro:seed` del provisionador espera `title`, NO `name`. También `category_slug`, NO `service_category_slug`. Y `sort_order`, NO `order`.

```json
{
  "slug": "derecho-penal",
  "title": "Defensa Penal",
  "subtitle": "Acompañamiento desde la primera audiencia",
  "description": "Representación legal en causas penales. Asesoramiento desde el primer momento de la imputación.",
  "icon": "fas fa-balance-scale",
  "category_slug": "areas-de-practica",
  "is_active": 1,
  "sort_order": 1
}
```

> El provisionador tiene un **JSON normalizer** que mapea `name→title`, `service_category_slug→category_slug`, `order→sort_order` defensive. Pero por consistencia, usá el shape canónico de arriba.

### Cómo derivar services del catálogo

En `hunter-catalog.json`, agregar un campo `services_template` por producto con la lista de services candidatos por industria. Ejemplo:

```json
{
  "products": {
    "juridico": {
      "core": "law-firm-digital",
      "services_template": [
        {"slug": "derecho-familia", "title": "Derecho de Familia", "description": "Divorcios, alimentos, régimen de visitas, adopciones.", "icon": "fas fa-home", "keywords_match": ["familia", "divorcio", "alimentos"]},
        {"slug": "derecho-penal", "title": "Defensa Penal", "description": "Representación legal en causas penales.", "icon": "fas fa-balance-scale", "keywords_match": ["penal", "criminal", "accidentes"]},
        {"slug": "derecho-sucesorio", "title": "Sucesiones", "description": "Trámites sucesorios, testamentos, particiones.", "icon": "fas fa-scroll", "keywords_match": ["sucesoria", "herencia", "testamento"]},
        {"slug": "derecho-laboral", "title": "Derecho Laboral", "description": "Indemnizaciones, despidos, accidentes laborales.", "icon": "fas fa-briefcase", "keywords_match": ["laboral", "trabajo", "despido"]},
        {"slug": "derecho-comercial", "title": "Derecho Comercial", "description": "Sociedades, contratos, conflictos comerciales.", "icon": "fas fa-handshake", "keywords_match": ["comercial", "societario", "empresas"]}
      ]
    }
  }
}
```

### Algoritmo sugerido

```python
def generate_services(lead, product_config):
    template = product_config['services_template']
    lead_text = ' '.join([
        lead['name'].lower(),
        lead.get('description', '').lower(),
        ' '.join(lead.get('keywords', [])).lower()
    ])

    # Score cada service candidato por keyword match
    scored = []
    for svc in template:
        score = sum(1 for kw in svc.get('keywords_match', []) if kw in lead_text)
        scored.append((score, svc))

    scored.sort(key=lambda x: -x[0])

    # Tomar top 3-5: los matched primero, completar con defaults si quedan slots
    selected = [s for score, s in scored if score > 0][:5]
    if len(selected) < 3:
        # Completar con los primeros del template
        selected += [s for _, s in scored if s not in selected][:3 - len(selected)]

    return [{
        'slug': s['slug'],
        'name': s['name'],
        'description': s['description'],
        'icon': s.get('icon', 'fas fa-circle'),
        'image': None,
        'service_category_slug': 'areas-de-practica',  # depende del template
        'is_active': 1,
        'order': i + 1
    } for i, s in enumerate(selected)]
```

> **Service category slug** depende del template. Para law-firm-digital es `areas-de-practica`. Mirar `service_categories[].slug` del template canónico para cada caso.

### Templates `services_template` propuestos por producto

(Estructura mínima — el equipo del hunter expande con descriptions ricas)

| Producto | services_template (slugs propuestos) |
|---|---|
| `law-firm-digital` | derecho-familia, derecho-penal, derecho-sucesorio, derecho-laboral, derecho-comercial |
| `restaurant-bar` | (no aplica — usa `menu[]`) |
| `real-estate` | venta-propiedades, alquiler, tasaciones, gestion-locativa, asesoramiento |
| `construction` | obras-civiles, refacciones, direccion-de-obra, proyecto-arquitectonico, instalaciones |
| `corporative` | consultoria, capacitacion, auditoria, gestion-cambio, transformacion-digital |
| `art-design` | diseno-interiores, arquitectura, proyecto-comercial, asesoramiento-decoracion |
| `creative-video-editor` | (no services — usa `projects[]` con video work) |
| `foundations-ong` | programas-comunitarios, voluntariado, donaciones, eventos-recaudacion |
| `personal-brand` | coaching, consultoria-1a1, charlas, talleres, mentoria |

---

## 7. `team[]` — solo law-firm-digital y real-estate

Templates que tienen `team` activo: `law-firm-digital`, `real-estate`.

**Hoy**: hunter NO genera team. Provisionador usa default genérico.

**Pedido**: no generar team automático (ético-borderline desde reviewers Google). En su lugar, dejar `team` vacío `[]` y que el reseller lo complete después en el panel admin. NO inventar nombres ni fotos.

---

## 8. `blog[]`, `faqs[]`, `references[]`, `gallery[]`, `projects[]`

Si el hunter no tiene fuentes específicas, **dejar los items del seed canónico tal cual** (3-4 items placeholder por sección). El provisionador los va a seedear igual; al menos el demo se ve "lleno".

### Generables con bajo riesgo

| Sección | Cómo generar |
|---|---|
| `faqs[]` | 3-5 preguntas genéricas por industria + 1-2 con copy contextualizada al `name` y `city` |
| `references[]` | Si Google tiene reviews 4★+ → tomar 2-3, anonimizar nombre del reviewer ("María G.") |
| `gallery[]` | Si Google Places tiene fotos → 4-8 con URLs estables (cachear vía CDN) |

### NO generables (dejar default del template)

- `blog[]` — requiere copy real; usar default
- `projects[]` — requiere portfolio real; usar default

---

## 9. `restaurant-bar` — caso especial con `menu[]`

Si el producto es `restaurant-bar`, NO va `services[]`. En su lugar:

### Estructura `menu[]`

```json
{
  "slug": "milanesa-napolitana",
  "name": "Milanesa Napolitana",
  "description": "Milanesa de ternera, jamón, queso, salsa de tomate. Acompañada con papas.",
  "price": 8500,
  "menu_category_slug": "principales",
  "is_active": 1,
  "order": 1,
  "image": null
}
```

### Algoritmo

Si el hunter NO scrapea menus reales, dejar 8-12 items genéricos del template canónico. Si en el futuro extiende para parsear sitios con menus → reemplazar.

`menu_categories[]` por defecto: `entradas`, `principales`, `postres`, `bebidas`, `pasteleria`.

---

## 10. Reglas técnicas — apply-safe

### 10.1 Slugs únicos dentro de cada lista

Cada item de `services`, `blog`, `faqs`, `team`, `references`, `projects`, `gallery`, `menu`, `testimonials` debe tener `slug` ÚNICO **dentro de su lista** (no globalmente).

El provisionador hace auto-dedup (sufija `-2`, `-3`) pero es mejor venir limpio.

### 10.2 URLs absolutas para imágenes

- `site.assets.main_logo`, `site.og.image`, `site.twitter.image` → URLs absolutas `https://...`
- Imágenes de `gallery[]`, `projects[]`, `services[].image` → URLs absolutas o `null`

NO usar paths relativos como `cd-project/img/...` (esos son defaults del demo; si querés mantenerlos, mejor dejar el `null` y que el seed canónico llene).

### 10.3 Encoding y formato

- UTF-8 sin BOM
- `JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES` (equivalente Python: `indent=2, ensure_ascii=False`)
- Filename: `<slug>__<core>.json` (doble underscore separa)

### 10.4 Validación pre-export

El hunter debería ejecutar antes de exportar:

```python
def validate_lead_json(payload, expected_core):
    meta = payload.get('_meta', {})
    assert meta.get('core') == expected_core, f"core mismatch: {meta.get('core')} != {expected_core}"

    # Site obligatorio
    site = payload.get('site', {})
    assert site.get('name'), "site.name missing"
    assert site.get('tagline'), "site.tagline missing"
    assert site.get('description'), "site.description missing"
    assert site.get('contact', {}).get('address'), "site.contact.address missing"

    # active_modules consistency
    for mod in meta.get('active_modules', []):
        assert mod in payload or mod in ('tokko',), f"active_module '{mod}' missing in JSON sections"

    # Slugs únicos por sección
    for section in ['services', 'blog', 'faqs', 'team', 'references', 'projects', 'gallery', 'menu', 'testimonials']:
        if section not in payload: continue
        slugs = [item.get('slug') for item in payload[section] if isinstance(item, dict) and item.get('slug')]
        assert len(slugs) == len(set(slugs)), f"Duplicate slugs in {section}: {slugs}"

    return True
```

---

## 11. Pipeline downstream — qué hace el provisionador con tu JSON

```
1. Hunter genera 13 JSONs + manifest.csv
   ↓
2. Operador (Coke) corre:
   php artisan bewpro:bulk-provision manifest.csv \
     --json-dir=<campaign_dir> \
     --reseller-email=lacompaniad@gmail.com \
     --override-email=coke@lacompaniadigital.com
   ↓
3. Provisionador:
   - Auto-dedup slugs duplicados (no debería haber, pero defensive)
   - Crea Airtable Project record con Pipeline_Status=Required
   - Stashea tu JSON en storage/app/reseller-imports/{airtable_id}.json
   ↓
4. Cron VPS1 (cada 1 min):
   - Detecta Required → provisiona tenant en VPS1/2/3 con HestiaCP
   - Corre bewpro:seed CANÓNICO (placeholder default del core)
   - Marca Pipeline_Status=On Development
   ↓
5. Cron VPS1 apply-pending (cada 1 min):
   - Detecta el stash JSON
   - Auto-detect en qué VPS vive el tenant
   - SCP -O del JSON al tenant
   - DELETE de tablas que el JSON va a reemplazar
   - su -s /bin/bash → bewpro:seed TU JSON
   - Marca Airtable Import_Status=Completed
   - Borra el stash
   ↓
6. Tenant queda live con TU data en https://{subdomain}.bewpro.com
```

**Tiempo total**: ~25-30min por tenant (provisión) + ~1min (seed del JSON). Batch de 13 → ~5h secuencial.

---

## 12. Cómo refrescar tus templates

Cuando el provisionador cambia `CoreTemplateBuilder.php` o un seed canónico:

```bash
# Desde el repo cd-system
cd /Applications/XAMPP/xamppfiles/htdocs/cd-system
php -r '
require "vendor/autoload.php";
$app = require_once "bootstrap/app.php";
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$b = new App\Services\CoreTemplateBuilder();
foreach ($b->availableCores() as $core) {
    file_put_contents(
        "docs/bewpro2.0/sprint-q2/templates-json/{$core}.json",
        json_encode($b->build($core), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
    );
}
'
git add docs/bewpro2.0/sprint-q2/templates-json/ && git commit && git push
```

Después el hunter hace `git pull` o `curl` a la URL raw de GitHub.

**Frecuencia esperada**: cambios al template ~1x por mes. Cuando pase, el hunter debería refrescar para no quedar drifteado.

---

## 13. Diferencias confirmadas en validación 2026-05-14

El primer batch real (13 leads law-firm-digital) reveló:

| Aspecto | Status |
|---|---|
| Shape canónico | ✅ Hunter rellena 239/239 keys del template |
| Universals (site.*) | ✅ Bien rellenados |
| `social_media` activación correcta | ✅ Bien |
| `services[]` específicos del lead | ❌ NO generados — todos los demos muestran services placeholders |
| `team[]` | ✅ No generado (correcto, ético-borderline) |
| `_meta.lead_snapshot` | ✅ Útil para trazabilidad |
| Subdomain cap | ⚠️ Hunter trunca a 30; provisionador soporta 48 |
| `site.twitter.handle` | ⚠️ Hunter agrega, ya NO existe en canónico v2026-05-14 |

---

## 14. Checklist de cambios pedidos al hunter

- [ ] Subir cap de subdomain de 30 → 48 chars
- [ ] Agregar columna `priority` al manifest CSV (= `qualified_for_demo`)
- [ ] Agregar columna `tier` al manifest CSV (`caliente`/`tibio`/`frío`)
- [ ] Remover `site.twitter.handle` del JSON (deprecated)
- [ ] **🔥 Implementar `services_template` por producto en `hunter-catalog.json`** (sección 6)
- [ ] Implementar algoritmo de generación de `services[]` por lead (sección 6)
- [ ] Validar shape pre-export con el script de sección 10.4
- [ ] Refrescar templates con la versión canónica del 2026-05-14
- [ ] (Opcional) Generar `gallery[]` con URLs CDN de fotos Google Places
- [ ] (Opcional) Aplicar `theme_color` scrapeado a `site.brand_defaults`

---

## 15. Referencias

- Templates canónicos: `docs/bewpro2.0/sprint-q2/templates-json/*.json` (9 archivos)
- README templates: `docs/bewpro2.0/sprint-q2/templates-json/README.md`
- Generador: `app/Services/CoreTemplateBuilder.php`
- Provisionador comando: `app/Console/Commands/BulkProvisionProjects.php`
- Doc operacional: `docs/bewpro2.0/sprint-q2/bulk-provisioning.md`
- Feedback contrato v1: `docs/bewpro2.0/sprint-q2/leadhunter-contract-feedback.md`
- Postmortem fix HestiaCP: `docs/bewpro2.0/sprint-q2/hestiacp-import-migration.md`
- Hunter local: `/Users/cokecolombres/Downloads/leadhunter/`

---

**Mantenido por**: Provisionador bewpro
**Para**: Equipo LeadHunter
**Última actualización**: 2026-05-14
