Files
botino/src/modules/3-turn-engine/machine/actors.js
Lucas Tettamanti 04ac33430f Tier 2: XState statechart como motor de turno (opt-in)
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>
2026-05-01 20:38:26 -03:00

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,
};