jsfe 1.7.1 → 1.7.3
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/README.md +5 -0
- package/dist/forms/contracts.d.ts.map +1 -1
- package/dist/forms/contracts.js +15 -5
- package/dist/forms/flows.d.ts +3 -155
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +98 -32
- package/dist/forms/proposal-runtime.d.ts +5 -0
- package/dist/forms/proposal-runtime.d.ts.map +1 -0
- package/dist/forms/proposal-runtime.js +9 -0
- package/dist/helpers/form-conversation.d.ts +28 -0
- package/dist/helpers/form-conversation.d.ts.map +1 -0
- package/dist/helpers/form-conversation.js +57 -0
- package/dist/helpers/form-proposals.d.ts +17 -0
- package/dist/helpers/form-proposals.d.ts.map +1 -0
- package/dist/helpers/form-proposals.js +292 -0
- package/dist/helpers/form-state.d.ts +1 -0
- package/dist/helpers/form-state.d.ts.map +1 -1
- package/dist/helpers/form-state.js +10 -2
- package/dist/helpers/index.d.ts +35 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +6 -1
- package/dist/helpers/match-inputs.js +1 -1
- package/dist/helpers/proposal-contract.d.ts +7 -0
- package/dist/helpers/proposal-contract.d.ts.map +1 -0
- package/dist/helpers/proposal-contract.js +95 -0
- package/dist/host/capabilities.d.ts.map +1 -1
- package/dist/host/capabilities.js +6 -0
- package/dist/host/flow-recovery.d.ts +19 -0
- package/dist/host/flow-recovery.d.ts.map +1 -0
- package/dist/host/flow-recovery.js +43 -0
- package/dist/host/index.d.ts +2 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +1 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -2
- package/dist/runtime-identity.js +1 -1
- package/docs/development-release.md +12 -0
- package/docs/forms.md +40 -1
- package/package.json +1 -1
package/dist/forms/flows.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* state that survives DISPATCH and reboot, so a caller who leaves a form resumes it. Results of finished forms:
|
|
17
17
|
* cargo.formResults[<form id>].
|
|
18
18
|
*/
|
|
19
|
+
import { INPUT_PROPOSALS_REQUIREMENT, hasInputProposals, proposalRuntimeSteps } from './proposal-runtime.js';
|
|
19
20
|
import { resolveFormInputs, validateFormIncompleteOutcome, validateFormExhaustion } from '../helpers/form-state.js';
|
|
20
21
|
// ── step helpers ────────────────────────────────────────────────────────────────────────────────────────────────
|
|
21
22
|
const SET = (id, variable, value) => ({ id, type: 'SET', variable, value });
|
|
@@ -28,7 +29,10 @@ const FLOW = (id, value, callType = 'call', parameters) => ({ id, type: 'FLOW',
|
|
|
28
29
|
const WHEN = (id, condition, step) => ({ id, type: 'CASE', branches: { [`condition: ${condition}`]: step, default: NOOP(`${id}_skip`) } });
|
|
29
30
|
const vars = (o) => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, { type: Array.isArray(v) ? 'array' : v === null || typeof v === 'object' ? 'object' : typeof v, value: v }]));
|
|
30
31
|
const incompleteAttrs = (form, reason) => ({ outcome: `{{formIncompleteOutcome(cargo,${form},{outcome:'unresolved',reason:${reason}}).outcome}}`, reason: `{{formIncompleteOutcome(cargo,${form},{outcome:'unresolved',reason:${reason}}).reason}}` });
|
|
31
|
-
const
|
|
32
|
+
const recoveryAttrs = (form, reason, failure = false, mode = 'resume') => { const call = `formConversationOutcome(cargo,${form},${reason},${failure},${JSON.stringify(mode)})`; return { outcome: `{{${call}.outcome}}`, reason: `{{${call}.reason}}`, recovery: { version: 1, mode: `{{${call}.recovery.mode}}`, reason: `{{${call}.recovery.reason}}`, form: `{{${call}.recovery.form || ''}}`, flow: `{{${call}.recovery.flow || ''}}` } }; };
|
|
33
|
+
const incompleteDispatch = (id, form, reason) => ({ id, type: 'DISPATCH', ...recoveryAttrs(form, reason, true, 'none') });
|
|
34
|
+
const yieldDispatch = (id, form, reason) => ({ id, type: 'DISPATCH', ...recoveryAttrs(form, reason) });
|
|
35
|
+
const recoveryStamp = (id, form, reason, mode = 'resume') => ({ ...SET(id, 'noop', 'true'), recovery: recoveryAttrs(form, reason, true, mode).recovery });
|
|
32
36
|
const optionalIncomplete = (id, condition, form, reason) => WHEN(id, `(${condition}) && !!formIncompleteOutcome(cargo,${form},{outcome:'unresolved',reason:${reason}}).owner`, incompleteDispatch(id + '_dispatch', form, reason));
|
|
33
37
|
/** A SET value that yields this text: with {{ }} it is a template (as is); without, a JS string literal. */
|
|
34
38
|
export const textValue = (text) => (String(text).includes('{{') ? String(text) : JSON.stringify(String(text)));
|
|
@@ -111,11 +115,12 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
111
115
|
id, name, version, description, scope, metadata: { formProtocol: 2 }, parameters: [...parameters, { name: 'form_instance', type: 'string', internal: true, description: 'Internal instance to resume' }], ...(primary ? { primary: true } : {}), ...(prompt ? { prompt, prompt_es } : {}),
|
|
112
116
|
variables: vars({ form_on_exhausted: on_exhausted, form_incomplete_outcome: incompleteFrom ? null : incomplete_outcome, form_instance: '', form_internal_resume: false, form_resume_owner: '', form_arguments: {}, form_exhaustion: null, form_values: {}, form_validation: '', initial_readings: [], inputs: resolveFormInputs(inputs, FIELD_PRESETS), parent_form: '', result_input: '', questions: [], provides: null, exit_context: '', exit_context_es: '', form_confirm_q: '', form_confirm_q_es: '', form_confirmation: confirm || {}, form_should_confirm: false, ...extraVars }),
|
|
113
117
|
steps: [
|
|
118
|
+
...proposalRuntimeSteps(id, [], hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []),
|
|
114
119
|
SET(`${id}_resume_kind`, 'form_internal_resume', '!!form_instance'),
|
|
115
120
|
SET(`${id}_open`, 'form_instance', `openFormInstance(cargo, ${JSON.stringify(id)}, inputs, { parent: parent_form || null, input: result_input || '', context:!!parent_form && !result_input, resume: form_instance || (!parent_form ? (cargo.formTasks || {})[${JSON.stringify(id)}] || '' : ''), providers: provides, questions: questions, incomplete_outcome: ${incompleteExpression}, on_exhausted: form_on_exhausted })`),
|
|
116
121
|
SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
|
|
117
122
|
SIDE(`${id}_provider_reset`, '(delete cargo.forms[form_instance].providerRequests, delete cargo.forms[form_instance].activeGates, delete cargo.forms[form_instance].providerGateActive, true)'),
|
|
118
|
-
SIDE(`${id}_pass_reset`, '(cargo.forms[form_instance].replays = 0, form_internal_resume ? true : (cargo.forms[form_instance].attempts = {}), true)'),
|
|
123
|
+
SIDE(`${id}_pass_reset`, '(cargo.forms[form_instance].replays = 0, form_internal_resume ? true : (cargo.forms[form_instance].attempts = {},cargo.forms[form_instance].misses={},cargo.forms[form_instance].feedback={}), true)'),
|
|
119
124
|
SET(`${id}_arguments`, 'form_arguments', `rememberFormArguments(cargo.forms[form_instance], currentContext, ${JSON.stringify(parameters.filter(p => !p.internal && !/^internal\b/i.test(p.description || '')).map(p => p.name))}, form_internal_resume)`),
|
|
120
125
|
...parameters.filter(p => !p.internal && !/^internal\b/i.test(p.description || '')).map(p => SET(`${id}_restore_${p.name}`, p.name, `form_internal_resume ? form_arguments[${JSON.stringify(p.name)}] : currentContext[${JSON.stringify(p.name)}]`)),
|
|
121
126
|
...before,
|
|
@@ -128,7 +133,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
128
133
|
WHEN(`${id}_parent_resume`, "rf_status === 'parent_changed'", FLOW(`${id}_parent_reenter`, 'resume-form', 'reboot', { form: '{{form_resume_owner}}' })),
|
|
129
134
|
] : []),
|
|
130
135
|
...(close_on_stop ? [WHEN(`${id}_close_stopped`, "rf_status === 'stopped'", SIDE(`${id}_stop_close`, '(closeFormInstance(cargo, form_instance, false), true)'))] : []),
|
|
131
|
-
WHEN(`${id}_left`, "rf_status !== 'complete'", END(`${id}_left_end`)),
|
|
136
|
+
WHEN(`${id}_left`, "rf_status !== 'complete' || !cargo.forms[form_instance]", END(`${id}_left_end`)),
|
|
132
137
|
...(validator ? [
|
|
133
138
|
SET(`${id}_values`, 'form_values', 'cargo.forms[form_instance].values'),
|
|
134
139
|
SIDE(`${id}_validation_reset`, '(cargo.formCheck = null, true)'),
|
|
@@ -137,6 +142,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
137
142
|
WHEN(`${id}_resolve_validation_exhausted`, "form_validation === 'failed'", SET(`${id}_validation_exhaustion`, 'form_exhaustion', "exhaustFormInput(cargo,form_instance,((cargo.formCheck || {}).invalid || [])[0] ? cargo.formCheck.invalid[0].name : '')")),
|
|
138
143
|
WHEN(`${id}_recover_validation_exhausted`, 'form_exhaustion', FLOW(`${id}_validation_recovery`, '{{form_exhaustion.flow}}', 'reboot', { form: '{{form_exhaustion.form}}', failed_form: '{{form_exhaustion.failed_form}}', input: '{{form_exhaustion.input}}', failed_input: '{{form_exhaustion.failed_input}}', reason: '{{form_exhaustion.reason}}' })),
|
|
139
144
|
optionalIncomplete(`${id}_optional_validation_failed`, "form_validation === 'failed'", 'form_instance', "form_instance+'_incomplete'"),
|
|
145
|
+
WHEN(`${id}_validation_failed_recovery`, "form_validation === 'failed'", recoveryStamp(`${id}_validation_stamp`, 'form_instance', "'validation_exhausted'", 'none')),
|
|
140
146
|
WHEN(`${id}_validation_failed`, "form_validation === 'failed'", FLOW(`${id}_validation_give_up`, 'give-up', 'reboot', { context: '{{exit_context}}', context_es: '{{exit_context_es}}' })),
|
|
141
147
|
WHEN(`${id}_validation_message`, "form_validation === 'retry' && cargo.formCheck.message", SAY(`${id}_validation_say`, '{{cargo.formCheck.message}}', '{{cargo.formCheck.message_es}}')),
|
|
142
148
|
WHEN(`${id}_validation_retry`, "form_validation === 'retry'", FLOW(`${id}_validate_again`, id, 'replace', { parent_form: '{{parent_form}}', result_input: '{{result_input}}', form_instance: '{{form_instance}}' })),
|
|
@@ -162,7 +168,7 @@ const runForm = {
|
|
|
162
168
|
id: 'run-form', name: 'RunForm', version: '1.0.0',
|
|
163
169
|
description: "Worker (shared scope, called by a form flow): the FORM loop. Merges the form's activation parameters into cargo.forms[rf_form] once, then each pass finds the first missing input whose `when` holds, collects it (collect-input) or runs its sub-form, and replaces itself until nothing is missing. Sets rf_status: 'complete' | 'stopped' (a path in rf_stop was set) | 'unwound'. Reads the form's `inputs`, `parent_form`, `result_input`, `questions`, `provides`, `exit_context(_es)`.",
|
|
164
170
|
parameters: [{ name: 'rf_resume_parent', type: 'boolean', description: 'Yield a context collector when its owning form accepts a change' }, { name: 'rf_form', type: 'string', description: 'The form id (the calling form flow id)' }, { name: 'rf_resume', type: 'string', description: "'true' on the loop's own replays" }, { name: 'rf_stop', type: 'string', description: 'A cargo path (or list) that ends the form when set' }],
|
|
165
|
-
variables: vars({ rf_stop: '', rf_resume: '', rf_resume_parent: false, rf_status: '', rf_next: '', rf_next_form: '', rf_questions: [], rf_provides: null, rf_max: 0, rf_pending: [], rf_deferred: [] }),
|
|
171
|
+
variables: vars({ rf_stop: '', rf_resume: '', rf_resume_parent: false, rf_status: '', rf_next: '', rf_next_form: '', rf_questions: [], rf_provides: null, rf_max: 0, rf_pending: [], rf_deferred: [], rf_proposal: null, rf_proposal_readings: [] }),
|
|
166
172
|
steps: [
|
|
167
173
|
SIDE('rf_pass_count', '(cargo.forms[rf_form] ? (cargo.forms[rf_form].replays = Number(cargo.forms[rf_form].replays || 0) + 1) : true, true)'),
|
|
168
174
|
SET('rf_pass', 'rf_status', "formInstanceApplies(cargo, rf_form) ? '' : (closeFormInstance(cargo, rf_form, false), 'unwound')"),
|
|
@@ -178,6 +184,12 @@ const runForm = {
|
|
|
178
184
|
SET('rf_pending', 'rf_pending', 'pendingFormReadings(cargo, rf_form)'),
|
|
179
185
|
WHEN('rf_pending_accept', 'rf_pending.length > 0', FLOW('rf_pending_batch', 'collect-input-accept', 'call', { hits: '{{rf_pending}}' })),
|
|
180
186
|
WHEN('rf_pending_repeat', 'rf_pending.length > 0', FLOW('rf_pending_again', 'run-form', 'replace', { rf_form: '{{rf_form}}', rf_resume: 'true', rf_stop: '{{rf_stop}}' })),
|
|
187
|
+
SET('rf_proposal_readings', 'rf_proposal_readings', 'formProposalReadings(cargo,rf_form)'),
|
|
188
|
+
WHEN('rf_proposal_revalidate', 'rf_proposal_readings.length > 0', FLOW('rf_proposal_batch', 'collect-input-accept', 'call', { hits: '{{rf_proposal_readings}}' })),
|
|
189
|
+
WHEN('rf_proposal_owner_closed', '!cargo.forms[rf_form]', END('rf_proposal_closed_end')),
|
|
190
|
+
SET('rf_proposal', 'rf_proposal', 'nextFormProposal(cargo,rf_form)'),
|
|
191
|
+
WHEN('rf_proposal_ask', 'rf_proposal', FLOW('rf_proposal_confirm', 'confirm-input-proposal', 'call', { parent_form: '{{rf_form}}', input: '{{rf_proposal.input}}' })),
|
|
192
|
+
WHEN('rf_proposal_repeat', 'rf_proposal || rf_proposal_readings.length > 0', FLOW('rf_proposal_again', 'run-form', 'replace', { rf_form: '{{rf_form}}', rf_resume: 'true', rf_stop: '{{rf_stop}}', rf_resume_parent: '{{rf_resume_parent}}' })),
|
|
181
193
|
SET('rf_find', 'rf_next', 'nextFormInput(cargo.forms[rf_form], cargo)'),
|
|
182
194
|
WHEN('rf_blocked', "rf_next === '__blocked__'", incompleteDispatch('rf_blocked_end', 'rf_form', "'form_dependencies_blocked'")),
|
|
183
195
|
SET('rf_find_form', 'rf_next_form', "rf_next ? ((cargo.forms[rf_form].specs || []).filter(function (i) { return i.name === rf_next; })[0] || {}).form || '' : ''"),
|
|
@@ -198,26 +210,31 @@ const collectInput = {
|
|
|
198
210
|
id: 'collect-input', name: 'CollectInput', version: '1.0.0', scope: 'isolated',
|
|
199
211
|
description: "Worker: ask the form's current input once and file the answer wherever it belongs in the form chain (matchInputs, decided by scope; a declared confirm_against pair is asked). One reading → collect-input-accept; ambiguous → disambiguate-input; a question about the form's data → provide-data; a short miss → counted, the loop re-asks; 3+ words that fit nothing → DISPATCH. Params: form, current, questions, provides, exit_context(_es).",
|
|
200
212
|
parameters: ['form', 'current', 'questions', 'provides', 'exit_context', 'exit_context_es'].map((n) => ({ name: n, type: ['questions'].includes(n) ? 'array' : n === 'provides' ? 'object' : 'string', description: n })),
|
|
201
|
-
variables: vars({ ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
|
|
213
|
+
variables: vars({ ci_exhaustion: null, ci_d: {}, ci_pre: '', ci_val: '', ci_routed: '', ci_names: [], ci_chain: [], ci_spec: null, ci_choices: [], ci_q: '', ci_q_es: '', ci_missed: 0, ci_max: 3, ci_misses: 0, ci_miss_limit: 2, ci_m: null, ci_qkey: '', ci_skip_gate: '', ci_words: 0, ci_digits: null, ci_amount: false, ci_as_spoken: false, ci_reader: {}, ci_pii: false, ci_ctx: '', ci_ctx_es: '' }),
|
|
202
214
|
steps: [
|
|
203
215
|
SET('ci_find_spec', 'ci_spec', "(function () { var s = ((cargo.forms[form] || {}).specs || []).filter(function (s) { return s.name === current; })[0]; return s ? (Array.isArray(s.any_of) ? Object.assign({}, s, { profile: { kind: 'group' } }) : s) : null; })()"),
|
|
204
216
|
SET('ci_current_names', 'ci_names', "Array.isArray(ci_spec && ci_spec.any_of) ? ci_spec.any_of.map(function (m) { return m.name; }) : [current]"),
|
|
205
|
-
WHEN('ci_no_spec', '!ci_spec', END('ci_no_spec_end',
|
|
217
|
+
WHEN('ci_no_spec', '!ci_spec', END('ci_no_spec_end', recoveryAttrs('form', "'collect_input_spec'", true, 'none'))),
|
|
206
218
|
SET('ci_count_missed', 'ci_missed', "Number(((cargo.forms[form] || {}).attempts || {})[current] || 0)"),
|
|
207
219
|
SET('ci_max_attempts', 'ci_max', 'Number(ci_spec.max_attempts || 3)'),
|
|
220
|
+
SET('ci_context_misses', 'ci_misses', 'Number(((cargo.forms[form] || {}).misses || {})[current] || 0)'),
|
|
221
|
+
SET('ci_context_limit', 'ci_miss_limit', 'Number(ci_spec.max_misses || 2)'),
|
|
222
|
+
WHEN('ci_context_yield', 'ci_misses >= ci_miss_limit', yieldDispatch('ci_misses_dispatch', 'form', "'input_misses_exhausted'")),
|
|
208
223
|
SET('ci_context', 'ci_ctx', "typeof exit_context !== 'undefined' && exit_context ? exit_context : 'your request'"),
|
|
209
224
|
SET('ci_context_es', 'ci_ctx_es', "typeof exit_context_es !== 'undefined' && exit_context_es ? exit_context_es : 'su solicitud'"),
|
|
210
225
|
WHEN('ci_resolve_exhausted', 'ci_missed >= ci_max', SET('ci_exhausted_policy', 'ci_exhaustion', 'exhaustFormInput(cargo,form,current)')),
|
|
211
226
|
WHEN('ci_declared_exhausted', 'ci_exhaustion', FLOW('ci_exhausted_recovery', '{{ci_exhaustion.flow}}', 'reboot', { form: '{{ci_exhaustion.form}}', failed_form: '{{ci_exhaustion.failed_form}}', input: '{{ci_exhaustion.input}}', failed_input: '{{ci_exhaustion.failed_input}}', reason: '{{ci_exhaustion.reason}}' })),
|
|
212
227
|
optionalIncomplete('ci_optional_exhausted', 'ci_missed >= ci_max', 'form', "form+'_incomplete'"),
|
|
228
|
+
WHEN('ci_failure_recovery', 'ci_missed >= ci_max', recoveryStamp('ci_failure_stamp', 'form', "'validation_exhausted'")),
|
|
213
229
|
WHEN('ci_give_up', 'ci_missed >= ci_max', FLOW('ci_give_up_flow', 'give-up', 'reboot', { context: '{{ci_ctx}}', context_es: '{{ci_ctx_es}}' })),
|
|
214
230
|
FLOW('ci_prepare', 'prepare-input', 'call', { form: '{{form}}', input: '{{current}}' }),
|
|
215
231
|
// `refresh`: a flow run before EVERY ask, which computes what the question shows into cargo.forms[form].data
|
|
216
232
|
WHEN('ci_refresh', 'ci_spec.refresh', FLOW('ci_refresh_flow', '{{ci_spec.refresh}}', 'call', { form: '{{form}}', input: '{{current}}' })),
|
|
217
233
|
SET('ci_refetch_spec', 'ci_spec', "(function(){var spec=cargo.forms[form].specs.filter(function(s){return s.name===current;})[0];return spec&&spec.any_of?Object.assign({},spec,{profile:{kind:'group'}}):spec;})()"),
|
|
218
234
|
SET('ci_data', 'ci_d', '(cargo.forms[form] || {}).data || {}'),
|
|
219
|
-
SET('ci_question', 'ci_q', "ci_spec
|
|
220
|
-
SET('ci_question_es', 'ci_q_es', "
|
|
235
|
+
SET('ci_question', 'ci_q', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'en')"),
|
|
236
|
+
SET('ci_question_es', 'ci_q_es', "formInputPrompt(cargo,form,current,ci_spec,ci_d,'es')"),
|
|
237
|
+
SIDE('ci_feedback_consumed', 'consumeFormFeedback(cargo,form,current)'),
|
|
221
238
|
SET('ci_own_choices', 'ci_choices', "ci_spec.choices_from && Array.isArray(ci_d[ci_spec.choices_from]) ? ci_d[ci_spec.choices_from] : (ci_spec.profile.kind === 'choice' && Array.isArray(ci_spec.profile.choices) ? ci_spec.profile.choices : [])"),
|
|
222
239
|
SET('ci_keypad', 'ci_digits', 'ci_spec.digits || null'),
|
|
223
240
|
SET('ci_is_amount', 'ci_amount', "ci_spec.amount === 'marked' ? 'marked' : (ci_spec.amount === true || ci_spec.profile.kind === 'amount')"),
|
|
@@ -236,6 +253,7 @@ const collectInput = {
|
|
|
236
253
|
// The labels and validators ride on each reading, so the presets stay in this one flow.
|
|
237
254
|
SET('ci_enrich', 'ci_m', 'enrichFormMatches(ci_m, ci_chain, form, ci_names, current)'),
|
|
238
255
|
SET('ci_question_key', 'ci_qkey', "ci_m.questions.length ? ci_m.questions[0] : !ci_m.hits.length && !ci_m.ambiguous.length && Array.isArray(questions) && questions.length && provides ? pickChoice(cargo.choiceText, questions, false) : ''"),
|
|
256
|
+
WHEN('ci_understood', 'ci_m.hits.length || ci_m.ambiguous.length || ci_m.questions.length || ci_qkey && ci_qkey.indexOf("__") !== 0', SIDE('ci_clear_misses', 'clearFormMisses(cargo,form,current)')),
|
|
239
257
|
SET('ci_count_words', 'ci_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function (w) { return /[a-záéíóúüñ]/i.test(w); }).length"),
|
|
240
258
|
// Independent field clauses are committed before a provider can suspend on its gate.
|
|
241
259
|
WHEN('ci_question_ambiguity', 'ci_m.questions.length > 0 && ci_m.ambiguous.length > 0', FLOW('ci_question_clarify', 'disambiguate-input', 'call', { readings: '{{ci_m.ambiguous}}', hits: '{{ci_m.hits}}', remaining: '{{ci_m.remaining}}' })),
|
|
@@ -248,8 +266,8 @@ const collectInput = {
|
|
|
248
266
|
'condition: ci_m.hits.length > 0': FLOW('ci_accept', 'collect-input-accept', 'call', { hits: '{{ci_m.hits}}' }),
|
|
249
267
|
"condition: ci_m.questions.length > 0": END('ci_question_handled'),
|
|
250
268
|
"condition: ci_qkey && ci_qkey.indexOf('__') !== 0": FLOW('ci_question_asked', 'provide-data', 'call', { form: '{{form}}', provides: '{{provides}}', asked: '{{ci_qkey}}', no_menu: 'true', no_gate: '{{ci_skip_gate}}' }),
|
|
251
|
-
'condition: ci_words >= 3':
|
|
252
|
-
default: SIDE('ci_miss', "(cargo
|
|
269
|
+
'condition: ci_words >= 3': yieldDispatch('ci_elsewhere', 'form', "'input_not_matched'"),
|
|
270
|
+
default: SIDE('ci_miss', "(missFormInput(cargo,form,current),true)")
|
|
253
271
|
} },
|
|
254
272
|
],
|
|
255
273
|
};
|
|
@@ -265,14 +283,15 @@ const prepareInput = {
|
|
|
265
283
|
const validateBatch = {
|
|
266
284
|
id: 'validate-input-batch', name: 'ValidateInputBatch', version: '2.0.0', scope: 'isolated',
|
|
267
285
|
description: 'Worker: validate dependency-ordered candidates; return accepted, rejected and pending readings before a single commit.',
|
|
268
|
-
parameters: ['hits', 'accepted', 'rejected', 'pending'].map(name => ({ name, type: 'array' })),
|
|
269
|
-
variables: vars({ accepted: [], rejected: [], pending: [], vb_h: null, vb_rest: [], vb_values: {}, vb_known: false, vb_ready: false, vb_relevant: false, vb_outcome: '' }),
|
|
286
|
+
parameters: ['hits', 'accepted', 'rejected', 'pending', 'ignored'].map(name => ({ name, type: 'array' })),
|
|
287
|
+
variables: vars({ accepted: [], rejected: [], pending: [], ignored: [], vb_h: null, vb_rest: [], vb_values: {}, vb_known: false, vb_ready: false, vb_relevant: false, vb_outcome: '', vb_candidate: null, vb_proposal_values: {}, vb_proposal: null, vb_proposal_ok: true }),
|
|
270
288
|
steps: [
|
|
271
289
|
SET('vb_init_accepted', 'accepted', 'Array.isArray(accepted) ? accepted : []'),
|
|
272
290
|
SET('vb_init_rejected', 'rejected', 'Array.isArray(rejected) ? rejected : []'),
|
|
273
291
|
SET('vb_init_pending', 'pending', 'Array.isArray(pending) ? pending : []'),
|
|
292
|
+
SET('vb_init_ignored', 'ignored', 'Array.isArray(ignored) ? ignored : []'),
|
|
274
293
|
SET('vb_first', 'vb_h', 'hits.length ? hits[0] : null'),
|
|
275
|
-
WHEN('vb_done', '!vb_h', SIDE('vb_result', '(cargo.inputBatch = { accepted: accepted, rejected: rejected, pending: pending }, true)')),
|
|
294
|
+
WHEN('vb_done', '!vb_h', SIDE('vb_result', '(cargo.inputBatch = { accepted: accepted, rejected: rejected, pending: pending, ignored: ignored }, true)')),
|
|
276
295
|
WHEN('vb_end', '!vb_h', END('vb_end_done')),
|
|
277
296
|
SET('vb_rest', 'vb_rest', 'hits.slice(1)'),
|
|
278
297
|
SET('vb_relevant', 'vb_relevant', 'formReadingRelevant(cargo, vb_h, accepted)'),
|
|
@@ -284,8 +303,15 @@ const validateBatch = {
|
|
|
284
303
|
SIDE('vb_reset', "(cargo.inputCheck = vb_h.invalid ? { ok: false } : (vb_h.validator && !vb_known ? null : { ok: true, value: vb_h.value }), true)"),
|
|
285
304
|
WHEN('vb_validator', 'vb_ready === true && vb_h.validator && !vb_known && !vb_h.invalid', FLOW('vb_validate', '{{vb_h.validator}}', 'call', { form: '{{vb_h.form}}', input: '{{vb_h.name}}', value: '{{vb_h.value}}', values: '{{vb_values}}', reading: '{{vb_h}}' })),
|
|
286
305
|
WHEN('vb_refused', 'vb_h.refused === true', SIDE('vb_withhold_refusal', "(cargo.inputCheck=Object.assign({},cargo.inputCheck || {},{ok:false}),true)")),
|
|
287
|
-
|
|
288
|
-
|
|
306
|
+
SET('vb_candidate', 'vb_candidate', "Object.assign({},vb_h,{ok:!!(cargo.inputCheck && cargo.inputCheck.ok === true),value:cargo.inputCheck && cargo.inputCheck.value !== undefined && cargo.inputCheck.value !== null ? cargo.inputCheck.value : vb_h.value,message:cargo.inputCheck && cargo.inputCheck.message,message_es:cargo.inputCheck && cargo.inputCheck.message_es})"),
|
|
307
|
+
SET('vb_proposal_policy', 'vb_proposal', 'vb_relevant === true && vb_ready === true && vb_candidate.ok && !vb_known ? formProposalPolicy(cargo,vb_candidate,cargo.inputCheck,accepted) : null'),
|
|
308
|
+
SET('vb_proposal_values', 'vb_proposal_values', 'proposedFormValues(cargo,vb_h.form,accepted.concat([vb_candidate]))'),
|
|
309
|
+
SIDE('vb_proposal_reset', '(cargo.proposalCheck=undefined,true)'),
|
|
310
|
+
WHEN('vb_proposal_prepare', 'vb_proposal && vb_proposal.prepare', FLOW('vb_proposal_formatter', '{{vb_proposal.prepare}}', 'call', { form: '{{vb_candidate.form}}', input: '{{vb_candidate.name}}', value: '{{vb_candidate.value}}', values: '{{vb_proposal_values}}', reading: '{{vb_candidate}}' })),
|
|
311
|
+
SET('vb_proposal_gate', 'vb_proposal_ok', 'vb_relevant !== true || vb_ready !== true || !vb_candidate.ok || vb_known ? true : stageFormProposal(cargo,vb_candidate,formProposalDescriptor(vb_proposal,vb_proposal && vb_proposal.prepare ? cargo.proposalCheck : undefined))'),
|
|
312
|
+
{ ...SET('vb_proposal_gate_valid', 'noop', "typeof vb_proposal_ok === 'boolean'"), requireTrue: true },
|
|
313
|
+
SIDE('vb_take', "(vb_relevant !== true ? ignored.push(vb_h) : vb_ready !== true ? pending.push(Object.assign({}, vb_h, {outcome:'pending'})) : vb_candidate.ok === true ? (vb_proposal_ok === true ? accepted.push(Object.assign({}, vb_candidate, { outcome: vb_outcome })) : null) : rejected.push(Object.assign({}, vb_candidate, { outcome: 'rejected' })), true)"),
|
|
314
|
+
FLOW('vb_next', 'validate-input-batch', 'replace', { hits: '{{vb_rest}}', accepted: '{{accepted}}', rejected: '{{rejected}}', pending: '{{pending}}', ignored: '{{ignored}}' }),
|
|
289
315
|
],
|
|
290
316
|
};
|
|
291
317
|
const accept = {
|
|
@@ -301,7 +327,9 @@ const accept = {
|
|
|
301
327
|
SET('ca_order', 'ca_ordered', 'orderFormReadings(cargo, hits)'),
|
|
302
328
|
FLOW('ca_validate', 'validate-input-batch', 'call', { hits: '{{ca_ordered}}' }),
|
|
303
329
|
SIDE('ca_settle', '(cargo.inputBatch = settleFormActivations(cargo.inputBatch, cargo), true)'),
|
|
330
|
+
{ ...SIDE('ca_proposal_validation', '(settleFormProposalValidation(cargo,cargo.inputBatch),true)'), requireTrue: true },
|
|
304
331
|
SIDE('ca_rejections', 'rejectFormReadings(cargo, cargo.inputBatch.rejected)'),
|
|
332
|
+
{ ...SIDE('ca_proposal_commit', 'commitFormProposals(cargo,cargo.inputBatch.accepted)'), requireTrue: true },
|
|
305
333
|
SIDE('ca_commit', 'acceptFormReadings(cargo, cargo.inputBatch.accepted, cargo.inputBatch.pending)'),
|
|
306
334
|
SIDE('ca_pending', 'queueFormReadings(cargo, cargo.inputBatch.pending)'),
|
|
307
335
|
SET('ca_results', 'ca_results', 'cargo.inputBatch.accepted.concat(cargo.inputBatch.rejected, cargo.inputBatch.pending)'),
|
|
@@ -317,7 +345,7 @@ const acceptedEffects = {
|
|
|
317
345
|
SET('ie_first', 'ca_h', 'readings.length ? readings[0] : null'),
|
|
318
346
|
WHEN('ie_none', '!ca_h', END('ie_end')),
|
|
319
347
|
SET('ie_rest', 'ca_rest', 'readings.slice(1)'),
|
|
320
|
-
WHEN('ie_reject', "ca_h.outcome === 'rejected'", SIDE('ie_attempt', "(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] = Number(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] || 0) + 1, true)")),
|
|
348
|
+
WHEN('ie_reject', "ca_h.outcome === 'rejected'", SIDE('ie_attempt', "(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] = Number(cargo.forms[ca_h.form].attempts[ca_h.asked || ca_h.name] || 0) + 1, cargo.forms[ca_h.form].feedback=cargo.forms[ca_h.form].feedback || {}, cargo.forms[ca_h.form].feedback[ca_h.asked || ca_h.name]='invalid', true)")),
|
|
321
349
|
SET('ie_say_en', 'ca_ack', "ca_h.outcome === 'rejected' ? (ca_h.message || ('That ' + (ca_h.label || ca_h.name) + \" doesn't look right.\")) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Thank you, I already have your ' + ca_h.label + '.') : ca_h.outcome === 'changed' ? ('Okay, I updated your ' + ca_h.label + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Got it, your ' + ca_h.label + '.') : ''"),
|
|
322
350
|
SET('ie_say_es', 'ca_ack_es', "ca_h.outcome === 'rejected' ? (ca_h.message_es || ca_h.message || ('Ese ' + (ca_h.label_es || ca_h.label || ca_h.name) + ' no parece correcto.')) : ca_h.silent === true || ca_h.fromPending ? '' : ca_h.outcome === 'known' && !ca_h.current ? ('Gracias, ya tengo su ' + ca_h.label_es + '.') : ca_h.outcome === 'changed' ? ('Listo, actualicé su ' + ca_h.label_es + '.') : (ca_h.outcome === 'taken' || ca_h.outcome === 'pending') && ca_h.origin !== 'activation' ? ('Entendido, su ' + ca_h.label_es + '.') : ''"),
|
|
323
351
|
WHEN('ie_say', 'ca_ack', SAY('ie_ack', '{{ca_ack}}', '{{ca_ack_es}}')),
|
|
@@ -330,7 +358,7 @@ const disambiguate = {
|
|
|
330
358
|
id: 'disambiguate-input', name: 'DisambiguateInput', version: '1.0.0', scope: 'isolated',
|
|
331
359
|
description: "Worker: the answer fits two or more inputs at the same scope (or a declared confirm_against pair). Stores the unambiguous readings, asks which input the rest was for (keys 1..n, never 8), stores the pick. No pick: nothing stored, not a miss. Params: readings, hits.",
|
|
332
360
|
parameters: [{ name: 'readings', type: 'array', description: 'The ambiguous readings' }, { name: 'hits', type: 'array', description: 'Readings to store as they are' }, { name: 'remaining', type: 'array' }],
|
|
333
|
-
variables: vars({ di_keys: [], di_choices: [], di_q: '', di_q_es: '', di_pick: null, di_value: '', remaining: [], di_next: [], di_rest: [] }),
|
|
361
|
+
variables: vars({ di_form: '', di_input: '', di_prompt: '', di_prompt_es: '', di_words: 0, di_misses: 0, di_keys: [], di_choices: [], di_q: '', di_q_es: '', di_pick: null, di_value: '', remaining: [], di_next: [], di_rest: [] }),
|
|
334
362
|
steps: [
|
|
335
363
|
WHEN('di_store_clear', 'Array.isArray(hits) && hits.length > 0', FLOW('di_store_hits', 'collect-input-accept', 'call', { hits: '{{hits}}' })),
|
|
336
364
|
SET('di_number', 'di_keys', `${KEYS_JS}(readings.length)`),
|
|
@@ -338,8 +366,19 @@ const disambiguate = {
|
|
|
338
366
|
SET('di_said', 'di_value', "readings[0].pii ? '' : String(readings[0].value)"),
|
|
339
367
|
SET('di_question', 'di_q', "(di_value ? 'Is ' + di_value + ' your ' : 'Is that your ') + readings.map(function (r) { return r.label; }).join(' or your ') + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Press ' + di_keys[i] + ' or say ' : 'Say ') + r.label.toUpperCase(); }).join(', ') + '.'"),
|
|
340
368
|
SET('di_question_es', 'di_q_es', "(di_value ? '¿' + di_value + ' es su ' : '¿Eso es su ') + readings.map(function (r) { return r.label_es; }).join(' o su ') + '? ' + readings.map(function (r, i) { return (cargo.voice ? 'Presione ' + di_keys[i] + ' o diga ' : 'Diga ') + r.label_es.toUpperCase(); }).join(', ') + '.'"),
|
|
341
|
-
|
|
369
|
+
SET('di_owner', 'di_form', 'readings[0].form'),
|
|
370
|
+
SET('di_counter', 'di_input', "'__ambiguity:'+readings.map(function(r){return r.form+':'+r.name;}).sort().join('|')"),
|
|
371
|
+
SET('di_prompt', 'di_prompt', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'en')"),
|
|
372
|
+
SET('di_prompt_es', 'di_prompt_es', "formInputPrompt(cargo,di_form,di_input,{question:di_q,question_es:di_q_es},{},'es')"),
|
|
373
|
+
SIDE('di_feedback_consumed', 'consumeFormFeedback(cargo,di_form,di_input)'),
|
|
374
|
+
FLOW('di_ask', 'ask-choice', 'call', { question: '{{di_prompt}}', question_es: '{{di_prompt_es}}', choices: '{{di_choices}}', accept_other: true, digits: { min: 1, max: 1 } }),
|
|
342
375
|
SET('di_picked', 'di_pick', "/^r\\d+$/.test(String(cargo.choice || '')) ? [readings[Number(String(cargo.choice).slice(1))]] : null"),
|
|
376
|
+
WHEN('di_understood', 'di_pick', SIDE('di_clear_misses', 'clearFormMisses(cargo,di_form,di_input)')),
|
|
377
|
+
SET('di_word_count', 'di_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function(w){return /[a-záéíóúüñ]/i.test(w);}).length"),
|
|
378
|
+
WHEN('di_context', '!di_pick && di_words >= 3', yieldDispatch('di_context_yield', 'di_form', "'ambiguity_not_resolved'")),
|
|
379
|
+
WHEN('di_miss', '!di_pick', SET('di_miss_count', 'di_misses', 'missFormInput(cargo,di_form,di_input)')),
|
|
380
|
+
WHEN('di_exhausted', '!di_pick && di_misses >= 2', yieldDispatch('di_misses_yield', 'di_form', "'ambiguity_misses_exhausted'")),
|
|
381
|
+
WHEN('di_retry', '!di_pick', FLOW('di_retry_question', 'disambiguate-input', 'replace', { readings: '{{readings}}', hits: [], remaining: '{{remaining}}' })),
|
|
343
382
|
WHEN('di_store_pick', 'di_pick', FLOW('di_store_pick_flow', 'collect-input-accept', 'call', { hits: '{{di_pick}}' })),
|
|
344
383
|
SET('di_remaining_first', 'di_next', 'Array.isArray(remaining) && remaining.length ? remaining[0] : []'),
|
|
345
384
|
SET('di_remaining_rest', 'di_rest', 'Array.isArray(remaining) ? remaining.slice(1) : []'),
|
|
@@ -351,13 +390,16 @@ const confirmForm = {
|
|
|
351
390
|
id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
|
|
352
391
|
description: "Worker: ask a form's confirmation (exact YES / NO; keys 1 and 2). A correction instead ('no, make it $40') is read against the form chain and stored, and the result is 'changed' so the form runs again; an unclear short answer is asked again, a long unrelated one is DISPATCHed. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
|
|
353
392
|
parameters: ['form', 'question', 'question_es', 'exit_context', 'exit_context_es', 'words', 'interrupts', 'no_keys', 'clarifications'].map((n) => ({ name: n, type: ['words', 'interrupts', 'no_keys', 'clarifications'].includes(n) ? 'array' : 'string', description: n })),
|
|
354
|
-
variables: vars({ cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_result: '', cf_words: 0, cf_tries: 0, words: [], clarifications: [], no_keys: ['2'], interrupts: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
|
|
393
|
+
variables: vars({ cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: null, cf_current: null, cf_result: '', cf_prompt: '', cf_prompt_es: '', cf_words: 0, cf_tries: 0, words: [], clarifications: [], no_keys: ['2'], interrupts: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
|
|
355
394
|
steps: [
|
|
356
395
|
SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
|
|
357
396
|
SET('cf_choices', 'cf_choices', "[{key:'yes',words:['1'].concat(Array.isArray(words)?words:[]),exact:true},{key:'no',words:Array.isArray(no_keys)?no_keys:['2']}].concat((Array.isArray(interrupts)?interrupts:[]).map(function(event){return {key:'event:'+event.key,words:event.words,exact:event.exact!==false};}))"),
|
|
358
397
|
SET('cf_initial_chain', 'cf_chain', CHAIN_JS),
|
|
359
398
|
SET('cf_amount_context', 'cf_amount', "cf_chain.some(function(input){return input.profile && input.profile.kind==='amount';})"),
|
|
360
|
-
|
|
399
|
+
SET('cf_prompt', 'cf_prompt', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'en')"),
|
|
400
|
+
SET('cf_prompt_es', 'cf_prompt_es', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'es')"),
|
|
401
|
+
SIDE('cf_feedback_consumed', "consumeFormFeedback(cargo,form,'__confirm')"),
|
|
402
|
+
FLOW('cf_ask', 'ask-choice', 'call', { amount: '{{cf_amount}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', choices: '{{cf_choices}}', accept_other: true, reask_long: true, digits: { min: 1, max: 1 }, exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
|
|
361
403
|
SET('cf_event', 'cf_event', "(Array.isArray(interrupts)?interrupts:[]).filter(function(event){return cargo.choice==='event:'+event.key;}).map(function(event){return event.flow;})[0] || ''"),
|
|
362
404
|
WHEN('cf_interrupt', 'cf_event', FLOW('cf_event_flow', '{{cf_event}}', 'reboot', { form: '{{form}}', event: '{{cargo.choice}}', text: '{{cargo.choiceText}}' })),
|
|
363
405
|
SET('cf_build_chain', 'cf_chain', CHAIN_JS),
|
|
@@ -365,7 +407,8 @@ const confirmForm = {
|
|
|
365
407
|
SET('cf_question_catalog', 'cf_questions', 'cargo.forms[form].questions || []'),
|
|
366
408
|
SET('cf_provider_catalog', 'cf_provides', 'cargo.forms[form].providers'),
|
|
367
409
|
SET('cf_gate_policy', 'cf_skip_gate', "!cf_provides || !cf_provides.gate || [].concat(cf_provides.gate.verified || []).every(function(p){return !!formPath({cargo:cargo},p);}) ? 'true' : ''"),
|
|
368
|
-
SET('
|
|
410
|
+
SET('cf_current', 'cf_current', "cargo.forms[form].proposalPrompt ? {form:cargo.forms[form].parent,name:cargo.forms[form].proposalPrompt.input} : {form:form,name:''}"),
|
|
411
|
+
SET('cf_match', 'cf_m', "cargo.choice === 'yes' || cargo.choice === 'no' && pickChoice(cargo.choiceText, [{key:'no',words:Array.isArray(no_keys)?no_keys:['2'],exact:true}], false) === 'no' ? { hits: [], ambiguous: [], questions:[] } : readFormAnswer(cargo.choiceText, cf_chain, cf_current, '',cf_questions)"),
|
|
369
412
|
SET('cf_enrich', 'cf_m', "(function(m){m.hits=m.hits.map(function(h){return Object.assign({},h,{silent:true});});return m;})(enrichFormMatches(cf_m, cf_chain, form))"),
|
|
370
413
|
SET('cf_count_words', 'cf_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function (w) { return /[a-záéíóúüñ]/i.test(w); }).length"),
|
|
371
414
|
WHEN('cf_question_fields', 'cf_m.questions.length > 0 && cf_m.hits.length > 0', FLOW('cf_question_accept', 'collect-input-accept', 'call', { hits: '{{cf_m.hits}}' })),
|
|
@@ -376,19 +419,41 @@ const confirmForm = {
|
|
|
376
419
|
'condition: cf_m.ambiguous.length > 0': FLOW('cf_ambiguous', 'disambiguate-input', 'call', { readings: '{{cf_m.ambiguous}}', hits: '{{cf_m.hits}}', remaining: '{{cf_m.remaining}}' }),
|
|
377
420
|
'condition: cf_m.hits.length > 0': FLOW('cf_accept', 'collect-input-accept', 'call', { hits: '{{cf_m.hits}}' }),
|
|
378
421
|
"condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
|
|
379
|
-
"condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',words:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'
|
|
380
|
-
'condition: cf_words >= 3':
|
|
422
|
+
"condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',words:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
|
|
423
|
+
'condition: cf_words >= 3': yieldDispatch('cf_elsewhere', 'form', "'confirmation_not_matched'"),
|
|
381
424
|
default: SET('cf_unclear', 'cf_result', "'unclear'")
|
|
382
425
|
} },
|
|
383
426
|
SET('cf_changed', 'cf_result', "cf_result ? cf_result : 'changed'"),
|
|
384
427
|
// an unclear short answer is asked again (the form runs again and reaches this question), at most twice
|
|
385
|
-
SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? (cargo
|
|
386
|
-
|
|
387
|
-
WHEN('cf_give_up', 'cf_tries > 2', FLOW('cf_give_up_flow', 'give-up', 'reboot', { context: '{{exit_context}}', context_es: '{{exit_context_es}}' })),
|
|
428
|
+
SET('cf_tries_count', 'cf_tries', "cf_result === 'unclear' ? missFormInput(cargo,form,'__confirm') : (clearFormMisses(cargo,form,'__confirm'),0)"),
|
|
429
|
+
WHEN('cf_misses_exhausted', 'cf_tries >= 2', yieldDispatch('cf_misses_dispatch', 'form', "'confirmation_misses_exhausted'")),
|
|
388
430
|
WHEN('cf_bind_revision', "cf_result === 'yes'", SIDE('cf_bind', 'confirmFormRevision(cargo, form)')),
|
|
389
|
-
SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' ? 'changed' : cf_result, true)"),
|
|
431
|
+
SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' || cf_result === 'clarification' ? 'changed' : cf_result, true)"),
|
|
390
432
|
],
|
|
391
433
|
};
|
|
434
|
+
// ── proposed input: owned confirmation before canonical acceptance or callbacks ────────────────────────────
|
|
435
|
+
const confirmInputProposal = formFlow({ id: 'confirm-input-proposal', name: 'ConfirmInputProposal', version: '1.0.0', description: 'Confirm an owned validated input candidate; retain it on yield and authorize only normal input acceptance.',
|
|
436
|
+
parameters: [{ name: 'input', type: 'string', description: 'Owning input awaiting proposal confirmation' }], extraVars: { input: '', proposal: null, proposal_readings: [] }, inputs: [],
|
|
437
|
+
before: [
|
|
438
|
+
SET('proposal_refresh_readings', 'proposal_readings', 'formProposalReadings(cargo,parent_form,input,false)'),
|
|
439
|
+
WHEN('proposal_refresh', 'proposal_readings.length > 0', FLOW('proposal_refresh_batch', 'collect-input-accept', 'call', { hits: '{{proposal_readings}}' })),
|
|
440
|
+
SET('proposal_current', 'proposal', 'cargo.forms[parent_form] && cargo.forms[parent_form].inputProposals && cargo.forms[parent_form].inputProposals[input] || null'),
|
|
441
|
+
WHEN('proposal_missing', "!proposal || !['pending','confirmed'].includes(proposal.status) || proposal.committed", FLOW('proposal_missing_close', 'form-done', 'replace', { form: '{{form_instance}}' })),
|
|
442
|
+
{ ...SIDE('proposal_bind', '(bindFormProposal(cargo,parent_form,input,form_instance),true)'), requireTrue: true },
|
|
443
|
+
SET('proposal_confirmation', 'form_confirmation', 'Object.assign({},proposal.descriptor)'),
|
|
444
|
+
],
|
|
445
|
+
confirm: { question: '{{proposal.descriptor.question}}', question_es: '{{proposal.descriptor.question_es || proposal.descriptor.question}}' },
|
|
446
|
+
after: [{ ...SIDE('proposal_confirmed', "decideFormProposal(cargo,parent_form,input,form_instance,'confirmed')"), requireTrue: true }],
|
|
447
|
+
declined: [NOOP('proposal_declined_marker')],
|
|
448
|
+
});
|
|
449
|
+
const proposalDeclined = { id: 'confirm-input-proposal-declined', name: 'ConfirmInputProposalDeclined', version: '1.0.0', scope: 'isolated', description: 'Reject an owned input proposal without accepting its candidate or handing incomplete data to its parent.', parameters: [{ name: 'form_instance', type: 'string' }], variables: vars({ owner: '', input: '', proposal: null }), steps: [
|
|
450
|
+
SET('proposal_decline_owner', 'owner', 'cargo.forms[form_instance].parent'),
|
|
451
|
+
SET('proposal_decline_input', 'input', 'cargo.forms[form_instance].proposalPrompt.input'),
|
|
452
|
+
SET('proposal_decline_record', 'proposal', 'cargo.forms[owner].inputProposals[input]'),
|
|
453
|
+
{ ...SIDE('proposal_decline_decision', "decideFormProposal(cargo,owner,input,form_instance,'declined')"), requireTrue: true },
|
|
454
|
+
SIDE('proposal_decline_close', '(closeFormInstance(cargo,form_instance,false),true)'),
|
|
455
|
+
WHEN('proposal_decline_callback', 'proposal.descriptor.on_declined', FLOW('proposal_decline_invoke', '{{proposal.descriptor.on_declined}}', 'call', { form: '{{owner}}', input: '{{input}}', value: '{{proposal.value}}', reading: '{{proposal.reading}}' })),
|
|
456
|
+
] };
|
|
392
457
|
// ── form-done: the result goes to the parent (or cargo.formResults); the form is cleared ─────────────────────────────
|
|
393
458
|
const formDone = {
|
|
394
459
|
id: 'form-done', name: 'FormDone', version: '1.0.0', scope: 'isolated',
|
|
@@ -497,7 +562,7 @@ const provideData = {
|
|
|
497
562
|
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)" }],
|
|
498
563
|
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 }),
|
|
499
564
|
steps: [
|
|
500
|
-
WHEN('pd_no_catalog', "!provides || typeof provides !== 'object' || !Array.isArray(provides.facts)", END('pd_no_catalog_end', { outcome: 'unresolved', reason: 'provide_data_catalog' })),
|
|
565
|
+
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' } })),
|
|
501
566
|
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;}));},[]); })()"),
|
|
502
567
|
// Nothing specific asked: offer the sets and facts as a menu (keys 1..n, never 8; at most 7 options shown).
|
|
503
568
|
SET('pd_menu_items', 'pd_menu', "(provides.sets || []).concat(provides.facts).slice(0, 7)"),
|
|
@@ -515,7 +580,7 @@ const provideData = {
|
|
|
515
580
|
SET('pd_deferred', 'pd_deferred', `(${finishProviderRequest.toString()})(cargo,pd_request.owner,provides,pd_keys)`),
|
|
516
581
|
SET('pd_merged_keys', 'pd_keys', 'pd_deferred.keys'),
|
|
517
582
|
SET('pd_gate_check', 'pd_ok', `!pd_gated || [].concat(provides.gate.verified).every(function (p) { return !!${PATH_JS}(p); })`),
|
|
518
|
-
WHEN('pd_not_verified', '!pd_ok', END('pd_not_verified_end', { outcome: 'unresolved', reason: 'provide_data_not_verified' })),
|
|
583
|
+
WHEN('pd_not_verified', '!pd_ok', END('pd_not_verified_end', { outcome: 'unresolved', reason: 'provide_data_not_verified', recovery: { version: 1, mode: 'none', reason: 'provide_data_not_verified' } })),
|
|
519
584
|
WHEN('pd_load', 'provides.source && provides.source.flow', FLOW('pd_load_flow', 'load-source', 'call', { source: '{{provides.source}}' })),
|
|
520
585
|
FLOW('pd_answer', 'answer-facts', 'call', { provides: '{{provides}}', keys: '{{pd_keys}}' }),
|
|
521
586
|
WHEN('pd_deferred_more', 'pd_deferred.remaining.length > 0', FLOW('pd_answer_deferred', 'provide-queued-data', 'call', { form: '{{form}}', requests: '{{pd_deferred.remaining}}' })),
|
|
@@ -584,7 +649,7 @@ const loadSource = {
|
|
|
584
649
|
WHEN('ls_begin', 'ls_stale', SIDE('ls_clear_old', 'beginSourceRefresh(cargo, source)')),
|
|
585
650
|
WHEN('ls_load', 'ls_stale', FLOW('ls_loader', '{{source.flow}}', 'call')),
|
|
586
651
|
SET('ls_loaded', 'ls_have', 'ls_stale ? completeSourceRefresh(cargo, source) : true'),
|
|
587
|
-
WHEN('ls_failed', '!ls_have', FLOW('ls_give_up', 'give-up', 'reboot', { context: 'your request', context_es: 'su solicitud' })),
|
|
652
|
+
WHEN('ls_failed', '!ls_have', { ...FLOW('ls_give_up', 'give-up', 'reboot', { context: 'your request', context_es: 'su solicitud' }), recovery: { version: 1, mode: 'none', reason: 'source_unavailable' } }),
|
|
588
653
|
],
|
|
589
654
|
};
|
|
590
655
|
const answerFacts = {
|
|
@@ -644,8 +709,9 @@ const resumeForm = { id: 'resume-form', name: 'ResumeForm', version: '1.0.0', sc
|
|
|
644
709
|
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 || ''"),
|
|
645
710
|
FLOW('resume_owned_form', '{{target}}', 'replace', { form_instance: '{{form}}', parent_form: '{{parent}}', result_input: '{{slot}}' })
|
|
646
711
|
] };
|
|
647
|
-
const LIBRARY_FLOWS = [resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
|
|
712
|
+
const LIBRARY_FLOWS = [confirmInputProposal, proposalDeclined, resumeForm, performAction, sendTextAction, prepareInput, runForm, collectInput, validateBatch, accept, acceptedEffects, disambiguate, confirmForm, formDone, providerMenu, ...validators, ...fieldCollectors, ...subForms, provideData, provideQueuedData, loadSource, answerFacts];
|
|
648
713
|
const LIBRARY_CONTRACTS = {
|
|
714
|
+
'confirm-input-proposal': { role: 'collector', requires: [INPUT_PROPOSALS_REQUIREMENT] }, 'confirm-input-proposal-declined': { role: 'worker', requires: [INPUT_PROPOSALS_REQUIREMENT] },
|
|
649
715
|
'resume-form': { role: 'worker' },
|
|
650
716
|
'perform-action': { role: 'worker', tools: ['jsfe:action-claim', 'jsfe:action-complete'], result: { path: 'cargo.actionResult', type: 'object' } },
|
|
651
717
|
'send-text-action': { role: 'worker', result: { path: 'cargo.actionOutcome', type: 'object' } },
|
|
@@ -670,6 +736,6 @@ export const FLOWS = LIBRARY_FLOWS.map(flow => {
|
|
|
670
736
|
throw new Error('Shared worker needs an explicit contract: ' + flow.id);
|
|
671
737
|
if ((declaration.role === 'collector') !== (flow.metadata?.formProtocol === 2))
|
|
672
738
|
throw new Error('Shared collector contract mismatch: ' + flow.id);
|
|
673
|
-
return { ...flow, variables: { ...flow.variables, contract: { type: 'object', value: { version: 1, summary: flow.description,
|
|
739
|
+
return { ...flow, steps: proposalRuntimeSteps(flow.id, flow.steps, declaration.requires), variables: { ...flow.variables, contract: { type: 'object', value: { version: 1, summary: flow.description,
|
|
674
740
|
parametersFrom: 'parameters', ...declaration, ...(declaration.role === 'collector' ? { form: { inputs: 'inputs' } } : {}) } } } };
|
|
675
741
|
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export function proposalRuntimeSteps(id: any, steps: any, requirements?: any[]): any;
|
|
2
|
+
/** Versioned engine support; declaring proposal consent must never become a no-op on older runtimes. */
|
|
3
|
+
export const INPUT_PROPOSALS_REQUIREMENT: "jsfe:input-proposals:v1";
|
|
4
|
+
export function hasInputProposals(inputs: any): any;
|
|
5
|
+
//# sourceMappingURL=proposal-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposal-runtime.d.ts","sourceRoot":"","sources":["../../src/forms/proposal-runtime.js"],"names":[],"mappings":"AAEA,qFAIC;AAND,wGAAwG;AACxG,0CAA2C,yBAAyB,CAAC;AAM9D,oDAA4K"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Versioned engine support; declaring proposal consent must never become a no-op on older runtimes. */
|
|
2
|
+
export const INPUT_PROPOSALS_REQUIREMENT = 'jsfe:input-proposals:v1';
|
|
3
|
+
export function proposalRuntimeSteps(id, steps, requirements = []) {
|
|
4
|
+
if (!requirements.includes(INPUT_PROPOSALS_REQUIREMENT))
|
|
5
|
+
return steps;
|
|
6
|
+
const guard = { id: id + '_proposal_protocol', type: 'SET', variable: 'noop', value: 'formProposalProtocol() === 1', requireTrue: true };
|
|
7
|
+
return steps[0]?.type === 'SET' && steps[0]?.value === guard.value && steps[0]?.requireTrue === true ? steps : [guard, ...steps];
|
|
8
|
+
}
|
|
9
|
+
export const hasInputProposals = inputs => (inputs || []).some(input => input.proposal !== undefined || hasInputProposals(input.any_of) || hasInputProposals(input.schema?.inputs));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function clearFormMisses(cargo: any, id: any, input: any): boolean;
|
|
2
|
+
export function missFormInput(cargo: any, id: any, input: any): any;
|
|
3
|
+
/** Explicit retry wins. Otherwise acknowledge only an actual unrecognized answer, once per re-ask. */
|
|
4
|
+
export function formInputPrompt(cargo: any, id: any, input: any, spec: any, data?: {}, language?: string): any;
|
|
5
|
+
export function consumeFormFeedback(cargo: any, id: any, input: any): boolean;
|
|
6
|
+
/** Recovery refers to the actual owning root, and never permits a fresh attempt at an unknown action. */
|
|
7
|
+
export function formConversationOutcome(cargo: any, id: any, reason: any, failure?: boolean, recoveryMode?: string): {
|
|
8
|
+
outcome: any;
|
|
9
|
+
reason: any;
|
|
10
|
+
recovery: {
|
|
11
|
+
version: number;
|
|
12
|
+
mode: string;
|
|
13
|
+
reason: any;
|
|
14
|
+
form?: undefined;
|
|
15
|
+
flow?: undefined;
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
outcome: any;
|
|
19
|
+
reason: any;
|
|
20
|
+
recovery: {
|
|
21
|
+
version: number;
|
|
22
|
+
mode: string;
|
|
23
|
+
reason: any;
|
|
24
|
+
form: any;
|
|
25
|
+
flow: any;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=form-conversation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-conversation.d.ts","sourceRoot":"","sources":["../../src/helpers/form-conversation.js"],"names":[],"mappings":"AAGA,0EAGC;AACD,oEAGC;AACD,sGAAsG;AACtG,+GAMC;AACD,8EAAwI;AAExI,yGAAyG;AACzG;;;;;;;;;;;;;;;;;;;;EAUC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Shared FORM retry, contextual yield and recovery policy; all state is conversation-owned. */
|
|
2
|
+
import { formIncompleteOutcome } from './form-state.js';
|
|
3
|
+
export function clearFormMisses(cargo, id, input) {
|
|
4
|
+
const state = cargo.forms?.[id];
|
|
5
|
+
if (!state)
|
|
6
|
+
return true;
|
|
7
|
+
state.misses ||= {};
|
|
8
|
+
state.feedback ||= {};
|
|
9
|
+
delete state.misses[input];
|
|
10
|
+
delete state.feedback[input];
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
export function missFormInput(cargo, id, input) {
|
|
14
|
+
const state = cargo.forms?.[id];
|
|
15
|
+
if (!state)
|
|
16
|
+
throw new Error('Cannot record a miss without its owned form');
|
|
17
|
+
state.misses ||= {};
|
|
18
|
+
state.feedback ||= {};
|
|
19
|
+
state.misses[input] = Number(state.misses[input] || 0) + 1;
|
|
20
|
+
state.feedback[input] = 'miss';
|
|
21
|
+
return state.misses[input];
|
|
22
|
+
}
|
|
23
|
+
/** Explicit retry wins. Otherwise acknowledge only an actual unrecognized answer, once per re-ask. */
|
|
24
|
+
export function formInputPrompt(cargo, id, input, spec, data = {}, language = 'en') {
|
|
25
|
+
const es = language === 'es', feedback = cargo.forms?.[id]?.feedback?.[input];
|
|
26
|
+
const question = spec.question_from && (es ? (data[spec.question_from + '_es'] || data[spec.question_from]) : data[spec.question_from]) || (es ? (spec.question_es || spec.question) : spec.question) || '';
|
|
27
|
+
const retry = es ? spec.retry_es : spec.retry;
|
|
28
|
+
if (feedback && retry)
|
|
29
|
+
return retry;
|
|
30
|
+
return (feedback === 'miss' ? (es ? 'Disculpe, no le entendí. ' : "Sorry, I didn't catch that. ") : '') + question;
|
|
31
|
+
}
|
|
32
|
+
export function consumeFormFeedback(cargo, id, input) { if (cargo.forms?.[id]?.feedback)
|
|
33
|
+
delete cargo.forms[id].feedback[input]; return true; }
|
|
34
|
+
/** Recovery refers to the actual owning root, and never permits a fresh attempt at an unknown action. */
|
|
35
|
+
export function formConversationOutcome(cargo, id, reason, failure = false, recoveryMode = 'resume') {
|
|
36
|
+
const completed = formIncompleteOutcome(cargo, id, { outcome: 'unresolved', reason });
|
|
37
|
+
if (completed.owner && ['resolved', 'deflected'].includes(completed.outcome))
|
|
38
|
+
return { outcome: completed.outcome, reason: completed.reason, recovery: { version: 1, mode: 'none', reason: completed.reason } };
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
let state = cargo.forms?.[id], root = state;
|
|
41
|
+
while (state && !seen.has(state.id)) {
|
|
42
|
+
seen.add(state.id);
|
|
43
|
+
root = state;
|
|
44
|
+
state = cargo.forms?.[state.parent];
|
|
45
|
+
}
|
|
46
|
+
if (!root)
|
|
47
|
+
return { outcome: 'unresolved', reason, recovery: { version: 1, mode: 'none', reason } };
|
|
48
|
+
const belongs = form => { const visited = new Set(); while (form && !visited.has(form.id)) {
|
|
49
|
+
if (form.id === root.id)
|
|
50
|
+
return true;
|
|
51
|
+
visited.add(form.id);
|
|
52
|
+
form = cargo.forms?.[form.parent];
|
|
53
|
+
} return false; };
|
|
54
|
+
const unknown = Object.values(cargo.forms || {}).filter(belongs).some(form => Object.values(form.actions || {}).some(record => ['attempted', 'unknown'].includes(record.status)));
|
|
55
|
+
const mode = unknown ? 'hold' : recoveryMode, why = unknown ? 'action_result_unknown' : completed.owner ? completed.reason : reason;
|
|
56
|
+
return { outcome: unknown ? 'unresolved' : completed.owner ? completed.outcome : failure ? 'unresolved' : 'pending', reason: why, recovery: { version: 1, mode, reason: why, form: root.id, flow: root.definition } };
|
|
57
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Static policy selectors use this batch's proposed values; a validator's explicit descriptor independently applies. */
|
|
2
|
+
export function formProposalPolicy(cargo: any, reading: any, check: any, accepted?: any[]): any;
|
|
3
|
+
/** A null descriptor admits ordinary input, but cannot silently waive the same retained candidate's consent. */
|
|
4
|
+
export function stageFormProposal(cargo: any, reading: any, descriptor: any): any;
|
|
5
|
+
/** Only actionable records enter the confirmation worker; dependency-deferred records await the normal batch. */
|
|
6
|
+
export function nextFormProposal(cargo: any, owner: any): any;
|
|
7
|
+
/** Owner replay issues approval only for confirmed records; named child refresh never invents approval. */
|
|
8
|
+
export function formProposalReadings(cargo: any, owner: any, input?: string, approve?: boolean): any[];
|
|
9
|
+
/** Bind only a real context child; ordinary child slots and unrelated owners cannot collect consent. */
|
|
10
|
+
export function bindFormProposal(cargo: any, owner: any, input: any, childId: any): any;
|
|
11
|
+
/** Exact YES must have passed the ordinary confirm-form revision check; NO never admits the candidate. */
|
|
12
|
+
export function decideFormProposal(cargo: any, owner: any, input: any, childId: any, decision: any): boolean;
|
|
13
|
+
/** Validate the entire final batch before consuming any proof; invoke immediately before acceptFormReadings. */
|
|
14
|
+
export function commitFormProposals(cargo: any, accepted?: any[]): boolean;
|
|
15
|
+
/** Failed validation withdraws the candidate; unmet prerequisites wait for the normal dependency replay. */
|
|
16
|
+
export function settleFormProposalValidation(cargo: any, batch?: {}): {};
|
|
17
|
+
//# sourceMappingURL=form-proposals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-proposals.d.ts","sourceRoot":"","sources":["../../src/helpers/form-proposals.js"],"names":[],"mappings":"AAgEA,yHAAyH;AACzH,gGASC;AACD,gHAAgH;AAChH,kFAkCC;AACD,iHAAiH;AACjH,8DAOC;AACD,2GAA2G;AAC3G,uGAYC;AACD,wGAAwG;AACxG,wFAMC;AACD,0GAA0G;AAC1G,6GASC;AACD,gHAAgH;AAChH,2EAmBC;AACD,4GAA4G;AAC5G,yEAIC"}
|