docs: documentacion completa para humanos y agentes IA

- README reescrito: ejemplos CLI corregidos (flags van en el subcomando scrape),
  webapp+doctor documentados, requisitos por OS, cookies, datos generados, migracion de maquina
- CLAUDE.md actualizado: conteo de tests, refs de linea reparadas, scripts nuevos,
  checklist de actualizacion de docs
- AGENTS.md nuevo: entrada estandar para cualquier agente de codigo (mapa de modulos,
  comandos, reglas de oro, punteros a docs)
- docs/GETTING-STARTED.md: de cero a webapp por OS con troubleshooting
- docs/CONFIG.md: referencia completa de las ~25 claves de config
- docs/COOKIES.md: adquisicion, import (web/navegador/CLI), rotacion y seguridad
- docs/backlog.md reemplaza OPPORTUNITIES.md (distilado: shipped vs pendiente)
- auditorias movidas a docs/audits/ y CLIPPER-COMPARISON-AUDIT.md versionado
This commit is contained in:
urieljareth
2026-09-10 00:32:32 -06:00
parent 3c49f73fa2
commit 450aee216d
11 changed files with 791 additions and 454 deletions
+28 -10
View File
@@ -11,7 +11,8 @@ Plataforma **100% local** de minería de contenido de creadores de YouTube: `yt-
```bash
pip install -e ".[dev,web,analysis]" # Python >= 3.10; ffmpeg es requisito del sistema para audio
python -m pytest tests/ -q # suite completa (61 tests, sin red)
python -m pytest tests/ -q # suite completa (~240 tests en 24 archivos, sin red)
uv run python -m pytest -q # equivalente si usas uv
python -m pytest tests/test_store_platform.py -v # un archivo
python -m pytest tests/test_store_platform.py::test_dashboard_aggregates -v # un test
python -m pytest -m "not integration" # marker declarado en pyproject (aún sin uso)
@@ -23,16 +24,20 @@ yt-scraper re-render --backfill # reimportar .md a la DB y regenerar Ma
start-server.bat # webapp: busca puerto libre 8000-8100, arranca uvicorn, abre browser
stop-server.bat # mata el proceso registrado en .run/server.info y libera el puerto
scripts/doctor.ps1 # (primera vez, lo llama start-server.bat) instala Python 3.12 + deps si faltan
make setup / make serve / make stop / make test / make clean # macOS/Linux vía Makefile
scripts/bootstrap.sh # macOS/Linux: prepara python/ffmpeg/node (brew) + venv + extras
scripts/start-server.sh / scripts/stop-server.sh # equivalentes bash de los .ps1
python -m uvicorn yt_scraper.webapp.app:app --port 8765 # arranque manual (debug)
```
No hay linter ni formatter configurado. Los `.bat` de la raíz solo envuelven `scripts/start-server.ps1` / `scripts/stop-server.ps1`.
No hay linter ni formatter configurado. Los `.bat` de la raíz solo envuelven `scripts/*.ps1` (`start-server.bat` pasa primero por `scripts/doctor.ps1`); en macOS/Linux el equivalente son `scripts/bootstrap.sh` + `scripts/start-server.sh` / `stop-server.sh`, expuestos en el `Makefile` raíz.
**Ojo:** `yt-scraper` sin subcomando ejecuta un scrape completo del canal de `config.yaml` (`invoke_without_command=True` en [cli.py:39](src/yt_scraper/cli.py#L39)).
**Ojo:** `yt-scraper` sin subcomando ejecuta un scrape completo del canal de `config.yaml` (`invoke_without_command=True` en [cli.py:40](src/yt_scraper/cli.py#L40)).
## Orientación antes de leer código
Existe un grafo de conocimiento en [graphify-out/](graphify-out/) y un hook que exige usarlo antes de leer/grepear fuentes: `graphify query "<pregunta>"`, `graphify explain "<concepto>"`, `graphify path "<A>" "<B>"`. Si el grafo está desactualizado respecto a los archivos, `graphify update`. [graphify-out/GRAPH_REPORT.md](graphify-out/GRAPH_REPORT.md) resume comunidades y god nodes (`Store` es el más conectado con diferencia).
Existe un grafo de conocimiento en [graphify-out/](graphify-out/) (gitignored, regenerable). Recomendado —no obligatorio, no es un hook del repo— consultarlo antes de leer/grepear fuentes: `graphify query "<pregunta>"`, `graphify explain "<concepto>"`, `graphify path "<A>" "<B>"`. Si el grafo está desactualizado respecto a los archivos, `graphify update`. [graphify-out/GRAPH_REPORT.md](graphify-out/GRAPH_REPORT.md) resume comunidades y god nodes (`Store` es el más conectado con diferencia).
## Arquitectura
@@ -64,7 +69,7 @@ SQLite sin ORM, `sqlite3.Row`, WAL, `foreign_keys=ON`. Cada método abre y cierr
### El Markdown es fuente de datos, no solo salida
`segments.backfill_from_markdown()` parsea `data/markdown/**/*.md` de vuelta a la DB (idempotente, salta vídeos que ya tienen segmentos). Se invoca al arrancar la webapp ([webapp/app.py:31](src/yt_scraper/webapp/app.py#L31)) y con `re-render --backfill`.
`segments.backfill_from_markdown()` parsea `data/markdown/**/*.md` de vuelta a la DB (idempotente, salta vídeos que ya tienen segmentos). Se invoca al arrancar la webapp (el reconcile de arranque corre en un hilo de fondo, [webapp/app.py:47-53](src/yt_scraper/webapp/app.py#L47)) y con `re-render --backfill`.
Eso convierte el formato del `.md` en un **contrato bidireccional**: [templates/video.md.j2](templates/video.md.j2) escribe `**MM:SS** · texto` y `### Título (MM:SS)`; los regex `_SEG_LINE` / `_CHAPTER` / `_FRONTMATTER_KEY` de [segments.py:17](src/yt_scraper/segments.py#L17) los leen. Si tocas la plantilla, actualiza los regex en el mismo cambio o el backfill se rompe en silencio.
@@ -123,7 +128,7 @@ Los tres se encontraron auditando la base viva, no razonando:
- **El desempate global rankeaba por tamaño de catálogo.** `channel_seq` cuenta hasta el número de vídeos del canal, así que compararlo **entre** canales ordena por quién tiene más. Con el **92 %** de los pares adyacentes empatados en `sort_date`, eso decidía casi toda la lista: un canal entero delante de otro solo porque 575 > 476. El desempate es ahora `channel_id` **y luego** `channel_seq`, de modo que cada bloque empatado queda contiguo por canal y el orden interno —lo que tiene que cuadrar con YouTube— no se toca. Verificado: 373 bloques de empate, **0** con un canal partido.
- **Una fila sin rank no queda desordenada, queda mal colocada.** `COALESCE(channel_seq, -1)` hace que la regla 2 herede el vídeo datado **más viejo** del canal, así que un vídeo que discovery acaba de encontrar —de los más nuevos— se muestra el último. Medido: dos subidas nuevas de Hormozi con `sort_date=20180720`, penúltima y última de 513. Pasa cuando un server de larga vida sigue con el código previo a la columna después de migrar. `_rank_unranked()` corre **siempre**, no solo al añadir la columna, y es no-op si no hay NULLs.
**Ojo al importar `yt_scraper.webapp.app`:** tiene `app = create_app()` a nivel de módulo ([app.py:102](src/yt_scraper/webapp/app.py#L102)), así que **el simple `import` abre la base real del proyecto** vía `config.yaml`, corre la migración, `auto_import_dir` y `reconcile_markdown` — aunque después le pases un `Config` distinto a `create_app()`. Para tocar solo una copia, importa `yt_scraper.store` / `yt_scraper.discover` directamente y nunca `webapp.app`.
**Ojo al importar `yt_scraper.webapp.app`:** tiene `app = create_app()` a nivel de módulo ([app.py:115](src/yt_scraper/webapp/app.py#L115)), así que **el simple `import` abre la base real del proyecto** vía `config.yaml`, corre la migración, `auto_import_dir` y lanza el reconcile en un hilo de fondo — aunque después le pases un `Config` distinto a `create_app()`. Para tocar solo una copia, importa `yt_scraper.store` / `yt_scraper.discover` directamente y nunca `webapp.app`.
### El botón de `.md` descarga `.md`
@@ -131,7 +136,7 @@ Los tres se encontraron auditando la base viva, no razonando:
### Estados terminales y frescura (la UI tiene que reflejar DB + disco)
`no_subtitles` **no** significa "este vídeo no tiene subtítulos". Se escribe siempre que `data.segments` viene vacío ([pipeline.py:52](src/yt_scraper/pipeline.py#L52)), lo que mezcla tres causas muy distintas: el vídeo no tiene pistas, la política de idiomas rechazó las que sí tiene, o la descarga vino vacía por throttling. `extract.describe_missing_subtitle()` distingue los casos y el motivo se guarda en `videos.error_msg` vía `mark_status(vid, status, reason)`.
`no_subtitles` **no** significa "este vídeo no tiene subtítulos". Se escribe siempre que `data.segments` viene vacío ([pipeline.py:146](src/yt_scraper/pipeline.py#L146)), lo que mezcla tres causas muy distintas: el vídeo no tiene pistas, la política de idiomas rechazó las que sí tiene, o la descarga vino vacía por throttling. `extract.describe_missing_subtitle()` distingue los casos y el motivo se guarda en `videos.error_msg` vía `mark_status(vid, status, reason)`.
Incidente que motivó esto: 511 vídeos de un canal quedaron en `no_subtitles` porque `config.example.yaml` ponía los idiomas en modo `manual` y el canal solo publica subtítulos automáticos. `_sources_for("manual")` no hace fallback. El default es ahora `any` (manual primero, auto después) — `manual` es opt-in explícito.
@@ -270,7 +275,20 @@ Solo `tmp_path` + `monkeypatch`, cero red: `yt_dlp.YoutubeDL` se sustituye por u
## Documentos de referencia
- [AGENTS.md](AGENTS.md) — guía de entrada para cualquier agente de código (pitch, mapa de módulos, reglas de oro, punteros a docs).
- [docs/superpowers/specs/2026-07-26-platform-design.md](docs/superpowers/specs/2026-07-26-platform-design.md) — diseño de referencia (esquema, endpoints, alcance, exclusiones). Es la fuente de verdad de las decisiones; donde el código difiere, gana el código.
- [OPPORTUNITIES.md](OPPORTUNITIES.md) — backlog de features con esfuerzo estimado.
- [YOUTUBE-TRANSCRIPT-AUDIT.md](YOUTUBE-TRANSCRIPT-AUDIT.md) — ingeniería inversa de Obsidian Web Clipper (mecanismo InnerTube). Contexto de *por qué* `yt-dlp` es la ruta elegida.
- `data/`, `cookies/`, `.run/` están gitignored y contienen datos reales (883 vídeos, sesión de YouTube). No los commitees ni pegues su contenido en respuestas.
- [docs/backlog.md](docs/backlog.md) — backlog destilado: lo pendiente y qué ya está shipped (sustituye al histórico OPPORTUNITIES.md).
- [docs/audits/](docs/audits/) — auditorías: ingeniería inversa del mecanismo InnerTube del Obsidian Web Clipper (`YOUTUBE-TRANSCRIPT-AUDIT.md`, contexto de *por qué* `yt-dlp` es la ruta elegida) y comparativa extensión vs scraper (`CLIPPER-COMPARISON-AUDIT.md`).
- `docs/GETTING-STARTED.md`, `docs/CONFIG.md`, `docs/COOKIES.md` — docs de usuario (máquina nueva, referencia de config, guía de cookies).
- `data/`, `cookies/`, `.run/` están gitignored y contienen datos reales (sesión de YouTube). No los commitees ni pegues su contenido en respuestas. `config.yaml` ya no está versionado (config privada del usuario; la plantilla versionada es `config.example.yaml`).
## Actualización de docs
Checklist rápida al aterrizar un cambio — las docs desactualizadas mienten peor que no existir:
- **Flags/subcomandos del CLI** nuevos o cambiados → `README.md` (sección CLI) y ejemplos de `docs/GETTING-STARTED.md`.
- **Plantilla `templates/video.md.j2`** → este archivo (el contrato bidireccional con `segments.py`) y el ejemplo de nota del `README.md`.
- **Endpoints de la webapp o UI** → `README.md` (sección webapp) y las secciones *Job runner* / *Frontend* de aquí.
- **Clave de config nueva o default distinto** → `config.example.yaml`, `docs/CONFIG.md` y el resumen de bloques del `README.md`.
- **Números que caducan** (cantidad de tests, refs `archivo.py:NN`) → usa órdenes de magnitud (`~240 tests`) y re-verifica las refs de línea antes de citarlas.
- **Feature que estaba en el backlog** → márcala como shipped en `docs/backlog.md`; si añade un módulo, actualiza el mapa de `AGENTS.md`.