packages/workflow/engine.mjs
const requiredByMarket={IN:['fullName','dateOfBirth','taxId','consent'],US:['fullName','dateOfBirth','taxId','consent']};
const allowedMarkets=new Set(Object.keys(requiredByMarket));
const copy=value=>JSON.parse(JSON.stringify(value));
export class MemoryCoreAdapter {
#records=new Map();
write(caseId,payload,idempotencyKey){
const prior=this.#records.get(idempotencyKey);
if(prior) return {created:false,record:copy(prior)};
const record={coreRecordId:`core-${caseId}`,caseId,payload:copy(payload)};
this.#records.set(idempotencyKey,record);
return {created:true,record:copy(record)};
}
count(){return this.#records.size}
}
export function createCase(input){
if(!allowedMarkets.has(input.market)) throw new Error(`Unsupported market: ${input.market}`);
if(!input.id||!input.channel) throw new Error('id and channel are required');
return {id:input.id,market:input.market,channel:input.channel,status:'captured',facts:{},exceptions:[],audit:[event('case.captured','system',{channel:input.channel})],coreWrite:null,nextAction:null};
}
export function captureConsent(state,{granted,sourceRef}){
const next=copy(state);
next.facts.consent=fact(Boolean(granted),sourceRef,1);
next.status=granted?'consented':'blocked';
if(!granted) next.exceptions=upsert(next.exceptions,{code:'MISSING_CONSENT',owner:'banker',reason:'Consent is required before interaction data can be used.'});
next.audit.push(event(granted?'consent.granted':'consent.denied','customer',{sourceRef}));
return next;
}
export function addFacts(state,facts){
const next=copy(state);
if(next.facts.consent?.value!==true) return block(next,'MISSING_CONSENT','banker','Consent is required before facts can be captured.');
for(const [key,item] of Object.entries(facts)){
if(!item.sourceRef) throw new Error(`${key} requires sourceRef`);
const confidence=Number(item.confidence);
if(!Number.isFinite(confidence)||confidence<0||confidence>1) throw new Error(`${key} confidence must be between 0 and 1`);
next.facts[key]=fact(item.value,item.sourceRef,confidence);
}
next.status='facts_captured';
next.audit.push(event('facts.captured','assistant',{fields:Object.keys(facts)}));
return next;
}
export function evaluate(state){
let next=copy(state); next.exceptions=[];
if(next.facts.consent?.value!==true) return block(next,'MISSING_CONSENT','banker','Consent is required before evaluation.');
for(const field of requiredByMarket[next.market]) if(next.facts[field]?.value===undefined) next=block(next,'MISSING_FIELD','banker',`${field} is required.`);
for(const [field,item] of Object.entries(next.facts)) if(item.confidence<0.8) next=block(next,'LOW_CONFIDENCE','reviewer',`${field} requires review.`,{field,confidence:item.confidence});
const prohibited=['race','religion','gender'];
for(const field of prohibited) if(next.facts[field]!==undefined) next=block(next,'PROHIBITED_FEATURE','compliance',`${field} cannot be used for eligibility.`,{field});
const kyc=next.facts.kycOutcome?.value;
if(!kyc) next=block(next,'MISSING_KYC_OUTCOME','reviewer','A cited KYC outcome is required.');
else if(kyc==='review') next=block(next,'KYC_MANUAL_REVIEW','reviewer','KYC provider returned manual review.');
else if(kyc==='decline') next=block(next,'KYC_DECLINE','compliance','KYC provider returned decline.');
else if(kyc!=='pass') next=block(next,'KYC_INDETERMINATE','reviewer','Unknown KYC outcome cannot proceed.');
if(next.facts.suspicious?.value===true) next=block(next,'DOWNSTREAM_RISK_REVIEW','compliance','KYC passed, but downstream risk control requires review.');
next.status=next.exceptions.length?'needs_review':'ready_to_complete';
next.audit.push(event('policy.evaluated','policy-engine',{market:next.market,outcome:next.status,exceptionCodes:next.exceptions.map(x=>x.code)}));
return next;
}
export function complete(state,adapter){
let next=evaluate(state);
if(next.status!=='ready_to_complete') return next;
const payload=Object.fromEntries(Object.entries(next.facts).filter(([key])=>!['consent','race','religion','gender'].includes(key)).map(([key,item])=>[key,item.value]));
const write=adapter.write(next.id,payload,`onboarding:${next.id}`);
next.coreWrite=write.record;
next.status='completed';
next.nextAction=recommend(next);
next.audit.push(event('core.write','system',{created:write.created,recordId:write.record.coreRecordId,idempotencyKey:`onboarding:${next.id}`}));
if(next.nextAction) next.audit.push(event('next_action.recommended','assistant',next.nextAction));
return next;
}
export function runScenario(scenario,adapter=new MemoryCoreAdapter()){
let state=createCase(scenario);
state=captureConsent(state,{granted:scenario.consent,sourceRef:'interaction:consent'});
if(scenario.consent) state=addFacts(state,scenario.facts||{});
return complete(state,adapter);
}
function recommend(state){
const need=state.facts.statedNeed?.value;
if(need==='everyday_spend') return {code:'CARD_DISCOVERY',reason:'Customer stated a need for everyday spending during the interaction.',sourceRefs:[state.facts.statedNeed.sourceRef]};
if(need==='initial_funding') return {code:'FUND_ACCOUNT',reason:'The opened account requires an initial funding step.',sourceRefs:[state.facts.statedNeed.sourceRef]};
return null;
}
function fact(value,sourceRef,confidence){return {value,sourceRef,confidence}}
function block(state,code,owner,reason,details={}){state.exceptions=upsert(state.exceptions,{code,owner,reason,...details});state.status='blocked';state.audit.push(event('exception.raised','policy-engine',{code,owner,reason,...details}));return state}
function upsert(items,item){return [...items.filter(x=>!(x.code===item.code&&x.field===item.field)),item]}
function event(type,actor,details){return {sequence:null,type,actor,details}}
export function finalizeAudit(state){const next=copy(state);next.audit=next.audit.map((x,i)=>({...x,sequence:i+1}));return next}