services/api/src/modules/reconciliation/deletion-inventory.classify.js

Overview

Este módulo implementa la clasificación pura del inventario de eliminaciones de Requirements. Recibe evidencia ya reunida por deletion-inventory.service.js y devuelve decisiones deterministas y explicables, sin acceder a MongoDB, ImageKit ni otros servicios .

Su papel es separar la lectura de evidencia de la decisión de clasificación. Las pruebas del inventario confirman que este flujo permanece de solo lectura, sin escrituras, sin $merge ni $out, y sin imports prohibidos . Las pruebas unitarias también verifican que authorizesExecution es siempre false fileciteturn0file6turn0file8turn0file11turn0file12.

[!IMPORTANT] Ninguna decisión de este módulo autoriza ejecución. AUTO_SAFE solo significa que una recuperación futura podría ser segura; no habilita la acción hoy .

How it works

flowchart TD
  A[Evidence already gathered] --> B[Select classifier]
  B --> C[classifyPendingRequirement]
  B --> D[classifyMediaClaim]
  B --> E[classifyConversationReference]
  B --> F[classifySharedFile]

  C --> G[Decision object]
  D --> G
  E --> G
  F --> G

  G --> H[requiresApproval from suggestedAction]
  G --> I[authorizesExecution false]

El módulo sigue un patrón estable. Cada clasificador valida la evidencia, detecta ausencias o contradicciones, y devuelve un objeto uniforme. Ese objeto siempre incluye una clasificación, un código de motivo y una acción sugerida, pero nunca habilita escritura fileciteturn0file0turn0file3.

[!NOTE] deletion-inventory.service.js prepara la evidencia por secciones y llama a estas funciones puras para decidir el resultado final de cada candidato .

Key components

{
    "title": "CLASSIFICATIONS",
    "content": "Objeto congelado con los estados AUTO_SAFE, VERIFY_FIRST, MANUAL_ONLY, NO_ACTION y UNRESOLVED.",
    "type": "enum",
    "filePath": "services/api/src/modules/reconciliation/deletion-inventory.classify.js",
    "badges": [
        "public"
    ]
}
{
    "title": "CLAIM_STATES",
    "content": "Lista congelada con los estados permitidos para una MediaFileClaim: active, deleting, erasing y deleted.",
    "type": "enum",
    "filePath": "services/api/src/modules/reconciliation/deletion-inventory.classify.js",
    "badges": [
        "public"
    ]
}
{
    "title": "classifyPendingRequirement",
    "content": "Clasifica Requirements con deletion.status = pending usando evidencia ya reunida. Devuelve una decisi\u00f3n explicable y no autoriza ejecuci\u00f3n.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/deletion-inventory.classify.js",
    "badges": [
        "public"
    ]
}
{
    "title": "classifyMediaClaim",
    "content": "Clasifica una asignaci\u00f3n MediaFileClaim seg\u00fan su estado, su holder y las referencias del Listing. Nunca autoriza una escritura.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/deletion-inventory.classify.js",
    "badges": [
        "public"
    ]
}
{
    "title": "classifyConversationReference",
    "content": "Clasifica una Conversation con activeRequirementId y decide si la referencia al Requirement es v\u00e1lida, pendiente de reparaci\u00f3n o requiere revisi\u00f3n humana.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/deletion-inventory.classify.js",
    "badges": [
        "public"
    ]
}
{
    "title": "classifySharedFile",
    "content": "Clasifica un archivo compartido por varios Listings. Solo informa sobre la evidencia observada; no prueba ausencia global de referencias.",
    "type": "function",
    "filePath": "services/api/src/modules/reconciliation/deletion-inventory.classify.js",
    "badges": [
        "public"
    ]
}

Data & fields

La salida de todas las funciones comparte la misma forma. La evidencia cambia según el clasificador, pero el contrato de respuesta es uniforme.

Resultado de decisión

[
    {
        "name": "classification",
        "type": "string",
        "required": true,
        "description": "Uno de AUTO_SAFE, VERIFY_FIRST, MANUAL_ONLY, NO_ACTION o UNRESOLVED."
    },
    {
        "name": "reasonCode",
        "type": "string",
        "required": true,
        "description": "Código estable y explicable que identifica la regla aplicada."
    },
    {
        "name": "suggestedAction",
        "type": "string",
        "required": true,
        "description": "Acción sugerida para el siguiente paso. No ejecuta cambios."
    },
    {
        "name": "requiresApproval",
        "type": "boolean",
        "required": true,
        "description": "True cuando suggestedAction no es none."
    },
    {
        "name": "authorizesExecution",
        "type": "boolean",
        "required": true,
        "description": "Siempre false en este módulo."
    }
]

CLASSIFICATIONS

  • AUTO_SAFE — Hay evidencia suficiente para considerar una recuperación automática futura.
  • VERIFY_FIRST — Falta una comprobación adicional antes de cualquier acción.
  • MANUAL_ONLY — Hace falta una decisión humana caso por caso.
  • NO_ACTION — El estado es coherente y no hay nada que recuperar.
  • UNRESOLVED — La evidencia es incompleta o la política aún no está decidida.

CLAIM_STATES

  • active
  • deleting
  • erasing
  • deleted

[!WARNING] erasing nunca puede producir AUTO_SAFE. El archivo todavía puede estar en una operación activa o inconclusa fileciteturn0file0turn0file12.

classifyPendingRequirement

[
    {
        "name": "stillPending",
        "type": "boolean",
        "required": true,
        "description": "Indica si el Requirement sigue pendiente al final del barrido."
    },
    {
        "name": "ownerExists",
        "type": "boolean",
        "required": true,
        "description": "Indica si el propietario persistido sigue existiendo."
    },
    {
        "name": "accountStatus",
        "type": "string",
        "required": true,
        "description": "Estado de la cuenta del propietario. Solo active permite continuar."
    },
    {
        "name": "conversationRefs",
        "type": "object",
        "required": true,
        "description": "Conteo de referencias a Conversations observadas en el inventario.",
        "children": [
            {
                "name": "own",
                "type": "number",
                "required": true,
                "description": "Referencias propias observadas."
            },
            {
                "name": "foreign",
                "type": "number",
                "required": true,
                "description": "Referencias de otros Listing observadas."
            }
        ]
    },
    {
        "name": "reservations",
        "type": "object",
        "required": true,
        "description": "Estado agregado de reservas asociadas al Requirement.",
        "children": [
            {
                "name": "total",
                "type": "number",
                "required": true,
                "description": "Total de reservas observadas."
            },
            {
                "name": "live",
                "type": "number",
                "required": true,
                "description": "Reservas todavía vigentes."
            }
        ]
    },
    {
        "name": "attachInProgress",
        "type": "object",
        "required": true,
        "description": "Estado agregado de asociaciones en curso.",
        "children": [
            {
                "name": "total",
                "type": "number",
                "required": true,
                "description": "Total de asociaciones observadas."
            },
            {
                "name": "live",
                "type": "number",
                "required": true,
                "description": "Asociaciones todavía vigentes."
            }
        ]
    },
    {
        "name": "fileClaimStates",
        "type": "array",
        "required": true,
        "description": "Estados observados de las MediaFileClaim asociadas."
    }
]

classifyMediaClaim

[
    {
        "name": "state",
        "type": "string",
        "required": true,
        "description": "Estado de la MediaFileClaim. Debe pertenecer a CLAIM_STATES."
    },
    {
        "name": "holderPresent",
        "type": "boolean",
        "required": true,
        "description": "Indica si el holder sigue presente."
    },
    {
        "name": "listing",
        "type": "object",
        "required": true,
        "description": "Evidencia del Listing al que pertenece la asignación.",
        "children": [
            {
                "name": "exists",
                "type": "boolean",
                "required": true,
                "description": "Indica si el Listing persiste."
            },
            {
                "name": "intent",
                "type": "string",
                "description": "Tipo de Listing observado. La rama erasing exige requirement."
            },
            {
                "name": "deletionPending",
                "type": "boolean",
                "description": "Indica si el Requirement asociado ya está pendiente de eliminación."
            },
            {
                "name": "referencesFile",
                "type": "boolean",
                "description": "Indica si el Listing aún referencia el archivo."
            },
            {
                "name": "fileIdentityInScope",
                "type": "boolean",
                "description": "Indica si la identidad persistida del archivo está dentro del ámbito esperado."
            }
        ]
    },
    {
        "name": "references",
        "type": "object",
        "required": true,
        "description": "Conteo de referencias al archivo.",
        "children": [
            {
                "name": "count",
                "type": "number",
                "required": true,
                "description": "Total de referencias observadas."
            },
            {
                "name": "includesOwnListing",
                "type": "boolean",
                "required": true,
                "description": "Indica si la referencia propia del Listing aparece en el conteo."
            }
        ]
    }
]

classifyConversationReference

[
    {
        "name": "target",
        "type": "object",
        "required": true,
        "description": "Destino de la referencia activeRequirementId.",
        "children": [
            {
                "name": "exists",
                "type": "boolean",
                "required": true,
                "description": "Indica si el Requirement objetivo sigue existiendo."
            },
            {
                "name": "intent",
                "type": "string",
                "description": "Tipo del objetivo observado."
            },
            {
                "name": "sameOwner",
                "type": "boolean",
                "description": "Indica si el objetivo pertenece al mismo propietario."
            },
            {
                "name": "deletionPending",
                "type": "boolean",
                "description": "Indica si el Requirement objetivo ya está pendiente de eliminación."
            }
        ]
    }
]

classifySharedFile

[
    {
        "name": "listingCount",
        "type": "number",
        "required": true,
        "description": "Cantidad de Listings observados que referencian el archivo."
    }
]

Behaviors

  • decision — Construye el objeto común de salida. Calcula requiresApproval a partir de suggestedAction y fija authorizesExecution en false.
  • missing — Detecta null o undefined y centraliza la validación de campos ausentes.
  • classifyPendingRequirement — Valida evidencia obligatoria, detecta cambios de estado durante el inventario, bloquea casos sin dueño, corta en cuentas no activas y distingue actividad directa de seguridad de reanudación.
  • classifyMediaClaim — Valida una asignación MediaFileClaim, cruza dos lecturas de evidencia y separa estados coherentes, huérfanos, contradictorios y activos.
  • classifyConversationReference — Clasifica referencias de Conversation hacia un Requirement según existencia, tipo, propiedad compartida y estado de eliminación.
  • classifySharedFile — Clasifica un archivo compartido observando solo el conteo disponible; no demuestra ausencia de otras referencias fuera del lote.

classifyPendingRequirement

Este clasificador trata los Requirements con deletion.status = "pending". Si la evidencia falta, devuelve UNRESOLVED con EVIDENCE_INCOMPLETE y sugiere rerun_inventory fileciteturn0file0turn0file3.

Las reglas visibles en el código son estas:

  • stillPending !== true → VERIFY_FIRST con STATE_CHANGED_DURING_INVENTORY.
  • ownerExists !== true → MANUAL_ONLY con OWNER_MISSING.
  • accountStatus ausente → UNRESOLVED.
  • accountStatus !== "active" → UNRESOLVED con D1_OWNER_NOT_ACTIVE.
  • fileClaimStates con valores desconocidos → UNRESOLVED con UNKNOWN_CLAIM_STATE.
  • fileClaimStates con erasing → VERIFY_FIRST con MEDIA_ERASING_IN_PROGRESS.
  • fileClaimStates con deleting → VERIFY_FIRST con MEDIA_DELETION_POSSIBLY_ACTIVE.
  • reservations.live > 0 o attachInProgress.live > 0 → VERIFY_FIRST con ASSOCIATION_POSSIBLY_ACTIVE.
  • Sin señales de actividad → AUTO_SAFE con RESUME_SAFE_EVEN_IF_ACTIVE.

Las pruebas confirman ese comportamiento. Verifican el caso AUTO_SAFE cuando no hay señales de actividad, VERIFY_FIRST cuando hay actividad posible, y UNRESOLVED ante evidencia incompleta fileciteturn0file6turn0file8turn0file11.

[!TIP] El comentario del archivo aclara que AUTO_SAFE no significa abandono. Solo significa que reanudar sería seguro incluso si el proceso original siguiera vivo .

classifyMediaClaim

Este clasificador analiza una MediaFileClaim con evidencia cruzada entre la propia asignación y el Listing asociado. Si la evidencia falta, devuelve UNRESOLVED con EVIDENCE_INCOMPLETE fileciteturn0file0turn0file3.

Las ramas principales son estas:

  • Estado desconocido → UNRESOLVED con UNKNOWN_CLAIM_STATE.
  • listing.referencesFile y references.includesOwnListing no coinciden → VERIFY_FIRST con CONTRADICTORY_EVIDENCE.
  • deleting o erasing sin holder → MANUAL_ONLY con HOLDER_MISSING.
  • active con holder presente → MANUAL_ONLY con UNEXPECTED_HOLDER.
  • deleted con referencias ajenas → MANUAL_ONLY con DANGLING_FILE_REFERENCE.
  • deleted sin la referencia propia → NO_ACTION con TOMBSTONE.
  • deleted con referencia propia y listing.deletionPending === true → NO_ACTION con COVERED_BY_REQUIREMENT_DELETION.
  • deleted con referencia propia pero sin pending → VERIFY_FIRST con DELETED_FILE_REFERENCED_BY_OWN_LISTING.
  • erasing con referencias ajenas → MANUAL_ONLY con ERASING_STILL_REFERENCED.
  • erasing sin listing.exists → MANUAL_ONLY con ERASING_LISTING_GONE.
  • erasing sin intent = requirement o sin deletionPending = true → MANUAL_ONLY con variantes de ERASING_OWN_LISTING_STILL_REFERENCES o ERASING_REMOVAL_LEFTOVER.
  • erasing sin fileIdentityInScope → MANUAL_ONLY con ERASING_IDENTITY_EVIDENCE_MISSING.
  • erasing con todo en orden → VERIFY_FIRST con ERASING_DURING_REQUIREMENT_DELETION.
  • deleting con deletionPending === true → NO_ACTION con COVERED_BY_REQUIREMENT_DELETION.
  • deleting sin pending → VERIFY_FIRST con DELETION_IN_PROGRESS_OR_STALLED o REMOVAL_LEFTOVER.
  • active con referencia al archivo → NO_ACTION con ATTACHED.
  • active sin referencia → VERIFY_FIRST con UNREFERENCED_ACTIVE_CLAIM.

Las pruebas cubren la prohibición total de AUTO_SAFE en erasing, la detección de evidencia contradictoria y el comportamiento por estado .

[!WARNING] El caso erasing nunca se clasifica como AUTO_SAFE. El clasificador siempre lo eleva a revisión o intervención humana fileciteturn0file0turn0file12.

classifyConversationReference

Este clasificador trata una Conversation con activeRequirementId. Si falta el destino o su bandera exists, devuelve UNRESOLVED con EVIDENCE_INCOMPLETE .

El comportamiento observado es este:

  • target.exists === false → MANUAL_ONLY con DANGLING_REQUIREMENT_REFERENCE.
  • target.intent ausente o target.sameOwner ausente → UNRESOLVED con EVIDENCE_INCOMPLETE.
  • target.intent !== "requirement" → MANUAL_ONLY con REFERENCES_NON_REQUIREMENT.
  • target.sameOwner === false → MANUAL_ONLY con CROSS_OWNER_REFERENCE.
  • target.deletionPending === true → NO_ACTION con COVERED_BY_REQUIREMENT_DELETION.
  • En otro caso → NO_ACTION con VALID_REFERENCE.

Las pruebas confirman los casos colgantes, cruzados y válidos .

classifySharedFile

Este clasificador es el más simple. Solo necesita listingCount. Si falta, devuelve UNRESOLVED con EVIDENCE_INCOMPLETE. Si está presente, devuelve NO_ACTION con SHARED_FILE fileciteturn0file0turn0file3.

[!NOTE] El comentario del código es explícito: esta función solo informa. No prueba ausencia de otras referencias fuera del lote observado .

Workflows

1. Reunir evidencia | `deletion-inventory.service.js` obtiene datos de Requirements, claims, Conversations y archivos compartidos.
2. Validar campos obligatorios | Cada clasificador rechaza la evidencia incompleta con `UNRESOLVED`.
3. Detectar contradicciones | Las lecturas inconsistentes se elevan a `VERIFY_FIRST`.
4. Aplicar la política fija | El clasificador devuelve una decisión explicable con una acción sugerida.
5. Bloquear ejecución automática | `authorizesExecution` permanece en `false` y `requiresApproval` depende de `suggestedAction`.

Este flujo encaja con el inventario completo. El servicio de inventario agrupa secciones, aplica paginación y emite un resumen final, mientras este archivo solo resuelve la decisión por candidato .

Gotchas & edge cases

[!WARNING] ageSeconds no decide nada por sí solo. El propio archivo aclara que la antigüedad es solo contexto y no una prueba de abandono fileciteturn0file0turn0file11.

[!CAUTION] Una evidencia incompleta nunca produce una clasificación segura. El resultado pasa a UNRESOLVED y sugiere repetir el inventario fileciteturn0file0turn0file3.

[!WARNING] Una evidencia contradictoria nunca se considera confiable. El resultado pasa a VERIFY_FIRST fileciteturn0file0turn0file12.

[!IMPORTANT] Ninguna clasificación de este módulo autoriza una escritura. La decisión puede sugerir pasos futuros, pero no ejecuta cambios .

services/api/src/modules/reconciliation/deletion-inventory.service.js es el consumidor directo de estas funciones. Allí se reúne la evidencia de solo lectura, se paginan los candidatos y se emiten registros de inventario con estas clasificaciones .

services/api/src/modules/reconciliation/pending-requirement-recovery.service.js consume el contexto de recuperación, pero revalida las precondiciones justo antes de actuar. El propio archivo indica que una clasificación previa del inventario, incluso AUTO_SAFE, no basta por sí sola .

services/api/test/deletion-inventory-classify.test.js cubre los caminos deterministas del clasificador puro. services/api/test/e6-1-inventory-static.test.js y services/api/integration/mongo/e6-1-deletion-inventory.mongo.js confirman el carácter de solo lectura del inventario completo fileciteturn0file5turn0file11turn0file12turn0file4turn0file7.