Imported from jfrem/AudFact (
.agent/skills/audfact-audit-gemini/SKILL.md). Install upstream withnpx skills add jfrem/AudFact --skill audfact-audit-gemini. Copyright stays with the author.
AudFact Audit Gemini (Event-Driven)
Objetivo
Mantener confiable el pipeline event-driven de auditoría documental con Redis Streams, extracción Gemini por Structured Outputs nativos (responseSchema), normalización y policy en PHP puro, y persistencia final en SQL Server.
Archivos clave
Servicios del pipeline event-driven
| Archivo | Rol |
|---|---|
app/Services/Audit/Pipeline/AuditEvent.php |
Value-object inmutable; followUp() deriva etapas de la misma auditoría conservando identidad, correlación y source / is_priority del padre |
app/Services/Audit/Pipeline/AuditEventPublisher.php |
Publica a streams duales .priority y .batch (audit.inbox.*, audit.documents.*, audit.results.*, audit.dlq); enrutamiento automático de prioridad para auditorías interactivas 1:1; rules_evaluated debe pasar exclusivamente por AuditPersistenceQueue |
app/Services/Audit/Pipeline/AuditEventConsumer.php |
Base abstracta multi-stream: consume prioritariamente con xReadGroupMulti (.priority antes de .batch), ack, reintentos y DLQ; SQL agotado y descarga técnica son terminales en la misma entrega |
app/Services/Audit/Pipeline/AuditStateStore.php |
Claves Redis de estado (audit:{id}:*, job:{id}:*, contadores, FDV cache) |
app/Services/Audit/Pipeline/AuditDataService.php |
Facade concreta de acceso interno a FDV, audit-config y catálogo documental usado por workers (no consultas SQL directas) |
app/Services/Audit/Pipeline/AttachmentDownloadService.php |
Descarga Drive/BLOB, valida bytes esperados y clasifica fallos técnicos |
app/Services/Audit/Pipeline/AttachmentDownloadException.php |
Taxonomía tipada de fuente ausente/vacía, transferencia incompleta y fallo externo |
app/Services/Audit/Pipeline/AttachmentDownloadWorker.php |
Consume document_registered, guarda el BLOB en Redis y publica solo document_downloaded; propaga fallos técnicos |
app/Services/Audit/Pipeline/DocumentRejectionReason.php |
Allowlist cerrada para rechazos comprobados de contenido |
app/Services/Audit/Pipeline/DocumentMappingRejectionReason.php |
Categoría y allowlist cerrada para rechazos de asociación lógica/física |
app/Services/Audit/Pipeline/DocumentAttachmentMatcher.php |
Matcher puro, determinista y global 1:1 por nombre, ID corroborado y alias único |
app/Services/Audit/Pipeline/DocumentAttachmentMatchResult.php |
DTO readonly que impide IDs lógicos o físicos duplicados en matches |
app/Services/Audit/Pipeline/DocumentIntegrityValidator.php |
Validación preventiva de integridad documental de adjuntos vacíos, corruptos o con MIME inconsistente antes de Gemini |
app/Services/Audit/Pipeline/DocumentAuditOrchestrator.php |
Consume audit_created, reconcilia todos los adjuntos físicos 1:1, publica matches y emite rechazos DOCUMENT_MAPPING controlados |
app/Services/Audit/Pipeline/DocumentExtractionContractBuilder.php |
Construye response_schema Gemini plano y unificado: document_conformity (tipología con short-circuit y directiva dinámica TipoDocumento/TIP), fields, items y visual_checks solo cuando aplican; document_quality y quality_notes siempre. Invalida cache deterministamente mediante contract_hash. |
app/Services/Audit/Pipeline/DocumentExtractionWorker.php |
Orquestador delgado que consume document_downloaded, delega estado a Redis, genera prompts y parsea respuestas. Produce rechazos document_content. |
app/Services/Audit/Pipeline/DocumentPdfRasterizer.php |
Pre-rasterizador determinista de PDFs a imágenes JPEG de alta resolución (200 DPI) con motor primario Ghostscript (gs, tolerante a fuentes Identity-H no incrustadas y reparación de sintaxis iText) y fallback a pdftoppm (poppler-utils). |
app/Services/Audit/Pipeline/ExtractionCacheManager.php |
Administra estado transitorio y cache de extracción Gemini en Redis mediante HSET/HGET. |
app/Services/Audit/Pipeline/ExtractionPromptBuilder.php |
Construye prompts del sistema y usuario para Structured Outputs de Gemini; inyecta directivas específicas de tipología (TipoDocumento/TIP) en la regla 1 de conformidad documental. |
app/Services/Audit/Pipeline/GeminiResponseParser.php |
Parsea JSON text generado por Gemini Structured Output y rehidrata campos planos a la shape canónica {valor, presente, estadoExtraccion}. |
app/Services/Audit/Pipeline/ExtractionState.php |
Enum tipado para el estado de extracción de campos (FOUND, FOUND_IN_LIST, NOT_FOUND, ILLEGIBLE) |
app/Services/Audit/Pipeline/ExtractedEvidence.php |
DTO tipado para representar de forma determinista la evidencia extraída y normalizada |
app/Services/Audit/AuditBatchOrchestrator.php |
Servicio que encapsula la orquestación asíncrona de lotes (reserva de slots Redis y rollback transaccional) |
app/Services/Audit/Pipeline/BatchRequestedWorker.php |
Worker que consume batch_requested de audit.batch.inbox, realiza consultas pesadas en SQL Server y reserva idempotencia por DisId en Redis |
bin/schedule-daily-batches.php |
CLI Cron: encola auditorías batch diarias emitiendo batch_requested para todos los clientes configurados, validando campos activos e idempotencia por cliente. Límite configurable vía AUDIT_BATCH_CRON_LIMIT (default: 5000) o --limit CLI. |
app/Services/Audit/Pipeline/DocumentNormalizer.php |
Worker autocontenido: consume document_extracted, normaliza fields / items / visual_checks (fechas ISO, identidad documental, numéricos canónicos y evidencia visual estructurada) y publica document_normalized |
app/Services/Audit/Pipeline/DocumentPolicyEngine.php |
Motor determinista por documento: delega reglas complejas y orquesta COINCIDE / VALOR_DISTINTO / NO_ENCONTRADO / OMITIDO / NO_CONCLUYENTE |
app/Services/Audit/Pipeline/VisualCheckEvaluator.php |
Servicio delegado de DocumentPolicyEngine que evalúa evidencia visual y resuelve discrepancias de calidad documental. |
app/Services/Audit/DocumentDuplicationEvaluator.php |
Servicio funcional que evalúa colisiones SHA256 para prevenir fraude por duplicación documental. |
app/Services/Audit/Pipeline/FieldValueResolver.php |
Utilidad que extrae y normaliza el valor del documento (header vs items), incluyendo candidatos valores de evidencia v1, resolviendo dependencias de normalización cruzada. |
app/Services/Audit/Pipeline/ResolvedAuditValue.php |
DTO inmutable para comparar FDV y documento con el mismo contrato (displayValue, values, normalizedValues, ambiguous, evidenceMeta). |
app/Services/Audit/Pipeline/RulesEvaluationWorker.php |
Consume document_normalized y document_rejected, consolida hallazgos, métricas, audit_result_data y decisiones documentales, guarda el outcome y lo entrega a AuditPersistenceQueue cuando todos los documentos están evaluados |
app/Services/Audit/Pipeline/AuditPersistenceQueue.php |
Scheduler Redis/Lua idempotente: mantiene un evento activo por job y promueve el siguiente al cerrar el turno |
app/Services/Audit/Pipeline/AuditPersistenceWorker.php |
Consume rules_evaluated, persiste en SQL, cierra Redis, libera el turno y publica eventos terminales. No toma decisiones funcionales de auditoría. |
app/Models/AuditResultPersistenceModel.php |
Escritura SQL transaccional del resumen, hallazgos por adjunto y trazabilidad de la factura |
app/Services/Audit/Pipeline/AuditTimingSummarizer.php |
Agrega duraciones de las fases del pipeline y extrae los phase_timings para reporte. |
app/Services/Audit/Telemetry/TelemetryPublisher.php |
Publica telemetría live best-effort en audit.telemetry desde cada worker en su fase real (orchestration, download, extraction, normalization, policy, aggregation). |
app/Services/Audit/AuditFindingRules.php |
Utilidad compartida para normalizar valores, sumar métricas y resolver severidad |
app/Services/Audit/Pipeline/BatchJobStore.php |
Estado batch, reservas y métricas atómicas; las transiciones usan job.status y cubren pending -> completed directo |
app/Services/Audit/AuditComparisonType.php |
Enum EXACT/SEMANTIC/BUSINESS/VISUAL + fromTipoCampo() (mapea E/S/B/V desde BD) — métodos isDateField/isQuantityField/isNumberField son puentes @deprecated que delegan a AuditFieldValueType |
app/Services/Audit/AuditFieldValueType.php |
Enum strategy-based de TipoDato explícito en audit-config: TEXT/DATE/QUANTITY/MONEY/IDENTITY_DOC_TYPE/IDENTITY_DOC_NUMBER/CODE/TRACE_TOKEN/PERSON_NAME/INSTITUTION_NAME/ARTICLE_NAME/NIT. Métodos de comportamiento: requiresSubsetComparison() (CODE), requiresTraceSetComparison() (TRACE_TOKEN), requiresArticleSetComparison() (ARTICLE_NAME), requiresTokenSortComparison() (PERSON_NAME), allowsMultiValueDocument() (CODE, TRACE_TOKEN, ARTICLE_NAME), allowsSemanticGeminiFallback() (ARTICLE_NAME y PERSON_NAME). NIT normaliza el número tributario colombiano eliminando el dígito de verificación (-X) y separadores de miles. Prohibido inferir tipos por nombre del campo. |
app/Services/Audit/GeminiConfig.php |
Value Object de configuración Gemini, incluyendo overrides por tarea (GEMINI_EXTRACTION_*, GEMINI_SEMANTIC_*) y opt-in explícito de mediaResolution |
app/Services/Audit/GeminiGateway.php |
Cliente HTTP para Gemini API con retry, timeout, function calling, perfiles explícitos (extraction, semantic_match) y métricas X-Audit-Metrics |
app/Services/Audit/SemanticMatchJudge.php |
Fallback semántico conservador y agnóstico de plataforma para arbitraje de entidades (productos, personas); usa evidencia estructurada, contexto documental enriquecido, cache versionada y no cachea fallos transitorios |
app/Services/Audit/GeminiCallMetrics.php |
Normaliza métricas Gemini por tarea: latencia, tokens de prompt/output/thinking/total y cache hits |
app/Services/Audit/ResponseIADiskStore.php |
Persiste snapshots de request/response Gemini en AUDIT_RESPONSE_IA_DIR solo cuando APP_ENV=development y AUDIT_RESPONSE_IA_ENABLED=1 |
Workers bootstrap (largas ejecuciones)
Tras la consolidación AUDIT-015 (2026-04-27), existe un único launcher bin/audit-worker.php que recibe el nombre de worker como primer argumento CLI y selecciona el consumer mediante un registry interno:
| Comando | Stream consumido | Consumer group |
|---|---|---|
php bin/audit-worker.php batch |
audit.batch.inbox |
batch-workers |
php bin/audit-worker.php orchestrator |
audit.inbox.priority / audit.inbox.batch |
orchestrator |
php bin/audit-worker.php downloader |
audit.documents.priority / audit.documents.batch |
downloaders |
php bin/audit-worker.php extraction |
audit.documents.priority / audit.documents.batch (filtrados por carril) |
extractors |
php bin/audit-worker.php normalizer |
audit.documents.priority / audit.documents.batch |
normalizers |
php bin/audit-worker.php policy |
audit.documents.priority / audit.documents.batch |
policy |
php bin/audit-worker.php persistence |
audit.persistence.priority / audit.persistence.batch |
persistence |
El launcher carga .env, instancia el consumer correspondiente, registra SIGTERM/SIGINT para stop gracioso y llama run(); pcntl_signal_dispatch se procesa dentro del loop del consumer base. Los consumer names son únicos por rol + hostname + PID para que Redis refleje réplicas reales. El inicio registra grupo, consumer, carril, streams, bloqueo, reintentos y límites de reclaim. Compose separa extracción en servicios VIP y batch usando el mismo launcher.
Controllers y endpoints
| Archivo | Endpoints |
|---|---|
app/Controllers/AuditController.php |
POST /audit/single (202), POST /audit/async (202), GET /audit/jobs/{job_id} |
app/Controllers/AuditDlqController.php |
GET /audit/dlq, POST /audit/dlq/reprocess (listar y republicar dead_letter) |
Streams y eventos
Invariante de transporte (2026-09-23)
Orquestación, descarga, extracción (éxito/cache/rechazo), normalización, reglas, persistencia y
fallo terminal usan AuditEvent::followUp(eventType, payload, documentId).
Conserva audit_id, job_id y referencia al evento padre; documentId es un
argumento obligatorio: ID en etapas documentales y null explícito en etapas
consolidadas. El payload funcional no puede reemplazar
source o is_priority: esas claves provienen exclusivamente del evento padre.
No inferir source=single desde prioridad o job_id=null; no copiar todo el
payload anterior para conservar solo transporte. El clasificador sigue siendo
AuditEventPublisher::isPriorityEvent() (source === 'single' o booleano true).
Cada señal basta aunque falte la otra; eventos directos sin ambas quedan en batch,
independientemente de job_id. No aceptar strings o enteros como booleano true.
El orquestador usa followUp() tanto para registros como para rechazos de mapping;
buildDocumentState() construye solo contexto funcional, sin copiar transporte.
La lista cerrada ROUTING_PAYLOAD_KEYS y el helper privado
inheritRoutingMetadata() concentran la copia de transporte; no duplicar esa
lógica en workers ni exponer helpers productivos solo para pruebas.
RulesEvaluationWorker aplica transporte después de recuperar el outcome
canónico de Redis, por lo que un reintento conserva origen y carril.
audit_failed se publica en AuditEventConsumer; audit_completed, en
AuditPersistenceWorker, después del cierre Redis. No publicar
rules_evaluated fuera de AuditPersistenceQueue.
PEL no equivale a backlog sin entregar: /metrics/async suma PEL de varios
grupos y no demuestra bloqueo en policy. El despliegue no corrige eventos
antiguos sin metadatos. Diagnóstico y recuperación: ver
plans/features/audit-workflow.md, sección «Despliegue, diagnóstico y recuperación
del desvío a batch». Mantener consumidores batch activos durante el drenaje;
no borrar ni duplicar mensajes para adelantar una auditoría.
| Stream | Productor | Eventos |
|---|---|---|
audit.batch.inbox |
AuditController / BatchRequestedWorker (re-encolado) |
batch_requested (con cursor keyset y chunk_index para ingesta fair-queuing) |
audit.inbox.priority / audit.inbox.batch |
BatchRequestedWorker / AuditController |
audit_created, batch_created |
audit.documents.priority / audit.documents.batch |
Orchestrator (registered/mapping rejected), Downloader (downloaded), Extractor (extracted/content rejected), Normalizer (normalized) |
document_registered, document_downloaded, document_extracted, document_rejected, document_normalized |
audit.persistence.priority / audit.persistence.batch |
AuditPersistenceQueue |
rules_evaluated |
audit.results.priority / audit.results.batch |
Persistence Worker / Consumer base | audit_completed, audit_failed, batch_completed(_with_errors) |
audit.telemetry |
Workers de auditoría | Eventos live started, completed, failed, rejected por fase real del DAG |
audit.dlq |
Cualquier worker | dead_letter (despliega payload original + etapa, attempts y last_error_*) |
Variables de entorno relevantes
| Variable | Uso |
|---|---|
GEMINI_API_KEY |
Credencial obligatoria para el extractor (fallback general) |
GEMINI_API_KEY_PRIORITY |
Credencial opcional dedicada para el carril VIP / prioritario (--priority-only) |
GEMINI_API_KEY_BATCH |
Credencial opcional dedicada para el carril Batch / masivo (--batch-only) |
GEMINI_MODEL |
Modelo Gemini (por defecto gemini-3.5-flash) |
GEMINI_TIMEOUT, GEMINI_MAX_OUTPUT_TOKENS, GEMINI_TEMPERATURE, GEMINI_TOP_P, GEMINI_TOP_K, GEMINI_SEED, GEMINI_MEDIA_RESOLUTION, GEMINI_THINKING_BUDGET, GEMINI_THINKING_LEVEL |
Configuración base de generación Gemini |
GEMINI_EXTRACTION_MAX_OUTPUT_TOKENS, GEMINI_EXTRACTION_THINKING_LEVEL, GEMINI_EXTRACTION_THINKING_BUDGET |
Perfil de generación para extracción documental |
GEMINI_SEMANTIC_MAX_OUTPUT_TOKENS, GEMINI_SEMANTIC_THINKING_LEVEL, GEMINI_SEMANTIC_THINKING_BUDGET |
Perfil de generación para homologación semántica; en Gemini 3.1 dejar THINKING_LEVEL vacío si se desea omitir thinkingConfig |
AUDIT_WORKER_LANE |
Filtro de carril en AuditEventConsumer (all, priority, batch) |
AUDIT_WORKER_EXTRACTION_VIP_REPLICAS |
Réplicas dedicadas para el worker VIP (worker-extraction-vip, default 2) |
AUDIT_WORKER_EXTRACTION_BATCH_REPLICAS |
Réplicas dedicadas para el worker Batch (worker-extraction-batch, default 6) |
AUDIT_STREAM_BLOCK_MS |
Bloqueo XREADGROUP |
AUDIT_EVENT_MAX_RETRIES |
Reintentos por evento antes de DLQ |
AUDIT_DLQ_STREAM |
Stream DLQ (default audit.dlq) |
AUDIT_CACHE_TTL, AUDIT_EXTRACTION_CACHE_TTL |
TTL cache extracción Gemini |
AUDIT_BATCH_CHUNK_SIZE |
Tamaño de chunk para ingesta fair-queuing de lotes masivos (default 50) |
AUDIT_BATCH_LOCK_TTL_SECONDS |
TTL del lock atómico de generación distribuida por chunk (default 300) |
AUDIT_JOB_TTL |
TTL de estado de jobs batch async en Redis (default 604800) |
AUDIT_STATE_TTL |
TTL de estado transitorio de auditorias en Redis (default 604800) |
AUDIT_RESERVATION_TTL |
TTL de reservas por DisId en Redis (default 86400) |
AUDIT_WORKER_PERSISTENCE_REPLICAS |
Réplicas SQL globales (default 3); la cola limita a una activa por job |
AUDIT_PERSISTENCE_QUEUE_TTL |
TTL de turnos, pendientes y deduplicación de persistencia (default 604800) |
AUDIT_FDV_TTL |
TTL de la FDV completa en Redis |
AUDIT_INTERNAL_API_BASE |
Base URL que los workers usan para la API interna (FDV/catalogos/adjuntos) |
AUDIT_RESPONSE_IA_ENABLED, AUDIT_RESPONSE_IA_DIR |
Controlan snapshots Gemini locales; producción nunca persiste snapshots por hard-deny de APP_ENV=production |
AUDIT_VERSION_EXTRACTOR, AUDIT_VERSION_NORMALIZER, AUDIT_VERSION_RULES |
Versionado para trazabilidad en AuditEvent |
GEMINI_MODEL es el único selector de versión de modelo usado por el gateway. GeminiConfig conserva un fallback local si la variable falta, pero GEMINI_EXTRACTION_* y GEMINI_SEMANTIC_* son perfiles de generación del mismo modelo configurado; no implementan fallback ni redirección a otra versión Gemini.
Flujo técnico
POST /audit/singlevalidaDisDetNro→ publicaaudit_createdenaudit.inbox.priority→ retorna 202 conaudit_id.DocumentAuditOrchestratorconsumeaudit_created, resuelve FDV,audit-config, catálogo y todos los adjuntos físicos. Ejecuta una sola reconciliación global medianteDocumentAttachmentMatcher: nombre exacto normalizado, ID corroborado y alias único. Cadaattachment_idse usa como máximo una vez. Los matches publicandocument_registeredcon trazabilidad lógica/física; missing, ambiguous, no content y reused se registran como rechazados y publicandocument_rejectedconrejection_category=DOCUMENT_MAPPING, sin descarga ni Gemini. La reglaAutorizacion=Rreutiliza ese mismo resultado y solo transforma ausencia real en el hallazgo sintéticoAUTexistente.AttachmentDownloadWorkerconsumedocument_registered, descarga el adjunto y lo almacena temporalmente en Redis con key lógicaaudit:blob:*(RedisClientaplicaREDIS_PREFIX). Para BLOB exigebytes === DATALENGTH. Publicadocument_downloaded; fuente ausente/vacía, transferencia parcial, SQL o Drive son fallos técnicos y se propagan, nunca publicandocument_rejected.DocumentExtractionWorkerconsumedocument_downloaded, lee el BLOB desde Redis y evalúa su integridad estructural medianteDocumentIntegrityValidator. Es el único productor autorizado de rechazos de contenido: todo rechazo incluyerejection_class=document_content, origen exacto y razón deDocumentRejectionReason. Si es válido, calculadocument_hash, arma prompt compacto, consulta cache; si no hay hit, invoca Gemini con Structured Outputs nativos (responseSchemayresponseMimeType: application/json). Si Gemini lanza HTTP 400 por error de decodificación o archivo corrupto confirmado, emite el rechazo tipado. Si es exitoso, parsea, rehidrata los campos planos a la forma canónica y publicadocument_extracted.DocumentNormalizernormalizafields/items/visual_checks(fechas ISO, identidad documental, numéricos canónicos, evidencia visual estructurada, null para vacío) y emitedocument_normalizedconnormalization_logsin PII cruda.RulesEvaluationWorkerevalúadocument_normalizedcontra FDV usandoDocumentPolicyEngine; FDV y documento se resuelven primero comoResolvedAuditValue. Valida dos contratos cerrados sin fallback: contenido solo desdeDocumentExtractionWorkery mapping solo desdeDocumentAuditOrchestrator. Mapping genera hallazgoMAP, severidad alta,RECHAZADOeintegrity, preservandological_doc_idy candidatos. Cualquier evento legacy oDOWNLOAD_ERRORfalla técnicamente. Esperadocs_done + docs_rejected >= docs_total, guarda el outcome y lo encola enAuditPersistenceQueue.AuditPersistenceQueuepublica un único turno activo por job enaudit.persistence.priorityoaudit.persistence.batch; sin job usa un scope por auditoría. Jobs diferentes usan las réplicas configuradas en paralelo.audit.persistence:{queue}:*contiene claves de scheduling, no streams.AuditPersistenceWorkeraplica una barrera independiente contraDOWNLOAD_ERRORy contratos de rechazo inválidos, ejecuta la transacción dual idempotente sobreAudDispEst+AdjuntosDispensacion+DispensacionDetalleServicio, libera el turno y publicaaudit_completed.- SQL usa PDO fresco por operación y replay interno solo para lectura/escritura idempotente, con pausas de 1/5/30 segundos. Al agotar SQL o ante un fallo técnico tipado de descarga,
AuditEventConsumergeneradead_letter, hace ACK y ejecuta el cierre terminal en la misma entrega; no esperaXAUTOCLAIM.
Resiliencia y Manejo de Errores Documentales (Pre-IA y Post-IA)
flowchart TD
A[Descarga de Documento / BLOB] --> B[Nivel 1: DocumentIntegrityValidator]
B -->|PDF sin %%EOF o Truncado| C[Rechazo Preventivo: CORRUPTED_DOCUMENT]
B -->|PDF sin Páginas| D[Rechazo Preventivo: EMPTY_PDF_NO_PAGES]
B -->|PDF con Password| E[Rechazo Preventivo: ENCRYPTED_DOCUMENT]
B -->|Estructura Válida| F[Llamada a Google Gemini API - Structured Output]
F -->|200 OK| G[document_extracted]
F -->|400 Decode Error / Corrupt File| H[Nivel 2: DocumentExtractionWorker]
H -->|Clasificación Determinista| I[Rechazo: GEMINI_DECODE_FAILURE]
C --> J[Emitir document_rejected]
D --> J
E --> J
I --> J
J --> K[Incrementar docs_done 3/3]
G --> K
K --> L[RulesEvaluationWorker: Sellar Auditoría y Batch al 100%]
Reglas de implementación (estrictas)
- IA sólo extrae: Gemini nunca toma decisiones de negocio finales; la comparación y aplicación de severidades dinámicas (CRITICO, ALTA, MEDIA, BAJA, INFO) viven en
DocumentPolicyEnginesegún elaudit-config. Las señales de disconformidad tipológica (document_conformity.matches_expected_type === false) emiten hallazgoTIPcon resultadoNO_CONCLUYENTEy derivación a revisión humana, evitando rechazos definitivos automáticos basados en un único booleano de la IA. - TipoCampo gobierna la comparación y TipoDato gobierna el valor — fuente de verdad: columnas
TipoCampoyTipoDatoenDiscolnet.dbo.AudDispCampo.TipoCampo=E→EXACT(igualdad normalizada)TipoCampo=S→SEMANTIC(umbral 0.82; Gemini solo siTipoDato=article_name)TipoCampo=B→BUSINESS(sumatoria de items + comparación numérica; soloTipoDato=quantity)TipoCampo=V→VISUAL(vive envisualChecks[]; no usaTipoDato)TipoDatopermitido:text,date,quantity,money,identity_doc_type,identity_doc_number,code,trace_token,person_name,institution_name,article_name,nit,auth_number. Prohibido inferirTipoDatopor nombre del campo.AuditFieldValueType::fromInput()debe recibir metadata explícita delaudit-config. La ubicaciónextract_fieldsvsextract_itemsla defineDocumentExtractionContractBuildercon reglas explícitas de dominio para evitar mezclar cabecera y líneas.
- [AUDIT-016] Subset matching para
CODE: Campos conAuditFieldValueType::CODEusanevaluateSubsetField(). Si el FDV tiene un código (ej.S202) y el documento lista múltiples (ej.S202, S273, F432), se evalúa comoCOINCIDEcontipo_auditoria=exact. Si la evidencia llega comovalor=null,valores=[...]yFOUND_IN_LIST,FieldValueResolverdebe usarvalorescomo evidencia encontrada, no emitirNO_ENCONTRADO.tokenizeCodeField()separa por coma, punto y coma o barra; normaliza a mayúsculas. El hallazgo incluyevalueType=codeyvaloresDocumento(array de tokens). - [TRACE_TOKEN] Set-based matching para Trazabilidad: Campos como
LoteusanTRACE_TOKEN. FDV y documento se resuelven como sets desdeResolvedAuditValue; si ambos sets son iguales, esCOINCIDE. Si el documento trae solo una parte de un FDV múltiple, esNO_CONCLUYENTE(evidencia parcial). Si trae un lote no registrado en FDV, esVALOR_DISTINTO. El hallazgo incluyevaloresFuenteVerdadyvaloresDocumento. - [AUDIT-016] Token-sort para
PERSON_NAME: CamposPERSON_NAMEen modo semántico usan una heurística estructural de similitud por tokens (exigiendo que al menos 1 token de la parte más corta coincida exactamente). Si la heurística falla, hace fallback aSemanticMatchJudgepara validar posibles alias o variaciones de escritura vía Gemini. - [AUDIT-016] No data-loss en multi-item/lista divergente (CAT-1): Si
resolveDocumentValue()encuentra múltiples items con valores distintos en un campo no sumable, o una evidencia escalar con varios candidatosvalores, emiteNO_CONCLUYENTEcondetalle: {ambiguous: true, valores: [...]}. Un único candidato envaloresse usa como escalar. Ya no se descarta silenciosamente el campo ni se convierte enNO_ENCONTRADOcuando existe evidencia. - [AUDIT-016] Hallazgo canónico v1:
buildDataFinding()inyectavalueType; paraCODEyTRACE_TOKENagregavaloresFuenteVerdady/ovaloresDocumentocuando existen tokens/set evaluables. Las cantidadesTipoCampo=BreportanvalorFuenteVerdadyvalorDocumentocomo sumatoria agregada de items. Si un hallazgo configurable falla y existecodigoCampo, eldetallese enriquece con el prefijo textual-CODIGO- detalle. - Items y Reconciliación Multilote: no derivar
itemsdesdefieldsy viceversa.FdvItemAggregatorconsolida previamente la FDV agrupando por campos no sumables (código de producto, lote, CUM, etc., sin importar sitipoCampoesB,EoS), calculando con precisión$expectedItemsCountpara documentos consolidados (ej. autorizaciones multilote). Si el extractor detecta segmentación incompleta, emite el warningITEM_SEGMENTATION_INCOMPLETEen el payload. Luego,DocumentPolicyEngineevalúa primero si la comparación cuantitativa directa resulta enCOINCIDE(balance 100% satisfecho); de ser así emiteCOINCIDEcon telemetría enextraction_meta. Si el balance no coincide (faltante real), fuerzaNO_CONCLUYENTEa nivel de línea (TipoCampo=B) para evitar sumatorias parciales peligrosas. - Prompt compacto de extracción: Gemini no recibe valores esperados de FDV (
Campos de cabecera esperados,Campos de línea esperados, diagnósticos, fechas, identidad, etc.). Solo recibe contexto estructural: documento objetivo, campos solicitados, separación identidad, ubicaciónfields/items, checks visuales y segmentación de filas cuando aplican. El schema puede usar descripciones configuradas delaudit-configcomovalor.descriptiony conserva las descripciones PHP solo como fallback; el system prompt personalizado se deduplica contra esas descripciones antes de calcularprompt_context_hash. En el schema Gemini,valoresse declara como array de strings solo paraTRACE_TOKEN; los camposCODEson escalares y se tokenizan en PHP;DocumentNormalizerreconstruyevaloresdesdevalorpara escalares. Las pistas de artículo para documentos prescriptivos se permiten solo cuandoNombreArticuloestá enitems. - Comparación determinista: umbrales
persona 0.85,artículo 0.82,texto 0.90; numéricos/IDs/fechas con igualdad normalizada. - Cadena documental: Fórmula → Autorización → Dispensa. El
audit-configruntime no persisterol; todo campo activo enfieldsse evalúa segúnTipoCampoy severidad. - Entrega parcial válida:
cantidad_entregada_total <= cantidad_autorizada(ocantidad_prescritasi no hay autorización). - Exclusiones documentales: no documentar campos como "informativos" si el
audit-configreal no trae esa marca. Para omitir un campo de auditoría debe no estar activo enfields;omitirSino está implementado en el runtime actual. - Sin código legacy: clean rebuild; no agregar shims ni compatibilidad con el pipeline monolítico anterior.
- XACK solo tras éxito: acknowledge después de publicar el evento siguiente o persistir resultado final.
- Errores técnicos de Gemini no son detalle funcional: loguear el error, devolver
NO_CONCLUYENTElimpio y no cachear fallos transitorios del fallback semántico. - Métricas Gemini por tarea: preservar
gemini_extraction,gemini_semanticygemini_totalenphase_timings, incluyendo respuestas malformadas cuando Gemini entregueusageMetadata. - Perfiles Gemini aislados:
mediaResolutionsolo se permite en perfilextraction;semantic_matchdebe ser text-only, con cache semántica versionada. Política de homologación matizada: omisión de marca/modelo/presentación comercial es compatible (los médicos no prescriben marcas), pero omisión de dosis/concentración cuando el otro producto la especifica esunresolved_differences=truey decisión PHP conservadora (INCONCLUSIVE). - Visuales calculables:
VigenciaEntregano se cierra como booleano enDocumentPolicyEngine; Gemini extraevalor,unidadyfecha_base,DocumentNormalizerlos canoniza yRulesEvaluationWorkercalculaFechaEntrega <= fecha_base + valor. Si falta evidencia suficiente en un visual activo, el resultado agregado esNO_CONCLUYENTE. - Identidad canónica E2E:
DisIdde auditoría debe cumplirvw_discolnet_dispensas.DisId == AudDispEst.FacSec(columna legacy).DisDetNro/Dispensase persiste comoAudDispEst.FacNro, que es la PK operativa de resultados;AuditResultPersistenceModelhace el upsert serializable yAuditStatusModelconsulta detalle y timings porFacNro. Verplans/audit-identity-contract.md. - Persistencia justa y atómica: máximo un evento SQL activo por job; nunca separar el resumen, los hallazgos del adjunto y la trazabilidad en transacciones distintas.
- Sin bypass de scheduling: está prohibido publicar
rules_evaluatedmedianteAuditEventPublisher; usar siempreAuditPersistenceQueue. - Fallo técnico no es rechazo documental: PDO, SQL Server, Drive, fuente ausente/vacía durante descarga y transferencia parcial deben propagarse como excepciones técnicas. Está prohibido emitir
DOWNLOAD_ERROR,document_rejectedo un hallazgo funcional desde descarga. La ausencia o falta de contenido detectada durante la reconciliación de metadata sí pertenece aDOCUMENT_MAPPINGy ocurre antes de encolar descarga. - Productores segregados y defensa en profundidad:
DocumentAuditOrchestratorsolo publica rechazosDOCUMENT_MAPPING;DocumentExtractionWorkersolo publica rechazosdocument_content.RulesEvaluationWorkervalida categoría/clase, origen y allowlist, yAuditPersistenceWorkerrepite la barrera antes de SQL. - Reconciliación cerrada 1:1: no seleccionar el primer candidato ni reutilizar un
attachment_id. La única API del matcher esmatchAll()y las únicas estrategias válidas sonexact_name,validated_idyunique_alias.
Aplicabilidad condicional por servicio (AplicaServicio)
Vigencia de entrega por cliente
DocumentAuditOrchestrator copia auditConfig.diasVigencia a dias_vigencia
del estado Redis. Conserva null y evita otra consulta desde policy.
DeliveryValidityEvaluator prioriza evidencia visual completa, luego entero
positivo configurado y finalmente 60 días. Usa el enum interno
DeliveryValiditySource para distinguir el origen en detalle; no serializa
el enum ni cambia la firma pública evaluate(array, array).
El sistema soporta aplicabilidad condicional declarativa por modalidad de servicio (AplicaServicio: TODOS, POS, MIPRES, etc.):
DocumentAuditOrchestrator::resolveServiceType()extrae dinámicamente la clave canónica'Tipo'de los ítems de la Fuente de Verdad ($fuenteVerdad['items']).- Al armar
configuredDocuments,DocumentAuditOrchestratorfiltra en memoria losfieldsyvisualCheckssegún el servicio resuelto ($aplica === 'TODOS' || $serviceType === 'TODOS' || $aplica === $serviceType). - Los campos o checks visuales no aplicables (por ejemplo
FirmaPrescriptorconAplicaServicio = 'POS'en una entrega conTipo = 'MIPRES') se eliminan en PHP antes de compilar el contrato de Gemini (DocumentExtractionContractBuilder), ahorrando tokens y previniendo falsos rechazos en prescripciones electrónicas. AuditConfigModel::getConfig()retornaaplicaServicioen cada campo y check visual;AuditConfigController::sanitizeFields()lo valida yAuditConfigModel::replaceFields()lo persiste enDiscolnet.dbo.AudDispCampo.
Implicación operativa:
- Si un campo tiene
AplicaServicio = 'TODOS', se audita siempre en cualquier tipo de entrega. - Si un campo tiene
AplicaServicio = 'POS', solo se audita cuandoitems[].Tipo === 'POS'. - Si un campo tiene
AplicaServicio = 'MIPRES', solo se audita cuandoitems[].Tipo === 'MIPRES'. OMITIDOpuede aparecer adicionalmente por condiciones internas del engine cuando hay ausencia simultánea de valor FDV y valor documental auditable.
Agregación de items en reglas B
Para TipoCampo = B, DocumentPolicyEngine suma los items de la FDV antes de comparar contra valorDocumento. Caso real T38250701547 (NitSec 2426): la FDV tiene 2 items con CantidadEntregada = 20 y 30; el hallazgo persistido reporta valorFuenteVerdad: "50", valorDocumento: "50", tipo_auditoria: "business", resultado: COINCIDE. Implicación: nunca documentar reglas B como "campo a campo" — son agregadas a nivel documento.
Contrato real de hallazgo (v1 — AUDIT-016)
Forma canónica del objeto en AudDispEst.Hallazgos[*]:
{
"severidad": "alta|media|baja",
"campo": "<nombre>",
"documento": "DISPENSA|AUTORIZACION|FORMULA MEDICA|...",
"valorDocumento": "<valor extraído por Gemini>",
"valorFuenteVerdad": "<valor de la FDV>",
"resultado": "COINCIDE|VALOR_DISTINTO|NO_ENCONTRADO|OMITIDO|NO_CONCLUYENTE|RECHAZADO",
"detalle": "<string|null>",
"tipo_auditoria": "exact|semantic|business|visual|integrity",
"valueType": "text|date|quantity|money|identity_doc_type|identity_doc_number|code|trace_token|person_name|institution_name|article_name",
"valoresFuenteVerdad":["5D03364","5G00989"], // opcional para CODE/TRACE_TOKEN
"valoresDocumento": ["5D03364","5G00989"] // opcional para CODE/TRACE_TOKEN
}
[!NOTE]
valueType,valoresFuenteVerdadyvaloresDocumentoson campos v1 inyectados porbuildDataFinding().valueTypedebe salir delTipoDatoexplícito delaudit-config, no del nombre del campo. Para resultados fallidos (VALOR_DISTINTO,NO_ENCONTRADO,NO_CONCLUYENTEyRECHAZADOcuando haya código disponible), eldetalleinicia con el prefijo textual-<codigoCampo>-tomado deAudDispCampo.CodigoCampo; no se agrega una propiedad pública separada.
El contrato runtime actual no incluye rol en hallazgos. Visual checks booleanos emiten un objeto similar, sin valueType; los visuales calculables como VigenciaEntrega emiten tipo_auditoria: "visual" desde la agregación de reglas.
Nota Gemini 3.x — thinking tokens
En Gemini 3.x los thinking tokens pueden superar 4× los output tokens (caso T38250701547: 5 594 thinking vs 1 177 output en extracción). Considerar al ajustar GEMINI_*_MAX_OUTPUT_TOKENS y GEMINI_*_THINKING_BUDGET para no truncar respuestas válidas.
Anti-patterns ⚠️
- No consultar vistas SQL directamente desde workers para FDV/adjuntos — usar
AuditDataServiceyAttachmentDownloadService. - No incluir base64, binarios o credenciales en el payload de eventos (solo en claves de estado Redis).
- No inyectar valores FDV esperados en el prompt de extracción; Gemini extrae evidencia visible y PHP compara.
- No fabricar
itemsdesdefieldsen normalizador o policy. - No borrar mensajes de streams; dejar ack/retry/DLQ hacer su trabajo.
- No mezclar dos responsabilidades en un worker: cada etapa publica exactamente un evento siguiente.
Ejemplos
Validar contrato POST /audit/single
curl -X POST http://localhost:8080/audit/single \
-H "Content-Type: application/json" \
-d '{"DisDetNro":"T38250701547"}'
# Respuesta esperada: 202 { "data": { "audit_id": "...", "status": "pending", ... } }
Golden case (validación humana obligatoria)
Invoke-RestMethod -Uri "http://localhost:8080/audit/single" `
-Method POST -ContentType "application/json" `
-Body '{"DisDetNro":"T38250701547"}' | ConvertTo-Json -Depth 20
Resultado esperado: EstadoDetallado: "manual_review", 34 coincidencias, 1 discrepancia (NO_ENCONTRADO CodigoDiagnostico en FORMULA MEDICA), 1 NO_CONCLUYENTE (NombreArticulo por homologación de artículo).
Levantar los workers localmente
php bin/audit-worker.php orchestrator &
php bin/audit-worker.php downloader &
php bin/audit-worker.php extraction &
php bin/audit-worker.php normalizer &
php bin/audit-worker.php policy &
php bin/audit-worker.php persistence &
Listar DLQ
curl http://localhost:8080/audit/dlq?limit=20
Checklist rápido
POST /audit/singleresponde 202 conaudit_id.POST /audit/asyncresponde 202 conjob_id.- Workers procesan el flujo completo para
T38250701547(cliente 2426). - Búsqueda de nombres legacy de cola y llamadas Redis list (
LPUSH/BRPOP) sin coincidencias; tampoco debe existir orquestador monolítico anterior enapp/Services/Audit/. audit.dlqrecibedead_letteral agotar reintentos.- Persistencia final transaccional en
AudDispEst+AdjuntosDispensacion+DispensacionDetalleServicio. - PHPUnit completo verde antes de merge; suite organizada bajo
tests/. php vendor/bin/phpunit tests/Services/Audit/GoldenSetReplayTest.php --no-coveragevalida los fixtures golden.- Hallazgos de tipo
CODE/TRACE_TOKENincluyenvalueTypey arrays de valores FDV/documento cuando aplican. - Multi-item divergente emite
NO_CONCLUYENTEconambiguous=true, no silencio.
⚠️ Auto-Sync (OBLIGATORIO post-implementación)
Después de cualquier cambio en el pipeline event-driven:
- Verificar que los archivos listados en "Archivos clave" existan.
- Confirmar que streams y consumer groups siguen alineados con
AuditEventPublisheryAuditEventConsumer. - Ejecutar
audfact-docs-synccomo segunda capa.
[!CAUTION] Dejar la skill desactualizada genera drift que confunde a agentes futuros.
Referencias
- Sprint checkpoints:
plans/checkpoints/ - Skill asociada para modelos SQL:
audfact-sqlsrv-models - Mapeo TipoCampo → tipo de comparación:
app/Services/Audit/AuditComparisonType.php - TipoDato explícito y reglas de compatibilidad TipoCampo/TipoDato:
app/Services/Audit/AuditFieldValueType.php - Casos de validación E2E:
plans/changelog.md(entradas DOCS-SYNC y AUDIT-014/015)