diff --git a/bots/bot_descarga.zip b/bots/bot_descarga.zip new file mode 100644 index 0000000..1d825be Binary files /dev/null and b/bots/bot_descarga.zip differ diff --git a/docker-compose.yml b/docker-compose.yml index 31f331f..a7280eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,12 +52,13 @@ services: environment: DATABASE_URL: postgresql+psycopg://ipsextractor:ipsextractor@database:5432/ipsextractor REDIS_URL: redis://redis:6379/0 - EXTRACTOR_BASE_URL: https://api.openai.com/v1 - EXTRACTOR_API_KEY: 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 + ASYNC_QUEUE: ${ASYNC_QUEUE:-false} + EXTRACTOR_BASE_URL: ${EXTRACTOR_BASE_URL:-https://api.openai.com/v1} + EXTRACTOR_API_KEY: ${EXTRACTOR_API_KEY} EXTRACTOR_MODEL: ${EXTRACTOR_MODEL:-gpt-5.4-mini} - NORMALIZER_BASE_URL: https://api.openai.com/v1 - NORMALIZER_API_KEY: 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 - NORMALIZER_MODEL: gpt-5.4-mini + NORMALIZER_BASE_URL: ${NORMALIZER_BASE_URL:-${EXTRACTOR_BASE_URL:-https://api.openai.com/v1}} + NORMALIZER_API_KEY: ${NORMALIZER_API_KEY:-${EXTRACTOR_API_KEY}} + NORMALIZER_MODEL: ${NORMALIZER_MODEL:-gpt-5.4-mini} EXTRACTOR_TIMEOUT_S: ${EXTRACTOR_TIMEOUT_S:-120} PDF_DPI: ${PDF_DPI:-150} PDF_THREAD_COUNT: ${PDF_THREAD_COUNT:-4} @@ -91,19 +92,26 @@ services: environment: DATABASE_URL: postgresql+psycopg://ipsextractor:ipsextractor@database:5432/ipsextractor REDIS_URL: redis://redis:6379/0 - EXTRACTOR_BASE_URL: https://api.openai.com/v1 - EXTRACTOR_API_KEY: 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 + ASYNC_QUEUE: ${ASYNC_QUEUE:-false} + EXTRACTOR_BASE_URL: ${EXTRACTOR_BASE_URL:-https://api.openai.com/v1} + EXTRACTOR_API_KEY: ${EXTRACTOR_API_KEY} EXTRACTOR_MODEL: ${EXTRACTOR_MODEL:-gpt-5.4-mini} - NORMALIZER_BASE_URL: https://api.openai.com/v1 - NORMALIZER_API_KEY: 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 - NORMALIZER_MODEL: gpt-5.4-mini + NORMALIZER_BASE_URL: ${NORMALIZER_BASE_URL:-${EXTRACTOR_BASE_URL:-https://api.openai.com/v1}} + NORMALIZER_API_KEY: ${NORMALIZER_API_KEY:-${EXTRACTOR_API_KEY}} + NORMALIZER_MODEL: ${NORMALIZER_MODEL:-gpt-5.4-mini} EXTRACTOR_TIMEOUT_S: ${EXTRACTOR_TIMEOUT_S:-120} PDF_DPI: ${PDF_DPI:-150} PDF_THREAD_COUNT: ${PDF_THREAD_COUNT:-4} - WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-25} + CONCURRENCY_LIMIT: ${CONCURRENCY_LIMIT:-50} ALLOWED_DOCUMENT_TYPES: ${ALLOWED_DOCUMENT_TYPES:-CC,TI,CE,RC,PA,PPT,PEP,EPT,NUIP} + WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-4} volumes: - ips_queue:/tmp/ips_queue + # Código montado para que fixes del worker (p.ej. FOR UPDATE) apliquen sin rebuild. + - ./services/extraction/worker:/app/worker + - ./services/extraction/domain:/app/domain + - ./services/extraction/core:/app/core + - ./services/extraction/infrastructure:/app/infrastructure cleaning: build: diff --git a/docs/plan_pruebas/README.md b/docs/plan_pruebas/README.md index cc5bb40..7a8c823 100644 --- a/docs/plan_pruebas/README.md +++ b/docs/plan_pruebas/README.md @@ -1,26 +1,36 @@ -# Plan / Informe de pruebas IPSExtractor +# Plan / Informe / Capacidad — IPSExtractor -## Entregable principal +Tres documentos LaTeX **para compartir**: -**Informe de Pruebas (resultados):** [`InformePruebas_IPSExtractor.pdf`](InformePruebas_IPSExtractor.pdf) -Fuentes: [`informe/`](informe/) +| PDF | Fuentes | Contenido | +|-----|---------|-----------| +| [`PlanPruebas_IPSExtractor.pdf`](PlanPruebas_IPSExtractor.pdf) | [`main.tex`](main.tex) | Estrategia, arquitectura, catálogo UT/IT/E2E/ST | +| [`InformePruebas_IPSExtractor.pdf`](InformePruebas_IPSExtractor.pdf) | [`informe/`](informe/) | Resultados, métricas, interpretación | +| [`Capacidad100k_IPSExtractor.pdf`](Capacidad100k_IPSExtractor.pdf) | [`capacidad_100k/`](capacidad_100k/) | Modelo 100k/mes, coste LLM, veredicto | -Incluye métricas, gráficas, scorecard y análisis de viabilidad de **100.000 facturas/mes**. - -## Plan histórico (referencia) - -El documento de plan de pruebas sigue en esta carpeta (`main.tex` → `PlanPruebas_IPSExtractor.pdf`) como referencia de metodología/IDs. El foco de QA es el **informe**. - -## Campañas - -- [`informes/2026-07-13-shared8g.md`](informes/2026-07-13-shared8g.md) -- Scripts k6: [`scripts/`](scripts/) - -## Compilar informe +## Compilar los tres ```powershell -cd docs\plan_pruebas\informe -pdflatex -interaction=nonstopmode main.tex -pdflatex -interaction=nonstopmode main.tex +# Plan +cd docs\plan_pruebas +pdflatex -interaction=nonstopmode -file-line-error main.tex +pdflatex -interaction=nonstopmode -file-line-error main.tex +Copy-Item main.pdf PlanPruebas_IPSExtractor.pdf -Force + +# Informe +cd informe +pdflatex -interaction=nonstopmode -file-line-error main.tex +pdflatex -interaction=nonstopmode -file-line-error main.tex Copy-Item main.pdf ..\InformePruebas_IPSExtractor.pdf -Force + +# Capacidad 100k +cd ..\capacidad_100k +pdflatex -interaction=nonstopmode -file-line-error main.tex +pdflatex -interaction=nonstopmode -file-line-error main.tex +Copy-Item main.pdf ..\Capacidad100k_IPSExtractor.pdf -Force ``` + +## Campañas y scripts + +- [`informes/2026-07-13-shared8g.md`](informes/2026-07-13-shared8g.md) +- Scripts: [`scripts/`](scripts/) diff --git a/docs/plan_pruebas/capacidad_100k/README.md b/docs/plan_pruebas/capacidad_100k/README.md new file mode 100644 index 0000000..edd6b41 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/README.md @@ -0,0 +1,16 @@ +# Capacidad 100 000 facturas/mes + +Documento LaTeX compartible: modelo por etapa, evidencias (bot/LLM/OCR), coste LLM y veredicto. + +## Compilar + +```powershell +cd docs\plan_pruebas\capacidad_100k +pdflatex -interaction=nonstopmode -file-line-error main.tex +pdflatex -interaction=nonstopmode -file-line-error main.tex +Copy-Item main.pdf ..\Capacidad100k_IPSExtractor.pdf -Force +``` + +Salida: [`../Capacidad100k_IPSExtractor.pdf`](../Capacidad100k_IPSExtractor.pdf) + +Complementa el [Informe de Pruebas](../informe/) y el [Plan](../). diff --git a/docs/plan_pruebas/capacidad_100k/main.aux b/docs/plan_pruebas/capacidad_100k/main.aux new file mode 100644 index 0000000..8b8d9fb --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/main.aux @@ -0,0 +1,78 @@ +\relax +\providecommand \babel@aux [2]{\global \let \babel@toc \@gobbletwo } +\@nameuse{bbl@beforestart} +\catcode `"\active +\catcode `<\active +\catcode `>\active +\@nameuse{es@quoting} +\providecommand\hyper@newdestlabel[2]{} +\providecommand\HyField@AuxAddToFields[1]{} +\providecommand\HyField@AuxAddToCoFields[2]{} +\babel@aux{spanish}{} +\@writefile{toc}{\contentsline {chapter}{\numberline {1}Objetivo: 100\,000 facturas al mes}{2}{chapter.1}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:objetivo}{{1}{2}{Objetivo: 100\,000 facturas al mes}{chapter.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {1.1}Qu\'e pregunta responde este documento}{2}{section.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.2}Traducci\'on a ritmo (facturas/hora)}{2}{section.1.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces Ritmo m\'inimo para cumplir 100k/mes.}}{2}{table.1.1}\protected@file@percent } +\newlabel{tab:objetivos-h}{{1.1}{2}{Ritmo m\'inimo para cumplir 100k/mes}{table.1.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {1.3}C\'omo usar este documento}{2}{section.1.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura del pipeline}{4}{chapter.2}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:arq-cap}{{2}{4}{Arquitectura del pipeline}{chapter.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.1}Patrones de dise\~no relevantes}{4}{section.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {2.2}Flujo de negocio (punta a punta)}{4}{section.2.2}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats.}}{5}{figure.2.1}\protected@file@percent } +\newlabel{fig:pipeline-negocio}{{2.1}{5}{Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats}{figure.2.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.3}Vista de componentes}{5}{section.2.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} hace OCR y \textbf {devuelve JSON} (no escribe en PG); el Desktop/extraction persiste esos resultados con \texttt {POST /count-jobs} en Postgres.}}{5}{figure.2.2}\protected@file@percent } +\newlabel{fig:arquitectura}{{2.2}{5}{Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} hace OCR y \textbf {devuelve JSON} (no escribe en PG); el Desktop/extraction persiste esos resultados con \texttt {POST /count-jobs} en Postgres}{figure.2.2}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {3}Modelo de capacidad por etapa}{6}{chapter.3}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:modelo}{{3}{6}{Modelo de capacidad por etapa}{chapter.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.1}C\'omo leer los n\'umeros}{6}{section.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.2}Tabla de capacidad (datos julio 2026)}{6}{section.3.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Ritmo por etapa. Pipeline $\approx \mathop {\mathgroup \symoperators m\acute {\dotlessi }n}$ de etapas (cuello OCR). $^{\mathrm {P}}$Conservador $\sim $100 $<$ 139; techo observado OCR $\sim $325 puede cubrir 139 si el host no degrada m\'as.}}{6}{table.3.1}\protected@file@percent } +\newlabel{tab:capacidad}{{3.1}{6}{Ritmo por etapa. Pipeline $\approx \min $ de etapas (cuello OCR). $^{\mathrm {P}}$Conservador $\sim $100 $<$ 139; techo observado OCR $\sim $325 puede cubrir 139 si el host no degrada m\'as}{table.3.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.3}C\'omo se obtiene el ritmo del pipeline}{7}{section.3.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.4}Gr\'afico 1 --- etapas vs objetivos}{8}{section.3.4}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Azul = host compartido (medido/estimado). Verde = host dedicado (estimado). L\'ineas = m\'inimos para 100k/mes.}}{8}{figure.3.1}\protected@file@percent } +\newlabel{fig:capacidad}{{3.1}{8}{Azul = host compartido (medido/estimado). Verde = host dedicado (estimado). L\'ineas = m\'inimos para 100k/mes}{figure.3.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.5}Gr\'afico 2 --- \textquestiondown llegamos a 100k/mes?}{8}{section.3.5}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {3.2}{\ignorespaces Solo capacidad (barras) vs metas (l\'ineas). Modelo por cuello OCR; no es E2E de 100k.}}{9}{figure.3.2}\protected@file@percent } +\newlabel{fig:pipeline-metas}{{3.2}{9}{Solo capacidad (barras) vs metas (l\'ineas). Modelo por cuello OCR; no es E2E de 100k}{figure.3.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.6}Qu\'e concluir aqu\'i}{9}{section.3.6}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {4}Evidencias que alimentan el modelo}{10}{chapter.4}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:evidencias}{{4}{10}{Evidencias que alimentan el modelo}{chapter.4}{}} +\@writefile{toc}{\contentsline {section}{\numberline {4.1}Resumen}{10}{section.4.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {4.1}{\ignorespaces Medido vs estimado (campa\~na julio 2026).}}{10}{table.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.2}Detalle por prueba}{10}{section.4.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.3}A\'un no medido a escala}{11}{section.4.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {5}Coste LLM (100k que pasan conteo)}{12}{chapter.5}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:coste}{{5}{12}{Coste LLM (100k que pasan conteo)}{chapter.5}{}} +\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Presupuesto LLM: $100\,000 \times 0{,}0257 = 2\,570$ USD. No incluye agent Selenium, orquestaci\'on \texttt {bot-descarga}, OCR, infra ni reintentos.}}{12}{table.5.1}\protected@file@percent } +\newlabel{tab:coste-llm-100k}{{5.1}{12}{Presupuesto LLM: $100\,000 \times 0{,}0257 = 2\,570$ USD. No incluye agent Selenium, orquestaci\'on \texttt {bot-descarga}, OCR, infra ni reintentos}{table.5.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {6}Veredicto}{13}{chapter.6}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:veredicto}{{6}{13}{Veredicto}{chapter.6}{}} +\@writefile{toc}{\contentsline {section}{\numberline {6.1}La respuesta corta}{13}{section.6.1}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {6.1}{\ignorespaces Barras = capacidad. L\'ineas = metas. Rojo no llega a 139; verde llega a 139 pero no a 570.}}{13}{figure.6.1}\protected@file@percent } +\newlabel{fig:veredicto-brecha}{{6.1}{13}{Barras = capacidad. L\'ineas = metas. Rojo no llega a 139; verde llega a 139 pero no a 570}{figure.6.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {6.2}Si hoy no est\'a garantizado: qu\'e hace falta}{14}{section.6.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.3}Por meta operativa}{14}{section.6.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {7}Requisitos sugeridos y pr\'oximos pasos}{15}{chapter.7}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:requisitos}{{7}{15}{Requisitos sugeridos y pr\'oximos pasos}{chapter.7}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.1}Para lograr 100k/mes (lista simple)}{15}{section.7.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.2}Pr\'oximas mediciones}{15}{section.7.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.3}Documentos relacionados}{15}{section.7.3}\protected@file@percent } +\gdef \@abspage@last{16} diff --git a/docs/plan_pruebas/capacidad_100k/main.out b/docs/plan_pruebas/capacidad_100k/main.out new file mode 100644 index 0000000..84643b3 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/main.out @@ -0,0 +1,28 @@ +\BOOKMARK [0][-]{chapter.1}{\376\377\000O\000b\000j\000e\000t\000i\000v\000o\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000\040\000a\000l\000\040\000m\000e\000s}{}% 1 +\BOOKMARK [1][-]{section.1.1}{\376\377\000Q\000u\000\351\000\040\000p\000r\000e\000g\000u\000n\000t\000a\000\040\000r\000e\000s\000p\000o\000n\000d\000e\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.1}% 2 +\BOOKMARK [1][-]{section.1.2}{\376\377\000T\000r\000a\000d\000u\000c\000c\000i\000\363\000n\000\040\000a\000\040\000r\000i\000t\000m\000o\000\040\000\050\000f\000a\000c\000t\000u\000r\000a\000s\000/\000h\000o\000r\000a\000\051}{chapter.1}% 3 +\BOOKMARK [1][-]{section.1.3}{\376\377\000C\000\363\000m\000o\000\040\000u\000s\000a\000r\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.1}% 4 +\BOOKMARK [0][-]{chapter.2}{\376\377\000A\000r\000q\000u\000i\000t\000e\000c\000t\000u\000r\000a\000\040\000d\000e\000l\000\040\000p\000i\000p\000e\000l\000i\000n\000e}{}% 5 +\BOOKMARK [1][-]{section.2.1}{\376\377\000P\000a\000t\000r\000o\000n\000e\000s\000\040\000d\000e\000\040\000d\000i\000s\000e\000\361\000o\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{chapter.2}% 6 +\BOOKMARK [1][-]{section.2.2}{\376\377\000F\000l\000u\000j\000o\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o\000\040\000\050\000p\000u\000n\000t\000a\000\040\000a\000\040\000p\000u\000n\000t\000a\000\051}{chapter.2}% 7 +\BOOKMARK [1][-]{section.2.3}{\376\377\000V\000i\000s\000t\000a\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 8 +\BOOKMARK [0][-]{chapter.3}{\376\377\000M\000o\000d\000e\000l\000o\000\040\000d\000e\000\040\000c\000a\000p\000a\000c\000i\000d\000a\000d\000\040\000p\000o\000r\000\040\000e\000t\000a\000p\000a}{}% 9 +\BOOKMARK [1][-]{section.3.1}{\376\377\000C\000\363\000m\000o\000\040\000l\000e\000e\000r\000\040\000l\000o\000s\000\040\000n\000\372\000m\000e\000r\000o\000s}{chapter.3}% 10 +\BOOKMARK [1][-]{section.3.2}{\376\377\000T\000a\000b\000l\000a\000\040\000d\000e\000\040\000c\000a\000p\000a\000c\000i\000d\000a\000d\000\040\000\050\000d\000a\000t\000o\000s\000\040\000j\000u\000l\000i\000o\000\040\0002\0000\0002\0006\000\051}{chapter.3}% 11 +\BOOKMARK [1][-]{section.3.3}{\376\377\000C\000\363\000m\000o\000\040\000s\000e\000\040\000o\000b\000t\000i\000e\000n\000e\000\040\000e\000l\000\040\000r\000i\000t\000m\000o\000\040\000d\000e\000l\000\040\000p\000i\000p\000e\000l\000i\000n\000e}{chapter.3}% 12 +\BOOKMARK [1][-]{section.3.4}{\376\377\000G\000r\000\341\000f\000i\000c\000o\000\040\0001\000\040\040\024\000\040\000e\000t\000a\000p\000a\000s\000\040\000v\000s\000\040\000o\000b\000j\000e\000t\000i\000v\000o\000s}{chapter.3}% 13 +\BOOKMARK [1][-]{section.3.5}{\376\377\000G\000r\000\341\000f\000i\000c\000o\000\040\0002\000\040\040\024\000\040\000\277\000l\000l\000e\000g\000a\000m\000o\000s\000\040\000a\000\040\0001\0000\0000\000k\000/\000m\000e\000s\000?}{chapter.3}% 14 +\BOOKMARK [1][-]{section.3.6}{\376\377\000Q\000u\000\351\000\040\000c\000o\000n\000c\000l\000u\000i\000r\000\040\000a\000q\000u\000\355}{chapter.3}% 15 +\BOOKMARK [0][-]{chapter.4}{\376\377\000E\000v\000i\000d\000e\000n\000c\000i\000a\000s\000\040\000q\000u\000e\000\040\000a\000l\000i\000m\000e\000n\000t\000a\000n\000\040\000e\000l\000\040\000m\000o\000d\000e\000l\000o}{}% 16 +\BOOKMARK [1][-]{section.4.1}{\376\377\000R\000e\000s\000u\000m\000e\000n}{chapter.4}% 17 +\BOOKMARK [1][-]{section.4.2}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000p\000o\000r\000\040\000p\000r\000u\000e\000b\000a}{chapter.4}% 18 +\BOOKMARK [1][-]{section.4.3}{\376\377\000A\000\372\000n\000\040\000n\000o\000\040\000m\000e\000d\000i\000d\000o\000\040\000a\000\040\000e\000s\000c\000a\000l\000a}{chapter.4}% 19 +\BOOKMARK [0][-]{chapter.5}{\376\377\000C\000o\000s\000t\000e\000\040\000L\000L\000M\000\040\000\050\0001\0000\0000\000k\000\040\000q\000u\000e\000\040\000p\000a\000s\000a\000n\000\040\000c\000o\000n\000t\000e\000o\000\051}{}% 20 +\BOOKMARK [0][-]{chapter.6}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o}{}% 21 +\BOOKMARK [1][-]{section.6.1}{\376\377\000L\000a\000\040\000r\000e\000s\000p\000u\000e\000s\000t\000a\000\040\000c\000o\000r\000t\000a}{chapter.6}% 22 +\BOOKMARK [1][-]{section.6.2}{\376\377\000S\000i\000\040\000h\000o\000y\000\040\000n\000o\000\040\000e\000s\000t\000\341\000\040\000g\000a\000r\000a\000n\000t\000i\000z\000a\000d\000o\000:\000\040\000q\000u\000\351\000\040\000h\000a\000c\000e\000\040\000f\000a\000l\000t\000a}{chapter.6}% 23 +\BOOKMARK [1][-]{section.6.3}{\376\377\000P\000o\000r\000\040\000m\000e\000t\000a\000\040\000o\000p\000e\000r\000a\000t\000i\000v\000a}{chapter.6}% 24 +\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000o\000s\000\040\000y\000\040\000p\000r\000\363\000x\000i\000m\000o\000s\000\040\000p\000a\000s\000o\000s}{}% 25 +\BOOKMARK [1][-]{section.7.1}{\376\377\000P\000a\000r\000a\000\040\000l\000o\000g\000r\000a\000r\000\040\0001\0000\0000\000k\000/\000m\000e\000s\000\040\000\050\000l\000i\000s\000t\000a\000\040\000s\000i\000m\000p\000l\000e\000\051}{chapter.7}% 26 +\BOOKMARK [1][-]{section.7.2}{\376\377\000P\000r\000\363\000x\000i\000m\000a\000s\000\040\000m\000e\000d\000i\000c\000i\000o\000n\000e\000s}{chapter.7}% 27 +\BOOKMARK [1][-]{section.7.3}{\376\377\000D\000o\000c\000u\000m\000e\000n\000t\000o\000s\000\040\000r\000e\000l\000a\000c\000i\000o\000n\000a\000d\000o\000s}{chapter.7}% 28 diff --git a/docs/plan_pruebas/capacidad_100k/main.tex b/docs/plan_pruebas/capacidad_100k/main.tex new file mode 100644 index 0000000..3aad642 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/main.tex @@ -0,0 +1,20 @@ +\documentclass[11pt,a4paper,oneside]{report} + +\input{preamble} + +\begin{document} + +\makecover + +\tableofcontents +\clearpage + +\input{sections/01_objetivo} +\input{sections/02_arquitectura_pipeline} +\input{sections/03_modelo_etapas} +\input{sections/04_evidencias} +\input{sections/05_coste_llm} +\input{sections/06_veredicto} +\input{sections/07_requisitos} + +\end{document} diff --git a/docs/plan_pruebas/capacidad_100k/main.toc b/docs/plan_pruebas/capacidad_100k/main.toc new file mode 100644 index 0000000..36a4447 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/main.toc @@ -0,0 +1,29 @@ +\babel@toc {spanish}{}\relax +\contentsline {chapter}{\numberline {1}Objetivo: 100\,000 facturas al mes}{2}{chapter.1}% +\contentsline {section}{\numberline {1.1}Qu\'e pregunta responde este documento}{2}{section.1.1}% +\contentsline {section}{\numberline {1.2}Traducci\'on a ritmo (facturas/hora)}{2}{section.1.2}% +\contentsline {section}{\numberline {1.3}C\'omo usar este documento}{2}{section.1.3}% +\contentsline {chapter}{\numberline {2}Arquitectura del pipeline}{4}{chapter.2}% +\contentsline {section}{\numberline {2.1}Patrones de dise\~no relevantes}{4}{section.2.1}% +\contentsline {section}{\numberline {2.2}Flujo de negocio (punta a punta)}{4}{section.2.2}% +\contentsline {section}{\numberline {2.3}Vista de componentes}{5}{section.2.3}% +\contentsline {chapter}{\numberline {3}Modelo de capacidad por etapa}{6}{chapter.3}% +\contentsline {section}{\numberline {3.1}C\'omo leer los n\'umeros}{6}{section.3.1}% +\contentsline {section}{\numberline {3.2}Tabla de capacidad (datos julio 2026)}{6}{section.3.2}% +\contentsline {section}{\numberline {3.3}C\'omo se obtiene el ritmo del pipeline}{7}{section.3.3}% +\contentsline {section}{\numberline {3.4}Gr\'afico 1 --- etapas vs objetivos}{8}{section.3.4}% +\contentsline {section}{\numberline {3.5}Gr\'afico 2 --- \textquestiondown llegamos a 100k/mes?}{8}{section.3.5}% +\contentsline {section}{\numberline {3.6}Qu\'e concluir aqu\'i}{9}{section.3.6}% +\contentsline {chapter}{\numberline {4}Evidencias que alimentan el modelo}{10}{chapter.4}% +\contentsline {section}{\numberline {4.1}Resumen}{10}{section.4.1}% +\contentsline {section}{\numberline {4.2}Detalle por prueba}{10}{section.4.2}% +\contentsline {section}{\numberline {4.3}A\'un no medido a escala}{11}{section.4.3}% +\contentsline {chapter}{\numberline {5}Coste LLM (100k que pasan conteo)}{12}{chapter.5}% +\contentsline {chapter}{\numberline {6}Veredicto}{13}{chapter.6}% +\contentsline {section}{\numberline {6.1}La respuesta corta}{13}{section.6.1}% +\contentsline {section}{\numberline {6.2}Si hoy no est\'a garantizado: qu\'e hace falta}{14}{section.6.2}% +\contentsline {section}{\numberline {6.3}Por meta operativa}{14}{section.6.3}% +\contentsline {chapter}{\numberline {7}Requisitos sugeridos y pr\'oximos pasos}{15}{chapter.7}% +\contentsline {section}{\numberline {7.1}Para lograr 100k/mes (lista simple)}{15}{section.7.1}% +\contentsline {section}{\numberline {7.2}Pr\'oximas mediciones}{15}{section.7.2}% +\contentsline {section}{\numberline {7.3}Documentos relacionados}{15}{section.7.3}% diff --git a/docs/plan_pruebas/capacidad_100k/preamble.tex b/docs/plan_pruebas/capacidad_100k/preamble.tex new file mode 100644 index 0000000..74f3983 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/preamble.tex @@ -0,0 +1,106 @@ +\usepackage[T1]{fontenc} +\usepackage[utf8]{inputenc} +\usepackage[spanish,es-tabla]{babel} +\usepackage{lmodern} +\usepackage{microtype} +\usepackage[ + a4paper, + margin=2.4cm, + headheight=14pt +]{geometry} +\usepackage{setspace} +\onehalfspacing + +\usepackage{fancyhdr} +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small IPSExtractor --- Capacidad 100k/mes} +\fancyhead[R]{\small\thepage} +\fancyfoot[C]{\footnotesize Documento para compartir} +\renewcommand{\headrulewidth}{0.4pt} + +\usepackage{titlesec} +\titleformat{\chapter}[display] + {\normalfont\huge\bfseries}{\chaptertitlename\ \thechapter}{14pt}{\Huge} +\titleformat{\section}{\normalfont\Large\bfseries}{\thesection}{1em}{} + +\usepackage{booktabs} +\usepackage{longtable} +\usepackage{tabularx} +\usepackage{multirow} +\usepackage{array} +\usepackage{enumitem} +\setlist[itemize]{nosep,leftmargin=1.4em} +\setlist[enumerate]{nosep,leftmargin=1.4em} + +\usepackage{siunitx} +\usepackage{xcolor} +\definecolor{ipsblue}{HTML}{1B4F72} +\definecolor{ipsgrey}{HTML}{566573} +\definecolor{codebg}{HTML}{F4F6F7} +\definecolor{passgreen}{HTML}{1E8449} +\definecolor{failred}{HTML}{C0392B} +\definecolor{warnorange}{HTML}{B9770E} + +\usepackage{listings} +\lstset{ + basicstyle=\ttfamily\footnotesize, + backgroundcolor=\color{codebg}, + frame=single, + framerule=0.3pt, + breaklines=true, + columns=fullflexible, + showstringspaces=false +} + +\usepackage{pgfplots} +\pgfplotsset{compat=1.18} + +\usepackage{tikz} +\usetikzlibrary{arrows.meta,positioning,fit,calc,shapes.geometric} + +\usepackage{graphicx} +\usepackage{float} +\usepackage{hyperref} +\hypersetup{ + colorlinks=true, + linkcolor=ipsblue, + urlcolor=ipsblue, + pdftitle={Capacidad 100000 facturas/mes IPSExtractor}, + pdfauthor={IPSExtractor QA} +} + +\newcommand{\docdate}{17 de julio de 2026} +\newcommand{\pass}{\textcolor{passgreen}{\textbf{PASS}}} +\newcommand{\fail}{\textcolor{failred}{\textbf{FAIL}}} +\newcommand{\parcial}{\textcolor{warnorange}{\textbf{PARCIAL}}} + +\newcommand{\callout}[2]{% + \begin{center} + \fbox{\parbox{0.92\textwidth}{\centering #1\\[0.3em]\normalsize #2}} + \end{center} +} + +\newcommand{\makecover}{% + \begin{titlepage} + \centering + \vspace*{1.2cm} + {\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[0.35cm] + {\Huge\bfseries IPSExtractor}\\[0.25cm] + {\Large\color{ipsgrey} Extracci\'on y an\'alisis de facturas m\'edicas}\\[0.35cm] + {\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[1.5cm] + {\LARGE\bfseries Capacidad: 100\,000 facturas/mes}\\[0.25cm] + {\large Modelo por etapa $\cdot$ Evidencias $\cdot$ Coste LLM}\\[1.8cm] + \begin{tabular}{@{}ll@{}} + \textbf{Fecha} & \docdate \\ + \textbf{Evidencias} & Campa\~na 13--17 jul 2026 (k6, LLM, ARQ, OCR) \\ + \textbf{Host de referencia} & ubuntu --- ASUS X441UAK, 8\,GiB (shared8g) \\ + \textbf{Audiencia} & Documento para compartir \\ + \end{tabular} + \vfill + {\small Complementa el \emph{Informe de Pruebas} y el \emph{Plan de pruebas}.}\\[0.15cm] + {\small\textbf{Veredicto breve:} host actual \textcolor{warnorange}{no garantizado} (cuello OCR); + dedicado 24$\times$7 \textcolor{passgreen}{S\'I condicional}.}\\[0.25cm] + {\color{ipsblue}\rule{0.45\textwidth}{0.6pt}} + \end{titlepage} +} diff --git a/docs/plan_pruebas/capacidad_100k/sections/01_objetivo.tex b/docs/plan_pruebas/capacidad_100k/sections/01_objetivo.tex new file mode 100644 index 0000000..a6f327b --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/01_objetivo.tex @@ -0,0 +1,45 @@ +\chapter{Objetivo: 100\,000 facturas al mes} +\label{ch:objetivo} + +\section{Qu\'e pregunta responde este documento} +\textquestiondown Puede IPSExtractor procesar del orden de \textbf{100\,000 facturas m\'edicas al mes}? + +\callout{\textbf{Anticipo del veredicto (detalle en cap.~\ref{ch:veredicto})}}{% +\textbf{En el host compartido actual: no garantizado} (pipeline $\approx$ OCR $\sim$100--325/h; piso $\sim$100 $<$ 139).\\ +\textbf{Con host dedicado y OCR dimensionado: s\'i plausible para 24$\times$7;} jornada 8\,h pide m\'as escalado. +} + +La respuesta depende de dos cosas: +\begin{enumerate} + \item \textbf{Horario:} 24$\times$7 vs solo jornada laboral. + \item \textbf{Infra:} host compartido (hoy) vs host dedicado + OCR, agent Selenium y LLM dimensionados (orquestador \texttt{bot-descarga} en Docker). +\end{enumerate} + +\section{Traducci\'on a ritmo (facturas/hora)} +\begin{table}[ht] +\centering +\begin{tabular}{@{}lrr@{}} +\toprule +\textbf{Escenario operativo} & \textbf{Facturas/d\'ia} & \textbf{Facturas/hora} \\ +\midrule +100\,000 / mes, 24$\times$7 & $\approx$3\,333 & $\approx$\textbf{139} \\ +100\,000 / mes, 8\,h $\times$ 22\,d & $\approx$4\,545 & $\approx$\textbf{570} \\ +\bottomrule +\end{tabular} +\caption{Ritmo m\'inimo para cumplir 100k/mes.} +\label{tab:objetivos-h} +\end{table} + +\callout{\large Lectura r\'apida}{% +24$\times$7 $\Rightarrow$ $\approx$\textbf{139 fact/h} punta a punta.\\ +Jornada 8\,h $\Rightarrow$ $\approx$\textbf{570 fact/h}.\\ +Gana la etapa m\'as lenta; el resto sobra si esa falla. +} + +\section{C\'omo usar este documento} +\begin{enumerate} + \item Mire el veredicto (cap.~\ref{ch:veredicto}): s\'i / no / qu\'e falta. + \item Compare etapas vs 139 y 570 (tabla~\ref{tab:capacidad}). + \item Revise evidencias medidas (cap.~\ref{ch:evidencias}) y coste LLM (cap.~\ref{ch:coste}). + \item Use el checklist de requisitos (cap.~\ref{ch:requisitos}) como plan de acci\'on. +\end{enumerate} diff --git a/docs/plan_pruebas/capacidad_100k/sections/02_arquitectura_pipeline.tex b/docs/plan_pruebas/capacidad_100k/sections/02_arquitectura_pipeline.tex new file mode 100644 index 0000000..dacd06d --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/02_arquitectura_pipeline.tex @@ -0,0 +1,44 @@ +\chapter{Arquitectura del pipeline} +\label{ch:arq-cap} + +\callout{\textbf{Nombres (importante)}}{% +\textbf{bot-descarga} = servicio de orquestaci\'on (API Docker): jobs, claim, heartbeats.\\ +\textbf{Agent Selenium} = programa en el PC que descarga PDFs (Imaging $\rightarrow$ MinIO).\\ +Las pruebas \textbf{ST-BOT-*} miden el orquestador. La capacidad $\sim$422 fact/h es del agent. +} + +\section{Patrones de dise\~no relevantes} +IPSExtractor no es un monolito: el volumen mensualmente viable depende de c\'omo +escalan \textbf{por separado} cada bounded context. + +\begin{description}[style=nextline,leftmargin=0.5cm] + \item[API Gateway] + \'Unica puerta HTTP en el host (\texttt{:8189}). Orquesta, no procesa OCR ni LLM. + Escala de control; no es el cuello de 100k. + \item[Microservicios por dominio] + \texttt{count-api} (OCR), \texttt{extraction}+workers (LLM), \texttt{bot-descarga} (orquestador) + + agent Selenium (PC), \texttt{cleaning} (CSV). Cada uno tiene su propio l\'imite de capacidad. + \item[Cola as\'incrona (ARQ/Redis)] + La extracci\'on puede encolar PDFs y drenar con $N$ workers. Throughput LLM + $\approx N \times (3600/\mathrm{p50})$ si no hay cuota del proveedor. + \item[Claim/lease del orquestador (\texttt{bot-descarga})] + Varios agents Selenium en PCs distintos reclaman jobs v\'ia la API; la capacidad de descarga + crece con $N$ agents, no con un solo proceso Selenium. +\end{description} + +\section{Flujo de negocio (punta a punta)} +\input{../diagrams/pipeline_negocio} + +Orden t\'ipico: +\begin{enumerate} + \item \textbf{Descarga:} agent Selenium $\rightarrow$ PDFs en MinIO. + \item \textbf{Conteo:} OCR/ONNX valida p\'aginas/palabras (filtro). + \item \textbf{Extracci\'on:} LLM rellena campos $\rightarrow$ filas en PostgreSQL. + \item \textbf{Limpieza:} reglas/CSV sobre el lote extra\'ido. +\end{enumerate} + +El pipeline completo solo es tan r\'apido como la etapa m\'as lenta bajo la misma +contenci\'on de CPU/RAM. + +\section{Vista de componentes} +\input{../diagrams/arquitectura} diff --git a/docs/plan_pruebas/capacidad_100k/sections/03_modelo_etapas.tex b/docs/plan_pruebas/capacidad_100k/sections/03_modelo_etapas.tex new file mode 100644 index 0000000..d812d7a --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/03_modelo_etapas.tex @@ -0,0 +1,237 @@ +\chapter{Modelo de capacidad por etapa} +\label{ch:modelo} + +\callout{\textbf{Idea en una frase}}{% +Cada etapa tiene un ritmo (facturas/hora).\\ +El \textbf{pipeline completo} solo va tan r\'apido como la etapa m\'as lenta +(hoy: OCR en host compartido).\\ +El agent Selenium y el LLM pueden ir bien por separado y el sistema entero igual no llega a 100k/mes. +} + +\section{C\'omo leer los n\'umeros} +\begin{itemize} + \item \textbf{Host actual:} servidor compartido de las pruebas (8\,GiB + MLflow y otros). + \item \textbf{Host dedicado:} estimaci\'on si IPSExtractor va solo (sin competir por CPU/RAM). + \item \textbf{139/h:} m\'inimo para 100k/mes en 24$\times$7. + \item \textbf{570/h:} m\'inimo para 100k/mes en jornada 8\,h $\times$ 22\,d. +\end{itemize} + +\noindent Sem\'aforo: +\textcolor{passgreen}{\textbf{S\'I}} = llega; +\textcolor{warnorange}{\textbf{JUSTO}} = cerca / hace falta margen; +\textcolor{failred}{\textbf{NO}} = no llega. + +\section{Tabla de capacidad (datos julio 2026)} +\begin{itemize} + \item LLM ST-EXT-02: p50 7{,}1\,s $\Rightarrow$ $\approx$506 fact/h (1 worker). + \item OCR ST-COUNT-01: p50 35{,}8\,s @3 concurrentes $\Rightarrow$ $\approx$100--325 fact/h + (en serie era $\sim$2\,s/PDF). + \item Agent Selenium: 633 PDFs / 90\,min $\Rightarrow$ $\approx$422 fact/h (1 agent PC). + \item Gateway ST-GW-01: p95 41\,ms, 0\,\% fallos --- no limita. +\end{itemize} + +\begin{table}[ht] +\centering +\small +\begin{tabular}{@{}lrrcc@{}} +\toprule +\textbf{Etapa} & \textbf{Host actual} & \textbf{Dedicado (est.)} & \textbf{\textquestiondown 139/h?} & \textbf{\textquestiondown 570/h?} \\ + & fact/h & fact/h & & \\ +\midrule +Gateway & $>$10\,000 & $>$20\,000 & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{passgreen}{\textbf{S\'I}} \\ +OCR (conteo) & $\sim$100--325 & $\sim$400 & \textcolor{warnorange}{\textbf{JUSTO}} & \textcolor{failred}{\textbf{NO}} \\ +LLM (extracci\'on) & $\sim$506 & $\sim$1\,000 (2\,w) & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{warnorange}{\textbf{JUSTO}} \\ +Descarga (agent) & $\sim$422 & $\sim$500+ & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{failred}{\textbf{NO}} \\ +Limpieza CSV & $\sim$800 & $\sim$3\,000 & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{warnorange}{\textbf{JUSTO}} \\ +\midrule +\textbf{Pipeline entero} & $\mathbf{\sim}$\textbf{100--325} & $\mathbf{\sim}$\textbf{400} & \textcolor{warnorange}{\textbf{JUSTO}}$^{\mathrm{P}}$ & \textcolor{failred}{\textbf{NO}} \\ +\bottomrule +\end{tabular} +\caption{Ritmo por etapa. Pipeline $\approx\min$ de etapas (cuello OCR). $^{\mathrm{P}}$Conservador +$\sim$100 $<$ 139; techo observado OCR $\sim$325 puede cubrir 139 si el host no degrada m\'as.} +\label{tab:capacidad} +\end{table} + +\section{C\'omo se obtiene el ritmo del pipeline} +\subsection*{Primero: qu\'e \emph{no} es ese n\'umero} +No corrimos 100\,000 PDFs de punta a punta midiendo el reloj. +El ritmo del ``pipeline entero'' es un \textbf{modelo}: se calcula a partir de las +etapas que s\'i medimos (agent, OCR, LLM, limpieza). + +\subsection*{Una factura es secuencial; muchas facturas no} +Para \textbf{una} factura el orden es obligatorio: +\begin{center} +descarga $\rightarrow$ conteo OCR $\rightarrow$ extracci\'on LLM $\rightarrow$ limpieza. +\end{center} +No puede empezar el LLM de esa factura si a\'un no termin\'o el OCR. + +Pero en el d\'ia a d\'ia no hay una sola factura: hay un \textbf{lote}. +Mientras la factura A est\'a en el LLM, la B puede estar en OCR y la C en descarga. +Es como una cadena de montaje: cada puesto trabaja en paralelo sobre piezas distintas. + +\subsection*{Analog\'ia del embudo} +El ritmo del sistema completo lo marca el \textbf{puesto m\'as lento} +(el cuello / embudo), no el m\'as r\'apido. + +\begin{center} +\begin{tabular}{@{}lrl@{}} +\toprule +\textbf{Puesto (etapa)} & \textbf{Ritmo medido} & \textbf{Lectura} \\ +\midrule +Descarga (agent Selenium) & $\approx$422 fact/h & r\'apido \\ +Conteo (OCR) & $\approx$100--325 fact/h & \textbf{el m\'as lento} \\ +Extracci\'on (LLM) & $\approx$506 fact/h & r\'apido \\ +Limpieza & $\approx$800 fact/h & r\'apido \\ +\midrule +\textbf{Pipeline entero} & $\approx$\textbf{100--325 fact/h} & $=\min$ = OCR \\ +\bottomrule +\end{tabular} +\end{center} + +Por eso da igual que el agent haga 422/h: si el OCR solo deja pasar 100--325/h, +el lote completo no puede ir m\'as r\'apido que eso. + +\subsection*{F\'ormula (r\'egimen normal / cadena de montaje)} +\[ + \text{ritmo del pipeline} \;\approx\; \min(\text{descarga},\,\text{OCR},\,\text{LLM},\,\text{limpieza}). +\] +Con los n\'umeros de arriba: $\min(422,\,100\text{--}325,\,506,\,800) = \mathbf{100\text{--}325}$. + +\subsection*{Y si proces\'aramos de una en una, sin solapar?} +Entonces s\'i se \textbf{suman} los tiempos de cada etapa por factura: +\[ + \text{fact/h} \approx 3600\big/\big(t_{\mathrm{descarga}}+t_{\mathrm{OCR}}+t_{\mathrm{LLM}}+t_{\mathrm{limpieza}}\big). +\] +Ejemplo con OCR lento ($\sim$36\,s): $\sim$70 fact/h. +Con OCR r\'apido en serie ($\sim$2\,s): $\sim$200 fact/h. + +Eso ser\'ia un laboratorio artificial (una factura y la siguiente solo cuando acaba). +En producci\'on real usamos el modelo del embudo ($\min$), que es el que importa. + +\subsection*{Qu\'e ponemos en tablas y gr\'aficos} +\begin{itemize} + \item Host actual: pipeline = $\sim$100--325 (cuello OCR). + \item Para el veredicto 24$\times$7 usamos el \textbf{piso} $\sim$100 (conservador; queda bajo 139). + \item Host dedicado (est.): $\sim$400 (OCR estimado $\sim$400). +\end{itemize} + +\section{Gr\'afico 1 --- etapas vs objetivos} +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=\textwidth, + height=6.8cm, + ylabel={Facturas / hora}, + symbolic x coords={OCR,LLM,Agent,Limpieza,Pipeline}, + xtick=data, + xticklabels={OCR,LLM,{Agent Selenium},Limpieza,Pipeline}, + x tick label style={font=\small}, + legend style={at={(0.02,0.98)},anchor=north west,font=\small,draw=ipsgrey!40}, + bar width=11pt, + ymin=0, ymax=1100, + enlarge x limits=0.12, + grid=major, + major grid style={ipsgrey!20}, +] +\addplot[fill=ipsblue!75, draw=ipsblue] coordinates { + (OCR,200) (LLM,506) (Agent,422) (Limpieza,800) (Pipeline,200) +}; +\addlegendentry{Host actual} +\addplot[fill=passgreen!60, draw=passgreen] coordinates { + (OCR,400) (LLM,1012) (Agent,500) (Limpieza,1000) (Pipeline,400) +}; +\addlegendentry{Host dedicado (est.)} +\draw[failred, ultra thick] (axis cs:OCR,139) -- (axis cs:Pipeline,139) + node[pos=0, left, font=\scriptsize\bfseries, text=failred] {139}; +\draw[warnorange, ultra thick] (axis cs:OCR,570) -- (axis cs:Pipeline,570) + node[pos=0, left, font=\scriptsize\bfseries, text=warnorange] {570}; +\end{axis} +\end{tikzpicture} +\caption{Azul = host compartido (medido/estimado). Verde = host dedicado (estimado). +L\'ineas = m\'inimos para 100k/mes.} +\label{fig:capacidad} +\end{figure} + +\section{Gr\'afico 2 --- \textquestiondown llegamos a 100k/mes?} +\subsection*{C\'omo leer este gr\'afico} +Solo hay \textbf{dos barras} = cu\'anto puede el pipeline (modelo): +\begin{itemize} + \item Rojo = host \textbf{actual} (piso $\sim$100 fact/h). + \item Verde = host \textbf{dedicado} estimado ($\sim$400 fact/h). +\end{itemize} +Las \textbf{l\'ineas verticales} no son barras: son las metas que hay que superar: +\begin{itemize} + \item Azul punteada = 139/h (100k/mes trabajando 24$\times$7). + \item Naranja punteada = 570/h (100k/mes en jornada 8\,h). +\end{itemize} +Si la barra \textbf{pasa} la l\'inea $\Rightarrow$ esa meta es alcanzable. Si se queda corta $\Rightarrow$ no. + +\begin{figure}[H] +\centering +\begin{tikzpicture} +\begin{axis}[ + xbar, + width=0.78\textwidth, + height=4.8cm, + xlabel={Facturas / hora que aguanta el pipeline}, + symbolic y coords={dedicado,actual}, + ytick=data, + yticklabels={\empty,\empty}, + xmin=0, xmax=650, + enlarge y limits=0.45, + bar width=22pt, + bar shift=0pt, + grid=major, + major grid style={ipsgrey!25}, + axis y line*=left, + axis x line*=bottom, + clip=false, +] +\addplot[fill=failred!75, draw=failred] coordinates {(100,actual)}; +\addplot[fill=passgreen!75, draw=passgreen] coordinates {(400,dedicado)}; +% Etiquetas izquierda +\node[anchor=east, font=\small\bfseries, align=right] + at (axis cs:-5,actual) {Host actual\\(hoy)}; +\node[anchor=east, font=\small\bfseries, align=right] + at (axis cs:-5,dedicado) {Host dedicado\\(estimado)}; +% Valores +\node[anchor=west, font=\normalsize\bfseries] at (axis cs:110,actual) {$\sim$100 fact/h}; +\node[anchor=west, font=\normalsize\bfseries] at (axis cs:410,dedicado) {$\sim$400 fact/h}; +% Metas como l\'ineas (no barras) +\draw[ipsblue, ultra thick, dashed] + (axis cs:139,actual) -- (axis cs:139,dedicado); +\node[font=\scriptsize\bfseries, text=ipsblue, anchor=south, align=center] + at (axis cs:139,dedicado) {meta 139\\(24$\times$7)}; +\draw[warnorange, ultra thick, dashed] + (axis cs:570,actual) -- (axis cs:570,dedicado); +\node[font=\scriptsize\bfseries, text=warnorange, anchor=south, align=center] + at (axis cs:570,dedicado) {meta 570\\(jornada)}; +\end{axis} +\end{tikzpicture} +\caption{Solo capacidad (barras) vs metas (l\'ineas). Modelo por cuello OCR; no es E2E de 100k.} +\label{fig:pipeline-metas} +\end{figure} + +\begin{center} +\renewcommand{\arraystretch}{1.25} +\begin{tabular}{@{}lcc@{}} +\toprule + & \textbf{\textquestiondown Llega a 139/h?} & \textbf{\textquestiondown Llega a 570/h?} \\ + & (100k en 24$\times$7) & (100k en 8\,h) \\ +\midrule +Host actual $\sim$100 & \textcolor{failred}{\textbf{NO}} (falta margen) & \textcolor{failred}{\textbf{NO}} \\ +Host dedicado $\sim$400 & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{failred}{\textbf{NO}} (falta escalar) \\ +\bottomrule +\end{tabular} +\end{center} + + +\section{Qu\'e concluir aqu\'i} +\begin{enumerate} + \item Gateway, agent Selenium y LLM \textbf{no} son el cuello para 139/h (por etapa). + \item El pipeline sigue al OCR: $\sim$100--325/h $\Rightarrow$ 139/h \textbf{no garantizado} + (el piso conservador $\sim$100 queda bajo). + \item Host dedicado ($\sim$400/h): \textbf{s\'i} a 139/h; \textbf{no} a 570/h sin m\'as OCR/agents. +\end{enumerate} diff --git a/docs/plan_pruebas/capacidad_100k/sections/04_evidencias.tex b/docs/plan_pruebas/capacidad_100k/sections/04_evidencias.tex new file mode 100644 index 0000000..5a2b473 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/04_evidencias.tex @@ -0,0 +1,40 @@ +\chapter{Evidencias que alimentan el modelo} +\label{ch:evidencias} + +\section{Resumen} +\begin{table}[ht] +\centering +\small +\begin{tabular}{@{}llp{5.4cm}@{}} +\toprule +\textbf{Etapa} & \textbf{Cifra} & \textbf{Origen} \\ +\midrule +Gateway & no es cuello (p95 41\,ms) & ST-GW-01 (13-jul), 0\,\% fallos \\ +Agent Selenium (1 agent PC) & $\approx$422 fact/h & 633 PDFs / 90\,min \\ +LLM (1 worker) & $\approx$506 fact/h & ST-EXT-02 (17-jul): p50 7{,}114\,s \\ +OCR count-api & $\sim$100--325 fact/h & ST-COUNT-01 (17-jul): p50 35{,}8\,s @3 \\ +Cola ARQ (mock) & $\approx$4\,500 docs/h cola & ST-EXT-01: 30/30 en $\approx$24\,s \\ +Pipeline host actual & $\sim$100--325 fact/h & $=\min$ etapas; cuello OCR (no E2E) \\ +Pipeline dedicado (est.) & $\sim$400 fact/h & $\min$ con OCR $\sim$400 \\ +\bottomrule +\end{tabular} +\caption{Medido vs estimado (campa\~na julio 2026).} +\end{table} + +\section{Detalle por prueba} +\begin{description}[style=nextline,leftmargin=0.4cm] + \item[ST-EXT-02 (LLM)] + gpt-5.4-mini; 23/23 ok; p50 7{,}114\,s $\Rightarrow$ 506 fact/h (1\,w); 0$\times$429. + \item[ST-EXT-01 (ARQ mock)] + 30/30 drenados en 22{,}5\,s. Valida cola async; \textbf{no} es capacidad LLM real. + \item[ST-COUNT-01 (OCR)] + 9/9 ok; p50 35{,}8\,s con 3 concurrentes (vs $\sim$2\,s en serie). Degrada fuerte en shared8g. + \item[Agent Selenium] + 422 fact/h con 1 agent PC: supera 139/h; no alcanza 570/h sin m\'as agents. + \item[ST-GW-01 / ST-BOT-01] + Gateway OK. ST-BOT-01 mide el orquestador \texttt{bot-descarga} (heartbeats): host contendido malo (p95 2{,}28\,s); host m\'as libre OK (86\,ms). +\end{description} + +\section{A\'un no medido a escala} +ST-COUNT-01 lab, ST-EXT-01 lab (100--500), E2E punta a punta a cientos/miles de PDFs, +ni 100k reales. diff --git a/docs/plan_pruebas/capacidad_100k/sections/05_coste_llm.tex b/docs/plan_pruebas/capacidad_100k/sections/05_coste_llm.tex new file mode 100644 index 0000000..71edb3a --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/05_coste_llm.tex @@ -0,0 +1,32 @@ +\chapter{Coste LLM (100k que pasan conteo)} +\label{ch:coste} + +Hip\'otesis: el filtro de conteo deja pasar \textbf{100\,000} PDFs a extracci\'on +con modelo \textbf{gpt-5.4 mini} a USD\,0{,}0257 por documento. + +\begin{table}[ht] +\centering +\begin{tabular}{@{}lr@{}} +\toprule +\textbf{Concepto} & \textbf{Valor} \\ +\midrule +Modelo de extracci\'on & gpt-5.4 mini \\ +Coste unitario por documento & USD\,0{,}0257 \\ +Volumen (pasan filtro de conteo) & 100\,000 \\ +\textbf{Coste estimado extracci\'on / mes} & \textbf{USD\,2\,570} \\ +\bottomrule +\end{tabular} +\caption{Presupuesto LLM: $100\,000 \times 0{,}0257 = 2\,570$ USD. +No incluye agent Selenium, orquestaci\'on \texttt{bot-descarga}, OCR, infra ni reintentos.} +\label{tab:coste-llm-100k} +\end{table} + +\begin{itemize} + \item Si pasan $n < 100\,000$, el coste es lineal: $n \times 0{,}0257$. + \item Ejemplo: 60\,000 v\'alidos $\Rightarrow$ $\approx$ USD\,1\,542. + \item El unitario 0{,}0257 se us\'o como estimaci\'on en ST-EXT-02; contrastar con tokens reales si se instrumenta \texttt{usage}. +\end{itemize} + +\callout{\textbf{Coste de referencia}}{% +\textbf{USD\,2\,570}/mes si 100k PDFs llegan a extracci\'on con gpt-5.4 mini. +} diff --git a/docs/plan_pruebas/capacidad_100k/sections/06_veredicto.tex b/docs/plan_pruebas/capacidad_100k/sections/06_veredicto.tex new file mode 100644 index 0000000..3319c15 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/06_veredicto.tex @@ -0,0 +1,91 @@ +\chapter{Veredicto} +\label{ch:veredicto} + +\section{La respuesta corta} +\begin{center} +\renewcommand{\arraystretch}{1.35} +\begin{tabular}{@{}p{4.2cm}p{2.2cm}p{6.5cm}@{}} +\toprule +\textbf{Escenario} & \textbf{\textquestiondown 100k?} & \textbf{Por qu\'e} \\ +\midrule +Host \textbf{actual} (compartido) + & \parcial\ / \fail + & Pipeline $\approx$ OCR $\sim$100--325/h. + Piso $\sim$100 $<$ 139 (no garantizado). \\ +Host \textbf{dedicado} + dimensionado, 24$\times$7 + & \pass\ \emph{si} + & Estimado $\sim$400/h $>$ 139/h si OCR/agents/LLM OK. \\ +Misma infra, jornada 8\,h (570/h) + & \fail + & 400 $<$ 570: falta m\'as OCR, m\'as agents Selenium y cuota LLM. \\ +\bottomrule +\end{tabular} +\end{center} + +\callout{\large Resumen para decidir}{% +\textbf{Hoy (host compartido): 100k/mes 24$\times$7 no est\'a garantizado} +(pipeline $\approx$ cuello OCR; piso $\sim$100 $<$ 139).\\ +\textbf{Con host dedicado + OCR dimensionado: s\'i plausible a 24$\times$7.}\\ +\textbf{Jornada 8\,h: no} sin escalar m\'as.\\[0.35em] +\footnotesize El pipeline \textbf{no} se midi\'o E2E a 100k. Ritmo = $\min$ de etapas medidas +(cap.~\ref{ch:modelo}). +} + +\begin{figure}[H] +\centering +\begin{tikzpicture} +\begin{axis}[ + xbar, + width=0.78\textwidth, + height=4.6cm, + xlabel={Facturas / hora que aguanta el pipeline}, + symbolic y coords={dedicado,actual}, + ytick=data, + yticklabels={\empty,\empty}, + xmin=0, xmax=650, + enlarge y limits=0.45, + bar width=22pt, + bar shift=0pt, + grid=major, + major grid style={ipsgrey!25}, + clip=false, +] +\addplot[fill=failred!75, draw=failred] coordinates {(100,actual)}; +\addplot[fill=passgreen!75, draw=passgreen] coordinates {(400,dedicado)}; +\node[anchor=east, font=\small\bfseries, align=right] + at (axis cs:-5,actual) {Host actual\\(hoy)}; +\node[anchor=east, font=\small\bfseries, align=right] + at (axis cs:-5,dedicado) {Host dedicado\\(estimado)}; +\node[anchor=west, font=\normalsize\bfseries] at (axis cs:110,actual) {$\sim$100}; +\node[anchor=west, font=\normalsize\bfseries] at (axis cs:410,dedicado) {$\sim$400}; +\draw[ipsblue, ultra thick, dashed] + (axis cs:139,actual) -- (axis cs:139,dedicado); +\node[font=\scriptsize\bfseries, text=ipsblue, anchor=south, align=center] + at (axis cs:139,dedicado) {meta 139}; +\draw[warnorange, ultra thick, dashed] + (axis cs:570,actual) -- (axis cs:570,dedicado); +\node[font=\scriptsize\bfseries, text=warnorange, anchor=south, align=center] + at (axis cs:570,dedicado) {meta 570}; +\end{axis} +\end{tikzpicture} +\caption{Barras = capacidad. L\'ineas = metas. Rojo no llega a 139; verde llega a 139 pero no a 570.} +\label{fig:veredicto-brecha} +\end{figure} + +\section{Si hoy no est\'a garantizado: qu\'e hace falta} +\begin{enumerate} + \item \textbf{Host dedicado} (sin MLflow/Airflow/n8n); RAM $\geq$16--32\,GiB. + \item \textbf{OCR con margen} hasta sostener $\geq$139 fact/h bajo carga real. + \item \textbf{LLM async} (\texttt{ASYNC\_QUEUE=true}, 1--2 workers; ya $\approx$506/h medidos). + \item \textbf{Agent Selenium:} 1 PC basta para 139/h; $N$ PCs para 570/h. + \item \textbf{Presupuesto LLM} $\approx$ USD\,2\,570/mes si pasan 100k a extracci\'on. + \item \textbf{Validar E2E} de cientos de PDFs punta a punta (a\'un pendiente). +\end{enumerate} + +\section{Por meta operativa} +\begin{description} + \item[100k/mes en 24$\times$7 ($\geq$139 fact/h)] + Plausible en host dedicado. En compartido depende de que el OCR no se quede en el piso $\sim$100. + \item[100k/mes en jornada 8\,h ($\geq$570 fact/h)] + No con el estimado dedicado ($\sim$400). Escalar OCR, agents Selenium y LLM, y re-medir. +\end{description} diff --git a/docs/plan_pruebas/capacidad_100k/sections/07_requisitos.tex b/docs/plan_pruebas/capacidad_100k/sections/07_requisitos.tex new file mode 100644 index 0000000..d5f5141 --- /dev/null +++ b/docs/plan_pruebas/capacidad_100k/sections/07_requisitos.tex @@ -0,0 +1,36 @@ +\chapter{Requisitos sugeridos y pr\'oximos pasos} +\label{ch:requisitos} + +\section{Para lograr 100k/mes (lista simple)} +Si hoy es \fail\ en el host compartido, esto es lo m\'inimo para apuntar a \pass\ en 24$\times$7: + +\begin{center} +\renewcommand{\arraystretch}{1.3} +\begin{tabular}{@{}clp{9.2cm}@{}} +\toprule +\textbf{\#} & \textbf{Estado} & \textbf{Qu\'e} \\ +\midrule +1 & Obligatorio & Host \textbf{dedicado} IPS (sin MLflow/Airflow/n8n); RAM $\geq$16--32\,GiB \\ +2 & Obligatorio & OCR con margen (CPU/RAM o r\'eplicas) hasta $\geq$139 fact/h bajo carga \\ +3 & Obligatorio & \texttt{ASYNC\_QUEUE=true} + 1--2 workers LLM \\ +4 & Obligatorio & Presupuesto LLM $\approx$ USD\,2\,570/mes (si pasan 100k a extracci\'on) \\ +5 & Seg\'un meta & 1 agent Selenium basta para 139/h; $N$ agents PC para 570/h \\ +6 & Validaci\'on & Repetir ST-COUNT-01 / ST-EXT-01 lab + E2E de cientos de PDFs \\ +\bottomrule +\end{tabular} +\end{center} + +\section{Pr\'oximas mediciones} +\begin{enumerate} + \item ST-COUNT-01 perfil lab ($\geq$3\,GiB libres). + \item ST-EXT-01 perfil lab (100--500 docs, 2 workers). + \item E2E controlado (200--500 PDFs punta a punta). + \item Tokens reales del LLM para validar USD\,0{,}0257/doc. +\end{enumerate} + +\section{Documentos relacionados} +\begin{itemize} + \item \textbf{Plan de pruebas} --- estrategia e IDs. + \item \textbf{Informe de resultados} --- m\'etricas de la campa\~na. + \item Modelo Markdown: \texttt{docs/plan\_pruebas/scripts/estimate\_capacity.md}. +\end{itemize} diff --git a/docs/plan_pruebas/diagrams/arquitectura.tex b/docs/plan_pruebas/diagrams/arquitectura.tex index 6d9f6db..63635b9 100644 --- a/docs/plan_pruebas/diagrams/arquitectura.tex +++ b/docs/plan_pruebas/diagrams/arquitectura.tex @@ -2,35 +2,47 @@ \centering \begin{tikzpicture}[ font=\small, - box/.style={draw=ipsblue, rounded corners=2pt, align=center, inner sep=4pt, minimum height=0.85cm, minimum width=2.4cm}, + box/.style={draw=ipsblue, rounded corners=2pt, align=center, inner sep=4pt, minimum height=0.85cm, minimum width=2.3cm}, ext/.style={draw=ipsgrey, dashed, rounded corners=2pt, align=center, inner sep=4pt, minimum height=0.85cm}, - arr/.style={-{Latex}, thick, ipsblue} + arr/.style={-{Latex}, thick, ipsblue}, + dbarr/.style={-{Latex}, thick, ipsgrey} ] \node[box, fill=codebg] (desk) {Desktop\\Electron}; - \node[box, fill=codebg, right=1.8cm of desk] (gw) {api\_gateway\\:8189}; - \node[box, below=1.1cm of gw] (ext) {extraction\\:8001}; - \node[box, left=0.8cm of ext] (clean) {cleaning\\:8002}; - \node[box, right=0.8cm of ext] (count) {count-api\\:8080}; - \node[box, below=1.1cm of ext] (bot) {bot-descarga\\:8200}; - \node[box, below left=1.0cm and 0.2cm of bot] (pg) {PostgreSQL}; - \node[box, below right=1.0cm and 0.2cm of bot] (redis) {Redis}; - \node[ext, left=1.6cm of desk] (agent) {Agent PC\\Selenium}; - \node[ext, below=2.6cm of agent] (minio) {MinIO/S3}; - \node[ext, below=0.9cm of minio] (img) {Imaging}; + \node[box, fill=codebg, right=1.7cm of desk] (gw) {api\_gateway\\:8189}; + \node[box, below=1.05cm of gw] (ext) {extraction\\(+ workers)}; + \node[box, left=0.7cm of ext] (clean) {cleaning\\:8002}; + \node[box, right=0.7cm of ext] (count) {count-api\\:8080}; + \node[box, below=1.05cm of ext] (bot) {bot-descarga\\orquestador}; + \node[box, below left=1.15cm and 1.4cm of bot] (pg) {PostgreSQL}; + \node[box, below right=1.15cm and 1.4cm of bot] (redis) {Redis}; + \node[ext, left=1.5cm of desk] (agent) {Agent Selenium\\(PC externo)}; + \node[ext, below=2.5cm of agent] (minio) {MinIO/S3}; + \node[ext, below=0.85cm of minio] (img) {Imaging}; + % HTTP via gateway \draw[arr] (desk) -- (gw); \draw[arr] (gw) -- (ext); \draw[arr] (gw) -- (clean); \draw[arr] (gw) -- (count); \draw[arr] (gw) -- (bot); - \draw[arr] (bot) -- (pg); - \draw[arr] (ext) -- (pg); - \draw[arr] (ext) -- (redis); \draw[arr] (agent) |- (gw); + + % PostgreSQL: solo servicios de dominio (NO gateway, NO count-api) + \draw[dbarr] (ext) -- (pg); + \draw[dbarr] (clean) |- (pg); + \draw[dbarr] (bot) -- (pg); + + % Redis: cola ARQ (extraction/workers) + \draw[dbarr] (ext) -- (redis); + + % MinIO / Imaging \draw[arr] (agent) -- (minio); \draw[arr] (bot) -| (minio); \draw[arr] (agent) -- (img); \end{tikzpicture} -\caption{Arquitectura IPSExtractor bajo prueba.} +\caption{Arquitectura bajo prueba. El \textbf{gateway no habla con Postgres}: solo hace proxy HTTP. +Usan la BD compartida: \texttt{extraction} (+ workers), \texttt{cleaning} y \texttt{bot-descarga}. +\texttt{count-api} hace OCR y \textbf{devuelve JSON} (no escribe en PG); el Desktop/extraction +persiste esos resultados con \texttt{POST /count-jobs} en Postgres.} \label{fig:arquitectura} \end{figure} diff --git a/docs/plan_pruebas/diagrams/patrones.tex b/docs/plan_pruebas/diagrams/patrones.tex new file mode 100644 index 0000000..7257cf3 --- /dev/null +++ b/docs/plan_pruebas/diagrams/patrones.tex @@ -0,0 +1,40 @@ +\begin{figure}[H] +\centering +\resizebox{\textwidth}{!}{% +\begin{tikzpicture}[ + font=\footnotesize, + node distance=0.45cm and 0.55cm, + box/.style={draw=ipsblue, rounded corners=3pt, align=center, inner sep=4pt, + minimum height=0.95cm, minimum width=2.55cm, fill=codebg, text=black}, + title/.style={font=\small\bfseries, text=ipsblue, anchor=west}, + arr/.style={-{Latex}, thick, draw=ipsblue} +] + % --- 1: API Gateway --- + \node[title] (t1) at (0,0) {API Gateway}; + \node[box, below=0.3cm of t1.west, anchor=west] (client) {Desktop / Agent}; + \node[box, right=of client] (gw) {Gateway :8189}; + \node[box, right=of gw] (svc) {Microservicios}; + \draw[arr] (client) -- (gw); + \draw[arr] (gw) -- (svc); + + % --- 2: ARQ --- + \node[title] (t2) at (0,-2.15) {Cola async (ARQ)}; + \node[box, below=0.3cm of t2.west, anchor=west] (api) {extraction API}; + \node[box, right=of api] (redis) {Redis / ARQ}; + \node[box, right=of redis] (w) {Workers $\times N$}; + \draw[arr] (api) -- (redis); + \draw[arr] (redis) -- (w); + + % --- 3: Claim --- + \node[title] (t3) at (0,-4.3) {Claim / lease (orquestaci\'on)}; + \node[box, below=0.3cm of t3.west, anchor=west] (store) {bot-descarga\\(orquestador)}; + \node[box, right=of store] (claim) {claim at\'omico}; + \node[box, right=of claim] (agent) {Agent Selenium\\(PC)}; + \draw[arr] (store) -- (claim); + \draw[arr] (claim) -- (agent); +\end{tikzpicture}% +} +\caption{Patrones: Gateway, cola ARQ y claim. El servicio \texttt{bot-descarga} \textbf{orquesta}; +el \textbf{agent Selenium} en el PC ejecuta la descarga.} +\label{fig:patrones} +\end{figure} diff --git a/docs/plan_pruebas/diagrams/pipeline_negocio.tex b/docs/plan_pruebas/diagrams/pipeline_negocio.tex new file mode 100644 index 0000000..be1dd84 --- /dev/null +++ b/docs/plan_pruebas/diagrams/pipeline_negocio.tex @@ -0,0 +1,30 @@ +\begin{figure}[ht] +\centering +\begin{tikzpicture}[ + font=\small, + stage/.style={draw=ipsblue, rounded corners=3pt, align=center, inner sep=5pt, + minimum height=1.0cm, minimum width=2.15cm, fill=codebg}, + arr/.style={-{Latex}, thick, ipsblue} +] + \node[stage] (dl) {1. Descarga\\agent Selenium}; + \node[stage, right=0.5cm of dl] (cnt) {2. Conteo\\OCR/ONNX}; + \node[stage, right=0.5cm of cnt] (ext) {3. Extracci\'on\\LLM + ARQ}; + \node[stage, right=0.5cm of ext] (cln) {4. Limpieza\\CSV/reglas}; + + \draw[arr] (dl) -- (cnt); + \draw[arr] (cnt) -- (ext); + \draw[arr] (ext) -- (cln); + + \node[below=0.35cm of dl, font=\footnotesize\itshape, text=ipsgrey, align=center] + {orquestado por\\bot-descarga}; + \node[below=0.35cm of cnt, font=\footnotesize\itshape, text=ipsgrey, align=center] + {filtro}; + \node[below=0.35cm of ext, font=\footnotesize\itshape, text=ipsgrey, align=center] + {filas PG}; + \node[below=0.35cm of cln, font=\footnotesize\itshape, text=ipsgrey, align=center] + {lote limpio}; +\end{tikzpicture} +\caption{Pipeline punta a punta. La descarga la hace el \textbf{agent Selenium} (PC); +el servicio \texttt{bot-descarga} solo orquesta jobs/claim/heartbeats.} +\label{fig:pipeline-negocio} +\end{figure} diff --git a/docs/plan_pruebas/informe/README.md b/docs/plan_pruebas/informe/README.md index a31f97f..924a07c 100644 --- a/docs/plan_pruebas/informe/README.md +++ b/docs/plan_pruebas/informe/README.md @@ -1,6 +1,8 @@ # Informe de Pruebas IPSExtractor -Entregable profesional de **resultados** (métricas, gráficas, capacidad 100k/mes). +Entregable de **resultados** (métricas, gráficas, interpretación). + +El análisis de **100 000 facturas/mes** está en [`../capacidad_100k/`](../capacidad_100k/). ## Compilar @@ -16,5 +18,5 @@ Salida: [`../InformePruebas_IPSExtractor.pdf`](../InformePruebas_IPSExtractor.pd ## Datos - `data/metricas.csv` — latencias k6 -- `data/capacidad.csv` — modelo 100k/mes +- `data/capacidad.csv` — modelo 100k/mes (referencia; detalle en PDF Capacidad) - `data/memoria.csv` — RAM contenedores diff --git a/docs/plan_pruebas/informe/main.aux b/docs/plan_pruebas/informe/main.aux index f3356c1..d8ba169 100644 --- a/docs/plan_pruebas/informe/main.aux +++ b/docs/plan_pruebas/informe/main.aux @@ -9,148 +9,191 @@ \providecommand\HyField@AuxAddToFields[1]{} \providecommand\HyField@AuxAddToCoFields[2]{} \babel@aux{spanish}{} -\@writefile{toc}{\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{5}{chapter.1}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{4}{chapter.1}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:resumen}{{1}{5}{Resumen ejecutivo}{chapter.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {1.1}Veredicto global}{5}{section.1.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{5}{section.1.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces KPIs ejecutivos de la campa\~na.}}{5}{table.1.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {1.3}Respuesta directa: 100\,000 facturas/mes}{5}{section.1.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {1.4}Riesgo principal}{6}{section.1.4}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {2}Objeto y alcance}{7}{chapter.2}\protected@file@percent } +\newlabel{ch:resumen}{{1}{4}{Resumen ejecutivo}{chapter.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {1.1}Veredicto global}{4}{section.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{4}{section.1.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces KPIs (carga 13-jul; LLM/ARQ/OCR 17-jul).}}{4}{table.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.3}Capacidad 100k/mes (resumen)}{5}{section.1.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.4}Riesgo principal}{5}{section.1.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.5}Alcance de esta campa\~na}{5}{section.1.5}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura y patrones (contexto)}{6}{chapter.2}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:alcance}{{2}{7}{Objeto y alcance}{chapter.2}{}} -\@writefile{toc}{\contentsline {section}{\numberline {2.1}Objeto}{7}{section.2.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {2.2}Alcance incluido}{7}{section.2.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {2.3}Fuera de alcance de esta campa\~na}{7}{section.2.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {2.4}Definici\'on de factura}{7}{section.2.4}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{8}{chapter.3}\protected@file@percent } +\newlabel{ch:arq-informe}{{2}{6}{Arquitectura y patrones (contexto)}{chapter.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.1}Vista de componentes}{6}{section.2.1}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres.}}{6}{figure.2.1}\protected@file@percent } +\newlabel{fig:arquitectura}{{2.1}{6}{Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres}{figure.2.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga.}}{7}{figure.2.2}\protected@file@percent } +\newlabel{fig:patrones}{{2.2}{7}{Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga}{figure.2.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.3}Pipeline de negocio}{7}{section.2.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.3}{\ignorespaces Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats.}}{8}{figure.2.3}\protected@file@percent } +\newlabel{fig:pipeline-negocio}{{2.3}{8}{Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats}{figure.2.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.4}Definici\'on de factura}{8}{section.2.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{9}{chapter.3}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:entorno}{{3}{8}{Entorno bajo prueba}{chapter.3}{}} -\@writefile{toc}{\contentsline {section}{\numberline {3.1}Hardware y sistema}{8}{section.3.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Host de ejecuci\'on.}}{8}{table.3.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{8}{section.3.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {3.3}Salud del stack}{8}{section.3.3}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Huella de memoria por contenedor (idle). MLflow domina el host.}}{9}{figure.3.1}\protected@file@percent } -\newlabel{fig:memoria}{{3.1}{9}{Huella de memoria por contenedor (idle). MLflow domina el host}{figure.3.1}{}} -\@writefile{toc}{\contentsline {chapter}{\numberline {4}Metodolog\'ia}{10}{chapter.4}\protected@file@percent } +\newlabel{ch:entorno}{{3}{9}{Entorno bajo prueba}{chapter.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.1}Hardware y sistema}{9}{section.3.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Host de ejecuci\'on.}}{9}{table.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{9}{section.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.3}Salud del stack}{9}{section.3.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Huella de memoria por contenedor (idle). MLflow domina el host.}}{10}{figure.3.1}\protected@file@percent } +\newlabel{fig:memoria}{{3.1}{10}{Huella de memoria por contenedor (idle). MLflow domina el host}{figure.3.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {4}C\'omo leer este informe}{11}{chapter.4}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:metodologia}{{4}{10}{Metodolog\'ia}{chapter.4}{}} -\@writefile{toc}{\contentsline {section}{\numberline {4.1}Tipos de prueba}{10}{section.4.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.2}Herramientas}{10}{section.4.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.3}SLOs perfil \texttt {shared8g}}{10}{section.4.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.4}Comandos representativos}{10}{section.4.4}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{11}{chapter.5}\protected@file@percent } +\newlabel{ch:como-leer}{{4}{11}{C\'omo leer este informe}{chapter.4}{}} +\@writefile{toc}{\contentsline {section}{\numberline {4.1}Plantilla de interpretaci\'on}{11}{section.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.2}Leyenda de estados}{11}{section.4.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.3}Perfil de carga \texttt {shared8g}}{11}{section.4.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.4}Herramientas}{11}{section.4.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.5}Mapa de cap\'itulos de resultados}{12}{section.4.5}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{13}{chapter.5}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:unitarias}{{5}{11}{Resultados --- pruebas unitarias}{chapter.5}{}} -\@writefile{toc}{\contentsline {section}{\numberline {5.1}Scorecard}{11}{section.5.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Resultados unitarios.}}{11}{table.5.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {5.2}Cobertura agent}{11}{section.5.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {5.3}Cobertura API store}{11}{section.5.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {5.4}Cobertura count-api}{11}{section.5.4}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {5.1}{\ignorespaces Unitarias ejecutadas vs pendientes.}}{12}{figure.5.1}\protected@file@percent } -\newlabel{fig:unitarias}{{5.1}{12}{Unitarias ejecutadas vs pendientes}{figure.5.1}{}} -\@writefile{toc}{\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{13}{chapter.6}\protected@file@percent } +\newlabel{ch:unitarias}{{5}{13}{Resultados --- pruebas unitarias}{chapter.5}{}} +\@writefile{toc}{\contentsline {section}{\numberline {5.1}Qu\'e eval\'uan}{13}{section.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.2}Scorecard}{13}{section.5.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Resultados unitarios.}}{13}{table.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.3}Cobertura agent Selenium}{13}{section.5.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.4}Cobertura API \texttt {bot-descarga} (orquestador)}{14}{section.5.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.5}Cobertura count-api}{14}{section.5.5}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.6}Cobertura desktop (Vitest)}{14}{section.5.6}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {5.1}{\ignorespaces Unitarias ejecutadas (todas en verde tras Vitest desktop).}}{14}{figure.5.1}\protected@file@percent } +\newlabel{fig:unitarias}{{5.1}{14}{Unitarias ejecutadas (todas en verde tras Vitest desktop)}{figure.5.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{15}{chapter.6}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:humo}{{6}{13}{Resultados --- humo y funcional}{chapter.6}{}} -\@writefile{toc}{\contentsline {section}{\numberline {6.1}Humo gateway}{13}{section.6.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {6.1}{\ignorespaces Humo HTTP.}}{13}{table.6.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {6.2}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{13}{section.6.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {6.3}Script preparado}{13}{section.6.3}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{14}{chapter.7}\protected@file@percent } +\newlabel{ch:humo}{{6}{15}{Resultados --- humo y funcional}{chapter.6}{}} +\@writefile{toc}{\contentsline {section}{\numberline {6.1}Qu\'e eval\'uan}{15}{section.6.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.2}Humo gateway}{15}{section.6.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {6.1}{\ignorespaces Humo HTTP.}}{15}{table.6.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.3}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{15}{section.6.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.4}Scripts}{16}{section.6.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{17}{chapter.7}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:carga}{{7}{14}{Resultados --- pruebas de carga}{chapter.7}{}} -\@writefile{toc}{\contentsline {section}{\numberline {7.1}Tabla consolidada}{14}{section.7.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces M\'etricas de carga k6 (perfil shared8g).}}{14}{table.7.1}\protected@file@percent } -\newlabel{tab:carga}{{7.1}{14}{M\'etricas de carga k6 (perfil shared8g)}{table.7.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {7.2}ST-GW-01}{14}{section.7.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {7.3}ST-BOT-01 --- comparativa}{14}{section.7.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {7.4}Throughput gateway}{14}{section.7.4}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {7.1}{\ignorespaces p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla.}}{15}{figure.7.1}\protected@file@percent } -\newlabel{fig:latencias}{{7.1}{15}{p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla}{figure.7.1}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {7.2}{\ignorespaces Before/after heartbeats (eje limitado; valores exactos en tabla~\ref {tab:carga}).}}{15}{figure.7.2}\protected@file@percent } -\newlabel{fig:bot-before-after}{{7.2}{15}{Before/after heartbeats (eje limitado; valores exactos en tabla~\ref {tab:carga})}{figure.7.2}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {7.3}{\ignorespaces Throughput observado en health liveness (no equivale a PDFs/h).}}{15}{figure.7.3}\protected@file@percent } -\newlabel{fig:throughput}{{7.3}{15}{Throughput observado en health liveness (no equivale a PDFs/h)}{figure.7.3}{}} -\@writefile{toc}{\contentsline {chapter}{\numberline {8}Capacidad: 100\,000 facturas al mes}{16}{chapter.8}\protected@file@percent } +\newlabel{ch:carga}{{7}{17}{Resultados --- pruebas de carga}{chapter.7}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.1}Qu\'e eval\'uan}{17}{section.7.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.2}Tabla consolidada}{17}{section.7.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces M\'etricas de carga k6 (perfil shared8g).}}{17}{table.7.1}\protected@file@percent } +\newlabel{tab:carga}{{7.1}{17}{M\'etricas de carga k6 (perfil shared8g)}{table.7.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.3}ST-GW-01}{17}{section.7.3}\protected@file@percent } +\newlabel{sec:st-gw-01}{{7.3}{17}{ST-GW-01}{section.7.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.4}ST-BOT-01 --- comparativa}{18}{section.7.4}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {7.1}{\ignorespaces Misma prueba ST-BOT-01, distinto estado del host. (*) El m\'aximo real de la corrida 1 fue 23{,}1\,s; la barra est\'a recortada solo para que se lean el resto de valores. Detalle num\'erico en la tabla~\ref {tab:carga}.}}{19}{figure.7.1}\protected@file@percent } +\newlabel{fig:bot-before-after}{{7.1}{19}{Misma prueba ST-BOT-01, distinto estado del host. (*) El m\'aximo real de la corrida 1 fue 23{,}1\,s; la barra est\'a recortada solo para que se lean el resto de valores. Detalle num\'erico en la tabla~\ref {tab:carga}}{figure.7.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.5}Throughput gateway}{19}{section.7.5}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {7.2}{\ignorespaces Ritmo del gateway en health liveness ($\approx $57\,req/s). Solo mide HTTP ligero, no facturas procesadas.}}{19}{figure.7.2}\protected@file@percent } +\newlabel{fig:throughput}{{7.2}{19}{Ritmo del gateway en health liveness ($\approx $57\,req/s). Solo mide HTTP ligero, no facturas procesadas}{figure.7.2}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {8}Count-api: qu\'e eval\'ua cada prueba}{20}{chapter.8}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:capacidad}{{8}{16}{Capacidad: 100\,000 facturas al mes}{chapter.8}{}} -\@writefile{toc}{\contentsline {section}{\numberline {8.1}Traducci\'on del objetivo}{16}{section.8.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {8.1}{\ignorespaces Throughput m\'inimo para cumplir 100k/mes.}}{16}{table.8.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {8.2}Modelo por etapa}{16}{section.8.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {8.2}{\ignorespaces Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger }$Con $N$ agents en paralelo.}}{16}{table.8.2}\protected@file@percent } -\newlabel{tab:capacidad}{{8.2}{16}{Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger }$Con $N$ agents en paralelo}{table.8.2}{}} -\@writefile{toc}{\contentsline {section}{\numberline {8.3}Interpretaci\'on}{16}{section.8.3}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {8.1}{\ignorespaces Brecha de capacidad. Gateway real $\gg $1000/h; se trunca para legibilidad.}}{17}{figure.8.1}\protected@file@percent } -\newlabel{fig:capacidad}{{8.1}{17}{Brecha de capacidad. Gateway real $\gg $1000/h; se trunca para legibilidad}{figure.8.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {8.4}Veredicto}{17}{section.8.4}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {8.5}Requisitos m\'inimos sugeridos para 100k/mes}{17}{section.8.5}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {9}An\'alisis y hallazgos}{18}{chapter.9}\protected@file@percent } +\newlabel{ch:count-latency}{{8}{20}{Count-api: qu\'e eval\'ua cada prueba}{chapter.8}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.1}Qu\'e eval\'ua / resultado / significado}{20}{section.8.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.2}Pipeline bajo prueba}{20}{section.8.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {8.1}{\ignorespaces Scorecard count-api --- campa\~na 23 PDFs (13 jul 2026).}}{21}{table.8.1}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {8.1}{\ignorespaces Cobertura count-api: 11 unitarias + 23 PDFs medidos (HTTP y Go).}}{21}{figure.8.1}\protected@file@percent } +\newlabel{fig:count-score}{{8.1}{21}{Cobertura count-api: 11 unitarias + 23 PDFs medidos (HTTP y Go)}{figure.8.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.3}Resultado global count-api}{21}{section.8.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.4}Features: qu\'e se eval\'ua (con ejemplo)}{21}{section.8.4}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.4}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.5}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.6}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.7}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.5}Clustering: qu\'e se eval\'ua (con ejemplo)}{22}{section.8.5}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.3}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.4}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {8.2}{\ignorespaces Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales).}}{23}{figure.8.2}\protected@file@percent } +\newlabel{fig:cluster-idea}{{8.2}{23}{Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales)}{figure.8.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.6}Pruebas con PDFs reales}{23}{section.8.6}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.6.1}Fixtures}{23}{subsection.8.6.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.6.2}TestProcessPDFTimingFixtures}{23}{subsection.8.6.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {8.6.3}Script HTTP \texttt {measure\_count\_latency}}{23}{subsection.8.6.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.7}Resultados agregados (23 PDFs)}{23}{section.8.7}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {8.2}{\ignorespaces Estad\'isticas sobre 23 facturas. HTTP v\'ia gateway; Go v\'ia \texttt {ProcessPDF} directo.}}{23}{table.8.2}\protected@file@percent } +\newlabel{tab:count-stats}{{8.2}{23}{Estad\'isticas sobre 23 facturas. HTTP v\'ia gateway; Go v\'ia \texttt {ProcessPDF} directo}{table.8.2}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {8.3}{\ignorespaces El OCR representa $\sim $93\,\% del tiempo de procesamiento.}}{24}{figure.8.3}\protected@file@percent } +\newlabel{fig:count-breakdown}{{8.3}{24}{El OCR representa $\sim $93\,\% del tiempo de procesamiento}{figure.8.3}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {8.4}{\ignorespaces Serie de tiempos totales. L\'inea punteada: promedio 2008\,ms.}}{24}{figure.8.4}\protected@file@percent } +\newlabel{fig:count-series}{{8.4}{24}{Serie de tiempos totales. L\'inea punteada: promedio 2008\,ms}{figure.8.4}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.8}Resultados de latencia HTTP}{24}{section.8.8}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {8.3}{\ignorespaces Resumen HTTP (sin concurrencia). Fuente: CSV del 13 jul 2026.}}{24}{table.8.3}\protected@file@percent } +\newlabel{tab:count-http}{{8.3}{24}{Resumen HTTP (sin concurrencia). Fuente: CSV del 13 jul 2026}{table.8.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {8.9}Detalle por archivo}{25}{section.8.9}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {8.10}C\'omo reproducir}{25}{section.8.10}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {9}Extracci\'on LLM: bench de latencia (ST-EXT-02)}{26}{chapter.9}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:analisis}{{9}{18}{An\'alisis y hallazgos}{chapter.9}{}} -\@writefile{toc}{\contentsline {section}{\numberline {9.1}Hallazgos}{18}{section.9.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {9.2}Scorecard global}{18}{section.9.2}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {9.1}{\ignorespaces Distribuci\'on de resultados por tipo de prueba.}}{18}{figure.9.1}\protected@file@percent } -\newlabel{fig:scorecard}{{9.1}{18}{Distribuci\'on de resultados por tipo de prueba}{figure.9.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {9.3}Limitaciones}{18}{section.9.3}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {10}Conclusiones y recomendaciones}{19}{chapter.10}\protected@file@percent } +\newlabel{ch:llm-latency}{{9}{26}{Extracci\'on LLM: bench de latencia (ST-EXT-02)}{chapter.9}{}} +\@writefile{toc}{\contentsline {section}{\numberline {9.1}Qu\'e eval\'ua / resultado / significado}{26}{section.9.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {9.2}C\'omo ejecutar}{26}{section.9.2}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {9.1}ST-EXT-02 (Windows PowerShell)}{26}{lstlisting.9.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {9.3}Resultados (2026-07-17)}{27}{section.9.3}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {9.1}{\ignorespaces ST-EXT-02 --- latencia LLM medida (mismo schema/campos).}}{27}{table.9.1}\protected@file@percent } +\newlabel{tab:llm-latency}{{9.1}{27}{ST-EXT-02 --- latencia LLM medida (mismo schema/campos)}{table.9.1}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {9.1}{\ignorespaces La etapa LLM sola supera 139/h; con 2 workers supera 570/h (sin cuota).}}{27}{figure.9.1}\protected@file@percent } +\newlabel{fig:llm-vs-obj}{{9.1}{27}{La etapa LLM sola supera 139/h; con 2 workers supera 570/h (sin cuota)}{figure.9.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {9.4}Lectura para capacidad}{27}{section.9.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {10}Cola ARQ: bench ST-EXT-01 (LLM mock)}{28}{chapter.10}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:conclusiones}{{10}{19}{Conclusiones y recomendaciones}{chapter.10}{}} -\@writefile{toc}{\contentsline {section}{\numberline {10.1}Conclusiones}{19}{section.10.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {10.2}Recomendaciones}{19}{section.10.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {10.3}Pr\'oxima campa\~na sugerida}{19}{section.10.3}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {11}Anexos}{20}{chapter.11}\protected@file@percent } +\newlabel{ch:arq-ext01}{{10}{28}{Cola ARQ: bench ST-EXT-01 (LLM mock)}{chapter.10}{}} +\@writefile{toc}{\contentsline {section}{\numberline {10.1}Qu\'e eval\'ua / resultado / significado}{28}{section.10.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {10.2}Condiciones de la corrida}{28}{section.10.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {10.1}{\ignorespaces ST-EXT-01 --- cola ARQ con LLM mock (shared8g, 30 docs).}}{29}{table.10.1}\protected@file@percent } +\newlabel{tab:st-ext-01}{{10.1}{29}{ST-EXT-01 --- cola ARQ con LLM mock (shared8g, 30 docs)}{table.10.1}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {10.1}{\ignorespaces Casi todo el tiempo es drenado por workers; el enqueue es despreciable.}}{29}{figure.10.1}\protected@file@percent } +\newlabel{fig:st-ext-01-times}{{10.1}{29}{Casi todo el tiempo es drenado por workers; el enqueue es despreciable}{figure.10.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {10.3}Resultados}{29}{section.10.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {10.4}Lectura}{29}{section.10.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {11}Count-api bajo carga (ST-COUNT-01)}{30}{chapter.11}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:anexos}{{11}{20}{Anexos}{chapter.11}{}} -\@writefile{toc}{\contentsline {section}{\numberline {11.1}Datos fuente}{20}{section.11.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.2}Extracto ST-GW-01}{20}{section.11.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.3}Extracto ST-BOT-01 \#2}{20}{section.11.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.4}Unitarias}{20}{section.11.4}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.5}Scripts}{20}{section.11.5}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {11.6}Reproducibilidad}{21}{section.11.6}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {12}Count-api: qu\'e eval\'ua cada prueba}{22}{chapter.12}\protected@file@percent } +\newlabel{ch:st-count-01}{{11}{30}{Count-api bajo carga (ST-COUNT-01)}{chapter.11}{}} +\@writefile{toc}{\contentsline {section}{\numberline {11.1}Qu\'e eval\'ua / resultado / significado}{30}{section.11.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.2}Condiciones}{30}{section.11.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.3}Resultados}{30}{section.11.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {11.4}Lectura}{30}{section.11.4}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {11.1}{\ignorespaces ST-COUNT-01 --- carga OCR (PDFs reales, shared8g).}}{31}{table.11.1}\protected@file@percent } +\newlabel{tab:st-count-01}{{11.1}{31}{ST-COUNT-01 --- carga OCR (PDFs reales, shared8g)}{table.11.1}{}} +\@writefile{lof}{\contentsline {figure}{\numberline {11.1}{\ignorespaces Bajo 3 concurrentes el OCR cubre 139/h en esta muestra; no alcanza 570/h. La latencia por PDF se degrada mucho frente a la serie ($\sim $2\,s).}}{31}{figure.11.1}\protected@file@percent } +\newlabel{fig:st-count-01}{{11.1}{31}{Bajo 3 concurrentes el OCR cubre 139/h en esta muestra; no alcanza 570/h. La latencia por PDF se degrada mucho frente a la serie ($\sim $2\,s)}{figure.11.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {12}An\'alisis y hallazgos}{32}{chapter.12}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:count-latency}{{12}{22}{Count-api: qu\'e eval\'ua cada prueba}{chapter.12}{}} -\@writefile{toc}{\contentsline {section}{\numberline {12.1}Pipeline bajo prueba}{22}{section.12.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.2}Resultado global count-api}{22}{section.12.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Scorecard count-api en esta campa\~na.}}{22}{table.12.1}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {12.1}{\ignorespaces Cobertura count-api por bloque.}}{22}{figure.12.1}\protected@file@percent } -\newlabel{fig:count-score}{{12.1}{22}{Cobertura count-api por bloque}{figure.12.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {12.3}Features: qu\'e se eval\'ua (con ejemplo)}{23}{section.12.3}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.1}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.3}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.4}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.5}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.6}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.7}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.4}Clustering: qu\'e se eval\'ua (con ejemplo)}{24}{section.12.4}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.1}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.3}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.4}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {12.2}{\ignorespaces Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales).}}{24}{figure.12.2}\protected@file@percent } -\newlabel{fig:cluster-idea}{{12.2}{24}{Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales)}{figure.12.2}{}} -\@writefile{toc}{\contentsline {section}{\numberline {12.5}Pruebas con PDFs reales}{24}{section.12.5}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.5.1}Fixtures}{24}{subsection.12.5.1}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.5.2}TestProcessPDFTimingFixtures}{25}{subsection.12.5.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.5.3}Script HTTP \texttt {measure\_count\_latency}}{25}{subsection.12.5.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.6}Resultados de latencia HTTP (medidos)}{25}{section.12.6}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {12.2}{\ignorespaces Wall-clock HTTP. Fuente: \texttt {informes/count\_latency\_20260713.csv}.}}{25}{table.12.2}\protected@file@percent } -\newlabel{tab:count-http}{{12.2}{25}{Wall-clock HTTP. Fuente: \texttt {informes/count\_latency\_20260713.csv}}{table.12.2}{}} -\@writefile{lof}{\contentsline {figure}{\numberline {12.3}{\ignorespaces Barras: tiempo por PDF. L\'inea naranja: promedio 2{,}98\,s.}}{25}{figure.12.3}\protected@file@percent } -\newlabel{fig:count-http-bars}{{12.3}{25}{Barras: tiempo por PDF. L\'inea naranja: promedio 2{,}98\,s}{figure.12.3}{}} -\@writefile{toc}{\contentsline {section}{\numberline {12.7}C\'omo reproducir}{25}{section.12.7}\protected@file@percent } -\gdef \@abspage@last{27} +\newlabel{ch:analisis}{{12}{32}{An\'alisis y hallazgos}{chapter.12}{}} +\@writefile{toc}{\contentsline {section}{\numberline {12.1}Hallazgos}{32}{section.12.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.2}Scorecard global}{32}{section.12.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.3}Limitaciones}{32}{section.12.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {12.1}{\ignorespaces Distribuci\'on de resultados por tipo de prueba.}}{33}{figure.12.1}\protected@file@percent } +\newlabel{fig:scorecard}{{12.1}{33}{Distribuci\'on de resultados por tipo de prueba}{figure.12.1}{}} +\@writefile{toc}{\contentsline {chapter}{\numberline {13}Conclusiones y recomendaciones}{34}{chapter.13}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:conclusiones}{{13}{34}{Conclusiones y recomendaciones}{chapter.13}{}} +\@writefile{toc}{\contentsline {section}{\numberline {13.1}Conclusiones}{34}{section.13.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {13.2}Recomendaciones}{34}{section.13.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {13.3}Documentos relacionados}{34}{section.13.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {14}Anexos}{35}{chapter.14}\protected@file@percent } +\@writefile{lof}{\addvspace {10\p@ }} +\@writefile{lot}{\addvspace {10\p@ }} +\newlabel{ch:anexos}{{14}{35}{Anexos}{chapter.14}{}} +\@writefile{toc}{\contentsline {section}{\numberline {14.1}Datos fuente}{35}{section.14.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.2}Extracto ST-GW-01}{35}{section.14.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.3}Extracto ST-BOT-01 \#2}{35}{section.14.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.4}Unitarias}{35}{section.14.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.5}Scripts}{35}{section.14.5}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.6}Extracto ST-EXT-01}{36}{section.14.6}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.7}Documentos hermanos}{36}{section.14.7}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {14.8}Reproducibilidad}{36}{section.14.8}\protected@file@percent } +\gdef \@abspage@last{37} diff --git a/docs/plan_pruebas/informe/main.out b/docs/plan_pruebas/informe/main.out index 305cf5d..1666359 100644 --- a/docs/plan_pruebas/informe/main.out +++ b/docs/plan_pruebas/informe/main.out @@ -1,76 +1,96 @@ \BOOKMARK [0][-]{chapter.1}{\376\377\000R\000e\000s\000u\000m\000e\000n\000\040\000e\000j\000e\000c\000u\000t\000i\000v\000o}{}% 1 \BOOKMARK [1][-]{section.1.1}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.1}% 2 \BOOKMARK [1][-]{section.1.2}{\376\377\000K\000P\000I\000s\000\040\000d\000e\000\040\000l\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.1}% 3 -\BOOKMARK [1][-]{section.1.3}{\376\377\000R\000e\000s\000p\000u\000e\000s\000t\000a\000\040\000d\000i\000r\000e\000c\000t\000a\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000/\000m\000e\000s}{chapter.1}% 4 +\BOOKMARK [1][-]{section.1.3}{\376\377\000C\000a\000p\000a\000c\000i\000d\000a\000d\000\040\0001\0000\0000\000k\000/\000m\000e\000s\000\040\000\050\000r\000e\000s\000u\000m\000e\000n\000\051}{chapter.1}% 4 \BOOKMARK [1][-]{section.1.4}{\376\377\000R\000i\000e\000s\000g\000o\000\040\000p\000r\000i\000n\000c\000i\000p\000a\000l}{chapter.1}% 5 -\BOOKMARK [0][-]{chapter.2}{\376\377\000O\000b\000j\000e\000t\000o\000\040\000y\000\040\000a\000l\000c\000a\000n\000c\000e}{}% 6 -\BOOKMARK [1][-]{section.2.1}{\376\377\000O\000b\000j\000e\000t\000o}{chapter.2}% 7 -\BOOKMARK [1][-]{section.2.2}{\376\377\000A\000l\000c\000a\000n\000c\000e\000\040\000i\000n\000c\000l\000u\000i\000d\000o}{chapter.2}% 8 -\BOOKMARK [1][-]{section.2.3}{\376\377\000F\000u\000e\000r\000a\000\040\000d\000e\000\040\000a\000l\000c\000a\000n\000c\000e\000\040\000d\000e\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.2}% 9 -\BOOKMARK [1][-]{section.2.4}{\376\377\000D\000e\000f\000i\000n\000i\000c\000i\000\363\000n\000\040\000d\000e\000\040\000f\000a\000c\000t\000u\000r\000a}{chapter.2}% 10 -\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 11 -\BOOKMARK [1][-]{section.3.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.3}% 12 -\BOOKMARK [1][-]{section.3.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000m\000e\000m\000o\000r\000i\000a\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\051}{chapter.3}% 13 -\BOOKMARK [1][-]{section.3.3}{\376\377\000S\000a\000l\000u\000d\000\040\000d\000e\000l\000\040\000s\000t\000a\000c\000k}{chapter.3}% 14 -\BOOKMARK [0][-]{chapter.4}{\376\377\000M\000e\000t\000o\000d\000o\000l\000o\000g\000\355\000a}{}% 15 -\BOOKMARK [1][-]{section.4.1}{\376\377\000T\000i\000p\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.4}% 16 -\BOOKMARK [1][-]{section.4.2}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s}{chapter.4}% 17 -\BOOKMARK [1][-]{section.4.3}{\376\377\000S\000L\000O\000s\000\040\000p\000e\000r\000f\000i\000l\000\040\000s\000h\000a\000r\000e\000d\0008\000g}{chapter.4}% 18 -\BOOKMARK [1][-]{section.4.4}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000r\000e\000p\000r\000e\000s\000e\000n\000t\000a\000t\000i\000v\000o\000s}{chapter.4}% 19 -\BOOKMARK [0][-]{chapter.5}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 20 -\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d}{chapter.5}% 21 -\BOOKMARK [1][-]{section.5.2}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000a\000g\000e\000n\000t}{chapter.5}% 22 -\BOOKMARK [1][-]{section.5.3}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000A\000P\000I\000\040\000s\000t\000o\000r\000e}{chapter.5}% 23 -\BOOKMARK [1][-]{section.5.4}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.5}% 24 -\BOOKMARK [0][-]{chapter.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000h\000u\000m\000o\000\040\000y\000\040\000f\000u\000n\000c\000i\000o\000n\000a\000l}{}% 25 -\BOOKMARK [1][-]{section.6.1}{\376\377\000H\000u\000m\000o\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.6}% 26 -\BOOKMARK [1][-]{section.6.2}{\376\377\000F\000u\000n\000c\000i\000o\000n\000a\000l\000\040\000E\0002\000E\000-\0000\0007\000\040\000\050\000w\000o\000r\000k\000e\000r\000\040\000o\000f\000f\000l\000i\000n\000e\000\040\000\040\0004\0000\0009\000\051}{chapter.6}% 27 -\BOOKMARK [1][-]{section.6.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000p\000r\000e\000p\000a\000r\000a\000d\000o}{chapter.6}% 28 -\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{}% 29 -\BOOKMARK [1][-]{section.7.1}{\376\377\000T\000a\000b\000l\000a\000\040\000c\000o\000n\000s\000o\000l\000i\000d\000a\000d\000a}{chapter.7}% 30 -\BOOKMARK [1][-]{section.7.2}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.7}% 31 -\BOOKMARK [1][-]{section.7.3}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000c\000o\000m\000p\000a\000r\000a\000t\000i\000v\000a}{chapter.7}% 32 -\BOOKMARK [1][-]{section.7.4}{\376\377\000T\000h\000r\000o\000u\000g\000h\000p\000u\000t\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.7}% 33 -\BOOKMARK [0][-]{chapter.8}{\376\377\000C\000a\000p\000a\000c\000i\000d\000a\000d\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000\040\000a\000l\000\040\000m\000e\000s}{}% 34 -\BOOKMARK [1][-]{section.8.1}{\376\377\000T\000r\000a\000d\000u\000c\000c\000i\000\363\000n\000\040\000d\000e\000l\000\040\000o\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 35 -\BOOKMARK [1][-]{section.8.2}{\376\377\000M\000o\000d\000e\000l\000o\000\040\000p\000o\000r\000\040\000e\000t\000a\000p\000a}{chapter.8}% 36 -\BOOKMARK [1][-]{section.8.3}{\376\377\000I\000n\000t\000e\000r\000p\000r\000e\000t\000a\000c\000i\000\363\000n}{chapter.8}% 37 -\BOOKMARK [1][-]{section.8.4}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o}{chapter.8}% 38 -\BOOKMARK [1][-]{section.8.5}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000m\000\355\000n\000i\000m\000o\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000o\000s\000\040\000p\000a\000r\000a\000\040\0001\0000\0000\000k\000/\000m\000e\000s}{chapter.8}% 39 -\BOOKMARK [0][-]{chapter.9}{\376\377\000A\000n\000\341\000l\000i\000s\000i\000s\000\040\000y\000\040\000h\000a\000l\000l\000a\000z\000g\000o\000s}{}% 40 -\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.9}% 41 -\BOOKMARK [1][-]{section.9.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d\000\040\000g\000l\000o\000b\000a\000l}{chapter.9}% 42 -\BOOKMARK [1][-]{section.9.3}{\376\377\000L\000i\000m\000i\000t\000a\000c\000i\000o\000n\000e\000s}{chapter.9}% 43 -\BOOKMARK [0][-]{chapter.10}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s\000\040\000y\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{}% 44 -\BOOKMARK [1][-]{section.10.1}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s}{chapter.10}% 45 -\BOOKMARK [1][-]{section.10.2}{\376\377\000R\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{chapter.10}% 46 -\BOOKMARK [1][-]{section.10.3}{\376\377\000P\000r\000\363\000x\000i\000m\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000s\000u\000g\000e\000r\000i\000d\000a}{chapter.10}% 47 -\BOOKMARK [0][-]{chapter.11}{\376\377\000A\000n\000e\000x\000o\000s}{}% 48 -\BOOKMARK [1][-]{section.11.1}{\376\377\000D\000a\000t\000o\000s\000\040\000f\000u\000e\000n\000t\000e}{chapter.11}% 49 -\BOOKMARK [1][-]{section.11.2}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.11}% 50 -\BOOKMARK [1][-]{section.11.3}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\000\043\0002}{chapter.11}% 51 -\BOOKMARK [1][-]{section.11.4}{\376\377\000U\000n\000i\000t\000a\000r\000i\000a\000s}{chapter.11}% 52 -\BOOKMARK [1][-]{section.11.5}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.11}% 53 -\BOOKMARK [1][-]{section.11.6}{\376\377\000R\000e\000p\000r\000o\000d\000u\000c\000i\000b\000i\000l\000i\000d\000a\000d}{chapter.11}% 54 -\BOOKMARK [0][-]{chapter.12}{\376\377\000C\000o\000u\000n\000t\000-\000a\000p\000i\000:\000\040\000q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000c\000a\000d\000a\000\040\000p\000r\000u\000e\000b\000a}{}% 55 -\BOOKMARK [1][-]{section.12.1}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{chapter.12}% 56 -\BOOKMARK [1][-]{section.12.2}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000g\000l\000o\000b\000a\000l\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.12}% 57 -\BOOKMARK [1][-]{section.12.3}{\376\377\000F\000e\000a\000t\000u\000r\000e\000s\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.12}% 58 -\BOOKMARK [2][-]{subsection.12.3.1}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 59 -\BOOKMARK [2][-]{subsection.12.3.2}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000s\000C\000o\000n\000t\000i\000n\000u\000a\000t\000i\000o\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 60 -\BOOKMARK [2][-]{subsection.12.3.3}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000H\000e\000a\000d\000e\000r\000K\000e\000y\000w\000o\000r\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 61 -\BOOKMARK [2][-]{subsection.12.3.4}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000C\000o\000d\000e\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 62 -\BOOKMARK [2][-]{subsection.12.3.5}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000N\000o\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 63 -\BOOKMARK [2][-]{subsection.12.3.6}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 64 -\BOOKMARK [2][-]{subsection.12.3.7}{\376\377\000T\000e\000s\000t\000F\000e\000a\000t\000u\000r\000e\000C\000o\000u\000n\000t\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 65 -\BOOKMARK [1][-]{section.12.4}{\376\377\000C\000l\000u\000s\000t\000e\000r\000i\000n\000g\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.12}% 66 -\BOOKMARK [2][-]{subsection.12.4.1}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000T\000w\000o\000B\000a\000n\000d\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 67 -\BOOKMARK [2][-]{subsection.12.4.2}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000S\000a\000m\000e\000B\000a\000n\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 68 -\BOOKMARK [2][-]{subsection.12.4.3}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 69 -\BOOKMARK [2][-]{subsection.12.4.4}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000N\000o\000i\000s\000e\000M\000i\000n\000S\000a\000m\000p\000l\000e\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 70 -\BOOKMARK [1][-]{section.12.5}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000c\000o\000n\000\040\000P\000D\000F\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.12}% 71 -\BOOKMARK [2][-]{subsection.12.5.1}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s}{section.12.5}% 72 -\BOOKMARK [2][-]{subsection.12.5.2}{\376\377\000T\000e\000s\000t\000P\000r\000o\000c\000e\000s\000s\000P\000D\000F\000T\000i\000m\000i\000n\000g\000F\000i\000x\000t\000u\000r\000e\000s}{section.12.5}% 73 -\BOOKMARK [2][-]{subsection.12.5.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000H\000T\000T\000P\000\040\000m\000e\000a\000s\000u\000r\000e\000\137\000c\000o\000u\000n\000t\000\137\000l\000a\000t\000e\000n\000c\000y}{section.12.5}% 74 -\BOOKMARK [1][-]{section.12.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000d\000e\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000H\000T\000T\000P\000\040\000\050\000m\000e\000d\000i\000d\000o\000s\000\051}{chapter.12}% 75 -\BOOKMARK [1][-]{section.12.7}{\376\377\000C\000\363\000m\000o\000\040\000r\000e\000p\000r\000o\000d\000u\000c\000i\000r}{chapter.12}% 76 +\BOOKMARK [1][-]{section.1.5}{\376\377\000A\000l\000c\000a\000n\000c\000e\000\040\000d\000e\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.1}% 6 +\BOOKMARK [0][-]{chapter.2}{\376\377\000A\000r\000q\000u\000i\000t\000e\000c\000t\000u\000r\000a\000\040\000y\000\040\000p\000a\000t\000r\000o\000n\000e\000s\000\040\000\050\000c\000o\000n\000t\000e\000x\000t\000o\000\051}{}% 7 +\BOOKMARK [1][-]{section.2.1}{\376\377\000V\000i\000s\000t\000a\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 8 +\BOOKMARK [1][-]{section.2.2}{\376\377\000P\000a\000t\000r\000o\000n\000e\000s\000\040\000d\000e\000\040\000d\000i\000s\000e\000\361\000o}{chapter.2}% 9 +\BOOKMARK [1][-]{section.2.3}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 10 +\BOOKMARK [1][-]{section.2.4}{\376\377\000D\000e\000f\000i\000n\000i\000c\000i\000\363\000n\000\040\000d\000e\000\040\000f\000a\000c\000t\000u\000r\000a}{chapter.2}% 11 +\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 12 +\BOOKMARK [1][-]{section.3.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.3}% 13 +\BOOKMARK [1][-]{section.3.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000m\000e\000m\000o\000r\000i\000a\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\051}{chapter.3}% 14 +\BOOKMARK [1][-]{section.3.3}{\376\377\000S\000a\000l\000u\000d\000\040\000d\000e\000l\000\040\000s\000t\000a\000c\000k}{chapter.3}% 15 +\BOOKMARK [0][-]{chapter.4}{\376\377\000C\000\363\000m\000o\000\040\000l\000e\000e\000r\000\040\000e\000s\000t\000e\000\040\000i\000n\000f\000o\000r\000m\000e}{}% 16 +\BOOKMARK [1][-]{section.4.1}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000t\000e\000r\000p\000r\000e\000t\000a\000c\000i\000\363\000n}{chapter.4}% 17 +\BOOKMARK [1][-]{section.4.2}{\376\377\000L\000e\000y\000e\000n\000d\000a\000\040\000d\000e\000\040\000e\000s\000t\000a\000d\000o\000s}{chapter.4}% 18 +\BOOKMARK [1][-]{section.4.3}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000d\000e\000\040\000c\000a\000r\000g\000a\000\040\000s\000h\000a\000r\000e\000d\0008\000g}{chapter.4}% 19 +\BOOKMARK [1][-]{section.4.4}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s}{chapter.4}% 20 +\BOOKMARK [1][-]{section.4.5}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000c\000a\000p\000\355\000t\000u\000l\000o\000s\000\040\000d\000e\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000s}{chapter.4}% 21 +\BOOKMARK [0][-]{chapter.5}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 22 +\BOOKMARK [1][-]{section.5.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000n}{chapter.5}% 23 +\BOOKMARK [1][-]{section.5.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d}{chapter.5}% 24 +\BOOKMARK [1][-]{section.5.3}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000a\000g\000e\000n\000t\000\040\000S\000e\000l\000e\000n\000i\000u\000m}{chapter.5}% 25 +\BOOKMARK [1][-]{section.5.4}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000A\000P\000I\000\040\000b\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000\050\000o\000r\000q\000u\000e\000s\000t\000a\000d\000o\000r\000\051}{chapter.5}% 26 +\BOOKMARK [1][-]{section.5.5}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.5}% 27 +\BOOKMARK [1][-]{section.5.6}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000d\000e\000s\000k\000t\000o\000p\000\040\000\050\000V\000i\000t\000e\000s\000t\000\051}{chapter.5}% 28 +\BOOKMARK [0][-]{chapter.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000h\000u\000m\000o\000\040\000y\000\040\000f\000u\000n\000c\000i\000o\000n\000a\000l}{}% 29 +\BOOKMARK [1][-]{section.6.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000n}{chapter.6}% 30 +\BOOKMARK [1][-]{section.6.2}{\376\377\000H\000u\000m\000o\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.6}% 31 +\BOOKMARK [1][-]{section.6.3}{\376\377\000F\000u\000n\000c\000i\000o\000n\000a\000l\000\040\000E\0002\000E\000-\0000\0007\000\040\000\050\000w\000o\000r\000k\000e\000r\000\040\000o\000f\000f\000l\000i\000n\000e\000\040\000\040\0004\0000\0009\000\051}{chapter.6}% 32 +\BOOKMARK [1][-]{section.6.4}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.6}% 33 +\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{}% 34 +\BOOKMARK [1][-]{section.7.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000n}{chapter.7}% 35 +\BOOKMARK [1][-]{section.7.2}{\376\377\000T\000a\000b\000l\000a\000\040\000c\000o\000n\000s\000o\000l\000i\000d\000a\000d\000a}{chapter.7}% 36 +\BOOKMARK [1][-]{section.7.3}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.7}% 37 +\BOOKMARK [1][-]{section.7.4}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000c\000o\000m\000p\000a\000r\000a\000t\000i\000v\000a}{chapter.7}% 38 +\BOOKMARK [1][-]{section.7.5}{\376\377\000T\000h\000r\000o\000u\000g\000h\000p\000u\000t\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.7}% 39 +\BOOKMARK [0][-]{chapter.8}{\376\377\000C\000o\000u\000n\000t\000-\000a\000p\000i\000:\000\040\000q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000c\000a\000d\000a\000\040\000p\000r\000u\000e\000b\000a}{}% 40 +\BOOKMARK [1][-]{section.8.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.8}% 41 +\BOOKMARK [1][-]{section.8.2}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{chapter.8}% 42 +\BOOKMARK [1][-]{section.8.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000g\000l\000o\000b\000a\000l\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.8}% 43 +\BOOKMARK [1][-]{section.8.4}{\376\377\000F\000e\000a\000t\000u\000r\000e\000s\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.8}% 44 +\BOOKMARK [2][-]{subsection.8.4.1}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 45 +\BOOKMARK [2][-]{subsection.8.4.2}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000s\000C\000o\000n\000t\000i\000n\000u\000a\000t\000i\000o\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 46 +\BOOKMARK [2][-]{subsection.8.4.3}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000H\000e\000a\000d\000e\000r\000K\000e\000y\000w\000o\000r\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 47 +\BOOKMARK [2][-]{subsection.8.4.4}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000C\000o\000d\000e\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 48 +\BOOKMARK [2][-]{subsection.8.4.5}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000N\000o\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 49 +\BOOKMARK [2][-]{subsection.8.4.6}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 50 +\BOOKMARK [2][-]{subsection.8.4.7}{\376\377\000T\000e\000s\000t\000F\000e\000a\000t\000u\000r\000e\000C\000o\000u\000n\000t\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 51 +\BOOKMARK [1][-]{section.8.5}{\376\377\000C\000l\000u\000s\000t\000e\000r\000i\000n\000g\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.8}% 52 +\BOOKMARK [2][-]{subsection.8.5.1}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000T\000w\000o\000B\000a\000n\000d\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 53 +\BOOKMARK [2][-]{subsection.8.5.2}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000S\000a\000m\000e\000B\000a\000n\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 54 +\BOOKMARK [2][-]{subsection.8.5.3}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 55 +\BOOKMARK [2][-]{subsection.8.5.4}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000N\000o\000i\000s\000e\000M\000i\000n\000S\000a\000m\000p\000l\000e\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 56 +\BOOKMARK [1][-]{section.8.6}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000c\000o\000n\000\040\000P\000D\000F\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.8}% 57 +\BOOKMARK [2][-]{subsection.8.6.1}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s}{section.8.6}% 58 +\BOOKMARK [2][-]{subsection.8.6.2}{\376\377\000T\000e\000s\000t\000P\000r\000o\000c\000e\000s\000s\000P\000D\000F\000T\000i\000m\000i\000n\000g\000F\000i\000x\000t\000u\000r\000e\000s}{section.8.6}% 59 +\BOOKMARK [2][-]{subsection.8.6.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000H\000T\000T\000P\000\040\000m\000e\000a\000s\000u\000r\000e\000\137\000c\000o\000u\000n\000t\000\137\000l\000a\000t\000e\000n\000c\000y}{section.8.6}% 60 +\BOOKMARK [1][-]{section.8.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000a\000g\000r\000e\000g\000a\000d\000o\000s\000\040\000\050\0002\0003\000\040\000P\000D\000F\000s\000\051}{chapter.8}% 61 +\BOOKMARK [1][-]{section.8.8}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000d\000e\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000H\000T\000T\000P}{chapter.8}% 62 +\BOOKMARK [1][-]{section.8.9}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000p\000o\000r\000\040\000a\000r\000c\000h\000i\000v\000o}{chapter.8}% 63 +\BOOKMARK [1][-]{section.8.10}{\376\377\000C\000\363\000m\000o\000\040\000r\000e\000p\000r\000o\000d\000u\000c\000i\000r}{chapter.8}% 64 +\BOOKMARK [0][-]{chapter.9}{\376\377\000E\000x\000t\000r\000a\000c\000c\000i\000\363\000n\000\040\000L\000L\000M\000:\000\040\000b\000e\000n\000c\000h\000\040\000d\000e\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000\050\000S\000T\000-\000E\000X\000T\000-\0000\0002\000\051}{}% 65 +\BOOKMARK [1][-]{section.9.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.9}% 66 +\BOOKMARK [1][-]{section.9.2}{\376\377\000C\000\363\000m\000o\000\040\000e\000j\000e\000c\000u\000t\000a\000r}{chapter.9}% 67 +\BOOKMARK [1][-]{section.9.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0007\000\051}{chapter.9}% 68 +\BOOKMARK [1][-]{section.9.4}{\376\377\000L\000e\000c\000t\000u\000r\000a\000\040\000p\000a\000r\000a\000\040\000c\000a\000p\000a\000c\000i\000d\000a\000d}{chapter.9}% 69 +\BOOKMARK [0][-]{chapter.10}{\376\377\000C\000o\000l\000a\000\040\000A\000R\000Q\000:\000\040\000b\000e\000n\000c\000h\000\040\000S\000T\000-\000E\000X\000T\000-\0000\0001\000\040\000\050\000L\000L\000M\000\040\000m\000o\000c\000k\000\051}{}% 70 +\BOOKMARK [1][-]{section.10.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.10}% 71 +\BOOKMARK [1][-]{section.10.2}{\376\377\000C\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s\000\040\000d\000e\000\040\000l\000a\000\040\000c\000o\000r\000r\000i\000d\000a}{chapter.10}% 72 +\BOOKMARK [1][-]{section.10.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s}{chapter.10}% 73 +\BOOKMARK [1][-]{section.10.4}{\376\377\000L\000e\000c\000t\000u\000r\000a}{chapter.10}% 74 +\BOOKMARK [0][-]{chapter.11}{\376\377\000C\000o\000u\000n\000t\000-\000a\000p\000i\000\040\000b\000a\000j\000o\000\040\000c\000a\000r\000g\000a\000\040\000\050\000S\000T\000-\000C\000O\000U\000N\000T\000-\0000\0001\000\051}{}% 75 +\BOOKMARK [1][-]{section.11.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.11}% 76 +\BOOKMARK [1][-]{section.11.2}{\376\377\000C\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s}{chapter.11}% 77 +\BOOKMARK [1][-]{section.11.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s}{chapter.11}% 78 +\BOOKMARK [1][-]{section.11.4}{\376\377\000L\000e\000c\000t\000u\000r\000a}{chapter.11}% 79 +\BOOKMARK [0][-]{chapter.12}{\376\377\000A\000n\000\341\000l\000i\000s\000i\000s\000\040\000y\000\040\000h\000a\000l\000l\000a\000z\000g\000o\000s}{}% 80 +\BOOKMARK [1][-]{section.12.1}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.12}% 81 +\BOOKMARK [1][-]{section.12.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d\000\040\000g\000l\000o\000b\000a\000l}{chapter.12}% 82 +\BOOKMARK [1][-]{section.12.3}{\376\377\000L\000i\000m\000i\000t\000a\000c\000i\000o\000n\000e\000s}{chapter.12}% 83 +\BOOKMARK [0][-]{chapter.13}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s\000\040\000y\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{}% 84 +\BOOKMARK [1][-]{section.13.1}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s}{chapter.13}% 85 +\BOOKMARK [1][-]{section.13.2}{\376\377\000R\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{chapter.13}% 86 +\BOOKMARK [1][-]{section.13.3}{\376\377\000D\000o\000c\000u\000m\000e\000n\000t\000o\000s\000\040\000r\000e\000l\000a\000c\000i\000o\000n\000a\000d\000o\000s}{chapter.13}% 87 +\BOOKMARK [0][-]{chapter.14}{\376\377\000A\000n\000e\000x\000o\000s}{}% 88 +\BOOKMARK [1][-]{section.14.1}{\376\377\000D\000a\000t\000o\000s\000\040\000f\000u\000e\000n\000t\000e}{chapter.14}% 89 +\BOOKMARK [1][-]{section.14.2}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.14}% 90 +\BOOKMARK [1][-]{section.14.3}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\000\043\0002}{chapter.14}% 91 +\BOOKMARK [1][-]{section.14.4}{\376\377\000U\000n\000i\000t\000a\000r\000i\000a\000s}{chapter.14}% 92 +\BOOKMARK [1][-]{section.14.5}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.14}% 93 +\BOOKMARK [1][-]{section.14.6}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000E\000X\000T\000-\0000\0001}{chapter.14}% 94 +\BOOKMARK [1][-]{section.14.7}{\376\377\000D\000o\000c\000u\000m\000e\000n\000t\000o\000s\000\040\000h\000e\000r\000m\000a\000n\000o\000s}{chapter.14}% 95 +\BOOKMARK [1][-]{section.14.8}{\376\377\000R\000e\000p\000r\000o\000d\000u\000c\000i\000b\000i\000l\000i\000d\000a\000d}{chapter.14}% 96 diff --git a/docs/plan_pruebas/informe/main.tex b/docs/plan_pruebas/informe/main.tex index bd5462b..1db155e 100644 --- a/docs/plan_pruebas/informe/main.tex +++ b/docs/plan_pruebas/informe/main.tex @@ -6,33 +6,22 @@ \makecover -\chapter*{Control de cambios} -\begin{tabularx}{\textwidth}{@{}l l X@{}} -\toprule -\textbf{Versi\'on} & \textbf{Fecha} & \textbf{Descripci\'on} \\ -\midrule -1.0.0 & 13 jul 2026 & Informe profesional: campa\~na shared8g, m\'etricas, capacidad 100k/mes. \\ -1.1.0 & 13 jul 2026 & Unitarias count-api pipeline 3/3 PASS (Ubuntu). \\ -1.2.0 & 13 jul 2026 & Unitarias features/clustering ampliadas; timings PDF + scripts latencia. \\ -1.3.0 & 13 jul 2026 & Latencia HTTP 3 PDFs reales (2{,}2--4{,}5\,s); fix path Timing test. \\ -1.4.0 & 13 jul 2026 & Cap.\ count-api: explicaci\'on test a test, ejemplos y gr\'aficas. \\ -\bottomrule -\end{tabularx} - \tableofcontents \clearpage \input{sections/01_resumen_ejecutivo} -\input{sections/02_objeto_alcance} +\input{sections/02_arquitectura_contexto} \input{sections/03_entorno} -\input{sections/04_metodologia} +\input{sections/04_como_leer} \input{sections/05_resultados_unitarias} \input{sections/06_resultados_humo_funcional} \input{sections/07_resultados_carga} -\input{sections/08_capacidad_100k} +\input{sections/12_count_latency} +\input{sections/13_llm_latency} +\input{sections/14_arq_ext01} +\input{sections/15_st_count_01} \input{sections/09_analisis_hallazgos} \input{sections/10_conclusiones} \input{sections/11_anexos} -\input{sections/12_count_latency} \end{document} diff --git a/docs/plan_pruebas/informe/main.toc b/docs/plan_pruebas/informe/main.toc index 9c8551c..b9827a1 100644 --- a/docs/plan_pruebas/informe/main.toc +++ b/docs/plan_pruebas/informe/main.toc @@ -1,77 +1,97 @@ \babel@toc {spanish}{}\relax -\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{5}{chapter.1}% -\contentsline {section}{\numberline {1.1}Veredicto global}{5}{section.1.1}% -\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{5}{section.1.2}% -\contentsline {section}{\numberline {1.3}Respuesta directa: 100\,000 facturas/mes}{5}{section.1.3}% -\contentsline {section}{\numberline {1.4}Riesgo principal}{6}{section.1.4}% -\contentsline {chapter}{\numberline {2}Objeto y alcance}{7}{chapter.2}% -\contentsline {section}{\numberline {2.1}Objeto}{7}{section.2.1}% -\contentsline {section}{\numberline {2.2}Alcance incluido}{7}{section.2.2}% -\contentsline {section}{\numberline {2.3}Fuera de alcance de esta campa\~na}{7}{section.2.3}% -\contentsline {section}{\numberline {2.4}Definici\'on de factura}{7}{section.2.4}% -\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{8}{chapter.3}% -\contentsline {section}{\numberline {3.1}Hardware y sistema}{8}{section.3.1}% -\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{8}{section.3.2}% -\contentsline {section}{\numberline {3.3}Salud del stack}{8}{section.3.3}% -\contentsline {chapter}{\numberline {4}Metodolog\'ia}{10}{chapter.4}% -\contentsline {section}{\numberline {4.1}Tipos de prueba}{10}{section.4.1}% -\contentsline {section}{\numberline {4.2}Herramientas}{10}{section.4.2}% -\contentsline {section}{\numberline {4.3}SLOs perfil \texttt {shared8g}}{10}{section.4.3}% -\contentsline {section}{\numberline {4.4}Comandos representativos}{10}{section.4.4}% -\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{11}{chapter.5}% -\contentsline {section}{\numberline {5.1}Scorecard}{11}{section.5.1}% -\contentsline {section}{\numberline {5.2}Cobertura agent}{11}{section.5.2}% -\contentsline {section}{\numberline {5.3}Cobertura API store}{11}{section.5.3}% -\contentsline {section}{\numberline {5.4}Cobertura count-api}{11}{section.5.4}% -\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{13}{chapter.6}% -\contentsline {section}{\numberline {6.1}Humo gateway}{13}{section.6.1}% -\contentsline {section}{\numberline {6.2}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{13}{section.6.2}% -\contentsline {section}{\numberline {6.3}Script preparado}{13}{section.6.3}% -\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{14}{chapter.7}% -\contentsline {section}{\numberline {7.1}Tabla consolidada}{14}{section.7.1}% -\contentsline {section}{\numberline {7.2}ST-GW-01}{14}{section.7.2}% -\contentsline {section}{\numberline {7.3}ST-BOT-01 --- comparativa}{14}{section.7.3}% -\contentsline {section}{\numberline {7.4}Throughput gateway}{14}{section.7.4}% -\contentsline {chapter}{\numberline {8}Capacidad: 100\,000 facturas al mes}{16}{chapter.8}% -\contentsline {section}{\numberline {8.1}Traducci\'on del objetivo}{16}{section.8.1}% -\contentsline {section}{\numberline {8.2}Modelo por etapa}{16}{section.8.2}% -\contentsline {section}{\numberline {8.3}Interpretaci\'on}{16}{section.8.3}% -\contentsline {section}{\numberline {8.4}Veredicto}{17}{section.8.4}% -\contentsline {section}{\numberline {8.5}Requisitos m\'inimos sugeridos para 100k/mes}{17}{section.8.5}% -\contentsline {chapter}{\numberline {9}An\'alisis y hallazgos}{18}{chapter.9}% -\contentsline {section}{\numberline {9.1}Hallazgos}{18}{section.9.1}% -\contentsline {section}{\numberline {9.2}Scorecard global}{18}{section.9.2}% -\contentsline {section}{\numberline {9.3}Limitaciones}{18}{section.9.3}% -\contentsline {chapter}{\numberline {10}Conclusiones y recomendaciones}{19}{chapter.10}% -\contentsline {section}{\numberline {10.1}Conclusiones}{19}{section.10.1}% -\contentsline {section}{\numberline {10.2}Recomendaciones}{19}{section.10.2}% -\contentsline {section}{\numberline {10.3}Pr\'oxima campa\~na sugerida}{19}{section.10.3}% -\contentsline {chapter}{\numberline {11}Anexos}{20}{chapter.11}% -\contentsline {section}{\numberline {11.1}Datos fuente}{20}{section.11.1}% -\contentsline {section}{\numberline {11.2}Extracto ST-GW-01}{20}{section.11.2}% -\contentsline {section}{\numberline {11.3}Extracto ST-BOT-01 \#2}{20}{section.11.3}% -\contentsline {section}{\numberline {11.4}Unitarias}{20}{section.11.4}% -\contentsline {section}{\numberline {11.5}Scripts}{20}{section.11.5}% -\contentsline {section}{\numberline {11.6}Reproducibilidad}{21}{section.11.6}% -\contentsline {chapter}{\numberline {12}Count-api: qu\'e eval\'ua cada prueba}{22}{chapter.12}% -\contentsline {section}{\numberline {12.1}Pipeline bajo prueba}{22}{section.12.1}% -\contentsline {section}{\numberline {12.2}Resultado global count-api}{22}{section.12.2}% -\contentsline {section}{\numberline {12.3}Features: qu\'e se eval\'ua (con ejemplo)}{23}{section.12.3}% -\contentsline {subsection}{\numberline {12.3.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.1}% -\contentsline {subsection}{\numberline {12.3.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.2}% -\contentsline {subsection}{\numberline {12.3.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.3}% -\contentsline {subsection}{\numberline {12.3.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.4}% -\contentsline {subsection}{\numberline {12.3.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.5}% -\contentsline {subsection}{\numberline {12.3.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.6}% -\contentsline {subsection}{\numberline {12.3.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.7}% -\contentsline {section}{\numberline {12.4}Clustering: qu\'e se eval\'ua (con ejemplo)}{24}{section.12.4}% -\contentsline {subsection}{\numberline {12.4.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.1}% -\contentsline {subsection}{\numberline {12.4.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.2}% -\contentsline {subsection}{\numberline {12.4.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.3}% -\contentsline {subsection}{\numberline {12.4.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.4}% -\contentsline {section}{\numberline {12.5}Pruebas con PDFs reales}{24}{section.12.5}% -\contentsline {subsection}{\numberline {12.5.1}Fixtures}{24}{subsection.12.5.1}% -\contentsline {subsection}{\numberline {12.5.2}TestProcessPDFTimingFixtures}{25}{subsection.12.5.2}% -\contentsline {subsection}{\numberline {12.5.3}Script HTTP \texttt {measure\_count\_latency}}{25}{subsection.12.5.3}% -\contentsline {section}{\numberline {12.6}Resultados de latencia HTTP (medidos)}{25}{section.12.6}% -\contentsline {section}{\numberline {12.7}C\'omo reproducir}{25}{section.12.7}% +\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{4}{chapter.1}% +\contentsline {section}{\numberline {1.1}Veredicto global}{4}{section.1.1}% +\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{4}{section.1.2}% +\contentsline {section}{\numberline {1.3}Capacidad 100k/mes (resumen)}{5}{section.1.3}% +\contentsline {section}{\numberline {1.4}Riesgo principal}{5}{section.1.4}% +\contentsline {section}{\numberline {1.5}Alcance de esta campa\~na}{5}{section.1.5}% +\contentsline {chapter}{\numberline {2}Arquitectura y patrones (contexto)}{6}{chapter.2}% +\contentsline {section}{\numberline {2.1}Vista de componentes}{6}{section.2.1}% +\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}% +\contentsline {section}{\numberline {2.3}Pipeline de negocio}{7}{section.2.3}% +\contentsline {section}{\numberline {2.4}Definici\'on de factura}{8}{section.2.4}% +\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{9}{chapter.3}% +\contentsline {section}{\numberline {3.1}Hardware y sistema}{9}{section.3.1}% +\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{9}{section.3.2}% +\contentsline {section}{\numberline {3.3}Salud del stack}{9}{section.3.3}% +\contentsline {chapter}{\numberline {4}C\'omo leer este informe}{11}{chapter.4}% +\contentsline {section}{\numberline {4.1}Plantilla de interpretaci\'on}{11}{section.4.1}% +\contentsline {section}{\numberline {4.2}Leyenda de estados}{11}{section.4.2}% +\contentsline {section}{\numberline {4.3}Perfil de carga \texttt {shared8g}}{11}{section.4.3}% +\contentsline {section}{\numberline {4.4}Herramientas}{11}{section.4.4}% +\contentsline {section}{\numberline {4.5}Mapa de cap\'itulos de resultados}{12}{section.4.5}% +\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{13}{chapter.5}% +\contentsline {section}{\numberline {5.1}Qu\'e eval\'uan}{13}{section.5.1}% +\contentsline {section}{\numberline {5.2}Scorecard}{13}{section.5.2}% +\contentsline {section}{\numberline {5.3}Cobertura agent Selenium}{13}{section.5.3}% +\contentsline {section}{\numberline {5.4}Cobertura API \texttt {bot-descarga} (orquestador)}{14}{section.5.4}% +\contentsline {section}{\numberline {5.5}Cobertura count-api}{14}{section.5.5}% +\contentsline {section}{\numberline {5.6}Cobertura desktop (Vitest)}{14}{section.5.6}% +\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{15}{chapter.6}% +\contentsline {section}{\numberline {6.1}Qu\'e eval\'uan}{15}{section.6.1}% +\contentsline {section}{\numberline {6.2}Humo gateway}{15}{section.6.2}% +\contentsline {section}{\numberline {6.3}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{15}{section.6.3}% +\contentsline {section}{\numberline {6.4}Scripts}{16}{section.6.4}% +\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{17}{chapter.7}% +\contentsline {section}{\numberline {7.1}Qu\'e eval\'uan}{17}{section.7.1}% +\contentsline {section}{\numberline {7.2}Tabla consolidada}{17}{section.7.2}% +\contentsline {section}{\numberline {7.3}ST-GW-01}{17}{section.7.3}% +\contentsline {section}{\numberline {7.4}ST-BOT-01 --- comparativa}{18}{section.7.4}% +\contentsline {section}{\numberline {7.5}Throughput gateway}{19}{section.7.5}% +\contentsline {chapter}{\numberline {8}Count-api: qu\'e eval\'ua cada prueba}{20}{chapter.8}% +\contentsline {section}{\numberline {8.1}Qu\'e eval\'ua / resultado / significado}{20}{section.8.1}% +\contentsline {section}{\numberline {8.2}Pipeline bajo prueba}{20}{section.8.2}% +\contentsline {section}{\numberline {8.3}Resultado global count-api}{21}{section.8.3}% +\contentsline {section}{\numberline {8.4}Features: qu\'e se eval\'ua (con ejemplo)}{21}{section.8.4}% +\contentsline {subsection}{\numberline {8.4.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.1}% +\contentsline {subsection}{\numberline {8.4.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.2}% +\contentsline {subsection}{\numberline {8.4.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.3}% +\contentsline {subsection}{\numberline {8.4.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.4}% +\contentsline {subsection}{\numberline {8.4.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.5}% +\contentsline {subsection}{\numberline {8.4.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.6}% +\contentsline {subsection}{\numberline {8.4.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.7}% +\contentsline {section}{\numberline {8.5}Clustering: qu\'e se eval\'ua (con ejemplo)}{22}{section.8.5}% +\contentsline {subsection}{\numberline {8.5.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.1}% +\contentsline {subsection}{\numberline {8.5.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.2}% +\contentsline {subsection}{\numberline {8.5.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.3}% +\contentsline {subsection}{\numberline {8.5.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.4}% +\contentsline {section}{\numberline {8.6}Pruebas con PDFs reales}{23}{section.8.6}% +\contentsline {subsection}{\numberline {8.6.1}Fixtures}{23}{subsection.8.6.1}% +\contentsline {subsection}{\numberline {8.6.2}TestProcessPDFTimingFixtures}{23}{subsection.8.6.2}% +\contentsline {subsection}{\numberline {8.6.3}Script HTTP \texttt {measure\_count\_latency}}{23}{subsection.8.6.3}% +\contentsline {section}{\numberline {8.7}Resultados agregados (23 PDFs)}{23}{section.8.7}% +\contentsline {section}{\numberline {8.8}Resultados de latencia HTTP}{24}{section.8.8}% +\contentsline {section}{\numberline {8.9}Detalle por archivo}{25}{section.8.9}% +\contentsline {section}{\numberline {8.10}C\'omo reproducir}{25}{section.8.10}% +\contentsline {chapter}{\numberline {9}Extracci\'on LLM: bench de latencia (ST-EXT-02)}{26}{chapter.9}% +\contentsline {section}{\numberline {9.1}Qu\'e eval\'ua / resultado / significado}{26}{section.9.1}% +\contentsline {section}{\numberline {9.2}C\'omo ejecutar}{26}{section.9.2}% +\contentsline {section}{\numberline {9.3}Resultados (2026-07-17)}{27}{section.9.3}% +\contentsline {section}{\numberline {9.4}Lectura para capacidad}{27}{section.9.4}% +\contentsline {chapter}{\numberline {10}Cola ARQ: bench ST-EXT-01 (LLM mock)}{28}{chapter.10}% +\contentsline {section}{\numberline {10.1}Qu\'e eval\'ua / resultado / significado}{28}{section.10.1}% +\contentsline {section}{\numberline {10.2}Condiciones de la corrida}{28}{section.10.2}% +\contentsline {section}{\numberline {10.3}Resultados}{29}{section.10.3}% +\contentsline {section}{\numberline {10.4}Lectura}{29}{section.10.4}% +\contentsline {chapter}{\numberline {11}Count-api bajo carga (ST-COUNT-01)}{30}{chapter.11}% +\contentsline {section}{\numberline {11.1}Qu\'e eval\'ua / resultado / significado}{30}{section.11.1}% +\contentsline {section}{\numberline {11.2}Condiciones}{30}{section.11.2}% +\contentsline {section}{\numberline {11.3}Resultados}{30}{section.11.3}% +\contentsline {section}{\numberline {11.4}Lectura}{30}{section.11.4}% +\contentsline {chapter}{\numberline {12}An\'alisis y hallazgos}{32}{chapter.12}% +\contentsline {section}{\numberline {12.1}Hallazgos}{32}{section.12.1}% +\contentsline {section}{\numberline {12.2}Scorecard global}{32}{section.12.2}% +\contentsline {section}{\numberline {12.3}Limitaciones}{32}{section.12.3}% +\contentsline {chapter}{\numberline {13}Conclusiones y recomendaciones}{34}{chapter.13}% +\contentsline {section}{\numberline {13.1}Conclusiones}{34}{section.13.1}% +\contentsline {section}{\numberline {13.2}Recomendaciones}{34}{section.13.2}% +\contentsline {section}{\numberline {13.3}Documentos relacionados}{34}{section.13.3}% +\contentsline {chapter}{\numberline {14}Anexos}{35}{chapter.14}% +\contentsline {section}{\numberline {14.1}Datos fuente}{35}{section.14.1}% +\contentsline {section}{\numberline {14.2}Extracto ST-GW-01}{35}{section.14.2}% +\contentsline {section}{\numberline {14.3}Extracto ST-BOT-01 \#2}{35}{section.14.3}% +\contentsline {section}{\numberline {14.4}Unitarias}{35}{section.14.4}% +\contentsline {section}{\numberline {14.5}Scripts}{35}{section.14.5}% +\contentsline {section}{\numberline {14.6}Extracto ST-EXT-01}{36}{section.14.6}% +\contentsline {section}{\numberline {14.7}Documentos hermanos}{36}{section.14.7}% +\contentsline {section}{\numberline {14.8}Reproducibilidad}{36}{section.14.8}% diff --git a/docs/plan_pruebas/informe/preamble.tex b/docs/plan_pruebas/informe/preamble.tex index 0070f03..a6c2933 100644 --- a/docs/plan_pruebas/informe/preamble.tex +++ b/docs/plan_pruebas/informe/preamble.tex @@ -16,7 +16,7 @@ \fancyhf{} \fancyhead[L]{\small IPSExtractor --- Informe de Pruebas} \fancyhead[R]{\small\thepage} -\fancyfoot[C]{\footnotesize Confidencial --- Uso interno} +\fancyfoot[C]{\footnotesize Documento para compartir} \renewcommand{\headrulewidth}{0.4pt} \usepackage{titlesec} @@ -58,9 +58,10 @@ \usepgfplotslibrary{statistics} \usepackage{tikz} -\usetikzlibrary{arrows.meta,positioning} +\usetikzlibrary{arrows.meta,positioning,fit,calc,shapes.geometric} \usepackage{graphicx} +\usepackage{float} \usepackage{hyperref} \hypersetup{ colorlinks=true, @@ -70,14 +71,19 @@ pdfauthor={IPSExtractor QA} } -\newcommand{\docversion}{1.4.0} -\newcommand{\docdate}{13 de julio de 2026} +\newcommand{\docdate}{17 de julio de 2026} \newcommand{\pass}{\textcolor{passgreen}{\textbf{PASS}}} \newcommand{\fail}{\textcolor{failred}{\textbf{FAIL}}} \newcommand{\parcial}{\textcolor{warnorange}{\textbf{PARCIAL}}} \newcommand{\neje}{\textcolor{ipsgrey}{\textbf{N/E}}} +\newcommand{\callout}[2]{% + \begin{center} + \fbox{\parbox{0.92\textwidth}{\centering #1\\[0.3em]\normalsize #2}} + \end{center} +} + \newcommand{\makecover}{% \begin{titlepage} \centering @@ -87,16 +93,16 @@ {\Large\color{ipsgrey} Extracci\'on y an\'alisis de facturas m\'edicas}\\[0.35cm] {\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[1.5cm] {\LARGE\bfseries Informe de Pruebas}\\[0.25cm] - {\large Resultados $\cdot$ M\'etricas $\cdot$ Capacidad 100\,000/mes}\\[1.8cm] + {\large Resultados $\cdot$ M\'etricas $\cdot$ Interpretaci\'on}\\[1.8cm] \begin{tabular}{@{}ll@{}} - \textbf{Versi\'on} & \docversion \\ \textbf{Fecha} & \docdate \\ - \textbf{Campa\~na} & 2026-07-13 (perfil \texttt{shared8g}) \\ + \textbf{Campa\~na carga} & 13 de julio de 2026 (perfil \texttt{shared8g}) \\ + \textbf{Bench LLM} & 17 de julio de 2026 (ST-EXT-02) \\ \textbf{Host} & ubuntu --- ASUS X441UAK, 8\,GiB \\ - \textbf{Clasificaci\'on} & Uso interno \\ + \textbf{Audiencia} & Documento para compartir \\ \end{tabular} \vfill - {\small Documento de \emph{resultados} (no plan). Incluye an\'alisis de viabilidad de 100\,000 facturas/mes.}\\[0.25cm] + {\small Resultados de pruebas. El an\'alisis de 100\,000 facturas/mes est\'a en el PDF \emph{Capacidad 100k}.}\\[0.25cm] {\color{ipsblue}\rule{0.45\textwidth}{0.6pt}} \end{titlepage} } diff --git a/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex b/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex index 38e0f82..215447b 100644 --- a/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex +++ b/docs/plan_pruebas/informe/sections/01_resumen_ejecutivo.tex @@ -2,15 +2,20 @@ \label{ch:resumen} \section{Veredicto global} -La campa\~na de pruebas del \textbf{13 de julio de 2026} sobre el stack IPSExtractor desplegado en el host \texttt{ubuntu} (perfil \texttt{shared8g}) concluye: +La campa\~na de pruebas (carga 13-jul-2026 + LLM/ARQ 17-jul-2026) sobre el stack +IPSExtractor en el host \texttt{ubuntu} (perfil \texttt{shared8g}) concluye: -\begin{center} -\large -Stack \textbf{operativo y estable} para orquestaci\'on y control remoto del bot.\\[0.3em] -\normalsize -Carga ligera del gateway y heartbeats: \pass\ bajo contenci\'on moderada.\\[0.2em] -Pipeline completo a \textbf{100\,000 facturas/mes} en este host: \fail\ (ver cap.~\ref{ch:capacidad}). -\end{center} +\callout{\large Stack operativo: orquestador y agent}{% +Gateway y heartbeats del orquestador \texttt{bot-descarga} (ST-BOT-*): \pass\ bajo contenci\'on moderada.\\ +Unitarias agent Selenium / API \texttt{bot-descarga}: \pass.\\ +Vitest desktop: \pass\ (25/25).\\ +LLM ST-EXT-02 (23 PDFs): \pass\ --- p50 7{,}1\,s $\approx$506 fact/h.\\ +Cola ARQ ST-EXT-01 (30 docs, mock): \pass\ --- drenado en 22{,}5\,s.\\ +OCR ST-COUNT-01 (9 PDFs, 3 conc.): \pass\ --- p50 35{,}8\,s; $\approx$325 fact/h obs.\\ +E2E-07 (agent offline $\rightarrow$ 409): \pass.\\[0.3em] +\textbf{100\,000 facturas/mes} en este host: \fail\ del pipeline completo\\ +(detalle en el documento \emph{Capacidad 100k/mes}). +} \section{KPIs de la campa\~na} \begin{table}[ht] @@ -22,20 +27,35 @@ Pipeline completo a \textbf{100\,000 facturas/mes} en este host: \fail\ (ver cap ST-GW-01 p95 & 40{,}85\,ms & $<$500\,ms & \pass \\ ST-GW-01 fail rate & 0\,\% & $<$1\,\% & \pass \\ ST-BOT-01 p95 (reintento) & 85{,}55\,ms & $<$800\,ms & \pass \\ -Unitarias agent bot & 19/19 & 100\,\% & \pass \\ -Unitarias API bot store & 7/7 & 100\,\% & \pass \\ +Unitarias agent Selenium & 19/19 & 100\,\% & \pass \\ +Unitarias API \texttt{bot-descarga} (orquestador) & 7/7 & 100\,\% & \pass \\ Unitarias count-api (pipeline) & 3/3 & 100\,\% & \pass \\ +ST-EXT-02 LLM p50 & 7{,}114\,s & --- & $\approx$506 fact/h \\ +ST-EXT-01 ARQ mock & 30/30 drain & --- & 22{,}5\,s; $\approx$4\,5k docs/h cola \\ +ST-COUNT-01 OCR & p50 35{,}8\,s & --- & 9/9; $\approx$325 fact/h obs. \\ \bottomrule \end{tabular} -\caption{KPIs ejecutivos de la campa\~na.} +\caption{KPIs (carga 13-jul; LLM/ARQ/OCR 17-jul).} \end{table} -\section{Respuesta directa: 100\,000 facturas/mes} +\section{Capacidad 100k/mes (resumen)} \begin{description} - \item[Host actual (8\,GiB + MLflow/n8n/MinIO/\ldots)] \fail\ para el \emph{pipeline completo} (conteo + extracci\'on + bot + limpieza). - \item[Infra dedicada dimensionada] \pass\ \emph{condicional}: workers LLM, OCR con RAM $\geq$16--32\,GiB, $N$ agents Selenium, cuota API, sin zoo Docker competidor. + \item[Host actual] \parcial\ / \fail\ --- pipeline $\approx$ OCR $\sim$100--325/h; 139/h no garantizado. + \item[Infra dedicada] \pass\ condicional (OCR, workers, $N$ agents, cuota LLM). + \item[Coste LLM] $\approx$ USD\,2\,570/mes si 100k pasan a extracci\'on (gpt-5.4 mini). \end{description} -Detalle cuantitativo en el cap\'itulo~\ref{ch:capacidad}. +El modelo por etapa, gr\'aficas y requisitos est\'an en el PDF +\textbf{Capacidad100k\_IPSExtractor.pdf} (carpeta \texttt{docs/plan\_pruebas/capacidad\_100k/}). \section{Riesgo principal} -Contenci\'on de plataforma: swap $\approx$2{,}1\,GiB y \texttt{mlflow-server} $\approx$1{,}84\,GiB provocaron un outlier de 23\,s en heartbeats (corrida 1). Tras aliviar carga, p95 cay\'o a 86\,ms. Los fallos de latencia deben correlacionarse siempre con \texttt{free -h} / \texttt{docker stats}. +Contenci\'on de plataforma: swap $\approx$2{,}1\,GiB y \texttt{mlflow-server} $\approx$1{,}84\,GiB +provocaron un outlier de 23\,s en heartbeats (corrida 1). Tras aliviar carga, p95 cay\'o a 86\,ms. +Correlacionar siempre latencias con \texttt{free -h} / \texttt{docker stats}. + +\section{Alcance de esta campa\~na} +\textbf{Incluido:} stack Docker IPS, perfil shared8g, unitarias agent Selenium / orquestador \texttt{bot-descarga} / count / Vitest desktop (25/25), +humo gateway, E2E-07 (worker offline $\rightarrow$ 409), k6 ST-GW-01/ST-BOT-01 (orquestador), +bench LLM ST-EXT-02 (23 PDFs), cola ARQ ST-EXT-01 (30 docs, mock), +ST-COUNT-01 (9 PDFs reales, 3 concurrentes).\\ +\textbf{Fuera de alcance:} ST-COUNT-01 lab agresivo, ST-EXT-01 lab (100--500), E2E Imaging/VPN, +ST-BOT-03 events, procesar 100k PDFs reales. diff --git a/docs/plan_pruebas/informe/sections/02_arquitectura_contexto.tex b/docs/plan_pruebas/informe/sections/02_arquitectura_contexto.tex new file mode 100644 index 0000000..b865fa4 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/02_arquitectura_contexto.tex @@ -0,0 +1,43 @@ +\chapter{Arquitectura y patrones (contexto)} +\label{ch:arq-informe} + +Este cap\'itulo resume c\'omo est\'a construido IPSExtractor para que los resultados +de las pruebas tengan sentido sin leer el plan completo. + +\callout{\textbf{Nombres (importante)}}{% +\textbf{bot-descarga} = servicio de orquestaci\'on (API Docker): jobs, claim, heartbeats.\\ +\textbf{Agent Selenium} = programa en el PC que descarga PDFs.\\ +ST-BOT-* mide el orquestador; $\sim$422 fact/h es capacidad del agent. +} + +\section{Vista de componentes} +El host solo expone el Gateway en \textbf{:8189}. El resto de APIs est\'a en la red Docker. +Los agents Selenium corren en PCs externas. + +\input{../diagrams/arquitectura} + +\section{Patrones de dise\~no} +\begin{description}[style=nextline,leftmargin=0.5cm] + \item[API Gateway] + Puerta \'unica. Las pruebas ST-GW-* miden latencia/errores del proxy, no del OCR ni del LLM. + \item[Microservicios] + count-api (OCR), extraction+workers (LLM), \texttt{bot-descarga} (orquestador), cleaning (CSV). + Cada uno tiene su propio cuello de botella. + \item[Cola ARQ] + Con \texttt{ASYNC\_QUEUE=true} el ingest encola y los workers drenan. + El bench LLM de esta campa\~na corri\'o en modo sync (respuesta en el mismo request). + \item[Claim del orquestador (\texttt{bot-descarga})] + Un job pending lo toma un solo agent Selenium (claim at\'omico). Heartbeats marcan al worker online. +\end{description} + +\input{../diagrams/patrones} + +\section{Pipeline de negocio} +\input{../diagrams/pipeline_negocio} + +Cuando este informe habla de ``capacidad'', se refiere a cu\'antas facturas/hora +puede sostener \textbf{cada etapa} y, en el documento de Capacidad 100k, el pipeline completo. + +\section{Definici\'on de factura} +Una \textbf{factura} = un PDF de cuenta m\'edica evaluado en una etapa +(conteo OCR, extracci\'on LLM, descarga agent Selenium o limpieza). diff --git a/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex b/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex index 361bde8..2c9a8f7 100644 --- a/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex +++ b/docs/plan_pruebas/informe/sections/02_objeto_alcance.tex @@ -8,17 +8,18 @@ Documentar de forma profesional los \textbf{resultados} de pruebas de IPSExtract \begin{itemize} \item Stack Docker IPS: gateway, extraction, workers, cleaning, count-api, bot-descarga, PostgreSQL, Redis. \item Perfil de carga \texttt{shared8g} en host de staging compartido. - \item Unitarias del agent (\texttt{bots/bot\_descarga}) y del store API (\texttt{services/bot\_descarga}). + \item Unitarias del agent Selenium (\texttt{bots/bot\_descarga}) y del orquestador API (\texttt{services/bot\_descarga}). \item Modelo anal\'itico de capacidad por etapa del pipeline. + \item E2E-07: rechazo de job si el agent Selenium est\'a offline (HTTP 409). \end{itemize} \section{Fuera de alcance de esta campa\~na} \begin{itemize} \item ST-COUNT-01 / OCR masivo y ST-EXT bajo LLM real a escala. - \item E2E Imaging/Selenium con VPN. + \item E2E Imaging/Selenium con VPN (E2E-05/08 a volumen). \item ST-BOT-03 (events) --- requiere \texttt{DOWNLOAD\_JOB\_ID} de un job real (\neje). \item Procesar 100\,000 PDFs reales. \end{itemize} \section{Definici\'on de factura} -Una \textbf{factura} = un PDF de cuenta m\'edica evaluado por etapa (conteo OCR, extracci\'on LLM, descarga bot, limpieza). El veredicto 100k/mes se expresa por etapa y para el pipeline completo. +Una \textbf{factura} = un PDF de cuenta m\'edica evaluado por etapa (conteo OCR, extracci\'on LLM, descarga agent Selenium, limpieza). El veredicto 100k/mes se expresa por etapa y para el pipeline completo. diff --git a/docs/plan_pruebas/informe/sections/04_como_leer.tex b/docs/plan_pruebas/informe/sections/04_como_leer.tex new file mode 100644 index 0000000..3ae5ed8 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/04_como_leer.tex @@ -0,0 +1,51 @@ +\chapter{C\'omo leer este informe} +\label{ch:como-leer} + +\section{Plantilla de interpretaci\'on} +Cada bloque de resultados sigue (expl\'icita o impl\'icitamente) cuatro preguntas: + +\begin{center} +\begin{tabular}{@{}p{3.2cm}p{9.5cm}@{}} +\toprule +\textbf{Pregunta} & \textbf{Qu\'e responde} \\ +\midrule +\textbf{Qu\'e eval\'ua} & Riesgo o propiedad del sistema bajo prueba \\ +\textbf{C\'omo se midi\'o} & Herramienta, perfil, comando o script \\ +\textbf{Resultado} & N\'umeros, PASS/FAIL/PARCIAL/N/E \\ +\textbf{Qu\'e significa} & Implicaci\'on pr\'actica (seguir, dimensionar o investigar) \\ +\bottomrule +\end{tabular} +\end{center} + +\section{Leyenda de estados} +\begin{itemize} + \item \pass --- cumple el umbral o criterio acordado. + \item \fail --- no cumple; requiere acci\'on o redespliegue de entorno. + \item \parcial --- funcional OK pero latencia/SLO degradado (t\'ipico: contenci\'on host). + \item \neje --- no ejecutado en esta campa\~na (falta dato, VPN, job id, etc.). +\end{itemize} + +\section{Perfil de carga \texttt{shared8g}} +Host compartido 8\,GiB con MLflow, n8n, MinIO, etc. Los SLOs son m\'as permisivos +que en laboratorio dedicado: + +\begin{itemize} + \item Gateway \texttt{/health/live}: p95 $<$500\,ms; fail $<$1\,\%. + \item Heartbeats bot: p95 $<$800\,ms; checks $>$95\,\%. + \item No usar \texttt{/health} agregado como SLO de latencia (puede superar 10\,s). +\end{itemize} + +\section{Herramientas} +k6, pytest, go test, Docker stats, curl, scripts en \texttt{docs/plan\_pruebas/scripts/}. + +\section{Mapa de cap\'itulos de resultados} +\begin{enumerate} + \item Unitarias --- correctitud de dominio (agent, store, count-api). + \item Humo / funcional --- el stack responde. + \item Carga k6 --- gateway y heartbeats bajo VUs. + \item Count-api --- qu\'e prueba el OCR y latencias medidas. + \item LLM ST-EXT-02 --- latencia real gpt-5.4-mini. + \item Cola ARQ ST-EXT-01 --- enqueue + drenado con mock. + \item ST-COUNT-01 --- OCR bajo carga concurrente. + \item Hallazgos y conclusiones --- s\'intesis. +\end{enumerate} diff --git a/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex b/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex index 97e8fa2..269c5ee 100644 --- a/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex +++ b/docs/plan_pruebas/informe/sections/05_resultados_unitarias.tex @@ -1,6 +1,24 @@ \chapter{Resultados --- pruebas unitarias} \label{ch:unitarias} +\callout{\textbf{Veredicto unitarias:} \pass}{% +54 casos verdes: 19 agent Selenium + 7 API \texttt{bot-descarga} + 11 count-api + \textbf{25 Vitest desktop}. +} + +\section{Qu\'e eval\'uan} +\begin{center} +\begin{tabular}{@{}p{2.8cm}p{4.2cm}p{2.2cm}p{3.5cm}@{}} +\toprule +\textbf{Suite} & \textbf{Qu\'e eval\'ua} & \textbf{Resultado} & \textbf{Significado} \\ +\midrule +Agent Selenium & Parsers, cancel, MinIO keys, GUI & 19/19 \pass & Dominio del PC agent est\'able \\ +API \texttt{bot-descarga} & Register, claim, events, auth & 7/7 \pass & Orquestador estable en unitario \\ +count-api & Features + clustering OCR & 11/11 \pass & Pipeline de conteo en izolaci\'on \\ +Desktop (Vitest) & UI shell, API client, CSV, schema & 25/25 \pass & Cliente Electron coherente \\ +\bottomrule +\end{tabular} +\end{center} + \section{Scorecard} \begin{table}[ht] \centering @@ -11,43 +29,72 @@ \texttt{bots/bot\_descarga/tests} & \pass & 19/19 & 3{,}21\,s \\ \texttt{services/bot\_descarga/tests} & \pass & 7/7 & 1{,}45\,s \\ \texttt{go test} count-api (\texttt{internal/pipeline}) & \pass & 11/11 & + Timing/HTTP en cap.~\ref{ch:count-latency} \\ -Vitest desktop & \neje & --- & No ejecutado en esta sesi\'on \\ +Vitest \texttt{services/desktop\_app} & \pass & 25/25 & $\approx$40\,s (7 archivos) \\ \bottomrule \end{tabular} \caption{Resultados unitarios.} \end{table} -\section{Cobertura agent} +\section{Cobertura agent Selenium} Incluye excel reader, PDF validator, file manager, cancel, browser fatal, MinIO keys, GUI settings (19 casos). -\section{Cobertura API store} +\section{Cobertura API \texttt{bot-descarga} (orquestador)} Incluye helpers MinIO, register/heartbeat/offline, claim at\'omico, cancel, events, filtros por job padre (7 casos). \section{Cobertura count-api} -Ver cap\'itulo~\ref{ch:count-latency}: explicaci\'on test a test con ejemplos, -gr\'aficas y resultados (11 unitarias PASS; HTTP 3 PDFs ok en 2{,}2--4{,}5\,s). +Ver cap\'itulo~\ref{ch:count-latency}: 11 unitarias PASS + \textbf{23 PDFs} medidos (HTTP y Go ok; promedio $\sim$2\,s/PDF). + +\section{Cobertura desktop (Vitest)} +Ejecutado: \texttt{cd services/desktop\_app \&\& npm test}\\ +7 archivos / 25 tests \pass\ (warnings \texttt{act(...)} en \texttt{App.test.tsx}, sin fallos). +Incluye: \texttt{App} (shell/home/config), \texttt{apiClient}, CSV, path mapping, payloads, +cleaning settings, schema display. \begin{figure}[ht] \centering \begin{tikzpicture} \begin{axis}[ - xbar, - width=0.85\textwidth, - height=4.2cm, + xbar stacked, + width=0.88\textwidth, + height=5.2cm, xlabel={Casos}, - symbolic y coords={agent,api-store,count-api,desktop}, + symbolic y coords={desktop,count-api,api-store,agent}, ytick=data, - xmin=0, xmax=22, - nodes near coords, - bar width=10pt, - legend style={at={(0.98,0.35)},anchor=east,font=\small} + yticklabels={desktop Vitest,count-api,bot-descarga API,agent Selenium}, + xmin=0, xmax=28, + enlarge y limits=0.18, + bar width=12pt, + axis y line*=left, + axis x line*=bottom, + tick align=outside, + legend style={ + at={(0.98,0.12)}, + anchor=south east, + font=\small, + draw=ipsgrey!40, + fill=white + }, + visualization depends on={rawx \as \rawxval}, + nodes near coords={% + \pgfmathparse{abs(\rawxval)<0.5 ? "":int(\rawxval)}% + \pgfmathresult + }, + every node near coord/.append style={ + font=\footnotesize\bfseries, + text=black, + /pgf/number format/assume math mode=true + }, ] -\addplot[fill=passgreen!70] coordinates {(19,agent) (7,api-store) (11,count-api) (0,desktop)}; +\addplot[fill=passgreen!75, draw=passgreen] coordinates { + (25,desktop) (11,count-api) (7,api-store) (19,agent) +}; \addlegendentry{PASS} -\addplot[fill=ipsgrey!40] coordinates {(0,agent) (0,api-store) (0,count-api) (1,desktop)}; +\addplot[fill=ipsgrey!35, draw=ipsgrey] coordinates { + (0,desktop) (0,count-api) (0,api-store) (0,agent) +}; \addlegendentry{N/E} \end{axis} \end{tikzpicture} -\caption{Unitarias ejecutadas vs pendientes.} +\caption{Unitarias ejecutadas (todas en verde tras Vitest desktop).} \label{fig:unitarias} \end{figure} diff --git a/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex b/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex index 5d5e035..d3660a6 100644 --- a/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex +++ b/docs/plan_pruebas/informe/sections/06_resultados_humo_funcional.tex @@ -1,6 +1,24 @@ \chapter{Resultados --- humo y funcional} \label{ch:humo} +\callout{\textbf{Veredicto humo / funcional:} \pass}{% +Gateway live y health agregado OK.\\ +E2E-07 (worker offline $\rightarrow$ 409): \pass. +} + +\section{Qu\'e eval\'uan} +\begin{center} +\begin{tabular}{@{}p{3.2cm}p{3.2cm}p{2.4cm}p{4.0cm}@{}} +\toprule +\textbf{Check} & \textbf{Qu\'e eval\'ua} & \textbf{Resultado} & \textbf{Significado} \\ +\midrule +\texttt{/health/live} & Liveness gateway & \pass\ 11\,ms & Responde sin depender de upstreams \\ +\texttt{/health} & Health agregado & \pass\ 194\,ms & extraction, cleaning, counting, bot-descarga ok \\ +E2E-07 & Job con agent offline & \pass\ 409 & Orquestador no asigna sin agent online \\ +\bottomrule +\end{tabular} +\end{center} + \section{Humo gateway} \begin{table}[ht] \centering @@ -10,14 +28,49 @@ \midrule \texttt{GET /health/live} & \pass & 11\,ms & Liveness \\ \texttt{GET /health} & \pass & 194\,ms & 4 servicios ok \\ -Contenedores IPS healthy & \pass & --- & 17\,h uptime \\ +Contenedores IPS healthy & \pass & --- & Stack arriba \\ \bottomrule \end{tabular} \caption{Humo HTTP.} \end{table} \section{Funcional E2E-07 (worker offline $\rightarrow$ 409)} -\neje\ en esta campa\~na (no se envi\'o \texttt{POST /bot-descarga/jobs} contra worker offline). Queda como follow-up de 2 minutos en staging. +\subsection*{Qu\'e es} +Comprueba que el orquestador \texttt{bot-descarga} \textbf{no} crea un job de descarga +si el \textbf{agent Selenium} no est\'a online (sin heartbeat). Debe responder \textbf{HTTP 409}. -\section{Script preparado} -Se a\~nadi\'o \texttt{docs/plan\_pruebas/scripts/k6\_smoke\_suite.js} para repeticiones automatizadas de live/health/config. +\subsection*{C\'omo se ejecut\'o} +Script: \texttt{docs/plan\_pruebas/scripts/e2e\_07\_worker\_offline.py} +\begin{enumerate} + \item \texttt{POST /jobs} --- crea job Desktop padre. + \item Verifica que el worker \texttt{e2e07-offline-pc} no est\'a online. + \item \texttt{POST /bot-descarga/jobs} (multipart Excel + \texttt{worker\_id} offline). +\end{enumerate} + +\subsection*{Resultado} +\begin{center} +\begin{tabular}{@{}ll@{}} +\toprule +\textbf{Paso} & \textbf{Resultado} \\ +\midrule +Health live & 200 \\ +Job padre Desktop & 200 (id creado) \\ +Worker online? & no \\ +Crear job descarga & \textbf{409} (esperado) \\ +Detalle API & Worker no est\'a online; enciende el agent primero \\ +\midrule +\textbf{Veredicto} & \pass \\ +\bottomrule +\end{tabular} +\end{center} + +\callout{\textbf{Significado}}{% +El orquestador protege el flujo: sin agent Selenium encendido no se encola descarga.\\ +Evidencia en \texttt{docs/plan\_pruebas/informes/e2e\_07\_latest.json}. +} + +\section{Scripts} +\begin{itemize} + \item Humo: \texttt{docs/plan\_pruebas/scripts/k6\_smoke\_suite.js} + \item E2E-07: \texttt{docs/plan\_pruebas/scripts/e2e\_07\_worker\_offline.py} +\end{itemize} diff --git a/docs/plan_pruebas/informe/sections/07_resultados_carga.tex b/docs/plan_pruebas/informe/sections/07_resultados_carga.tex index 98e878c..350dee8 100644 --- a/docs/plan_pruebas/informe/sections/07_resultados_carga.tex +++ b/docs/plan_pruebas/informe/sections/07_resultados_carga.tex @@ -1,6 +1,24 @@ \chapter{Resultados --- pruebas de carga} \label{ch:carga} +\callout{\textbf{Veredicto carga:} \pass\ (con matiz)}{% +ST-GW-01 \pass. ST-BOT-01: corrida 1 \parcial\ (contenci\'on); corrida 2 \pass.\\ +ST-BOT-03 \neje\ (falt\'o DOWNLOAD\_JOB\_ID). +} + +\section{Qu\'e eval\'uan} +\begin{center} +\begin{tabular}{@{}p{2.2cm}p{3.8cm}p{3.0cm}p{3.8cm}@{}} +\toprule +\textbf{ID} & \textbf{Qu\'e eval\'ua} & \textbf{Resultado} & \textbf{Significado} \\ +\midrule +ST-GW-01 & Gateway bajo VUs (\texttt{/health/live}) & p95 41\,ms, 0\,\% fail \pass & Orquestaci\'on no es cuello \\ +ST-BOT-01 & Heartbeats masivos (orquestador \texttt{bot-descarga}) & \#2 p95 86\,ms \pass & Orquestador OK si hay RAM \\ +ST-BOT-03 & Tormenta de events & \neje & Pendiente job real \\ +\bottomrule +\end{tabular} +\end{center} + \section{Tabla consolidada} \begin{table}[ht] \centering @@ -19,46 +37,68 @@ ST-BOT-03 & --- & --- & --- & --- & --- & \neje \\ \end{table} \section{ST-GW-01} +\label{sec:st-gw-01} + +\subsection*{Qu\'e es} +Prueba de \textbf{carga del API Gateway} (puerto :8189). Con la herramienta k6 se simulan +varios usuarios virtuales (VUs) que llaman una y otra vez a \texttt{GET /health/live} +(``\textquestiondown est\'as vivo?''). + +No procesa facturas: no pasa por OCR, LLM ni agent Selenium. Solo comprueba que la \textbf{puerta de entrada} +sigue respondiendo r\'apido cuando hay muchas peticiones a la vez. + +\subsection*{Umbrales que deb\'ia cumplir} \begin{itemize} - \item Checks 13768/13768 (100\,\%); throughput $\approx$57\,req/s. - \item Thresholds p95$<$500\,ms y fail$<$1\,\% en verde. + \item p95 (el 95\,\% de las respuestas) $<$ 500\,ms + \item porcentaje de fallos $<$ 1\,\% \end{itemize} -\section{ST-BOT-01 --- comparativa} -Corrida 1: funcional OK, latencia FAIL (contenci\'on host).\\ -Corrida 2: funcional OK, latencia PASS (host m\'as libre). +\subsection*{Resultado} +\begin{center} +\begin{tabular}{@{}lrl@{}} +\toprule +\textbf{M\'etrica} & \textbf{Valor} & \textbf{Lectura} \\ +\midrule +Peticiones & 6\,884 & carga real enviada \\ +Fallos & 0\,\% & ninguna petici\'on rota \\ +Checks k6 & 13\,768/13\,768 (100\,\%) & todas las aserciones OK \\ +Promedio & 12{,}6\,ms & muy por debajo del umbral \\ +p95 & 40{,}9\,ms & \pass\ (l\'imite 500\,ms) \\ +M\'aximo & 125\,ms & sin outliers graves \\ +Ritmo & $\approx$57\,req/s & ver secci\'on~7.5 \\ +\bottomrule +\end{tabular} +\end{center} -\begin{figure}[ht] -\centering -\begin{tikzpicture} -\begin{axis}[ - ybar, - width=0.95\textwidth, - height=7cm, - ylabel={Latencia (ms)}, - symbolic x coords={GW-p95,BOT1-p95,BOT2-p95,GW-max,BOT1-max,BOT2-max}, - xtick=data, - x tick label style={rotate=30, anchor=east, font=\small}, - nodes near coords, - nodes near coords style={font=\scriptsize}, - bar width=12pt, - ymin=0, - enlarge x limits=0.12, - grid=major, - legend entries={medici\'on, umbral GW 500, umbral BOT 800}, - legend style={at={(0.02,0.98)},anchor=north west,font=\scriptsize} -] -\addplot[fill=ipsblue!75] coordinates { - (GW-p95,40.85) (BOT1-p95,2280) (BOT2-p95,85.55) - (GW-max,125.37) (BOT1-max,23120) (BOT2-max,98.31) -}; -\addplot[red, sharp plot, update limits=false, forget plot] coordinates {(GW-p95,500) (BOT2-max,500)}; -\addplot[orange, sharp plot, update limits=false] coordinates {(BOT1-p95,800) (BOT2-p95,800)}; -\end{axis} -\end{tikzpicture} -\caption{p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla.} -\label{fig:latencias} -\end{figure} +\callout{\textbf{Veredicto ST-GW-01:} \pass}{% +El gateway aguanta carga concurrente ligera sin errores y con latencia baja.\\ +\textbf{Significado:} la orquestaci\'on HTTP \textbf{no} es el cuello de botella del sistema; +el l\'imite de capacidad est\'a en OCR / LLM / agent Selenium (otros cap\'itulos), no en :8189. +} + + +\section{ST-BOT-01 --- comparativa} +\subsection*{Qu\'e es} +Prueba de \textbf{carga del orquestador \texttt{bot-descarga}} (API Docker en :8200, v\'ia gateway). +k6 env\'ia 80 \texttt{POST /bot-descarga/workers/\{id\}/heartbeat} simulando agents online. +\textbf{No} mide descarga Selenium ni la capacidad $\sim$422 fact/h del agent PC; solo la API de control +(jobs, claim, heartbeats) bajo presi\'on. + +Misma prueba (80 heartbeats k6 contra el orquestador \texttt{bot-descarga}), dos momentos del host: + +\begin{center} +\begin{tabular}{@{}p{3.6cm}p{4.6cm}p{4.6cm}@{}} +\toprule + & \textbf{Corrida 1 --- host compartido} & \textbf{Corrida 2 --- host m\'as libre} \\ +\midrule +Qu\'e pasaba & Swap $\approx$2{,}1\,GiB; MLflow y otros servicios compitiendo & Menos carga; m\'as RAM libre \\ +Funcional & 100\,\% HTTP 200 & 100\,\% HTTP 200 \\ +p95 & \textbf{2{,}28\,s} (umbral 800\,ms) & \textbf{86\,ms} \\ +max & \textbf{23{,}1\,s} & \textbf{98\,ms} \\ +Veredicto & \parcial\ (latencia) & \pass \\ +\bottomrule +\end{tabular} +\end{center} \begin{figure}[ht] \centering @@ -66,45 +106,91 @@ Corrida 2: funcional OK, latencia PASS (host m\'as libre). \begin{axis}[ ybar, width=0.9\textwidth, - height=6cm, - ylabel={ms}, - symbolic x coords={avg,med,p95,max}, + height=6.4cm, + ylabel={Latencia heartbeat (ms)}, + symbolic x coords={promedio,p95,maximo}, xtick=data, - legend style={at={(0.02,0.98)},anchor=north west}, - bar width=10pt, - ymin=0, ymax=600, - enlarge x limits=0.15, + xticklabels={promedio,p95,m\'aximo}, + enlarge x limits=0.25, + ymin=0, ymax=2800, + bar width=16pt, grid=major, - title={ST-BOT-01: corrida 1 vs 2 (max de \#1 truncado en eje)} + major grid style={ipsgrey!25}, + legend style={ + at={(0.5,-0.2)}, + anchor=north, + legend columns=2, + font=\small, + draw=ipsgrey!40 + }, + point meta=explicit symbolic, + nodes near coords, + every node near coord/.append style={font=\scriptsize\bfseries, anchor=south, yshift=1pt}, ] -\addplot[fill=failred!60] coordinates {(avg,511) (med,57) (p95,500) (max,600)}; -\addlegendentry{\#1 (p95 real 2280; max 23120)} -\addplot[fill=passgreen!70] coordinates {(avg,57) (med,56) (p95,86) (max,98)}; -\addlegendentry{\#2 PASS} +% Maximo corrida 1: valor visual 2500 (recortado); etiqueta indica 23,1 s real +\addplot[fill=failred!65, draw=failred] coordinates { + (promedio,511) [511 ms] + (p95,2280) [2,28 s] + (maximo,2500) [23,1 s*] +}; +\addlegendentry{Corrida 1 --- host compartido} +\addplot[fill=passgreen!75, draw=passgreen] coordinates { + (promedio,57) [57 ms] + (p95,86) [86 ms] + (maximo,98) [98 ms] +}; +\addlegendentry{Corrida 2 --- host m\'as libre} +\draw[orange, thick, dashed] (axis cs:promedio,800) -- (axis cs:maximo,800) + node[midway, above, font=\scriptsize, text=orange] {umbral p95 = 800\,ms}; \end{axis} \end{tikzpicture} -\caption{Before/after heartbeats (eje limitado; valores exactos en tabla~\ref{tab:carga}).} +\caption{Misma prueba ST-BOT-01, distinto estado del host. (*) El m\'aximo real de la corrida 1 +fue 23{,}1\,s; la barra est\'a recortada solo para que se lean el resto de valores. +Detalle num\'erico en la tabla~\ref{tab:carga}.} \label{fig:bot-before-after} \end{figure} +\callout{\textbf{Conclusi\'on de esta prueba:}}{% +Con un \textbf{host dedicado} (sin competir con MLflow/swap/otros stacks) el servicio \texttt{bot-descarga} se comporta +mucho mejor: p95 baja de 2{,}28\,s a 86\,ms y desaparece el outlier de 23\,s. +El fallo de la corrida 1 no es un bug del heartbeat (siempre HTTP 200), sino +\textbf{contenci\'on del host compartido}. Para SLOs estables en staging/prod hace falta +aislar IPSExtractor en un nodo dedicado. +} + + \section{Throughput gateway} +\textbf{Qu\'e mide.} En ST-GW-01, k6 martillea \texttt{GET /health/live} del gateway +(:8189). Ese endpoint solo comprueba que el proceso responde; \textbf{no} cuenta PDFs, +\textbf{no} pasa por OCR ni por el LLM. + +\textbf{Resultado.} $\approx$57 peticiones por segundo (req/s), con p95 41\,ms y 0\,\% de fallos. +Eso indica que la ``puerta\'\' del sistema aguanta bien la orquestaci\'on HTTP ligera. + +\textbf{Qu\'e \emph{no} significa.} 57\,req/s \textbf{no} se traduce a 57 facturas/hora ni a +capacidad del pipeline. La capacidad real la marcan las etapas pesadas (OCR, LLM, agent Selenium), +documentadas en otros cap\'itulos y en Capacidad 100k. + \begin{figure}[ht] \centering \begin{tikzpicture} \begin{axis}[ - width=0.7\textwidth, - height=5cm, + width=0.65\textwidth, + height=4.2cm, ybar, symbolic x coords={ST-GW-01}, xtick=data, - ylabel={req/s}, + ylabel={peticiones / segundo}, nodes near coords, - ymin=0, - bar width=30pt + every node near coord/.append style={font=\small\bfseries}, + ymin=0, ymax=80, + bar width=36pt, + enlarge x limits=0.6 ] -\addplot[fill=ipsblue] coordinates {(ST-GW-01,57.33)}; +\addplot[fill=ipsblue!80, draw=ipsblue] coordinates {(ST-GW-01,57.33)}; \end{axis} \end{tikzpicture} -\caption{Throughput observado en health liveness (no equivale a PDFs/h).} +\caption{Ritmo del gateway en health liveness ($\approx$57\,req/s). Solo mide HTTP ligero, +no facturas procesadas.} \label{fig:throughput} \end{figure} diff --git a/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex b/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex index 5b55c11..f5350d6 100644 --- a/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex +++ b/docs/plan_pruebas/informe/sections/08_capacidad_100k.tex @@ -1,104 +1,10 @@ -\chapter{Capacidad: 100\,000 facturas al mes} -\label{ch:capacidad} +\chapter*{Movido} +\label{ch:capacidad-moved} -\section{Traducci\'on del objetivo} -\begin{table}[ht] -\centering -\begin{tabular}{@{}lrr@{}} -\toprule -\textbf{Escenario operativo} & \textbf{Facturas/d\'ia} & \textbf{Facturas/hora} \\ -\midrule -100\,000 / mes, 24$\times$7 & $\approx$3\,333 & $\approx$\textbf{139} \\ -100\,000 / mes, 8\,h $\times$ 22\,d & $\approx$4\,545 & $\approx$\textbf{570} \\ -\bottomrule -\end{tabular} -\caption{Throughput m\'inimo para cumplir 100k/mes.} -\end{table} +El an\'alisis de \textbf{100\,000 facturas/mes} vive ahora en el documento independiente: -\section{Modelo por etapa} -Supuestos: host compartido medido el 2026-07-13; estimaciones \textbf{conservadoras} (no bench OCR/LLM masivo). Detalle en \texttt{scripts/estimate\_capacity.md}. - -\begin{table}[ht] -\centering -\begin{tabular}{@{}lrrll@{}} -\toprule -\textbf{Etapa} & \textbf{Host act.} & \textbf{Dedicado} & \textbf{vs 139/h} & \textbf{vs 570/h} \\ -\midrule -Gateway (orquestaci\'on) & $\gg$10\,000 & $\gg$20\,000 & OK & OK \\ -count-api OCR & $\sim$40 & $\sim$400 & RIESGO & NO \\ -extraction LLM & $\sim$100 & $\sim$800 & \'AMBAR & NO$^{*}$ \\ -bot Selenium (1 agent) & $\sim$60 & $\sim$500$^{\dagger}$ & RIESGO & NO \\ -cleaning & $\sim$800 & $\sim$3\,000 & OK & \'AMBAR \\ -\textbf{Pipeline completo} & $\sim$35 & $\sim$450 & \fail & \'AMBAR \\ -\bottomrule -\end{tabular} -\caption{Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger}$Con $N$ agents en paralelo.} -\label{tab:capacidad} -\end{table} - -\begin{figure}[ht] -\centering -\begin{tikzpicture} -\begin{axis}[ - ybar, - width=\textwidth, - height=7.5cm, - ylabel={Facturas / hora (estimadas)}, - symbolic x coords={Gateway,OCR,LLM,Bot,Cleaning,Pipeline}, - xtick=data, - x tick label style={font=\small}, - legend style={at={(0.02,0.98)},anchor=north west,font=\small}, - bar width=8pt, - ymin=0, ymax=1200, - enlarge x limits=0.08, - grid=major, - title={Capacidad estimada vs objetivos 139/h y 570/h} -] -\addplot[fill=ipsblue!70] coordinates { - (Gateway,1000) (OCR,40) (LLM,100) (Bot,60) (Cleaning,800) (Pipeline,35) -}; -\addlegendentry{Host actual (Gateway truncado)} -\addplot[fill=passgreen!55] coordinates { - (Gateway,1000) (OCR,400) (LLM,800) (Bot,500) (Cleaning,1000) (Pipeline,450) -}; -\addlegendentry{Dedicado (estim.)} -\addplot[failred, ultra thick, sharp plot, update limits=false] coordinates { - (Gateway,139) (Pipeline,139) -}; -\addlegendentry{Objetivo 139/h} -\addplot[warnorange, ultra thick, sharp plot, update limits=false] coordinates { - (Gateway,570) (Pipeline,570) -}; -\addlegendentry{Objetivo 570/h} -\end{axis} -\end{tikzpicture} -\caption{Brecha de capacidad. Gateway real $\gg$1000/h; se trunca para legibilidad.} -\label{fig:capacidad} -\end{figure} - -\section{Interpretaci\'on} -\begin{enumerate} - \item El \textbf{gateway} no es el cuello de botella (evidencia ST-GW-01). - \item En el \textbf{host actual}, OCR + Selenium + LLM compiten por CPU/RAM/swap con MLflow: el pipeline completo queda $\ll$139/h. - \item Con infra \textbf{dedicada} (sin zoo, m\'as RAM, workers, $N$ PCs bot, cuota LLM), acercarse a 139/h (24$\times$7) es plausible; 570/h exige dimensionamiento agresivo y prueba de carga espec\'ifica. -\end{enumerate} - -\section{Veredicto} \begin{center} -\fbox{\parbox{0.92\textwidth}{\centering -\large \textbf{Se pueden procesar +100\,000 facturas/mes?}\\[0.4em] -\normalsize -En el servidor actual compartido: \fail.\\[0.2em] -En arquitectura dedicada y dimensionada: \pass\ condicional (cuota LLM, OCR, agents).\\[0.3em] -\footnotesize Esta campa\~na no ejecut\'o 100k PDFs; el veredicto es modelo + evidencia de carga ligera. -}} +\texttt{docs/plan\_pruebas/capacidad\_100k/} $\rightarrow$ \textbf{Capacidad100k\_IPSExtractor.pdf} \end{center} -\section{Requisitos m\'inimos sugeridos para 100k/mes} -\begin{itemize} - \item Host(s) dedicados IPS: RAM $\geq$16--32\,GiB; SSD; sin MLflow/Airflow en el mismo nodo. - \item count-api: pool OCR dimensionado; l\'imite de memoria realista. - \item extraction: r\'eplicas worker + concurrencia acorde a RPM del LLM; presupuesto de coste. - \item bot: varios agents PC en paralelo hacia Imaging/MinIO. - \item Observabilidad: colas ARQ, p95 por etapa, alertas de swap. -\end{itemize} +Este archivo se conserva solo como marcador para no romper enlaces antiguos. diff --git a/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex b/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex index ed39690..3eb3535 100644 --- a/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex +++ b/docs/plan_pruebas/informe/sections/09_analisis_hallazgos.tex @@ -3,10 +3,21 @@ \section{Hallazgos} \begin{enumerate} - \item \textbf{H1 --- Contenci\'on de plataforma:} ST-BOT-01 \#1 (p95 2{,}28\,s, max 23\,s) vs \#2 (p95 86\,ms) demuestra sensibilidad a swap/MLflow, no a un bug funcional del heartbeat (100\,\% HTTP 200 en ambas). + \item \textbf{H1 --- Contenci\'on de plataforma:} ST-BOT-01 \#1 (p95 2{,}28\,s, max 23\,s) vs \#2 (p95 86\,ms) + demuestra que un \textbf{host dedicado / sin contenci\'on} se comporta mucho mejor. + No es un bug del heartbeat (100\,\% HTTP 200 en ambas); es sensibilidad a swap/MLflow + en el host compartido. \item \textbf{H2 --- Gateway saludable:} 0\,\% errores y p95 41\,ms bajo 25 VUs en liveness. - \item \textbf{H3 --- Unitarias s\'olidas:} 29 casos verdes (19 agent + 7 API store + 3 count-api pipeline). - \item \textbf{H4 --- Capacidad 100k:} imposible en host compartido actual; viable solo con escalado dedicado. + \item \textbf{H3 --- Unitarias s\'olidas:} agent Selenium + \texttt{bot-descarga} + count-api + + Vitest desktop (25/25) en verde. + \item \textbf{H4 --- LLM medido:} p50 7{,}1\,s $\approx$506 fact/h; etapa viable vs 139/h. + \item \textbf{H5 --- Cola ARQ validada:} ST-EXT-01 30/30 drenados en 22{,}5\,s (mock); prod debe usar \texttt{ASYNC\_QUEUE=true}. + \item \textbf{H6 --- OCR bajo carga:} ST-COUNT-01 9/9 ok; p50 35{,}8\,s con 3 concurrentes + (vs $\sim$2\,s en serie) $\Rightarrow$ $\approx$100--325 fact/h en shared8g. + \item \textbf{H7 --- Capacidad 100k:} imposible en host compartido actual (OCR/contenci\'on); + ver PDF \emph{Capacidad 100k/mes}. + \item \textbf{H8 --- E2E-07:} crear job con agent offline $\rightarrow$ HTTP 409 \pass; + el orquestador \texttt{bot-descarga} no asigna descarga sin agent online. \end{enumerate} \section{Scorecard global} @@ -18,18 +29,18 @@ width=0.9\textwidth, height=5.5cm, ylabel={Cantidad de casos}, - symbolic x coords={Unitaria,Humo,Carga,Funcional,Capacidad}, + symbolic x coords={Unitaria,Humo,Carga,LLM,ARQ,Count}, xtick=data, legend style={at={(0.5,-0.22)},anchor=north,legend columns=3,font=\small}, - bar width=18pt, + bar width=14pt, ymin=0, ymax=8, - enlarge x limits=0.12 + enlarge x limits=0.1 ] -\addplot[fill=passgreen!70] coordinates {(Unitaria,3) (Humo,3) (Carga,2) (Funcional,0) (Capacidad,0)}; +\addplot[fill=passgreen!70] coordinates {(Unitaria,4) (Humo,4) (Carga,2) (LLM,1) (ARQ,1) (Count,3)}; \addlegendentry{PASS} -\addplot[fill=warnorange!70] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (Funcional,0) (Capacidad,0)}; +\addplot[fill=warnorange!70] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (LLM,0) (ARQ,0) (Count,0)}; \addlegendentry{PARCIAL} -\addplot[fill=ipsgrey!50] coordinates {(Unitaria,1) (Humo,0) (Carga,1) (Funcional,1) (Capacidad,1)}; +\addplot[fill=ipsgrey!50] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (LLM,0) (ARQ,0) (Count,0)}; \addlegendentry{N/E} \end{axis} \end{tikzpicture} @@ -38,4 +49,6 @@ \end{figure} \section{Limitaciones} -No se midieron tiempos reales OCR/LLM por PDF en esta campa\~na; las cifras de capacidad son estimaciones documentadas para decisi\'on de arquitectura, no un compromiso de SLA. +No se ejecut\'o ST-COUNT-01 lab agresivo ni E2E Imaging a volumen; ST-EXT-01 lab (100--500) pendiente. +El veredicto 100k/mes del pipeline completo es modelo + evidencia parcial +(documento Capacidad 100k), no un compromiso de SLA. diff --git a/docs/plan_pruebas/informe/sections/10_conclusiones.tex b/docs/plan_pruebas/informe/sections/10_conclusiones.tex index e3968b2..9e47917 100644 --- a/docs/plan_pruebas/informe/sections/10_conclusiones.tex +++ b/docs/plan_pruebas/informe/sections/10_conclusiones.tex @@ -3,24 +3,34 @@ \section{Conclusiones} \begin{enumerate} - \item IPSExtractor en staging responde correctamente a humo y carga ligera de control (gateway + heartbeats). - \item La calidad de latencia depende cr\'iticamente de no saturar el host compartido. - \item \textbf{100\,000 facturas/mes no son alcanzables} en el laptop Ubuntu actual con el zoo Docker observado. - \item Con infraestructura dedicada y dimensionada, el objetivo es \textbf{condicional\-mente viable}; requiere prueba de carga espec\'ifica por etapa (OCR, LLM, bot). + \item IPSExtractor en staging responde correctamente a humo y carga ligera (gateway + heartbeats). + \item \textbf{Host dedicado vs compartido:} ST-BOT-01 muestra que, con la misma prueba, + un host libre (sin MLflow/swap compitiendo) baja el p95 de 2{,}28\,s a 86\,ms y elimina + el outlier de 23\,s. El comportamiento es claramente mejor en nodo dedicado; + la latencia mala no fue un bug funcional (HTTP 200 en ambas corridas). + \item Agent Selenium ($\approx$422\,fact/h) y LLM ($\approx$506\,fact/h) cubren el objetivo 24$\times$7 \emph{por etapa}. + \item La cola ARQ (ST-EXT-01) dren\'o 30 docs en $\approx$23\,s con mock; arquitectura async validada. + \item OCR bajo 3 concurrentes (ST-COUNT-01): p50 35{,}8\,s; $\approx$100--325 fact/h en shared8g; + degradaci\'on fuerte vs serie ($\sim$2\,s). + \item El pipeline a 100k/mes en el host actual \textbf{no est\'a garantizado} + (ritmo $\approx\min$ etapas $=$ OCR $\sim$100--325/h; piso $\sim$100 $<$ 139); + detalle en \textbf{Capacidad100k\_IPSExtractor.pdf}. + \item Con infra dedicada + OCR dimensionado, 24$\times$7 es \textbf{condicionalmente viable}. \end{enumerate} \section{Recomendaciones} \begin{enumerate} - \item Separar IPS de MLflow/Airflow/n8n en el nodo de producci\'on/staging de rendimiento. - \item Ejecutar ST-COUNT-01 y bench LLM solo con $\geq$3\,GiB libres y sin swap activo. - \item Completar E2E-07 y ST-BOT-03; archivar JSON de k6 en \texttt{informes/}. - \item Dimensionar agents bot ($N$) antes de comprometer vol\'umenes mensuales de descarga. - \item Incorporar CI: pytest agent + store en cada PR. + \item Separar IPSExtractor en un \textbf{host dedicado} (sin MLflow/Airflow/n8n en el mismo nodo + de rendimiento); ST-BOT-01 lo justifica con evidencia. + \item Producci\'on: \texttt{ASYNC\_QUEUE=true} + workers dimensionados a la cuota LLM. + \item Repetir ST-COUNT-01 en perfil lab (m\'as rps / VUs) con $\geq$3\,GiB libres. + \item Completar ST-BOT-03, ST-EXT-01 perfil lab (100--500) y E2E pendientes. + \item Dimensionar agents Selenium ($N$) y workers extraction antes de comprometer vol\'umenes. + \item Incorporar CI: pytest agent Selenium + orquestador \texttt{bot-descarga} en cada PR. \end{enumerate} -\section{Pr\'oxima campa\~na sugerida} +\section{Documentos relacionados} \begin{itemize} - \item Ventana con MLflow detenido. - \item Bench: 50--200 PDFs reales por etapa (OCR, LLM) midiendo p50/p95 y coste. - \item Actualizar gr\'afica de capacidad con datos medidos (reemplazar estimaciones). + \item \textbf{Plan de pruebas} --- estrategia e IDs. + \item \textbf{Capacidad 100k/mes} --- modelo, coste LLM, veredicto de volumen. \end{itemize} diff --git a/docs/plan_pruebas/informe/sections/11_anexos.tex b/docs/plan_pruebas/informe/sections/11_anexos.tex index c8cbcf5..e8be474 100644 --- a/docs/plan_pruebas/informe/sections/11_anexos.tex +++ b/docs/plan_pruebas/informe/sections/11_anexos.tex @@ -26,11 +26,27 @@ services/bot_descarga/tests: 7 passed in 1.45s count-api internal/pipeline: 3 passed (+8 features/clustering nuevos) Timing: go test -run Timing -v (3 PDFs en docs/plan_pruebas/data/) HTTP: bash docs/plan_pruebas/scripts/measure_count_latency.sh +LLM: python docs/plan_pruebas/scripts/measure_llm_latency.py \end{lstlisting} \section{Scripts} \texttt{k6\_gateway\_health.js}, \texttt{k6\_bot\_heartbeats.js}, \texttt{k6\_bot\_events.js}, -\texttt{k6\_count\_batches.js}, \texttt{k6\_smoke\_suite.js}, \texttt{estimate\_capacity.md}. +\texttt{k6\_count\_batches.js}, \texttt{k6\_smoke\_suite.js}, +\texttt{st\_ext\_01\_arq\_bench.py}, \texttt{st\_ext\_02\_llm\_bench.py}, +\texttt{llm\_mock\_server.py}, \texttt{measure\_count\_latency.*}, \texttt{estimate\_capacity.md}. + +\section{Extracto ST-EXT-01} +\begin{lstlisting} +mode=async queued=30/30 drained=True +enqueue_wall_s=1.307 drain_s=22.544 total_s=23.851 +throughput_docs_per_h ~ 4528 (mock; no SLO LLM) +\end{lstlisting} + +\section{Documentos hermanos} +\begin{itemize} + \item Plan: \texttt{PlanPruebas\_IPSExtractor.pdf} + \item Capacidad 100k: \texttt{Capacidad100k\_IPSExtractor.pdf} +\end{itemize} \section{Reproducibilidad} \begin{enumerate} diff --git a/docs/plan_pruebas/informe/sections/12_count_latency.tex b/docs/plan_pruebas/informe/sections/12_count_latency.tex index ead1bf0..918cd59 100644 --- a/docs/plan_pruebas/informe/sections/12_count_latency.tex +++ b/docs/plan_pruebas/informe/sections/12_count_latency.tex @@ -1,9 +1,26 @@ \chapter{Count-api: qu\'e eval\'ua cada prueba} \label{ch:count-latency} -Este cap\'itulo explica, con ejemplos, las pruebas del microservicio \textbf{count-api} -(conteo de servicios en facturas PDF) y resume los \textbf{resultados} obtenidos -en el host Ubuntu el 13 de julio de 2026. +\callout{\textbf{Veredicto count-api:} \pass\ en muestra}{% +11/11 unitarias; 23/23 PDFs HTTP ok; latencia tipica $\sim$2\,s/PDF.\\ +No sustituye ST-COUNT-01 agresivo (capacidad OCR bajo carga). +} + +\section{Qu\'e eval\'ua / resultado / significado} +\begin{center} +\begin{tabular}{@{}p{3.0cm}p{4.0cm}p{5.5cm}@{}} +\toprule +\textbf{Bloque} & \textbf{Resultado} & \textbf{Qu\'e significa} \\ +\midrule +Features + clustering & 11/11 \pass & L\'ogica OCR de conteo correcta sin PDF \\ +HTTP 23 PDFs & 23/23 \pass & Endpoint responde con PDFs reales \\ +Latencia $\sim$2\,s/PDF & medido & En host dedicado $\sim$1800/h te\'oricos; en shared8g contenci\'on baja a $\sim$40/h estimados \\ +\bottomrule +\end{tabular} +\end{center} + +Este cap\'itulo explica las pruebas del microservicio \textbf{count-api} +(conteo de servicios en facturas PDF) y resume los \textbf{resultados} del 13 de julio de 2026. \section{Pipeline bajo prueba} \begin{center} @@ -27,7 +44,7 @@ en el host Ubuntu el 13 de julio de 2026. \end{center} Las unitarias de \texttt{features} y \texttt{clustering} \textbf{no} abren el PDF: -simulan ``cajas'' OCR \texttt{\{texto, X, Y\}}. Timing y HTTP s\'i usan los 3 PDFs reales. +simulan ``cajas'' OCR \texttt{\{texto, X, Y\}}. Timing y HTTP usan los \textbf{23 PDFs} reales en \texttt{docs/plan\_pruebas/data/}. \section{Resultado global count-api} \begin{table}[ht] @@ -37,11 +54,11 @@ simulan ``cajas'' OCR \texttt{\{texto, X, Y\}}. Timing y HTTP s\'i usan los 3 PD \textbf{Bloque} & \textbf{Resultado} & \textbf{Casos} & \textbf{Notas} \\ \midrule Features + clustering & \pass & 11/11 & Ubuntu, \texttt{go test -v} \\ -HTTP 3 PDFs reales & \pass & 3/3 & 200, status ok, $\sim$2--4{,}5\,s \\ -Timing Go por etapa & pendiente / skip path & --- & re-ejecutar \texttt{-run Timing -count=1} \\ +HTTP 23 PDFs reales & \pass & 23/23 & 200, status ok \\ +Timing Go por etapa & \pass & 23/23 & 46{,}3\,s total; todos ok \\ \bottomrule \end{tabular} -\caption{Scorecard count-api en esta campa\~na.} +\caption{Scorecard count-api --- campa\~na 23 PDFs (13 jul 2026).} \end{table} \begin{figure}[ht] @@ -55,18 +72,16 @@ Timing Go por etapa & pendiente / skip path & --- & re-ejecutar \texttt{-run Tim symbolic x coords={Features,Clustering,HTTP-PDF,Timing-Go}, xtick=data, nodes near coords, - ymin=0, ymax=12, + ymin=0, ymax=25, bar width=14pt, enlarge x limits=0.15, legend style={at={(0.98,0.98)},anchor=north east,font=\small} ] -\addplot[fill=passgreen!75] coordinates {(Features,7) (Clustering,4) (HTTP-PDF,3) (Timing-Go,0)}; +\addplot[fill=passgreen!75] coordinates {(Features,11) (Clustering,0) (HTTP-PDF,23) (Timing-Go,23)}; \addlegendentry{PASS} -\addplot[fill=ipsgrey!45] coordinates {(Features,0) (Clustering,0) (HTTP-PDF,0) (Timing-Go,1)}; -\addlegendentry{Pendiente} \end{axis} \end{tikzpicture} -\caption{Cobertura count-api por bloque.} +\caption{Cobertura count-api: 11 unitarias + 23 PDFs medidos (HTTP y Go).} \label{fig:count-score} \end{figure} @@ -168,12 +183,8 @@ un par cercano forma cluster. \section{Pruebas con PDFs reales} \subsection{Fixtures} -\begin{itemize} - \item \texttt{500000284335937.pdf} - \item \texttt{500000284335940.pdf} - \item \texttt{500000284335954.pdf} -\end{itemize} -Ubicaci\'on: \texttt{docs/plan\_pruebas/data/}. +\textbf{23 PDFs} en \texttt{docs/plan\_pruebas/data/} (facturas m\'edicas reales, $\sim$280--294\,KB c/u). +Listado completo en el CSV \texttt{informes/count\_latency\_20260713.csv}. \subsection{TestProcessPDFTimingFixtures} \textbf{Eval\'ua:} pipeline completo (render$\rightarrow$OCR$\rightarrow$cluster$\rightarrow$conteo) y que @@ -182,55 +193,125 @@ Ubicaci\'on: \texttt{docs/plan\_pruebas/data/}. \subsection{Script HTTP \texttt{measure\_count\_latency}} \textbf{Eval\'ua:} el camino real del usuario/Desktop: gateway \texttt{POST /count/v1/count}. -\section{Resultados de latencia HTTP (medidos)} -Host \texttt{ubuntu}, gateway \texttt{127.0.0.1:8189}, 13 jul 2026: +\section{Resultados agregados (23 PDFs)} +Host \texttt{ubuntu}, 13 jul 2026. Corrida secuencial (\texttt{-count=1}). \begin{table}[ht] \centering -\begin{tabular}{@{}lrrll@{}} +\begin{tabular}{@{}lrrrr@{}} \toprule -\textbf{PDF} & \textbf{t (s)} & \textbf{HTTP} & \textbf{servicios} & \textbf{status} \\ +\textbf{M\'etrica} & \textbf{HTTP (s)} & \textbf{Total Go (ms)} & \textbf{OCR (ms)} & \textbf{Render (ms)} \\ \midrule -500000284335937.pdf & 4{,}45 & 200 & 1 & ok \\ -500000284335940.pdf & 2{,}29 & 200 & 1 & ok \\ -500000284335954.pdf & 2{,}19 & 200 & 1 & ok \\ +Promedio & 1{,}81 & 2008 & 1876 & 131 \\ +M\'inimo & 1{,}62 & 1823 & 1689 & 108 \\ +M\'aximo & 2{,}42 & 2435 & 2285 & 155 \\ +Desv.\ est.\ aprox. & 0{,}21 & 150 & 140 & 14 \\ \midrule -Promedio & 2{,}98 & --- & --- & --- \\ -M\'in / M\'ax & 2{,}19 / 4{,}45 & --- & --- & --- \\ +Servicios detectados/factura & \multicolumn{4}{c}{41--48 (promedio 43{,}5)} \\ +Cluster (ms) & \multicolumn{4}{c}{$<$2 (despreciable)} \\ +Infer (ms) & \multicolumn{4}{c}{0 (heur\'istica sin ONNX en test)} \\ \bottomrule \end{tabular} -\caption{Wall-clock HTTP. Fuente: \texttt{informes/count\_latency\_20260713.csv}.} -\label{tab:count-http} +\caption{Estad\'isticas sobre 23 facturas. HTTP v\'ia gateway; Go v\'ia \texttt{ProcessPDF} directo.} +\label{tab:count-stats} \end{table} \begin{figure}[ht] \centering \begin{tikzpicture} \begin{axis}[ - ybar, - width=0.95\textwidth, + ybar stacked, + width=0.92\textwidth, height=6cm, - ylabel={Segundos (HTTP)}, - symbolic x coords={...937,...940,...954}, + ylabel={Milisegundos (promedio)}, + symbolic x coords={Pipeline}, xtick=data, - nodes near coords, - nodes near coords style={font=\small}, - ymin=0, ymax=5.5, - bar width=22pt, - enlarge x limits=0.2, - title={Tiempo de conteo por PDF (gateway)} + ymin=0, ymax=2200, + legend style={at={(0.5,-0.18)},anchor=north,legend columns=3,font=\small}, + title={Desglose promedio del tiempo por etapa (Go)} ] -\addplot[fill=ipsblue!80] coordinates {(...937,4.45) (...940,2.29) (...954,2.19)}; -\addplot[warnorange, ultra thick, sharp plot, update limits=false] coordinates {(...937,2.98) (...954,2.98)}; +\addplot[fill=ipsblue!60] coordinates {(Pipeline,131)}; +\addlegendentry{Render} +\addplot[fill=warnorange!70] coordinates {(Pipeline,1876)}; +\addlegendentry{OCR} +\addplot[fill=passgreen!50] coordinates {(Pipeline,1)}; +\addlegendentry{Cluster} \end{axis} \end{tikzpicture} -\caption{Barras: tiempo por PDF. L\'inea naranja: promedio 2{,}98\,s.} -\label{fig:count-http-bars} +\caption{El OCR representa $\sim$93\,\% del tiempo de procesamiento.} +\label{fig:count-breakdown} \end{figure} -\textbf{Lectura:} en este host, contar \emph{una} factura tarda del orden de \textbf{2--5 segundos} -(OCR suele dominar). Extrapolaci\'on ingenua a 3\,s/PDF $\approx$ 1\,200 facturas/hora de conteo -si el nodo estuviera dedicado y sin colas --- ver cap\'itulo de capacidad 100k/mes para el contexto completo. +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + width=0.95\textwidth, + height=5.5cm, + xlabel={PDF (orden alfab\'etico, n=23)}, + ylabel={Total (ms)}, + title={Tiempo total por factura (TestProcessPDFTimingFixtures)}, + grid=major, + ymin=1700, ymax=2500, +] +\addplot[blue, mark=*, thick] coordinates { + (1,2435) (2,1954) (3,1980) (4,1994) (5,1823) (6,1955) (7,1965) (8,1873) + (9,1937) (10,1947) (11,2430) (12,1999) (13,2016) (14,1963) (15,1875) + (16,2186) (17,1901) (18,2002) (19,2024) (20,1840) (21,2280) (22,1884) (23,1916) +}; +\addplot[warnorange, dashed, thick] coordinates {(0,2008) (24,2008)}; +\end{axis} +\end{tikzpicture} +\caption{Serie de tiempos totales. L\'inea punteada: promedio 2008\,ms.} +\label{fig:count-series} +\end{figure} + +\section{Resultados de latencia HTTP} +Gateway \texttt{127.0.0.1:8189}, \texttt{POST /count/v1/count}. Los 23 PDFs respondieron \textbf{200 / ok}. + +\begin{table}[ht] +\centering +\small +\begin{tabular}{@{}rr@{}} +\toprule +\textbf{HTTP wall-clock} & \textbf{Valor} \\ +\midrule +Promedio & 1{,}81\,s \\ +M\'in / M\'ax & 1{,}62 / 2{,}42\,s \\ +Throughput secuencial & $\approx$0{,}55 PDF/s ($\approx$1\,980/h) \\ +Tiempo total corrida HTTP & $\approx$42\,s (23 archivos) \\ +\bottomrule +\end{tabular} +\caption{Resumen HTTP (sin concurrencia). Fuente: CSV del 13 jul 2026.} +\label{tab:count-http} +\end{table} + +\textbf{Lectura:} +\begin{itemize} + \item \textbf{100\,\% PASS} en 23/23 PDFs (HTTP y Go). + \item El cuello de botella es \textbf{OCR Tesseract} ($\sim$1{,}7--2{,}3\,s por factura). + \item Render PDF $\sim$130\,ms; clustering $<$2\,ms. + \item Cada factura devuelve $\sim$41--48 l\'ineas de servicio detectadas. + \item A $\sim$2\,s/PDF secuencial $\Rightarrow$ $\approx$1\,800--2\,000 conteos/hora en este host si estuviera dedicado al count-api. +\end{itemize} + +\section{Detalle por archivo} +El listado completo (HTTP + ms por etapa) est\'a en: +\texttt{docs/plan\_pruebas/informes/count\_latency\_20260713.csv}. + +Ejemplo de fila (\texttt{500000284335937.pdf}): +\begin{center} +\small +\begin{tabular}{@{}lr@{}} +Render & 155\,ms \\ +OCR & 2279\,ms \\ +Cluster & 1\,ms \\ +Total Go & 2435\,ms \\ +HTTP & 2{,}42\,s \\ +Servicios & 44 \\ +Status & ok \\ +\end{tabular} +\end{center} \section{C\'omo reproducir} \begin{lstlisting}[language=bash] diff --git a/docs/plan_pruebas/informe/sections/13_llm_latency.tex b/docs/plan_pruebas/informe/sections/13_llm_latency.tex new file mode 100644 index 0000000..f2ee218 --- /dev/null +++ b/docs/plan_pruebas/informe/sections/13_llm_latency.tex @@ -0,0 +1,90 @@ +\chapter{Extracci\'on LLM: bench de latencia (ST-EXT-02)} +\label{ch:llm-latency} + +\callout{\textbf{Veredicto LLM:} \pass\ (23/23)}{% +Modelo \textbf{gpt-5.4-mini}; 20 campos congelados.\\ +p50 $=\mathbf{7{,}114}$\,s $\Rightarrow$ $\approx\mathbf{506}$ fact/h (1 worker).\\ +0 errores, 0$\times$429. Coste corrida est.\ USD 0{,}59. +} + +\section{Qu\'e eval\'ua / resultado / significado} +\begin{center} +\begin{tabular}{@{}p{3.2cm}p{4.2cm}p{5.2cm}@{}} +\toprule +\textbf{Pregunta} & \textbf{Respuesta} & \textbf{Implicaci\'on} \\ +\midrule +Qu\'e eval\'ua & Latencia punta a punta ingest+LLM con mismos campos & Comparabilidad entre PDFs \\ +C\'omo & Script \texttt{st\_ext\_02\_llm\_bench} & Schema congelado en JSON \\ +Resultado & 23 ok; p50 7{,}1\,s; p95 8{,}6\,s & Etapa LLM cubre 139/h \\ +Significado & $\approx$506 fact/h (1\,w); $\approx$1012 (2\,w) & Cuello del mes no es el LLM en esta muestra \\ +\bottomrule +\end{tabular} +\end{center} + +El script mide wall-clock por PDF +(\texttt{POST /extraction/schemas/upload} $\rightarrow$ \texttt{POST /tasks/extraction} +$\rightarrow$ \texttt{POST /tasks/\{id\}/ingest}). + +\section{C\'omo ejecutar} +\begin{lstlisting}[language=bash,caption={ST-EXT-02 (Windows PowerShell)}] +.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 +.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Limit 10 -Model gpt-5.4-mini +\end{lstlisting} + +Schema congelado: \texttt{docs/plan\_pruebas/informes/st\_ext\_02\_schema.json}. +CSV: \texttt{informes/llm\_latency\_20260717.csv}. + +\section{Resultados (2026-07-17)} +\texttt{task\_id=aeee04f9-34c5-4cb0-a303-353dde128265}; modo ingest sync en esta campa\~na. + +\begin{table}[ht] +\centering +\begin{tabular}{@{}llrl@{}} +\toprule +\textbf{M\'etrica} & \textbf{Valor} & \textbf{Unidad} & \textbf{Notas} \\ +\midrule +Modelo & gpt-5.4-mini & --- & 20 campos fijos \\ +PDFs ok / total & 23 / 23 & --- & 0 fallos, 0$\times$429 \\ +wall\_s promedio & 7{,}326 & s & \\ +wall\_s p50 & \textbf{7{,}114} & s & \\ +wall\_s p95 & 8{,}582 & s & min 6{,}463 / max 9{,}944 \\ +Throughput 1 worker & \textbf{$\approx$506} & fact/h & $3600/\mathrm{p50}$ \\ +Throughput 2 workers & $\approx$1\,012 & fact/h & sin cuota RPM \\ +Coste corrida (est.) & 0{,}5911 & USD & $23\times 0{,}0257$ \\ +\bottomrule +\end{tabular} +\caption{ST-EXT-02 --- latencia LLM medida (mismo schema/campos).} +\label{tab:llm-latency} +\end{table} + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=0.85\textwidth, + height=5.5cm, + ylabel={Facturas / hora}, + symbolic x coords={LLM-1w,LLM-2w,Obj-139,Obj-570}, + xtick=data, + nodes near coords, + bar width=14pt, + ymin=0, ymax=1200, + enlarge x limits=0.15, + grid=major, + title={Throughput LLM vs objetivos 100k/mes} +] +\addplot[fill=ipsblue!70] coordinates {(LLM-1w,506) (LLM-2w,1012) (Obj-139,139) (Obj-570,570)}; +\end{axis} +\end{tikzpicture} +\caption{La etapa LLM sola supera 139/h; con 2 workers supera 570/h (sin cuota).} +\label{fig:llm-vs-obj} +\end{figure} + +\section{Lectura para capacidad} +Detalle del modelo 100k/mes (pipeline, OCR, coste USD\,2\,570) en el documento +\textbf{Capacidad100k\_IPSExtractor.pdf}. Aqu\'i solo: la etapa LLM \textbf{no} es el cuello +en el host actual frente a OCR/contenci\'on. + +\subsection*{Nota agent Selenium} +633 PDFs / 90\,min $\Rightarrow$ $\approx$422 fact/h (1 agent PC) --- tambi\'en cubre 139/h en descarga. diff --git a/docs/plan_pruebas/informe/sections/14_arq_ext01.tex b/docs/plan_pruebas/informe/sections/14_arq_ext01.tex new file mode 100644 index 0000000..6a0a6ef --- /dev/null +++ b/docs/plan_pruebas/informe/sections/14_arq_ext01.tex @@ -0,0 +1,83 @@ +\chapter{Cola ARQ: bench ST-EXT-01 (LLM mock)} +\label{ch:arq-ext01} + +\callout{\textbf{Veredicto ST-EXT-01:} \pass}{% +30/30 encolados y drenados; modo \textbf{async}; mock LLM.\\ +Enqueue 1{,}3\,s; drain 22{,}5\,s; throughput cola $\approx$4\,500 docs/h (mock, no LLM real). +} + +\section{Qu\'e eval\'ua / resultado / significado} +\begin{center} +\begin{tabular}{@{}p{3.2cm}p{4.5cm}p{5.0cm}@{}} +\toprule +\textbf{Pregunta} & \textbf{Respuesta} & \textbf{Implicaci\'on} \\ +\midrule +Qu\'e eval\'ua & Enqueue + drenado ARQ con LLM mock & Capacidad de cola, no latencia OpenAI \\ +C\'omo & \texttt{st\_ext\_01\_arq\_bench} + mock :8099 & \texttt{ASYNC\_QUEUE=true}, 1 worker \\ +Resultado & 30/30 queued; drained; 23{,}9\,s total & Cola operativa en shared8g \\ +Significado & Prod debe usar async + workers & El cuello real sigue siendo OCR/LLM real \\ +\bottomrule +\end{tabular} +\end{center} + +\section{Condiciones de la corrida} +\begin{itemize} + \item Fecha: 2026-07-17; perfil \texttt{shared8g}; \texttt{Limit=30}. + \item \texttt{task\_id=25890e3d-9e07-46a7-b961-046421d26eb6}. + \item Mock OpenAI-compatible (\texttt{llm\_mock\_server}, delay $\sim$50\,ms). + \item \texttt{ASYNC\_QUEUE=true}; \texttt{EXTRACTOR\_BASE\_URL}$\rightarrow$ mock; 1 r\'eplica worker. + \item CSV: \texttt{informes/st\_ext\_01\_arq\_20260717.csv}. +\end{itemize} + +\section{Resultados} +\begin{table}[ht] +\centering +\begin{tabular}{@{}llrl@{}} +\toprule +\textbf{M\'etrica} & \textbf{Valor} & \textbf{Unidad} & \textbf{Notas} \\ +\midrule +mode & async & --- & \texttt{queued=true} en ingest \\ +Encolados & 30 / 30 & docs & 0 fallos HTTP \\ +enqueue wall & 1{,}307 & s & avg 0{,}043\,s/doc \\ +drain & 22{,}544 & s & backlog vaciado \\ +total & 23{,}851 & s & enqueue + drain \\ +processed & 30 & docs & \pass\ drained \\ +Throughput cola & $\approx$4\,528 & docs/h & \textbf{solo mock}; no usar como SLO LLM \\ +\bottomrule +\end{tabular} +\caption{ST-EXT-01 --- cola ARQ con LLM mock (shared8g, 30 docs).} +\label{tab:st-ext-01} +\end{table} + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=0.75\textwidth, + height=5.2cm, + ylabel={Segundos}, + symbolic x coords={Enqueue,Drain,Total}, + xtick=data, + nodes near coords, + bar width=18pt, + ymin=0, ymax=30, + enlarge x limits=0.25, + grid=major, + title={ST-EXT-01 --- tiempos (30 docs, mock)} +] +\addplot[fill=ipsblue!70] coordinates {(Enqueue,1.307) (Drain,22.544) (Total,23.851)}; +\end{axis} +\end{tikzpicture} +\caption{Casi todo el tiempo es drenado por workers; el enqueue es despreciable.} +\label{fig:st-ext-01-times} +\end{figure} + +\section{Lectura} +\begin{itemize} + \item La arquitectura \textbf{Gateway $\rightarrow$ Redis/ARQ $\rightarrow$ workers} funciona bajo el perfil shared8g. + \item El throughput $\approx$4\,500 docs/h \textbf{no} es capacidad de extracci\'on real: + ST-EXT-02 midió $\approx$506 fact/h con gpt-5.4-mini. + \item Recomendaci\'on prod: \texttt{ASYNC\_QUEUE=true} + workers dimensionados a la cuota LLM. + \item Pendiente de escala: perfil \texttt{lab} (100--500 docs, 2 workers). +\end{itemize} diff --git a/docs/plan_pruebas/informe/sections/15_st_count_01.tex b/docs/plan_pruebas/informe/sections/15_st_count_01.tex new file mode 100644 index 0000000..85b3d6f --- /dev/null +++ b/docs/plan_pruebas/informe/sections/15_st_count_01.tex @@ -0,0 +1,85 @@ +\chapter{Count-api bajo carga (ST-COUNT-01)} +\label{ch:st-count-01} + +\callout{\textbf{Veredicto ST-COUNT-01:} \pass\ (shared8g)}{% +9/9 PDFs reales OK; 0$\times$5xx; sin OOM observado.\\ +Bajo concurrencia 3, p50 sube a \textbf{35{,}8\,s} (vs $\sim$2\,s en serie).\\ +Throughput observado $\approx$\textbf{325} fact/h en la ventana. +} + +\section{Qu\'e eval\'ua / resultado / significado} +\begin{center} +\begin{tabular}{@{}p{3.2cm}p{4.5cm}p{5.0cm}@{}} +\toprule +\textbf{Pregunta} & \textbf{Respuesta} & \textbf{Implicaci\'on} \\ +\midrule +Qu\'e eval\'ua & OCR count-api con lotes concurrentes & Cuello de 100k/mes \\ +C\'omo & \texttt{st\_count\_01\_bench} 60\,s, max 3 workers & PDFs reales en \texttt{data/} \\ +Resultado & 9 ok; p50 35{,}8\,s; 0 fallos & Contenci\'on fuerte vs serie \\ +Significado & Host shared: $\sim$100--325 fact/h & vs 139/h \'ambar/OK; vs 570/h NO \\ +\bottomrule +\end{tabular} +\end{center} + +\section{Condiciones} +\begin{itemize} + \item Fecha: 2026-07-17; perfil \texttt{shared8g}; rate objetivo 1/s; max 3 concurrentes; 60\,s. + \item Endpoint: \texttt{POST /count/v1/count} (campo multipart \texttt{files}). + \item CSV: \texttt{informes/st\_count\_01\_20260717.csv}. + \item Nota: k6 no estaba en el PATH de Windows; se us\'o el bench Python equivalente. + El script k6 se corrigi\'o (\texttt{files} en lugar de \texttt{file}). +\end{itemize} + +\section{Resultados} +\begin{table}[ht] +\centering +\begin{tabular}{@{}llrl@{}} +\toprule +\textbf{M\'etrica} & \textbf{Valor} & \textbf{Unidad} & \textbf{Notas} \\ +\midrule +ok / total & 9 / 9 & --- & 0 fallos \\ +wall\_s p50 & \textbf{35{,}825} & s & bajo 3 concurrentes \\ +wall\_s p95 & 43{,}927 & s & min 17{,}6 / max 44{,}7 \\ +wall total & 99{,}7 & s & incluye drenado final \\ +Throughput observado & $\approx$325 & fact/h & $9/99{,}7\times 3600$ \\ +Throughput serie (p50) & $\approx$100 & fact/h & $3600/\mathrm{p50}$ \\ +$\times$3 ideal & $\approx$301 & fact/h & sin m\'as contenci\'on \\ +\bottomrule +\end{tabular} +\caption{ST-COUNT-01 --- carga OCR (PDFs reales, shared8g).} +\label{tab:st-count-01} +\end{table} + +\begin{figure}[ht] +\centering +\begin{tikzpicture} +\begin{axis}[ + ybar, + width=0.8\textwidth, + height=5.5cm, + ylabel={Facturas / hora}, + symbolic x coords={Serie-p50,Obs-3conc,Obj-139,Obj-570}, + xtick=data, + x tick label style={font=\small}, + nodes near coords, + bar width=12pt, + ymin=0, ymax=700, + enlarge x limits=0.15, + grid=major, + title={OCR ST-COUNT-01 vs objetivos 100k} +] +\addplot[fill=ipsblue!70] coordinates {(Serie-p50,100) (Obs-3conc,325) (Obj-139,139) (Obj-570,570)}; +\end{axis} +\end{tikzpicture} +\caption{Bajo 3 concurrentes el OCR cubre 139/h en esta muestra; no alcanza 570/h. +La latencia por PDF se degrada mucho frente a la serie ($\sim$2\,s).} +\label{fig:st-count-01} +\end{figure} + +\section{Lectura} +\begin{itemize} + \item \pass\ funcional: count-api aguanta concurrencia ligera sin 5xx/OOM en esta corrida. + \item La degradaci\'on 2\,s $\rightarrow$ 36\,s confirma que el OCR es sensible a CPU/RAM compartida. + \item Actualiza la fila OCR del modelo 100k: de $\sim$40 estimados a $\sim$100--325 medidos (shared8g). + \item Para afirmar 570/h hace falta host dedicado + pool OCR (perfil lab / ST-COUNT agresivo). +\end{itemize} diff --git a/docs/plan_pruebas/informes/count_latency_20260713.csv b/docs/plan_pruebas/informes/count_latency_20260713.csv index aad73e0..2db59ae 100644 --- a/docs/plan_pruebas/informes/count_latency_20260713.csv +++ b/docs/plan_pruebas/informes/count_latency_20260713.csv @@ -1,4 +1,24 @@ -file,http_time_s,http_code,service_count,status -500000284335937.pdf,4.449556,200,1,ok -500000284335940.pdf,2.290720,200,1,ok -500000284335954.pdf,2.190165,200,1,ok +file,http_time_s,http_code,service_count,status,render_ms,ocr_ms,cluster_ms,total_ms +500000284335937.pdf,2.419290,200,1,ok,155,2279,1,2435 +500000284335940.pdf,2.189024,200,1,ok,134,1820,0,1954 +500000284335954.pdf,1.615784,200,1,ok,127,1851,0,1980 +500000284335956.pdf,1.956593,200,1,ok,147,1846,1,1994 +500000284335963.pdf,1.981622,200,1,ok,132,1689,1,1823 +500000284335964.pdf,1.696154,200,1,ok,117,1837,0,1955 +500000284335968.pdf,1.948234,200,1,ok,150,1813,1,1965 +500000284335997.pdf,1.659651,200,1,ok,125,1746,1,1873 +500000284336071.pdf,1.667741,200,1,ok,127,1808,0,1937 +500000284336521.pdf,1.741292,200,1,ok,130,1816,0,1947 +500000284336836.pdf,1.916852,200,1,ok,143,2285,1,2430 +500000284336915.pdf,1.679965,200,1,ok,144,1853,0,1999 +500000284337016.pdf,1.666228,200,1,ok,138,1877,0,2016 +500000284337418.pdf,1.642242,200,1,ok,133,1829,1,1963 +500000284337861.pdf,1.768077,200,1,ok,142,1730,1,1875 +500000284338027.pdf,1.677103,200,1,ok,116,2068,0,2186 +500000284338092.pdf,1.667427,200,1,ok,109,1792,0,1901 +500000284338214.pdf,1.781158,200,1,ok,128,1873,0,2002 +500000284338327.pdf,1.796004,200,1,ok,126,1897,0,2024 +500000284338353.pdf,1.803265,200,1,ok,108,1731,0,1840 +500000284338458.pdf,1.824801,200,1,ok,135,2144,0,2280 +500000284338503.pdf,1.897579,200,1,ok,117,1766,0,1884 +500000284338624.pdf,1.635177,200,1,ok,126,1788,0,1916 diff --git a/docs/plan_pruebas/informes/llm_latency_20260717.csv b/docs/plan_pruebas/informes/llm_latency_20260717.csv new file mode 100644 index 0000000..86ef6fc --- /dev/null +++ b/docs/plan_pruebas/informes/llm_latency_20260717.csv @@ -0,0 +1,24 @@ +file,wall_s,http_code,status,queued,cost_usd_est,error +500000284335937.pdf,9.944,200,ok,0,0.0257, +500000284335940.pdf,7.925,200,ok,0,0.0257, +500000284335954.pdf,7.659,200,ok,0,0.0257, +500000284335956.pdf,7.114,200,ok,0,0.0257, +500000284335963.pdf,6.997,200,ok,0,0.0257, +500000284335964.pdf,7.889,200,ok,0,0.0257, +500000284335968.pdf,7.001,200,ok,0,0.0257, +500000284335997.pdf,6.822,200,ok,0,0.0257, +500000284336071.pdf,6.463,200,ok,0,0.0257, +500000284336521.pdf,7.472,200,ok,0,0.0257, +500000284336836.pdf,7.199,200,ok,0,0.0257, +500000284336915.pdf,7.094,200,ok,0,0.0257, +500000284337016.pdf,6.874,200,ok,0,0.0257, +500000284337418.pdf,7.146,200,ok,0,0.0257, +500000284337861.pdf,6.816,200,ok,0,0.0257, +500000284338027.pdf,7.161,200,ok,0,0.0257, +500000284338092.pdf,7.927,200,ok,0,0.0257, +500000284338214.pdf,6.563,200,ok,0,0.0257, +500000284338327.pdf,8.655,200,ok,0,0.0257, +500000284338353.pdf,6.963,200,ok,0,0.0257, +500000284338458.pdf,7.327,200,ok,0,0.0257, +500000284338503.pdf,6.672,200,ok,0,0.0257, +500000284338624.pdf,6.816,200,ok,0,0.0257, diff --git a/docs/plan_pruebas/informes/llm_latency_PENDING.csv b/docs/plan_pruebas/informes/llm_latency_PENDING.csv new file mode 100644 index 0000000..2e6e08d --- /dev/null +++ b/docs/plan_pruebas/informes/llm_latency_PENDING.csv @@ -0,0 +1 @@ +file,wall_s,http_code,status,queued,error diff --git a/docs/plan_pruebas/informes/st_count_01_20260717.csv b/docs/plan_pruebas/informes/st_count_01_20260717.csv new file mode 100644 index 0000000..155eeb9 --- /dev/null +++ b/docs/plan_pruebas/informes/st_count_01_20260717.csv @@ -0,0 +1,10 @@ +file,wall_s,http_code,status,service_count,error +500000284335954.pdf,17.562,200,ok,1, +500000284335940.pdf,18.557,200,ok,1, +500000284335937.pdf,19.556,200,ok,1, +500000284335956.pdf,35.423,200,ok,1, +500000284335963.pdf,35.825,200,ok,1, +500000284335964.pdf,35.909,200,ok,1, +500000284335997.pdf,42.798,200,ok,1, +500000284336071.pdf,41.739,200,ok,1, +500000284335968.pdf,44.679,200,ok,1, diff --git a/docs/plan_pruebas/informes/st_ext_01_arq_20260717.csv b/docs/plan_pruebas/informes/st_ext_01_arq_20260717.csv new file mode 100644 index 0000000..7cbe501 --- /dev/null +++ b/docs/plan_pruebas/informes/st_ext_01_arq_20260717.csv @@ -0,0 +1,31 @@ +radicado,file,enqueue_s,http_code,status,queued,error +500000284335937_0000,500000284335937.pdf,0.23,200,queued,1, +500000284335940_0001,500000284335940.pdf,0.042,200,queued,1, +500000284335954_0002,500000284335954.pdf,0.026,200,queued,1, +500000284335956_0003,500000284335956.pdf,0.042,200,queued,1, +500000284335963_0004,500000284335963.pdf,0.052,200,queued,1, +500000284335964_0005,500000284335964.pdf,0.025,200,queued,1, +500000284335968_0006,500000284335968.pdf,0.028,200,queued,1, +500000284335997_0007,500000284335997.pdf,0.035,200,queued,1, +500000284336071_0008,500000284336071.pdf,0.029,200,queued,1, +500000284336521_0009,500000284336521.pdf,0.028,200,queued,1, +500000284336836_0010,500000284336836.pdf,0.031,200,queued,1, +500000284336915_0011,500000284336915.pdf,0.041,200,queued,1, +500000284337016_0012,500000284337016.pdf,0.046,200,queued,1, +500000284337418_0013,500000284337418.pdf,0.043,200,queued,1, +500000284337861_0014,500000284337861.pdf,0.058,200,queued,1, +500000284338027_0015,500000284338027.pdf,0.045,200,queued,1, +500000284338092_0016,500000284338092.pdf,0.084,200,queued,1, +500000284338214_0017,500000284338214.pdf,0.041,200,queued,1, +500000284338327_0018,500000284338327.pdf,0.057,200,queued,1, +500000284338353_0019,500000284338353.pdf,0.027,200,queued,1, +500000284338458_0020,500000284338458.pdf,0.023,200,queued,1, +500000284338503_0021,500000284338503.pdf,0.028,200,queued,1, +500000284338624_0022,500000284338624.pdf,0.042,200,queued,1, +500000284335937_0023,500000284335937.pdf,0.026,200,queued,1, +500000284335940_0024,500000284335940.pdf,0.021,200,queued,1, +500000284335954_0025,500000284335954.pdf,0.026,200,queued,1, +500000284335956_0026,500000284335956.pdf,0.022,200,queued,1, +500000284335963_0027,500000284335963.pdf,0.023,200,queued,1, +500000284335964_0028,500000284335964.pdf,0.028,200,queued,1, +500000284335968_0029,500000284335968.pdf,0.033,200,queued,1, diff --git a/docs/plan_pruebas/main.aux b/docs/plan_pruebas/main.aux index a51283d..93b7961 100644 --- a/docs/plan_pruebas/main.aux +++ b/docs/plan_pruebas/main.aux @@ -9,59 +9,67 @@ \providecommand\HyField@AuxAddToFields[1]{} \providecommand\HyField@AuxAddToCoFields[2]{} \babel@aux{spanish}{} -\@writefile{toc}{\contentsline {chapter}{\numberline {1}Introducci\'on}{6}{chapter.1}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {1}Introducci\'on}{5}{chapter.1}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:intro}{{1}{6}{Introducci\'on}{chapter.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {1.1}Objetivo}{6}{section.1.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {1.2}Alcance}{6}{section.1.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{6}{subsection.1.2.1}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{6}{subsection.1.2.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{6}{section.1.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {1.4}Referencias del sistema}{6}{section.1.4}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces Cobertura existente y gaps.}}{7}{table.1.1}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{8}{chapter.2}\protected@file@percent } +\newlabel{ch:intro}{{1}{5}{Introducci\'on}{chapter.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {1.1}Objetivo}{5}{section.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.2}Alcance}{5}{section.1.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{5}{subsection.1.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{5}{subsection.1.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{5}{section.1.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {1.4}Referencias del sistema}{5}{section.1.4}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces Cobertura existente y gaps.}}{6}{table.1.1}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{7}{chapter.2}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:arquitectura}{{2}{8}{Arquitectura bajo prueba}{chapter.2}{}} -\@writefile{toc}{\contentsline {section}{\numberline {2.1}Vista de componentes}{8}{section.2.1}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Arquitectura IPSExtractor bajo prueba.}}{8}{figure.2.1}\protected@file@percent } -\newlabel{fig:arquitectura}{{2.1}{8}{Arquitectura IPSExtractor bajo prueba}{figure.2.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {2.2}Inventario de componentes}{8}{section.2.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {2.3}Flujo cr\'itico: bot de descarga}{8}{section.2.3}\protected@file@percent } +\newlabel{ch:arquitectura}{{2}{7}{Arquitectura bajo prueba}{chapter.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.1}C\'omo est\'a dise\~nado el sistema}{7}{section.2.1}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres.}}{7}{figure.2.1}\protected@file@percent } +\newlabel{fig:arquitectura}{{2.1}{7}{Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres}{figure.2.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga.}}{8}{figure.2.2}\protected@file@percent } +\newlabel{fig:patrones}{{2.2}{8}{Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga}{figure.2.2}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.3}Pipeline de negocio}{8}{section.2.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.3}{\ignorespaces Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats.}}{9}{figure.2.3}\protected@file@percent } +\newlabel{fig:pipeline-negocio}{{2.3}{9}{Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats}{figure.2.3}{}} \@writefile{lot}{\contentsline {table}{\numberline {2.1}{\ignorespaces Componentes del sistema bajo prueba.}}{9}{table.2.1}\protected@file@percent } -\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Secuencia del flujo bot de descarga.}}{9}{figure.2.2}\protected@file@percent } -\newlabel{fig:flujo-bot}{{2.2}{9}{Secuencia del flujo bot de descarga}{figure.2.2}{}} -\@writefile{lol}{\contentsline {lstlisting}{\numberline {2.1}Prefijos MinIO del bot}{9}{lstlisting.2.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {2.4}Otros flujos de negocio}{9}{section.2.4}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{10}{chapter.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {2.4}Inventario de componentes}{9}{section.2.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {2.5}Flujo cr\'itico: descarga (orquestador + agent Selenium)}{9}{section.2.5}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {2.1}Prefijos MinIO del pipeline de descarga}{9}{lstlisting.2.1}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {2.4}{\ignorespaces Secuencia del flujo bot de descarga.}}{10}{figure.2.4}\protected@file@percent } +\newlabel{fig:flujo-bot}{{2.4}{10}{Secuencia del flujo bot de descarga}{figure.2.4}{}} +\@writefile{toc}{\contentsline {section}{\numberline {2.6}Otros flujos de negocio}{10}{section.2.6}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{11}{chapter.3}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:estrategia}{{3}{10}{Estrategia de pruebas}{chapter.3}{}} -\@writefile{toc}{\contentsline {section}{\numberline {3.1}Pir\'amide}{10}{section.3.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Pir\'amide de pruebas IPSExtractor.}}{10}{table.3.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{10}{section.3.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {3.3}Entornos}{10}{section.3.3}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {3.2}{\ignorespaces TC = Testcontainers; LS = LocalStack.}}{11}{table.3.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {3.4}Mocks vs sistemas reales}{11}{section.3.4}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {3.5}Principios}{11}{section.3.5}\protected@file@percent } +\newlabel{ch:estrategia}{{3}{11}{Estrategia de pruebas}{chapter.3}{}} +\@writefile{toc}{\contentsline {section}{\numberline {3.1}Pir\'amide}{11}{section.3.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Pir\'amide de pruebas IPSExtractor.}}{11}{table.3.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{11}{section.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.3}C\'omo leer un caso del cat\'alogo}{11}{section.3.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.4}Entornos}{11}{section.3.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.5}Mocks vs sistemas reales}{12}{section.3.5}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {3.2}{\ignorespaces TC = Testcontainers; LS = LocalStack.}}{12}{table.3.2}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {3.6}Principios}{12}{section.3.6}\protected@file@percent } \gdef \LT@i {\LT@entry - {2}{74.28644pt}\LT@entry - {1}{131.50148pt}\LT@entry - {1}{162.49014pt}} -\gdef \LT@ii {\LT@entry - {1}{79.97733pt}\LT@entry + {1}{68.59596pt}\LT@entry + {1}{111.58464pt}\LT@entry {1}{125.81102pt}\LT@entry - {1}{162.49014pt}} -\@writefile{toc}{\contentsline {chapter}{\numberline {4}Pruebas unitarias}{12}{chapter.4}\protected@file@percent } + {1}{91.35826pt}} +\gdef \LT@ii {\LT@entry + {1}{74.28644pt}\LT@entry + {1}{108.73918pt}\LT@entry + {1}{120.12054pt}\LT@entry + {1}{91.35826pt}} +\@writefile{toc}{\contentsline {chapter}{\numberline {4}Pruebas unitarias}{13}{chapter.4}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:unitarias}{{4}{12}{Pruebas unitarias}{chapter.4}{}} -\@writefile{toc}{\contentsline {section}{\numberline {4.1}Gateway}{12}{section.4.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {4.1}{Unitarias gateway.}}{12}{table.4.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{12}{section.4.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {4.2}{Unitarias bot-descarga API.}}{12}{table.4.2}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.3}Agent bot (PC)}{12}{section.4.3}\protected@file@percent } +\newlabel{ch:unitarias}{{4}{13}{Pruebas unitarias}{chapter.4}{}} +\@writefile{toc}{\contentsline {section}{\numberline {4.1}Gateway}{13}{section.4.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {4.1}{Unitarias gateway.}}{13}{table.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{13}{section.4.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {4.2}{Unitarias bot-descarga API.}}{13}{table.4.2}\protected@file@percent } \gdef \LT@iii {\LT@entry {1}{79.97733pt}\LT@entry {1}{125.81102pt}\LT@entry @@ -70,11 +78,12 @@ {1}{79.97733pt}\LT@entry {1}{131.50148pt}\LT@entry {1}{156.79968pt}} -\@writefile{lot}{\contentsline {table}{\numberline {4.3}{Unitarias agent.}}{13}{table.4.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{13}{section.4.4}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {4.4}{Unitarias resto de servicios.}}{13}{table.4.4}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{13}{section.4.5}\protected@file@percent } -\@writefile{lol}{\contentsline {lstlisting}{\numberline {4.1}Suite unitaria r\'apida}{13}{lstlisting.4.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.3}Agent bot (PC)}{14}{section.4.3}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {4.3}{Unitarias agent.}}{14}{table.4.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{14}{section.4.4}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {4.4}{Unitarias resto de servicios.}}{14}{table.4.4}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{14}{section.4.5}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {4.1}Suite unitaria r\'apida}{14}{lstlisting.4.1}\protected@file@percent } \gdef \LT@v {\LT@entry {1}{74.28644pt}\LT@entry {1}{131.50148pt}\LT@entry @@ -83,62 +92,57 @@ {1}{79.97733pt}\LT@entry {1}{140.0374pt}\LT@entry {1}{148.26378pt}} -\@writefile{toc}{\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{14}{chapter.5}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{15}{chapter.5}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:integracion}{{5}{14}{Pruebas de integraci\'on}{chapter.5}{}} -\@writefile{toc}{\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{14}{section.5.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {5.2}Gateway}{14}{section.5.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {5.1}{Integraci\'on gateway.}}{14}{table.5.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {5.3}Bot-descarga}{14}{section.5.3}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {5.2}{Integraci\'on bot-descarga.}}{14}{table.5.2}\protected@file@percent } +\newlabel{ch:integracion}{{5}{15}{Pruebas de integraci\'on}{chapter.5}{}} +\@writefile{toc}{\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{15}{section.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.2}Gateway}{15}{section.5.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {5.1}{Integraci\'on gateway.}}{15}{table.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.3}Bot-descarga}{15}{section.5.3}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {5.2}{Integraci\'on bot-descarga.}}{15}{table.5.2}\protected@file@percent } \gdef \LT@vii {\LT@entry {1}{79.97733pt}\LT@entry {1}{140.0374pt}\LT@entry {1}{148.26378pt}} -\@writefile{toc}{\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{15}{section.5.4}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {5.3}{Integraci\'on extraction, cleaning, count y BD.}}{15}{table.5.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{15}{section.5.5}\protected@file@percent } -\@writefile{lol}{\contentsline {lstlisting}{\numberline {5.1}Smoke health}{15}{lstlisting.5.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{16}{section.5.4}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {5.3}{Integraci\'on extraction, cleaning, count y BD.}}{16}{table.5.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{16}{section.5.5}\protected@file@percent } +\@writefile{lol}{\contentsline {lstlisting}{\numberline {5.1}Smoke health}{16}{lstlisting.5.1}\protected@file@percent } \gdef \LT@viii {\LT@entry {1}{57.21504pt}\LT@entry {1}{91.6678pt}\LT@entry {1}{154.26378pt}\LT@entry {1}{91.35826pt}} -\@writefile{toc}{\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{16}{chapter.6}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{17}{chapter.6}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:e2e}{{6}{16}{Pruebas end-to-end}{chapter.6}{}} -\@writefile{toc}{\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{16}{section.6.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {6.1}{Casos E2E cr\'iticos.}}{16}{table.6.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{16}{section.6.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{16}{subsection.6.2.1}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.2}Pasos}{17}{subsection.6.2.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.3}Asserts}{17}{subsection.6.2.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {6.3}Desktop}{17}{section.6.3}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{18}{chapter.7}\protected@file@percent } +\newlabel{ch:e2e}{{6}{17}{Pruebas end-to-end}{chapter.6}{}} +\@writefile{toc}{\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{17}{section.6.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {6.1}{Casos E2E cr\'iticos.}}{17}{table.6.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{17}{section.6.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{17}{subsection.6.2.1}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.2}Pasos}{18}{subsection.6.2.2}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.3}Asserts}{18}{subsection.6.2.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {6.3}Desktop}{18}{section.6.3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{19}{chapter.7}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:estres}{{7}{18}{Pruebas de estr\'es y carga}{chapter.7}{}} -\@writefile{toc}{\contentsline {section}{\numberline {7.1}Herramienta}{18}{section.7.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {7.2}Perfiles de carga}{18}{section.7.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces Cargas por perfil de entorno.}}{18}{table.7.1}\protected@file@percent } -\newlabel{tab:perfiles-carga}{{7.1}{18}{Cargas por perfil de entorno}{table.7.1}{}} -\@writefile{toc}{\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{18}{section.7.3}\protected@file@percent } -\gdef \LT@ix {\LT@entry - {1}{74.28644pt}\LT@entry - {1}{103.04872pt}\LT@entry - {1}{111.58464pt}\LT@entry - {1}{97.04872pt}} -\@writefile{lot}{\contentsline {table}{\numberline {7.2}{Escenarios de estr\'es.}}{19}{table.7.2}\protected@file@percent } +\newlabel{ch:estres}{{7}{19}{Pruebas de estr\'es y carga}{chapter.7}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.1}Herramienta}{19}{section.7.1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.2}Perfiles de carga}{19}{section.7.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces Cargas por perfil de entorno.}}{19}{table.7.1}\protected@file@percent } +\newlabel{tab:perfiles-carga}{{7.1}{19}{Cargas por perfil de entorno}{table.7.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{19}{section.7.3}\protected@file@percent } \@writefile{toc}{\contentsline {section}{\numberline {7.4}Scripts k6 incluidos}{19}{section.7.4}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {7.3}{\ignorespaces Mapeo script $\leftrightarrow $ escenario.}}{19}{table.7.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{19}{section.7.5}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{19}{subsection.7.5.1}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {7.2}{\ignorespaces Escenarios de estr\'es (objetivo y riesgo).}}{20}{table.7.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {7.3}{\ignorespaces Mapeo script $\leftrightarrow $ escenario.}}{20}{table.7.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{20}{section.7.5}\protected@file@percent } +\@writefile{toc}{\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{20}{subsection.7.5.1}\protected@file@percent } \@writefile{toc}{\contentsline {subsection}{\numberline {7.5.2}Perfil laboratorio}{20}{subsection.7.5.2}\protected@file@percent } \@writefile{toc}{\contentsline {section}{\numberline {7.6}Ejecuci\'on ejemplo}{20}{section.7.6}\protected@file@percent } \@writefile{lol}{\contentsline {lstlisting}{\numberline {7.1}Estr\'es gateway en host compartido}{20}{lstlisting.7.1}\protected@file@percent } -\gdef \LT@x {\LT@entry +\gdef \LT@ix {\LT@entry {1}{57.21504pt}\LT@entry {1}{111.58464pt}\LT@entry {1}{125.81102pt}\LT@entry @@ -167,7 +171,7 @@ \@writefile{toc}{\contentsline {subsection}{\numberline {9.1.3}Carga concurrente del host (no IPS)}{24}{subsection.9.1.3}\protected@file@percent } \@writefile{toc}{\contentsline {subsection}{\numberline {9.1.4}Requisitos previos a una campa\~na de estr\'es}{24}{subsection.9.1.4}\protected@file@percent } \@writefile{toc}{\contentsline {section}{\numberline {9.2}Fixtures recomendadas}{24}{section.9.2}\protected@file@percent } -\gdef \LT@xi {\LT@entry +\gdef \LT@x {\LT@entry {1}{125.50148pt}\LT@entry {1}{233.62204pt}} \@writefile{toc}{\contentsline {section}{\numberline {9.3}Variables de entorno relevantes}{25}{section.9.3}\protected@file@percent } @@ -183,7 +187,7 @@ \@writefile{toc}{\contentsline {section}{\numberline {10.3}Compilar este documento}{26}{section.10.3}\protected@file@percent } \@writefile{lol}{\contentsline {lstlisting}{\numberline {10.1}latexmk}{26}{lstlisting.10.1}\protected@file@percent } \@writefile{toc}{\contentsline {section}{\numberline {10.4}Prioridad de implementaci\'on de tests (ROI)}{27}{section.10.4}\protected@file@percent } -\gdef \LT@xii {\LT@entry +\gdef \LT@xi {\LT@entry {1}{48.67912pt}\LT@entry {1}{140.0374pt}\LT@entry {1}{182.407pt}} @@ -198,29 +202,23 @@ \@writefile{toc}{\contentsline {section}{\numberline {11.4}Plantilla de informe de ejecuci\'on}{29}{section.11.4}\protected@file@percent } \@writefile{lol}{\contentsline {lstlisting}{\numberline {11.1}Campos m\'inimos del informe}{29}{lstlisting.11.1}\protected@file@percent } \@writefile{toc}{\contentsline {section}{\numberline {11.5}Responsables (roles)}{29}{section.11.5}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {12}Informe de ejecuci\'on --- campa\~na 2026-07-13}{30}{chapter.12}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {12}Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}{30}{chapter.12}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\newlabel{ch:informe-2026-07-13}{{12}{30}{Informe de ejecuci\'on --- campa\~na 2026-07-13}{chapter.12}{}} +\newlabel{ch:informe-2026-07-13}{{12}{30}{Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}{chapter.12}{}} \@writefile{toc}{\contentsline {section}{\numberline {12.1}Metadatos}{30}{section.12.1}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Metadatos de la campa\~na.}}{30}{table.12.1}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.2}Baseline del entorno}{30}{section.12.2}\protected@file@percent } -\@writefile{lot}{\contentsline {table}{\numberline {12.2}{\ignorespaces Resumen de la campa\~na 2026-07-13.}}{31}{table.12.2}\protected@file@percent } -\newlabel{tab:campana-2026-07-13}{{12.2}{31}{Resumen de la campa\~na 2026-07-13}{table.12.2}{}} -\@writefile{toc}{\contentsline {section}{\numberline {12.3}Resultados por caso}{31}{section.12.3}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.1}ST-GW-01 --- gateway health}{31}{subsection.12.3.1}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.2}ST-BOT-01 --- heartbeats (corrida 1)}{31}{subsection.12.3.2}\protected@file@percent } -\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.3}ST-BOT-01 --- heartbeats (corrida 2, reintento)}{31}{subsection.12.3.3}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.4}Hallazgos}{31}{section.12.4}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.5}No ejecutado en esta campa\~na}{32}{section.12.5}\protected@file@percent } -\@writefile{toc}{\contentsline {section}{\numberline {12.6}Follow-ups}{32}{section.12.6}\protected@file@percent } -\@writefile{toc}{\contentsline {chapter}{\numberline {A}Glosario}{33}{appendix.Alph1}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.2}Resultados resumidos}{30}{section.12.2}\protected@file@percent } +\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Resumen; detalle en el Informe de Pruebas.}}{30}{table.12.1}\protected@file@percent } +\newlabel{tab:campana-2026-07-13}{{12.1}{30}{Resumen; detalle en el Informe de Pruebas}{table.12.1}{}} +\@writefile{toc}{\contentsline {section}{\numberline {12.3}Lecci\'on operativa}{30}{section.12.3}\protected@file@percent } +\@writefile{toc}{\contentsline {section}{\numberline {12.4}Pendiente respecto a este plan}{31}{section.12.4}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {A}Glosario}{32}{appendix.Alph1}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\@writefile{toc}{\contentsline {chapter}{\numberline {B}Checklist de release}{34}{appendix.Alph2}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {B}Checklist de release}{33}{appendix.Alph2}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\@writefile{toc}{\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{35}{appendix.Alph3}\protected@file@percent } +\@writefile{toc}{\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{34}{appendix.Alph3}\protected@file@percent } \@writefile{lof}{\addvspace {10\p@ }} \@writefile{lot}{\addvspace {10\p@ }} -\gdef \@abspage@last{36} +\gdef \@abspage@last{35} diff --git a/docs/plan_pruebas/main.out b/docs/plan_pruebas/main.out index 3ab47a0..a79dc82 100644 --- a/docs/plan_pruebas/main.out +++ b/docs/plan_pruebas/main.out @@ -6,79 +6,77 @@ \BOOKMARK [1][-]{section.1.3}{\376\377\000E\000s\000t\000a\000d\000o\000\040\000a\000c\000t\000u\000a\000l\000\040\000d\000e\000l\000\040\000r\000e\000p\000o\000s\000i\000t\000o\000r\000i\000o}{chapter.1}% 6 \BOOKMARK [1][-]{section.1.4}{\376\377\000R\000e\000f\000e\000r\000e\000n\000c\000i\000a\000s\000\040\000d\000e\000l\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.1}% 7 \BOOKMARK [0][-]{chapter.2}{\376\377\000A\000r\000q\000u\000i\000t\000e\000c\000t\000u\000r\000a\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 8 -\BOOKMARK [1][-]{section.2.1}{\376\377\000V\000i\000s\000t\000a\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 9 -\BOOKMARK [1][-]{section.2.2}{\376\377\000I\000n\000v\000e\000n\000t\000a\000r\000i\000o\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 10 -\BOOKMARK [1][-]{section.2.3}{\376\377\000F\000l\000u\000j\000o\000\040\000c\000r\000\355\000t\000i\000c\000o\000:\000\040\000b\000o\000t\000\040\000d\000e\000\040\000d\000e\000s\000c\000a\000r\000g\000a}{chapter.2}% 11 -\BOOKMARK [1][-]{section.2.4}{\376\377\000O\000t\000r\000o\000s\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 12 -\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000s\000t\000r\000a\000t\000e\000g\000i\000a\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000s}{}% 13 -\BOOKMARK [1][-]{section.3.1}{\376\377\000P\000i\000r\000\341\000m\000i\000d\000e}{chapter.3}% 14 -\BOOKMARK [1][-]{section.3.2}{\376\377\000C\000o\000n\000v\000e\000n\000c\000i\000\363\000n\000\040\000d\000e\000\040\000i\000d\000e\000n\000t\000i\000f\000i\000c\000a\000d\000o\000r\000e\000s}{chapter.3}% 15 -\BOOKMARK [1][-]{section.3.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000s}{chapter.3}% 16 -\BOOKMARK [1][-]{section.3.4}{\376\377\000M\000o\000c\000k\000s\000\040\000v\000s\000\040\000s\000i\000s\000t\000e\000m\000a\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.3}% 17 -\BOOKMARK [1][-]{section.3.5}{\376\377\000P\000r\000i\000n\000c\000i\000p\000i\000o\000s}{chapter.3}% 18 -\BOOKMARK [0][-]{chapter.4}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 19 -\BOOKMARK [1][-]{section.4.1}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.4}% 20 -\BOOKMARK [1][-]{section.4.2}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000A\000P\000I\000\040\000\050\000J\000o\000b\000S\000t\000o\000r\000e\000\051}{chapter.4}% 21 -\BOOKMARK [1][-]{section.4.3}{\376\377\000A\000g\000e\000n\000t\000\040\000b\000o\000t\000\040\000\050\000P\000C\000\051}{chapter.4}% 22 -\BOOKMARK [1][-]{section.4.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t\000\040\000/\000\040\000D\000e\000s\000k\000t\000o\000p}{chapter.4}% 23 -\BOOKMARK [1][-]{section.4.5}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.4}% 24 -\BOOKMARK [0][-]{chapter.5}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{}% 25 -\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{chapter.5}% 26 -\BOOKMARK [1][-]{section.5.2}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.5}% 27 -\BOOKMARK [1][-]{section.5.3}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a}{chapter.5}% 28 -\BOOKMARK [1][-]{section.5.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t}{chapter.5}% 29 -\BOOKMARK [1][-]{section.5.5}{\376\377\000S\000m\000o\000k\000e\000\040\000m\000\355\000n\000i\000m\000o\000\040\000p\000o\000s\000t\000-\000c\000o\000m\000p\000o\000s\000e}{chapter.5}% 30 -\BOOKMARK [0][-]{chapter.6}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000e\000n\000d\000-\000t\000o\000-\000e\000n\000d}{}% 31 -\BOOKMARK [1][-]{section.6.1}{\376\377\000M\000a\000t\000r\000i\000z\000\040\000d\000e\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.6}% 32 -\BOOKMARK [1][-]{section.6.2}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000E\0002\000E\000-\0000\0005\000\040\000\050\000b\000o\000t\000\040\000d\000e\000s\000c\000a\000r\000g\000a\000\051}{chapter.6}% 33 -\BOOKMARK [2][-]{subsection.6.2.1}{\376\377\000P\000r\000e\000c\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s}{section.6.2}% 34 -\BOOKMARK [2][-]{subsection.6.2.2}{\376\377\000P\000a\000s\000o\000s}{section.6.2}% 35 -\BOOKMARK [2][-]{subsection.6.2.3}{\376\377\000A\000s\000s\000e\000r\000t\000s}{section.6.2}% 36 -\BOOKMARK [1][-]{section.6.3}{\376\377\000D\000e\000s\000k\000t\000o\000p}{chapter.6}% 37 -\BOOKMARK [0][-]{chapter.7}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s\000\040\000y\000\040\000c\000a\000r\000g\000a}{}% 38 -\BOOKMARK [1][-]{section.7.1}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a}{chapter.7}% 39 -\BOOKMARK [1][-]{section.7.2}{\376\377\000P\000e\000r\000f\000i\000l\000e\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{chapter.7}% 40 -\BOOKMARK [1][-]{section.7.3}{\376\377\000E\000s\000c\000e\000n\000a\000r\000i\000o\000s\000\040\000\050\000c\000a\000t\000\341\000l\000o\000g\000o\000\040\000c\000o\000m\000p\000l\000e\000t\000o\000\051}{chapter.7}% 41 -\BOOKMARK [1][-]{section.7.4}{\376\377\000S\000c\000r\000i\000p\000t\000s\000\040\000k\0006\000\040\000i\000n\000c\000l\000u\000i\000d\000o\000s}{chapter.7}% 42 -\BOOKMARK [1][-]{section.7.5}{\376\377\000S\000L\000O\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{chapter.7}% 43 -\BOOKMARK [2][-]{subsection.7.5.1}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000h\000o\000s\000t\000-\000c\000o\000m\000p\000a\000r\000t\000i\000d\000o\000-\0008\000g\000b\000\040\000\050\000d\000e\000f\000a\000u\000l\000t\000\051}{section.7.5}% 44 -\BOOKMARK [2][-]{subsection.7.5.2}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000l\000a\000b\000o\000r\000a\000t\000o\000r\000i\000o}{section.7.5}% 45 -\BOOKMARK [1][-]{section.7.6}{\376\377\000E\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\000e\000j\000e\000m\000p\000l\000o}{chapter.7}% 46 -\BOOKMARK [0][-]{chapter.8}{\376\377\000R\000e\000s\000i\000l\000i\000e\000n\000c\000i\000a\000\040\000y\000\040\000c\000h\000a\000o\000s}{}% 47 -\BOOKMARK [1][-]{section.8.1}{\376\377\000O\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 48 -\BOOKMARK [1][-]{section.8.2}{\376\377\000C\000a\000s\000o\000s}{chapter.8}% 49 -\BOOKMARK [1][-]{section.8.3}{\376\377\000M\000o\000d\000o\000s\000\040\000d\000e\000\040\000f\000a\000l\000l\000o\000\040\000c\000o\000n\000o\000c\000i\000d\000o\000s\000\040\000\050\000b\000o\000t\000\051}{chapter.8}% 50 -\BOOKMARK [1][-]{section.8.4}{\376\377\000C\000r\000i\000t\000e\000r\000i\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.8}% 51 -\BOOKMARK [0][-]{chapter.9}{\376\377\000D\000a\000t\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000\040\000y\000\040\000e\000n\000t\000o\000r\000n\000o\000s}{}% 52 -\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000o\000s\000t\000\040\000d\000o\000n\000d\000e\000\040\000s\000e\000\040\000e\000j\000e\000c\000u\000t\000a\000n\000\040\000l\000a\000s\000\040\000p\000r\000u\000e\000b\000a\000s}{chapter.9}% 53 -\BOOKMARK [2][-]{subsection.9.1.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{section.9.1}% 54 -\BOOKMARK [2][-]{subsection.9.1.2}{\376\377\000S\000t\000a\000c\000k\000\040\000I\000P\000S\000E\000x\000t\000r\000a\000c\000t\000o\000r\000\040\000o\000b\000s\000e\000r\000v\000a\000d\000o\000\040\000\050\000i\000d\000l\000e\000\051}{section.9.1}% 55 -\BOOKMARK [2][-]{subsection.9.1.3}{\376\377\000C\000a\000r\000g\000a\000\040\000c\000o\000n\000c\000u\000r\000r\000e\000n\000t\000e\000\040\000d\000e\000l\000\040\000h\000o\000s\000t\000\040\000\050\000n\000o\000\040\000I\000P\000S\000\051}{section.9.1}% 56 -\BOOKMARK [2][-]{subsection.9.1.4}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000p\000r\000e\000v\000i\000o\000s\000\040\000a\000\040\000u\000n\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s}{section.9.1}% 57 -\BOOKMARK [1][-]{section.9.2}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000d\000a\000s}{chapter.9}% 58 -\BOOKMARK [1][-]{section.9.3}{\376\377\000V\000a\000r\000i\000a\000b\000l\000e\000s\000\040\000d\000e\000\040\000e\000n\000t\000o\000r\000n\000o\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{chapter.9}% 59 -\BOOKMARK [1][-]{section.9.4}{\376\377\000S\000e\000g\000u\000r\000i\000d\000a\000d\000\040\000e\000n\000\040\000e\000n\000t\000o\000r\000n\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.9}% 60 -\BOOKMARK [0][-]{chapter.10}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s\000\040\000p\000r\000o\000f\000e\000s\000i\000o\000n\000a\000l\000e\000s\000\040\000y\000\040\000C\000I}{}% 61 -\BOOKMARK [1][-]{section.10.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000t\000o\000o\000l\000i\000n\000g}{chapter.10}% 62 -\BOOKMARK [1][-]{section.10.2}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000C\000I\000\040\000o\000b\000j\000e\000t\000i\000v\000o\000\040\000\050\000n\000o\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000e\000n\000t\000r\000e\000g\000a\000\051}{chapter.10}% 63 -\BOOKMARK [1][-]{section.10.3}{\376\377\000C\000o\000m\000p\000i\000l\000a\000r\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.10}% 64 -\BOOKMARK [1][-]{section.10.4}{\376\377\000P\000r\000i\000o\000r\000i\000d\000a\000d\000\040\000d\000e\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000c\000i\000\363\000n\000\040\000d\000e\000\040\000t\000e\000s\000t\000s\000\040\000\050\000R\000O\000I\000\051}{chapter.10}% 65 -\BOOKMARK [0][-]{chapter.11}{\376\377\000C\000r\000o\000n\000o\000g\000r\000a\000m\000a\000\040\000y\000\040\000c\000r\000i\000t\000e\000r\000i\000o\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{}% 66 -\BOOKMARK [1][-]{section.11.1}{\376\377\000F\000a\000s\000e\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000a\000s}{chapter.11}% 67 -\BOOKMARK [1][-]{section.11.2}{\376\377\000D\000e\000f\000i\000n\000i\000t\000i\000o\000n\000\040\000o\000f\000\040\000D\000o\000n\000e\000\040\000p\000o\000r\000\040\000r\000e\000l\000e\000a\000s\000e}{chapter.11}% 68 -\BOOKMARK [1][-]{section.11.3}{\376\377\000C\000a\000m\000p\000a\000\361\000a\000\040\000r\000e\000g\000i\000s\000t\000r\000a\000d\000a}{chapter.11}% 69 -\BOOKMARK [1][-]{section.11.4}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.11}% 70 -\BOOKMARK [1][-]{section.11.5}{\376\377\000R\000e\000s\000p\000o\000n\000s\000a\000b\000l\000e\000s\000\040\000\050\000r\000o\000l\000e\000s\000\051}{chapter.11}% 71 -\BOOKMARK [0][-]{chapter.12}{\376\377\000I\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\040\024\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003}{}% 72 -\BOOKMARK [1][-]{section.12.1}{\376\377\000M\000e\000t\000a\000d\000a\000t\000o\000s}{chapter.12}% 73 -\BOOKMARK [1][-]{section.12.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000l\000\040\000e\000n\000t\000o\000r\000n\000o}{chapter.12}% 74 -\BOOKMARK [1][-]{section.12.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000p\000o\000r\000\040\000c\000a\000s\000o}{chapter.12}% 75 -\BOOKMARK [2][-]{subsection.12.3.1}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001\000\040\040\024\000\040\000g\000a\000t\000e\000w\000a\000y\000\040\000h\000e\000a\000l\000t\000h}{section.12.3}% 76 -\BOOKMARK [2][-]{subsection.12.3.2}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000h\000e\000a\000r\000t\000b\000e\000a\000t\000s\000\040\000\050\000c\000o\000r\000r\000i\000d\000a\000\040\0001\000\051}{section.12.3}% 77 -\BOOKMARK [2][-]{subsection.12.3.3}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000h\000e\000a\000r\000t\000b\000e\000a\000t\000s\000\040\000\050\000c\000o\000r\000r\000i\000d\000a\000\040\0002\000,\000\040\000r\000e\000i\000n\000t\000e\000n\000t\000o\000\051}{section.12.3}% 78 -\BOOKMARK [1][-]{section.12.4}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.12}% 79 -\BOOKMARK [1][-]{section.12.5}{\376\377\000N\000o\000\040\000e\000j\000e\000c\000u\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.12}% 80 -\BOOKMARK [1][-]{section.12.6}{\376\377\000F\000o\000l\000l\000o\000w\000-\000u\000p\000s}{chapter.12}% 81 -\BOOKMARK [0][-]{appendix.Alph1}{\376\377\000G\000l\000o\000s\000a\000r\000i\000o}{}% 82 -\BOOKMARK [0][-]{appendix.Alph2}{\376\377\000C\000h\000e\000c\000k\000l\000i\000s\000t\000\040\000d\000e\000\040\000r\000e\000l\000e\000a\000s\000e}{}% 83 -\BOOKMARK [0][-]{appendix.Alph3}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000r\000u\000t\000a\000s\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{}% 84 +\BOOKMARK [1][-]{section.2.1}{\376\377\000C\000\363\000m\000o\000\040\000e\000s\000t\000\341\000\040\000d\000i\000s\000e\000\361\000a\000d\000o\000\040\000e\000l\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.2}% 9 +\BOOKMARK [1][-]{section.2.2}{\376\377\000P\000a\000t\000r\000o\000n\000e\000s\000\040\000d\000e\000\040\000d\000i\000s\000e\000\361\000o}{chapter.2}% 10 +\BOOKMARK [1][-]{section.2.3}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 11 +\BOOKMARK [1][-]{section.2.4}{\376\377\000I\000n\000v\000e\000n\000t\000a\000r\000i\000o\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 12 +\BOOKMARK [1][-]{section.2.5}{\376\377\000F\000l\000u\000j\000o\000\040\000c\000r\000\355\000t\000i\000c\000o\000:\000\040\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000\050\000o\000r\000q\000u\000e\000s\000t\000a\000d\000o\000r\000\040\000+\000\040\000a\000g\000e\000n\000t\000\040\000S\000e\000l\000e\000n\000i\000u\000m\000\051}{chapter.2}% 13 +\BOOKMARK [1][-]{section.2.6}{\376\377\000O\000t\000r\000o\000s\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 14 +\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000s\000t\000r\000a\000t\000e\000g\000i\000a\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000s}{}% 15 +\BOOKMARK [1][-]{section.3.1}{\376\377\000P\000i\000r\000\341\000m\000i\000d\000e}{chapter.3}% 16 +\BOOKMARK [1][-]{section.3.2}{\376\377\000C\000o\000n\000v\000e\000n\000c\000i\000\363\000n\000\040\000d\000e\000\040\000i\000d\000e\000n\000t\000i\000f\000i\000c\000a\000d\000o\000r\000e\000s}{chapter.3}% 17 +\BOOKMARK [1][-]{section.3.3}{\376\377\000C\000\363\000m\000o\000\040\000l\000e\000e\000r\000\040\000u\000n\000\040\000c\000a\000s\000o\000\040\000d\000e\000l\000\040\000c\000a\000t\000\341\000l\000o\000g\000o}{chapter.3}% 18 +\BOOKMARK [1][-]{section.3.4}{\376\377\000E\000n\000t\000o\000r\000n\000o\000s}{chapter.3}% 19 +\BOOKMARK [1][-]{section.3.5}{\376\377\000M\000o\000c\000k\000s\000\040\000v\000s\000\040\000s\000i\000s\000t\000e\000m\000a\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.3}% 20 +\BOOKMARK [1][-]{section.3.6}{\376\377\000P\000r\000i\000n\000c\000i\000p\000i\000o\000s}{chapter.3}% 21 +\BOOKMARK [0][-]{chapter.4}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 22 +\BOOKMARK [1][-]{section.4.1}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.4}% 23 +\BOOKMARK [1][-]{section.4.2}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000A\000P\000I\000\040\000\050\000J\000o\000b\000S\000t\000o\000r\000e\000\051}{chapter.4}% 24 +\BOOKMARK [1][-]{section.4.3}{\376\377\000A\000g\000e\000n\000t\000\040\000b\000o\000t\000\040\000\050\000P\000C\000\051}{chapter.4}% 25 +\BOOKMARK [1][-]{section.4.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t\000\040\000/\000\040\000D\000e\000s\000k\000t\000o\000p}{chapter.4}% 26 +\BOOKMARK [1][-]{section.4.5}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.4}% 27 +\BOOKMARK [0][-]{chapter.5}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{}% 28 +\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{chapter.5}% 29 +\BOOKMARK [1][-]{section.5.2}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.5}% 30 +\BOOKMARK [1][-]{section.5.3}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a}{chapter.5}% 31 +\BOOKMARK [1][-]{section.5.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t}{chapter.5}% 32 +\BOOKMARK [1][-]{section.5.5}{\376\377\000S\000m\000o\000k\000e\000\040\000m\000\355\000n\000i\000m\000o\000\040\000p\000o\000s\000t\000-\000c\000o\000m\000p\000o\000s\000e}{chapter.5}% 33 +\BOOKMARK [0][-]{chapter.6}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000e\000n\000d\000-\000t\000o\000-\000e\000n\000d}{}% 34 +\BOOKMARK [1][-]{section.6.1}{\376\377\000M\000a\000t\000r\000i\000z\000\040\000d\000e\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.6}% 35 +\BOOKMARK [1][-]{section.6.2}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000E\0002\000E\000-\0000\0005\000\040\000\050\000b\000o\000t\000\040\000d\000e\000s\000c\000a\000r\000g\000a\000\051}{chapter.6}% 36 +\BOOKMARK [2][-]{subsection.6.2.1}{\376\377\000P\000r\000e\000c\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s}{section.6.2}% 37 +\BOOKMARK [2][-]{subsection.6.2.2}{\376\377\000P\000a\000s\000o\000s}{section.6.2}% 38 +\BOOKMARK [2][-]{subsection.6.2.3}{\376\377\000A\000s\000s\000e\000r\000t\000s}{section.6.2}% 39 +\BOOKMARK [1][-]{section.6.3}{\376\377\000D\000e\000s\000k\000t\000o\000p}{chapter.6}% 40 +\BOOKMARK [0][-]{chapter.7}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s\000\040\000y\000\040\000c\000a\000r\000g\000a}{}% 41 +\BOOKMARK [1][-]{section.7.1}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a}{chapter.7}% 42 +\BOOKMARK [1][-]{section.7.2}{\376\377\000P\000e\000r\000f\000i\000l\000e\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{chapter.7}% 43 +\BOOKMARK [1][-]{section.7.3}{\376\377\000E\000s\000c\000e\000n\000a\000r\000i\000o\000s\000\040\000\050\000c\000a\000t\000\341\000l\000o\000g\000o\000\040\000c\000o\000m\000p\000l\000e\000t\000o\000\051}{chapter.7}% 44 +\BOOKMARK [1][-]{section.7.4}{\376\377\000S\000c\000r\000i\000p\000t\000s\000\040\000k\0006\000\040\000i\000n\000c\000l\000u\000i\000d\000o\000s}{chapter.7}% 45 +\BOOKMARK [1][-]{section.7.5}{\376\377\000S\000L\000O\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{chapter.7}% 46 +\BOOKMARK [2][-]{subsection.7.5.1}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000h\000o\000s\000t\000-\000c\000o\000m\000p\000a\000r\000t\000i\000d\000o\000-\0008\000g\000b\000\040\000\050\000d\000e\000f\000a\000u\000l\000t\000\051}{section.7.5}% 47 +\BOOKMARK [2][-]{subsection.7.5.2}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000l\000a\000b\000o\000r\000a\000t\000o\000r\000i\000o}{section.7.5}% 48 +\BOOKMARK [1][-]{section.7.6}{\376\377\000E\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\000e\000j\000e\000m\000p\000l\000o}{chapter.7}% 49 +\BOOKMARK [0][-]{chapter.8}{\376\377\000R\000e\000s\000i\000l\000i\000e\000n\000c\000i\000a\000\040\000y\000\040\000c\000h\000a\000o\000s}{}% 50 +\BOOKMARK [1][-]{section.8.1}{\376\377\000O\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 51 +\BOOKMARK [1][-]{section.8.2}{\376\377\000C\000a\000s\000o\000s}{chapter.8}% 52 +\BOOKMARK [1][-]{section.8.3}{\376\377\000M\000o\000d\000o\000s\000\040\000d\000e\000\040\000f\000a\000l\000l\000o\000\040\000c\000o\000n\000o\000c\000i\000d\000o\000s\000\040\000\050\000b\000o\000t\000\051}{chapter.8}% 53 +\BOOKMARK [1][-]{section.8.4}{\376\377\000C\000r\000i\000t\000e\000r\000i\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.8}% 54 +\BOOKMARK [0][-]{chapter.9}{\376\377\000D\000a\000t\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000\040\000y\000\040\000e\000n\000t\000o\000r\000n\000o\000s}{}% 55 +\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000o\000s\000t\000\040\000d\000o\000n\000d\000e\000\040\000s\000e\000\040\000e\000j\000e\000c\000u\000t\000a\000n\000\040\000l\000a\000s\000\040\000p\000r\000u\000e\000b\000a\000s}{chapter.9}% 56 +\BOOKMARK [2][-]{subsection.9.1.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{section.9.1}% 57 +\BOOKMARK [2][-]{subsection.9.1.2}{\376\377\000S\000t\000a\000c\000k\000\040\000I\000P\000S\000E\000x\000t\000r\000a\000c\000t\000o\000r\000\040\000o\000b\000s\000e\000r\000v\000a\000d\000o\000\040\000\050\000i\000d\000l\000e\000\051}{section.9.1}% 58 +\BOOKMARK [2][-]{subsection.9.1.3}{\376\377\000C\000a\000r\000g\000a\000\040\000c\000o\000n\000c\000u\000r\000r\000e\000n\000t\000e\000\040\000d\000e\000l\000\040\000h\000o\000s\000t\000\040\000\050\000n\000o\000\040\000I\000P\000S\000\051}{section.9.1}% 59 +\BOOKMARK [2][-]{subsection.9.1.4}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000p\000r\000e\000v\000i\000o\000s\000\040\000a\000\040\000u\000n\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s}{section.9.1}% 60 +\BOOKMARK [1][-]{section.9.2}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000d\000a\000s}{chapter.9}% 61 +\BOOKMARK [1][-]{section.9.3}{\376\377\000V\000a\000r\000i\000a\000b\000l\000e\000s\000\040\000d\000e\000\040\000e\000n\000t\000o\000r\000n\000o\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{chapter.9}% 62 +\BOOKMARK [1][-]{section.9.4}{\376\377\000S\000e\000g\000u\000r\000i\000d\000a\000d\000\040\000e\000n\000\040\000e\000n\000t\000o\000r\000n\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.9}% 63 +\BOOKMARK [0][-]{chapter.10}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s\000\040\000p\000r\000o\000f\000e\000s\000i\000o\000n\000a\000l\000e\000s\000\040\000y\000\040\000C\000I}{}% 64 +\BOOKMARK [1][-]{section.10.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000t\000o\000o\000l\000i\000n\000g}{chapter.10}% 65 +\BOOKMARK [1][-]{section.10.2}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000C\000I\000\040\000o\000b\000j\000e\000t\000i\000v\000o\000\040\000\050\000n\000o\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000e\000n\000t\000r\000e\000g\000a\000\051}{chapter.10}% 66 +\BOOKMARK [1][-]{section.10.3}{\376\377\000C\000o\000m\000p\000i\000l\000a\000r\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.10}% 67 +\BOOKMARK [1][-]{section.10.4}{\376\377\000P\000r\000i\000o\000r\000i\000d\000a\000d\000\040\000d\000e\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000c\000i\000\363\000n\000\040\000d\000e\000\040\000t\000e\000s\000t\000s\000\040\000\050\000R\000O\000I\000\051}{chapter.10}% 68 +\BOOKMARK [0][-]{chapter.11}{\376\377\000C\000r\000o\000n\000o\000g\000r\000a\000m\000a\000\040\000y\000\040\000c\000r\000i\000t\000e\000r\000i\000o\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{}% 69 +\BOOKMARK [1][-]{section.11.1}{\376\377\000F\000a\000s\000e\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000a\000s}{chapter.11}% 70 +\BOOKMARK [1][-]{section.11.2}{\376\377\000D\000e\000f\000i\000n\000i\000t\000i\000o\000n\000\040\000o\000f\000\040\000D\000o\000n\000e\000\040\000p\000o\000r\000\040\000r\000e\000l\000e\000a\000s\000e}{chapter.11}% 71 +\BOOKMARK [1][-]{section.11.3}{\376\377\000C\000a\000m\000p\000a\000\361\000a\000\040\000r\000e\000g\000i\000s\000t\000r\000a\000d\000a}{chapter.11}% 72 +\BOOKMARK [1][-]{section.11.4}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.11}% 73 +\BOOKMARK [1][-]{section.11.5}{\376\377\000R\000e\000s\000p\000o\000n\000s\000a\000b\000l\000e\000s\000\040\000\050\000r\000o\000l\000e\000s\000\051}{chapter.11}% 74 +\BOOKMARK [0][-]{chapter.12}{\376\377\000E\000s\000t\000a\000d\000o\000\040\000d\000e\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\040\024\000\040\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\040\000\050\000+\000\040\000L\000L\000M\000\040\0001\0007\000-\000j\000u\000l\000\051}{}% 75 +\BOOKMARK [1][-]{section.12.1}{\376\377\000M\000e\000t\000a\000d\000a\000t\000o\000s}{chapter.12}% 76 +\BOOKMARK [1][-]{section.12.2}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000r\000e\000s\000u\000m\000i\000d\000o\000s}{chapter.12}% 77 +\BOOKMARK [1][-]{section.12.3}{\376\377\000L\000e\000c\000c\000i\000\363\000n\000\040\000o\000p\000e\000r\000a\000t\000i\000v\000a}{chapter.12}% 78 +\BOOKMARK [1][-]{section.12.4}{\376\377\000P\000e\000n\000d\000i\000e\000n\000t\000e\000\040\000r\000e\000s\000p\000e\000c\000t\000o\000\040\000a\000\040\000e\000s\000t\000e\000\040\000p\000l\000a\000n}{chapter.12}% 79 +\BOOKMARK [0][-]{appendix.Alph1}{\376\377\000G\000l\000o\000s\000a\000r\000i\000o}{}% 80 +\BOOKMARK [0][-]{appendix.Alph2}{\376\377\000C\000h\000e\000c\000k\000l\000i\000s\000t\000\040\000d\000e\000\040\000r\000e\000l\000e\000a\000s\000e}{}% 81 +\BOOKMARK [0][-]{appendix.Alph3}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000r\000u\000t\000a\000s\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{}% 82 diff --git a/docs/plan_pruebas/main.tex b/docs/plan_pruebas/main.tex index c23d771..d5248a3 100644 --- a/docs/plan_pruebas/main.tex +++ b/docs/plan_pruebas/main.tex @@ -6,25 +6,22 @@ \makecover -\chapter*{Control de cambios} -\begin{tabularx}{\textwidth}{@{}l l X@{}} -\toprule -\textbf{Versi\'on} & \textbf{Fecha} & \textbf{Descripci\'on} \\ -\midrule -1.0.0 & 13 jul 2026 & Versi\'on inicial del plan de pruebas y estr\'es. \\ -1.1.0 & 13 jul 2026 & Entorno real de ejecuci\'on (host Ubuntu compartido 8\,GiB) y perfiles de carga. \\ -1.2.0 & 13 jul 2026 & Informe campa\~na k6: smoke, ST-GW-01 PASS, ST-BOT-01 PASS (reintento). \\ -\bottomrule -\end{tabularx} - \chapter*{Resumen ejecutivo} -Este documento define la estrategia y la matriz de pruebas del monorepo \textbf{IPSExtractor}: API Gateway, extraction/workers ARQ, cleaning, count-api (Go), bot-descarga (API + agent Selenium), PostgreSQL, Redis, MinIO y la aplicaci\'on de escritorio. +Este documento define la \textbf{estrategia y la matriz de pruebas} del monorepo IPSExtractor: +API Gateway, extraction/workers ARQ, cleaning, count-api, bot-descarga (API + agent Selenium), +PostgreSQL, Redis, MinIO y la aplicaci\'on de escritorio. -Hoy el repositorio cuenta principalmente con pruebas unitarias aisladas; no existe pipeline CI, ni suites de integraci\'on/E2E automatizadas. Este plan cierra ese gap con identificadores trazables (\texttt{UT-*}, \texttt{IT-*}, \texttt{E2E-*}, \texttt{ST-*}, \texttt{CH-*}), SLOs medibles y scripts \textbf{k6} de referencia en \texttt{docs/plan\_pruebas/scripts/}. - -\textbf{Importante:} las pruebas de integraci\'on, humo y estr\'es documentadas contra el stack vivo se ejecutan en el host descrito en la secci\'on~\ref{sec:host-pruebas} (laptop Ubuntu 24.04, 8\,GiB RAM, Docker compartido con MLflow, Airflow, n8n, etc.). Los resultados deben interpretarse en ese contexto: un OOM o p95 alto puede deberse a contenci\'on del host, no solo a IPSExtractor. - -\textbf{Estado campa\~na 2026-07-13:} smoke PASS; ST-GW-01 PASS; ST-BOT-01 PASS en reintento (detalle en cap.~\ref{ch:informe-2026-07-13}). +\begin{itemize} + \item \textbf{Objetivo:} cerrar el gap entre unitarias aisladas y un plan trazable + (UT / IT / E2E / ST / CH) con SLOs y scripts k6. + \item \textbf{Arquitectura:} microservicios + API Gateway + cola ARQ + claim del bot + (cap.~\ref{ch:arquitectura}). + \item \textbf{Entorno de referencia:} host Ubuntu 8\,GiB compartido (sec.~\ref{sec:host-pruebas}); + perfiles \texttt{shared8g} / \texttt{lab}. + \item \textbf{Estado campa\~na:} smoke PASS; ST-GW-01 PASS; ST-BOT-01 PASS (reintento); + ST-EXT-02 LLM medido --- detalle en el \emph{Informe de Pruebas}. + \item \textbf{Volumen 100k/mes:} modelo y veredicto en el PDF \emph{Capacidad 100k}. +\end{itemize} \tableofcontents \clearpage @@ -57,13 +54,13 @@ Hoy el repositorio cuenta principalmente con pruebas unitarias aisladas; no exis \chapter{Checklist de release} \begin{enumerate} \item Unitarios verdes: bots, bot-descarga store, Vitest, \texttt{go test}. - \item Migraciones Alembic en BD limpia (0001--0004). + \item Migraciones Alembic en BD limpia. \item Smoke Compose: \texttt{GET /health/live} y \texttt{GET /health}. \item E2E-01 a E2E-05 ejecutados en staging (E2E-05 con agent real si hay Imaging). \item Al menos ST-GW-01, ST-BOT-01, ST-COUNT-01 y ST-EXT-01 con umbrales cumplidos. - \item Informe: p95, error rate, picos RAM, jobs fallidos, costos LLM. - \item \textbf{Entorno citado:} hostname, \texttt{free -h}, \texttt{docker stats}, perfil k6 (\texttt{shared8g}/\texttt{lab}). - \item Sin secretos hardcodeados en compose para entornos de prueba. + \item Informe de resultados + PDF Capacidad 100k actualizados si aplica. + \item Entorno citado: hostname, \texttt{free -h}, \texttt{docker stats}, perfil k6. + \item Sin secretos hardcodeados en compose de prueba. \end{enumerate} \chapter{Mapa de rutas relevantes} diff --git a/docs/plan_pruebas/main.toc b/docs/plan_pruebas/main.toc index ac8b53e..851652a 100644 --- a/docs/plan_pruebas/main.toc +++ b/docs/plan_pruebas/main.toc @@ -1,48 +1,51 @@ \babel@toc {spanish}{}\relax -\contentsline {chapter}{\numberline {1}Introducci\'on}{6}{chapter.1}% -\contentsline {section}{\numberline {1.1}Objetivo}{6}{section.1.1}% -\contentsline {section}{\numberline {1.2}Alcance}{6}{section.1.2}% -\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{6}{subsection.1.2.1}% -\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{6}{subsection.1.2.2}% -\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{6}{section.1.3}% -\contentsline {section}{\numberline {1.4}Referencias del sistema}{6}{section.1.4}% -\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{8}{chapter.2}% -\contentsline {section}{\numberline {2.1}Vista de componentes}{8}{section.2.1}% -\contentsline {section}{\numberline {2.2}Inventario de componentes}{8}{section.2.2}% -\contentsline {section}{\numberline {2.3}Flujo cr\'itico: bot de descarga}{8}{section.2.3}% -\contentsline {section}{\numberline {2.4}Otros flujos de negocio}{9}{section.2.4}% -\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{10}{chapter.3}% -\contentsline {section}{\numberline {3.1}Pir\'amide}{10}{section.3.1}% -\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{10}{section.3.2}% -\contentsline {section}{\numberline {3.3}Entornos}{10}{section.3.3}% -\contentsline {section}{\numberline {3.4}Mocks vs sistemas reales}{11}{section.3.4}% -\contentsline {section}{\numberline {3.5}Principios}{11}{section.3.5}% -\contentsline {chapter}{\numberline {4}Pruebas unitarias}{12}{chapter.4}% -\contentsline {section}{\numberline {4.1}Gateway}{12}{section.4.1}% -\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{12}{section.4.2}% -\contentsline {section}{\numberline {4.3}Agent bot (PC)}{12}{section.4.3}% -\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{13}{section.4.4}% -\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{13}{section.4.5}% -\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{14}{chapter.5}% -\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{14}{section.5.1}% -\contentsline {section}{\numberline {5.2}Gateway}{14}{section.5.2}% -\contentsline {section}{\numberline {5.3}Bot-descarga}{14}{section.5.3}% -\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{15}{section.5.4}% -\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{15}{section.5.5}% -\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{16}{chapter.6}% -\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{16}{section.6.1}% -\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{16}{section.6.2}% -\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{16}{subsection.6.2.1}% -\contentsline {subsection}{\numberline {6.2.2}Pasos}{17}{subsection.6.2.2}% -\contentsline {subsection}{\numberline {6.2.3}Asserts}{17}{subsection.6.2.3}% -\contentsline {section}{\numberline {6.3}Desktop}{17}{section.6.3}% -\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{18}{chapter.7}% -\contentsline {section}{\numberline {7.1}Herramienta}{18}{section.7.1}% -\contentsline {section}{\numberline {7.2}Perfiles de carga}{18}{section.7.2}% -\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{18}{section.7.3}% +\contentsline {chapter}{\numberline {1}Introducci\'on}{5}{chapter.1}% +\contentsline {section}{\numberline {1.1}Objetivo}{5}{section.1.1}% +\contentsline {section}{\numberline {1.2}Alcance}{5}{section.1.2}% +\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{5}{subsection.1.2.1}% +\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{5}{subsection.1.2.2}% +\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{5}{section.1.3}% +\contentsline {section}{\numberline {1.4}Referencias del sistema}{5}{section.1.4}% +\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{7}{chapter.2}% +\contentsline {section}{\numberline {2.1}C\'omo est\'a dise\~nado el sistema}{7}{section.2.1}% +\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}% +\contentsline {section}{\numberline {2.3}Pipeline de negocio}{8}{section.2.3}% +\contentsline {section}{\numberline {2.4}Inventario de componentes}{9}{section.2.4}% +\contentsline {section}{\numberline {2.5}Flujo cr\'itico: descarga (orquestador + agent Selenium)}{9}{section.2.5}% +\contentsline {section}{\numberline {2.6}Otros flujos de negocio}{10}{section.2.6}% +\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{11}{chapter.3}% +\contentsline {section}{\numberline {3.1}Pir\'amide}{11}{section.3.1}% +\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{11}{section.3.2}% +\contentsline {section}{\numberline {3.3}C\'omo leer un caso del cat\'alogo}{11}{section.3.3}% +\contentsline {section}{\numberline {3.4}Entornos}{11}{section.3.4}% +\contentsline {section}{\numberline {3.5}Mocks vs sistemas reales}{12}{section.3.5}% +\contentsline {section}{\numberline {3.6}Principios}{12}{section.3.6}% +\contentsline {chapter}{\numberline {4}Pruebas unitarias}{13}{chapter.4}% +\contentsline {section}{\numberline {4.1}Gateway}{13}{section.4.1}% +\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{13}{section.4.2}% +\contentsline {section}{\numberline {4.3}Agent bot (PC)}{14}{section.4.3}% +\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{14}{section.4.4}% +\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{14}{section.4.5}% +\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{15}{chapter.5}% +\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{15}{section.5.1}% +\contentsline {section}{\numberline {5.2}Gateway}{15}{section.5.2}% +\contentsline {section}{\numberline {5.3}Bot-descarga}{15}{section.5.3}% +\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{16}{section.5.4}% +\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{16}{section.5.5}% +\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{17}{chapter.6}% +\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{17}{section.6.1}% +\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{17}{section.6.2}% +\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{17}{subsection.6.2.1}% +\contentsline {subsection}{\numberline {6.2.2}Pasos}{18}{subsection.6.2.2}% +\contentsline {subsection}{\numberline {6.2.3}Asserts}{18}{subsection.6.2.3}% +\contentsline {section}{\numberline {6.3}Desktop}{18}{section.6.3}% +\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{19}{chapter.7}% +\contentsline {section}{\numberline {7.1}Herramienta}{19}{section.7.1}% +\contentsline {section}{\numberline {7.2}Perfiles de carga}{19}{section.7.2}% +\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{19}{section.7.3}% \contentsline {section}{\numberline {7.4}Scripts k6 incluidos}{19}{section.7.4}% -\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{19}{section.7.5}% -\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{19}{subsection.7.5.1}% +\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{20}{section.7.5}% +\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{20}{subsection.7.5.1}% \contentsline {subsection}{\numberline {7.5.2}Perfil laboratorio}{20}{subsection.7.5.2}% \contentsline {section}{\numberline {7.6}Ejecuci\'on ejemplo}{20}{section.7.6}% \contentsline {chapter}{\numberline {8}Resiliencia y chaos}{21}{chapter.8}% @@ -70,16 +73,11 @@ \contentsline {section}{\numberline {11.3}Campa\~na registrada}{28}{section.11.3}% \contentsline {section}{\numberline {11.4}Plantilla de informe de ejecuci\'on}{29}{section.11.4}% \contentsline {section}{\numberline {11.5}Responsables (roles)}{29}{section.11.5}% -\contentsline {chapter}{\numberline {12}Informe de ejecuci\'on --- campa\~na 2026-07-13}{30}{chapter.12}% +\contentsline {chapter}{\numberline {12}Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}{30}{chapter.12}% \contentsline {section}{\numberline {12.1}Metadatos}{30}{section.12.1}% -\contentsline {section}{\numberline {12.2}Baseline del entorno}{30}{section.12.2}% -\contentsline {section}{\numberline {12.3}Resultados por caso}{31}{section.12.3}% -\contentsline {subsection}{\numberline {12.3.1}ST-GW-01 --- gateway health}{31}{subsection.12.3.1}% -\contentsline {subsection}{\numberline {12.3.2}ST-BOT-01 --- heartbeats (corrida 1)}{31}{subsection.12.3.2}% -\contentsline {subsection}{\numberline {12.3.3}ST-BOT-01 --- heartbeats (corrida 2, reintento)}{31}{subsection.12.3.3}% -\contentsline {section}{\numberline {12.4}Hallazgos}{31}{section.12.4}% -\contentsline {section}{\numberline {12.5}No ejecutado en esta campa\~na}{32}{section.12.5}% -\contentsline {section}{\numberline {12.6}Follow-ups}{32}{section.12.6}% -\contentsline {chapter}{\numberline {A}Glosario}{33}{appendix.Alph1}% -\contentsline {chapter}{\numberline {B}Checklist de release}{34}{appendix.Alph2}% -\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{35}{appendix.Alph3}% +\contentsline {section}{\numberline {12.2}Resultados resumidos}{30}{section.12.2}% +\contentsline {section}{\numberline {12.3}Lecci\'on operativa}{30}{section.12.3}% +\contentsline {section}{\numberline {12.4}Pendiente respecto a este plan}{31}{section.12.4}% +\contentsline {chapter}{\numberline {A}Glosario}{32}{appendix.Alph1}% +\contentsline {chapter}{\numberline {B}Checklist de release}{33}{appendix.Alph2}% +\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{34}{appendix.Alph3}% diff --git a/docs/plan_pruebas/preamble.tex b/docs/plan_pruebas/preamble.tex index dadfea6..a8fa854 100644 --- a/docs/plan_pruebas/preamble.tex +++ b/docs/plan_pruebas/preamble.tex @@ -19,7 +19,7 @@ \fancyhf{} \fancyhead[L]{\small IPSExtractor --- Plan de pruebas} \fancyhead[R]{\small\thepage} -\fancyfoot[C]{\footnotesize Confidencial --- Uso interno} +\fancyfoot[C]{\footnotesize Documento para compartir} \renewcommand{\headrulewidth}{0.4pt} \renewcommand{\footrulewidth}{0.2pt} @@ -68,6 +68,7 @@ morestring=[b]' } +\usepackage{float} \usepackage{tikz} \usetikzlibrary{arrows.meta,positioning,fit,calc,shapes.geometric,shapes.multipart} @@ -83,8 +84,7 @@ pdfsubject={Pruebas unitarias, integracion, E2E, estres y resiliencia} } -\newcommand{\docversion}{1.2.0} -\newcommand{\docdate}{13 de julio de 2026} +\newcommand{\docdate}{17 de julio de 2026} \newcommand{\statusbadge}[1]{\texttt{#1}} \newcommand{\makecover}{% @@ -95,16 +95,15 @@ {\Huge\bfseries IPSExtractor}\\[0.35cm] {\Large\color{ipsgrey} Sistema de extracci\'on y an\'alisis de facturas m\'edicas}\\[0.4cm] {\color{ipsblue}\rule{\textwidth}{2pt}}\\[1.8cm] - {\LARGE\bfseries Plan completo de pruebas}\\[0.3cm] + {\LARGE\bfseries Plan de pruebas}\\[0.3cm] {\large Unitarias $\cdot$ Integraci\'on $\cdot$ E2E $\cdot$ Estr\'es $\cdot$ Resiliencia}\\[2.2cm] \begin{tabular}{@{}ll@{}} - \textbf{Versi\'on} & \docversion \\ \textbf{Fecha} & \docdate \\ - \textbf{Clasificaci\'on} & Uso interno \\ - \textbf{Herramientas} & pytest, Vitest, go test, k6, Docker Compose, latexmk \\ + \textbf{Audiencia} & Documento para compartir \\ + \textbf{Herramientas} & pytest, Vitest, go test, k6, Docker Compose \\ \end{tabular} \vfill - {\small Generado a partir de la arquitectura real del monorepo IPSExtractor.}\\[0.3cm] + {\small Estrategia y matriz de pruebas. Resultados en el Informe; volumen en Capacidad 100k.}\\[0.3cm] {\color{ipsblue}\rule{0.5\textwidth}{0.6pt}} \end{titlepage} } diff --git a/docs/plan_pruebas/scripts/README.md b/docs/plan_pruebas/scripts/README.md index 9f006d0..e3ac0cc 100644 --- a/docs/plan_pruebas/scripts/README.md +++ b/docs/plan_pruebas/scripts/README.md @@ -47,11 +47,31 @@ k6 run -e PROFILE=shared8g -e DOWNLOAD_JOB_ID= k6_bot_events.js k6 run -e PROFILE=shared8g k6_smoke_suite.js # Solo si free -h muestra ≥3GiB disponibles: k6 run -e PROFILE=shared8g k6_count_batches.js +# o Python (PDFs reales, Windows-friendly): +.\docs\plan_pruebas\scripts\st_count_01_bench.ps1 -Profile shared8g # Latencia count-api con PDFs reales: bash docs/plan_pruebas/scripts/measure_count_latency.sh + +# Latencia LLM ST-EXT-02 (mismo schema/campos, gpt-5.4-mini): +bash docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh +# Windows: .\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Limit 10 + +# Cola ARQ ST-EXT-01 (LLM mock, sin coste OpenAI): +# 1) Mock (otra terminal) + extraction con ASYNC_QUEUE=true y EXTRACTOR_BASE_URL→mock +bash docs/plan_pruebas/scripts/llm_mock_server.sh +# 2) Bench (shared8g ≈30 docs / lab ≈200): +bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh +PROFILE=lab ARQ_BENCH_LIMIT=100 bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh +# Windows: +# .\docs\plan_pruebas\scripts\llm_mock_server.ps1 +# .\docs\plan_pruebas\scripts\st_ext_01_arq_bench.ps1 -Profile shared8g -Limit 30 +# .\docs\plan_pruebas\scripts\st_ext_01_arq_bench.ps1 -StartMock -Limit 20 ``` Modelo de capacidad 100k/mes: [`estimate_capacity.md`](estimate_capacity.md). Informe PDF: [`../InformePruebas_IPSExtractor.pdf`](../InformePruebas_IPSExtractor.pdf). PDFs de prueba: [`../data/`](../data/) (`500000284335937.pdf`, `...940.pdf`, `...954.pdf`). + +Tras el bench LLM: resultados en [`../informe/sections/13_llm_latency.tex`](../informe/sections/13_llm_latency.tex) +(CSV `informes/llm_latency_20260717.csv`). Schema: `informes/st_ext_02_schema.json`. diff --git a/docs/plan_pruebas/scripts/e2e_07_worker_offline.py b/docs/plan_pruebas/scripts/e2e_07_worker_offline.py new file mode 100644 index 0000000..ce09b0c --- /dev/null +++ b/docs/plan_pruebas/scripts/e2e_07_worker_offline.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""E2E-07 — Crear job de descarga con worker offline debe devolver HTTP 409. + +Pasos: + 1. POST /jobs (Desktop padre vía gateway → extraction) + 2. POST /bot-descarga/jobs con worker_id que NO está online (sin heartbeat) + 3. Assert status == 409 + +Uso: + python e2e_07_worker_offline.py + python e2e_07_worker_offline.py --base-url http://127.0.0.1:8189 +""" +from __future__ import annotations + +import argparse +import json +import sys +import time +from io import BytesIO +from pathlib import Path + +try: + import requests +except ImportError: + print("ERROR: pip install requests", file=sys.stderr) + sys.exit(2) + +# Excel mínimo (ZIP/OOXML vacío no hace falta: el 409 ocurre antes de validar contenido) +_MIN_XLSX = ( + b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00!\x00" + b"e2e07-dummy-xlsx-content-for-multipart" +) + + +def main() -> int: + p = argparse.ArgumentParser(description="E2E-07 worker offline → 409") + p.add_argument("--base-url", default="http://127.0.0.1:8189") + p.add_argument( + "--token", + default="", + help="BOT_API_TOKEN (default: env BOT_API_TOKEN o dev-token-change-me)", + ) + p.add_argument( + "--worker-id", + default="e2e07-offline-pc", + help="Worker que no debe estar online", + ) + args = p.parse_args() + + import os + + token = args.token or os.getenv("BOT_API_TOKEN", "dev-token-change-me") + base = args.base_url.rstrip("/") + headers_bot = {"Authorization": f"Bearer {token}"} + worker_id = args.worker_id + out: dict = { + "test_id": "E2E-07", + "base_url": base, + "worker_id": worker_id, + "steps": [], + "verdict": "FAIL", + } + + # 0) Health + t0 = time.perf_counter() + live = requests.get(f"{base}/health/live", timeout=10) + out["steps"].append( + {"step": "health_live", "status": live.status_code, "ms": round((time.perf_counter() - t0) * 1000, 1)} + ) + if live.status_code != 200: + out["error"] = "gateway /health/live no OK" + _write(out) + print(json.dumps(out, indent=2, ensure_ascii=False)) + return 1 + + # 1) Job Desktop padre + t0 = time.perf_counter() + parent = requests.post( + f"{base}/jobs", + json={"title": "E2E-07 worker offline"}, + timeout=15, + ) + out["steps"].append( + { + "step": "create_parent_job", + "status": parent.status_code, + "ms": round((time.perf_counter() - t0) * 1000, 1), + "body": _safe_json(parent), + } + ) + if parent.status_code not in (200, 201): + out["error"] = "no se pudo crear job Desktop padre" + _write(out) + print(json.dumps(out, indent=2, ensure_ascii=False)) + return 1 + parent_id = parent.json().get("id") + if not parent_id: + out["error"] = "respuesta padre sin id" + _write(out) + print(json.dumps(out, indent=2, ensure_ascii=False)) + return 1 + out["parent_job_id"] = parent_id + + # 2) Asegurar que el worker NO está online (no registrar / no heartbeat) + t0 = time.perf_counter() + workers = requests.get(f"{base}/bot-descarga/workers", headers=headers_bot, timeout=10) + online = False + if workers.status_code == 200: + for w in workers.json(): + if w.get("id") == worker_id and w.get("status") in ("online", "busy"): + online = True + out["steps"].append( + { + "step": "check_worker_not_online", + "status": workers.status_code, + "ms": round((time.perf_counter() - t0) * 1000, 1), + "worker_online": online, + } + ) + if online: + out["error"] = ( + f"worker '{worker_id}' está online; usa otro --worker-id o apaga el agent" + ) + _write(out) + print(json.dumps(out, indent=2, ensure_ascii=False)) + return 1 + + # 3) POST create download job → esperar 409 + t0 = time.perf_counter() + files = {"file": ("e2e07.xlsx", BytesIO(_MIN_XLSX), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")} + data = { + "worker_id": worker_id, + "job_id": parent_id, + "title": "E2E-07 offline", + } + create = requests.post( + f"{base}/bot-descarga/jobs", + headers=headers_bot, + data=data, + files=files, + timeout=30, + ) + elapsed = round((time.perf_counter() - t0) * 1000, 1) + detail = _safe_json(create) + out["steps"].append( + { + "step": "create_job_offline_worker", + "status": create.status_code, + "ms": elapsed, + "body": detail, + } + ) + out["expected"] = 409 + out["got"] = create.status_code + + if create.status_code == 409: + out["verdict"] = "PASS" + out["meaning"] = ( + "El orquestador bot-descarga rechaza crear job si el agent Selenium " + "no está online (sin heartbeat). Correcto." + ) + _write(out) + print(json.dumps(out, indent=2, ensure_ascii=False)) + return 0 + + out["verdict"] = "FAIL" + out["error"] = f"se esperaba HTTP 409, se obtuvo {create.status_code}" + _write(out) + print(json.dumps(out, indent=2, ensure_ascii=False)) + return 1 + + +def _safe_json(resp: requests.Response): + try: + return resp.json() + except Exception: + return (resp.text or "")[:500] + + +def _write(out: dict) -> None: + dest = Path(__file__).resolve().parents[1] / "informes" + dest.mkdir(parents=True, exist_ok=True) + stamp = time.strftime("%Y%m%d_%H%M%S") + path = dest / f"e2e_07_{stamp}.json" + path.write_text(json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8") + # también latest + (dest / "e2e_07_latest.json").write_text( + json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8" + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/plan_pruebas/scripts/estimate_capacity.md b/docs/plan_pruebas/scripts/estimate_capacity.md index 56e8354..f119bad 100644 --- a/docs/plan_pruebas/scripts/estimate_capacity.md +++ b/docs/plan_pruebas/scripts/estimate_capacity.md @@ -14,12 +14,96 @@ No sustituye una corrida real de 100k PDFs. Usa supuestos conservadores + eviden | Etapa | facturas/h estimadas | vs 139/h | vs 570/h | |-------|----------------------|----------|----------| | Gateway (orquestación) | >>10 000 (health ~57 rps) | OK | OK | -| count-api OCR | 20–80 (CPU/RAM contención) | RIESGO | NO | -| extraction LLM | 60–200 (si cuota LLM; 30–60s/PDF, pocos workers) | ÁMBAR | NO | -| bot Selenium (1 agent) | 30–120 | RIESGO | NO | +| count-api OCR | **≈100–325** (ST-COUNT-01 2026-07-17: p50 35,8 s @3 conc.; 9/9 ok) | ÁMBAR/OK | NO | +| extraction LLM | **≈506** (ST-EXT-02 2026-07-17: p50 7,114 s, gpt-5.4-mini, 23 PDFs) | OK | ÁMBAR (1w); OK≈1012/h con 2w sin cuota | +| Agent Selenium (1 agent PC) | **≈422** (medido: 633 PDFs / 90 min) | OK vs 139/h | NO vs 570/h | | cleaning CSV | 500–2000 | OK | ÁMBAR | -| Pipeline completo (este host) | <50 | NO | NO | +| Pipeline completo (este host) | **≈100–325** (= min etapas; cuello OCR; **no** medido E2E) | JUSTO/NO vs 139 | NO vs 570 | + +## Bench cola ARQ (ST-EXT-01) — LLM mock + +Mide enqueue + drenado de backlog **sin coste OpenAI**. Requiere mock + `ASYNC_QUEUE=true`. + +```bash +# Terminal 1 — mock OpenAI-compatible +bash docs/plan_pruebas/scripts/llm_mock_server.sh +# Apuntar extraction/workers: +# EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1 +# ASYNC_QUEUE=true +# WORKER_REPLICAS=1 # shared8g; lab → 2 + +# Terminal 2 — bench +bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh +PROFILE=lab ARQ_BENCH_LIMIT=100 bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh + +# Windows +.\docs\plan_pruebas\scripts\llm_mock_server.ps1 +.\docs\plan_pruebas\scripts\st_ext_01_arq_bench.ps1 -Profile shared8g -Limit 30 +``` + +Salida: `informes/st_ext_01_arq_YYYYMMDD.csv` + `.summary.json`. Schema: `st_ext_01_schema.json`. + +**Resultados 2026-07-17 (shared8g, Limit=30):** mode=async; 30/30 queued; drained en **22,5 s**; total **23,9 s**; throughput cola ≈ **4 528 docs/h** (mock ≈50 ms — no es SLO LLM). Producción: `ASYNC_QUEUE=true`. + +## Bench OCR carga (ST-COUNT-01) + +```bash +# Python (recomendado si no hay k6): +python docs/plan_pruebas/scripts/st_count_01_bench.py --profile shared8g +# Windows: +.\docs\plan_pruebas\scripts\st_count_01_bench.ps1 -Profile shared8g + +# k6 (campo multipart corregido a "files"): +k6 run -e PROFILE=shared8g k6_count_batches.js +``` + +**Resultados 2026-07-17:** 9/9 ok; p50 **35,8 s** @3 concurrentes; throughput obs. ≈ **325 fact/h**; sin 5xx/OOM. + +## Bench LLM (ST-EXT-02) — mismo schema/campos + +Congela `fields` en `informes/st_ext_02_schema.json` y los reutiliza en toda la corrida +(y en re-runs) para que latencia/coste sean comparables. Modelo default: **gpt-5.4-mini**. + +```bash +# Linux/macOS +bash docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh +LLM_BENCH_LIMIT=10 bash docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh + +# Windows +.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 +.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Limit 10 -Model gpt-5.4-mini +# Forzar redescubrimiento de schema: +.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Rediscover +``` + +Salida: +- `docs/plan_pruebas/informes/llm_latency_YYYYMMDD.csv` +- `docs/plan_pruebas/informes/llm_latency_YYYYMMDD.summary.json` +- `docs/plan_pruebas/informes/st_ext_02_schema.json` (campos fijos) + +Coste de referencia: USD **0,0257**/doc → **USD 2 570** por 100 000 que pasan conteo. + +**Resultados 2026-07-17:** 23/23 ok; p50 **7,114** s; p95 **8,582** s; ≈**506** fact/h (1 worker). +Volcados en `informe/sections/13_llm_latency.tex` y fila LLM de `08_capacidad_100k.tex`. ## Escenario dedicado (condicional) -Con host ≥16–32 GiB solo IPS, OCR pool, 2+ workers extraction, cuota LLM, N agents bot: pipeline completo puede acercarse a 139–570/h según dimensionamiento. +Con host ≥16–32 GiB solo IPS, OCR pool, 2+ workers extraction, cuota LLM, N agents Selenium: pipeline completo puede acercarse a 139–570/h según dimensionamiento (y p50 LLM medido). + +## Coste LLM — 100 000 facturas que pasan el filtro de conteo + +Supuesto: el conteo filtra el lote y **100 000 PDFs** llegan a extracción. + +| Concepto | Valor | +|----------|-------| +| Modelo de extracción | **gpt-5.4 mini** | +| Coste por documento | **USD 0,0257** | +| Volumen (pasan filtro conteo) | 100 000 | +| **Coste estimado extracción/mes** | **USD 2 570** | + +Fórmula: `100_000 × 0.0257 = 2_570`. + +Notas: +- No incluye agent Selenium, orquestación `bot-descarga`, OCR de conteo, infra ni reintentos. +- Si el filtro de conteo deja pasar menos de 100k, el coste baja linealmente (`n_válidos × 0.0257`). +- ST-EXT-02 2026-07-17 usó el unitario 0.0257 como estimación; contrastar con tokens reales si se instrumenta usage. diff --git a/docs/plan_pruebas/scripts/k6_count_batches.js b/docs/plan_pruebas/scripts/k6_count_batches.js index b9bac70..402ad78 100644 --- a/docs/plan_pruebas/scripts/k6_count_batches.js +++ b/docs/plan_pruebas/scripts/k6_count_batches.js @@ -52,11 +52,13 @@ const tinyPdf = encoding.b64decode( export default function () { const bin = __ENV.PDF_B64 ? encoding.b64decode(__ENV.PDF_B64) : tinyPdf; + // count-api espera el campo multipart "files" (puede ser múltiple). const data = { - file: http.file(bin, 'sample.pdf', 'application/pdf'), + files: http.file(bin, 'sample.pdf', 'application/pdf'), }; const res = http.post(`${BASE_URL}/count/v1/count`, data); check(res, { + 'status 2xx': (r) => r.status >= 200 && r.status < 300, 'not 5xx': (r) => r.status < 500, }); sleep(0.1); diff --git a/docs/plan_pruebas/scripts/llm_mock_server.ps1 b/docs/plan_pruebas/scripts/llm_mock_server.ps1 new file mode 100644 index 0000000..58bd3c4 --- /dev/null +++ b/docs/plan_pruebas/scripts/llm_mock_server.ps1 @@ -0,0 +1,10 @@ +# Arranca mock OpenAI para ST-EXT-01 (Windows). +param( + [int]$Port = 8099, + [double]$DelayMs = 50 +) + +$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") +$py = Join-Path $Root "docs\plan_pruebas\scripts\llm_mock_server.py" +Write-Host "LLM mock → python $py --port $Port --delay-ms $DelayMs" +python $py --port $Port --delay-ms $DelayMs diff --git a/docs/plan_pruebas/scripts/llm_mock_server.py b/docs/plan_pruebas/scripts/llm_mock_server.py new file mode 100644 index 0000000..64f856c --- /dev/null +++ b/docs/plan_pruebas/scripts/llm_mock_server.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Mock OpenAI-compatible para ST-EXT-01 (cola ARQ sin coste LLM real). + +Endpoints: + GET /v1/models + POST /v1/chat/completions + +Responde JSON fijo con delay configurable (default 50 ms). + +Ejemplo: + python docs/plan_pruebas/scripts/llm_mock_server.py --port 8099 --delay-ms 50 + +Apuntar extraction/workers (Docker): + EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1 + NORMALIZER_BASE_URL=http://host.docker.internal:8099/v1 + EXTRACTOR_API_KEY=mock-key + ASYNC_QUEUE=true +""" +from __future__ import annotations + +import argparse +import json +import threading +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlparse + + +MOCK_SCHEMA = { + "numero_factura": "str", + "fecha": "str", + "nit_prestador": "str", + "razon_social": "str", + "valor_total": "number", +} + +MOCK_EXTRACTION = { + "numero_factura": "MOCK-0001", + "fecha": "2026-01-15", + "nit_prestador": "900000000", + "razon_social": "IPS MOCK ST-EXT-01", + "valor_total": 1000, +} + + +class Stats: + def __init__(self) -> None: + self.lock = threading.Lock() + self.completions = 0 + self.models = 0 + + def inc(self, kind: str) -> None: + with self.lock: + if kind == "completions": + self.completions += 1 + else: + self.models += 1 + + +STATS = Stats() + + +def make_handler(delay_ms: float): + class Handler(BaseHTTPRequestHandler): + def log_message(self, fmt: str, *args) -> None: # quieter + pass + + def _json(self, code: int, payload: dict) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: # noqa: N802 + path = urlparse(self.path).path.rstrip("/") + if path in ("/v1/models", "/models"): + STATS.inc("models") + self._json( + 200, + { + "object": "list", + "data": [ + {"id": "mock-llm", "object": "model", "owned_by": "st-ext-01"}, + {"id": "gpt-5.4-mini", "object": "model", "owned_by": "st-ext-01"}, + ], + }, + ) + return + if path in ("/health", "/v1/health"): + self._json(200, {"status": "ok", "completions": STATS.completions}) + return + self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}}) + + def do_POST(self) -> None: # noqa: N802 + path = urlparse(self.path).path.rstrip("/") + length = int(self.headers.get("Content-Length") or 0) + raw = self.rfile.read(length) if length else b"{}" + try: + req = json.loads(raw.decode("utf-8") or "{}") + except Exception: + req = {} + + if path not in ("/v1/chat/completions", "/chat/completions"): + self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}}) + return + + if delay_ms > 0: + time.sleep(delay_ms / 1000.0) + + # Heurística: schema discovery suele pedir "campos" / schema; extracción pide factura. + messages = req.get("messages") or [] + blob = json.dumps(messages, ensure_ascii=False).lower() + if "schema" in blob or "campos" in blob or "field" in blob or "descub" in blob: + content = json.dumps(MOCK_SCHEMA, ensure_ascii=False) + else: + content = json.dumps(MOCK_EXTRACTION, ensure_ascii=False) + + model = req.get("model") or "mock-llm" + STATS.inc("completions") + self._json( + 200, + { + "id": f"chatcmpl-mock-{STATS.completions}", + "object": "chat.completion", + "created": int(time.time()), + "model": model, + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": content}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 100, + "completion_tokens": 50, + "total_tokens": 150, + }, + }, + ) + + return Handler + + +def main() -> int: + p = argparse.ArgumentParser(description="Mock OpenAI API for ST-EXT-01") + p.add_argument("--host", default="0.0.0.0") + p.add_argument("--port", type=int, default=int(__import__("os").environ.get("LLM_MOCK_PORT", "8099"))) + p.add_argument("--delay-ms", type=float, default=float(__import__("os").environ.get("LLM_MOCK_DELAY_MS", "50"))) + args = p.parse_args() + + httpd = ThreadingHTTPServer((args.host, args.port), make_handler(args.delay_ms)) + print(f"[llm-mock] http://{args.host}:{args.port}/v1 delay={args.delay_ms}ms") + print(f"[llm-mock] health: http://127.0.0.1:{args.port}/health") + print("[llm-mock] Ctrl+C para detener") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n[llm-mock] stopped") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/plan_pruebas/scripts/llm_mock_server.sh b/docs/plan_pruebas/scripts/llm_mock_server.sh new file mode 100644 index 0000000..ec4d06f --- /dev/null +++ b/docs/plan_pruebas/scripts/llm_mock_server.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Arranca mock OpenAI para ST-EXT-01. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +PORT="${LLM_MOCK_PORT:-8099}" +DELAY="${LLM_MOCK_DELAY_MS:-50}" +exec python3 "$ROOT/docs/plan_pruebas/scripts/llm_mock_server.py" --port "$PORT" --delay-ms "$DELAY" "$@" diff --git a/docs/plan_pruebas/scripts/measure_llm_latency.ps1 b/docs/plan_pruebas/scripts/measure_llm_latency.ps1 new file mode 100644 index 0000000..18e8a7d --- /dev/null +++ b/docs/plan_pruebas/scripts/measure_llm_latency.ps1 @@ -0,0 +1,11 @@ +# Alias → ST-EXT-02 +param( + [string]$BaseUrl = "http://127.0.0.1:8189", + [string]$DataDir = "", + [string]$OutCsv = "", + [int]$Limit = 0, + [string]$Model = "gpt-5.4-mini" +) +& (Join-Path $PSScriptRoot "st_ext_02_llm_bench.ps1") ` + -BaseUrl $BaseUrl -DataDir $DataDir -OutCsv $OutCsv -Limit $Limit -Model $Model +exit $LASTEXITCODE diff --git a/docs/plan_pruebas/scripts/measure_llm_latency.py b/docs/plan_pruebas/scripts/measure_llm_latency.py new file mode 100644 index 0000000..f42d54e --- /dev/null +++ b/docs/plan_pruebas/scripts/measure_llm_latency.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +"""Alias de compatibilidad → ST-EXT-02 (st_ext_02_llm_bench.py).""" +from __future__ import annotations + +import runpy +import sys +from pathlib import Path + +sys.argv[0] = str(Path(__file__).with_name("st_ext_02_llm_bench.py")) +runpy.run_path(str(Path(__file__).with_name("st_ext_02_llm_bench.py")), run_name="__main__") diff --git a/docs/plan_pruebas/scripts/measure_llm_latency.sh b/docs/plan_pruebas/scripts/measure_llm_latency.sh new file mode 100644 index 0000000..6893086 --- /dev/null +++ b/docs/plan_pruebas/scripts/measure_llm_latency.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Alias → ST-EXT-02 +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +exec bash "$ROOT/docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh" "$@" diff --git a/docs/plan_pruebas/scripts/st_count_01_bench.ps1 b/docs/plan_pruebas/scripts/st_count_01_bench.ps1 new file mode 100644 index 0000000..4b10f44 --- /dev/null +++ b/docs/plan_pruebas/scripts/st_count_01_bench.ps1 @@ -0,0 +1,20 @@ +# ST-COUNT-01 — carga count-api (Windows). +param( + [string]$BaseUrl = "http://127.0.0.1:8189", + [string]$Profile = "shared8g", + [int]$Duration = 0, + [double]$Rate = 0, + [int]$MaxWorkers = 0 +) + +$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") +$py = Join-Path $Root "docs\plan_pruebas\scripts\st_count_01_bench.py" + +$argsList = @("--base-url", $BaseUrl, "--profile", $Profile) +if ($Duration -gt 0) { $argsList += @("--duration", "$Duration") } +if ($Rate -gt 0) { $argsList += @("--rate", "$Rate") } +if ($MaxWorkers -gt 0) { $argsList += @("--max-workers", "$MaxWorkers") } + +Write-Host "ST-COUNT-01 → python $py $($argsList -join ' ')" +python $py @argsList +exit $LASTEXITCODE diff --git a/docs/plan_pruebas/scripts/st_count_01_bench.py b/docs/plan_pruebas/scripts/st_count_01_bench.py new file mode 100644 index 0000000..d2b581b --- /dev/null +++ b/docs/plan_pruebas/scripts/st_count_01_bench.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python3 +"""ST-COUNT-01 — carga concurrente hacia count-api (vía gateway). + +Equivalente práctico al k6 k6_count_batches.js para hosts sin k6 instalado. +Perfiles: + shared8g → ~1 req/s durante 60s, concurrencia máxima 3 (conservador) + lab → ~3 req/s durante 120s, concurrencia máxima 8 + +Usa PDFs reales de docs/plan_pruebas/data/ (rota entre ellos). + +Ejemplos: + python docs/plan_pruebas/scripts/st_count_01_bench.py + python docs/plan_pruebas/scripts/st_count_01_bench.py --profile shared8g + PROFILE=lab python docs/plan_pruebas/scripts/st_count_01_bench.py --duration 90 +""" +from __future__ import annotations + +import argparse +import csv +import json +import os +import statistics +import sys +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import date, datetime, timezone +from pathlib import Path + +try: + import requests +except ImportError: + print("Requiere 'requests': pip install requests", file=sys.stderr) + sys.exit(1) + +ROOT = Path(__file__).resolve().parents[3] +DEFAULT_DATA = ROOT / "docs" / "plan_pruebas" / "data" +DEFAULT_INFORMES = ROOT / "docs" / "plan_pruebas" / "informes" + +PROFILES = { + "shared8g": {"rate": 1.0, "duration_s": 60, "max_workers": 3}, + "lab": {"rate": 3.0, "duration_s": 120, "max_workers": 8}, +} + + +def list_pdfs(data_dir: Path) -> list[Path]: + if not data_dir.is_dir(): + raise SystemExit(f"No existe DATA_DIR={data_dir}") + pdfs = sorted({p.resolve() for p in list(data_dir.glob("*.pdf")) + list(data_dir.glob("*.PDF"))}) + if not pdfs: + raise SystemExit(f"No hay PDFs en {data_dir}") + return pdfs + + +def percentile(vals: list[float], p: float) -> float: + if not vals: + return float("nan") + s = sorted(vals) + if len(s) == 1: + return s[0] + k = (len(s) - 1) * (p / 100.0) + f = int(k) + c = min(f + 1, len(s) - 1) + if f == c: + return s[f] + return s[f] + (s[c] - s[f]) * (k - f) + + +def count_one(base_url: str, pdf: Path, timeout_s: float) -> dict: + t0 = time.perf_counter() + status = "error" + http_code = 0 + service_count = "" + err = "" + try: + with pdf.open("rb") as fh: + r = requests.post( + f"{base_url}/count/v1/count", + files={"files": (pdf.name, fh, "application/pdf")}, + timeout=timeout_s, + ) + http_code = r.status_code + try: + body = r.json() + except Exception: + body = {} + if r.status_code >= 500: + status = f"http_{r.status_code}" + err = (r.text or "")[:200].replace("\n", " ") + elif r.status_code >= 400: + status = f"http_{r.status_code}" + err = str(body.get("error") or r.text)[:200] + else: + results = body.get("results") or [] + if results: + service_count = results[0].get("service_count", "") + status = str(results[0].get("status") or "ok") + else: + status = "ok_empty" + except requests.Timeout: + status = "timeout" + except Exception as ex: + status = "exception" + err = str(ex)[:200] + + return { + "file": pdf.name, + "wall_s": round(time.perf_counter() - t0, 3), + "http_code": http_code, + "status": status, + "service_count": service_count, + "error": err, + } + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="ST-COUNT-01 count-api load bench") + p.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8189")) + p.add_argument("--data-dir", type=Path, default=Path(os.environ.get("COUNT_BENCH_PDF_DIR") or DEFAULT_DATA)) + p.add_argument( + "--profile", + choices=sorted(PROFILES.keys()), + default=os.environ.get("PROFILE") or "shared8g", + ) + p.add_argument("--duration", type=float, default=float(os.environ.get("COUNT_BENCH_DURATION_S") or "0")) + p.add_argument("--rate", type=float, default=float(os.environ.get("COUNT_BENCH_RATE") or "0")) + p.add_argument("--max-workers", type=int, default=int(os.environ.get("COUNT_BENCH_MAX_WORKERS") or "0")) + p.add_argument("--timeout", type=float, default=float(os.environ.get("COUNT_BENCH_TIMEOUT_S") or "120")) + p.add_argument( + "--out-csv", + type=Path, + default=Path( + os.environ.get("COUNT_BENCH_CSV") + or (DEFAULT_INFORMES / f"st_count_01_{date.today().strftime('%Y%m%d')}.csv") + ), + ) + return p.parse_args() + + +def main() -> int: + args = parse_args() + base_url = str(args.base_url).rstrip("/") + cfg = PROFILES[args.profile] + rate = args.rate if args.rate > 0 else cfg["rate"] + duration_s = args.duration if args.duration > 0 else cfg["duration_s"] + max_workers = args.max_workers if args.max_workers > 0 else cfg["max_workers"] + interval = 1.0 / rate if rate > 0 else 1.0 + + try: + h = requests.get(f"{base_url}/health/live", timeout=5) + print(f"health/live -> {h.status_code}") + except Exception as ex: + print(f"AVISO: health/live ({ex})", file=sys.stderr) + + pdfs = list_pdfs(args.data_dir) + print(f"BASE_URL={base_url}") + print(f"PROFILE={args.profile} rate={rate}/s duration={duration_s}s max_workers={max_workers}") + print(f"DATA_DIR={args.data_dir} ({len(pdfs)} PDFs)") + print(f"OUT={args.out_csv}") + print() + + rows: list[dict] = [] + lock = threading.Lock() + idx = {"i": 0} + stop_at = time.perf_counter() + duration_s + next_slot = time.perf_counter() + + def submit_one(ex: ThreadPoolExecutor, futures: set): + nonlocal next_slot + now = time.perf_counter() + if now < next_slot: + time.sleep(next_slot - now) + next_slot = max(next_slot + interval, time.perf_counter()) + with lock: + pdf = pdfs[idx["i"] % len(pdfs)] + idx["i"] += 1 + n = idx["i"] + print(f"[launch {n}] {pdf.name} ...", flush=True) + futures.add(ex.submit(count_one, base_url, pdf, args.timeout)) + + args.out_csv.parent.mkdir(parents=True, exist_ok=True) + fieldnames = ["file", "wall_s", "http_code", "status", "service_count", "error"] + t0 = time.perf_counter() + futures: set = set() + + with ThreadPoolExecutor(max_workers=max_workers) as ex: + while time.perf_counter() < stop_at: + # No saturar más allá de max_workers en vuelo + while len(futures) >= max_workers: + done = {f for f in futures if f.done()} + for f in done: + row = f.result() + rows.append(row) + print( + f" done wall_s={row['wall_s']} code={row['http_code']} status={row['status']}", + flush=True, + ) + futures.remove(f) + if len(futures) >= max_workers: + time.sleep(0.05) + if time.perf_counter() >= stop_at: + break + submit_one(ex, futures) + + for f in as_completed(futures): + row = f.result() + rows.append(row) + print( + f" done wall_s={row['wall_s']} code={row['http_code']} status={row['status']}", + flush=True, + ) + + wall_total = round(time.perf_counter() - t0, 3) + + with args.out_csv.open("w", encoding="utf-8", newline="") as fh: + w = csv.DictWriter(fh, fieldnames=fieldnames) + w.writeheader() + for row in rows: + w.writerow(row) + + ok = [r for r in rows if r["http_code"] and 200 <= int(r["http_code"]) < 300] + fail = [r for r in rows if r not in ok] + times = [float(r["wall_s"]) for r in ok] + meta = { + "scenario": "ST-COUNT-01", + "profile": args.profile, + "rate_per_s": rate, + "duration_s_planned": duration_s, + "max_workers": max_workers, + "n_total": len(rows), + "n_ok": len(ok), + "n_fail": len(fail), + "wall_total_s": wall_total, + "achieved_rate_per_s": round(len(rows) / wall_total, 3) if wall_total else 0, + "csv": str(args.out_csv), + "finished_at": datetime.now(timezone.utc).isoformat(), + } + if times: + meta["wall_s_avg"] = round(statistics.mean(times), 3) + meta["wall_s_p50"] = round(percentile(times, 50), 3) + meta["wall_s_p95"] = round(percentile(times, 95), 3) + meta["wall_s_min"] = round(min(times), 3) + meta["wall_s_max"] = round(max(times), 3) + # Throughput efectivo si se procesara en serie con p50 (referencia conservadora) + meta["throughput_serial_p50_per_h"] = round(3600 / meta["wall_s_p50"], 1) if meta["wall_s_p50"] else None + meta["throughput_parallel_est_per_h"] = ( + round(meta["throughput_serial_p50_per_h"] * max_workers, 1) + if meta.get("throughput_serial_p50_per_h") + else None + ) + + meta_path = args.out_csv.with_suffix(".summary.json") + meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + print() + print("=== Resumen ST-COUNT-01 (count-api carga) ===") + print(f"CSV: {args.out_csv}") + print(f"Summary: {meta_path}") + print(f"ok={len(ok)} fail={len(fail)} total={len(rows)} wall_total={wall_total}s") + print(f"achieved_rate ~ {meta['achieved_rate_per_s']}/s (plan {rate}/s)") + if times: + print( + f"wall_s avg={meta['wall_s_avg']:.3f} p50={meta['wall_s_p50']:.3f} " + f"p95={meta['wall_s_p95']:.3f} min={meta['wall_s_min']:.3f} max={meta['wall_s_max']:.3f}" + ) + print( + f"Throughput ref. serie(p50) ~ {meta['throughput_serial_p50_per_h']} fact/h; " + f"x{max_workers} workers ~ {meta['throughput_parallel_est_per_h']} fact/h (ideal)" + ) + oom_hint = any("timeout" in r["status"] or str(r["http_code"]).startswith("5") for r in rows) + if oom_hint: + print("AVISO: timeouts/5xx — posible contencion RAM/CPU; revisa docker stats.", file=sys.stderr) + # PASS si hay ok y tasa de fallo < 15% (umbral checks k6 rate>0.85) + fail_rate = len(fail) / max(len(rows), 1) + return 0 if len(ok) > 0 and fail_rate <= 0.15 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/plan_pruebas/scripts/st_count_01_bench.sh b/docs/plan_pruebas/scripts/st_count_01_bench.sh new file mode 100644 index 0000000..37e2130 --- /dev/null +++ b/docs/plan_pruebas/scripts/st_count_01_bench.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# ST-COUNT-01 — carga count-api. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +export BASE_URL="${BASE_URL:-http://127.0.0.1:8189}" +export PROFILE="${PROFILE:-shared8g}" +exec python3 "$ROOT/docs/plan_pruebas/scripts/st_count_01_bench.py" "$@" diff --git a/docs/plan_pruebas/scripts/st_ext_01_arq_bench.ps1 b/docs/plan_pruebas/scripts/st_ext_01_arq_bench.ps1 new file mode 100644 index 0000000..94c8ce9 --- /dev/null +++ b/docs/plan_pruebas/scripts/st_ext_01_arq_bench.ps1 @@ -0,0 +1,27 @@ +# ST-EXT-01 — bench cola ARQ con LLM mock (Windows). +param( + [string]$BaseUrl = "http://127.0.0.1:8189", + [string]$DataDir = "", + [string]$OutCsv = "", + [string]$SchemaFile = "", + [string]$Profile = "shared8g", + [int]$Limit = 0, + [string]$Model = "mock-llm", + [ValidateSet("one", "many")] + [string]$IngestMode = "one", + [switch]$StartMock +) + +$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") +$py = Join-Path $Root "docs\plan_pruebas\scripts\st_ext_01_arq_bench.py" + +$argsList = @("--base-url", $BaseUrl, "--profile", $Profile, "--model", $Model, "--ingest-mode", $IngestMode) +if ($DataDir) { $argsList += @("--data-dir", $DataDir) } +if ($OutCsv) { $argsList += @("--out-csv", $OutCsv) } +if ($SchemaFile) { $argsList += @("--schema-file", $SchemaFile) } +if ($Limit -gt 0) { $argsList += @("--limit", "$Limit") } +if ($StartMock) { $argsList += "--start-mock" } + +Write-Host "ST-EXT-01 → python $py $($argsList -join ' ')" +python $py @argsList +exit $LASTEXITCODE diff --git a/docs/plan_pruebas/scripts/st_ext_01_arq_bench.py b/docs/plan_pruebas/scripts/st_ext_01_arq_bench.py new file mode 100644 index 0000000..4f0e4cb --- /dev/null +++ b/docs/plan_pruebas/scripts/st_ext_01_arq_bench.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python3 +"""ST-EXT-01 — Bench de cola ARQ con LLM mock (sin coste OpenAI). + +Mide enqueue + drenado de backlog (no latencia LLM real; eso es ST-EXT-02). + +Prerrequisitos: + 1. Mock LLM en marcha: + python docs/plan_pruebas/scripts/llm_mock_server.py --port 8099 --delay-ms 50 + 2. Extraction/workers apuntando al mock y con cola async: + EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1 + NORMALIZER_BASE_URL=http://host.docker.internal:8099/v1 + EXTRACTOR_API_KEY=mock-key + ASYNC_QUEUE=true + WORKER_REPLICAS=1 # shared8g; lab → 2 + 3. Gateway en BASE_URL (default http://127.0.0.1:8189) + +Perfiles (--profile): + shared8g → default --limit 30 (plan: 20–50) + lab → default --limit 200 (plan: 100–500) + +Flujo: + 1. Carga schema congelado (informes/st_ext_01_schema.json) + 2. POST /tasks/extraction con N documentos + 3. Encola N PDFs (ingest o ingest-many) midiendo enqueue_s + 4. Sondea GET /tasks/{id} hasta processed >= N (drain_s) + 5. Escribe CSV + summary JSON + +Ejemplos: + python docs/plan_pruebas/scripts/st_ext_01_arq_bench.py + python docs/plan_pruebas/scripts/st_ext_01_arq_bench.py --profile lab --limit 100 + python docs/plan_pruebas/scripts/st_ext_01_arq_bench.py --start-mock --limit 20 +""" +from __future__ import annotations + +import argparse +import csv +import json +import os +import statistics +import subprocess +import sys +import time +import urllib.parse +from datetime import date, datetime, timezone +from pathlib import Path + +try: + import requests +except ImportError: + print("Requiere 'requests': pip install requests", file=sys.stderr) + sys.exit(1) + +ROOT = Path(__file__).resolve().parents[3] +SCRIPTS = Path(__file__).resolve().parent +DEFAULT_DATA = ROOT / "docs" / "plan_pruebas" / "data" +DEFAULT_INFORMES = ROOT / "docs" / "plan_pruebas" / "informes" +DEFAULT_SCHEMA = DEFAULT_INFORMES / "st_ext_01_schema.json" +MOCK_SCRIPT = SCRIPTS / "llm_mock_server.py" + +PROFILE_DEFAULTS = { + "shared8g": {"limit": 30, "workers_hint": 1}, + "lab": {"limit": 200, "workers_hint": 2}, +} + + +def list_pdfs(data_dir: Path) -> list[Path]: + if not data_dir.is_dir(): + raise SystemExit(f"No existe DATA_DIR={data_dir}") + pdfs = sorted(data_dir.glob("*.pdf")) + sorted(data_dir.glob("*.PDF")) + seen: set[str] = set() + unique: list[Path] = [] + for p in pdfs: + key = p.name.lower() + if key in seen: + continue + seen.add(key) + unique.append(p) + if not unique: + raise SystemExit(f"No hay PDFs en {data_dir}") + return unique + + +def expand_jobs(pdfs: list[Path], n: int) -> list[tuple[Path, str]]: + """Reutiliza PDFs con radicado único si n > len(pdfs).""" + jobs: list[tuple[Path, str]] = [] + for i in range(n): + pdf = pdfs[i % len(pdfs)] + rad = f"{pdf.stem}_{i:04d}" + jobs.append((pdf, rad)) + return jobs + + +def load_schema(path: Path) -> dict: + data = json.loads(path.read_text(encoding="utf-8")) + fields = list(data.get("fields") or []) + if not fields: + raise SystemExit(f"Schema sin fields: {path}") + if "alias_map" not in data: + data["alias_map"] = {f: f.split("/")[-1] for f in fields} + print(f"[ST-EXT-01] Schema: {path} ({len(fields)} campos)") + return data + + +def create_task(base_url: str, jobs: list[tuple[Path, str]], freeze: dict, model: str) -> str: + fields = list(freeze["fields"]) + alias_map = dict(freeze.get("alias_map") or {f: f.split("/")[-1] for f in fields}) + structure_mode = str(freeze.get("structure_mode") or "general") + documents = [ + {"radicado": rad, "filename": pdf.name, "doc_type": None} for pdf, rad in jobs + ] + body = { + "source": "desktop", + "fields": fields, + "alias_map": alias_map, + "structure_mode": structure_mode, + "retry_on_empty": False, + "documents": documents, + "document_count": len(documents), + "model": model, + } + r = requests.post(f"{base_url}/tasks/extraction", json=body, timeout=60) + if r.status_code >= 400: + raise SystemExit(f"tasks/extraction HTTP {r.status_code}: {r.text[:500]}") + task = r.json() + task_id = task.get("id") + if not task_id: + raise SystemExit(f"Sin task id: {task}") + print(f"[ST-EXT-01] Tarea {task_id} — {len(jobs)} docs, model={model}") + return str(task_id) + + +def task_snapshot(base_url: str, task_id: str) -> dict: + r = requests.get(f"{base_url}/tasks/{urllib.parse.quote(task_id)}", timeout=30) + r.raise_for_status() + data = r.json() + result = data.get("result") or {} + processed = int(result.get("processed") or 0) + row_count = int(result.get("row_count") or 0) + + # Fuente de verdad más fiable ante carrera en result.processed: + # contar filas persistidas vía /tasks/{id}/rows. + rows_n = 0 + try: + rr = requests.get( + f"{base_url}/tasks/{urllib.parse.quote(task_id)}/rows", + params={"limit": 5000, "offset": 0}, + timeout=30, + ) + if rr.status_code < 400: + body = rr.json() + if isinstance(body, list): + rows_n = len(body) + elif isinstance(body, dict): + rows_n = int(body.get("count") or len(body.get("rows") or [])) + except Exception: + rows_n = 0 + + progress = max(processed, row_count, rows_n) + return { + "status": str(data.get("status") or "unknown"), + "processed": progress, + "processed_raw": processed, + "row_count": max(row_count, rows_n), + "total": int(result.get("total") or 0), + "errors": list(result.get("errors") or []), + } + + +def enqueue_one( + base_url: str, + task_id: str, + pdf: Path, + radicado: str, + timeout_s: float, +) -> dict: + t0 = time.perf_counter() + http_code = 0 + status = "error" + queued = False + err_msg = "" + try: + with pdf.open("rb") as fh: + r = requests.post( + f"{base_url}/tasks/{urllib.parse.quote(task_id)}/ingest", + files={"file": (pdf.name, fh, "application/pdf")}, + data={"radicado": radicado, "filename": pdf.name}, + timeout=timeout_s, + ) + http_code = r.status_code + try: + body = r.json() + except Exception: + body = {} + if r.status_code >= 400: + status = f"http_{r.status_code}" + err_msg = (r.text or "")[:200].replace("\n", " ") + elif body.get("queued") is True or body.get("queued") == 1: + queued = True + status = "queued" + else: + # sync: respuesta ya incluye progreso + task = body.get("task") or body + result = (task.get("result") if isinstance(task, dict) else None) or {} + processed = int(result.get("processed") or 0) + status = "sync_ok" if processed > 0 or r.status_code < 300 else "sync_no_progress" + errors = result.get("errors") or [] + if errors: + err_msg = str(errors[-1])[:200] + except requests.Timeout: + status = "timeout" + except Exception as ex: + status = "exception" + err_msg = str(ex)[:200] + + return { + "radicado": radicado, + "file": pdf.name, + "enqueue_s": round(time.perf_counter() - t0, 3), + "http_code": http_code, + "status": status, + "queued": "1" if queued else "0", + "error": err_msg, + } + + +def enqueue_many_batch( + base_url: str, + task_id: str, + batch: list[tuple[Path, str]], + timeout_s: float, +) -> list[dict]: + """POST /ingest-many; un row por PDF del lote.""" + t0 = time.perf_counter() + files = [] + open_handles = [] + try: + for pdf, _rad in batch: + fh = pdf.open("rb") + open_handles.append(fh) + files.append(("files", (pdf.name, fh, "application/pdf"))) + data = { + "radicados": "|".join(rad for _, rad in batch), + "filenames": "|".join(pdf.name for pdf, _ in batch), + } + r = requests.post( + f"{base_url}/tasks/{urllib.parse.quote(task_id)}/ingest-many", + files=files, + data=data, + timeout=timeout_s, + ) + finally: + for fh in open_handles: + fh.close() + + wall = round(time.perf_counter() - t0, 3) + per = round(wall / max(len(batch), 1), 3) + try: + body = r.json() + except Exception: + body = {} + + queued_flag = False + status = "error" + err_msg = "" + if r.status_code >= 400: + status = f"http_{r.status_code}" + err_msg = (r.text or "")[:200].replace("\n", " ") + elif isinstance(body.get("queued"), int) or body.get("queued") is True: + queued_flag = True + status = "queued" + else: + status = "sync_batch" + + rows = [] + for pdf, rad in batch: + rows.append( + { + "radicado": rad, + "file": pdf.name, + "enqueue_s": per, + "http_code": r.status_code, + "status": status, + "queued": "1" if queued_flag else "0", + "error": err_msg, + } + ) + return rows + + +def wait_drain( + base_url: str, + task_id: str, + target: int, + poll_timeout_s: float, + poll_interval_s: float, +) -> dict: + t0 = time.perf_counter() + deadline = t0 + poll_timeout_s + last = {"status": "unknown", "processed": 0, "total": target, "errors": []} + samples: list[tuple[float, int]] = [] + while time.perf_counter() < deadline: + last = task_snapshot(base_url, task_id) + elapsed = time.perf_counter() - t0 + samples.append((elapsed, last["processed"])) + if last["processed"] >= target: + break + if last["status"] in ("failed", "cancelled"): + break + print( + f" … processed={last['processed']}/{target} status={last['status']} " + f"elapsed={elapsed:.1f}s", + flush=True, + ) + time.sleep(poll_interval_s) + drain_s = round(time.perf_counter() - t0, 3) + return { + "drain_s": drain_s, + "processed": last["processed"], + "task_status": last["status"], + "errors_tail": (last.get("errors") or [])[-3:], + "samples": samples, + "drained": last["processed"] >= target, + } + + +def start_mock(port: int, delay_ms: float) -> subprocess.Popen: + if not MOCK_SCRIPT.is_file(): + raise SystemExit(f"No existe mock: {MOCK_SCRIPT}") + print(f"[ST-EXT-01] Arrancando mock LLM :{port} delay={delay_ms}ms") + proc = subprocess.Popen( + [sys.executable, str(MOCK_SCRIPT), "--port", str(port), "--delay-ms", str(delay_ms)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + # Esperar health + url = f"http://127.0.0.1:{port}/health" + for _ in range(30): + try: + r = requests.get(url, timeout=1) + if r.status_code == 200: + print(f"[ST-EXT-01] Mock listo → {url}") + return proc + except Exception: + pass + time.sleep(0.2) + if proc.poll() is not None: + raise SystemExit(f"Mock salió con código {proc.returncode}") + raise SystemExit("Mock no respondió /health a tiempo") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="ST-EXT-01 ARQ bench (LLM mock)") + p.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8189")) + p.add_argument("--data-dir", type=Path, default=Path(os.environ.get("ARQ_BENCH_PDF_DIR") or DEFAULT_DATA)) + p.add_argument( + "--out-csv", + type=Path, + default=Path( + os.environ.get("ARQ_BENCH_CSV") + or (DEFAULT_INFORMES / f"st_ext_01_arq_{date.today().strftime('%Y%m%d')}.csv") + ), + ) + p.add_argument( + "--schema-file", + type=Path, + default=Path(os.environ.get("ARQ_BENCH_SCHEMA_FILE") or DEFAULT_SCHEMA), + ) + p.add_argument( + "--profile", + choices=sorted(PROFILE_DEFAULTS.keys()), + default=os.environ.get("PROFILE") or "shared8g", + ) + p.add_argument("--limit", type=int, default=int(os.environ.get("ARQ_BENCH_LIMIT") or "0")) + p.add_argument("--model", default=os.environ.get("ARQ_BENCH_MODEL") or "mock-llm") + p.add_argument( + "--ingest-mode", + choices=["one", "many"], + default=os.environ.get("ARQ_BENCH_INGEST_MODE") or "one", + help="one=POST /ingest por PDF; many=POST /ingest-many por lote", + ) + p.add_argument("--batch-size", type=int, default=int(os.environ.get("ARQ_BENCH_BATCH_SIZE") or "10")) + p.add_argument("--poll-timeout", type=float, default=float(os.environ.get("ARQ_BENCH_POLL_TIMEOUT_S") or "600")) + p.add_argument("--poll-interval", type=float, default=float(os.environ.get("ARQ_BENCH_POLL_INTERVAL_S") or "1.0")) + p.add_argument("--enqueue-timeout", type=float, default=float(os.environ.get("ARQ_BENCH_ENQUEUE_TIMEOUT_S") or "120")) + p.add_argument("--start-mock", action="store_true", help="Arranca llm_mock_server.py en este proceso") + p.add_argument("--mock-port", type=int, default=int(os.environ.get("LLM_MOCK_PORT") or "8099")) + p.add_argument("--mock-delay-ms", type=float, default=float(os.environ.get("LLM_MOCK_DELAY_MS") or "50")) + return p.parse_args() + + +def main() -> int: + args = parse_args() + base_url = str(args.base_url).rstrip("/") + profile = PROFILE_DEFAULTS[args.profile] + limit = args.limit if args.limit > 0 else int(profile["limit"]) + + mock_proc: subprocess.Popen | None = None + if args.start_mock: + mock_proc = start_mock(args.mock_port, args.mock_delay_ms) + + try: + try: + h = requests.get(f"{base_url}/health/live", timeout=5) + print(f"health/live → {h.status_code}") + except Exception as ex: + print(f"AVISO: no responde {base_url}/health/live ({ex})", file=sys.stderr) + + pdfs = list_pdfs(args.data_dir) + jobs = expand_jobs(pdfs, limit) + freeze = load_schema(args.schema_file) + + print(f"BASE_URL={base_url}") + print(f"PROFILE={args.profile} LIMIT={limit} (plan workers≈{profile['workers_hint']})") + print(f"DATA_DIR={args.data_dir} ({len(pdfs)} PDFs base, {len(jobs)} jobs)") + print(f"OUT={args.out_csv}") + print(f"INGEST={args.ingest_mode} MODEL={args.model}") + print() + print( + "AVISO: ST-EXT-01 requiere ASYNC_QUEUE=true + workers + EXTRACTOR_BASE_URL→mock. " + "Si ves status=sync_* el stack está en modo síncrono.", + flush=True, + ) + print() + + task_id = create_task(base_url, jobs, freeze, args.model) + + args.out_csv.parent.mkdir(parents=True, exist_ok=True) + fieldnames = ["radicado", "file", "enqueue_s", "http_code", "status", "queued", "error"] + rows: list[dict] = [] + + t_enq0 = time.perf_counter() + with args.out_csv.open("w", encoding="utf-8", newline="") as fh: + w = csv.DictWriter(fh, fieldnames=fieldnames) + w.writeheader() + + if args.ingest_mode == "many": + bs = max(1, args.batch_size) + for i in range(0, len(jobs), bs): + batch = jobs[i : i + bs] + print(f"[enqueue {i + 1}-{i + len(batch)}/{len(jobs)}] ingest-many …", flush=True) + batch_rows = enqueue_many_batch( + base_url, task_id, batch, args.enqueue_timeout + ) + for row in batch_rows: + rows.append(row) + w.writerow(row) + fh.flush() + if any(str(r["status"]).startswith("http_") for r in batch_rows): + print( + "AVISO: error HTTP en lote — abortando enqueue restante.", + file=sys.stderr, + ) + break + else: + consecutive_fail = 0 + for i, (pdf, rad) in enumerate(jobs, 1): + print(f"[enqueue {i}/{len(jobs)}] {rad} ({pdf.name}) …", flush=True) + row = enqueue_one(base_url, task_id, pdf, rad, args.enqueue_timeout) + rows.append(row) + w.writerow(row) + fh.flush() + print( + f" enqueue_s={row['enqueue_s']} code={row['http_code']} " + f"status={row['status']} queued={row['queued']}", + flush=True, + ) + if str(row["status"]).startswith("http_") or row["status"] in ( + "error", + "timeout", + "exception", + ): + consecutive_fail += 1 + if consecutive_fail >= 3: + print( + "AVISO: 3 fallos consecutivos de enqueue — abortando.", + file=sys.stderr, + ) + break + else: + consecutive_fail = 0 + + enqueue_wall_s = round(time.perf_counter() - t_enq0, 3) + n_queued = sum(1 for r in rows if r["queued"] == "1") + n_fail = sum(1 for r in rows if str(r["status"]).startswith("http_") or r["status"] in ("error", "timeout", "exception")) + mode = "async" if n_queued > 0 else "sync_or_unknown" + + print() + print(f"[ST-EXT-01] Enqueue wall={enqueue_wall_s}s queued={n_queued}/{len(rows)} mode={mode}") + print(f"[ST-EXT-01] Drenando hasta processed>={len(jobs)} (timeout={args.poll_timeout}s)…", flush=True) + + drain = wait_drain(base_url, task_id, len(jobs), args.poll_timeout, args.poll_interval) + total_s = round(enqueue_wall_s + drain["drain_s"], 3) + processed = int(drain["processed"]) + thr_h = round(processed / (total_s / 3600.0), 1) if total_s > 0 and processed else 0.0 + + enqueue_times = [float(r["enqueue_s"]) for r in rows if r.get("enqueue_s") is not None] + meta = { + "scenario": "ST-EXT-01", + "profile": args.profile, + "task_id": task_id, + "model": args.model, + "mode": mode, + "ingest_mode": args.ingest_mode, + "n_target": len(jobs), + "n_queued": n_queued, + "n_enqueue_fail": n_fail, + "processed": processed, + "drained": drain["drained"], + "task_status": drain["task_status"], + "enqueue_wall_s": enqueue_wall_s, + "drain_s": drain["drain_s"], + "total_s": total_s, + "throughput_docs_per_h": thr_h, + "workers_hint": profile["workers_hint"], + "schema_file": str(args.schema_file), + "csv": str(args.out_csv), + "errors_tail": drain.get("errors_tail") or [], + "finished_at": datetime.now(timezone.utc).isoformat(), + } + if enqueue_times: + meta["enqueue_s_avg"] = round(statistics.mean(enqueue_times), 3) + meta["enqueue_s_p50"] = round(sorted(enqueue_times)[len(enqueue_times) // 2], 3) + + meta_path = args.out_csv.with_suffix(".summary.json") + meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + print() + print("=== Resumen ST-EXT-01 (cola ARQ / LLM mock) ===") + print(f"CSV: {args.out_csv}") + print(f"Summary: {meta_path}") + print(f"task_id: {task_id}") + print(f"mode: {mode} (ASYNC_QUEUE esperado=true)") + print(f"profile: {args.profile} target={len(jobs)} processed={processed} drained={drain['drained']}") + print(f"enqueue_s: wall={enqueue_wall_s} avg={meta.get('enqueue_s_avg', 'n/a')}") + print(f"drain_s: {drain['drain_s']} task_status={drain['task_status']}") + print(f"total_s: {total_s}") + print(f"throughput: ≈ {thr_h} docs/h (processed/total_s)") + if mode != "async": + print( + "AVISO: no se detectó queued=true — revisa ASYNC_QUEUE=true, Redis y workers.", + file=sys.stderr, + ) + if not drain["drained"]: + print( + "AVISO: backlog no drenado a tiempo — revisa WORKER_REPLICAS, mock LLM y logs.", + file=sys.stderr, + ) + print() + print("Checklist plan: shared8g 20–50 docs / 1 worker; lab 100–500 / WORKER_REPLICAS=2") + return 0 if drain["drained"] and n_fail == 0 else 1 + finally: + if mock_proc is not None and mock_proc.poll() is None: + mock_proc.terminate() + try: + mock_proc.wait(timeout=5) + except Exception: + mock_proc.kill() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh b/docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh new file mode 100644 index 0000000..e625e4c --- /dev/null +++ b/docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# ST-EXT-01 — bench cola ARQ con LLM mock. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +export BASE_URL="${BASE_URL:-http://127.0.0.1:8189}" +export PROFILE="${PROFILE:-shared8g}" +export ARQ_BENCH_MODEL="${ARQ_BENCH_MODEL:-mock-llm}" +exec python3 "$ROOT/docs/plan_pruebas/scripts/st_ext_01_arq_bench.py" "$@" diff --git a/docs/plan_pruebas/scripts/st_ext_02_llm_bench.ps1 b/docs/plan_pruebas/scripts/st_ext_02_llm_bench.ps1 new file mode 100644 index 0000000..4c83cdc --- /dev/null +++ b/docs/plan_pruebas/scripts/st_ext_02_llm_bench.ps1 @@ -0,0 +1,27 @@ +# ST-EXT-02 — bench LLM con schema/campos congelados (Windows). +# Mismo schema para todos los PDFs; reutiliza st_ext_02_schema.json en re-runs. +param( + [string]$BaseUrl = "http://127.0.0.1:8189", + [string]$DataDir = "", + [string]$OutCsv = "", + [string]$SchemaFile = "", + [int]$Limit = 0, + [string]$Model = "gpt-5.4-mini", + [switch]$Rediscover +) + +$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..") +$py = Join-Path $Root "docs\plan_pruebas\scripts\st_ext_02_llm_bench.py" + +$argsList = @("--base-url", $BaseUrl, "--model", $Model) +if ($DataDir) { $argsList += @("--data-dir", $DataDir) } +if ($OutCsv) { $argsList += @("--out-csv", $OutCsv) } +if ($SchemaFile) { $argsList += @("--schema-file", $SchemaFile) } +if ($Limit -gt 0) { $argsList += @("--limit", "$Limit") } +if ($Rediscover) { $argsList += "--rediscover" } + +$env:LLM_BENCH_COST_PER_DOC = if ($env:LLM_BENCH_COST_PER_DOC) { $env:LLM_BENCH_COST_PER_DOC } else { "0.0257" } + +Write-Host "ST-EXT-02 → python $py $($argsList -join ' ')" +python $py @argsList +exit $LASTEXITCODE diff --git a/docs/plan_pruebas/scripts/st_ext_02_llm_bench.py b/docs/plan_pruebas/scripts/st_ext_02_llm_bench.py new file mode 100644 index 0000000..b6cc9cc --- /dev/null +++ b/docs/plan_pruebas/scripts/st_ext_02_llm_bench.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python3 +"""ST-EXT-02 — Bench de latencia LLM con el mismo schema/campos para todos los PDFs. + +Garantiza comparabilidad de latencia y coste: + - Descubre schema UNA vez (o carga un JSON congelado). + - Congela fields + alias_map en informes/st_ext_02_schema.json. + - Usa exactamente esos campos en la tarea y en cada ingest. + - Modelo por defecto: gpt-5.4-mini. + - Estima coste @ USD 0.0257/doc (referencia 100k que pasan conteo). + +Flujo: + 1. POST /extraction/schemas/upload (solo si no hay schema congelado) + 2. Guarda/carga schema JSON (mismos campos) + 3. POST /tasks/extraction (documents[], fields fijos) + 4. Por cada PDF: POST /tasks/{id}/ingest → wall_s + +Ejemplos: + python docs/plan_pruebas/scripts/st_ext_02_llm_bench.py + python docs/plan_pruebas/scripts/st_ext_02_llm_bench.py --limit 10 + python docs/plan_pruebas/scripts/st_ext_02_llm_bench.py --schema-file docs/plan_pruebas/informes/st_ext_02_schema.json + LLM_BENCH_LIMIT=5 .\\docs\\plan_pruebas\\scripts\\st_ext_02_llm_bench.ps1 +""" +from __future__ import annotations + +import argparse +import csv +import json +import os +import statistics +import sys +import time +import urllib.parse +from datetime import date, datetime, timezone +from pathlib import Path + +try: + import requests +except ImportError: + print("Requiere 'requests': pip install requests", file=sys.stderr) + sys.exit(1) + +ROOT = Path(__file__).resolve().parents[3] +DEFAULT_DATA = ROOT / "docs" / "plan_pruebas" / "data" +DEFAULT_INFORMES = ROOT / "docs" / "plan_pruebas" / "informes" +DEFAULT_SCHEMA = DEFAULT_INFORMES / "st_ext_02_schema.json" +COST_PER_DOC_USD = float(os.environ.get("LLM_BENCH_COST_PER_DOC", "0.0257")) +DEFAULT_MODEL = os.environ.get("LLM_BENCH_MODEL") or "gpt-5.4-mini" + + +def percentile(sorted_vals: list[float], p: float) -> float: + if not sorted_vals: + return float("nan") + if len(sorted_vals) == 1: + return sorted_vals[0] + k = (len(sorted_vals) - 1) * (p / 100.0) + f = int(k) + c = min(f + 1, len(sorted_vals) - 1) + if f == c: + return sorted_vals[f] + return sorted_vals[f] + (sorted_vals[c] - sorted_vals[f]) * (k - f) + + +def list_pdfs(data_dir: Path, limit: int) -> list[Path]: + if not data_dir.is_dir(): + raise SystemExit(f"No existe DATA_DIR={data_dir}") + pdfs = sorted(data_dir.glob("*.pdf")) + sorted(data_dir.glob("*.PDF")) + seen: set[str] = set() + unique: list[Path] = [] + for p in pdfs: + key = p.name.lower() + if key in seen: + continue + seen.add(key) + unique.append(p) + if not unique: + raise SystemExit(f"No hay PDFs en {data_dir}") + if limit > 0: + unique = unique[:limit] + return unique + + +def flatten_schema_payload(payload: dict) -> dict[str, str]: + schema = payload.get("schema") or {} + if schema: + return {str(k): str(v) for k, v in schema.items()} + flat: dict[str, str] = {} + for type_name, fields in (payload.get("schema_by_type") or {}).items(): + for path, kind in (fields or {}).items(): + flat[f"{type_name}/{path}"] = str(kind) + return flat + + +def discover_schema(base_url: str, sample: Path, structure_mode: str, max_fields: int) -> dict: + print(f"[ST-EXT-02] Descubriendo schema con muestra: {sample.name}") + with sample.open("rb") as fh: + r = requests.post( + f"{base_url}/extraction/schemas/upload", + files={"file": (sample.name, fh, "application/pdf")}, + data={"structure_mode": structure_mode}, + timeout=180, + ) + if r.status_code >= 400: + raise SystemExit(f"extraction/schemas/upload HTTP {r.status_code}: {r.text[:500]}") + payload = r.json() + schema_map = flatten_schema_payload(payload) + fields = sorted(schema_map.keys()) + if not fields: + raise SystemExit(f"Schema vacío: {json.dumps(payload)[:400]}") + if max_fields > 0: + fields = fields[:max_fields] + schema_map = {k: schema_map[k] for k in fields} + freeze = { + "version": 1, + "scenario": "ST-EXT-02", + "created_at": datetime.now(timezone.utc).isoformat(), + "sample_file": sample.name, + "structure_mode": structure_mode, + "model_hint": DEFAULT_MODEL, + "cost_per_doc_usd": COST_PER_DOC_USD, + "fields": fields, + "alias_map": {f: f.split("/")[-1] for f in fields}, + "schema": schema_map, + } + print(f" Campos congelados ({len(fields)}): {', '.join(fields[:8])}{'…' if len(fields) > 8 else ''}") + return freeze + + +def load_schema(path: Path) -> dict: + data = json.loads(path.read_text(encoding="utf-8")) + fields = list(data.get("fields") or []) + if not fields: + raise SystemExit(f"Schema sin fields: {path}") + if "alias_map" not in data: + data["alias_map"] = {f: f.split("/")[-1] for f in fields} + print(f"[ST-EXT-02] Schema cargado: {path} ({len(fields)} campos, mismos para todos los PDFs)") + print(f" Campos: {', '.join(fields[:8])}{'…' if len(fields) > 8 else ''}") + return data + + +def save_schema(path: Path, freeze: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(freeze, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"[ST-EXT-02] Schema guardado → {path}") + + +def resolve_schema( + *, + base_url: str, + pdfs: list[Path], + schema_file: Path, + structure_mode: str, + max_fields: int, + rediscover: bool, +) -> dict: + if schema_file.is_file() and not rediscover: + return load_schema(schema_file) + freeze = discover_schema(base_url, pdfs[0], structure_mode, max_fields) + save_schema(schema_file, freeze) + return freeze + + +def create_task( + base_url: str, + pdfs: list[Path], + freeze: dict, + model: str | None, +) -> str: + fields = list(freeze["fields"]) + alias_map = dict(freeze.get("alias_map") or {f: f.split("/")[-1] for f in fields}) + structure_mode = str(freeze.get("structure_mode") or "general") + documents = [{"radicado": p.stem, "filename": p.name, "doc_type": None} for p in pdfs] + body = { + "source": "desktop", + "fields": fields, + "alias_map": alias_map, + "structure_mode": structure_mode, + "retry_on_empty": False, + "documents": documents, + "document_count": len(documents), + "model": model, + } + r = requests.post(f"{base_url}/tasks/extraction", json=body, timeout=60) + if r.status_code >= 400: + raise SystemExit(f"tasks/extraction HTTP {r.status_code}: {r.text[:500]}") + task = r.json() + task_id = task.get("id") + if not task_id: + raise SystemExit(f"Sin task id: {task}") + print(f"[ST-EXT-02] Tarea {task_id} — {len(pdfs)} docs, {len(fields)} campos fijos, model={model}") + return str(task_id) + + +def task_processed(base_url: str, task_id: str) -> int: + r = requests.get(f"{base_url}/tasks/{task_id}", timeout=30) + r.raise_for_status() + result = r.json().get("result") or {} + return int(result.get("processed") or 0) + + +def wait_processed( + base_url: str, + task_id: str, + before: int, + poll_timeout_s: float, + poll_interval_s: float, +) -> tuple[str, int]: + deadline = time.time() + poll_timeout_s + processed = before + while time.time() < deadline: + r = requests.get(f"{base_url}/tasks/{task_id}", timeout=30) + if r.status_code >= 400: + return f"poll_http_{r.status_code}", processed + data = r.json() + status = str(data.get("status") or "unknown") + result = data.get("result") or {} + processed = int(result.get("processed") or 0) + errors = result.get("errors") or [] + if processed > before: + return "ok", processed + if status in ("failed", "cancelled"): + err = errors[-1] if errors else status + return f"task_{err}", processed + time.sleep(poll_interval_s) + return "timeout", processed + + +def ingest_one( + base_url: str, + task_id: str, + pdf: Path, + poll_timeout_s: float, + poll_interval_s: float, +) -> dict: + before = task_processed(base_url, task_id) + t0 = time.perf_counter() + http_code = 0 + status = "error" + queued = False + err_msg = "" + try: + with pdf.open("rb") as fh: + r = requests.post( + f"{base_url}/tasks/{urllib.parse.quote(task_id)}/ingest", + files={"file": (pdf.name, fh, "application/pdf")}, + data={"radicado": pdf.stem, "filename": pdf.name}, + timeout=poll_timeout_s + 30, + ) + http_code = r.status_code + try: + body = r.json() + except Exception: + body = {} + if r.status_code >= 400: + status = f"http_{r.status_code}" + err_msg = (r.text or "")[:200].replace("\n", " ") + elif body.get("queued"): + queued = True + status, _ = wait_processed(base_url, task_id, before, poll_timeout_s, poll_interval_s) + else: + task = body.get("task") or body + result = (task.get("result") if isinstance(task, dict) else None) or {} + processed = int(result.get("processed") or 0) + status = "ok" if (processed > before or r.status_code < 300) else "no_progress" + errors = result.get("errors") or [] + if errors: + err_msg = str(errors[-1])[:200] + except requests.Timeout: + status = "timeout" + except Exception as ex: + status = "exception" + err_msg = str(ex)[:200] + + wall_s = round(time.perf_counter() - t0, 3) + cost_est = round(COST_PER_DOC_USD, 6) if status == "ok" else "" + return { + "file": pdf.name, + "wall_s": wall_s, + "http_code": http_code, + "status": status, + "queued": "1" if queued else "0", + "cost_usd_est": cost_est, + "error": err_msg, + } + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="ST-EXT-02 LLM bench (mismo schema/campos)") + p.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8189")) + p.add_argument("--data-dir", type=Path, default=Path(os.environ.get("LLM_BENCH_PDF_DIR") or DEFAULT_DATA)) + p.add_argument( + "--out-csv", + type=Path, + default=Path( + os.environ.get("LLM_LATENCY_CSV") + or (DEFAULT_INFORMES / f"llm_latency_{date.today().strftime('%Y%m%d')}.csv") + ), + ) + p.add_argument( + "--schema-file", + type=Path, + default=Path(os.environ.get("LLM_BENCH_SCHEMA_FILE") or DEFAULT_SCHEMA), + help="JSON congelado con fields (mismo schema para toda la corrida y re-runs)", + ) + p.add_argument("--rediscover", action="store_true", help="Ignora schema congelado y vuelve a descubrir") + p.add_argument("--limit", type=int, default=int(os.environ.get("LLM_BENCH_LIMIT") or "0")) + p.add_argument("--max-fields", type=int, default=int(os.environ.get("LLM_BENCH_MAX_FIELDS") or "20")) + p.add_argument("--model", default=DEFAULT_MODEL) + p.add_argument("--structure-mode", default=os.environ.get("LLM_BENCH_STRUCTURE_MODE") or "general") + p.add_argument("--poll-timeout", type=float, default=float(os.environ.get("LLM_BENCH_POLL_TIMEOUT_S") or "300")) + p.add_argument("--poll-interval", type=float, default=float(os.environ.get("LLM_BENCH_POLL_INTERVAL_S") or "1.5")) + return p.parse_args() + + +def main() -> int: + args = parse_args() + base_url = str(args.base_url).rstrip("/") + + try: + h = requests.get(f"{base_url}/health/live", timeout=5) + print(f"health/live → {h.status_code}") + except Exception as ex: + print(f"AVISO: no responde {base_url}/health/live ({ex})", file=sys.stderr) + + pdfs = list_pdfs(args.data_dir, args.limit) + print(f"BASE_URL={base_url}") + print(f"DATA_DIR={args.data_dir}") + print(f"OUT={args.out_csv}") + print(f"SCHEMA={args.schema_file}") + print(f"PDFs={len(pdfs)} MODEL={args.model} COST/doc=${COST_PER_DOC_USD}") + print() + + freeze = resolve_schema( + base_url=base_url, + pdfs=pdfs, + schema_file=args.schema_file, + structure_mode=args.structure_mode, + max_fields=args.max_fields, + rediscover=args.rediscover, + ) + # Asegurar que todos los PDFs usan exactamente freeze["fields"] + task_id = create_task(base_url, pdfs, freeze, args.model) + + args.out_csv.parent.mkdir(parents=True, exist_ok=True) + fieldnames = ["file", "wall_s", "http_code", "status", "queued", "cost_usd_est", "error"] + rows: list[dict] = [] + with args.out_csv.open("w", encoding="utf-8", newline="") as fh: + w = csv.DictWriter(fh, fieldnames=fieldnames) + w.writeheader() + for i, pdf in enumerate(pdfs, 1): + print(f"[{i}/{len(pdfs)}] {pdf.name} …", flush=True) + row = ingest_one(base_url, task_id, pdf, args.poll_timeout, args.poll_interval) + rows.append(row) + w.writerow(row) + fh.flush() + print( + f" wall_s={row['wall_s']} code={row['http_code']} status={row['status']} " + f"cost_est={row['cost_usd_est']}", + flush=True, + ) + + ok_times = sorted(float(r["wall_s"]) for r in rows if r["status"] == "ok") + n_ok = len(ok_times) + n_fail = len(rows) - n_ok + cost_run = round(n_ok * COST_PER_DOC_USD, 4) + cost_100k = round(100_000 * COST_PER_DOC_USD, 2) + + meta = { + "scenario": "ST-EXT-02", + "task_id": task_id, + "model": args.model, + "schema_file": str(args.schema_file), + "fields": freeze.get("fields"), + "n_fields": len(freeze.get("fields") or []), + "n_ok": n_ok, + "n_fail": n_fail, + "cost_per_doc_usd": COST_PER_DOC_USD, + "cost_run_usd": cost_run, + "cost_100k_usd": cost_100k, + "csv": str(args.out_csv), + "finished_at": datetime.now(timezone.utc).isoformat(), + } + if ok_times: + meta["wall_s_avg"] = round(statistics.mean(ok_times), 3) + meta["wall_s_p50"] = round(percentile(ok_times, 50), 3) + meta["wall_s_p95"] = round(percentile(ok_times, 95), 3) + meta["throughput_1w_per_h"] = round(3600 / meta["wall_s_p50"], 1) if meta["wall_s_p50"] else None + + meta_path = args.out_csv.with_suffix(".summary.json") + meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + print() + print("=== Resumen ST-EXT-02 (mismo schema/campos) ===") + print(f"CSV: {args.out_csv}") + print(f"Schema: {args.schema_file} ({meta['n_fields']} campos fijos)") + print(f"Summary: {meta_path}") + print(f"task_id: {task_id}") + print(f"model: {args.model}") + print(f"ok={n_ok} fail={n_fail} total={len(rows)}") + if ok_times: + print( + f"wall_s avg={meta['wall_s_avg']:.3f} p50={meta['wall_s_p50']:.3f} " + f"p95={meta['wall_s_p95']:.3f} min={ok_times[0]:.3f} max={ok_times[-1]:.3f}" + ) + if meta.get("throughput_1w_per_h"): + print(f"Throughput 1 worker ≈ {meta['throughput_1w_per_h']:.0f} fact/h (p50)") + print(f"Throughput 2 workers ≈ {2 * meta['throughput_1w_per_h']:.0f} fact/h (sin cuota)") + else: + print("Sin muestras ok — revisa API key, workers y logs de extraction.") + print(f"Coste corrida (ok×{COST_PER_DOC_USD}): USD {cost_run}") + print(f"Coste 100k que pasan conteo: USD {cost_100k}") + print() + print("PENDIENTE docs: volcar p50/p95 en informe/sections/13_llm_latency.tex") + print(" y fila LLM en sections/08_capacidad_100k.tex") + return 0 if n_ok > 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh b/docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh new file mode 100644 index 0000000..1cdaa9f --- /dev/null +++ b/docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# ST-EXT-02 — bench LLM con schema/campos congelados (gpt-5.4-mini por defecto). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +export BASE_URL="${BASE_URL:-http://127.0.0.1:8189}" +export LLM_BENCH_MODEL="${LLM_BENCH_MODEL:-gpt-5.4-mini}" +export LLM_BENCH_COST_PER_DOC="${LLM_BENCH_COST_PER_DOC:-0.0257}" +exec python3 "$ROOT/docs/plan_pruebas/scripts/st_ext_02_llm_bench.py" "$@" diff --git a/docs/plan_pruebas/sections/02_arquitectura.tex b/docs/plan_pruebas/sections/02_arquitectura.tex index 8766bfe..5dee9d9 100644 --- a/docs/plan_pruebas/sections/02_arquitectura.tex +++ b/docs/plan_pruebas/sections/02_arquitectura.tex @@ -1,11 +1,48 @@ \chapter{Arquitectura bajo prueba} \label{ch:arquitectura} -\section{Vista de componentes} -El host solo expone el Gateway en el puerto \textbf{8189}. El resto de APIs viven en la red Docker. El agent del bot corre en PCs externas y se comunica por HTTP + PutObject a MinIO. +\section{C\'omo est\'a dise\~nado el sistema} +IPSExtractor sigue una arquitectura de \textbf{microservicios} detrás de un +\textbf{API Gateway}. El host solo expone el puerto \textbf{8189}; el resto vive +en la red Docker. Los agents Selenium corren en PCs externas (HTTP + PutObject a MinIO). + +\noindent\textbf{Nombres:} \texttt{bot-descarga} = orquestador (API); \textbf{agent Selenium} = PC que descarga. +ST-BOT-* mide el orquestador; $\sim$422 fact/h es del agent. \input{diagrams/arquitectura} +\section{Patrones de dise\~no} +\begin{description}[style=nextline,leftmargin=0.8cm,labelwidth=3.2cm] + \item[API Gateway] + \'Unica puerta p\'ublica. Proxy a extraction, cleaning, count-api y bot-descarga. + Centraliza CORS, health y timeouts (JSON 30\,s; uploads del orquestador 600\,s). + \emph{Implicaci\'on de prueba:} ST-GW-* miden el gateway, no el OCR ni el LLM. + \item[Bounded contexts] + Cada servicio tiene un dominio claro: OCR (count-api), LLM (extraction+workers), + orquestaci\'on de descarga (\texttt{bot-descarga}), limpieza CSV (cleaning). + \emph{Implicaci\'on:} fallos y capacidad se prueban por servicio, no solo E2E. + \item[Cola as\'incrona (ARQ)] + Con \texttt{ASYNC\_QUEUE=true}, el ingest encola en Redis y los workers drenan. + Con \texttt{false} (default), el API procesa en el mismo request (sync). + \emph{Implicaci\'on:} ST-EXT-01 exige async + workers; ST-EXT-02 puede correr sync. + \item[Claim / lease] + Un job \texttt{pending} solo lo toma un agent Selenium (claim at\'omico v\'ia \texttt{bot-descarga}). Heartbeat + mantiene al worker online; sin heartbeat, no se asignan jobs. + \emph{Implicaci\'on:} ST-BOT-01/02 miden el orquestador (heartbeats y contenci\'on de claim). +\end{description} + +\input{diagrams/patrones} + +\section{Pipeline de negocio} +\input{diagrams/pipeline_negocio} + +\begin{enumerate} + \item \textbf{Descarga:} agent Selenium obtiene PDFs $\rightarrow$ MinIO. + \item \textbf{Conteo:} OCR/ONNX valida el documento (filtro de calidad). + \item \textbf{Extracci\'on:} LLM rellena campos $\rightarrow$ PostgreSQL. + \item \textbf{Limpieza:} reglas/CSV sobre el lote. +\end{enumerate} + \section{Inventario de componentes} \begin{table}[ht] \centering @@ -14,35 +51,35 @@ El host solo expone el Gateway en el puerto \textbf{8189}. El resto de APIs vive \textbf{Componente} & \textbf{Puerto} & \textbf{Responsabilidad} \\ \midrule \texttt{api\_gateway} & Host 8189 & Proxy HTTP, health agregado, CORS \\ -\texttt{extraction} & 8001 & Jobs/tasks, LLM, ingest PDFs \\ +\texttt{extraction} & 8001 & Tasks, LLM, ingest PDFs \\ \texttt{worker} (ARQ) & --- & Procesamiento async v\'ia Redis \\ \texttt{cleaning} & 8002 & Limpieza CSV / reglas \\ \texttt{count-api} & 8080 & OCR + ONNX, stateless \\ -\texttt{bot-descarga} & 8200 & Control remoto de bots \\ +\texttt{bot-descarga} & 8200 & Orquestador: jobs, claim, heartbeats, events \\ PostgreSQL 16 & 5432 & Persistencia compartida \\ Redis 7 & 6379 & Cola ARQ + rate limiter \\ Desktop & local & Cliente Electron/React \\ -Agent bot & local PC & Selenium + MinIO upload \\ -MinIO/S3 & externo & Excel y PDFs del bot \\ +Agent Selenium & local PC & Descarga PDFs (Selenium/Imaging) + MinIO upload \\ +MinIO/S3 & externo & Excel y PDFs del pipeline de descarga \\ \bottomrule \end{tabular} \caption{Componentes del sistema bajo prueba.} \end{table} -\section{Flujo cr\'itico: bot de descarga} +\section{Flujo cr\'itico: descarga (orquestador + agent Selenium)} \input{diagrams/flujo_bot} Secuencia resumida: \begin{enumerate} - \item Agent: \texttt{register} + loop \texttt{heartbeat} / \texttt{claim\_pending} ($\sim$8\,s). + \item Agent Selenium: \texttt{register} + loop \texttt{heartbeat} / \texttt{claim\_pending} ($\sim$8\,s). \item Desktop: \texttt{POST /bot-descarga/jobs} (Excel, \texttt{worker\_id}, \texttt{job\_id}). \item API: valida job padre y worker online; sube Excel a MinIO; estado \texttt{pending}. - \item Agent: claim $\rightarrow$ \texttt{RUNNING}; descarga Excel; Selenium/Imaging; PutObject PDFs. + \item Agent Selenium: claim $\rightarrow$ \texttt{RUNNING}; descarga Excel; Selenium/Imaging; PutObject PDFs. \item Eventos \texttt{POST /events}; cierre \texttt{PATCH /jobs/\{id\}} (\texttt{completed}/\texttt{failed}). \end{enumerate} Layout MinIO: -\begin{lstlisting}[language={},caption={Prefijos MinIO del bot}] +\begin{lstlisting}[language={},caption={Prefijos MinIO del pipeline de descarga}] uploads/{download_job_id}/input.xlsx downloads/{download_job_id}/{nit}/{radicado}.pdf downloads/{download_job_id}/_manifest.json @@ -51,6 +88,6 @@ downloads/{download_job_id}/_manifest.json \section{Otros flujos de negocio} \begin{description} \item[Conteo] Desktop $\rightarrow$ \texttt{POST /count/v1/count} $\rightarrow$ \texttt{POST /count-jobs} $\rightarrow$ validaci\'on humana. - \item[Extracci\'on] \texttt{from-count} o ingest de PDFs $\rightarrow$ workers ARQ $\rightarrow$ filas en BD. + \item[Extracci\'on] \texttt{from-count} o ingest de PDFs $\rightarrow$ (ARQ) workers $\rightarrow$ filas en BD. \item[Limpieza] Job cleaning desde \texttt{extraction\_batch\_id} o CSV upload. \end{description} diff --git a/docs/plan_pruebas/sections/03_estrategia.tex b/docs/plan_pruebas/sections/03_estrategia.tex index 5701876..53aa5e2 100644 --- a/docs/plan_pruebas/sections/03_estrategia.tex +++ b/docs/plan_pruebas/sections/03_estrategia.tex @@ -2,9 +2,13 @@ \label{ch:estrategia} \section{Pir\'amide} +Cada capa responde a una pregunta distinta: correctitud de dominio, +integraci\'on entre servicios, cierre del flujo de negocio, resistencia a carga +y degradaci\'on ante fallos. + \begin{table}[h] \centering -\begin{tabular}{@{}llp{4.5cm}l@{}} +\begin{tabular}{@{}llp{4.2cm}l@{}} \toprule \textbf{Capa} & \textbf{Qu\'e valida} & \textbf{D\'onde} & \textbf{Frecuencia} \\ \midrule @@ -21,13 +25,18 @@ Chaos & Ca\'idas de dependencias & Staging & Mensual \\ \section{Convenci\'on de identificadores} \begin{itemize} - \item \texttt{UT--} --- unitarias - \item \texttt{IT--} --- integraci\'on + \item \texttt{UT--} --- unitarias (riesgo de regresi\'on de dominio) + \item \texttt{IT--} --- integraci\'on (contratos HTTP + infra) \item \texttt{E2E-} --- end-to-end de negocio - \item \texttt{ST--} --- estr\'es / carga + \item \texttt{ST--} --- estr\'es / carga (capacidad y SLO) \item \texttt{CH-} --- chaos / resiliencia \end{itemize} +\section{C\'omo leer un caso del cat\'alogo} +Para cada ID: \textbf{caso} (qu\'e se hace) + \textbf{criterio} (c\'omo pasa) + +\textbf{riesgo cubierto} (por qu\'e importa). Los resultados medidos viven en el +\emph{Informe de Pruebas}; el modelo de volumen en \emph{Capacidad 100k}. + \section{Entornos} \begin{description} \item[local] Mocks de LLM, Imaging y MinIO (moto/LocalStack). @@ -58,6 +67,6 @@ Chrome & Mock & Mock & N/A & Real \\ \begin{enumerate} \item Un caso = un objetivo verificable (assert expl\'icito). \item Datos de prueba versionados (fixtures), no datos de producci\'on. - \item Estr\'es solo en staging aislado; umbrales escritos antes de ejecutar. - \item Fallos externos (LLM/Imaging) se aislan con mocks en CI. + \item Interpretar p95 altos junto con \texttt{free -h} / \texttt{docker stats} en shared8g. + \item Documentar entorno en cada informe de ejecuci\'on. \end{enumerate} diff --git a/docs/plan_pruebas/sections/04_unitarias.tex b/docs/plan_pruebas/sections/04_unitarias.tex index c2294db..dd2cb30 100644 --- a/docs/plan_pruebas/sections/04_unitarias.tex +++ b/docs/plan_pruebas/sections/04_unitarias.tex @@ -1,33 +1,36 @@ \chapter{Pruebas unitarias} \label{ch:unitarias} +Las unitarias cubren \textbf{regresiones de dominio} sin levantar el stack completo. +Un fallo aqu\'i suele ser m\'as barato de corregir que un fallo E2E. + \section{Gateway} -\begin{longtable}{@{}p{2.4cm}p{4.2cm}p{5.5cm}@{}} +\begin{longtable}{@{}p{2.2cm}p{3.5cm}p{4.0cm}p{3.0cm}@{}} \caption{Unitarias gateway.}\\ \toprule -\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} \\ +\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} & \textbf{Riesgo} \\ \midrule \endhead -UT-GW-01 & Clasificaci\'on path upload vs JSON & Timeouts 600\,s vs 30\,s correctos \\ -UT-GW-02 & Health live sin upstream & Respuesta inmediata \texttt{ok} \\ -UT-GW-03 & Health agregado degradado & Si extraction/cleaning fallan $\rightarrow$ \texttt{degraded} \\ +UT-GW-01 & Path upload vs JSON & Timeouts 600\,s vs 30\,s & Uploads cortados \\ +UT-GW-02 & Health live sin upstream & \texttt{ok} inmediato & Orquestaci\'on ciega \\ +UT-GW-03 & Health agregado degradado & Upstream down $\rightarrow$ \texttt{degraded} & Falsos ``todo ok'' \\ \bottomrule \end{longtable} \section{Bot-descarga API (\texttt{JobStore})} -Cobertura existente en \texttt{test\_store.py}; ampliar con: -\begin{longtable}{@{}p{2.6cm}p{4.0cm}p{5.5cm}@{}} +Cobertura en \texttt{test\_store.py}; ampliar con: +\begin{longtable}{@{}p{2.4cm}p{3.4cm}p{3.8cm}p{3.0cm}@{}} \caption{Unitarias bot-descarga API.}\\ \toprule -\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} \\ +\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} & \textbf{Riesgo} \\ \midrule \endhead -UT-BOT-01 & Register + heartbeat & Worker online; offline tras $>$45\,s \\ -UT-BOT-02 & Claim at\'omico & Un solo worker obtiene el job \\ -UT-BOT-03 & Cancel pending & Estado \texttt{cancelled} inmediato \\ -UT-BOT-04 & Cancel running & \texttt{cancel\_requested=true} \\ -UT-BOT-05 & Auth Bearer & Token inv\'alido $\rightarrow$ 401 \\ -UT-BOT-06 & Keys MinIO & Prefijos uploads/downloads correctos \\ +UT-BOT-01 & Register + heartbeat & Online; offline $>$45\,s & Jobs a PCs muertos \\ +UT-BOT-02 & Claim at\'omico & Un solo worker & Doble descarga \\ +UT-BOT-03 & Cancel pending & \texttt{cancelled} & Jobs fantasma \\ +UT-BOT-04 & Cancel running & \texttt{cancel\_requested} & Selenium sin freno \\ +UT-BOT-05 & Auth Bearer & 401 si inv\'alido & Acceso indebido \\ +UT-BOT-06 & Keys MinIO & Prefijos correctos & PDFs perdidos \\ \bottomrule \end{longtable} diff --git a/docs/plan_pruebas/sections/07_estres.tex b/docs/plan_pruebas/sections/07_estres.tex index 64182d5..5a4d48d 100644 --- a/docs/plan_pruebas/sections/07_estres.tex +++ b/docs/plan_pruebas/sections/07_estres.tex @@ -27,25 +27,28 @@ ST-EXT-01 & 20--50 tasks; 1 r\'eplica worker & 100--500 tasks; 2 r\'eplicas \\ Con el zoo Docker actual (MLflow $\sim$1{,}86\,GiB + Airflow + n8n, etc.) \textbf{no} ejecutar ST-COUNT-01 ni ST-EXT agresivo sin liberar memoria primero. \section{Escenarios (cat\'alogo completo)} -\begin{longtable}{@{}p{2.4cm}p{3.2cm}p{3.5cm}p{3.2cm}@{}} -\caption{Escenarios de estr\'es.}\\ +\begin{table}[ht] +\centering +\small +\begin{tabular}{@{}llll@{}} \toprule -\textbf{ID} & \textbf{Objetivo} & \textbf{Carga (laboratorio)} & \textbf{Observar} \\ +\textbf{ID} & \textbf{Objetivo} & \textbf{Carga (lab)} & \textbf{Observar / riesgo} \\ \midrule -\endhead -ST-GW-01 & Gateway concurrente & 50--100 VUs \texttt{/health/live} & p95, 5xx \\ -ST-BOT-01 & Heartbeats masivos & 20--50 workers / 8\,s & PG, offline falsos \\ -ST-BOT-02 & Claim contention & 10 workers, cola jobs & Un claim/job \\ -ST-BOT-03 & Event storm & Hasta 5k events/job & Escritura PG \\ -ST-BOT-04 & Excel grande & 10--50\,MB upload & Timeout 600\,s \\ -ST-MINIO-01 & PutObject paralelo & N PCs & Errores S3 \\ -ST-COUNT-01 & OCR paralelo & 5--20 lotes $\times$10 & RAM, OOM \\ -ST-EXT-01 & Cola ARQ & 100--500 tasks (LLM mock) & Backlog \\ -ST-EXT-02 & LLM real & 20--50 PDFs & 429, costos \\ -ST-DES-01 & Polling desktop & 10 clientes poll & CPU gateway \\ -ST-DB-01 & PG saturaci\'on & Mezcla ST-BOT+EXT & Locks \\ +ST-GW-01 & Gateway concurrente & 50--100 VUs live & p95, 5xx --- orquestaci\'on lenta \\ +ST-BOT-01 & Heartbeats masivos & 20--50 workers & Offline falsos --- PCs muertos \\ +ST-BOT-02 & Claim contention & 10 workers & Un claim/job --- doble descarga \\ +ST-BOT-03 & Event storm & Hasta 5k events & Escritura PG --- perdida eventos \\ +ST-BOT-04 & Excel grande & 10--50\,MB & Timeout 600\,s --- upload cortado \\ +ST-MINIO-01 & PutObject paralelo & N PCs & Errores S3 --- PDFs perdidos \\ +ST-COUNT-01 & OCR paralelo & 5--20 lotes x10 & RAM/OOM --- cuello 100k \\ +ST-EXT-01 & Cola ARQ & 100--500 (mock) & Backlog --- workers insuficientes \\ +ST-EXT-02 & LLM real & 20--50 PDFs & 429, coste --- cuota LLM \\ +ST-DES-01 & Polling desktop & 10 clientes & CPU GW --- UI lenta \\ +ST-DB-01 & PG saturaci\'on & Mezcla BOT+EXT & Locks --- deadlocks \\ \bottomrule -\end{longtable} +\end{tabular} +\caption{Escenarios de estr\'es (objetivo y riesgo).} +\end{table} \section{Scripts k6 incluidos} \begin{table}[h] diff --git a/docs/plan_pruebas/sections/12_informe_2026_07_13.tex b/docs/plan_pruebas/sections/12_informe_2026_07_13.tex index 1638ca9..acfc07a 100644 --- a/docs/plan_pruebas/sections/12_informe_2026_07_13.tex +++ b/docs/plan_pruebas/sections/12_informe_2026_07_13.tex @@ -1,94 +1,42 @@ -\chapter{Informe de ejecuci\'on --- campa\~na 2026-07-13} +\chapter{Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)} \label{ch:informe-2026-07-13} +Este cap\'itulo es un \textbf{resumen de estado} del plan. Las m\'etricas completas, +gr\'aficas e interpretaci\'on est\'an en el \emph{Informe de Pruebas} +(\texttt{InformePruebas\_IPSExtractor.pdf}). El modelo de volumen est\'a en +\emph{Capacidad 100k}. + \section{Metadatos} +Host \texttt{ubuntu} (ASUS X441UAK), Ubuntu 24.04, 8\,GiB, perfil \texttt{shared8g}, +gateway \texttt{http://127.0.0.1:8189}. + +\section{Resultados resumidos} \begin{table}[ht] \centering -\begin{tabular}{@{}lp{9cm}@{}} -\toprule -\textbf{Campo} & \textbf{Valor} \\ -\midrule -Fecha & 13 de julio de 2026 \\ -Host & \texttt{ubuntu} (ASUS X441UAK), Ubuntu 24.04.4 LTS \\ -CPU / RAM & Intel i5-7200U; 7{,}6\,GiB (Docker limit $\approx$ 7{,}64\,GiB) \\ -Perfil k6 & \texttt{shared8g} \\ -Gateway & \texttt{http://127.0.0.1:8189} \\ -Herramienta & k6 (Grafana), Docker 29.1.3 \\ -Rama / repo & \texttt{main} --- IPSExtractor \\ -Ejecutor & erik (host staging compartido) \\ -\bottomrule -\end{tabular} -\caption{Metadatos de la campa\~na.} -\end{table} - -\section{Baseline del entorno} -Medici\'on previa a las cargas: - -\begin{itemize} - \item Memoria: total 7{,}6\,GiB; usados $\approx$4{,}4\,GiB; disponibles $\approx$3{,}3\,GiB; \textbf{swap 2{,}1\,GiB en uso}. - \item Contenedores IPS: todos \texttt{Up} / healthy (api, bot-descarga, extraction, workers, count-api, cleaning, db, redis) --- $\sim$17\,h uptime. - \item Vecino dominante: \texttt{mlflow-server} $\approx$1{,}84\,GiB; tambi\'en n8n, MinIO, postgres compartido, etc. - \item \texttt{GET /health/live}: \texttt{ok}, $\approx$11\,ms. - \item \texttt{GET /health}: \texttt{ok} (extraction, cleaning, counting, bot\_descarga); $\approx$194\,ms. -\end{itemize} - -\section{Resultados por caso} -\begin{table}[ht] -\centering -\begin{tabular}{@{}llp{6.2cm}@{}} +\begin{tabular}{@{}llp{6.5cm}@{}} \toprule \textbf{ID} & \textbf{Resultado} & \textbf{Detalle} \\ \midrule -Smoke health & \textbf{PASS} & Live + agregado OK; servicios healthy \\ -ST-GW-01 & \textbf{PASS} & 6884 req; fail 0\,\%; p95 $=$ 40{,}85\,ms (SLO $<$500\,ms) \\ -ST-BOT-01 (1) & \textbf{PARCIAL} & 100\,\% HTTP 200; p95 $=$ 2{,}28\,s (SLO $<$800\,ms) \textbf{FAIL} latencia; max 23\,s \\ -ST-BOT-01 (2) & \textbf{PASS} & 100\,\% HTTP 200; p95 $=$ 85{,}55\,ms; max 98\,ms \\ +Smoke health & PASS & Live + agregado OK \\ +ST-GW-01 & PASS & p95 40{,}85\,ms; fail 0\,\% \\ +ST-BOT-01 \#1 & PARCIAL & Funcional OK; p95 latencia FAIL (contenci\'on) \\ +ST-BOT-01 \#2 & PASS & p95 85{,}55\,ms tras aliviar host \\ +ST-EXT-02 & PASS & 23/23; p50 7{,}1\,s $\approx$506 fact/h (17-jul) \\ +ST-EXT-01 & PASS & 30/30 async; drain 22{,}5\,s; cola $\approx$4\,5k docs/h mock \\ +ST-COUNT-01 & PASS & 9/9; p50 35{,}8\,s @3 conc.; $\approx$325 fact/h obs. \\ \bottomrule \end{tabular} -\caption{Resumen de la campa\~na 2026-07-13.} +\caption{Resumen; detalle en el Informe de Pruebas.} \label{tab:campana-2026-07-13} \end{table} -\subsection{ST-GW-01 --- gateway health} -\begin{itemize} - \item Script: \texttt{k6\_gateway\_health.js}, \texttt{PROFILE=shared8g}. - \item Duraci\'on: $\approx$2\,min; hasta 25 VUs. - \item Checks: 13768/13768 (100\,\%). - \item \texttt{http\_req\_failed}: 0\,\%; p95 $=$ 40{,}85\,ms; avg $=$ 12{,}55\,ms. - \item Thresholds: todos en verde. -\end{itemize} +\section{Lecci\'on operativa} +Un p95 alto en heartbeats con 100\,\% HTTP 200 suele ser \textbf{contenci\'on del host} +(swap + MLflow), no un bug del claim. Siempre adjuntar \texttt{free -h} y +\texttt{docker stats} al interpretar ST-BOT-*. +ST-EXT-01 confirma que la cola ARQ drena bien con mock; el throughput de cola +no sustituye ST-EXT-02 (LLM real). +ST-COUNT-01 muestra degradaci\'on fuerte del OCR bajo paralelismo (2\,s $\rightarrow$ 36\,s). -\subsection{ST-BOT-01 --- heartbeats (corrida 1)} -\begin{itemize} - \item Script: \texttt{k6\_bot\_heartbeats.js}, 5 VUs, 2\,min, token \texttt{dev-token-change-me}. - \item Checks: 75/75 (100\,\%) --- todos los heartbeats HTTP 200. - \item Latencia: avg 511\,ms; med 57\,ms; p95 2{,}28\,s; max 23{,}12\,s. - \item Threshold \texttt{p(95)<800}: \textbf{cruzado} (k6 exit con error de thresholds). - \item Interpretaci\'on: API correcta; picos atribuibles a contenci\'on del host (swap + MLflow/HDD), no a error de negocio. -\end{itemize} - -\subsection{ST-BOT-01 --- heartbeats (corrida 2, reintento)} -\begin{itemize} - \item Mismo script y perfil, tras aliviar presi\'on del host. - \item Checks: 75/75 (100\,\%). - \item Latencia: avg 57\,ms; med 56\,ms; p95 85{,}55\,ms; max 98\,ms. - \item Thresholds: todos en verde. - \item Conclusi\'on: el FAIL de latencia de la corrida 1 \textbf{no se reproduce} con menos contenci\'on; confirma causa ambiental. -\end{itemize} - -\section{Hallazgos} -\begin{enumerate} - \item IPSExtractor (gateway + bot-descarga heartbeat) cumple SLO funcional y de latencia del perfil \texttt{shared8g} cuando el host no est\'a saturado. - \item Con swap activo ($\sim$2\,GiB) y MLflow $\sim$1{,}8\,GiB, pueden aparecer outliers de varios segundos en endpoints ligeros; deben reportarse como riesgo de \emph{plataforma compartida}, no como regresi\'on de c\'odigo sin evidencia adicional. - \item El health agregado es usable ($\sim$200\,ms en esta sesi\'on); no usarlo como SLO de liveness (usar \texttt{/health/live}). -\end{enumerate} - -\section{No ejecutado en esta campa\~na} -ST-BOT-03 (events), ST-COUNT-01, ST-EXT-*, E2E Imaging, chaos. Motivo: swap/contenci\'on residual; se aplazan a una ventana con $\geq$3\,GiB libres o MLflow/Airflow detenidos. - -\section{Follow-ups} -\begin{itemize} - \item Repetir ST-BOT-01 peri\'odicamente; si p95 $>$800\,ms, adjuntar \texttt{free -h} y \texttt{docker stats}. - \item Ventana dedicada (parar MLflow) antes de ST-COUNT-01. - \item Opcional: excluir requests de \texttt{setup()} del threshold de latencia en k6 (registros de workers) para no mezclar cold-start con steady-state. -\end{itemize} +\section{Pendiente respecto a este plan} +ST-BOT-03, ST-COUNT-01 perfil lab, ST-EXT-01 perfil lab (100--500), E2E Imaging/VPN, chaos. diff --git a/services/bot_descarga/api/main.py b/services/bot_descarga/api/main.py index 53a212f..7800d14 100644 --- a/services/bot_descarga/api/main.py +++ b/services/bot_descarga/api/main.py @@ -182,6 +182,43 @@ def list_job_files(download_job_id: str, _: str = Depends(require_bearer)) -> di raise HTTPException(status_code=502, detail=f"Error listando MinIO: {e}") from e +@app.get("/jobs/{download_job_id}/object") +def get_job_object( + download_job_id: str, + key: str = Query(..., min_length=1, description="Object key bajo downloads_prefix"), + _: str = Depends(require_bearer), +): + """Stream de un PDF del resultado MinIO (solo keys del prefijo del job).""" + job = store.get_job(download_job_id) + if not job: + raise HTTPException(status_code=404, detail="Job no encontrado") + + prefix = (job.downloads_prefix or f"downloads/{download_job_id}/").lstrip("/") + if not prefix.endswith("/"): + prefix += "/" + normalized = key.lstrip("/") + if not normalized.startswith(prefix): + raise HTTPException(status_code=403, detail="Key fuera del prefijo del job") + if not normalized.lower().endswith(".pdf"): + raise HTTPException(status_code=400, detail="Solo se permiten objetos PDF") + if ".." in normalized.split("/"): + raise HTTPException(status_code=400, detail="Key no permitida") + if normalized.endswith("_manifest.json"): + raise HTTPException(status_code=400, detail="Key no permitida") + + try: + data = minio.get_bytes(normalized) + except Exception as e: + raise HTTPException(status_code=502, detail=f"Error leyendo MinIO: {e}") from e + + filename = Path(normalized).name + return Response( + content=data, + media_type="application/pdf", + headers={"Content-Disposition": f'inline; filename="{filename}"'}, + ) + + @app.get("/jobs/{download_job_id}/events") def get_events( download_job_id: str, diff --git a/services/bot_descarga/infrastructure/storage_minio.py b/services/bot_descarga/infrastructure/storage_minio.py index 3cd2720..7471c21 100644 --- a/services/bot_descarga/infrastructure/storage_minio.py +++ b/services/bot_descarga/infrastructure/storage_minio.py @@ -190,24 +190,42 @@ class MinioStorage: return key def list_download_files(self, job_id: str, prefix: str | None = None) -> dict: - """Lista PDFs de una corrida (manifest o list_keys).""" + """Lista PDFs de una corrida (manifest o list_keys). + + Si el manifest falta, falla o queda vacío, hace fallback a list_keys + bajo el prefijo (cubre varias carpetas NIT planas). + """ prefix = prefix or downloads_prefix(job_id) files: List[dict] = [] try: raw = self.get_bytes(manifest_key(job_id)) payload = json.loads(raw.decode("utf-8")) for key in payload.get("files") or []: - files.append(self._file_entry(str(key))) + entry = self._file_entry(str(key)) + if entry is not None: + files.append(entry) except Exception: - for key in self.list_keys(prefix): - if key.endswith("/_manifest.json") or key.endswith("_manifest.json"): - continue - files.append(self._file_entry(key)) + files = [] + + if not files: + files = self._list_pdf_entries(prefix) return {"job_id": job_id, "prefix": prefix, "files": files, "count": len(files)} + def _list_pdf_entries(self, prefix: str) -> List[dict]: + files: List[dict] = [] + for key in self.list_keys(prefix): + entry = self._file_entry(key) + if entry is not None: + files.append(entry) + return files + @staticmethod - def _file_entry(key: str) -> dict: + def _file_entry(key: str) -> dict | None: # downloads/{job_id}/{nit}/{radicado}.pdf + if key.endswith("/_manifest.json") or key.endswith("_manifest.json"): + return None + if not key.lower().endswith(".pdf"): + return None parts = key.strip("/").split("/") nit = parts[-2] if len(parts) >= 3 else None name = parts[-1] if parts else key diff --git a/services/bot_descarga/tests/test_list_download_files.py b/services/bot_descarga/tests/test_list_download_files.py new file mode 100644 index 0000000..994a153 --- /dev/null +++ b/services/bot_descarga/tests/test_list_download_files.py @@ -0,0 +1,38 @@ +"""Tests for MinIO list_download_files fallback on empty manifest.""" +from __future__ import annotations + +from unittest.mock import MagicMock + +from infrastructure.storage_minio import MinioStorage + + +def test_list_download_files_falls_back_when_manifest_empty(monkeypatch): + storage = MinioStorage() + storage.get_bytes = MagicMock( + return_value=b'{"job_id": "j1", "files": [], "count": 0}' + ) + storage.list_keys = MagicMock( + return_value=[ + "downloads/j1/_manifest.json", + "downloads/j1/900808423/a.pdf", + "downloads/j1/900884937/b.pdf", + "downloads/j1/901679492/c.pdf", + ] + ) + result = storage.list_download_files("j1") + assert result["count"] == 3 + nits = {f["nit"] for f in result["files"]} + assert nits == {"900808423", "900884937", "901679492"} + + +def test_list_download_files_falls_back_when_manifest_missing(monkeypatch): + storage = MinioStorage() + storage.get_bytes = MagicMock(side_effect=Exception("NoSuchKey")) + storage.list_keys = MagicMock( + return_value=[ + "downloads/j1/900808423/a.pdf", + ] + ) + result = storage.list_download_files("j1") + assert result["count"] == 1 + assert result["files"][0]["radicado"] == "a" diff --git a/services/db/alembic/versions/0005_count_job_files_nit_object_key.py b/services/db/alembic/versions/0005_count_job_files_nit_object_key.py new file mode 100644 index 0000000..dfa73ba --- /dev/null +++ b/services/db/alembic/versions/0005_count_job_files_nit_object_key.py @@ -0,0 +1,45 @@ +"""Add nit and object_key to count_job_files for MinIO download bridge. + +Revision ID: 0005 +Revises: 0004 +Create Date: 2026-07-17 + +""" +from __future__ import annotations + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "0005" +down_revision: Union[str, None] = "0004" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _column_exists(conn: sa.engine.Connection, table: str, column: str) -> bool: + cols = {c["name"] for c in sa.inspect(conn).get_columns(table)} + return column in cols + + +def upgrade() -> None: + conn = op.get_bind() + if not sa.inspect(conn).has_table("count_job_files"): + return + if not _column_exists(conn, "count_job_files", "nit"): + op.add_column("count_job_files", sa.Column("nit", sa.String(255), nullable=True)) + op.create_index("ix_count_job_files_nit", "count_job_files", ["nit"]) + if not _column_exists(conn, "count_job_files", "object_key"): + op.add_column("count_job_files", sa.Column("object_key", sa.Text(), nullable=True)) + + +def downgrade() -> None: + conn = op.get_bind() + if not sa.inspect(conn).has_table("count_job_files"): + return + if _column_exists(conn, "count_job_files", "object_key"): + op.drop_column("count_job_files", "object_key") + if _column_exists(conn, "count_job_files", "nit"): + op.drop_index("ix_count_job_files_nit", table_name="count_job_files") + op.drop_column("count_job_files", "nit") diff --git a/services/desktop_app/src/renderer/components/CountResultsTable.tsx b/services/desktop_app/src/renderer/components/CountResultsTable.tsx index b385cfc..9acb17a 100644 --- a/services/desktop_app/src/renderer/components/CountResultsTable.tsx +++ b/services/desktop_app/src/renderer/components/CountResultsTable.tsx @@ -8,7 +8,7 @@ import { Search, XCircle, } from "lucide-react"; -import { useMemo, useRef, useState } from "react"; +import { Fragment, useMemo, useRef, useState } from "react"; import type { CountJobFile } from "../../shared/types"; interface Props { @@ -41,6 +41,7 @@ export function CountResultsTable({ const [search, setSearch] = useState(""); const [expanded, setExpanded] = useState>(new Set()); const [toggling, setToggling] = useState>(new Set()); + const [collapsedNits, setCollapsedNits] = useState>(new Set()); const tagInputRef = useRef(null); const summary = useMemo(() => { @@ -74,11 +75,47 @@ export function CountResultsTable({ else if (filter === "errors") list = list.filter((f) => f.status === "error"); const q = search.trim().toLowerCase(); - if (q) list = list.filter((f) => f.filename.toLowerCase().includes(q) || (f.radicado ?? "").toLowerCase().includes(q)); + if (q) { + list = list.filter( + (f) => + f.filename.toLowerCase().includes(q) || + (f.radicado ?? "").toLowerCase().includes(q) || + (f.nit ?? "").toLowerCase().includes(q), + ); + } return list; }, [files, filter, search, validCounts]); + const nitGroups = useMemo(() => { + const hasNit = files.some((f) => f.nit); + if (!hasNit) return null; + const map = new Map(); + for (const f of filtered) { + const nit = (f.nit || "").trim() || "Sin NIT"; + const list = map.get(nit) ?? []; + list.push(f); + map.set(nit, list); + } + return [...map.entries()] + .sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true })) + .map(([nit, group]) => ({ nit, files: group })); + }, [files, filtered]); + + const folderCount = useMemo(() => { + const nits = new Set(files.map((f) => (f.nit || "").trim()).filter(Boolean)); + return nits.size; + }, [files]); + + function toggleNitGroup(nit: string) { + setCollapsedNits((prev) => { + const next = new Set(prev); + if (next.has(nit)) next.delete(nit); + else next.add(nit); + return next; + }); + } + function toggleExpand(id: string) { setExpanded((prev) => { const next = new Set(prev); @@ -142,6 +179,12 @@ export function CountResultsTable({ {summary.total} Total + {folderCount > 0 && ( +
+ {folderCount} + Carpetas +
+ )}
{summary.valid} Válidos @@ -157,6 +200,12 @@ export function CountResultsTable({
)} + {folderCount > 0 && ( +

+ {folderCount} carpeta{folderCount === 1 ? "" : "s"} · {summary.total} PDF + {summary.total === 1 ? "" : "s"} +

+ )} {/* ── Filtro por conteo exacto ── */}
@@ -241,7 +290,7 @@ export function CountResultsTable({
0 ? "Buscar archivo o NIT…" : "Buscar archivo…"} value={search} onChange={(e) => setSearch(e.target.value)} /> @@ -275,109 +324,231 @@ export function CountResultsTable({ )} - {filtered.map((f) => { - const st = rowStatus(f); - const isExpanded = expanded.has(f.id); - const hasServices = f.services && f.services.length > 0; - const isToggling = toggling.has(f.id); - const wasAutoAdjusted = autoAdjustedFilenames?.has(f.filename); - - return ( - <> - - -
- {hasServices ? ( + {nitGroups + ? nitGroups.map(({ nit, files: groupFiles }) => { + const collapsed = collapsedNits.has(nit); + const servicesSum = groupFiles.reduce( + (acc, f) => acc + (f.status === "ok" ? f.service_count ?? 0 : 0), + 0, + ); + return ( + + + - ) : ( - - )} -
-
- {f.filename} - {wasAutoAdjusted && ( - + + {!collapsed && + groupFiles.map((f) => { + const st = rowStatus(f); + const isExpanded = expanded.has(f.id); + const hasServices = f.services && f.services.length > 0; + const isToggling = toggling.has(f.id); + const wasAutoAdjusted = autoAdjustedFilenames?.has(f.filename); + return ( + + + +
+ {hasServices ? ( + + ) : ( + + )} +
+
+ {f.filename} + {wasAutoAdjusted && ( + + auto→1 + + )} +
+ {f.radicado && f.radicado !== f.filename && ( + {f.radicado} + )} + {f.status === "error" && f.error && ( + {f.error} + )} +
+
+ + + {f.status === "error" ? ( + — + ) : ( + {f.service_count ?? "?"} + )} + + + {st === "error" && } + {st === "valid" && } + {st === "invalid" && } + + + {f.status !== "error" && ( +
+ + +
+ )} + + + {isExpanded && hasServices && ( + + +
+ {f.services.map((svc, i) => ( +
+ {svc.text} + {(svc.probability * 100).toFixed(0)}% +
+ ))} +
+ + + )} +
+ ); + })} + + ); + }) + : filtered.map((f) => { + const st = rowStatus(f); + const isExpanded = expanded.has(f.id); + const hasServices = f.services && f.services.length > 0; + const isToggling = toggling.has(f.id); + const wasAutoAdjusted = autoAdjustedFilenames?.has(f.filename); + return ( + + + +
+ {hasServices ? ( + + ) : ( + )} -
- {f.radicado && f.radicado !== f.filename && ( - {f.radicado} - )} - {f.status === "error" && f.error && ( - {f.error} - )} -
-
- - - - {f.status === "error" ? ( - — - ) : ( - {f.service_count ?? "?"} - )} - - - - {st === "error" && } - {st === "valid" && } - {st === "invalid" && } - - - - {f.status !== "error" && ( -
- - -
- )} - - - - {isExpanded && hasServices && ( - - -
- {f.services.map((svc, i) => ( -
- {svc.text} - {(svc.probability * 100).toFixed(0)}% +
+
+ {f.filename} + {wasAutoAdjusted && ( + + auto→1 + + )} +
+ {f.radicado && f.radicado !== f.filename && ( + {f.radicado} + )} + {f.status === "error" && f.error && ( + {f.error} + )}
- ))} -
- - - )} - - ); - })} +
+ + + {f.status === "error" ? ( + — + ) : ( + {f.service_count ?? "?"} + )} + + + {st === "error" && } + {st === "valid" && } + {st === "invalid" && } + + + {f.status !== "error" && ( +
+ + +
+ )} + + + {isExpanded && hasServices && ( + + +
+ {f.services.map((svc, i) => ( +
+ {svc.text} + {(svc.probability * 100).toFixed(0)}% +
+ ))} +
+ + + )} +
+ ); + })}
diff --git a/services/desktop_app/src/renderer/components/CountingWizard.tsx b/services/desktop_app/src/renderer/components/CountingWizard.tsx index 31169f4..a2aa869 100644 --- a/services/desktop_app/src/renderer/components/CountingWizard.tsx +++ b/services/desktop_app/src/renderer/components/CountingWizard.tsx @@ -41,6 +41,9 @@ interface Props { onTaskCreated?: (task: Task) => void; /** Callback de progreso en segundo plano, para mostrarlo desde otras secciones. */ onProgress?: (progress: BackgroundProgress) => void; + /** Origen MinIO: corrida de bot de descarga. */ + downloadJobId?: string; + downloadFiles?: Array<{ key: string; nit?: string | null; radicado?: string | null }>; } interface CountProgress { @@ -56,6 +59,32 @@ const BATCH_SIZE = 10; /** Peticiones HTTP simultáneas al count-api. */ const BATCH_CONCURRENCY = 2; +function downloadFilesToManifest( + files: Array<{ key: string; nit?: string | null; radicado?: string | null }>, +): ManifestEntry[] { + return files + .filter((f) => f.key.toLowerCase().endsWith(".pdf")) + .map((f) => { + const filename = f.key.split("/").pop() || f.key; + const radicado = + (f.radicado || "").trim() || + filename.replace(/\.pdf$/i, ""); + return { + radicado, + filename, + localPath: f.key, + size: 0, + nit: f.nit ?? null, + objectKey: f.key, + }; + }) + .sort((a, b) => { + const nitCmp = String(a.nit ?? "").localeCompare(String(b.nit ?? ""), undefined, { numeric: true }); + if (nitCmp !== 0) return nitCmp; + return a.radicado.localeCompare(b.radicado, undefined, { numeric: true }); + }); +} + function formatEta(progress: CountProgress): string | null { if (!progress.startedAt || progress.processed === 0) return null; const elapsed = (Date.now() - progress.startedAt) / 1000; @@ -68,7 +97,18 @@ function formatEta(progress: CountProgress): string | null { return `~${Math.ceil(secs / 60)} min · ${rateStr}`; } -export function CountingWizard({ client, localRoot, containerRoot, job, onStartExtraction, onTaskCreated, onProgress }: Props) { +export function CountingWizard({ + client, + localRoot, + containerRoot, + job, + onStartExtraction, + onTaskCreated, + onProgress, + downloadJobId, + downloadFiles, +}: Props) { + const fromDownload = Boolean(downloadJobId); const [step, setStep] = useState(1); const [maxReached, setMaxReached] = useState(1); const [form, setForm] = useState({ @@ -95,6 +135,7 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE const [error, setError] = useState(null); const [running, setRunning] = useState(false); const abortRef = useRef(false); + const downloadJobIdRef = useRef(downloadJobId); const indexLocalFolder = useCallback( async (dir: string) => { @@ -118,6 +159,33 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE [docTypes], ); + const indexFromDownload = useCallback( + async (jobId: string, seedFiles?: Array<{ key: string; nit?: string | null; radicado?: string | null }>) => { + setIndexing(true); + setError(null); + try { + let files = seedFiles ?? []; + if (files.length === 0) { + const resp = await client.listDownloadJobFiles(jobId); + files = resp.files; + } + const entries = downloadFilesToManifest(files); + setManifest(entries); + if (entries.length === 0) { + setError("No se encontraron PDF en MinIO para esta corrida de descarga."); + } + return entries; + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + setManifest([]); + return []; + } finally { + setIndexing(false); + } + }, + [client], + ); + useEffect(() => { setForm((f) => ({ ...f, @@ -127,13 +195,22 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE }, [localRoot, containerRoot]); useEffect(() => { + downloadJobIdRef.current = downloadJobId; + if (downloadJobId) { + void indexFromDownload(downloadJobId, downloadFiles); + return; + } if (!localRoot) return; let cancelled = false; indexLocalFolder(localRoot).catch(() => { if (!cancelled) setError("Error al indexar la carpeta."); }); - return () => { cancelled = true; }; - }, [localRoot, indexLocalFolder]); + return () => { + cancelled = true; + }; + // Solo reindexar al cambiar de corrida/carpeta; seed files es opcional. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [localRoot, downloadJobId, indexLocalFolder, indexFromDownload]); function goToStep(target: CountingStep) { setStep(target); @@ -154,6 +231,15 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE return norm.startsWith(root) ? container + norm.slice(root.length) : norm; } + async function readEntryBytes(entry: ManifestEntry): Promise { + const jobId = downloadJobIdRef.current; + if (jobId && entry.objectKey) { + return client.fetchDownloadObject(jobId, entry.objectKey); + } + const fileData = await desktopApi.readFile(entry.localPath); + return fileData.data; + } + async function runCounting() { if (!manifest.length) { setError("No hay archivos en la carpeta seleccionada."); @@ -215,7 +301,8 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE running: true, label: `Preparando archivos (${Math.min(batchStart + fi + 1, manifest.length)}/${manifest.length})…`, }); - return desktopApi.readFile(entry.localPath); + const data = await readEntryBytes(entry); + return { filename: entry.filename, data }; }), ); @@ -317,16 +404,27 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE }); try { + const nitCount = new Set(manifest.map((m) => m.nit).filter(Boolean)).size; + const titleBase = fromDownload + ? `Conteo desde descarga${nitCount ? ` · ${nitCount} carpeta${nitCount === 1 ? "" : "s"}` : ""}` + : `Conteo — ${form.localRoot.split(/[\\/]/).pop()}`; const payload: CreateCountJobPayload = { - title: `Conteo — ${form.localRoot.split(/[\\/]/).pop()} (${finalResults.length} archivos)`, + title: `${titleBase} (${finalResults.length} archivos)`, job_id: job?.id, - results: finalResults.map((r) => { - const entry = manifest.find((m) => m.filename === r.filename); + download_job_id: downloadJobIdRef.current ?? null, + source: fromDownload ? "from-download" : "desktop", + results: finalResults.map((r, i) => { + const entry = manifest[i]; return { radicado: entry?.radicado ?? null, filename: r.filename, - file_path: entry ? localToContainer(entry.localPath) : null, + file_path: + fromDownload || !entry + ? entry?.objectKey ?? null + : localToContainer(entry.localPath), doc_type: entry?.docType ?? null, + nit: entry?.nit ?? null, + object_key: entry?.objectKey ?? null, service_count: r.status === "ok" ? r.service_count : null, services: r.services ?? [], status: r.status === "ok" ? "ok" : "error", @@ -377,16 +475,22 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE function handleExportCsv() { if (!savedFiles.length) return; - const headers = ["filename", "radicado", "service_count", "auto_ajustado", "status", "is_valid", "error"]; - const rows = savedFiles.map((f) => [ - f.filename, - f.radicado ?? "", - f.service_count ?? "", - autoAdjustedFilenames.has(f.filename) ? "si" : "", - f.status, - f.is_valid === null ? "" : String(f.is_valid), - f.error ?? "", - ]); + const hasNit = savedFiles.some((f) => f.nit); + const headers = hasNit + ? ["nit", "filename", "radicado", "service_count", "auto_ajustado", "status", "is_valid", "error"] + : ["filename", "radicado", "service_count", "auto_ajustado", "status", "is_valid", "error"]; + const rows = savedFiles.map((f) => { + const base = [ + f.filename, + f.radicado ?? "", + f.service_count ?? "", + autoAdjustedFilenames.has(f.filename) ? "si" : "", + f.status, + f.is_valid === null ? "" : String(f.is_valid), + f.error ?? "", + ]; + return hasNit ? [f.nit ?? "", ...base] : base; + }); downloadCsv(buildExportCsvFilename("conteo", job?.title), toCsv(headers, rows)); } @@ -399,8 +503,10 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE setAutoAdjustedFilenames(new Set()); setError(null); setProgress({ processed: 0, total: 0, failed: 0, currentBatch: "", startedAt: 0 }); - if (form.localRoot) { - indexLocalFolder(form.localRoot); + if (downloadJobIdRef.current) { + void indexFromDownload(downloadJobIdRef.current, downloadFiles); + } else if (form.localRoot) { + void indexLocalFolder(form.localRoot); } } @@ -423,58 +529,94 @@ export function CountingWizard({ client, localRoot, containerRoot, job, onStartE

Origen de las facturas

- Selecciona la carpeta local con los PDF de facturas médicas. - El servicio contará cuántos servicios contiene cada documento usando OCR. + {fromDownload + ? "Los PDF se leen directamente desde MinIO (todas las carpetas NIT de la corrida de descarga)." + : "Selecciona la carpeta local con los PDF de facturas médicas. El servicio contará cuántos servicios contiene cada documento usando OCR."}

-
- -
-
- {form.localRoot ? ( - <> - - {form.localRoot} - - ) : ( - Ninguna carpeta seleccionada - )} + {fromDownload ? ( +
+ +
+
+ + + downloads/{downloadJobId} + +
+
- + {manifest.length > 0 && !indexing && ( +
+ + + {manifest.length} PDF(s) en{" "} + {new Set(manifest.map((m) => m.nit).filter(Boolean)).size || 1}{" "} + carpeta(s) planas — listos para contar + +
+ )}
- - {manifest.length > 0 && !indexing && ( -
- - - {manifest.length} documento(s) encontrados y listos para contar - + ) : ( +
+ +
+
+ {form.localRoot ? ( + <> + + {form.localRoot} + + ) : ( + Ninguna carpeta seleccionada + )} +
+
- )} - {form.localRoot && manifest.length === 0 && !indexing && !error && ( -

No se encontraron PDF en esta carpeta.

- )} -
+ + {manifest.length > 0 && !indexing && ( +
+ + + {manifest.length} documento(s) encontrados y listos para contar + +
+ )} + {form.localRoot && manifest.length === 0 && !indexing && !error && ( +

No se encontraron PDF en esta carpeta.

+ )} +
+ )}
Opciones avanzadas
-
- - setForm((f) => ({ ...f, containerRoot: e.target.value }))} - placeholder="/data" - /> -

- Prefijo donde están montados los documentos dentro de Docker (para guardar rutas en BD). -

-
+ {!fromDownload && ( +
+ + setForm((f) => ({ ...f, containerRoot: e.target.value }))} + placeholder="/data" + /> +

+ Prefijo donde están montados los documentos dentro de Docker (para guardar rutas en BD). +

+
+ )}
@@ -236,15 +243,21 @@ function CountingResultsSection({ function exportCsv(subset: "valid" | "invalid") { const subset_files = subset === "valid" ? files.filter(isFileValid) : files.filter((f) => !isFileValid(f)); - const headers = ["filename", "radicado", "service_count", "status", "is_valid", "error"]; - const rows = subset_files.map((f) => [ - f.filename, - f.radicado ?? "", - f.service_count ?? "", - f.status, - f.is_valid === null ? "" : String(f.is_valid), - f.error ?? "", - ]); + const hasNit = subset_files.some((f) => f.nit); + const headers = hasNit + ? ["nit", "filename", "radicado", "service_count", "status", "is_valid", "error"] + : ["filename", "radicado", "service_count", "status", "is_valid", "error"]; + const rows = subset_files.map((f) => { + const base = [ + f.filename, + f.radicado ?? "", + f.service_count ?? "", + f.status, + f.is_valid === null ? "" : String(f.is_valid), + f.error ?? "", + ]; + return hasNit ? [f.nit ?? "", ...base] : base; + }); downloadCsv(buildExportCsvFilename("conteo", job.title, subset), toCsv(headers, rows)); } diff --git a/services/desktop_app/src/renderer/views/JobView.tsx b/services/desktop_app/src/renderer/views/JobView.tsx index d0ee822..0fa4401 100644 --- a/services/desktop_app/src/renderer/views/JobView.tsx +++ b/services/desktop_app/src/renderer/views/JobView.tsx @@ -149,6 +149,7 @@ export function JobView({ client, job, section, sectionContext, onActivateSectio setCountingProgress(p.running ? p : null)} onActivateModule={(type, ctx) => onActivateSection(type as JobSection, ctx)} @@ -198,6 +199,7 @@ export function JobView({ client, job, section, sectionContext, onActivateSectio onStartExtraction={(ctx?: Record) => onActivateSection("extraction", ctx)} onStartCleaning={(ctx?: Record) => onActivateSection("cleaning", ctx)} onStartDownload={() => onActivateSection("downloads")} + onContinueToCounting={(ctx) => onActivateSection("counting", ctx)} />
diff --git a/services/desktop_app/src/shared/types.ts b/services/desktop_app/src/shared/types.ts index e3922f8..5e8a2be 100644 --- a/services/desktop_app/src/shared/types.ts +++ b/services/desktop_app/src/shared/types.ts @@ -6,6 +6,10 @@ export interface ManifestEntry { docType?: string; localPath: string; size: number; + /** NIT / carpeta plana (resultados bot descarga MinIO). */ + nit?: string | null; + /** Object key en MinIO cuando el origen es descarga remota. */ + objectKey?: string | null; } export interface FileUploadData { @@ -117,6 +121,8 @@ export interface CountJobFile { is_valid: boolean | null; error: string | null; created_at: string; + nit?: string | null; + object_key?: string | null; } export interface CountJobFileCreateEntry { @@ -128,12 +134,16 @@ export interface CountJobFileCreateEntry { services?: Array<{ text: string; probability: number; is_service?: boolean | null }>; status?: "ok" | "error"; error?: string | null; + nit?: string | null; + object_key?: string | null; } export interface CreateCountJobPayload { title?: string; job_id?: string; results: CountJobFileCreateEntry[]; + download_job_id?: string | null; + source?: string | null; } export interface ExtractionFromCountPayload { diff --git a/services/extraction/api/main.py b/services/extraction/api/main.py index 90c48c1..7629c88 100644 --- a/services/extraction/api/main.py +++ b/services/extraction/api/main.py @@ -1107,12 +1107,16 @@ class CountJobFileCreate(BaseModel): services: list[dict[str, Any]] = Field(default_factory=list) status: str = "ok" error: str | None = None + nit: str | None = None + object_key: str | None = None class CreateCountJobRequest(BaseModel): title: str | None = None job_id: str | None = None # si se pasa, se usa el job existente results: list[CountJobFileCreate] + download_job_id: str | None = None + source: str | None = None class CountJobFileRead(BaseModel): @@ -1128,6 +1132,8 @@ class CountJobFileRead(BaseModel): is_valid: bool | None error: str | None created_at: str + nit: str | None = None + object_key: str | None = None class UpdateCountJobFileRequest(BaseModel): @@ -1163,6 +1169,8 @@ def serialize_count_file(f: CountJobFile) -> dict[str, Any]: "is_valid": f.is_valid, "error": f.error, "created_at": f.created_at.isoformat(), + "nit": getattr(f, "nit", None), + "object_key": getattr(f, "object_key", None), } @@ -1193,12 +1201,16 @@ def create_count_job(request: CreateCountJobRequest) -> dict[str, Any]: type="counting", status="completed", title=default_title, - request={}, + request={ + "source": request.source or ("from-download" if request.download_job_id else "desktop"), + "download_job_id": request.download_job_id, + }, result={ "total": total, "processed": total, "ok": sum(1 for r in request.results if r.status == "ok"), "failed": sum(1 for r in request.results if r.status != "ok"), + "nits": sorted({r.nit for r in request.results if r.nit}), }, started_at=utcnow(), finished_at=utcnow(), @@ -1215,6 +1227,8 @@ def create_count_job(request: CreateCountJobRequest) -> dict[str, Any]: filename=r.filename, file_path=r.file_path, doc_type=r.doc_type, + nit=r.nit, + object_key=r.object_key, service_count=r.service_count, services=r.services, status=r.status, diff --git a/services/extraction/infrastructure/database.py b/services/extraction/infrastructure/database.py index dfbc08a..46c8a01 100644 --- a/services/extraction/infrastructure/database.py +++ b/services/extraction/infrastructure/database.py @@ -111,6 +111,8 @@ class CountJobFile(Base): filename: Mapped[str] = mapped_column(String(512)) file_path: Mapped[str | None] = mapped_column(Text, nullable=True) doc_type: Mapped[str | None] = mapped_column(String(128), nullable=True) + nit: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) + object_key: Mapped[str | None] = mapped_column(Text, nullable=True) service_count: Mapped[int | None] = mapped_column(Integer, nullable=True) services: Mapped[list[Any]] = mapped_column(JSON, default=list) status: Mapped[str] = mapped_column(String(32), default="ok", index=True) diff --git a/services/extraction/worker/tasks.py b/services/extraction/worker/tasks.py index f40a4be..252ea70 100644 --- a/services/extraction/worker/tasks.py +++ b/services/extraction/worker/tasks.py @@ -11,6 +11,7 @@ import logging import os from typing import Any +from sqlalchemy.orm.attributes import flag_modified from infrastructure.database import ExtractionRow, Task, session_scope, utcnow from infrastructure.serialization import flatten_extraction_result from api.desktop_ingest import ( @@ -128,7 +129,8 @@ async def process_document( # Persistir resultado en BD def _persist() -> dict[str, Any]: with session_scope() as session: - task = session.get(Task, task_id) + # Lock evita lost-update en result.processed con N workers concurrentes. + task = session.get(Task, task_id, with_for_update=True) if task is None: return {"skipped": True, "reason": "task_not_found"} @@ -185,6 +187,7 @@ async def process_document( result["row_count"] = row_index result["processed"] = int(result.get("processed") or 0) + 1 task.result = result + flag_modified(task, "result") task.updated_at = utcnow() session.flush() mark_task_completed_if_done(session, task, batch_id) @@ -206,7 +209,7 @@ async def process_document( def _record_error() -> None: try: with session_scope() as session: - task = session.get(Task, task_id) + task = session.get(Task, task_id, with_for_update=True) if task is None: return result = dict(task.result or {}) @@ -219,6 +222,7 @@ async def process_document( result["errors"] = errors result["processed"] = int(result.get("processed") or 0) + 1 task.result = result + flag_modified(task, "result") task.updated_at = utcnow() mark_task_completed_if_done(session, task, batch_id) except Exception as inner: