jsfe 1.28.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -134,7 +134,7 @@ export function defineProvider({ provides, contract, parameters = [], variables
134
134
  parameters: [...parameters, asked], variables: { ...variables, ...(gateParameters !== undefined ? { provider_gate_arguments: value(null) } : {}), asked: value(''), provides: value(provides) },
135
135
  steps: steps || [...before, ...(provides.default !== undefined ? [{ id: `${flow.id}_default`, type: 'SET', variable: 'asked', value: `asked || ${JSON.stringify(provides.default)}` }] : []),
136
136
  ...(gateParameters !== undefined ? [{ id: `${flow.id}_gate_arguments`, type: 'SET', variable: 'provider_gate_arguments', value: gateArgumentExpression(gateParameters) }] : []),
137
- { id: `${flow.id}_provide`, type: 'FLOW', value: 'provide-data', callType: 'call', parameters: { provides: '{{provides}}', asked: '{{asked}}', ...(gateParameters !== undefined ? { gate_params: '{{provider_gate_arguments}}' } : {}) } }] }, { role: 'provider', ...contract, catalog: 'provides' });
137
+ { id: `${flow.id}_provide`, type: 'FLOW', value: 'provide-data', callType: 'call', parameters: { provides: '{{provides}}', asked: '{{asked}}', reply: '{{userInput}}', ...(gateParameters !== undefined ? { gate_params: '{{provider_gate_arguments}}' } : {}) } }] }, { role: 'provider', ...contract, catalog: 'provides' });
138
138
  }
139
139
  export function defineForm({ contract, provides, ...spec }) {
140
140
  const flow = formFlow({ ...spec, ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DC;AACD;;;GAGG;AACH;;;;;;EAGC;AA4gBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAltBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAi5B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DC;AACD;;;GAGG;AACH;;;;;;EAGC;AA4gBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAltBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA26B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -776,11 +776,14 @@ export function finishProviderRequest(cargo, owner, provides, keys) {
776
776
  const provideData = {
777
777
  id: 'provide-data', name: 'ProvideData', version: '1.0.0', scope: 'isolated',
778
778
  description: "Worker: answer a question about a provider catalog. Resolves `asked` (a fact or set key; empty = ask which), passes the catalog's gate for non-public facts (gate.flow, called with gate_params; then every path in gate.verified must be truthy), loads the source (load-source), says the facts (answer-facts). Params: provides, asked.",
779
- parameters: [{ name: 'form', type: 'string', description: 'Originating FORM instance for task-owned deferred questions' }, { name: 'provides', type: 'object', description: 'The provider catalog' }, { name: 'asked', type: 'string', description: 'Fact or set key' }, { name: 'asked_keys', type: 'array', description: 'Independent requested facts or sets' }, { name: 'no_menu', type: 'string', description: "'true': answer only (a question asked inside a form)" }, { name: 'gate_params', type: 'object', description: 'Identifiers the caller already gave, passed to the gate flow as gate_params' }, { name: 'no_gate', type: 'string', description: "'true': the caller is inside a task on the verified account; the gate flow is not run (the verified paths are still checked)" }],
780
- variables: vars({ form: '', pd_request: null, pd_deferred: null, pd_ready: [], asked_keys: [], no_gate: '', pd_close: false, pd_who: '', no_menu: '', gate_params: null, pd_keys: [], pd_menu: [], pd_mkeys: [], pd_q: '', pd_q_es: '', pd_gated: false, pd_ok: false }),
779
+ parameters: [{ name: 'form', type: 'string', description: 'Originating FORM instance for task-owned deferred questions' }, { name: 'provides', type: 'object', description: 'The provider catalog' }, { name: 'asked', type: 'string', description: 'Fact or set key' }, { name: 'reply', type: 'string', description: 'The words that started the provider, this turn: tells a question back from its own menu from a later one' }, { name: 'asked_keys', type: 'array', description: 'Independent requested facts or sets' }, { name: 'no_menu', type: 'string', description: "'true': answer only (a question asked inside a form)" }, { name: 'gate_params', type: 'object', description: 'Identifiers the caller already gave, passed to the gate flow as gate_params' }, { name: 'no_gate', type: 'string', description: "'true': the caller is inside a task on the verified account; the gate flow is not run (the verified paths are still checked)" }],
780
+ variables: vars({ reply: '', pd_unanswered: false, form: '', pd_request: null, pd_deferred: null, pd_ready: [], asked_keys: [], no_gate: '', pd_close: false, pd_who: '', no_menu: '', gate_params: null, pd_keys: [], pd_menu: [], pd_mkeys: [], pd_q: '', pd_q_es: '', pd_gated: false, pd_ok: false }),
781
781
  steps: [
782
782
  WHEN('pd_no_catalog', "!provides || typeof provides !== 'object' || !Array.isArray(provides.facts)", END('pd_no_catalog_end', { outcome: 'unresolved', reason: 'provide_data_catalog', recovery: { version: 1, mode: 'none', reason: 'provide_data_catalog' } })),
783
783
  SET('pd_resolve', 'pd_keys', "(function () { var requested=Array.isArray(asked_keys)&&asked_keys.length?asked_keys:[typeof asked!=='undefined'?String(asked||''):''];return requested.reduce(function(keys,a){var set=(provides.sets||[]).filter(function(x){return x.key===a;})[0];var facts=set?set.facts:provides.facts.some(function(f){return f.key===a;})?[a]:[];return keys.concat(facts.filter(function(key){return keys.indexOf(key)<0;}));},[]); })()"),
784
+ // Back from its own menu with nothing it can answer: the ranker found no fact, or intent detection named a catch-all (unanswered.asked).
785
+ SET('pd_unanswered_check', 'pd_unanswered', "(function () { var u = provides.menu && provides.menu.unanswered, m = cargo.providerAsked; delete cargo.providerAsked; var same = function (x) { return String(x == null ? '' : x).replace(/\\\\/g, '').trim(); }; return !!u && !!m && m.subject === String(provides.subject || '') && typeof reply !== 'undefined' && same(m.input) === same(reply) && (m.ranked === true || (u.asked || []).indexOf(typeof asked !== 'undefined' ? String(asked || '') : '') > -1); })()"),
786
+ WHEN('pd_unanswered_go', 'pd_unanswered', FLOW('pd_unanswered_flow', 'provider-unanswered', 'replace', { provides: '{{provides}}' })),
784
787
  // Nothing specific asked: offer the sets and facts as a menu (keys 1..n, never 8; at most 7 options shown).
785
788
  SET('pd_menu_items', 'pd_menu', "(provides.sets || []).concat(provides.facts).slice(0, 7)"),
786
789
  SET('pd_menu_keys', 'pd_mkeys', `${KEYS_JS}(pd_menu.length)`),
@@ -825,6 +828,26 @@ const provideQueuedData = {
825
828
  provideData.variables.pd_menu_choices = { type: 'array', value: [] };
826
829
  provideData.steps.splice(provideData.steps.findIndex((s) => s.id === 'pd_menu_q'), 0, SET('pd_menu_build', 'pd_menu_choices', "pd_menu.map(function (m, i) { return { key: m.key, phrases: [pd_mkeys[i], m.label || m.key, m.label_es || ''].filter(Boolean) }; })"));
827
830
  // provider-menu: what next, after an answer — the catalog's actions, another fact, or done (owner MENU-1016 shape)
831
+ // The menu's own question came back to its provider and no fact answers it (owner 2026-11-02, prod-shaped "por qué subió mi saldo",
832
+ // which re-read the summary): say so, offer the action the catalog names (provides.menu.unanswered), and otherwise return to the menu.
833
+ const providerUnanswered = {
834
+ id: 'provider-unanswered', name: 'ProviderUnanswered', version: '1.0.0', scope: 'isolated',
835
+ description: "Worker: a question the provider has no fact for, sent back to it from its own menu: says provides.menu.unanswered.question(_es) and offers that menu's choice unanswered.choice. YES runs it, NO returns to the menu, anything else is DISPATCHed. Param: provides.",
836
+ parameters: [{ name: 'provides', type: 'object', description: 'The provider catalog' }],
837
+ variables: vars({ pu_choice: null, pu_yes: false, pu_no: false, pu_flow: '' }),
838
+ steps: [
839
+ SET('pu_find', 'pu_choice', "(provides.menu.choices || []).filter(function (c) { return c.key === provides.menu.unanswered.choice; })[0] || null"),
840
+ FLOW('pu_ask', 'ask-choice', 'call', { question: '{{provides.menu.unanswered.question}}', question_es: '{{provides.menu.unanswered.question_es}}', choices: [{ key: 'yes', phrases: ['1'] }, { key: 'no', phrases: ['2', 'no', 'no thanks', 'no thank you', 'not now', 'no gracias', 'ahora no'] }], accept_other: true, digits: { min: 1, max: 1 } }),
841
+ SET('pu_read_yes', 'pu_yes', "String(cargo.choice) === 'yes' || isAnswer(cargo.choiceText, 'yes')"),
842
+ SET('pu_read_no', 'pu_no', "!pu_yes && String(cargo.choice) === 'no'"),
843
+ SET('pu_take', 'pu_flow', "pu_yes && pu_choice ? (function (c) { if (c.set) { Object.keys(c.set).forEach(function (k) { cargo[k] = c.set[k]; }); } else { /* nothing to set */ } return c.flow; })(pu_choice) : ''"),
844
+ { id: 'pu_route', type: 'CASE', branches: {
845
+ 'condition: pu_flow': FLOW('pu_act', '{{pu_flow}}', 'replace'),
846
+ 'condition: pu_no': FLOW('pu_menu', 'provider-menu', 'replace', { provides: '{{provides}}' }),
847
+ default: { id: 'pu_elsewhere', type: 'DISPATCH' }
848
+ } },
849
+ ],
850
+ };
828
851
  const providerMenu = {
829
852
  id: 'provider-menu', name: 'ProviderMenu', version: '1.1.0', scope: 'isolated',
830
853
  description: "Worker: after a provider answered, offer its menu (provides.menu: question(_voice)(_es) with {verb} / {name}; choices [{ key, phrases, flow, exact, set }]; done { phrases, say(_es) }). A fact or set named by its words is answered and the menu comes back (menu.whole_phrases: only when the phrase is the whole reply; a phrase inside a sentence goes to the reply ranker, whose no-pick is the host's); an action runs its flow (replace) after setting its `set` cargo fields; done says goodbye. Other phrases: DISPATCH. No phrases: asked once more, then 'anything else?'. Params: provides, retry.",
@@ -841,6 +864,8 @@ const providerMenu = {
841
864
  // existing reserved done fallback retain their normal behavior.
842
865
  SET('pm_take', 'pm_key', "(function(){var key=String(cargo.choice || '');if(key.indexOf('fact:')===0)return pickProviderQuestion(cargo.choiceText,pm_choices.filter(function(c){return c.key.indexOf('fact:')===0;}));if(key.indexOf('act:')===0 && provides.menu.done && pickChoice(cargo.choiceText,[{key:'terminal:done',phrases:provides.menu.done.phrases ?? provides.menu.done.words,exact:true}],true)==='terminal:done')return 'done';return key;})()"),
843
866
  SET('pm_contained_check', 'pm_contained', "provides.menu.whole_phrases === true && pm_key.indexOf('fact:') === 0 && pickChoice(cargo.choiceText, pm_choices.filter(function (c) { return c.key === pm_key; }).map(function (c) { return { key: c.key, phrases: c.phrases, exact: true }; }), false) !== pm_key"),
867
+ // Who asked: provide-data knows the reply came back from this menu (same words, this turn), and whether the ranker read it.
868
+ SIDE('pm_mark_reply', "(provides.menu.unanswered ? cargo.providerAsked = { subject: String(provides.subject || ''), input: String(cargo.choiceText || ''), ranked: pm_contained } : null, true)"),
844
869
  WHEN('pm_rank', 'pm_contained', FLOW('pm_read_reply', 'read-provider-reply', 'call', { question: '{{pm_q}}', question_es: '{{pm_q_es}}', choices: '{{pm_choices}}' })),
845
870
  SET('pm_ranked_also', 'pm_also', "pm_contained ? ((cargo.providerReply || {}).also || []) : []"),
846
871
  SET('pm_ranked_key', 'pm_key', "pm_contained ? String((cargo.providerReply || {}).key || '') : pm_key"),
@@ -935,7 +960,7 @@ const resumeForm = { id: 'resume-form', name: 'ResumeForm', version: '1.0.0', sc
935
960
  SET('resume_definition', 'target', 'cargo.forms[form].definition'), SET('resume_parent', 'parent', "cargo.forms[form].parent || ''"), SET('resume_slot', 'slot', "cargo.forms[form].result_input || ''"),
936
961
  FLOW('resume_owned_form', '{{target}}', 'replace', { form_instance: '{{form}}', parent_form: '{{parent}}', result_input: '{{slot}}' })
937
962
  ] };
938
- const LIBRARY_FLOWS = [readProviderReply, readFormReply, readFormChoice, readConfirmReply, confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, requestCancelled, readFormResponse, collectContextualAnswer, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
963
+ const LIBRARY_FLOWS = [providerUnanswered, readProviderReply, readFormReply, readFormChoice, readConfirmReply, confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, requestCancelled, readFormResponse, collectContextualAnswer, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
939
964
  const LIBRARY_CONTRACTS = {
940
965
  'confirm-input-proposal': { role: 'collector', requires: [INPUT_PROPOSALS_REQUIREMENT] }, 'confirm-input-proposal-declined': { role: 'worker', requires: [INPUT_PROPOSALS_REQUIREMENT] },
941
966
  'resume-form': { role: 'worker' },
@@ -949,7 +974,7 @@ const LIBRARY_CONTRACTS = {
949
974
  'validate-input-batch': { role: 'worker', result: { path: 'cargo.inputBatch', type: 'object' } },
950
975
  'collect-input-accept': { role: 'worker' }, 'input-accepted-effects': { role: 'worker' },
951
976
  'disambiguate-input': { role: 'worker' }, 'confirm-form': { role: 'worker' }, 'form-done': { role: 'worker' },
952
- 'provider-menu': { role: 'worker' }, 'provide-data': { role: 'worker' }, 'provide-queued-data': { role: 'worker' },
977
+ 'provider-menu': { role: 'worker' }, 'provider-unanswered': { role: 'worker' }, 'provide-data': { role: 'worker' }, 'provide-queued-data': { role: 'worker' },
953
978
  'load-source': { role: 'worker' }, 'answer-facts': { role: 'worker' },
954
979
  'validate-card-number': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
955
980
  'validate-card-expiration': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:80c880b71fd6c48f39f31a723104940b23f2a66e4cab17b95ebb48d334f7b54f";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:62d3efc670fd676fec2fb6fa94c06f160fd8f1e3a16881f38cae0d5af6419509";
@@ -278,7 +278,7 @@ does this reply do with the question the flow asked? The model picks from a clos
278
278
 
279
279
  **Word readers and the ranker.** An input with `profile.whole_phrases` reads a choice from words only when the whole reply is accounted for: its phrase, another input's argument, courtesy, connecting words and the input's `template_words`. A reply with any other word goes to the ranker, so a word inside a sentence ("Quiero hacer una nueva compra" at a pay menu) never picks an option. Compound answers that carry arguments stay with the reader ("pay $50 with my Visa card"), because a picked option extracts no arguments. A proposal's confirmation offers the ranker its input's choices, so a correction ("yes, it was stolen") picks the changed candidate.
280
280
 
281
- **A provider's menu.** `provides.menu.whole_phrases: true` (default off) applies the same rule to a menu: a fact's phrase answers only as the whole reply, and a phrase inside a longer sentence goes to the reply ranker, offered every fact with phrases, every action, `done` and a person. A fact pick is answered with every other fact that cleared `REPLY_ALSO_CUT` (0.6), so one sentence that asks two things hears both; an action pick runs alone. `answers` and `irrelevant` fall to the menu's plain DISPATCH, which hands the turn to the host without intent detection (a Guru answers from the user context the flow set); `new_request` runs intent detection. A reply with no phrase is unchanged.
281
+ **A provider's menu.** `provides.menu.whole_phrases: true` (default off) applies the same rule to a menu: a fact's phrase answers only as the whole reply, and a phrase inside a longer sentence goes to the reply ranker, offered every fact with phrases, every action, `done` and a person. A fact pick is answered with every other fact that cleared `REPLY_ALSO_CUT` (0.6), so one sentence that asks two things hears both; an action pick runs alone. `answers` and `irrelevant` fall to the menu's plain DISPATCH, which hands the turn to the host without intent detection (a Guru answers from the user context the flow set); `new_request` runs intent detection. A reply with no phrase is unchanged. `menu.unanswered` handles a question the provider cannot answer: when a reply from its own menu comes back to it through intent detection with no fact to say, the caller hears the declared apology and offer (`provider-unanswered`) instead of the same facts again.
282
282
 
283
283
  Some options are **never offered**, so the ranker cannot pick them: the YES of a firm confirmation or of a form's
284
284
  confirmation, a `current_only` choice while another question is asked, and text. `exact` on a choice says how words
@@ -301,7 +301,7 @@ A fact has unique `key`, `description`, optional `phrases`, and a `path` within
301
301
 
302
302
  Supported simple `format` names include `money`, `money_nonzero`, `date`, `date_no_year`, `last4`, `digits`. An object format maps result properties to format names. List format uses `{type:"list",item,item_es?,fields?,join?,join_es?}` with `{items}` / `{count}` in the outer template; optional `say_one` / `say_one_es` handles one item. Templates use `{value}`, named result properties and supplied `{name}` / `{label}`. Bracketed optional segments can provide alternatives separated by `|`.
303
303
 
304
- `menu` supports `question`, `question_es`, optional voice variants, and `choices:[{key,phrases,exact?,flow?,set?}]`. `set` applies declared cargo assignments before an action route. `menu.whole_phrases: true` (off by default): a fact's phrase answers only as the whole reply; a phrase inside a longer sentence goes to the reply ranker, offered every fact, action and `done` (each by its `label` or a fact's `description`, else its key and phrases) and a person. A pick is taken, and every other fact the reply clears at 0.6 is answered with it. A reply that picks nothing goes to the host, without intent detection, so the Guru answers from the account in its user context; `new_request` goes to intent detection. A reply with no phrase is a plain DISPATCH, as before. `done` supports `phrases`, `say`, `say_es`. Keep whole-answer terminal phrases such as “no thanks” separate from a neutral “no” that offers further help. Facts take priority; a declared terminal answer must not be shortened into a different action. Test following-turn choices and facts, not just direct `asked` activations.
304
+ `menu` supports `question`, `question_es`, optional voice variants, and `choices:[{key,phrases,exact?,flow?,set?}]`. `set` applies declared cargo assignments before an action route. `menu.unanswered: {asked:[catch-all keys],choice,question,question_es}` (off by default): a reply from this menu that intent detection sends back to this provider, when the ranker found no fact or `asked` is a listed catch-all, gets that question instead of the facts again; YES runs the named menu choice, NO returns to the menu, anything else is DISPATCHed. `menu.whole_phrases: true` (off by default): a fact's phrase answers only as the whole reply; a phrase inside a longer sentence goes to the reply ranker, offered every fact, action and `done` (each by its `label` or a fact's `description`, else its key and phrases) and a person. A pick is taken, and every other fact the reply clears at 0.6 is answered with it. A reply that picks nothing goes to the host, without intent detection, so the Guru answers from the account in its user context; `new_request` goes to intent detection. A reply with no phrase is a plain DISPATCH, as before. `done` supports `phrases`, `say`, `say_es`. Keep whole-answer terminal phrases such as “no thanks” separate from a neutral “no” that offers further help. Facts take priority; a declared terminal answer must not be shortened into a different action. Test following-turn choices and facts, not just direct `asked` activations.
305
305
 
306
306
  The shared `provide-data`, `load-source`, `answer-facts` and provider-menu workers gate, load and answer only the requested facts. Top-level `gateParameters` may pass literal data or whole-expression templates to the gate. Refresh clears old results before loading and records freshness only on success. `no_gate` may avoid rerunning a gate only while all declared verification paths still hold. It cannot grant access. Mid-form provider questions preserve the owner and pending verification instead of stacking repeated authentication tasks.
307
307
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
5
5
  "license": "MIT",
6
6
  "author": "Ron Pinkas",