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 fileciteturn0file0turn0file6.
[!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 fileciteturn0file3turn0file6.
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 fileciteturn0file4turn0file6.
{
"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 onull.matchStartingAt(matches, position, predicate)— busca la coincidencia que empieza exactamente en una posición.exactValue(integerDigits, fractionDigits, multiplier)— calcula un entero exacto conBigInty evita errores de coma flotante.fragment(text, start, end)— construye un fragmento conspanymatched.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 producebudgetounresolved.
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 onull.matchStartingAt(matches, position, predicate)— localiza una coincidencia que empieza en una posición exacta.exactValue(integerDigits, fractionDigits, multiplier)— calcula un entero exacto y devuelvenullsi 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 fileciteturn0file0turn0file1.
[!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 fileciteturn0file1turn0file8.
Related
Este archivo se integra con componentes cercanos del subsistema de interpretación.
services/api/src/modules/interpretation/deterministic-interpreter.js— importaextractUpperBoundBudgetsy lo usa al construir la interpretación determinista .services/api/src/modules/interpretation/real-estate-lexicon.js— proveefindLexiconMatches(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 deamounts.
[!NOTE] La relación con
deterministic-interpreter.jses directa. Ese módulo consume la salida de esta función como parte de la interpretación NLU-02A .