94 KiB
Contrato de implementacion: ciclo de vida del conocimiento y OCR
Proyecto: Workspace de tools IA para empresas Modulo: RAG Ultima actualizacion: 2026-09-22 Ultima modificacion por: Agente RAG 2 Estado: Punto 2 validado; pipeline OCR desplegado; aceptacion productiva bloqueada hasta completar el hardening de runtime definido en este contrato
Resultado esperado
Este documento define como completar, en este orden estricto:
- el pendiente 2, ciclo de vida del conocimiento;
- el pendiente 3, OCR integrado en la ingesta.
El punto 3 no debe implementarse ni desplegarse hasta que el punto 2 haya sido validado en produccion. El OCR necesita versionado, estados y activacion controlada para no sustituir conocimiento correcto por una extraccion defectuosa.
Decisiones cerradas
| Area | Decision |
|---|---|
| Catalogo | PostgreSQL 17 es la fuente de verdad para fuentes, versiones, estados y version activa. |
| Vectores | Qdrant conserva chunks y embeddings versionados, pero no decide que version esta activa. |
| Visibilidad | Toda recuperacion filtra por los source_version_id activos resueltos en PostgreSQL. |
| Atomicidad | Los puntos nuevos se escriben primero como version invisible; una transaccion PostgreSQL cambia despues el puntero activo. |
| Rollback | Se cambia el puntero activo a una version anterior verificada; no se recalculan embeddings. |
| OCR principal | PaddleOCR 3.4.0 con PaddlePaddle CPU 3.2.2. |
| Despliegue OCR | Servicio Python privado e independiente, en la red interna de EasyPanel y sin dominio publico. |
| Revision OCR | Si una sola pagina usa OCR, la version queda en review_required y no se indexa hasta aprobacion explicita. |
| OCRmyPDF | Fuera del pipeline productivo y sin fallback automatico. Solo puede conservarse como herramienta offline de diagnostico. |
| Fallos | Ante cualquier fallo se conserva la version activa anterior. Nunca se recupera sin filtro desde Qdrant. |
Evidencia de partida
Defectos confirmados en el pipeline actual
IngestServiceescribe directamente en Qdrant y marca todos los chunks comoactive.- El retrieval no filtra el campo
status. - Los IDs fisicos no incluyen version; una reingesta sobrescribe posiciones coincidentes.
- Si un documento nuevo tiene menos chunks, permanecen chunks sobrantes de la version anterior.
- Una ingesta parcial de carpeta puede mezclar conocimiento antiguo y nuevo.
- No existen hash de contenido, version, rollback, catalogo ni reconciliacion.
/sourcesreconstruye fuentes recorriendo los puntos de Qdrant.- Los uploads individuales usan una ruta temporal con timestamp para construir
document_key. - El parser PDF usa
pdf-parsesobre el documento completo y no evalua calidad pagina a pagina. multer.memoryStorage()mantiene el upload completo en RAM y no impone limite especifico.
Infraestructura confirmada
- PostgreSQL 17 ya esta desplegado en
easypanel-ia_servicioscon almacenamiento persistente. - Qdrant contiene aproximadamente 22.605 puntos de dimension 4096 y no tiene indices de payload.
- VPS2 dispone de 6 vCPU y 11 GiB de RAM, sin swap.
- El contenedor RAG actual no tiene Python ni dependencias OCR y debe mantenerse ligero.
Benchmark OCR real
Documento: Errores Junio 2026.pdf, 25 paginas y 34 entradas verificadas manualmente.
| Motor | Tiempo | Resultado relevante |
|---|---|---|
| OCRmyPDF 17.11.0 + Tesseract | 55 s | Recupero SQLSTATE, 23505 y FE666, pero omitio varios codigos criticos. |
| PaddleOCR 3.4.0 + PaddlePaddle 3.2.2, CPU 3 cores/5 GiB | 245 s | Recupero todas las lineas criticas perdidas por Tesseract, pero confundio 0/O/o en cuatro codigos. |
Errores PaddleOCR observados incluso con confianza entre 0.9669 y 0.9846:
| Canonico | OCR bruto |
|---|---|
CBG04a |
CBGO4a |
FAT07 |
FATo7 |
DSAU08 |
DSAuo8 |
NSAV06 |
NSAvo6 |
Conclusion: PaddleOCR aporta mejor cobertura, pero la confianza numerica no garantiza integridad de identificadores. No se deben autocorregir caracteres ambiguos ni activar automaticamente contenido OCR.
Punto 2. Ciclo de vida del conocimiento
Alcance
El punto 2 debe entregar:
- catalogo persistente de fuentes y versiones;
- identidad estable de fuente y documento;
- deteccion de ingestas identicas;
- versiones fisicas aisladas en Qdrant;
- activacion atomica desde la perspectiva del retrieval;
- rollback sin recalcular embeddings;
- purga auditable de versiones no activas;
- migracion controlada de los puntos existentes;
- reconciliacion entre PostgreSQL y Qdrant.
No incluye OCR, busqueda hibrida, autenticacion general de la API ni rediseño del chunking.
Modelo de autoridad
PostgreSQL responde a estas preguntas:
- que fuentes existen;
- que versiones tiene cada fuente;
- cual es la unica version activa;
- que documentos y hashes forman una version;
- si una version esta preparada, fallo, fue reemplazada o fue purgada.
Qdrant responde solo a estas preguntas:
- que chunks fisicos forman una version;
- que vector y payload tiene cada chunk;
- que chunks de las versiones autorizadas son semanticamente relevantes.
El campo write_state de Qdrant sera diagnostico. La visibilidad siempre depende de rag_sources.active_version_id.
Estados de version
pending
-> indexing
-> review_required # reservado para el punto 3
-> ready
-> active
-> superseded
-> active # rollback
-> purging -> purged
-> failed
-> indexing # reintento explicito
-> purging -> purged
review_required -> indexing # tras aprobacion OCR
review_required -> rejected
rejected -> purging -> purged
ready -> purging -> purged
Reglas obligatorias:
- Puede haber cero o una version
activepor fuente. - Una version
activeno se puede purgar. readyexige que todos los documentos y puntos esperados hayan sido verificados.supersedednunca participa en retrieval normal, pero permanece disponible para rollback.failedpuede conservar puntos incompletos, siempre invisibles.purgedconserva el registro historico, pero no tiene puntos ni artefactos fisicos.review_requiredyrejectedse incorporan en el esquema desde el punto 2 para no migrar la maquina de estados durante el punto 3.
Esquema PostgreSQL minimo
Crear migrations/001_knowledge_lifecycle.sql con este contrato logico. La implementacion puede ajustar nombres de constraints, no columnas ni invariantes sin actualizar antes este documento.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE rag_sources (
source_id text PRIMARY KEY,
tenant text NOT NULL DEFAULT 'default',
source_type text NOT NULL CHECK (source_type IN ('file', 'folder')),
source_ref text NOT NULL,
active_version_id uuid NULL,
needs_reingest boolean NOT NULL DEFAULT false,
disabled_at timestamptz NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE rag_source_versions (
version_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
source_id text NOT NULL REFERENCES rag_sources(source_id) ON DELETE RESTRICT,
version_number bigint NOT NULL CHECK (version_number > 0),
previous_version_id uuid NULL,
state text NOT NULL CHECK (state IN (
'pending', 'indexing', 'review_required', 'ready', 'active',
'superseded', 'failed', 'rejected', 'purging', 'purged'
)),
original_manifest_hash char(64) NOT NULL,
source_content_hash char(64) NULL,
processing_fingerprint char(64) NOT NULL,
metadata_hash char(64) NOT NULL,
tags text[] NOT NULL DEFAULT '{}',
activate_requested boolean NOT NULL DEFAULT true,
base_active_version_id uuid NULL,
embedding_provider text NOT NULL,
embedding_model text NOT NULL,
embedding_dimensions integer NOT NULL CHECK (embedding_dimensions > 0),
qdrant_collection text NOT NULL,
expected_document_count integer NOT NULL DEFAULT 0 CHECK (expected_document_count >= 0),
expected_point_count integer NOT NULL DEFAULT 0 CHECK (expected_point_count >= 0),
verified_point_count integer NOT NULL DEFAULT 0 CHECK (verified_point_count >= 0),
error_code text NULL,
error_detail text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
indexing_started_at timestamptz NULL,
ready_at timestamptz NULL,
activated_at timestamptz NULL,
superseded_at timestamptz NULL,
reviewed_at timestamptz NULL,
reviewed_by text NULL,
artifact_state text NOT NULL DEFAULT 'none'
CHECK (artifact_state IN ('none', 'present', 'retention_deleting', 'retention_deleted')),
retention_due_at timestamptz NULL,
purged_at timestamptz NULL,
UNIQUE (source_id, version_number),
UNIQUE (version_id, source_id),
FOREIGN KEY (previous_version_id, source_id)
REFERENCES rag_source_versions(version_id, source_id)
DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (base_active_version_id, source_id)
REFERENCES rag_source_versions(version_id, source_id)
DEFERRABLE INITIALLY DEFERRED
);
ALTER TABLE rag_sources
ADD CONSTRAINT rag_sources_active_version_fk
FOREIGN KEY (active_version_id, source_id)
REFERENCES rag_source_versions(version_id, source_id)
DEFERRABLE INITIALLY DEFERRED;
CREATE UNIQUE INDEX rag_one_active_version_per_source
ON rag_source_versions(source_id)
WHERE state = 'active';
CREATE UNIQUE INDEX rag_one_reusable_content_version
ON rag_source_versions(source_id, source_content_hash, processing_fingerprint, metadata_hash)
WHERE source_content_hash IS NOT NULL
AND state NOT IN ('failed', 'rejected', 'purged');
CREATE INDEX rag_source_versions_source_state_idx
ON rag_source_versions(source_id, state);
CREATE TABLE rag_version_documents (
version_id uuid NOT NULL REFERENCES rag_source_versions(version_id) ON DELETE RESTRICT,
document_id text NOT NULL,
document_key text NOT NULL,
original_hash char(64) NOT NULL,
original_hash_kind text NOT NULL CHECK (original_hash_kind IN ('bytes', 'legacy-derived')),
content_hash char(64) NULL,
mime_type text NOT NULL,
title text NOT NULL,
extraction_method text NOT NULL DEFAULT 'native',
index_state text NOT NULL CHECK (index_state IN ('pending', 'indexing', 'ready', 'failed')),
chunk_count integer NOT NULL DEFAULT 0 CHECK (chunk_count >= 0),
artifact_manifest_path text NULL,
error_detail text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (version_id, document_id),
UNIQUE (version_id, document_key)
);
CREATE INDEX rag_version_documents_document_idx
ON rag_version_documents(document_id);
CREATE TABLE rag_ingestion_attempts (
attempt_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
source_id text NOT NULL REFERENCES rag_sources(source_id) ON DELETE RESTRICT,
version_id uuid NULL REFERENCES rag_source_versions(version_id) ON DELETE SET NULL,
state text NOT NULL CHECK (state IN ('received', 'processing', 'completed', 'failed')),
input_locator text NOT NULL,
input_hash char(64) NULL,
error_code text NULL,
error_detail text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz NULL
);
CREATE TABLE rag_system_state (
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
maintenance boolean NOT NULL DEFAULT false,
maintenance_reason text NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO rag_system_state(singleton) VALUES (true);
CREATE TABLE rag_legacy_migration_batches (
batch_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
state text NOT NULL CHECK (state IN ('planned', 'running', 'verified', 'rolled_back', 'failed')),
snapshot_ref text NOT NULL,
report jsonb NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz NULL
);
CREATE TABLE rag_legacy_migration_items (
batch_id uuid NOT NULL REFERENCES rag_legacy_migration_batches(batch_id) ON DELETE RESTRICT,
source_id text NOT NULL,
version_id uuid NULL REFERENCES rag_source_versions(version_id) ON DELETE SET NULL,
expected_point_count integer NOT NULL CHECK (expected_point_count >= 0),
state text NOT NULL CHECK (state IN ('planned', 'migrated', 'verified', 'rolled_back', 'failed')),
error_detail text NULL,
PRIMARY KEY (batch_id, source_id)
);
Las migraciones se ejecutaran desde Node usando pg, una tabla rag_schema_migrations con nombre y checksum, y un advisory lock global. El arranque usa node dist/modules/catalog/migrations.js && node dist/server.js: una migracion fallida o con checksum alterado impide arrancar y, por tanto, impide readiness.
La FK compuesta garantiza que el puntero pertenece a la fuente, pero no que su estado sea active. La migracion debe añadir constraint triggers diferidos sobre rag_sources y rag_source_versions que, al commit, validen ambas direcciones:
- todo
active_version_idno nulo apunta a una versionactivede la misma fuente; - toda version
activees elactive_version_idde su fuente.
El repositorio de catalogo es el unico codigo autorizado a mutar estos estados. El reconciliador vuelve a comprobar la misma invariante para detectar corrupcion externa.
Identidad y hashes
Mantener la jerarquia actual:
source_id = src:<tenant>:<source_kind>:<stable_name>
document_id = doc:<source_id>:<document_key>
chunk_id = chk:<document_id>:<chunk_mode>:<0001>
Reglas nuevas:
document_keyde un archivo subido se deriva desource_ref, nunca dereadPathni del timestamp temporal.source_idexplicito tiene prioridad. Si se genera, se deriva de una referencia logica estable.original_hashes SHA-256 de los bytes originales cuandooriginal_hash_kind = bytes; para legacy sin original usa el hash derivado de chunks yoriginal_hash_kind = legacy-derived.original_manifest_hashes SHA-256 de los pares ordenadosdocument_key + NUL + original_hash; siempre existe antes de parsear.content_hashes SHA-256 del contenido final normalizado. Puede ser nulo mientras una extraccion OCR espera revision.source_content_hashusa los pares ordenadosdocument_key + NUL + content_hash; puede ser nulo hasta que todo el contenido final exista.processing_fingerprintcubre version del parser, politica de normalizacion, politica de chunking y proveedor/modelo/dimensiones de embeddings.metadata_hashes SHA-256 del JSON canonico de tags y metadatos recuperables; un cambio de tags crea una version aunque el contenido sea identico.- Una coincidencia reutilizable solo aplica a versiones no fallidas, no rechazadas y no purgadas.
Serializacion canonica:
- todos los hashes reciben bytes UTF-8;
- el contenido elimina BOM, convierte CRLF/CR a LF, elimina espacios finales por linea, elimina lineas vacias exteriores y termina con un unico LF;
- los documentos se ordenan por bytes UTF-8 de
document_keyascendente; - el fingerprint usa JSON con claves ordenadas lexicograficamente, sin espacios y con valores de version explicitos;
- el point ID usa UUIDv5 con namespace URL
6ba7b811-9dad-11d1-80b4-00c04fd430c8y nombrerag-qdrant-point:<source_version_id>:<chunk_id>.
Comportamiento de una coincidencia:
| Estado encontrado | Resultado |
|---|---|
active |
noOp: true; no escribir puntos. |
ready o superseded |
Reutilizar esa version y activarla solo con precondicion explicita. |
pending, indexing o review_required |
409 con la version en curso. |
failed, rejected o purged |
Crear una version nueva; no devolver noOp. |
El ID fisico de Qdrant incorpora la version:
point_id = UUIDv5(namespace_url, "rag-qdrant-point:" + source_version_id + ":" + chunk_id)
No usar el hash dentro de source_id, document_id o chunk_id; esos IDs representan identidad logica estable.
Payload obligatorio de Qdrant
{
"source_id": "src:default:folder:workspace-docs",
"source_version_id": "uuid",
"source_version_number": 2,
"source_type": "folder",
"source_ref": "docs",
"document_id": "doc:...",
"document_key": "docs/readme.md",
"document_content_hash": "sha256",
"chunk_id": "chk:...:0001",
"chunk_mode": "documental",
"chunk_index": 0,
"start_line": 1,
"end_line": 20,
"title": "readme.md",
"section_title": "...",
"mime_type": "text/markdown",
"content": "...",
"tags": ["project-docs"],
"embedding_provider": "openrouter",
"embedding_model": "qwen/qwen3-embedding-8b",
"embedding_dimensions": 4096,
"processing_fingerprint": "sha256",
"indexed_at": "ISO-8601",
"write_state": "staged"
}
Crear indices de payload Qdrant para:
source_id;source_version_id;document_id;chunk_mode;tags.
Secuencia de ingesta y activacion
- Resolver la identidad logica, crear/actualizar
rag_sourcesy registrarrag_ingestion_attempts.state = receivedantes de descubrir archivos. - Adquirir un advisory lock PostgreSQL derivado de
source_id; descubrir originales y calcularoriginal_manifest_hashsin crear aun una version. - Rechazar con
422una fuente sin documentos soportados o sin contenido util; nunca reemplazar por una version vacia. - Calcular hashes y
processing_fingerprint. - Aplicar la tabla de coincidencias anterior;
noOpsolo corresponde a una version activa util. - Crear una sola version
pending, asignarversion_number, persistiractivate_requested,base_active_version_id, tags y documentos. - Cambiarla a
indexingy generar embeddings. - Escribir puntos versionados en Qdrant con IDs deterministas.
- Contar puntos por
source_version_idy verificar documentos, dimensiones y cantidad esperada. - Cambiar la version a
ready. - En una transaccion PostgreSQL, bloquear la fuente, validar la precondicion obligatoria
expectedActiveVersionId, pasar la activa anterior asuperseded, pasar la nueva aactivey actualizaractive_version_id. - Responder solo despues del commit.
No existe transaccion distribuida entre PostgreSQL y Qdrant. La garantia correcta es que una escritura parcial en Qdrant permanece invisible hasta el cambio atomico del puntero PostgreSQL.
Retrieval y listado
GET /sourcesconsulta PostgreSQL y deja de recorrer Qdrant.search()ybrowseScope()resuelven primero las versiones activas aplicables.- El filtro Qdrant siempre incluye
source_version_idactivo ademas de modo y scope. - Si PostgreSQL no esta disponible, retrieval falla con
503; nunca busca sin filtro como fallback. - Los resultados y citas añaden
sourceVersionIdysourceVersionNumber. - Las fuentes deshabilitadas no participan en retrieval.
- retrieval mantiene un advisory lock compartido por las versiones resueltas hasta terminar la consulta Qdrant; purga usa lock exclusivo sobre las mismas claves.
- una fuente activa con recuento o dimensiones inconsistentes queda bloqueada con
503 SOURCE_VERSION_INCONSISTENThasta rollback o reparacion; nunca devuelve un corpus parcial como valido.
Semantica de scope:
sourceId,sourceRefy cada tag recibido se combinan con AND;- varios tags significan que todos deben estar presentes, igual que en el comportamiento actual;
sourceRefusa coincidencia exacta sobre la fuente activa;tenantesdefaultmientras el contrato publico no lo exponga y nunca se cruza entre fuentes;- los tags viven en
rag_source_versionsy Qdrant como snapshot; cambiar tags alterametadata_hashy exige nueva version.
GET /sources conserva sourceId, sourceRef, chunkModes y tags para no romper el playground, y añade activeVersionId, activeVersionNumber, state, needsReingest y updatedAt.
Contrato HTTP del punto 2
Ingesta
POST /ingest y POST /ingest/upload aceptan ademas:
{
"activate": true,
"expectedActiveVersionId": "uuid-o-null"
}
Si activate es true, expectedActiveVersionId es obligatorio: UUID para reemplazo y null para primera activacion. Omitirlo devuelve 400; una diferencia devuelve 409. Si activate es false, se omite porque no cambia el puntero.
Respuesta sincronica 201 Created:
{
"accepted": true,
"sourceId": "...",
"versionId": "uuid",
"versionNumber": 2,
"state": "active",
"previousVersionId": "uuid",
"documentsProcessed": 10,
"chunksStored": 80,
"activated": true,
"noOp": false,
"collectionName": "rag_chunks"
}
activate: false deja la version en ready. El cambio de 202 a 201 debe anunciarse en API_RAG.md y verificarse en consumidores conocidos.
Errores:
409: cambio concurrente de version activa o conflicto de idempotencia;422: fuente vacia, no soportada o inconsistente;503: PostgreSQL, embeddings o Qdrant no disponibles;500: fallo interno registrado en la version.
Catalogo y operaciones
GET /sources
GET /sources/:sourceId
GET /sources/:sourceId/versions
GET /sources/:sourceId/versions/:versionId
POST /sources/:sourceId/versions/:versionId/activate
POST /sources/:sourceId/rollback
DELETE /sources/:sourceId/versions/:versionId
Rollback:
{
"targetVersionId": "uuid",
"expectedActiveVersionId": "uuid"
}
Las operaciones de activacion, rollback y purga exigen Authorization: Bearer <LIFECYCLE_ADMIN_TOKEN>. El secreto se configura solo en EasyPanel, se compara en tiempo constante y nunca se registra. Ausente o invalido devuelve 401; el OpenAPI define bearerAuth. Esta proteccion acotada no sustituye la autenticacion general del pendiente 5.
POST /cleanup queda deprecado. Durante una version de transicion puede traducirse a purga de versiones no activas, pero nunca puede borrar implicitamente la version activa.
Reintentos, reconciliacion y purga
| Fallo | Comportamiento obligatorio |
|---|---|
| Descubrimiento o parsing | Marcar el intento y la version failed cuando ya exista; mantener activa anterior. |
| Upsert parcial | Exigir Qdrant wait=true, validar estado de operacion y reintentar con los mismos IDs fisicos deterministas. |
| Qdrant completo y PostgreSQL sin actualizar | El reconciliador cuenta puntos y recupera a ready o marca failed. |
| Activacion PostgreSQL fallida | El puntero anterior permanece activo. |
| Respuesta HTTP perdida tras commit | Reintento devuelve la version ya activa sin duplicarla. |
Proceso muerto en indexing |
Reintenta puntos solo si los originales siguen disponibles; un upload temporal sin original durable se marca failed. |
| Purga parcial | Usar lock exclusivo, wait=true, mantener purging, repetir delete y verificar lectura consistente con cero puntos. |
| Puntos activos ausentes | Marcar needs_reingest, bloquear esa fuente con 503 y exigir rollback o reparacion explicita. |
Ejecutar reconciliacion al arrancar y periodicamente. Conservar al menos una version superseded verificada hasta que la nueva haya superado la validacion funcional en produccion.
Migracion del corpus actual
La migracion debe ser explicita y reversible:
- Añadir codigo y esquema con
KNOWLEDGE_LIFECYCLE_ENFORCED=false. - Bloquear en EasyPanel/Traefik las rutas de escritura o detener la instancia RAG antigua; no confiar en que codigo legacy consulte PostgreSQL. Desplegar el codigo nuevo con
INGEST_WRITES_ENABLED=false, tomar advisory lock global exclusivo y ponerrag_system_state.maintenance=true. Toda creacion/activacion nueva comprueba esa fila bajo lock y devuelve503 maintenance; esperar a que no haya ingestas activas. - Ejecutar un
dry-runque agrupe puntos porsource_idy reporte colisiones, referencias incompatibles, modelos, dimensiones y recuentos. - Detener la migracion ante un
source_idasociado a varias referencias incompatibles o proveedores/dimensiones incompatibles. - Crear un snapshot Qdrant y una fila
rag_legacy_migration_batchescon esa referencia antes de modificar payloads. - Crear un
rag_legacy_migration_itemspor fuente y una version 1 asociada albatch_id. - Poblar cada documento legacy agrupando por
document_id; derivardocument_key, titulo, MIME, chunk count y hash de contenido ordenado. Si falta identidad documental fuerte, bloquear esa fuente. - Poblar proveedor, modelo, dimensiones y coleccion desde los payloads, exigiendo un unico valor por version.
- Marcar
original_manifest_hashysource_content_hashcomo derivados legacy; no presentarlos como hashes de archivos originales. - Añadir a cada punto
source_version_id,source_version_number: 1yprocessing_fingerprintigual a SHA-256 delegacy:<batch_id>. - Verificar documentos, recuentos PostgreSQL/Qdrant y payloads; marcar item y lote
verified. - Activar version 1 en PostgreSQL.
- Activar
KNOWLEDGE_LIFECYCLE_ENFORCED=true, redeployar y validar retrieval. - Reactivar
INGEST_WRITES_ENABLED=truesolo despues de la validacion. - Eliminar el camino legacy tras la validacion final, no mantener compatibilidad indefinida.
Rollback de migracion:
- Bloquear en Traefik lectura y escritura del RAG y mantener
rag_system_state.maintenance=true. - En una transaccion diferida, bloquear las fuentes del lote, cambiar sus versiones
activeapurgingy ponerrag_sources.active_version_id = null; confirmar que no queda ningun puntero activo del lote. - Detener las instancias con enforcement antes de restaurar Qdrant, para impedir retrieval contra un estado intermedio.
- Restaurar el snapshot Qdrant y verificar sus recuentos legacy.
- En transaccion, poner a nulo
rag_legacy_migration_items.version_id, eliminar documentos y versiones del lote, y marcar items/loterolled_back. - Desplegar con enforcement desactivado, verificar retrieval legacy y solo entonces reabrir trafico e ingesta.
No se admite migracion concurrente con nuevas escrituras, retrieval durante restauracion ni un modo dual-write implicito.
Archivos del punto 2
Crear:
migrations/001_knowledge_lifecycle.sql;src/modules/catalog/client.ts;src/modules/catalog/repository.ts;src/modules/catalog/migrations.ts;src/modules/catalog/reconciler.ts;src/scripts/migrate-legacy-lifecycle.ts.
Modificar:
package.jsony lockfile parapg, tipos y scripts de migracion;src/config/env.tspara PostgreSQL, secreto administrativo y feature flag temporal;src/shared/types/rag.tspara versiones, estados y respuestas;src/shared/utils/ids.tspara point IDs versionados y hashes;src/modules/ingest/service.tspara staging, verificacion y activacion;src/modules/vectorstore/client.tspara filtros e indices de payload;src/modules/retrieve/service.tspara resolver versiones activas;src/modules/answer/service.tspara propagar version en citas;src/app.ts,src/api/openapi.tsy playground para el nuevo contrato;Dockerfilepara arrancar connode dist/modules/catalog/migrations.js && node dist/server.js;docs/API_RAG.md,docs/INGESTA.mdy documentación de despliegue.
Orden de tareas del punto 2
- Añadir esquema, migrador, configuracion y health de PostgreSQL.
- Implementar repositorio de catalogo y tests de invariantes.
- Versionar IDs y payloads Qdrant; crear indices de payload.
- Refactorizar ingesta para staging, no-op, verificacion y activacion.
- Hacer que retrieval y
/sourcesdependan del catalogo. - Añadir API de versiones, rollback y purga administrativa.
- Implementar reconciliador y migrador legacy con
dry-run. - Actualizar playground, OpenAPI y documentacion.
- Ejecutar
npm run check,npm run buildy validaciones estaticas. - Commit y push; desplegar con enforcement desactivado.
- Ejecutar dry-run, snapshot, migracion y validacion de recuentos en produccion.
- Activar enforcement, redeployar y ejecutar criterios funcionales.
Criterios de aceptacion del punto 2
- Una ingesta inicial crea version 1 y solo ella aparece en retrieval.
- Una ingesta identica devuelve
noOp: truey no duplica version ni puntos. - Una version con un documento acortado no expone chunks antiguos.
- Una version de carpeta sin un archivo anterior deja de recuperar ese archivo.
- Un fallo a mitad de ingesta no altera los resultados activos.
- Dos activaciones concurrentes no pueden quedar activas simultaneamente.
- Rollback cambia resultados sin recalcular embeddings.
- No se puede purgar la version activa.
- Una purga interrumpida se puede reanudar.
- Un cambio de modelo o chunking cambia
processing_fingerprint. - PostgreSQL caido provoca
503, no retrieval sin filtro. - El corpus legacy conserva sus scopes y resultados despues de la migracion.
- OpenAPI coincide con rutas, codigos HTTP y esquemas reales.
/healthinforma PostgreSQL, Qdrant y estado del reconciliador.
Puerta de salida del punto 2
El punto 2 solo se marca completado cuando:
- los criterios anteriores se prueban en produccion;
- no existen fuentes legacy sin catalogar, salvo excepciones documentadas;
- se conserva al menos una version apta para rollback de una fuente de prueba;
- el usuario confirma que retrieval y playground siguen operativos;
- se actualizan backlog e historiales.
Punto 3. OCR integrado en la ingesta
Prerrequisito
No iniciar este punto hasta superar la puerta de salida del punto 2. Cada candidato OCR se representara como una rag_source_version no visible.
Alcance
El punto 3 debe entregar:
- extraccion PDF pagina a pagina;
- deteccion de paginas con texto nativo insuficiente;
- OCR privado y asincrono solo sobre paginas necesarias;
- artefactos y metricas auditables;
- deteccion de identificadores de riesgo;
- revision humana obligatoria cuando interviene OCR;
- aprobacion, rechazo y posterior activacion versionada;
- limites de recursos, tamaño, tiempo y retencion.
No incluye correccion automatica con LLM, exposicion publica del servicio OCR ni busqueda hibrida por codigos.
Arquitectura
Cliente
-> RAG API
-> guarda original y manifiesto
-> extrae texto nativo por pagina
-> selecciona paginas OCR
-> envia trabajo al servicio OCR privado
-> construye candidato revisable
-> espera aprobacion
-> genera chunks y embeddings
-> activa usando el contrato del punto 2
RAG API -> red interna EasyPanel -> OCR service Python/PaddleOCR
Responsabilidades:
- RAG posee fuentes, estados, artefactos, revision, embeddings y activacion.
- OCR recibe un PDF y una lista cerrada de paginas, devuelve texto bruto, cajas y confianza.
- OCR no escribe en PostgreSQL del RAG ni en Qdrant y no decide activacion.
Persistencia OCR
Crear migrations/002_ocr_review.sql:
CREATE TABLE rag_ocr_jobs (
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
version_id uuid NOT NULL,
document_id text NOT NULL,
remote_job_id text NULL,
remote_idempotency_key text NOT NULL,
state text NOT NULL CHECK (state IN ('queued', 'running', 'succeeded', 'failed')),
requested_pages integer[] NOT NULL,
completed_pages integer NOT NULL DEFAULT 0,
config_version text NOT NULL,
attempt_count integer NOT NULL DEFAULT 0,
heartbeat_at timestamptz NULL,
lease_expires_at timestamptz NULL,
next_attempt_at timestamptz NULL,
error_code text NULL,
error_detail text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
started_at timestamptz NULL,
completed_at timestamptz NULL,
UNIQUE (version_id, document_id),
FOREIGN KEY (version_id, document_id)
REFERENCES rag_version_documents(version_id, document_id) ON DELETE RESTRICT
);
CREATE TABLE rag_document_pages (
version_id uuid NOT NULL,
document_id text NOT NULL,
page_number integer NOT NULL CHECK (page_number > 0),
extraction_method text NOT NULL CHECK (extraction_method IN ('native', 'ocr', 'blank')),
native_text_hash char(64) NULL,
ocr_text_hash char(64) NULL,
candidate_text_hash char(64) NULL,
reviewed_text_hash char(64) NULL,
metrics jsonb NOT NULL DEFAULT '{}',
risk_tokens jsonb NOT NULL DEFAULT '[]',
blocked_reason text NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (version_id, document_id, page_number),
FOREIGN KEY (version_id, document_id)
REFERENCES rag_version_documents(version_id, document_id) ON DELETE RESTRICT
);
CREATE TABLE rag_review_corrections (
correction_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
version_id uuid NOT NULL,
document_id text NOT NULL,
page_number integer NOT NULL,
line_id text NOT NULL,
expected_line_hash char(64) NOT NULL,
replacement_text text NOT NULL,
reviewed_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
FOREIGN KEY (version_id, document_id, page_number)
REFERENCES rag_document_pages(version_id, document_id, page_number) ON DELETE RESTRICT,
UNIQUE (version_id, document_id, page_number, line_id)
);
Los textos completos viven en los artefactos comprimidos; PostgreSQL conserva hashes, metricas, riesgos, estados y correcciones.
La identidad local del job es version_id + document_id; varios jobs locales pueden reutilizar el mismo remote_idempotency_key y resultado remoto. El dispatcher toma jobs con FOR UPDATE SKIP LOCKED, renueva heartbeat_at y lease_expires_at, y al arrancar recupera leases vencidos. Un reconciliador periodico reenvia con la misma clave remota y recupera remote_job_id sin duplicar OCR.
Contrato asincrono hacia consumidores
La ingesta nativa que no requiere OCR conserva la respuesta sincronica 201 del punto 2. Cuando un PDF necesita OCR:
- se guarda el original durable;
- se crea la version con
source_content_hash = null,activate_requestedybase_active_version_id, y despues el job; - se responde sin esperar los minutos de OCR.
Respuesta 202 Accepted de /ingest o /ingest/upload:
{
"accepted": true,
"sourceId": "...",
"versionId": "uuid",
"versionNumber": 3,
"state": "indexing",
"phase": "ocr_queued",
"statusUrl": "/ingestions/uuid",
"reviewUrl": null,
"activated": false
}
GET /ingestions/:versionId devuelve state, phase, progreso por documentos/paginas, error reintentable, statusUrl y reviewUrl. Fases: native_extracting, ocr_queued, ocr_running, review_required, indexing, ready, active, failed y rejected.
Estados terminales para el trabajo solicitado: active, ready, failed o rejected. review_required espera accion humana y no caduca a activa. Un reintento con la misma identidad y original devuelve el job no terminal existente; si fallo, crea una version nueva salvo reintento administrativo explicito con artefactos aun validos.
Deteccion por pagina
Despues de extraccion nativa calcular:
N: caracteres no blancos normalizados;A: caracteres alfanumericos;W: palabras o tokens;R: proporcion de caracteres de reemplazo/control;rasterCoverage: page-area fraction painted by PDF.js raster operators, derived from transforms and capped at 1; intrinsic pixel dimensions and image count are not routing signals.inkCoverage: proporcion no blanca calculada por el servicio OCR sobre un render de baja resolucion.
La extraccion nativa debe usar un callback por pagina probado con fixture. Si pdf-parse no conserva paginas de forma fiable, se sustituye por una libreria Node con licencia permisiva antes de continuar; no se simulan paginas cortando el texto agregado.
Una pagina tiene texto nativo suficiente solo si cumple todo:
N >= 120
A >= 80
W >= 20
R <= 0.01
Todo texto nativo insuficiente se envia al servicio OCR. El servicio clasifica la pagina como vacia solo si:
inkCoverage < 0.015 AND nonWhitespaceCharactersOCR < 10
Reglas:
N == 0siempre solicita inspeccion OCR; el servicio decide si esta vacia.- Text-rich pages also request OCR when
rasterCoverage >= 0.05; smaller raster marks such as logos do not force OCR. - Una pagina con
inkCoverage >= 0.015no puede clasificarse como vacia aunque el OCR no encuentre texto: falla el quality gate. - Nunca decidir por promedio del documento.
- Umbrales configurables y registrados bajo
detectionPolicyVersion: "pdf-detection-v2".
Composicion del candidato:
- pagina nativa suficiente: usar solo texto nativo;
- pagina OCR: ordenar lineas por bbox superior, bbox izquierda e indice original; el candidato es esas lineas unidas por LF;
textde pagina es una representacion derivada de los registros de linea, no una segunda fuente editable;- cada correccion reemplaza el texto completo de un
lineId, por lo que el texto revisado se reconstruye con el mismo orden; - conservar texto nativo y salida OCR bruta para comparacion;
- pagina vacia verificada: no aportar texto;
- ordenar por
page_numberascendente y separar paginas no vacias con\n\n--- Page <N> ---\n\n; - no concatenar texto nativo insuficiente con OCR porque duplica y mezcla variantes.
Contrato interno del servicio OCR ocr-v1
Esta seccion describe el contrato base entregado en 0.2.0. Para 0.2.1, las subsecciones Identidad y compatibilidad ocr-v2, Calidad bloqueante frente a advertencias y Diagnostico durable del anexo correctivo sustituyen los payloads de identidad, respuestas de estado/resultado/imagen y quality gate aqui definidos. Los campos antiguos no se consideran suficientes para una solicitud ocr-v2.
Crear trabajo
POST /v1/jobs usa multipart/form-data, bearer interno e Idempotency-Key. Limite total: 50 MiB.
Partes obligatorias:
file:application/pdf, bytes del PDF cuyo SHA-256 coincide condocumentSha256;request:application/json, objeto siguiente.
Los numeros de pagina son enteros unicos, ordenados y basados en 1. El servidor rechaza configuraciones no incluidas en su allowlist; el cliente no puede elegir modelos o DPI arbitrarios.
{
"documentSha256": "sha256",
"pages": [4, 7, 8, 14, 15],
"languages": ["es", "en"],
"dpi": 200,
"engine": "paddleocr",
"engineVersion": "3.4.0",
"runtimeVersion": "3.2.2",
"configVersion": "ocr-v1",
"returnLayout": true
}
Idempotency-Key:
<documentSha256>:<configVersion>:<sha256-de-pages>
Respuesta 202:
{
"jobId": "ocr_uuid",
"status": "queued",
"documentSha256": "sha256",
"requestedPages": [4, 7, 8, 14, 15],
"configVersion": "ocr-v1",
"createdAt": "ISO-8601"
}
Consultar trabajo
GET /v1/jobs/:jobId:
{
"jobId": "ocr_uuid",
"status": "queued|running|succeeded|failed",
"completedPages": 2,
"totalPages": 5,
"error": null
}
Obtener resultado
GET /v1/jobs/:jobId/result devuelve por pagina:
{
"schemaVersion": "1",
"jobId": "ocr_uuid",
"documentSha256": "sha256",
"engine": {
"name": "paddleocr",
"version": "3.4.0",
"runtime": "paddlepaddle-3.2.2",
"device": "cpu",
"configVersion": "ocr-v1",
"dpi": 200
},
"pages": [{
"page": 7,
"width": 1654,
"height": 2339,
"processingMs": 9750,
"text": "texto OCR bruto",
"metrics": {
"lineCount": 42,
"nonWhitespaceCharacters": 1380,
"medianConfidence": 0.94,
"p10Confidence": 0.71,
"lowConfidenceLineRatio": 0.07
},
"lines": [{
"lineId": "p7-l12-120-340-245-372",
"text": "FATo7",
"confidence": 0.98,
"bbox": [120, 340, 245, 372]
}]
}]
}
lineId es determinista dentro de documentSha256 + configVersion: pagina, indice de lectura y bbox normalizado. El resultado conserva texto bruto. No normaliza ni autocorrige identificadores.
Otros endpoints:
DELETE /v1/jobs/:jobId
GET /health/live
GET /health/ready
Errores: 400 solicitud invalida, 401/403 autenticacion, 409 conflicto de idempotencia, 413 limite excedido, 422 PDF corrupto/cifrado/no soportado, 429 cola llena y 500/503 fallo de motor.
Quality gate OCR ocr-v1
Esta puerta documenta el comportamiento de 0.2.0. Queda sustituida exclusivamente para trabajos ocr-v2 por la tabla y la persistencia del anexo correctivo. En ocr-v2, p10Confidence < 0.50 aislado es una advertencia y no ejecuta la regla de fallo de esta seccion.
Una pagina OCR produce candidato mecanicamente revisable si cumple:
nonWhitespaceCharacters >= 40
medianConfidence >= 0.80
p10Confidence >= 0.50
lowConfidenceLineRatio <= 0.20
El documento solo llega a review_required cuando:
- cada pagina no vacia tiene texto nativo u OCR util;
- ninguna pagina solicitada fallo;
- numero y orden de paginas coinciden con el original;
- hashes de original, extracciones y manifiesto coinciden;
- ninguna pagina se omitio silenciosamente.
Si no cumple, queda failed con error_code: OCR_QUALITY_BLOCKED. No puede aprobarse; requiere una nueva version mediante reintento con configuracion corregida o transcripcion manual fuera de este alcance. Si cumple y alguna pagina uso OCR, queda review_required.
Identificadores de riesgo
Marcar tokens similares a codigos:
\b[A-Za-z]{2,}[A-Za-z0-9_-]*\d[A-Za-z0-9_-]*\b
Elevar prioridad cuando:
- mezclan mayusculas y minusculas de forma inesperada;
- difieren entre texto nativo y OCR;
- aparecen una sola vez;
- estan junto a
codigo,error,regla,SQLSTATE,estadooidentificador.
No autocorregir O/0, I/1/l, S/5 ni casing. La revision muestra imagen, texto nativo, texto OCR, bbox, confianza y diferencias.
Revision y activacion
Endpoints RAG administrativos:
GET /ingestions/:versionId
GET /ingestions/:versionId/review
POST /ingestions/:versionId/approve
POST /ingestions/:versionId/reject
Todos exigen Authorization: Bearer <LIFECYCLE_ADMIN_TOKEN>, incluidos lectura de revision, imagenes, aprobar y rechazar.
Aprobacion:
{
"candidateSha256": "sha256",
"expectedActiveVersionId": "uuid-o-null",
"reviewedBy": "operator-id",
"corrections": [{
"documentId": "doc:...",
"page": 8,
"lineId": "p8-l05-120-340-245-372",
"expectedLineSha256": "sha256-de-FATo7",
"replacementText": "FAT07"
}]
}
candidateSha256 se calcula sobre JSON canonico UTF-8 con paginas y lineas ordenadas, claves lexicograficas y sin espacios. Cada correccion apunta a una linea unica y exige el hash del texto previo; una linea repetida, bbox cambiado o candidato obsoleto devuelve 409 sin aplicar correcciones parciales.
Secuencia:
- Verificar que
candidateSha256y todos losexpectedLineSha256siguen vigentes, y queexpectedActiveVersionIdcoincide conbase_active_version_idpersistido. - Aplicar todas las correcciones en una unica operacion y guardar texto revisado inmutable.
- Registrar revisor, fecha y hashes.
- Calcular
content_hash,source_content_hashy comprobar bajo lock si ya existe una version reutilizable con el mismo contenido, fingerprint y metadata. - Si existe, marcar el candidato
rejectedconDUPLICATE_REUSABLE_VERSIONy activar la version existente solo si cumple la precondicion. - Si no existe, persistir hashes y cambiar
review_required -> indexing. - Solo entonces generar chunks y embeddings.
- Verificar puntos y comparar entonces
expectedActiveVersionIdcon la version activa actual; si coincide, activar con el protocolo del punto 2.
Si otra version se activo durante la revision, guardar el candidato aprobado como ready, devolver 409 ACTIVE_VERSION_CHANGED y no activar. El operador puede activarlo despues con una nueva precondicion explicita; nunca se sobrescribe silenciosamente una version mas reciente.
Rechazar cambia a rejected, conserva auditoria temporal y no genera embeddings.
Artefactos y retencion
El RAG usa un volumen persistente privado montado en /data/ingestions. Estructura por version:
/data/ingestions/<versionId>/
manifest.json
metrics.json
documents/
<documentArtifactId>/
original.pdf
native-pages.json.gz
ocr-raw.json.gz
candidate-pages.json.gz
reviewed-pages.json.gz
review-images/
page-0001.png
documentArtifactId es UUIDv5 del document_id, no una ruta ni un nombre aportado por el usuario. El manifiesto relaciona ese ID con document_id y evita colisiones en fuentes con varios PDFs.
manifest.json incluye IDs, hashes, decisiones por pagina, versiones de motor y politicas, tiempos, flags, revisor y estado.
Retencion inicial:
- pendiente de revision: 30 dias;
- fallido, bloqueado o rechazado: 7 dias;
- imagenes renderizadas de revision: 7 dias despues de aprobar o rechazar;
- original, texto revisado, OCR bruto y manifiesto activos: mientras la version este activa y 30 dias despues de ser reemplazada;
- copias del servicio OCR: borrado al transferir correctamente o TTL maximo de 24 horas.
Los archivos usan permisos 0600, no tienen URL publica y no se comparten por filesystem entre los dos servicios.
Un limpiador diario adquiere el mismo lock/CAS de version usado por revision y purga. Primero cambia artifact_state a retention_deleting, despues elimina idempotentemente y finalmente marca retention_deleted. Caducar review_required cambia la version a rejected con error_code: REVIEW_EXPIRED; caducar failed o rejected ejecuta purging -> purged. Una version active nunca se elimina por TTL. Las imagenes se sirven solo por endpoint administrativo autenticado y validando que la ruta resuelta permanece bajo el directorio de la version.
Limites operativos iniciales
| Limite | Valor |
|---|---|
| Upload PDF | 50 MiB |
| Paginas por documento | 100 |
| Paginas OCR por trabajo | 100 |
| Render | 200 DPI |
| Pagina renderizada | 25 megapixeles |
| Trabajos Paddle concurrentes | 1 |
| Cola OCR | 3 |
| Timeout por pagina | 60 s |
| Timeout total | 15 min |
| OCR service | maximo 3 CPU y 5 GiB RAM |
Antes de habilitar OCR, sustituir multer.memoryStorage() por upload a disco con limite y limpieza garantizada. Rechazar PDFs cifrados, corruptos, sobredimensionados o con riesgo de bomba de descompresion antes del render completo.
Reintentos y degradacion
- Reintentar envio dos veces ante conexion,
502o503, con backoff y misma clave idempotente. - Polling con backoff de 2 a 15 segundos.
- Reintentar una pagina una vez solo por crash o fallo transitorio del runtime.
- No reintentar
400,401,403,413,422ni errores deterministas de render. 429conserva el candidato pendiente y devuelve estado reintentable.- Paddle no disponible implica fallo cerrado; no activar contenido parcial ni cambiar silenciosamente de motor.
- Todo fallo conserva intacta la version activa anterior.
Despliegue OCR
Crear dentro del repo:
ocr-service/
Dockerfile
requirements.txt
app/
main.py
engine.py
models.py
jobs.py
Condiciones:
- imagen Python CPU separada;
- PaddleOCR
3.4.0y PaddlePaddle3.2.2fijados exactamente; - resto de dependencias bloqueadas con versiones y hashes despues de validar compatibilidad;
- modelos descargados durante build, nunca al arrancar ni al procesar;
- un solo worker y una sola replica;
- readiness falsa hasta cargar modelos;
- solo red
easypanel-ia_servicios; - sin dominio Traefik;
- bearer interno incluso dentro de la red Docker;
- token en
OCR_INTERNAL_TOKEN, enviado comoAuthorization: Bearery comparado en tiempo constante; - URL RAG
OCR_SERVICE_URL=http://<servicio-interno>:8000; - deploy y rollback independientes del RAG.
El servicio OCR usa un volumen efimero acotado en /data/jobs con SQLite para cola, idempotencia y recuperacion tras reinicio. No es la copia durable: el RAG conserva original y resultados. Los trabajos del volumen OCR se eliminan tras transferencia confirmada o por TTL de 24 horas.
OCRmyPDF/Tesseract no se instala en estas imagenes productivas. Puede mantenerse en scripts de benchmark no desplegados.
Archivos del punto 3
Crear:
migrations/002_ocr_review.sql;ocr-service/con el servicio y sus pruebas;src/modules/ocr/client.ts;src/modules/ocr/detection.ts;src/modules/ocr/artifacts.ts;src/modules/ocr/review.ts.
Modificar:
src/modules/parsers/parser-registry.tspara resultado por pagina;src/modules/ingest/service.tspara estados OCR y aprobacion;src/shared/types/rag.tspara paginas, metricas, jobs y revision;src/config/env.tspara URL, token, volumen, limites y timeouts;src/app.tspara upload acotado y endpoints de revision;src/api/openapi.ts, playground y documentación API;Dockerfiley configuracion EasyPanel para el volumen RAG.
Orden de tareas del punto 3
- Añadir migracion OCR y contratos TypeScript sin cambiar aun el flujo nativo.
- Implementar extraccion PDF por pagina, composicion canonica, metricas y tests de deteccion.
- Crear servicio OCR privado con modelos baked, auth, limites, idempotencia y health.
- Implementar cliente RAG, reintentos, polling y validacion estricta de respuesta.
- Añadir volumen y gestor de artefactos con hashes, permisos y retencion.
- Integrar respuesta asincrona, estados
review_required,rejectedy fallo cerrado. - Añadir deteccion de identificadores y vista de revision en playground.
- Implementar aprobar/rechazar y enlazar aprobacion con indexacion versionada.
- Actualizar OpenAPI, API, ingesta, procesado y despliegue.
- Ejecutar comprobaciones estaticas Node y tests del servicio OCR.
- Commit y push; desplegar primero OCR privado y validar readiness.
- Desplegar RAG con OCR deshabilitado y comprobar regresiones nativas.
- Activar OCR, probar el fixture real, revisar/corregir y validar retrieval exacto.
Criterios de aceptacion del punto 3
- Un PDF textual no llama al servicio OCR y sigue el flujo normal.
- Un PDF escaneado selecciona todas las paginas no vacias necesarias.
- Un PDF mixto selecciona solo paginas con texto nativo insuficiente.
- Ninguna version que haya usado OCR se activa automaticamente.
- Los cuatro errores conocidos de
O/0/oaparecen marcados para revision. - La aprobacion con correcciones produce chunks con
CBG04a,FAT07,DSAU08yNSAV06exactos. - Si una pagina falla, no hay embeddings ni activacion parcial.
- Si OCR no esta disponible, la fuente activa anterior no cambia.
- Un reenvio con la misma clave idempotente no duplica trabajo.
- Los artefactos tienen hashes verificables y respetan retencion.
- El servicio OCR no es accesible desde Internet.
- CPU, RAM, cola y tiempos respetan los limites definidos.
/healthinforma disponibilidad y cola OCR sin exponer secretos.- El PDF real de FacturaTech completa sus 34 entradas tras revision.
- Retrieval en produccion devuelve los codigos canonicos corregidos.
Puerta de salida del punto 3
El punto 3 solo se marca completado cuando:
- el flujo nativo sigue funcionando sin regresion;
- el fixture mixto real completa deteccion, OCR, revision y activacion;
- los codigos exactos se recuperan en produccion;
- se verifica que no hay rutas publicas hacia OCR ni artefactos;
- se valida limpieza de temporales y TTL;
- se actualizan backlog, API, despliegue e historiales.
ODD: hardening del runtime OCR antes de la aceptacion productiva
Resultado requerido
La aceptacion de FacturaTech solo puede retomarse cuando OCR procese y sirva las evidencias de revision sin volver a renderizar el PDF bajo carga concurrente. Esta mejora se ejecuta como un unico flujo ODD y sustituye la ampliacion del SDD ocr-ingest-integration, que se archiva con su tarea productiva 7.4 incompleta.
El flujo debe entregar:
- una cola SQLite durable atendida por un unico worker real;
- recuperacion acotada de trabajos tras reiniciar el contenedor;
- PNG de revision persistidos durante el render inicial de cada pagina;
- lectura de imagenes desde fichero, sin nuevas llamadas a PDFium;
- transferencia RAG secuencial, idempotente y reanudable;
- exclusion mutua defensiva para cualquier operacion residual con PDFium;
- limpieza que no elimine trabajos activos ni evidencias aun no transferidas.
Incidente que obliga al hardening
La candidata FacturaTech v5 5f2317c6-7a8a-4e08-a614-f8189602ebb8 completo y persistio el OCR de 25 paginas. Despues, RAG solicito las 25 imagenes de revision concurrentemente mediante Promise.all. El servicio volvio a renderizar el PDF para cada peticion y PDFium/FreeType termino con SIGSEGV en FPDF_RenderPageBitmap -> FT_Load_Glyph.
El contenedor salio con codigo 139; OOMKilled=false. RAG aplico el contrato fail-closed, marco la candidata como fallida y mantuvo intacta la version activa. No fue un fallo de memoria, red, autenticacion ni procesamiento PaddleOCR.
Invariantes
| Area | Contrato obligatorio |
|---|---|
| Admision | La API persiste el trabajo en SQLite antes de responder 202; no inicia un BackgroundTask independiente por solicitud. |
| Concurrencia | Una instancia OCR ejecuta exactamente un worker de procesamiento y como maximo un trabajo running. |
| Reinicio | Al arrancar, el worker recupera trabajos queued y reclasifica de forma segura los running interrumpidos; nunca duplica un resultado completed. |
| Render | Cada pagina se renderiza una sola vez por intento de procesamiento. El mismo render alimenta OCR y publica el PNG privado de revision. |
| Imagenes | El endpoint de imagen valida identidad y contencion, y transmite un PNG persistido. No abre ni renderiza el PDF. |
| Transferencia | RAG descarga una pagina cada vez. Una transferencia interrumpida continua desde los artefactos ya publicados y verificados por hash. |
| PDFium | Toda operacion que aun use PDFium queda bajo un lock de proceso e incluye cierre determinista de pagina, documento y bitmap. |
| Limpieza | Un trabajo con lease vigente no se elimina. Un running no puede permanecer activo mas de 15 minutos: al expirar su lease se recupera una vez o pasa a failed. Tras la transferencia durable se elimina inmediatamente; sin confirmacion, el limite absoluto es 24 horas desde created_at, mas como maximo la ejecucion vigente de 15 minutos. |
| Errores | RAG no reintenta HTTP 500. Solo conserva los reintentos idempotentes ya definidos para conexion, 502 y 503. |
| Activacion | El hardening, sus pruebas y su despliegue no aprueban, indexan ni activan contenido. La version activa anterior permanece intacta. |
Ubicacion y limpieza exacta de los ficheros
OCR y RAG no comparten filesystem. Cada servicio conserva una copia con una finalidad y una retencion diferentes.
Copia transitoria del servicio OCR
El volumen OCR /data/jobs usa esta estructura:
/data/jobs/
jobs.db
artifacts/
<job_id>/
input.pdf
review-images/
page-0001.png
page-0002.png
jobs.dbguarda solo metadatos, hashes, estados, intentos, heartbeat, lease y rutas relativas. No guarda el PDF ni los PNG como BLOB.- Cada fichero se publica con escritura temporal,
fsyncy rename atomico dentro del directorio del trabajo. - RAG confirma la transferencia solo despues de publicar y releer su copia durable completa. Entonces solicita
DELETE /v1/jobs/:job_idy OCR elimina recursivamente/data/jobs/artifacts/<job_id>y su fila SQLite. - El sweeper se ejecuta al arrancar y cada 15 minutos. Un
runningcon lease vencido se devuelve aqueueduna sola vez; si vuelve a interrumpirse o supera 15 minutos totales, pasa afailed. - Ningun trabajo queda protegido indefinidamente por llamarse activo. Los trabajos sin confirmacion expiran a las 24 horas desde
created_at; si en ese instante existe una ejecucion con lease vigente, primero se deja terminar o alcanzar su limite de 15 minutos y despues se purga. - La purga elimina el directorio completo y la fila SQLite. SQLite se configura con
auto_vacuum=INCREMENTALy el mantenimiento ejecuta checkpoint eincremental_vacuumpara que metadatos eliminados tampoco acumulen espacio muerto. - Antes de admitir un trabajo y antes de publicar cada PNG, OCR comprueba uso del volumen y espacio libre. El presupuesto transitorio es el menor entre el 10 % de la capacidad del filesystem y 2 GiB; siempre reserva el mayor entre el 10 % y 2 GiB libres. Si no puede respetarlo, rechaza o falla de forma cerrada con
OCR_STORAGE_PRESSUREy limpia el intento parcial.
Con cola maxima 3, timeout total de 15 minutos, recuperacion unica, limite de volumen y sweeper periodico, no existe un camino normal que deje PDFs o PNG flotando indefinidamente.
Copia durable del RAG
RAG conserva las imagenes privadas en:
/data/ingestions/<version_id>/documents/<document_artifact_id>/review-images/page-NNNN.png
- La copia se publica con permisos
0600, manifiesto y SHA-256 por fichero. - Mientras la version espera revision, sus evidencias se conservan hasta 30 dias.
- Despues de aprobar o rechazar, las imagenes de revision se conservan 7 dias y despues se eliminan mediante el limpiador de retencion.
- El resto de artefactos de una version activa se conserva mientras siga activa y 30 dias despues de ser reemplazada.
- Una version activa nunca se elimina por TTL; las imagenes no se guardan en PostgreSQL ni Qdrant y no tienen URL publica.
Unidades de desarrollo
Cada unidad se implementa en una rama de feature y termina con un commit de unidad de trabajo Conventional Commit. Pruebas y documentacion propias de la conducta viajan en el mismo commit; la validacion independiente se ejecuta despues y no se mezcla con la implementacion.
D1. Cola durable y worker unico
Estado: Implementada y validada de forma independiente el 2026-09-21.
- Convertir SQLite en la fuente de verdad de trabajos
queued,running,completedyfailed. - Mantener en SQLite solo metadatos y rutas; persistir PDF y PNG bajo
/data/jobs/artifacts/<job_id>para que su borrado libere espacio real. - Sustituir el lanzamiento por peticion por un loop de worker unico con claim atomico.
- Recuperar trabajos interrumpidos al arrancar, con intento acotado y transiciones auditables.
- Hacer que readiness distinga entre modelo cargado, worker operativo y almacenamiento disponible.
D2. Artefactos de revision sin rerender
Estado: Implementada y validada de forma independiente el 2026-09-21.
- Persistir cada PNG privado durante el render inicial usado por OCR.
- Publicar resultado, manifiesto e imagenes de manera atomica o dejar el trabajo recuperable.
- Servir unicamente ficheros persistidos y rechazar rutas, paginas o hashes no vinculados al trabajo.
- Mantener un lock defensivo alrededor de las operaciones PDFium y liberar recursos en todos los caminos.
D3. Transferencia RAG secuencial y reanudable
Estado: Implementada y validada de forma independiente el 2026-09-21.
- Eliminar la descarga concurrente de imagenes mediante
Promise.all. - Transferir paginas en orden, una por una, validando identidad, tipo, tamano y hash.
- Conservar artefactos ya transferidos tras reinicio y reanudar solo los que falten.
- Confirmar limpieza remota unicamente despues de publicar durablemente toda la candidata local.
D4. Observabilidad y limites operativos
Estado: Implementada y validada de forma independiente el 2026-09-21.
- Exponer estados reales de cola, worker y recuperaciones sin filtrar secretos.
- Registrar transiciones, recuperaciones y fallos terminales con
job_idy categoria segura. - Mantener un trabajo Paddle concurrente, cola maxima 3 y limites actuales de CPU, RAM y tiempo.
- Aplicar presupuesto de disco, reserva de espacio libre, sweeper cada 15 minutos y vacuum incremental de SQLite.
- Conservar el fallo cerrado y la idempotencia entre RAG y OCR.
Validacion independiente
Estado: Superada el 2026-09-21. La primera comprobacion incompleta fue rechazada; la repeticion valida proceso un PDF real de 25 paginas con PaddleOCR, interrumpio y recupero el trabajo sin duplicados y midio la limpieza en completado, fallo y expiracion.
La validacion comienza solo cuando D1-D4 estan implementadas. Si descubre un defecto, se abre una unidad correctiva separada y se vuelve a ejecutar la validacion afectada; no se corrige silenciosamente durante la comprobacion.
- Ejecutar las pruebas focalizadas de cola, reinicio, render, imagenes, transferencia y limpieza.
- Ejecutar las suites completas Node y Python,
npm run checkynpm run build. - Procesar localmente un PDF de 25 paginas, reiniciar OCR durante un trabajo y comprobar recuperacion sin duplicados.
- Solicitar imagenes de revision concurrentemente y demostrar que el endpoint solo lee PNG persistidos y no llama a PDFium.
- Interrumpir la transferencia RAG y demostrar que continua desde la primera pagina ausente sin redescargar las verificadas.
- Verificar que un HTTP
500es terminal para RAG y que conexion,502y503mantienen el reintento idempotente acotado. - Confirmar que los temporales se eliminan solo tras transferencia completa o TTL y nunca mientras el trabajo esta activo.
- Medir el espacio de
/data/jobsantes y despues de completar, fallar y expirar trabajos; no deben quedar directorios huerfanos y el espacio debe volver a estar disponible.
Despliegue y aceptacion de FacturaTech
- Desplegar juntos OCR y RAG
0.2.0con OCR habilitado desde el inicio. - Verificar live, readiness, versiones, commit desplegado, worker unico, volumen SQLite y salud general del RAG.
- Crear una nueva candidata FacturaTech no activa. Si aparece un fallo cuyo origen no se pueda identificar, usar temporalmente la activacion separada de OCR y RAG solo como procedimiento de diagnostico.
- Verificar evidencia durable, 25 imagenes de revision y las 34 entradas esperadas.
- Comprobar exactamente
CBG04a,FAT07,DSAU08yNSAV06. - Presentar la evidencia al usuario y esperar autorizacion explicita.
- Solo tras esa autorizacion, aprobar, indexar y, si tambien se autoriza, activar.
La mejora queda cerrada cuando la validacion independiente pasa, FacturaTech completa la revision productiva y la activacion autorizada conserva rollback verificable. Una candidata fallida o una validacion parcial no permite declarar completada la antigua tarea SDD 7.4.
Verificacion y entrega comun
Cada punto se entrega como bloque independiente:
- implementar en local;
- ejecutar
npm run check,npm run buildy las pruebas estaticas aplicables; - documentar;
- revisar que no entran secretos;
- commit y push al repo
paco/rag-service; - deploy en EasyPanel;
- pruebas funcionales exclusivamente en produccion;
- rollback por Git y redeploy si falla;
- cerrar el punto en
PENDIENTES_RAG.mdsolo con evidencia.
Esta es la regla general de las fases anteriores. Para la correccion 0.2.1, el anexo siguiente la sustituye de forma explicita: R1-R4 se implementan, prueban y documentan sin desplegar ni ejecutar pruebas productivas parciales. Solo R5 puede desplegar, y debe hacerlo como una unica operacion controlada que incluye RAG y OCR, seguida de pruebas conjuntas de ambos servicios. OCR_INGEST_ENABLED permanece en true: no se desactiva preventivamente ni se despliega por etapas salvo que un fallo real y su evidencia justifiquen aislar un servicio. La entrega no se considera completada hasta que ambos servicios coincidan en version y revision.
Exclusiones explicitas
- No corregir texto OCR con un LLM en esta fase.
- No usar confianza OCR como garantia de exactitud de codigos.
- No combinar automáticamente resultados Paddle y Tesseract.
- No guardar originales o resultados OCR en Qdrant.
- No usar Qdrant como catalogo de estados.
- No borrar versiones activas mediante
/cleanup. - No conservar un camino legacy indefinido despues de migrar.
- No iniciar busqueda hibrida; corresponde al pendiente 4.
- No presentar el servicio OCR en un dominio publico.
Anexo correctivo: hallazgos productivos v6-v7
Decision y alcance
La aceptacion productiva de FacturaTech queda pausada. Antes de crear otra candidata se entrega una correccion conjunta RAG/OCR 0.2.1 que resuelve los defectos observados en v6 y v7 sin aprobar, indexar ni activar contenido.
Este anexo es el contrato canonico de la correccion. No autoriza su implementacion: el usuario debe revisar este diseno y dar un visto bueno explicito antes de modificar codigo.
La correccion incluye:
- identidad idempotente versionada para no reutilizar trabajos de contratos incompatibles;
- recuperacion segura de respuestas idempotentes
queued,running,succeededyfailed; - separacion entre fallo de calidad bloqueante y advertencia que requiere revision humana;
- diagnostico durable y estructurado de las paginas que no superen calidad;
- validacion local, independiente y productiva antes de aprobar o activar.
Quedan fuera:
- corregir automaticamente texto o codigos con reglas, expresiones regulares o LLM;
- relajar silenciosamente todos los umbrales de calidad;
- reutilizar trabajos legacy que no garanticen PNG persistidos;
- recuperar, aprobar o activar v5, v6 o v7;
- borrar las versiones fallidas de PostgreSQL, que permanecen como evidencia auditable.
Evidencia que obliga a corregir
| Intento | Resultado | Hallazgo |
|---|---|---|
v6 b9281626-088a-46e5-9f66-b4a3bbe2159f |
Fallo inmediato, 0/25 |
El mismo PDF, paginas y configVersion=ocr-v1 reutilizaron la clave de un trabajo terminal anterior. OCR devolvio 202 con la fila existente, cuyo estado ya no era queued; RAG rechazo el acknowledgement como fallo generico de integridad. |
v7 c9756e59-f91d-4b57-8f31-e7b5b2b09558 |
OCR 25/25, despues OCR_QUALITY_BLOCKED |
Las paginas 2, 20 y 21 fallaron solo por p10Confidence < 0.5. Sus medianas fueron 0.983-0.999, el ratio de lineas de baja confianza 0.122-0.158 y el contenido era legible. |
La evidencia durable de v7 esta completa: original, paginas nativas, resultado OCR y 25 PNG unicos; todos los hashes y firmas PNG son validos y las imagenes ocupan 12.556.918 bytes. Las 34 entradas esperadas estan presentes. Al considerar que una entrada puede continuar en la pagina siguiente, la coincidencia minima por tokens es 88,9 % para mensaje y 100 % para causa y solucion.
El OCR produjo tres ambiguedades que deben llegar a revision independiente, no provocar correccion automatica:
| Esperado | OCR v7 | Pagina |
|---|---|---|
FAT07 |
FATo7 |
8 |
NSAV06 |
NSAvo6 |
15 |
DSAU04 |
DSAUo4 |
16 |
CBG04a y DSAU08 se recuperaron exactamente. La version activa 3fc78163-9cfb-4979-985c-1520a63327b0 permanecio intacta durante ambos intentos.
Limite de la evidencia historica
Las cifras anteriores describen la inspeccion productiva ya realizada, pero no sustituyen evidencia reproducible dentro del repositorio. El documento manual de contraste vive fuera de este repositorio en Empresa/Clientes/FacturaTech/Fidi/Info-para-Agente-Comercial/Errores Junio 2026 - OCR verificado.md; por tanto, cualquier afirmacion que dependa de su contenido se etiqueta como verificacion externa del agente y no obtiene PASS por si sola.
R4 debe aportar fixtures o generadores autocontenidos para probar identidad, recuperacion, calidad e integridad sin depender del documento del cliente. R5 debe registrar en el historial los IDs, estados, recuentos, hashes, versiones, revisiones y digests necesarios para auditar la ejecucion; la comprobacion semantica de las 34 entradas y los codigos se mantiene separada como evidencia externa producida por un modelo visual independiente, sin copiar contenido del cliente al repositorio.
Causas raiz
C1. La identidad remota no representa el contrato de artefactos
La clave <documentSha256>:ocr-v1:<pagesHash> identifica motor, paginas y configuracion, pero no diferencia el pipeline anterior del pipeline endurecido que persiste PNG de revision. Un trabajo succeeded antiguo puede ser valido como texto y, al mismo tiempo, incompatible con el contrato actual de evidencias.
Ademas, OCR devuelve la fila existente para una clave idempotente, cualquiera que sea su estado, mientras el cliente RAG exige que todo acknowledgement tenga status=queued. El servidor y el cliente discrepan en un caso normal de idempotencia.
C2. Un percentil aislado bloquea paginas revisables
La politica actual exige simultaneamente:
nonWhitespaceCharacters >= 40;medianConfidence >= 0.8;p10Confidence >= 0.5;lowConfidenceLineRatio <= 0.2.
En v7, p10Confidence bloqueo tres paginas aunque los otros tres indicadores pasaron y las entradas eran legibles. El percentil bajo reacciona a botones, iconos y fragmentos visuales con baja confianza; no demuestra que toda la pagina sea inutilizable.
C3. El fallo pierde el diagnostico que permitiria revisarlo
La composicion se detiene en la primera pagina bloqueada y propaga solo OCR_QUALITY_BLOCKED. El estado publico no identifica pagina, predicados incumplidos ni metricas. Tampoco publica una candidata revisable, aunque el resultado y las imagenes ya sean durables.
Contrato corregido
Identidad y compatibilidad ocr-v2
- RAG y OCR usan una unica constante por runtime con valor
ocr-v2paraconfigVersion. - La clave remota sigue el formato
<documentSha256>:<configVersion>:<pagesHash>; el cambio aocr-v2crea un espacio de identidad nuevo y evita reutilizar trabajosocr-v1. ocr-v2significa, como minimo: resultado OCR validado, PNG privados persistidos durante el render, manifiesto de imagenes y endpoint de lectura sin rerender.- RAG rechaza cualquier acknowledgement, estado, resultado o imagen cuya identidad no coincida exactamente con hash, paginas y
ocr-v2. - No se agrega fallback a
ocr-v1. Un despliegue mixto falla cerrado y no crea contenido activo.
La identidad canonica de una solicitud es el JSON canonico formado por idempotencyKey, documentSha256, requestedPages ordenadas y configVersion; su SHA-256 se denomina requestIdentitySha256. El contrato exige los siguientes campos:
| Respuesta | Identidad obligatoria |
|---|---|
| Acknowledgement | jobId, status, idempotencyKey, documentSha256, requestedPages, configVersion, requestIdentitySha256, createdAt. |
| Estado | jobId, status, idempotencyKey, documentSha256, requestedPages, configVersion, requestIdentitySha256, contadores y error seguro. |
| Resultado | schemaVersion, jobId, documentSha256, requestedPages, configVersion, requestIdentitySha256, motor y paginas. |
| Imagen | cabeceras X-Ocr-Job-Id, X-Ocr-Identity-Sha256, X-Ocr-Config-Version, X-Document-Sha256, X-Page-Number y X-Content-Sha256. |
RAG valida todos los campos contra la identidad persistida, no solo contra el jobId. Un campo ausente, un orden de paginas distinto o cualquier discrepancia falla cerrado con OCR_RESPONSE_INTEGRITY_FAILED antes de publicar artefactos locales.
Reenvio idempotente y estados terminales
POST /v1/jobs puede devolver el trabajo existente con status igual a queued, running, succeeded o failed, siempre ligado a la misma identidad completa.
RAG actua asi:
| Estado devuelto | Conducta |
|---|---|
queued o running |
Persiste remoteJobId, consulta el estado y continua el polling acotado existente. |
succeeded |
Persiste remoteJobId, valida el resultado y transfiere las imagenes; no repite PaddleOCR. |
failed |
Persiste remoteJobId y propaga el codigo terminal seguro del trabajo; no lo presenta como corrupcion de integridad. |
| Identidad o estado invalido | Falla cerrado con OCR_RESPONSE_INTEGRITY_FAILED, distinto de errores de red, calidad o procesamiento. |
El borrado remoto conserva el orden actual: solo despues de publicar y releer resultado, 25 comprobantes de imagen, candidata compuesta y estado local durable. Si RAG falla antes, el trabajo remoto puede reutilizarse hasta su TTL.
Recuperacion despues de succeeded
El estado succeeded de rag_ocr_jobs confirma que el resultado OCR y las imagenes requeridas quedaron transferidos y releidos, pero no implica que la version haya terminado su composicion. Durante todo el flujo OCR la version permanece en indexing, de acuerdo con la maquina de estados canonica; phase expresa ocr_queued, ocr_running o indexing durante la finalizacion local. El reconciliador debe buscar versiones indexing con todos sus trabajos en succeeded y completar idempotentemente los pasos que falten:
- releer y validar manifiesto de fuente, paginas nativas, resultados OCR y manifiestos de imagenes;
- crear o releer
quality-report.jsona partir de esos artefactos durables; - persistir diagnostico por pagina y decidir
failedo candidata revisable; - crear o releer
candidate-pages.jsoncuando no haya bloqueos; - persistir los hashes finales y transicionar mediante compare-and-set a
failedoreview_required.
Cada publicacion es inmutable: si el fichero ya existe debe coincidir byte a byte. Una caida despues de cualquier paso repite la reconciliacion sin ejecutar PaddleOCR, sin duplicar filas y sin borrar el trabajo remoto. Si falta un artefacto local y el trabajo remoto sigue disponible, se reanuda solo la transferencia ausente; si ya no puede recuperarse, la version falla cerrada con un error de artefacto auditable. El borrado remoto queda prohibido hasta que la version local tenga informe, diagnostico y candidata o fallo durables.
Calidad bloqueante frente a advertencias
La clasificacion sigue siendo cerrada para paginas vacias o inutilizables, pero p10Confidence deja de ser un veto aislado. extractionMethod describe de donde sale el texto y qualityOutcome decide la puerta de calidad:
| Condicion | extractionMethod |
qualityOutcome |
Diagnostico |
|---|---|---|---|
| Pagina con texto nativo suficiente | native |
accepted |
Sin advertencias ni bloqueos OCR. |
inkCoverage < 0.015 y menos de 10 caracteres OCR |
blank |
accepted |
Pagina vacia verificada. |
| Cualquier pagina no vacia con menos de 40 caracteres OCR | ocr |
blocked |
INSUFFICIENT_TEXT. |
medianConfidence < 0.8 |
ocr |
blocked |
LOW_MEDIAN_CONFIDENCE. |
lowConfidenceLineRatio > 0.2 |
ocr |
blocked |
EXCESS_LOW_CONFIDENCE_LINES. |
Solo p10Confidence < 0.5 |
ocr |
warning |
LOW_P10_CONFIDENCE; pasa obligatoriamente a revision humana. |
| Todos los umbrales OCR superados | ocr |
accepted |
Sin advertencia de pagina. |
No se cambian los otros tres umbrales sin nueva evidencia. La confianza nunca garantiza exactitud de codigos. Los tokens alfanumericos ambiguos siguen priorizados como riesgos de revision y nunca se autocorrigen.
Una pagina puede acumular mas de un bloqueo. blockingReasons se ordena siempre como INSUFFICIENT_TEXT, LOW_MEDIAN_CONFIDENCE, EXCESS_LOW_CONFIDENCE_LINES; primaryBlockingReason es el primer elemento. LOW_P10_CONFIDENCE solo se incluye en warnings cuando no existe ningun bloqueo. Todas las paginas del original aparecen una vez en el informe, incluidas las nativas suficientes y las vacias verificadas.
Diagnostico durable
La composicion clasifica todas las paginas antes de decidir el estado final y publica atomicamente quality-report.json junto al resultado OCR. El informe no contiene texto ni secretos:
{
"schemaVersion": "1",
"versionId": "uuid",
"configVersion": "ocr-v2",
"sourceManifestSha256": "sha256",
"ocrResults": [
{"documentId": "doc:...", "artifactSha256": "sha256", "resultSha256": "sha256"}
],
"reviewImageManifests": [
{"documentId": "doc:...", "manifestSha256": "sha256"}
],
"pages": [
{
"documentId": "doc:...",
"page": 2,
"extractionMethod": "ocr",
"qualityOutcome": "warning",
"warnings": ["LOW_P10_CONFIDENCE"],
"blockingReasons": [],
"primaryBlockingReason": null,
"metrics": {
"nonWhitespaceCharacters": 399,
"medianConfidence": 0.988,
"p10Confidence": 0.339,
"lowConfidenceLineRatio": 0.158,
"inkCoverage": 0.321
}
}
]
}
El informe se calcula sobre el manifiesto de fuente, cada sobre durable ocr-result.json y cada manifiesto durable de imagenes. Los hashes de entrada forman parte del propio informe; qualityReportSha256 se persiste en PostgreSQL y se referencia desde candidate-pages.json o desde error.details. La lectura rechaza cualquier informe cuyo versionId, configVersion o cadena de hashes no coincida con los artefactos releidos.
Si existe al menos una pagina bloqueada:
- no se publica
candidate-pages.jsonni se pasa a revision; - la version queda
failedcon codigoOCR_QUALITY_BLOCKED; GET /ingestions/:versionIddevuelveerror.details.blockedPagescon documento, pagina y razones, sin texto OCR;error.detailsincluye el SHA-256 del informe para ligar el diagnostico de PostgreSQL con el artefacto durable;- el informe durable permite auditar el fallo sin volver a ejecutar OCR.
Si solo existen advertencias:
- se publica la candidata;
candidate-pages.jsonincluyequalityReportSha256y las advertencias por pagina;- PostgreSQL conserva las advertencias por pagina en
quality_warnings, separadas de los tokens de riesgo; - la version llega a
review_required; - la API y la vista de revision muestran primero paginas advertidas y tokens de riesgo.
La migracion 004_ocr_quality_diagnostics.sql anade a rag_document_pages quality_outcome text NULL, quality_warnings jsonb NOT NULL DEFAULT '[]', blocking_reasons jsonb NOT NULL DEFAULT '[]', primary_blocking_reason text NULL y quality_report_sha256 char(64) NULL. quality_outcome solo admite accepted, warning o blocked; las filas nuevas ocr-v2 deben informarlo. extractionMethod del informe se persiste en la columna existente extraction_method; qualityOutcome, warnings, blockingReasons y primaryBlockingReason se mapean respectivamente a las cuatro columnas nuevas. Si el resultado es blocked, blocking_reasons contiene todas las razones en el orden canonico definido arriba y primary_blocking_reason coincide con la primera; si no es bloqueante, ambos quedan vacios. blocked_reason se conserva solo para compatibilidad historica y no es la fuente canonica de filas ocr-v2.
La migracion no reinterpreta filas antiguas ni altera estados existentes. error_detail conserva compatibilidad como texto para errores historicos; solo OCR_QUALITY_BLOCKED nuevo persiste un JSON canonico acotado que el repositorio valida antes de exponer como error.details. El diagnostico por pagina y quality_report_sha256 se confirman en PostgreSQL antes de marcar la version como failed o review_required.
Aceptacion operativa del OCR best-effort
Para la fase autonoma actual, el servicio OCR se considera aceptable cuando completa el contrato tecnico, conserva evidencia durable e informa calidad y estado revisable. No se exige exactitud semantica total: el modelo puede confundir caracteres con confianza alta aunque la imagen sea legible. Esta limitacion no debe ocultarse ni corregirse mediante sustituciones silenciosas.
La ingesta puede ser orquestada de extremo a extremo por un agente sin interfaz grafica. El agente consume los mismos artefactos y rutas de revision que usara una futura consola humana. Una futura revision visual por IA podra proponer correcciones usando imagen, bounding box, texto y contexto, pero queda fuera de esta fase.
Aceptar el servicio no equivale a aceptar cualquier candidata ni elimina los estados del ciclo de vida. Las correcciones de contenido, la aprobacion/indexacion y la activacion conservan sus precondiciones y autorizaciones separadas.
Revision de FacturaTech
Llegar a review_required no equivale a PASS. Antes de aprobar v8 o posterior:
Para esta aceptacion, la revision humana externa se sustituye por una revision visual independiente realizada por el agente. La referencia de calidad es una capacidad visual profesional distinta de PaddleOCR. Esta excepcion no elimina el estado review_required, no autoriza correcciones automaticas y no cambia las autorizaciones posteriores.
Entradas y limites
- Candidata productiva: v8
f4fd1502-1d4d-454f-bbfb-4c3b6820ee97, o una posterior identificada expresamente si v8 deja de ser la candidata vigente. - Evidencia visual: las 25 imagenes durables servidas por las rutas de revision de RAG.
- Texto a evaluar: la candidata producida por OCR, obtenida desde la ruta administrativa de revision.
- Referencia externa:
Empresa/Clientes/FacturaTech/Fidi/Info-para-Agente-Comercial/Errores Junio 2026 - OCR verificado.md. - Capacidad independiente: modelo con vision que no consuma como entrada el texto producido por PaddleOCR durante su primera lectura de cada imagen.
- Alcance de esta fase: solo lectura, comparacion y preparacion de correcciones. Quedan prohibidos aprobar, rechazar, indexar o activar.
- Privacidad: las imagenes temporales viven bajo
/tmp/opencode/, no se copian al repositorio y se eliminan al cerrar la revision.
Procedimiento obligatorio para el agente
- Consultar de nuevo la candidata y detenerse si no esta
review_required, siactivateRequestedno esfalse, si cambiocandidateSha256o si la version activa ya no coincide conbaseActiveVersionId. - Descargar en solo lectura las 25 imagenes de revision a
/tmp/opencode/facturatech-v8-review/, con nombres establespage-01.pngapage-25.png. Confirmar que hay 25 ficheros no vacios y registrar sus SHA-256 sin incluir contenido del cliente en documentos versionados. - Ejecutar una primera pasada visual independiente pagina por pagina. Si el agente principal no tiene vision, debe invocar el subagente
visiony pasarle las rutas absolutas de los PNG materializados; nunca debe pedirle que lea una imagen "adjunta" o una imagen del contexto anterior. Procesar como maximo cinco paginas por lote y exigir una salida separada para cada pagina. - En cada lote, proporcionar al modelo visual solo las imagenes y esta instruccion:
Transcribe literalmente cada entrada visible. Conserva mayusculas, minusculas, numeros y sufijos. No corrijas por contexto. Marca cada caracter dudoso en uncertainTokens. Devuelve page, entries, codes, uncertainTokens y visualNotes por pagina.No mostrarle aun el texto OCR productivo, los cuatro codigos esperados ni el documento manual. - Guardar temporalmente la salida estructurada de todos los lotes. Comprobar que existen resultados para las paginas 1-25, sin duplicados. Si el modelo no puede leer una pagina o devuelve una salida incompleta, repetir una vez con un recorte o mayor resolucion; si sigue sin resolverse, bloquear el PASS.
- Solo despues de terminar la primera pasada, obtener el texto candidato de RAG y leer el documento manual externo. Construir una comparacion de tres vias: imagen transcrita por el modelo, candidata OCR y referencia manual.
- Enumerar exactamente las 34 entradas. Para cada una registrar pagina, identificador esperado, valor visual, valor OCR, valor de referencia y resultado
match,correction_requiredounresolved. - Verificar por igualdad literal
CBG04a,FAT07,DSAU08yNSAV06. Comprobar tambien si reaparecenFATo7,NSAvo6oDSAUo4y preparar, sin aplicar, las correcciones exactas. - Considerar resuelta una diferencia solo cuando la lectura visual y la referencia manual coinciden. Si discrepan entre si, si falta una entrada o si queda cualquier token incierto, clasificarla como
unresolvedy no declarar PASS. - Preparar un informe sin texto sensible innecesario que incluya version, estado, hashes, recuento
34/34, resultado de los cuatro codigos criticos, diferencias por pagina, correcciones propuestas y cualquier incertidumbre. No incluir imagenes ni transcripciones completas en el repositorio. - Consultar de nuevo la candidata antes de presentar el resultado y confirmar que estado,
candidateSha256,currentActiveVersionIdyactivateRequested=falseno cambiaron durante la revision. - Presentar el informe al usuario. El resultado solo puede ser
READY_FOR_APPROVALsi hay 34 entradas, cero casosunresolved, las 25 imagenes fueron verificadas y las correcciones propuestas producen exactamente los cuatro codigos criticos. En cualquier otro caso el resultado esREVIEW_BLOCKED. - Esperar una autorizacion separada antes de aprobar y comenzar la indexacion. La solicitud debe mostrar las correcciones concretas que se aplicarian. La activacion requiere otra autorizacion posterior.
Salida minima de la revision
| Campo | Requisito |
|---|---|
reviewActor |
Modelo y agente que realizaron la revision visual independiente. |
versionId y candidateSha256 |
Deben coincidir con la candidata consultada antes y despues de la revision. |
imageCount |
25; cada imagen con SHA-256 registrado en evidencia no versionada o resumida. |
entryCount |
34; ninguna entrada ausente o duplicada. |
criticalCodes |
Resultado literal para CBG04a, FAT07, DSAU08 y NSAV06. |
differences |
Pagina, valor OCR, valor independiente y correccion propuesta, sin aplicar. |
unresolvedCount |
Debe ser 0 para READY_FOR_APPROVAL. |
sideEffects |
Debe confirmar que no hubo aprobacion, indexacion ni activacion. |
Unidades de implementacion
Las unidades se ejecutan en orden. Cada una incluye pruebas y documentacion propia. R1-R4 no despliegan ningun servicio, no ejecutan pruebas productivas y no crean otra candidata. R5 es la unica unidad autorizada para desplegar y probar conjuntamente RAG y OCR, siempre tras superar R4 y recibir aprobacion explicita.
R1. Versionar identidad y recuperar acknowledgements terminales
- Centralizar
ocr-v2en cliente RAG y servicio OCR y actualizar payload, acknowledgement, resultado, huella de procesamiento e idempotency key. - Aceptar los cuatro estados conocidos en el acknowledgement sin debilitar las validaciones de identidad.
- Persistir
remoteJobIdantes de consultar o consumir un trabajo repetido. - Reutilizar
succeeded, propagarfailedy conservar polling paraqueued/running. - Incorporar y validar la identidad canonica completa en acknowledgement, estado, resultado e imagenes.
- Recuperar una version local
indexingcuyos trabajos ya estensucceededhasta informe, diagnostico y candidata o fallo durables, sin repetir OCR. - Cubrir duplicados en los cuatro estados y demostrar que un trabajo
ocr-v1nunca satisface una solicitudocr-v2.
R2. Separar bloqueo y advertencia de calidad
- Sustituir la decision booleana por una clasificacion con
extractionMethod,qualityOutcome,warnings,blockingReasonsyprimaryBlockingReason. - Convertir exclusivamente
p10Confidence < 0.5enLOW_P10_CONFIDENCEno bloqueante. - Mantener como bloqueantes los demas umbrales y la deteccion de pagina vacia.
- Clasificar todas las paginas antes de fallar y conservar orden estable.
- Persistir todas las razones de bloqueo por pagina, una razon primaria determinista y las advertencias por separado.
- Anadir regresiones con las metricas exactas de las paginas 2, 20 y 21 de v7, sin copiar contenido del cliente al repositorio.
R3. Persistir y exponer diagnostico seguro
- Publicar y releer
quality-report.jsoncon permisos0600, escritura atomica, identidad, hashes de los resultados OCR y hashes de los manifiestos de imagenes. - Crear y validar
004_ocr_quality_diagnostics.sqlsin modificar estados ni datos historicos. - Propagar detalles estructurados de paginas bloqueadas hasta
GET /ingestions/:versionId. - Ligar manifiesto de fuente, resultados OCR, manifiestos de imagenes, informe, candidata y error mediante SHA-256; rechazar cualquier discrepancia de identidad.
- Conservar
quality_outcome,quality_warnings, todas lasblocking_reasons, la razon primaria yquality_report_sha256por pagina en PostgreSQL. - Priorizar en revision paginas advertidas y tokens alfanumericos ambiguos.
- Probar que diagnosticos y logs no incluyen texto OCR, rutas internas, tokens ni secretos.
R4. Validacion integral e independiente
R4 se ejecuta exclusivamente en local. La validacion de 004_ocr_quality_diagnostics.sql debe usar un contenedor PostgreSQL local aislado y reutilizable, sin instalar un servicio nativo permanente y sin cargar .env.local, .env.easypanel.local, POSTGRES_URL ni DATABASE_URL de EasyPanel. Antes de ejecutar SQL se debe confirmar que el destino es ese contenedor. El runner npm run migrate:lifecycle aplica todas las migraciones pendientes, no solo 004; por eso la prueba debe controlar la secuencia 001-003, conservar una fila historica representativa, aplicar 004 y verificar que la fila no cambia. El contenedor y su volumen se conservan para futuras pruebas, pero debe configurarse sin reinicio automatico, arrancarse solo durante la prueba y quedar detenido al terminar. Un contenedor detenido no mantiene PostgreSQL consumiendo CPU o RAM asignada; la imagen y los datos siguen ocupando disco. No se apaga ni limpia Docker globalmente y no se permite usar docker system prune.
- Ejecutar pruebas focalizadas Node y Python para R1-R3.
- Ejecutar suites completas,
npm run check,npm run build,py_compileygit diff --check. - Simular caida de RAG despues de que OCR llegue a
succeeded; el reenvio debe reutilizar el mismo trabajo y completar la transferencia. - Simular caidas despues de resultado, imagenes, informe y diagnostico; el reconciliador debe completar la finalizacion local exactamente una vez.
- Simular un trabajo remoto
failed; el estado debe conservar su codigo y no convertirse en error de integridad. - Validar con un fixture autocontenido de 25 paginas advertencias p10, 25 PNG y composicion revisable.
- Validar por separado una pagina realmente bloqueada y comprobar informe, detalles API y ausencia de candidata.
- Verificar la cadena de hashes completa y usar evidencia autocontenida para todas las pruebas automatizadas; etiquetar por separado cualquier comprobacion manual externa.
- Encargar una revision independiente que no corrija silenciosamente defectos.
- Validar
004en el contenedor PostgreSQL local reutilizable: destino comprobado, historial001-003, fila historica intacta, columnas y restriccion nuevas verificadas y contenedor detenido al terminar.
R5. Entrega conjunta 0.2.1 y nueva aceptacion
- Antes de aplicar migraciones en produccion, identificar explicitamente base y entorno, revisar
rag_schema_migrationsy confirmar que solo estan pendientes las migraciones esperadas. Preflight de solo lectura en VPS2 (2026-09-23): RAG usadb_ragcon el rolusr_rag;001-003estan aplicadas con checksums coincidentes y solo queda004_ocr_quality_diagnostics.sql. La validacion local de R4 no autoriza esta operacion. - Crear una unica fuente versionada en la raiz,
VERSION, con0.2.1; RAG, OCR, pruebas y proceso de build deben consumirla y fallar si aparece otra version declarada. - Publicar RAG/OCR desde el mismo commit: entrega
0654ce5, con registro documentalec0d233.BUILD_REVISIONse carga manualmente con0654ce5mientras no exista automatizacion aprobada; los digests de imagen siguen siendo la identidad exacta autoritativa. - Ejecutar una unica operacion conjunta de despliegue con
OCR_INGEST_ENABLED=true. EasyPanel puede reconstruir cada servicio por separado; no se ejecuta la aceptacion intencional de FacturaTech y no se considera desplegada la entrega hasta que ambos terminen. El 2026-09-23 ambos servicios quedaron en ejecucion conBUILD_REVISION=0654ce5; no se introdujo una desactivacion preventiva ni un despliegue por etapas. - Cuando ambos despliegues terminen, comprobar que health de RAG y OCR devuelve
version=0.2.1, la misma revision esperada y que las etiquetas OCI de ambas imagenes coinciden; una diferencia bloquea la aceptacion. Ambos exponen0.2.1y0654ce5; digests: RAGsha256:9a22730406c09c6b773891d8953f451e6263bf9fb0033188f4479f0b93ec8bbf, OCRsha256:6725991d3893a4f16ae6c75f28e9dd59b051bdbc344b28d79f1292224ddceda1. - Verificar OCR live/ready, cola vacia, almacenamiento, RAG, PostgreSQL, Qdrant y reconciliador como una unica bateria posterior al despliegue conjunto. OCR esta preparado con cola vacia, worker y sweeper operativos; RAG, PostgreSQL, Qdrant y reconciliador estan sanos;
004quedo aplicada. - Cerrar o conservar auditadamente v7 sin aprobarla, indexarla ni activarla. v7
c9756e59-f91d-4b57-8f31-e7b5b2b09558siguefailedporOCR_QUALITY_BLOCKED, con artefactos presentes y la version activa anterior intacta. - Crear una candidata nueva con
activate=falsey supervisarla hastareview_requiredo fallo. v8f4fd1502-1d4d-454f-bbfb-4c3b6820ee97alcanzoreview_requiredsin activacion: 25/25 paginas OCR, 25 imagenes verificadas, 22 paginas aceptadas, tres advertenciasLOW_P10_CONFIDENCE(2, 20 y 21) y cero bloqueos. - Ejecutar y reauditar la revision visual independiente de FacturaTech definida en este anexo. La primera revision quedo bloqueada por cuatro dudas; una auditoria posterior con un modelo visual mas potente resolvio esas dudas contra imagen, candidata y referencia, y dejo v8
READY_FOR_APPROVAL: 34/34 entradas, cero duplicados, cero ausencias y cero casos sin resolver. No se aprobo, indexo ni activo. - Registrar evidencia autocontenida de IDs, estados, recuentos, hashes, versiones, revisiones y digests; mantener separada la comparacion con el documento del cliente. v8: candidata SHA-256
7705407ecbc6f6ec991202e46f0f37ba5a3c7defffb1addad6ed99ba67ea1827, informe de calidad SHA-25676344dbbffc47bf6603013098a8f328141b76905cd9539ed696cae1d38e4c08e, fingerprintb26d6e8bb445d69928f4da7845ec2e1b484f376e0bc07284c29cd4fcc830b131y metadata SHA-256545436706e3fda7dc56742e0cdefc8510b49b4a1c005e1708139cc42a0f536c1. La comparacion visual contabilizo las 34 entradas y las correcciones pendientes exactas sonFATo7->FAT07,NSAvo6->NSAV06yDSAUo4->DSAU04. - Solicitar autorizacion explicita antes de aprobar, indexar o activar.
Criterios de salida
La correccion solo obtiene PASS cuando se cumplen todos estos puntos:
- Ningun trabajo
ocr-v1colisiona ni se reutiliza comoocr-v2. - Un reenvio de cada estado terminal o no terminal tiene conducta determinista y cubierta por pruebas.
- Las metricas de v7 para paginas 2, 20 y 21 producen advertencias, no bloqueo.
- Una pagina realmente inutilizable sigue fallando cerrada con diagnostico por pagina.
- Todas las razones de bloqueo y advertencias quedan persistidas por pagina con una razon primaria determinista.
- La cadena de hashes liga manifiesto de fuente, resultados OCR, manifiestos de imagenes, informe de calidad y candidata o error.
- Una caida local posterior a
succeededse recupera sin repetir OCR ni perder la finalizacion de la version. - Las 25 imagenes y todos los artefactos conservan identidad, hash, permisos y retencion correctos.
- RAG y OCR obtienen
0.2.1deVERSION, exponen metadatos coincidentes y se despliegan y prueban como una unica unidad. - La nueva candidata contiene las 34 entradas y alcanza
review_requiredsin indexacion ni activacion. - Los cuatro codigos criticos son exactos despues de la revision visual independiente del agente, con las 34 entradas contabilizadas, cero casos sin resolver y una comparacion de tres vias entre imagen, candidata OCR y referencia manual.
- La version activa anterior permanece disponible hasta una activacion autorizada y conserva rollback verificable.
Rollback
- RAG y OCR
0.2.1forman una unidad de compatibilidad; no se revierte solo uno de los dos. - Ante fallo, mantener detenidas las nuevas ingestas OCR, volver ambos servicios al commit previamente validado de la entrega
0.2.0dentro de la misma operacion y verificar conjuntamente salud, versiones y digests antes de reabrir trafico. - La migracion
004_ocr_quality_diagnostics.sqles aditiva y permanece aplicada al volver a0.2.0; no se eliminan columnas ni se rebaja el historial. Antes de reabrir trafico se debe confirmar que ambos servicios0.2.0arrancan y operan con ese esquema extendido. - No borrar versiones fallidas ni artefactos antes de capturar la evidencia necesaria.
- La version activa actual no se modifica durante implementacion, validacion, despliegue ni revision.