Reemplaza el dispatcher en turnEngineV3.js por un statechart formal en
XState v5. La machine es pura: produce un effect log (pending_actions) +
un descriptor de reply (pending_reply) que el runner traduce afuera.
API externa intacta: runTurnV3 sigue retornando { plan, decision } con
shape compatible con pipeline.js. Snapshot persiste en
context.xstate_snapshot dentro del JSONB existente.
- machine/index.js: statechart top-level (idle/cart/shipping/payment/
waiting/awaiting_human) + cart sub-statechart con todo el flujo
multi-turno (searching/resolving/askingClarification/askingQuantity/
computingFromPersonas/added/showing/pricing/researching).
- guards.js: portados de fsm.js (hasCart, wantsToAddProduct, etc).
- actions.js: assigns para mutations + reply descriptors (pending_reply
con templateKey/vars/rawText). Las async no entran en la machine.
- actors.js: fromPromise wrappers de retrieveCandidates y getProductQtyRules.
- runner.js: boot con prev_context.xstate_snapshot o migrateOldContext.
NLU → nluToEvent → send → settle (espera invokes) → realizeReply
(renderReply real con rewriter) → getPersistedSnapshot → format.
- nluToEvent.js: adapter NLU intent → evento XState (1:1).
Feature flags: USE_XSTATE=1 reemplaza el path; XSTATE_SHADOW=1 corre
ambos en paralelo, devuelve legacy y loguea diffs estructurales para
validar antes de flippar prod.
16 unit tests para la machine cubren: arranque, regla universal cart-on-add,
flow de cart con strong/multi match, checkout completo (shipping/pickup/
payment/cash) y rehidratación de snapshot. 224 tests totales pasando.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
46 lines
1.4 KiB
JavaScript
46 lines
1.4 KiB
JavaScript
/**
|
|
* Actores XState (fromPromise) — wrappers de side effects async.
|
|
* La machine es pura; estos actores aíslan llamadas a DB / WooCommerce / LLM.
|
|
*/
|
|
|
|
import { fromPromise } from "xstate";
|
|
import { retrieveCandidates } from "../catalogRetrieval.js";
|
|
import { getProductQtyRules } from "../../0-ui/db/repo.js";
|
|
|
|
/**
|
|
* Busca candidatos para una lista de queries de producto.
|
|
* Input: { tenantId, items: [{product_query, quantity, unit}, ...] }
|
|
* Output: array paralelo a items con { query, quantity, unit, candidates }
|
|
*/
|
|
export const searchCatalogActor = fromPromise(async ({ input }) => {
|
|
const { tenantId, items = [] } = input || {};
|
|
const results = [];
|
|
for (const it of items) {
|
|
if (!it?.product_query) continue;
|
|
const r = await retrieveCandidates({ tenantId, query: it.product_query, limit: 20 });
|
|
results.push({
|
|
query: it.product_query,
|
|
quantity: it.quantity ?? null,
|
|
unit: it.unit ?? null,
|
|
candidates: r?.candidates || [],
|
|
});
|
|
}
|
|
return results;
|
|
});
|
|
|
|
/**
|
|
* Lookup de qty rules para un producto.
|
|
* Input: { tenantId, wooProductId }
|
|
* Output: array de rules
|
|
*/
|
|
export const getQtyRulesActor = fromPromise(async ({ input }) => {
|
|
const { tenantId, wooProductId } = input || {};
|
|
if (!tenantId || !wooProductId) return [];
|
|
return await getProductQtyRules({ tenantId, wooProductId });
|
|
});
|
|
|
|
export const actors = {
|
|
searchCatalogActor,
|
|
getQtyRulesActor,
|
|
};
|