From 551cfe88168a3ccaa9eb309b3d1d115a8348bece Mon Sep 17 00:00:00 2001 From: Paco POR-CORREO Date: Fri, 11 Sep 2026 19:31:45 +0200 Subject: [PATCH] feat(rag): add knowledge lifecycle management --- .env.example | 7 + Dockerfile | 4 +- docs/API_RAG.md | 89 +- docs/CONTRATO_CICLO_VIDA_Y_OCR.md | 1169 +++++++++++++++++++++++ docs/DESPLIEGUE_EASYPANEL.md | 43 +- docs/HISTORIAL_SESIONES.md | 120 +++ docs/INGESTA.md | 51 +- docs/PENDIENTES_RAG.md | 5 + docs/SISTEMA_RAG_BASE.md | 14 +- migrations/001_knowledge_lifecycle.sql | 206 ++++ package-lock.json | 509 ++++++---- package.json | 13 +- public/playground/app.js | 28 +- public/playground/index.html | 13 +- src/api/openapi.ts | 237 ++++- src/app.ts | 238 ++++- src/config/env.ts | 33 +- src/modules/answer/service.ts | 2 + src/modules/catalog/client.ts | 94 ++ src/modules/catalog/errors.ts | 9 + src/modules/catalog/legacy-migration.ts | 150 +++ src/modules/catalog/lifecycle.ts | 11 + src/modules/catalog/migrations.ts | 90 ++ src/modules/catalog/reconciler.ts | 123 +++ src/modules/catalog/repository.ts | 641 +++++++++++++ src/modules/ingest/service.ts | 338 ++++++- src/modules/retrieve/service.ts | 70 +- src/modules/vectorstore/client.ts | 135 ++- src/scripts/migrate-legacy-lifecycle.ts | 448 +++++++++ src/shared/types/rag.ts | 32 +- src/shared/utils/ids.ts | 72 ++ tests/lifecycle-services.test.ts | 514 ++++++++++ tests/lifecycle-utils.test.ts | 61 ++ 33 files changed, 5308 insertions(+), 261 deletions(-) create mode 100644 docs/CONTRATO_CICLO_VIDA_Y_OCR.md create mode 100644 migrations/001_knowledge_lifecycle.sql create mode 100644 src/modules/catalog/client.ts create mode 100644 src/modules/catalog/errors.ts create mode 100644 src/modules/catalog/legacy-migration.ts create mode 100644 src/modules/catalog/lifecycle.ts create mode 100644 src/modules/catalog/migrations.ts create mode 100644 src/modules/catalog/reconciler.ts create mode 100644 src/modules/catalog/repository.ts create mode 100644 src/scripts/migrate-legacy-lifecycle.ts create mode 100644 tests/lifecycle-services.test.ts create mode 100644 tests/lifecycle-utils.test.ts diff --git a/.env.example b/.env.example index e204341..c83473d 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,10 @@ ANSWER_PROVIDER=openrouter ANSWER_MODEL=openai/gpt-4.1-mini ANSWER_BASE_URL=https://openrouter.ai/api/v1 ANSWER_API_KEY= +POSTGRES_URL=postgresql://rag:rag@postgres:5432/rag +POSTGRES_SSL=false +LIFECYCLE_ADMIN_TOKEN= +KNOWLEDGE_LIFECYCLE_ENFORCED=false +INGEST_WRITES_ENABLED=true +LIFECYCLE_RECONCILE_INTERVAL_MS=300000 +LIFECYCLE_INDEXING_STALE_TIMEOUT_MS=1800000 diff --git a/Dockerfile b/Dockerfile index df49f83..e06f719 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ WORKDIR /app COPY package.json package-lock.json tsconfig.json ./ RUN npm ci COPY src ./src +COPY migrations ./migrations COPY public ./public RUN npm run build @@ -12,6 +13,7 @@ ENV NODE_ENV=production COPY package.json package-lock.json ./ RUN npm ci --omit=dev COPY --from=build /app/dist ./dist +COPY --from=build /app/migrations ./migrations COPY public ./public EXPOSE 3000 -CMD ["node", "dist/server.js"] +CMD ["sh", "-c", "node dist/modules/catalog/migrations.js && node dist/server.js"] diff --git a/docs/API_RAG.md b/docs/API_RAG.md index b523bd3..26f7717 100644 --- a/docs/API_RAG.md +++ b/docs/API_RAG.md @@ -2,9 +2,9 @@ **Proyecto:** Workspace de tools IA para empresas **Modulo:** RAG -**Ultima actualizacion:** 2026-09-08 -**Ultima modificacion por:** Agente RAG 2 -**Estado:** Operativa y validada en produccion +**Ultima actualizacion:** 2026-09-11 +**Ultima modificacion por:** Subagente Implementacion ciclo de vida del conocimiento +**Estado:** Punto 2 implementado localmente; pendiente de migracion y validacion en produccion Esta guia explica como descubrir y consumir la API. El contrato tecnico completo y canonico se publica en OpenAPI 3.1.1; este documento prioriza el camino rapido, las decisiones de uso y los ejemplos habituales. @@ -24,7 +24,7 @@ https://rag.por-correo.com `GET /help` indica expresamente que debe consultarse `/openapi.json` para obtener los parametros, cuerpos, respuestas, errores y ejemplos completos de cada endpoint. -La API no requiere autenticacion actualmente. No debe asumirse que esta situacion es definitiva: la autenticacion y la proteccion de operaciones sensibles forman parte del pendiente 5. +La API general sigue sin autenticacion completa. Las operaciones administrativas del ciclo de vida usan una proteccion acotada con `Authorization: Bearer `; esto no sustituye la autenticacion general pendiente. ## Elegir la operacion correcta @@ -32,18 +32,20 @@ La API no requiere autenticacion actualmente. No debe asumirse que esta situacio |---|---| | Comprobar servicio y capacidades | `GET /health` | | Ver scopes ingeridos | `GET /sources` | +| Ver versiones de una fuente | `GET /sources/{sourceId}/versions` | +| Activar, rollback o purgar versiones | `/sources/*` con token administrativo | | Recuperar contexto para otro agente | `POST /retrieve` | | Obtener una respuesta final con citas | `POST /answer` | | Comparar una respuesta sin RAG | `POST /answer/direct` | | Conversar con bootstrap e historial | `POST /chat` | | Ingerir una ruta accesible por el servidor | `POST /ingest` | | Subir un archivo o carpeta ZIP | `POST /ingest/upload` | -| Eliminar una fuente antes de reingestar | `POST /cleanup` | +| Eliminar una fuente antes de reingestar | `POST /cleanup` legacy, o purga de version no activa | | Consultar o actualizar evaluaciones | `/logs/*` | ## Catalogo completo -La API queda formada por 16 operaciones tras incorporar el descubrimiento: +La API queda formada por las operaciones de descubrimiento, ingesta, retrieval, evaluacion y ciclo de vida: | Metodo | Ruta | Descripcion | |---|---|---| @@ -52,6 +54,13 @@ La API queda formada por 16 operaciones tras incorporar el descubrimiento: | `GET` | `/playground` | Interfaz web de prueba | | `GET` | `/health` | Estado, proveedores, parsers y chunking | | `GET` | `/sources` | Scopes disponibles en Qdrant | +| `GET` | `/sources/{sourceId}` | Fuente registrada en el catalogo | +| `GET` | `/sources/{sourceId}/versions` | Versiones de una fuente | +| `GET` | `/sources/{sourceId}/versions/{versionId}` | Detalle de una version | +| `POST` | `/sources/{sourceId}/versions/{versionId}/activate` | Activacion administrativa con precondicion | +| `POST` | `/sources/{sourceId}/rollback` | Rollback administrativo sin recalcular embeddings | +| `POST` | `/sources/{sourceId}/versions/{versionId}/retry` | Reintento explicito de version fallida o huerfana con fuente durable | +| `DELETE` | `/sources/{sourceId}/versions/{versionId}` | Purga administrativa de una version no activa | | `GET` | `/models/answer` | Modelo por defecto y modelos seleccionables | | `POST` | `/ingest` | Ingesta desde una ruta del servidor | | `POST` | `/ingest/upload` | Upload `multipart/form-data` | @@ -102,6 +111,15 @@ Los errores JSON siguen esta forma: Actualmente la mayoria de fallos de validacion o de proveedores se devuelven como `500`. `POST /cleanup` y `POST /ingest/upload` tambien pueden devolver `400`, y `PATCH /logs/{id}` devuelve `404` si no encuentra el log. Mejorar la validacion queda fuera de este bloque para no cambiar silenciosamente el comportamiento existente. +Con `KNOWLEDGE_LIFECYCLE_ENFORCED=true`, la API devuelve errores mas especificos para el catalogo: + +| Codigo HTTP | Caso | +|---|---| +| `400` | Falta `expectedActiveVersionId` cuando `activate=true` | +| `409` | Cambio concurrente de version activa, version en curso o intento de purgar una activa | +| `422` | Fuente vacia, sin documentos soportados o sin contenido util | +| `503` | PostgreSQL, Qdrant, embeddings o version activa inconsistente | + ## Recuperar contexto `POST /retrieve` es la operacion recomendada cuando otro agente o flujo generara la respuesta final. @@ -248,12 +266,22 @@ Respuesta: "sourceType": "folder", "sourceRef": "/data/project-docs", "mode": "mechanical", - "tags": ["project", "documentation"] + "tags": ["project", "documentation"], + "activate": true, + "expectedActiveVersionId": null } ``` `readPath` es un campo interno para separar la ruta fisica de lectura de la referencia logica. Los consumidores normales no deben enviarlo. +Con ciclo de vida activo: + +- `activate` por defecto es `true`. +- si `activate=true`, `expectedActiveVersionId` es obligatorio: `null` para primera activacion y UUID para reemplazar una version existente. +- una ingesta identica a la version activa responde `noOp: true` y no escribe nuevos puntos. +- `activate=false` deja la version en `ready` para activarla despues con una operacion administrativa. +- la respuesta exitosa cambia a `201 Created` e incluye `sourceId`, `versionId`, `versionNumber`, `state`, `previousVersionId`, `activated` y `noOp`. + ### Upload `POST /ingest/upload` usa `multipart/form-data`: @@ -265,6 +293,8 @@ Respuesta: | `mode` | No | `mechanical` o `interactive` | | `tags` | No | Lista separada por comas | | `isZipFolder` | No | Texto `true` o `false` | +| `activate` | No | Texto `true` o `false` | +| `expectedActiveVersionId` | Si activa | UUID o texto `null` | ```bash curl -sS -X POST "https://rag.por-correo.com/ingest/upload" \ @@ -274,11 +304,52 @@ curl -sS -X POST "https://rag.por-correo.com/ingest/upload" \ -F "tags=customer-a,support" ``` -Ambas variantes responden `202` con el origen aceptado, archivos descubiertos, documentos procesados, chunks almacenados y coleccion. El upload añade `uploadedResource`. +Con enforcement desactivado, ambas variantes conservan `202`. Con enforcement activado, responden `201` y usan el catalogo PostgreSQL como autoridad de version activa. El upload añade `uploadedResource`. + +## Ciclo de vida de fuentes + +El catalogo PostgreSQL es la autoridad para saber que fuentes existen, que versiones tienen y cual es la version activa. Qdrant conserva los puntos fisicos versionados, pero retrieval solo consulta versiones activas resueltas desde PostgreSQL. + +### Listado y detalle + +```text +GET /sources +GET /sources/{sourceId} +GET /sources/{sourceId}/versions +GET /sources/{sourceId}/versions/{versionId} +``` + +`GET /sources` mantiene los campos usados por el playground (`sourceId`, `sourceRef`, `chunkModes`, `tags`) y añade `activeVersionId`, `activeVersionNumber`, `state`, `needsReingest` y `updatedAt`. + +### Activacion y rollback + +```bash +curl -sS -X POST "https://rag.por-correo.com/sources/src:default:folder:docs/versions//activate" \ + -H "Authorization: Bearer $LIFECYCLE_ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"expectedActiveVersionId":""}' +``` + +Rollback usa la misma activacion atomica del puntero PostgreSQL: + +```json +{ + "targetVersionId": "uuid-version-anterior", + "expectedActiveVersionId": "uuid-version-activa-actual" +} +``` + +### Purga + +`DELETE /sources/{sourceId}/versions/{versionId}` exige token administrativo y solo acepta versiones no activas. La purga marca la version como `purging`, borra puntos Qdrant con `wait=true`, verifica que no quedan puntos y finalmente deja la version como `purged`. + +### Reintento explicito + +`POST /sources/{sourceId}/versions/{versionId}/retry` acepta versiones `failed`, `pending` o `indexing`. Si recibe una fuente durable (`sourceType`, `sourceRef` y opcionalmente `readPath`) cierra versiones huerfanas como fallidas y crea una version nueva mediante `/ingest`. Si no recibe fuente durable, devuelve `202` con instrucciones: los uploads temporales no se reintentan en sitio porque el original ya no esta garantizado. ## Limpieza controlada -`POST /cleanup` elimina los chunks que coinciden con un scope: +`POST /cleanup` elimina los chunks que coinciden con un scope solo en modo legacy y con `INGEST_WRITES_ENABLED=true`: ```json { diff --git a/docs/CONTRATO_CICLO_VIDA_Y_OCR.md b/docs/CONTRATO_CICLO_VIDA_Y_OCR.md new file mode 100644 index 0000000..a7a5fb5 --- /dev/null +++ b/docs/CONTRATO_CICLO_VIDA_Y_OCR.md @@ -0,0 +1,1169 @@ +# Contrato de implementacion: ciclo de vida del conocimiento y OCR + +**Proyecto:** Workspace de tools IA para empresas +**Modulo:** RAG +**Ultima actualizacion:** 2026-09-08 +**Ultima modificacion por:** Agente RAG 2 +**Estado:** Diseño cerrado, pendiente de implementacion + +## Resultado esperado + +Este documento define como completar, en este orden estricto: + +1. el pendiente 2, ciclo de vida del conocimiento; +2. 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 + +- `IngestService` escribe directamente en Qdrant y marca todos los chunks como `active`. +- 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. +- `/sources` reconstruye fuentes recorriendo los puntos de Qdrant. +- Los uploads individuales usan una ruta temporal con timestamp para construir `document_key`. +- El parser PDF usa `pdf-parse` sobre 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_servicios` con 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 + +```text +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: + +1. Puede haber cero o una version `active` por fuente. +2. Una version `active` no se puede purgar. +3. `ready` exige que todos los documentos y puntos esperados hayan sido verificados. +4. `superseded` nunca participa en retrieval normal, pero permanece disponible para rollback. +5. `failed` puede conservar puntos incompletos, siempre invisibles. +6. `purged` conserva el registro historico, pero no tiene puntos ni artefactos fisicos. +7. `review_required` y `rejected` se 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. + +```sql +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_id` no nulo apunta a una version `active` de la misma fuente; +- toda version `active` es el `active_version_id` de 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: + +```text +source_id = src::: +document_id = doc:: +chunk_id = chk:::<0001> +``` + +Reglas nuevas: + +- `document_key` de un archivo subido se deriva de `source_ref`, nunca de `readPath` ni del timestamp temporal. +- `source_id` explicito tiene prioridad. Si se genera, se deriva de una referencia logica estable. +- `original_hash` es SHA-256 de los bytes originales cuando `original_hash_kind = bytes`; para legacy sin original usa el hash derivado de chunks y `original_hash_kind = legacy-derived`. +- `original_manifest_hash` es SHA-256 de los pares ordenados `document_key + NUL + original_hash`; siempre existe antes de parsear. +- `content_hash` es SHA-256 del contenido final normalizado. Puede ser nulo mientras una extraccion OCR espera revision. +- `source_content_hash` usa los pares ordenados `document_key + NUL + content_hash`; puede ser nulo hasta que todo el contenido final exista. +- `processing_fingerprint` cubre version del parser, politica de normalizacion, politica de chunking y proveedor/modelo/dimensiones de embeddings. +- `metadata_hash` es 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_key` ascendente; +- 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-00c04fd430c8` y nombre `rag-qdrant-point::`. + +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: + +```text +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 + +```json +{ + "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 + +1. Resolver la identidad logica, crear/actualizar `rag_sources` y registrar `rag_ingestion_attempts.state = received` antes de descubrir archivos. +2. Adquirir un advisory lock PostgreSQL derivado de `source_id`; descubrir originales y calcular `original_manifest_hash` sin crear aun una version. +3. Rechazar con `422` una fuente sin documentos soportados o sin contenido util; nunca reemplazar por una version vacia. +4. Calcular hashes y `processing_fingerprint`. +5. Aplicar la tabla de coincidencias anterior; `noOp` solo corresponde a una version activa util. +6. Crear una sola version `pending`, asignar `version_number`, persistir `activate_requested`, `base_active_version_id`, tags y documentos. +7. Cambiarla a `indexing` y generar embeddings. +8. Escribir puntos versionados en Qdrant con IDs deterministas. +9. Contar puntos por `source_version_id` y verificar documentos, dimensiones y cantidad esperada. +10. Cambiar la version a `ready`. +11. En una transaccion PostgreSQL, bloquear la fuente, validar la precondicion obligatoria `expectedActiveVersionId`, pasar la activa anterior a `superseded`, pasar la nueva a `active` y actualizar `active_version_id`. +12. 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 /sources` consulta PostgreSQL y deja de recorrer Qdrant. +- `search()` y `browseScope()` resuelven primero las versiones activas aplicables. +- El filtro Qdrant siempre incluye `source_version_id` activo 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 `sourceVersionId` y `sourceVersionNumber`. +- 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_INCONSISTENT` hasta rollback o reparacion; nunca devuelve un corpus parcial como valido. + +Semantica de scope: + +- `sourceId`, `sourceRef` y cada tag recibido se combinan con AND; +- varios tags significan que todos deben estar presentes, igual que en el comportamiento actual; +- `sourceRef` usa coincidencia exacta sobre la fuente activa; +- `tenant` es `default` mientras el contrato publico no lo exponga y nunca se cruza entre fuentes; +- los tags viven en `rag_source_versions` y Qdrant como snapshot; cambiar tags altera `metadata_hash` y 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: + +```json +{ + "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`: + +```json +{ + "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 + +```text +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: + +```json +{ + "targetVersionId": "uuid", + "expectedActiveVersionId": "uuid" +} +``` + +Las operaciones de activacion, rollback y purga exigen `Authorization: Bearer `. 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: + +1. Añadir codigo y esquema con `KNOWLEDGE_LIFECYCLE_ENFORCED=false`. +2. 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 poner `rag_system_state.maintenance=true`. Toda creacion/activacion nueva comprueba esa fila bajo lock y devuelve `503 maintenance`; esperar a que no haya ingestas activas. +3. Ejecutar un `dry-run` que agrupe puntos por `source_id` y reporte colisiones, referencias incompatibles, modelos, dimensiones y recuentos. +4. Detener la migracion ante un `source_id` asociado a varias referencias incompatibles o proveedores/dimensiones incompatibles. +5. Crear un snapshot Qdrant y una fila `rag_legacy_migration_batches` con esa referencia antes de modificar payloads. +6. Crear un `rag_legacy_migration_items` por fuente y una version 1 asociada al `batch_id`. +7. Poblar cada documento legacy agrupando por `document_id`; derivar `document_key`, titulo, MIME, chunk count y hash de contenido ordenado. Si falta identidad documental fuerte, bloquear esa fuente. +8. Poblar proveedor, modelo, dimensiones y coleccion desde los payloads, exigiendo un unico valor por version. +9. Marcar `original_manifest_hash` y `source_content_hash` como derivados legacy; no presentarlos como hashes de archivos originales. +10. Añadir a cada punto `source_version_id`, `source_version_number: 1` y `processing_fingerprint` igual a SHA-256 de `legacy:`. +11. Verificar documentos, recuentos PostgreSQL/Qdrant y payloads; marcar item y lote `verified`. +12. Activar version 1 en PostgreSQL. +13. Activar `KNOWLEDGE_LIFECYCLE_ENFORCED=true`, redeployar y validar retrieval. +14. Reactivar `INGEST_WRITES_ENABLED=true` solo despues de la validacion. +15. Eliminar el camino legacy tras la validacion final, no mantener compatibilidad indefinida. + +Rollback de migracion: + +1. Bloquear en Traefik lectura y escritura del RAG y mantener `rag_system_state.maintenance=true`. +2. En una transaccion diferida, bloquear las fuentes del lote, cambiar sus versiones `active` a `purging` y poner `rag_sources.active_version_id = null`; confirmar que no queda ningun puntero activo del lote. +3. Detener las instancias con enforcement antes de restaurar Qdrant, para impedir retrieval contra un estado intermedio. +4. Restaurar el snapshot Qdrant y verificar sus recuentos legacy. +5. En transaccion, poner a nulo `rag_legacy_migration_items.version_id`, eliminar documentos y versiones del lote, y marcar items/lote `rolled_back`. +6. 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.json` y lockfile para `pg`, tipos y scripts de migracion; +- `src/config/env.ts` para PostgreSQL, secreto administrativo y feature flag temporal; +- `src/shared/types/rag.ts` para versiones, estados y respuestas; +- `src/shared/utils/ids.ts` para point IDs versionados y hashes; +- `src/modules/ingest/service.ts` para staging, verificacion y activacion; +- `src/modules/vectorstore/client.ts` para filtros e indices de payload; +- `src/modules/retrieve/service.ts` para resolver versiones activas; +- `src/modules/answer/service.ts` para propagar version en citas; +- `src/app.ts`, `src/api/openapi.ts` y playground para el nuevo contrato; +- `Dockerfile` para arrancar con `node dist/modules/catalog/migrations.js && node dist/server.js`; +- `docs/API_RAG.md`, `docs/INGESTA.md` y documentación de despliegue. + +## Orden de tareas del punto 2 + +1. Añadir esquema, migrador, configuracion y health de PostgreSQL. +2. Implementar repositorio de catalogo y tests de invariantes. +3. Versionar IDs y payloads Qdrant; crear indices de payload. +4. Refactorizar ingesta para staging, no-op, verificacion y activacion. +5. Hacer que retrieval y `/sources` dependan del catalogo. +6. Añadir API de versiones, rollback y purga administrativa. +7. Implementar reconciliador y migrador legacy con `dry-run`. +8. Actualizar playground, OpenAPI y documentacion. +9. Ejecutar `npm run check`, `npm run build` y validaciones estaticas. +10. Commit y push; desplegar con enforcement desactivado. +11. Ejecutar dry-run, snapshot, migracion y validacion de recuentos en produccion. +12. 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: true` y 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. +- `/health` informa 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 + +```text +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`: + +```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: + +1. se guarda el original durable; +2. se crea la version con `source_content_hash = null`, `activate_requested` y `base_active_version_id`, y despues el job; +3. se responde sin esperar los minutos de OCR. + +Respuesta `202 Accepted` de `/ingest` o `/ingest/upload`: + +```json +{ + "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; +- `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: + +```text +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: + +```text +inkCoverage < 0.015 AND nonWhitespaceCharactersOCR < 10 +``` + +Reglas: + +- `N == 0` siempre solicita inspeccion OCR; el servicio decide si esta vacia. +- Una pagina con `inkCoverage >= 0.015` no 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-v1"`. + +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; +- `text` de 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_number` ascendente y separar paginas no vacias con `\n\n--- Page ---\n\n`; +- no concatenar texto nativo insuficiente con OCR porque duplica y mezcla variantes. + +## Contrato interno del servicio OCR + +### 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 con `documentSha256`; +- `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. + +```json +{ + "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`: + +```text +:: +``` + +Respuesta `202`: + +```json +{ + "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`: + +```json +{ + "jobId": "ocr_uuid", + "status": "queued|running|succeeded|failed", + "completedPages": 2, + "totalPages": 5, + "error": null +} +``` + +### Obtener resultado + +`GET /v1/jobs/:jobId/result` devuelve por pagina: + +```json +{ + "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: + +```text +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 + +Una pagina OCR produce candidato mecanicamente revisable si cumple: + +```text +nonWhitespaceCharacters >= 40 +medianConfidence >= 0.80 +p10Confidence >= 0.50 +lowConfidenceLineRatio <= 0.20 +``` + +El documento solo llega a `review_required` cuando: + +1. cada pagina no vacia tiene texto nativo u OCR util; +2. ninguna pagina solicitada fallo; +3. numero y orden de paginas coinciden con el original; +4. hashes de original, extracciones y manifiesto coinciden; +5. 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: + +```regex +\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`, `estado` o `identificador`. + +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: + +```text +GET /ingestions/:versionId +GET /ingestions/:versionId/review +POST /ingestions/:versionId/approve +POST /ingestions/:versionId/reject +``` + +Todos exigen `Authorization: Bearer `, incluidos lectura de revision, imagenes, aprobar y rechazar. + +Aprobacion: + +```json +{ + "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: + +1. Verificar que `candidateSha256` y todos los `expectedLineSha256` siguen vigentes, y que `expectedActiveVersionId` coincide con `base_active_version_id` persistido. +2. Aplicar todas las correcciones en una unica operacion y guardar texto revisado inmutable. +3. Registrar revisor, fecha y hashes. +4. Calcular `content_hash`, `source_content_hash` y comprobar bajo lock si ya existe una version reutilizable con el mismo contenido, fingerprint y metadata. +5. Si existe, marcar el candidato `rejected` con `DUPLICATE_REUSABLE_VERSION` y activar la version existente solo si cumple la precondicion. +6. Si no existe, persistir hashes y cambiar `review_required -> indexing`. +7. Solo entonces generar chunks y embeddings. +8. Verificar puntos y comparar entonces `expectedActiveVersionId` con 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: + +```text +/data/ingestions// + manifest.json + metrics.json + documents/ + / + original.pdf + native-pages.json.gz + ocr-raw.json.gz + candidate-pages.json.gz + reviewed-pages.json.gz + review-images/ + page-0001.webp +``` + +`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, `502` o `503`, 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`, `422` ni errores deterministas de render. +- `429` conserva 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: + +```text +ocr-service/ + Dockerfile + requirements.txt + app/ + main.py + engine.py + models.py + jobs.py +``` + +Condiciones: + +- imagen Python CPU separada; +- PaddleOCR `3.4.0` y PaddlePaddle `3.2.2` fijados 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 como `Authorization: Bearer` y comparado en tiempo constante; +- URL RAG `OCR_SERVICE_URL=http://: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.ts` para resultado por pagina; +- `src/modules/ingest/service.ts` para estados OCR y aprobacion; +- `src/shared/types/rag.ts` para paginas, metricas, jobs y revision; +- `src/config/env.ts` para URL, token, volumen, limites y timeouts; +- `src/app.ts` para upload acotado y endpoints de revision; +- `src/api/openapi.ts`, playground y documentación API; +- `Dockerfile` y configuracion EasyPanel para el volumen RAG. + +## Orden de tareas del punto 3 + +1. Añadir migracion OCR y contratos TypeScript sin cambiar aun el flujo nativo. +2. Implementar extraccion PDF por pagina, composicion canonica, metricas y tests de deteccion. +3. Crear servicio OCR privado con modelos baked, auth, limites, idempotencia y health. +4. Implementar cliente RAG, reintentos, polling y validacion estricta de respuesta. +5. Añadir volumen y gestor de artefactos con hashes, permisos y retencion. +6. Integrar respuesta asincrona, estados `review_required`, `rejected` y fallo cerrado. +7. Añadir deteccion de identificadores y vista de revision en playground. +8. Implementar aprobar/rechazar y enlazar aprobacion con indexacion versionada. +9. Actualizar OpenAPI, API, ingesta, procesado y despliegue. +10. Ejecutar comprobaciones estaticas Node y tests del servicio OCR. +11. Commit y push; desplegar primero OCR privado y validar readiness. +12. Desplegar RAG con OCR deshabilitado y comprobar regresiones nativas. +13. 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/o` aparecen marcados para revision. +- La aprobacion con correcciones produce chunks con `CBG04a`, `FAT07`, `DSAU08` y `NSAV06` exactos. +- 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. +- `/health` informa 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. + +# Verificacion y entrega comun + +Cada punto se entrega como bloque independiente: + +1. implementar en local; +2. ejecutar `npm run check`, `npm run build` y las pruebas estaticas aplicables; +3. documentar; +4. revisar que no entran secretos; +5. commit y push al repo `paco/rag-service`; +6. deploy en EasyPanel; +7. pruebas funcionales exclusivamente en produccion; +8. rollback por Git y redeploy si falla; +9. cerrar el punto en `PENDIENTES_RAG.md` solo con evidencia. + +## 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. diff --git a/docs/DESPLIEGUE_EASYPANEL.md b/docs/DESPLIEGUE_EASYPANEL.md index 8410bd3..f8420cc 100644 --- a/docs/DESPLIEGUE_EASYPANEL.md +++ b/docs/DESPLIEGUE_EASYPANEL.md @@ -2,9 +2,9 @@ **Proyecto:** Workspace de tools IA para empresas **Modulo:** RAG -**Ultima actualizacion:** 2026-04-05 -**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales -**Estado:** V1 desplegada en VPS2 +**Ultima actualizacion:** 2026-09-11 +**Ultima modificacion por:** Subagente Implementacion ciclo de vida del conocimiento +**Estado:** V1 desplegada en VPS2; ciclo de vida implementado localmente pendiente de migracion productiva --- @@ -200,6 +200,12 @@ Dato importante para `RAG`: - `ANSWER_MODEL=openai/gpt-4.1-mini` - `ANSWER_BASE_URL=https://openrouter.ai/api/v1` - `ANSWER_API_KEY=` +- `POSTGRES_URL=` +- `POSTGRES_SSL=false` salvo que la conexion lo exija +- `LIFECYCLE_ADMIN_TOKEN=` +- `KNOWLEDGE_LIFECYCLE_ENFORCED=false` durante despliegue inicial y migracion +- `INGEST_WRITES_ENABLED=false` durante la ventana de migracion legacy +- `LIFECYCLE_RECONCILE_INTERVAL_MS=300000` ### Variables de entorno con las que quedo operativo en VPS2 @@ -219,6 +225,15 @@ Valores funcionales confirmados en EasyPanel: - `ANSWER_BASE_URL=https://openrouter.ai/api/v1` - `ANSWER_API_KEY=` +Variables pendientes de completar para activar el punto 2 en produccion: + +- `POSTGRES_URL=` +- `POSTGRES_SSL=false` +- `LIFECYCLE_ADMIN_TOKEN=` +- `KNOWLEDGE_LIFECYCLE_ENFORCED=false` hasta completar la migracion legacy +- `INGEST_WRITES_ENABLED=true` en operacion normal; `false` durante migracion +- `LIFECYCLE_RECONCILE_INTERVAL_MS=300000` + Para respaldo operativo local fuera de EasyPanel se deja un archivo ignorado por Git: - `RAG/.env.easypanel.local` @@ -228,7 +243,27 @@ Ese archivo no debe versionarse porque contiene secretos reales. ### Volumen persistente - no es obligatorio para la app RAG en esta fase -- el estado persistente principal vive en `qdrant` +- el estado persistente vectorial vive en `qdrant` +- desde el punto 2, el catalogo de fuentes y versiones vive en PostgreSQL + +### Arranque con migraciones + +La imagen arranca ejecutando primero: + +```bash +node dist/modules/catalog/migrations.js && node dist/server.js +``` + +Si `POSTGRES_URL` no esta configurada, el migrador informa que omite migraciones. Si PostgreSQL esta configurado y una migracion falla o cambia su checksum, el servicio no debe completar el arranque. + +### Secuencia operativa del punto 2 + +1. Desplegar el codigo con `KNOWLEDGE_LIFECYCLE_ENFORCED=false`. +2. Bloquear escrituras con `INGEST_WRITES_ENABLED=false` durante la migracion. +3. Ejecutar `npm run migrate:legacy:lifecycle` en modo dry-run y revisar colisiones. +4. Tomar snapshot Qdrant antes de cualquier migracion destructiva. +5. Catalogar el corpus legacy y verificar recuentos PostgreSQL/Qdrant. Si el proceso cae, reanudar con `npm run migrate:legacy:lifecycle -- --apply --snapshot-ref= --batch-id=`. +6. Activar `KNOWLEDGE_LIFECYCLE_ENFORCED=true` solo despues de validar retrieval y playground. --- diff --git a/docs/HISTORIAL_SESIONES.md b/docs/HISTORIAL_SESIONES.md index a5293e1..b776bc7 100644 --- a/docs/HISTORIAL_SESIONES.md +++ b/docs/HISTORIAL_SESIONES.md @@ -93,3 +93,123 @@ Dar continuidad al RAG en `RAG/` a partir del estado actual documentado. - Pendiente 1 completado y validado definitivamente en `https://rag.por-correo.com`. - La API dispone de descubrimiento rapido y contrato OpenAPI para sus 16 operaciones. - Siguiente prioridad: ciclo de vida del conocimiento. + +--- + +### 2026-09-08 - Agente RAG 2 + +**Modelo:** gpt-5.6-sol +**Session ID OpenCode:** `ses_29bdbd003ffeLrLjUlFgnp08Y7` +**Directorio:** `/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG` + +**Subagentes de analisis:** +- `Subagente Extraccion de versiones PaddleOCR`: contraste de versiones PyPI compatibles para el benchmark CPU. +- `Subagente Validacion del ciclo de vida`: revision adversarial del catalogo PostgreSQL y versionado Qdrant. +- `Subagente Validacion OCR`: revision adversarial de deteccion, servicio privado, quality gates y revision humana. +- `Subagente Auditoria de claridad del contrato`: detecto contradicciones de hashes, idempotencia, migracion y revision. +- `Subagente Revalidacion de ejecutabilidad`: detecto bloqueos restantes en metadata, concurrencia y artefactos. +- `Subagente Revalidacion de resiliencia`: reviso carreras, barreras de migracion y recuperacion de jobs. +- `Subagente Revision final del contrato`: verifico y cerro la secuencia segura de rollback legacy. + +**Trabajo realizado:** +- Auditoria del pipeline actual de ingesta, IDs, Qdrant, parser PDF, API e infraestructura EasyPanel. +- Confirmacion de PostgreSQL 17 como catalogo transaccional y Qdrant como almacenamiento vectorial versionado. +- Benchmark aislado en VPS2 con el PDF mixto real de FacturaTech. +- Descarte de Tesseract como motor unico por omitir identificadores criticos. +- Seleccion de PaddleOCR como motor principal y deteccion de errores alfanumericos de alta confianza que obligan a revision humana. +- Creacion de `docs/CONTRATO_CICLO_VIDA_Y_OCR.md` con esquema, estados, APIs, migracion, OCR, tareas, pruebas, despliegue y rollback. +- Enlace del contrato desde el backlog canonico. +- Correccion del contrato mediante revisiones adversariales hasta eliminar sus bloqueos criticos conocidos. +- Limpieza de los artefactos e imagenes temporales usados en el benchmark local y de VPS2. + +**Estado final:** +- Diseño de los pendientes 2 y 3 cerrado y listo para implementacion secuencial. +- El punto 2 debe completarse y validarse en produccion antes de iniciar el punto 3. +- No se modifico codigo ni produccion durante esta fase de diseño. + +**Archivos modificados:** +- `RAG/docs/CONTRATO_CICLO_VIDA_Y_OCR.md` +- `RAG/docs/PENDIENTES_RAG.md` +- `RAG/docs/HISTORIAL_SESIONES.md` +- `docs/INDICE_DOCUMENTACION.md` +- `docs/HISTORIAL_SESIONES.md` + +--- + +### 2026-09-11 - Subagente Implementacion ciclo de vida del conocimiento + +**Modelo:** openai/gpt-5.5 +**Session ID OpenCode:** `ses_f6fe0ad30ffeiottYsATsRemCo` +**Directorio:** `/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG` + +**Rol asumido:** +Ejecutar la fase apply del punto 2 del RAG y corregir los bloqueadores finales detectados antes de commit, push o despliegue. + +**Trabajo realizado:** +- Ajuste del migrador legacy para permitir reanudacion por `--batch-id`, reutilizando versiones ya creadas y completando activacion tras actualizaciones de payload ya aplicadas. +- Endurecimiento de rollback legacy con lock global de migracion, comprobacion de snapshot y operaciones serializadas sobre fuentes/versiones. +- Reconciliador protegido con advisory lock no bloqueante, timeout configurable basado en `indexing_started_at` y validacion de coleccion/dimensiones. +- Protecciones adicionales: `POST /cleanup` respeta `INGEST_WRITES_ENABLED`, `markPurged` valida `rowCount`, rollback usa locks y OpenAPI refleja `503` en health con `ok` booleano. +- Script `migrate:legacy:lifecycle` ajustado para ejecutarse desde `dist/` con Node tras build. +- Añadidos tests unitarios para reanudacion legacy, activacion idempotente, cleanup con escrituras deshabilitadas, reconciliador, mismatch de coleccion y `markPurged`. + +**Validacion local:** +- `npm run check` correcto. +- `npm test` correcto: 18 tests pasan. +- `npm run build` correcto. +- `git diff --check` correcto. + +**Estado final:** +- Punto 2 implementado y validado localmente. +- No se hizo commit, push, deploy ni lectura de `.env`. +- Sigue pendiente la verificacion independiente y la validacion/migracion en produccion antes de marcar el punto 2 como cerrado. + +**Archivos modificados en esta correccion:** +- `.env.example` +- `package.json` +- `package-lock.json` +- `src/config/env.ts` +- `src/modules/catalog/client.ts` +- `src/modules/catalog/repository.ts` +- `src/modules/catalog/reconciler.ts` +- `src/modules/vectorstore/client.ts` +- `src/modules/retrieve/service.ts` +- `src/modules/ingest/service.ts` +- `src/scripts/migrate-legacy-lifecycle.ts` +- `src/api/openapi.ts` +- `tests/lifecycle-services.test.ts` +- `docs/API_RAG.md` +- `docs/DESPLIEGUE_EASYPANEL.md` +- `docs/SISTEMA_RAG_BASE.md` +- `RAG/docs/HISTORIAL_SESIONES.md` + +--- + +### 2026-09-11 (reanudacion final) - Subagente Implementacion ciclo de vida del conocimiento + +**Modelo:** openai/gpt-5.5 +**Session ID OpenCode:** `ses_f6fe0ad30ffeiottYsATsRemCo` +**Directorio:** `/home/pancho/Documentos/Empresa/Desarrollo/IA/RAG` + +**Rol asumido:** +Completar los 4 bloqueos del sdd-verify y cerrar la validacion local del punto 2. + +**Trabajo realizado:** +- Reconciliador: recupera versiones huerfanas `pending/indexing` a `ready` contando puntos Qdrant y validando documentos, coleccion y dimensiones; marca `failed` solo si quedan incompletas; try-locks evitan matar ingestas vivas. +- Ingesta: rechaza embeddings con dimensiones distintas de las esperadas antes del upsert y antes de alcanzar `ready`. +- Rollback legacy: transaccion diferida del lote, confirmacion de cero punteros activos, restauracion de snapshot con verificacion de recuentos y limpieza posterior del catalogo. +- OpenAPI: `/sources` documenta `503`. +- Retrieval: locks compartidos adquiridos antes de validar recuento/dimensiones y mantenidos durante toda la consulta Qdrant. +- Interrupcion por limite de uso a mitad de la validacion; reanudada tras cambio de cuenta y completada hasta el final. + +**Validacion local:** +- `npm run check` correcto. +- `npm test` correcto: 25/25 tests pasan. +- `npm run build` correcto. +- `git diff --check` correcto. +- Verificacion independiente (sdd-verify): PASS local. + +**Estado final:** +- Implementacion local del punto 2 verificada de forma independiente: PASS local. +- No se hizo commit, push, deploy ni lectura de `.env`. +- Pendiente: commit/push, deploy con enforcement desactivado, migracion legacy y validacion en produccion antes de cerrar el punto 2. diff --git a/docs/INGESTA.md b/docs/INGESTA.md index 0e6ba6e..601c734 100644 --- a/docs/INGESTA.md +++ b/docs/INGESTA.md @@ -2,9 +2,9 @@ **Proyecto:** Workspace de tools IA para empresas **Modulo:** RAG -**Ultima actualizacion:** 2026-04-02 -**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales -**Estado:** En definicion +**Ultima actualizacion:** 2026-09-11 +**Ultima modificacion por:** Subagente Implementacion ciclo de vida del conocimiento +**Estado:** Punto 2 implementado localmente; pendiente de migracion y validacion en produccion --- @@ -531,3 +531,48 @@ El enriquecimiento ligero debe ser: 2. Como construiremos exactamente `source_id`, `document_id` y `chunk_id`. 3. Que nombre tendran las fuentes logicas por defecto para documentos sueltos. 4. Como distinguiremos entre fuente nueva, fuente actualizada y fuente obsoleta. + +--- + +## Ciclo de vida del conocimiento implementado localmente + +El punto 2 introduce un catalogo PostgreSQL como autoridad de fuentes y versiones. Qdrant deja de decidir que conocimiento esta activo: cada punto fisico queda asociado a `source_version_id`, y retrieval filtra por las versiones activas resueltas desde PostgreSQL cuando `KNOWLEDGE_LIFECYCLE_ENFORCED=true`. + +### Flujo de ingesta versionada + +1. Resolver `source_id` y registrar un intento en `rag_ingestion_attempts`. +2. Descubrir documentos soportados y calcular hashes de originales. +3. Normalizar contenido y calcular hashes de contenido, fingerprint de procesamiento y hash de metadatos. +4. Detectar si ya existe una version reutilizable. +5. Crear una version `pending`, pasarla a `indexing`, escribir puntos Qdrant versionados y verificar recuentos. +6. Pasar la version a `ready`. +7. Si `activate=true`, activar con precondicion `expectedActiveVersionId`. + +Una escritura parcial en Qdrant no queda visible para retrieval hasta que PostgreSQL mueve atomica y correctamente el puntero `active_version_id`. + +### Nuevos campos de ingesta + +```json +{ + "activate": true, + "expectedActiveVersionId": null +} +``` + +- `activate=true` exige `expectedActiveVersionId`. +- Para primera activacion se envia `null`. +- Para reemplazo se envia el UUID de la version activa que el cliente espera reemplazar. +- `activate=false` permite dejar una version preparada en estado `ready`. + +### Identidad y hashes + +- `document_key` de uploads se deriva de `source_ref`, no de rutas temporales. +- `original_hash` usa SHA-256 de bytes originales. +- `content_hash` usa SHA-256 del contenido normalizado. +- `source_content_hash` y `original_manifest_hash` usan pares ordenados `document_key + NUL + hash`. +- `processing_fingerprint` cambia cuando cambian parser, normalizacion, chunking, proveedor, modelo o dimensiones de embeddings. +- El ID fisico de Qdrant usa UUIDv5 con `source_version_id + chunk_id`. + +### Compatibilidad temporal + +Con `KNOWLEDGE_LIFECYCLE_ENFORCED=false`, el camino legacy se conserva para permitir despliegue, dry-run y migracion controlada. La validacion final del punto 2 exige activar enforcement despues de catalogar el corpus legacy. diff --git a/docs/PENDIENTES_RAG.md b/docs/PENDIENTES_RAG.md index 2b8be0c..5448ccd 100644 --- a/docs/PENDIENTES_RAG.md +++ b/docs/PENDIENTES_RAG.md @@ -17,6 +17,8 @@ Este documento es la fuente canonica del orden de trabajo pendiente del modulo R ## 2. Ciclo de vida del conocimiento +**Diseño:** Contrato de implementacion cerrado en [`CONTRATO_CICLO_VIDA_Y_OCR.md`](./CONTRATO_CICLO_VIDA_Y_OCR.md). Pendiente de implementar y validar en produccion antes de iniciar el punto 3. + - Crear un catalogo de fuentes y versiones ingeridas. - Saber que documento esta vigente, obsoleto, reemplazado o pendiente de reingesta. - Evitar duplicados y permitir actualizaciones incrementales controladas. @@ -25,6 +27,8 @@ Este documento es la fuente canonica del orden de trabajo pendiente del modulo R ## 3. OCR integrado en la ingesta +**Diseño:** Contrato de implementacion cerrado en [`CONTRATO_CICLO_VIDA_Y_OCR.md`](./CONTRATO_CICLO_VIDA_Y_OCR.md). Su ejecucion queda bloqueada hasta completar el punto 2. + - Detectar PDFs con capturas, imagenes o una capa textual insuficiente. - Ejecutar OCR automaticamente o bloquear la ingesta para revision cuando no pueda garantizarse la cobertura. - Generar un documento intermedio auditable cuando sea necesario. @@ -46,6 +50,7 @@ Este documento es la fuente canonica del orden de trabajo pendiente del modulo R - Aplicar rate limiting y limites de tamano o tipo de archivo. - Revisar CORS, exposicion publica, registros sensibles y permisos por operacion. - Sustituir la configuracion sin credenciales de n8n cuando exista el mecanismo de autenticacion definitivo. +- Actualizar Express/`qs` para cerrar los avisos moderados pendientes de `npm audit`. ## 6. Pruebas automatizadas diff --git a/docs/SISTEMA_RAG_BASE.md b/docs/SISTEMA_RAG_BASE.md index de571a0..44166b8 100644 --- a/docs/SISTEMA_RAG_BASE.md +++ b/docs/SISTEMA_RAG_BASE.md @@ -2,9 +2,9 @@ **Proyecto:** Workspace de tools IA para empresas **Modulo:** RAG -**Ultima actualizacion:** 2026-04-05 -**Ultima modificacion por:** Agente tools IA para potenciar servicios empresariales -**Estado:** V1 operativa desplegada +**Ultima actualizacion:** 2026-09-11 +**Ultima modificacion por:** Agente RAG 2 +**Estado:** V1 operativa; ciclo de vida implementado localmente y pendiente de despliegue --- @@ -105,6 +105,14 @@ Pruebas funcionales ya superadas: - consulta conceptual sobre caracteristicas del RAG - consulta tecnica en modo codigo sobre la construccion de `source_id` +### Ciclo de vida del conocimiento + +Cada fuente dispone de versiones controladas en PostgreSQL, mientras Qdrant conserva sus chunks y embeddings. Solo la version activa participa en las consultas. + +Esto permite actualizar una fuente sin mezclar contenido antiguo y nuevo, detectar ingestas identicas, conservar versiones anteriores y hacer rollback sin recalcular embeddings. Los parametros de consulta actuales no cambian. + +Esta mejora esta implementada y validada localmente. Falta desplegarla y registrar el corpus existente como version inicial antes de activarla en produccion. + Los pendientes vigentes, su prioridad y su estado se mantienen en [`PENDIENTES_RAG.md`](./PENDIENTES_RAG.md). --- diff --git a/migrations/001_knowledge_lifecycle.sql b/migrations/001_knowledge_lifecycle.sql new file mode 100644 index 0000000..73af98e --- /dev/null +++ b/migrations/001_knowledge_lifecycle.sql @@ -0,0 +1,206 @@ +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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 +); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'rag_sources_active_version_fk' + ) THEN + 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; + END IF; +END $$; + +CREATE UNIQUE INDEX IF NOT EXISTS rag_one_active_version_per_source + ON rag_source_versions(source_id) + WHERE state = 'active'; + +CREATE UNIQUE INDEX IF NOT EXISTS 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 IF NOT EXISTS rag_source_versions_source_state_idx + ON rag_source_versions(source_id, state); + +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS rag_version_documents_document_idx + ON rag_version_documents(document_id); + +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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) +ON CONFLICT (singleton) DO NOTHING; + +CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS 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) +); + +CREATE OR REPLACE FUNCTION rag_touch_updated_at() +RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + NEW.updated_at = now(); + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS rag_sources_touch_updated_at ON rag_sources; +CREATE TRIGGER rag_sources_touch_updated_at +BEFORE UPDATE ON rag_sources +FOR EACH ROW EXECUTE FUNCTION rag_touch_updated_at(); + +CREATE OR REPLACE FUNCTION rag_validate_active_source_pointer() +RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE + invalid_pointer_count integer; + orphan_active_count integer; +BEGIN + SELECT count(*) INTO invalid_pointer_count + FROM rag_sources s + LEFT JOIN rag_source_versions v + ON v.version_id = s.active_version_id + AND v.source_id = s.source_id + AND v.state = 'active' + WHERE s.active_version_id IS NOT NULL + AND v.version_id IS NULL; + + IF invalid_pointer_count > 0 THEN + RAISE EXCEPTION 'rag_sources.active_version_id must point to an active version in the same source'; + END IF; + + SELECT count(*) INTO orphan_active_count + FROM rag_source_versions v + JOIN rag_sources s ON s.source_id = v.source_id + WHERE v.state = 'active' + AND s.active_version_id IS DISTINCT FROM v.version_id; + + IF orphan_active_count > 0 THEN + RAISE EXCEPTION 'every active rag_source_versions row must be the source active_version_id'; + END IF; + + RETURN NULL; +END; +$$; + +DROP TRIGGER IF EXISTS rag_sources_validate_active_pointer ON rag_sources; +CREATE CONSTRAINT TRIGGER rag_sources_validate_active_pointer +AFTER INSERT OR UPDATE ON rag_sources +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION rag_validate_active_source_pointer(); + +DROP TRIGGER IF EXISTS rag_versions_validate_active_pointer ON rag_source_versions; +CREATE CONSTRAINT TRIGGER rag_versions_validate_active_pointer +AFTER INSERT OR UPDATE ON rag_source_versions +DEFERRABLE INITIALLY DEFERRED +FOR EACH ROW EXECUTE FUNCTION rag_validate_active_source_pointer(); diff --git a/package-lock.json b/package-lock.json index beb2bb3..67006b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,13 @@ "version": "0.1.0", "dependencies": { "@qdrant/js-client-rest": "^1.15.0", - "adm-zip": "^0.5.17", + "adm-zip": "^0.6.1", "dotenv": "^16.4.5", "express": "^4.21.2", - "multer": "^2.0.0", + "multer": "^2.3.0", "openai": "^4.104.0", - "pdf-parse": "^1.1.1" + "pdf-parse": "^1.1.1", + "pg": "^8.23.0" }, "devDependencies": { "@types/adm-zip": "^0.5.8", @@ -22,14 +23,15 @@ "@types/multer": "^1.4.12", "@types/node": "^22.15.3", "@types/pdf-parse": "^1.1.4", + "@types/pg": "^8.23.1", "tsx": "^4.19.3", "typescript": "^5.8.3" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -44,9 +46,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -61,9 +63,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -78,9 +80,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -95,9 +97,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -112,9 +114,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -129,9 +131,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -146,9 +148,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -163,9 +165,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -180,9 +182,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -197,9 +199,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -214,9 +216,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -231,9 +233,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -248,9 +250,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -265,9 +267,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -282,9 +284,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -299,9 +301,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -316,9 +318,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -333,9 +335,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -350,9 +352,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -367,9 +369,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -384,9 +386,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -401,9 +403,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -418,9 +420,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -435,9 +437,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -452,9 +454,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -597,6 +599,18 @@ "@types/node": "*" } }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, "node_modules/@types/qs": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", @@ -658,12 +672,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", - "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/agentkeepalive": { @@ -697,9 +711,9 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -710,7 +724,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.14.0", + "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -720,6 +734,22 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -962,9 +992,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -975,32 +1005,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/escape-html": { @@ -1028,14 +1058,14 @@ } }, "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "~1.20.3", + "body-parser": "~1.20.5", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", @@ -1054,7 +1084,7 @@ "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "~6.14.0", + "qs": "~6.15.1", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", @@ -1092,16 +1122,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -1205,19 +1235,6 @@ "node": ">= 0.4" } }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1258,9 +1275,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1401,9 +1418,9 @@ "license": "MIT" }, "node_modules/multer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", - "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.3.0.tgz", + "integrity": "sha512-cjNbm3sttszgZeGfJR124D+jFEfkXCVAsoPBmFn9X7UxmDSFHWqE2CoEj0vrmSpuAFnqWR1Szcm9QTsiHr60Xw==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", @@ -1574,6 +1591,134 @@ "url": "https://github.com/sponsors/mehmet-kozan" } }, + "node_modules/pg": { + "version": "8.23.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.0.tgz", + "integrity": "sha512-Ip2EQCngowJLGOfCwkFhPXU7/ljlhn6Rxlmy4XYfL2Y+vyRM59+8uR2xqRWKdYmbXmxCFOAmKxBuSUCdF34qLg==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.0.tgz", + "integrity": "sha512-XwWDGcLRGCXAR8F/AM5bG7Q+A3Wm2s6QeEjlOKZLlH3UYcguiqCWKyWXVag5TLTIjR7oOJUY8kcADaZgWPyLeg==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.0.tgz", + "integrity": "sha512-sILXutLVjCLjcDuOmvhX5e2Z4cS5qG/6Bu3VkpFwdf/633ElGLpEh9bgmuI5I4sqKqkifQiGyiCcx1HdtrK7tg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1588,12 +1733,13 @@ } }, "node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -1640,16 +1786,6 @@ "node": ">= 6" } }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1728,14 +1864,14 @@ "license": "ISC" }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -1747,13 +1883,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -1799,6 +1935,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1841,14 +1986,13 @@ "license": "MIT" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -1893,9 +2037,9 @@ } }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.28.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz", + "integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -1964,6 +2108,15 @@ "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } } } } diff --git a/package.json b/package.json index e5ea328..4b47062 100644 --- a/package.json +++ b/package.json @@ -6,17 +6,21 @@ "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc -p tsconfig.json", - "start": "node dist/server.js", + "start": "node dist/modules/catalog/migrations.js && node dist/server.js", + "migrate:lifecycle": "node dist/modules/catalog/migrations.js", + "migrate:legacy:lifecycle": "node dist/scripts/migrate-legacy-lifecycle.js", + "test": "NODE_ENV=test tsx --test tests/**/*.test.ts", "check": "tsc --noEmit -p tsconfig.json" }, "dependencies": { "@qdrant/js-client-rest": "^1.15.0", - "adm-zip": "^0.5.17", + "adm-zip": "^0.6.1", "dotenv": "^16.4.5", "express": "^4.21.2", - "multer": "^2.0.0", + "multer": "^2.3.0", "openai": "^4.104.0", - "pdf-parse": "^1.1.1" + "pdf-parse": "^1.1.1", + "pg": "^8.23.0" }, "devDependencies": { "@types/adm-zip": "^0.5.8", @@ -24,6 +28,7 @@ "@types/multer": "^1.4.12", "@types/node": "^22.15.3", "@types/pdf-parse": "^1.1.4", + "@types/pg": "^8.23.1", "tsx": "^4.19.3", "typescript": "^5.8.3" } diff --git a/public/playground/app.js b/public/playground/app.js index 2544076..188dc14 100644 --- a/public/playground/app.js +++ b/public/playground/app.js @@ -38,6 +38,8 @@ const btnClearUpload = document.getElementById("btnClearUpload"); const uploadStatusText = document.getElementById("uploadStatusText"); const ingestMode = document.getElementById("ingestMode"); const ingestTags = document.getElementById("ingestTags"); +const ingestActivate = document.getElementById("ingestActivate"); +const ingestExpectedActiveVersionId = document.getElementById("ingestExpectedActiveVersionId"); const ingestModeHint = document.getElementById("ingestModeHint"); const cleanupScopeSelect = document.getElementById("cleanupScopeSelect"); @@ -80,7 +82,17 @@ function splitTags(value) { function buildScopeLabel(scope) { const modes = scope.chunkModes.join(", ") || "sin modo"; - return `${scope.sourceRef} [${modes}]`; + const version = scope.activeVersionNumber ? ` v${scope.activeVersionNumber}` : ""; + const state = scope.state ? ` ${scope.state}` : ""; + return `${scope.sourceRef}${version}${state} [${modes}]`; +} + +function normalizeExpectedActiveVersionInput() { + const value = ingestExpectedActiveVersionId.value.trim(); + if (!value) { + return undefined; + } + return value; } let previousSourceRef = ""; // Para guardar el valor original de SourceRef @@ -363,6 +375,11 @@ ingestButton.addEventListener("click", async () => { formData.append("file", ingestUploadFile.files[0]); formData.append("mode", ingestMode.value); formData.append("tags", splitTags(ingestTags.value).join(",")); + formData.append("activate", ingestActivate.value); + const expectedActiveVersionId = normalizeExpectedActiveVersionInput(); + if (expectedActiveVersionId !== undefined) { + formData.append("expectedActiveVersionId", expectedActiveVersionId); + } if (ingestScopeMode.value === "custom" && ingestSourceId.value.trim()) { formData.append("sourceId", ingestSourceId.value.trim()); } @@ -414,6 +431,11 @@ ingestButton.addEventListener("click", async () => { formData.append("isZipFolder", "true"); formData.append("mode", ingestMode.value); formData.append("tags", splitTags(ingestTags.value).join(",")); + formData.append("activate", ingestActivate.value); + const expectedActiveVersionId = normalizeExpectedActiveVersionInput(); + if (expectedActiveVersionId !== undefined) { + formData.append("expectedActiveVersionId", expectedActiveVersionId); + } if (ingestScopeMode.value === "custom" && ingestSourceId.value.trim()) { formData.append("sourceId", ingestSourceId.value.trim()); } @@ -431,7 +453,9 @@ ingestButton.addEventListener("click", async () => { sourceType: ingestSourceType.value, sourceRef: ingestSourceRef.value, mode: ingestMode.value, - tags: splitTags(ingestTags.value) + tags: splitTags(ingestTags.value), + activate: ingestActivate.value === "true", + expectedActiveVersionId: normalizeExpectedActiveVersionInput() === "null" ? null : normalizeExpectedActiveVersionInput() }); } diff --git a/public/playground/index.html b/public/playground/index.html index cac87fb..628fbcd 100644 --- a/public/playground/index.html +++ b/public/playground/index.html @@ -75,8 +75,17 @@ + + -

Si seleccionas un archivo local, el playground usara upload directo y podras aislarlo con un `sourceId` propio para no mezclarlo con otros scopes.

+

Si el ciclo de vida esta activo, una ingesta con activacion exige indicar la version activa esperada; usa null para la primera activacion de una fuente.

@@ -87,7 +96,7 @@

Limpieza controlada

-

Permite eliminar contexto ya ingerido del RAG antes de reingestar una fuente, para evitar duplicados y versiones mezcladas.

+

Esta operacion queda como compatibilidad legacy. Con ciclo de vida, usa la purga administrativa de versiones no activas.