jsfe 1.28.0 → 1.30.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.
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +15 -1
- package/dist/forms/contracts.js +1 -1
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +31 -5
- package/dist/helpers/form-state.d.ts.map +1 -1
- package/dist/helpers/form-state.js +5 -1
- package/dist/helpers/match-inputs.d.ts +1 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +20 -3
- package/dist/runtime-identity.js +1 -1
- package/docs/architecture-guide.md +1 -1
- package/docs/copilot-reference.md +9 -4
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EA+RC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -410,7 +410,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
410
410
|
}
|
|
411
411
|
else { /* none */ }
|
|
412
412
|
if (flow.provides) {
|
|
413
|
-
out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), '', providerChart(flow.provides));
|
|
413
|
+
out.push('', '### Provider', '', ...(flow.provides.facts || []).map((f) => `- fact \`${f.key}\`: ${f.description || ''}${f.path ? ' (from `' + f.path + '`)' : ''}`), ...providerMenuNotes(flow.provides.menu), '', providerChart(flow.provides));
|
|
414
414
|
}
|
|
415
415
|
else { /* not a provider */ }
|
|
416
416
|
if (flow.form) {
|
|
@@ -441,6 +441,20 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
441
441
|
else { /* no steps */ }
|
|
442
442
|
return out.join('\n');
|
|
443
443
|
}
|
|
444
|
+
// What a provider's menu opted into: both change what the caller hears, and neither shows in the chart.
|
|
445
|
+
function providerMenuNotes(menu) {
|
|
446
|
+
const notes = [];
|
|
447
|
+
if (menu && menu.whole_phrases) {
|
|
448
|
+
notes.push('- menu `whole_phrases`: a fact\'s phrase inside a sentence goes to the reply ranker; a fact pick also answers every fact that cleared 0.6');
|
|
449
|
+
}
|
|
450
|
+
else { /* not opted in */ }
|
|
451
|
+
const u = menu && menu.unanswered;
|
|
452
|
+
if (u) {
|
|
453
|
+
notes.push(`- menu \`unanswered\`: a question with no fact, back from this menu${u.asked ? ' (asked: ' + u.asked.join(', ') + ')' : ''}, says "${u.question}"${u.question_es ? ' / "' + u.question_es + '"' : ''}; yes runs menu choice \`${u.choice}\`, no returns to the menu`);
|
|
454
|
+
}
|
|
455
|
+
else { /* not opted in */ }
|
|
456
|
+
return notes;
|
|
457
|
+
}
|
|
444
458
|
function upsert(file, title, generated, skeleton) {
|
|
445
459
|
const block = `${START}\n${generated}\n${END}`;
|
|
446
460
|
const old = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
|
package/dist/forms/contracts.js
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAsIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8DC;AACD;;;GAGG;AACH;;;;;;EAGC;AA4gBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAntBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA46B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -84,7 +84,8 @@ export const FIELD_PRESETS = {
|
|
|
84
84
|
card_expiration: { label: 'expiration date', label_es: 'fecha de vencimiento',
|
|
85
85
|
// MM0YY: a zero said between month and year ("020 28", prod 2026-10-25 14:10, was taken for the billing ZIP).
|
|
86
86
|
// Up to two words between month and year ("el mes 06 año 31", prod 2026-10-28 21:54, was asked again); a month by name.
|
|
87
|
-
|
|
87
|
+
// A spoken separator with the digits apart: "0 4 guion 3 1" (owner 2026-11-02; prod 2026-11-01 15:19 was asked again).
|
|
88
|
+
profile: { kind: 'pattern', regex: `\\b(0?[1-9]|1[0-2]|${MONTH_RE})\\.?(?:\\s*(?:/|-|del?|año|ano|year|of)(?=[\\s\\d])){0,2}\\s*(?:20)?(\\d{2})\\b|\\b0[1-9]0\\s+(?:20)?\\d{2}\\b|\\b(?:0\\s?[1-9]|1\\s?[0-2]|[1-9])\\s*(?:gui[oó]n|dash|hyphen|slash|barra|diagonal)\\s*(?:2\\s?0\\s?)?\\d\\s?\\d\\b` }, digits: { min: 3, max: 6 },
|
|
88
89
|
validator: 'validate-card-expiration',
|
|
89
90
|
question: 'What is the expiration date, month and year?', question_es: '¿Cuál es la fecha de vencimiento, mes y año?',
|
|
90
91
|
retry: 'Please say or enter the expiration as four digits, month then year, like 0 9 2 8.', retry_es: 'Por favor diga o ingrese el vencimiento en cuatro dígitos, mes y año, por ejemplo 0 9 2 8.',
|
|
@@ -776,11 +777,14 @@ export function finishProviderRequest(cargo, owner, provides, keys) {
|
|
|
776
777
|
const provideData = {
|
|
777
778
|
id: 'provide-data', name: 'ProvideData', version: '1.0.0', scope: 'isolated',
|
|
778
779
|
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 }),
|
|
780
|
+
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)" }],
|
|
781
|
+
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
782
|
steps: [
|
|
782
783
|
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
784
|
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;}));},[]); })()"),
|
|
785
|
+
// Back from its own menu with nothing it can answer: the ranker found no fact, or intent detection named a catch-all (unanswered.asked).
|
|
786
|
+
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); })()"),
|
|
787
|
+
WHEN('pd_unanswered_go', 'pd_unanswered', FLOW('pd_unanswered_flow', 'provider-unanswered', 'replace', { provides: '{{provides}}' })),
|
|
784
788
|
// Nothing specific asked: offer the sets and facts as a menu (keys 1..n, never 8; at most 7 options shown).
|
|
785
789
|
SET('pd_menu_items', 'pd_menu', "(provides.sets || []).concat(provides.facts).slice(0, 7)"),
|
|
786
790
|
SET('pd_menu_keys', 'pd_mkeys', `${KEYS_JS}(pd_menu.length)`),
|
|
@@ -825,6 +829,26 @@ const provideQueuedData = {
|
|
|
825
829
|
provideData.variables.pd_menu_choices = { type: 'array', value: [] };
|
|
826
830
|
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
831
|
// provider-menu: what next, after an answer — the catalog's actions, another fact, or done (owner MENU-1016 shape)
|
|
832
|
+
// 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",
|
|
833
|
+
// which re-read the summary): say so, offer the action the catalog names (provides.menu.unanswered), and otherwise return to the menu.
|
|
834
|
+
const providerUnanswered = {
|
|
835
|
+
id: 'provider-unanswered', name: 'ProviderUnanswered', version: '1.0.0', scope: 'isolated',
|
|
836
|
+
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.",
|
|
837
|
+
parameters: [{ name: 'provides', type: 'object', description: 'The provider catalog' }],
|
|
838
|
+
variables: vars({ pu_choice: null, pu_yes: false, pu_no: false, pu_flow: '' }),
|
|
839
|
+
steps: [
|
|
840
|
+
SET('pu_find', 'pu_choice', "(provides.menu.choices || []).filter(function (c) { return c.key === provides.menu.unanswered.choice; })[0] || null"),
|
|
841
|
+
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 } }),
|
|
842
|
+
SET('pu_read_yes', 'pu_yes', "String(cargo.choice) === 'yes' || isAnswer(cargo.choiceText, 'yes')"),
|
|
843
|
+
SET('pu_read_no', 'pu_no', "!pu_yes && String(cargo.choice) === 'no'"),
|
|
844
|
+
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) : ''"),
|
|
845
|
+
{ id: 'pu_route', type: 'CASE', branches: {
|
|
846
|
+
'condition: pu_flow': FLOW('pu_act', '{{pu_flow}}', 'replace'),
|
|
847
|
+
'condition: pu_no': FLOW('pu_menu', 'provider-menu', 'replace', { provides: '{{provides}}' }),
|
|
848
|
+
default: { id: 'pu_elsewhere', type: 'DISPATCH' }
|
|
849
|
+
} },
|
|
850
|
+
],
|
|
851
|
+
};
|
|
828
852
|
const providerMenu = {
|
|
829
853
|
id: 'provider-menu', name: 'ProviderMenu', version: '1.1.0', scope: 'isolated',
|
|
830
854
|
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 +865,8 @@ const providerMenu = {
|
|
|
841
865
|
// existing reserved done fallback retain their normal behavior.
|
|
842
866
|
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
867
|
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"),
|
|
868
|
+
// Who asked: provide-data knows the reply came back from this menu (same words, this turn), and whether the ranker read it.
|
|
869
|
+
SIDE('pm_mark_reply', "(provides.menu.unanswered ? cargo.providerAsked = { subject: String(provides.subject || ''), input: String(cargo.choiceText || ''), ranked: pm_contained } : null, true)"),
|
|
844
870
|
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
871
|
SET('pm_ranked_also', 'pm_also', "pm_contained ? ((cargo.providerReply || {}).also || []) : []"),
|
|
846
872
|
SET('pm_ranked_key', 'pm_key', "pm_contained ? String((cargo.providerReply || {}).key || '') : pm_key"),
|
|
@@ -935,7 +961,7 @@ const resumeForm = { id: 'resume-form', name: 'ResumeForm', version: '1.0.0', sc
|
|
|
935
961
|
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
962
|
FLOW('resume_owned_form', '{{target}}', 'replace', { form_instance: '{{form}}', parent_form: '{{parent}}', result_input: '{{slot}}' })
|
|
937
963
|
] };
|
|
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];
|
|
964
|
+
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
965
|
const LIBRARY_CONTRACTS = {
|
|
940
966
|
'confirm-input-proposal': { role: 'collector', requires: [INPUT_PROPOSALS_REQUIREMENT] }, 'confirm-input-proposal-declined': { role: 'worker', requires: [INPUT_PROPOSALS_REQUIREMENT] },
|
|
941
967
|
'resume-form': { role: 'worker' },
|
|
@@ -949,7 +975,7 @@ const LIBRARY_CONTRACTS = {
|
|
|
949
975
|
'validate-input-batch': { role: 'worker', result: { path: 'cargo.inputBatch', type: 'object' } },
|
|
950
976
|
'collect-input-accept': { role: 'worker' }, 'input-accepted-effects': { role: 'worker' },
|
|
951
977
|
'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' },
|
|
978
|
+
'provider-menu': { role: 'worker' }, 'provider-unanswered': { role: 'worker' }, 'provide-data': { role: 'worker' }, 'provide-queued-data': { role: 'worker' },
|
|
953
979
|
'load-source': { role: 'worker' }, 'answer-facts': { role: 'worker' },
|
|
954
980
|
'validate-card-number': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
|
|
955
981
|
'validate-card-expiration': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA8CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;
|
|
1
|
+
{"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA8CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAeC;AACD,uGAAuG;AACvG;;;;;;;;;;;;;;QAuCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AA7pBM,8CAAmE"}
|
|
@@ -219,7 +219,7 @@ export function enrichFormMatches(matches, chain, form, names = [], asked = '')
|
|
|
219
219
|
const enrich = h => {
|
|
220
220
|
const spec = chain.find(c => c.form === h.form && c.name === h.name) || {};
|
|
221
221
|
return { ...h, label: h.option_label || spec.label || h.name, label_es: h.option_label_es || spec.label_es || spec.label || h.name, ...(h.option_label ? { option: true } : {}),
|
|
222
|
-
validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), ...(spec.silent === true ? { silent: true } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
|
|
222
|
+
validator: spec.validator || '', ...(!(h.form === form && names.includes(h.name)) && spec.profile && !['choice', 'amount', 'text'].includes(spec.profile.kind) && /\d/.test(String(h.value)) ? { out_of_order_number: true } : {}), ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), ...(spec.silent === true ? { silent: true } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
|
|
223
223
|
};
|
|
224
224
|
return { ...matches, hits: matches.hits.map(enrich), ambiguous: matches.ambiguous.map(enrich), remaining: (matches.remaining || []).map(group => group.map(enrich)) };
|
|
225
225
|
}
|
|
@@ -276,6 +276,10 @@ export function expandFormActivations(cargo, hits = []) {
|
|
|
276
276
|
}
|
|
277
277
|
/** No partial activation: validation failure suppresses inferred changes; pending prerequisites retain the complete group. */
|
|
278
278
|
export function settleFormActivations(batch = {}, cargo = null) {
|
|
279
|
+
// A number read out of order is taken only when it is valid (owner 2026-11-02): one its validator rejects fills nothing and
|
|
280
|
+
// says nothing, and the current question is asked again.
|
|
281
|
+
const invalidNumbers = (batch.rejected || []).filter(h => h.out_of_order_number === true);
|
|
282
|
+
batch = { ...batch, rejected: (batch.rejected || []).filter(h => h.out_of_order_number !== true), ignored: [...(batch.ignored || []), ...invalidNumbers] };
|
|
279
283
|
const accepted = [...(batch.accepted || [])], rejected = [...(batch.rejected || [])], pending = [...(batch.pending || [])];
|
|
280
284
|
const all = accepted.concat(rejected, pending), keys = new Set(all.map(h => h.activation_group).filter(Boolean));
|
|
281
285
|
const held = new Set(), discarded = new Set();
|
|
@@ -22,6 +22,7 @@ export function readInputProfile(profile: any, answer: any, isCurrent?: boolean)
|
|
|
22
22
|
* 2. else the one input of the current form that fits takes it, and parent forms are not considered;
|
|
23
23
|
* 3. else the same test one form up, and so on;
|
|
24
24
|
* 4. two inputs at the nearest form that fits are ambiguous: asked, never ranked.
|
|
25
|
+
* Exception: a number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02).
|
|
25
26
|
* Exception: an input that declares `confirm_against: [names]` is asked about together with those inputs whenever both
|
|
26
27
|
* fit the same words, wherever scope would have put it ('62' at the payment offer: $62 or sub-account 62, prod
|
|
27
28
|
* 2026-10-19 07:38).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;
|
|
1
|
+
{"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAwFD;;;GAGG;AACH,sFA8GC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EA6QC"}
|
|
@@ -56,7 +56,8 @@ function amountMasked(raw) {
|
|
|
56
56
|
}
|
|
57
57
|
/** Source intervals let different readers recognize the same evidence without agreeing on its type. */
|
|
58
58
|
function numberSpans(raw, value, amount = false) {
|
|
59
|
-
|
|
59
|
+
// A field's number is located as its reader found it: DIGIT_RUN joins "31, 0 31". An unlocated number escaped every span rule.
|
|
60
|
+
const runs = [...raw.matchAll(amount ? /\d(?:[\s.,-]?\d)*/g : DIGIT_RUN)];
|
|
60
61
|
if (!amount) {
|
|
61
62
|
const wanted = String(value).replace(/\D/g, '');
|
|
62
63
|
return runs.filter(m => { const d = m[0].replace(/\D/g, ''); return d === wanted || d === '1' + wanted; })
|
|
@@ -271,6 +272,7 @@ export function readInputProfile(profile, answer, isCurrent = true) {
|
|
|
271
272
|
* 2. else the one input of the current form that fits takes it, and parent forms are not considered;
|
|
272
273
|
* 3. else the same test one form up, and so on;
|
|
273
274
|
* 4. two inputs at the nearest form that fits are ambiguous: asked, never ranked.
|
|
275
|
+
* Exception: a number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02).
|
|
274
276
|
* Exception: an input that declares `confirm_against: [names]` is asked about together with those inputs whenever both
|
|
275
277
|
* fit the same words, wherever scope would have put it ('62' at the payment offer: $62 or sub-account 62, prod
|
|
276
278
|
* 2026-10-19 07:38).
|
|
@@ -368,7 +370,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
368
370
|
// A declared confirm_against pair with the current question is relevant to it as well: the pair is asked about.
|
|
369
371
|
const pairedWithCurrent = (reading) => list.some((c) => isCur(c) && c.form === reading.form && ((c.confirm_against || []).includes(reading.name) || (fieldOf(reading).confirm_against || []).includes(c.name)));
|
|
370
372
|
const inOrder = (reading) => reading.current || invited(reading) || pairedWithCurrent(reading);
|
|
371
|
-
// Rule 3: a number read out of order
|
|
373
|
+
// Rule 3, the whole number: every digit run a number read out of order touches lies entirely inside it,
|
|
372
374
|
// so the last four of "88 91 7112" fill nothing. A written decimal is a run of its own ("51 72.45": 51, then 72.45).
|
|
373
375
|
const decimalRuns = [...raw.matchAll(DECIMAL_AMOUNT)].map((m) => [m.index, m.index + m[0].length]);
|
|
374
376
|
const undecimal = decimalRuns.reduce((text, [a, b]) => text.slice(0, a) + ' '.repeat(b - a) + text.slice(b), raw);
|
|
@@ -450,6 +452,20 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
450
452
|
}
|
|
451
453
|
// Rule 4: a single digit the current question offers as a key is that key (the keypad reading wins its group below);
|
|
452
454
|
// one it does not offer reaches the invited amount, whose own validation decides ("3 dollars?").
|
|
455
|
+
// Rule 3, a perfect match (owner 2026-11-02): a number read out of order accounts for every digit of the answer. "0 4 guion
|
|
456
|
+
// 31, 0 31" at the expiration is not a ZIP 31031 and "31st. 9 3 1." not a code 931. Several numbers are a compound answer only
|
|
457
|
+
// when each is read, and a free-text answer owns its own digits. An amount has rule 4, a choice its declared phrases.
|
|
458
|
+
const readsDigits = (reading) => !['choice', 'amount', 'text'].includes(fieldOf(reading).profile.kind) && /\d/.test(spanText(reading) + String(reading.value));
|
|
459
|
+
const outOfOrderDigits = readings.filter((reading) => reading.rule === 'out_of_order' && readsDigits(reading));
|
|
460
|
+
const unread = unclaimedSpans(raw, readings.filter((reading) => reading.unlocated !== true)).map(([a, b]) => raw.slice(a, b)).join(' ');
|
|
461
|
+
if (outOfOrderDigits.length && /\d/.test(unread)) {
|
|
462
|
+
for (const reading of outOfOrderDigits)
|
|
463
|
+
refused.push({ form: reading.form, name: reading.name, value: reading.value, rule: 'not_whole_answer', spans: reading.spans });
|
|
464
|
+
readings = readings.filter((reading) => !outOfOrderDigits.includes(reading));
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
// no number read out of order, or nothing of the answer's digits is left over
|
|
468
|
+
}
|
|
453
469
|
// profile.whole_phrases (a choice, or a pattern that reads words): a phrase picks only when the answer is accounted
|
|
454
470
|
// for: its phrases, the arguments read beside them (numbers, amounts, a card), courtesy or connecting words
|
|
455
471
|
// (isReaderFiller) and the field's own profile.template_words. A template such as "pay 50 with my Visa card" stays;
|
|
@@ -537,8 +553,9 @@ export function matchInputs(answer, candidates, current, choiceKey) {
|
|
|
537
553
|
winners = members.filter((m) => m.current);
|
|
538
554
|
}
|
|
539
555
|
else {
|
|
556
|
+
// A number that fits two inputs out of order is asked about wherever they are (owner 2026-11-02); scope settles words.
|
|
540
557
|
const nearest = Math.min(...members.map((m) => m.level));
|
|
541
|
-
winners = members.filter((m) => m.level === nearest);
|
|
558
|
+
winners = members.every(readsDigits) ? members : members.filter((m) => m.level === nearest);
|
|
542
559
|
}
|
|
543
560
|
// a winner declared to be confirmed against another member is asked about together with it
|
|
544
561
|
const confirmWith = winners.length === 1 ? members.filter((m) => m !== winners[0] && paired(m, winners[0])) : [];
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:2bf72b6928fba83b16952cd064e11de4ecb2619129b5e8b8020ade6ecc3e9137";
|
|
@@ -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
|
|
@@ -232,16 +232,21 @@ Rules, in order:
|
|
|
232
232
|
`depends_on` prerequisites are not filled yet (the sub-account before the account is known); a named answer ("50
|
|
233
233
|
dollars") still waits as a pending value. **The whole signature:** A digit field takes a number only when the whole number fits its
|
|
234
234
|
length; a pattern only when it covers the whole number; a choice only its declared or prepared values. A fragment of
|
|
235
|
-
a longer number (the last four of "88 91 7112") fills nothing.
|
|
235
|
+
a longer number (the last four of "88 91 7112") fills nothing. **A perfect match:** a number read out of order accounts for every digit
|
|
236
|
+
of the answer, alone or with the other fields a compound answer fills. "0 4 guion 31, 0 31" at the expiration fills no ZIP;
|
|
237
|
+
"the line is 8185551234" fills the phone. Its validation decides too: a number its validator rejects fills nothing and says nothing, and the question
|
|
238
|
+
is asked again.
|
|
236
239
|
4. **A bare number** (no money mark, no word naming a field) is an amount only at a question that is, invites or is
|
|
237
240
|
declared `confirm_against` an amount. A single digit the question offers as a key is that key; one it does not offer reaches the invited amount,
|
|
238
|
-
whose validation decides
|
|
241
|
+
whose validation decides. The engine holds no amounts, so "only a known amount" is the amount validator's rule: Curacao's
|
|
242
|
+
takes a bare number at the amount question, and at a question that only invites an amount only when it is an amount the
|
|
243
|
+
caller can pay (else it asks for it in dollars). A bare number that does not fit the current question is
|
|
239
244
|
read out of order under rule 3.
|
|
240
245
|
5. **Compound answers.** Several fields fill from one answer only through the form's compound parser, or when every
|
|
241
246
|
part of the answer fits exactly one field. **One part of an answer never fills two fields**; the engine refuses that
|
|
242
247
|
reading and records the refusal.
|
|
243
248
|
6. **Scope.** Across forms the nearest form wins; two fields of the same form that fit the same part are asked about,
|
|
244
|
-
never ranked.
|
|
249
|
+
never ranked. A number that fits two fields out of order is asked about wherever they are.
|
|
245
250
|
|
|
246
251
|
Every result records why: `decisions` lists each field filled other than as the current answer with its rule (`invited`, `out_of_order`, `compound`), and every
|
|
247
252
|
refused reading is listed with its rule, so hosts can log what was filled out of order and what was refused.
|
|
@@ -301,7 +306,7 @@ A fact has unique `key`, `description`, optional `phrases`, and a `path` within
|
|
|
301
306
|
|
|
302
307
|
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
308
|
|
|
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.
|
|
309
|
+
`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
310
|
|
|
306
311
|
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
312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.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",
|