services/api/src/modules/interpretation/deterministic-amounts.js — extracción determinista de montos con cota superior

Overview

Este módulo implementa la regla NLU-02A para reconocer montos con cota superior en texto natural. Su alcance cubre expresiones como “hasta 15 mil” o “máximo 3 mdp”, usando el texto original y las coincidencias del léxico como entrada. La lógica es puramente determinista y conserva evidencia exacta cuando una cifra no puede resolverse sin suponer algo .

El archivo trabaja con convención es-MX: usa coma de miles y punto decimal. También separa con claridad lo que puede resolverse de lo que debe quedar como unresolved; por ejemplo, números escritos, rangos, mínimos y cantidades sin cota quedan fuera de este corte y no se fuerzan a un valor inventado fileciteturn0file0turn0file6.

[!IMPORTANT] Este módulo no “adivina” montos. Cuando no puede resolver una expresión con evidencia suficiente, devuelve el fragmento exacto en unresolved .

How it works

La función pública recibe el texto original y las coincidencias de findLexiconMatches(text). Después busca coincidencias marcadas como budgetBound = max, lee el monto que sigue, valida formato, moneda y periodicidad, y decide entre un presupuesto resuelto o evidencia no resuelta fileciteturn0file3turn0file6.

flowchart TB
    A[findLexiconMatches output] --> B[extractUpperBoundBudgets]
    B --> C[readAmount]
    C --> D[skipWhitespace]
    C --> E[ambiguous dollar sign]
    C --> F[NUMBER regex]
    C --> G[magnitude match]
    C --> H[exactValue]
    C --> I[currency match]
    C --> J[periodicity match]
    C --> K[unexpectedFollowerEnd]
    K -->|valid| L[budget]
    K -->|invalid| M[unresolved]
    F -->|no number| M
    G -->|stacked or attached| M
    H -->|overflow or unsafe| M

El flujo interno usa regex con lastIndex para leer de forma incremental. Si encuentra un número válido, intenta asociarle una magnitud, una moneda y una periodicidad. Al final, unexpectedFollowerEnd valida que la palabra siguiente no convierta la cifra en algo que no sea un monto completo .

Key components

La pieza pública principal es extractUpperBoundBudgets. Esta función recorre las coincidencias léxicas y devuelve dos listas: budgets para montos resueltos y unresolved para evidencia no interpretable sin suposición. El resultado es consumido por deterministic-interpreter.js como parte de la extracción de amounts fileciteturn0file4turn0file6.

{
    "title": "extractUpperBoundBudgets",
    "content": "[badge:public] Funci\u00f3n que extrae montos con cota superior desde un texto y sus coincidencias l\u00e9xicas. Devuelve montos resueltos y fragmentos no resueltos.",
    "type": "function",
    "filePath": "services/api/src/modules/interpretation/deterministic-amounts.js",
    "badges": [
        "public"
    ]
}

Funciones internas

Estas funciones no se exportan, pero sostienen toda la lógica del módulo.

  • facetsOf(match) — devuelve [] cuando la coincidencia es ambigua; en caso contrario, devuelve sus facetas.
  • facetValue(match, category) — obtiene el valor de la primera faceta que coincida con la categoría pedida.
  • hasFacet(match, category, value) — verifica si existe una faceta y, opcionalmente, si tiene un valor específico.
  • skipWhitespace(text, position) — avanza sobre espacios con un regex sticky.
  • stickyEnd(pattern, text, position) — intenta un match sticky y devuelve el final de la coincidencia o null.
  • matchStartingAt(matches, position, predicate) — busca la coincidencia que empieza exactamente en una posición.
  • exactValue(integerDigits, fractionDigits, multiplier) — calcula un entero exacto con BigInt y evita errores de coma flotante.
  • fragment(text, start, end) — construye un fragmento con span y matched.
  • unexpectedFollowerEnd(text, matches, position) — detecta palabras o unidades que invalidan un monto completo.
  • readAmount(text, matches, bound, periodicityMatches) — lee el monto que sigue a una cota superior y decide si produce budget o unresolved.

Data & fields

El módulo depende de la estructura de coincidencias del léxico y produce una estructura de salida muy concreta. La siguiente forma está respaldada por el comentario JSDoc de extractUpperBoundBudgets y por el cuerpo de la función .

[
    {
        "name": "text",
        "type": "string",
        "required": true,
        "description": "Texto original que se analiza."
    },
    {
        "name": "matches",
        "type": "Array<object>",
        "required": true,
        "description": "Coincidencias de findLexiconMatches(text), sin términos negados.",
        "children": [
            {
                "name": "span",
                "type": "[number, number]",
                "required": true,
                "description": "Rango UTF-16 de la coincidencia."
            },
            {
                "name": "matched",
                "type": "string",
                "required": true,
                "description": "Fragmento exacto del texto."
            },
            {
                "name": "entryId",
                "type": "string",
                "required": true,
                "description": "Identificador del término léxico."
            },
            {
                "name": "facets",
                "type": "Array<object>",
                "description": "Facetas léxicas usadas por la regla, como budgetBound, currency o periodicity."
            },
            {
                "name": "ambiguous",
                "type": "boolean",
                "description": "Marca coincidencias ambiguas."
            }
        ]
    }
]

La salida también tiene forma estructurada y conserva evidencia exacta.

[
    {
        "name": "budgets",
        "type": "Array<object>",
        "required": true,
        "description": "Montos resueltos con cota superior.",
        "children": [
            {
                "name": "value",
                "type": "number",
                "required": true,
                "description": "Valor numérico exacto del monto."
            },
            {
                "name": "span",
                "type": "[number, number]",
                "required": true,
                "description": "Rango UTF-16 del fragmento interpretado."
            },
            {
                "name": "matched",
                "type": "string",
                "required": true,
                "description": "Fragmento exacto interpretado."
            },
            {
                "name": "currency",
                "type": "object | null",
                "description": "Moneda explícita o inferida desde una magnitud.",
                "children": [
                    {
                        "name": "value",
                        "type": "string",
                        "required": true,
                        "description": "Código o valor de moneda."
                    },
                    {
                        "name": "span",
                        "type": "[number, number]",
                        "required": true,
                        "description": "Rango del fragmento monetario."
                    },
                    {
                        "name": "matched",
                        "type": "string",
                        "required": true,
                        "description": "Texto exacto de la moneda."
                    },
                    {
                        "name": "rule",
                        "type": "string",
                        "required": true,
                        "description": "Regla que originó la moneda."
                    }
                ]
            },
            {
                "name": "dollar",
                "type": "object | null",
                "description": "Fragmento de símbolo dólar conservado cuando no hubo moneda explícita.",
                "children": [
                    {
                        "name": "span",
                        "type": "[number, number]",
                        "required": true,
                        "description": "Rango del símbolo."
                    },
                    {
                        "name": "matched",
                        "type": "string",
                        "required": true,
                        "description": "Texto exacto del símbolo."
                    }
                ]
            },
            {
                "name": "periodicity",
                "type": "object | null",
                "description": "Periodicidad asociada al monto.",
                "children": [
                    {
                        "name": "value",
                        "type": "string",
                        "required": true,
                        "description": "Valor de periodicidad."
                    },
                    {
                        "name": "span",
                        "type": "[number, number]",
                        "required": true,
                        "description": "Rango del fragmento de periodicidad."
                    },
                    {
                        "name": "matched",
                        "type": "string",
                        "required": true,
                        "description": "Texto exacto de la periodicidad."
                    },
                    {
                        "name": "rule",
                        "type": "string",
                        "required": true,
                        "description": "Regla de léxico usada para la periodicidad."
                    }
                ]
            },
            {
                "name": "periodicityConflicts",
                "type": "Array<object>",
                "required": true,
                "description": "Fragmentos de periodicidades distintas cuando la entrada menciona más de una."
            }
        ]
    },
    {
        "name": "unresolved",
        "type": "Array<object>",
        "required": true,
        "description": "Evidencia exacta que no pudo resolverse sin suponer.",
        "children": [
            {
                "name": "span",
                "type": "[number, number]",
                "required": true,
                "description": "Rango UTF-16 de la evidencia."
            },
            {
                "name": "matched",
                "type": "string",
                "required": true,
                "description": "Fragmento exacto no resuelto."
            }
        ]
    }
]

Constantes internas relevantes

Estas constantes reflejan las reglas de lectura del módulo.

  • WHITESPACE [badge:read-only] — regex sticky para saltar espacios.
  • NUMBER [badge:read-only] — entero con separadores de miles opcionales y decimal opcional.
  • NUMERIC_RUN [badge:read-only] — captura una corrida numérica mal formada completa.
  • ATTACHED [badge:read-only] — detecta letras o dígitos pegados a un número.
  • NEXT_WORD [badge:read-only] — captura la siguiente palabra después del monto.
  • FOLLOWER_WORDS [badge:read-only] — conjunto de conectores permitidos después de un monto completo.

Behaviors

Estas son las responsabilidades funcionales observables en el archivo.

  • facetsOf(match) — devuelve las facetas del match, salvo cuando el match es ambiguo.
  • facetValue(match, category) — extrae el valor de la primera faceta que coincide.
  • hasFacet(match, category, value) — comprueba la presencia de una faceta y, opcionalmente, su valor.
  • skipWhitespace(text, position) — avanza la posición sobre espacios y devuelve el nuevo índice.
  • stickyEnd(pattern, text, position) — devuelve el final de una coincidencia sticky o null.
  • matchStartingAt(matches, position, predicate) — localiza una coincidencia que empieza en una posición exacta.
  • exactValue(integerDigits, fractionDigits, multiplier) — calcula un entero exacto y devuelve null si el resultado no es seguro.
  • fragment(text, start, end) — crea un fragmento de evidencia con span y texto exacto.
  • unexpectedFollowerEnd(text, matches, position) — detecta unidades no monetarias o palabras desconocidas después del monto.
  • readAmount(text, matches, bound, periodicityMatches) — interpreta un monto que sigue a una cota superior.
  • extractUpperBoundBudgets(text, matches) — recorre las cotas máximas y produce listas de montos resueltos y no resueltos.

La función readAmount es la parte central del módulo. Primero busca un número, luego valida separadores, magnitudes, moneda y periodicidad. Si detecta un caso ambiguo, devuelve unresolved con el fragmento exacto en vez de inferir un valor incorrecto .

Gotchas & edge cases

[!WARNING] El módulo solo resuelve montos con cota superior. Minimos, rangos, aproximaciones, cantidades sin límite y números escritos en palabras quedan fuera de este corte y se reportan como no resueltos fileciteturn0file0turn0file1.

[!WARNING] Una secuencia numérica mal formada, una magnitud apilada, o letras pegadas al número invalidan el monto. En esos casos, el módulo conserva la evidencia exacta en unresolved .

[!NOTE] Si aparece una moneda explícita después del monto, se incorpora en currency. Si hay conflicto entre monedas, el resultado deja de ser un presupuesto resuelto y se conserva como evidencia no resuelta .

[!CAUTION] Cuando el texto menciona periodicidades distintas, el módulo no elige una por orden. En su lugar, conserva los fragmentos en periodicityConflicts .

Los tests confirman varios casos resueltos y no resueltos. Por ejemplo, cubren enteros, separadores de miles, magnitudes como mdp, y fracciones exactas como 2.8 millones. También verifican que frases demasiado largas o con evidencia insuficiente se devuelvan como unresolved fileciteturn0file1turn0file8.

Este archivo se integra con componentes cercanos del subsistema de interpretación.

  • services/api/src/modules/interpretation/deterministic-interpreter.js — importa extractUpperBoundBudgets y lo usa al construir la interpretación determinista .
  • services/api/src/modules/interpretation/real-estate-lexicon.js — provee findLexiconMatches(text), que alimenta esta función desde el léxico.
  • services/api/test/interpretation-deterministic-units.test.js — verifica el comportamiento de extracción de montos con ejemplos concretos.
  • services/api/test/interpretation-deterministic.test.js — valida que la interpretación determinista preserve procedencia y contrato, incluyendo el uso de amounts.

[!NOTE] La relación con deterministic-interpreter.js es directa. Ese módulo consume la salida de esta función como parte de la interpretación NLU-02A .