jsfe 1.37.1 → 1.38.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.
@@ -366,7 +366,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
366
366
  c.then({ label: `${i.name}${i.label ? ' (' + i.label + ')' : ''}${extra ? ': ' + extra : ''}`, kind: 'input' });
367
367
  }
368
368
  if (fm.confirm) {
369
- c.then({ label: `confirm (${confirmationFirmness(performs)})`, kind: 'decision' });
369
+ c.then({ label: `confirm (${['firm', 'soft'].includes(fm.confirm?.confirmation) ? fm.confirm.confirmation : confirmationFirmness(performs)})`, kind: 'decision' });
370
370
  }
371
371
  else { /* no confirmation */ }
372
372
  if (fm.after && fm.after.length) {
@@ -137,7 +137,7 @@ export function defineProvider({ provides, contract, parameters = [], variables
137
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
- const flow = formFlow({ ...spec, confirm_soft: false, ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
140
+ const flow = formFlow({ ...spec, confirm_soft: confirmationFirmness([]) === 'soft', ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
141
141
  return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' }, ...(provides ? { catalog: 'provides' } : {}) });
142
142
  }
143
143
  export function definePerformer({ performs, contract, prepare, present, project = [], provides, ...spec }) {
@@ -2,7 +2,7 @@
2
2
  * The form shell: declare inputs, run the loop, hand the result on. `after` = the form's own steps once complete.
3
3
  * `confirm` ({ question, question_es } — SAY-style templates that may read cargo.forms[<id>].values; build them in
4
4
  * `before_confirm` SET steps when they need computing): the confirmation, asked every time after the loop and
5
- * never an input. It is firm unless `confirm_soft` (definePerformer: no action it authorizes declares `confirm.exact`): a soft
5
+ * never an input. It is soft unless `confirm_soft` is false (confirmationFirmness: firm only for a payment): a soft
6
6
  * confirmation's YES is read as a soft input confirmation's and is offered to the reply ranker; a firm one's is neither. YES runs `after`; NO runs `declined`; a correction given there goes through the form's own matching
7
7
  * and the form runs again (so whatever the correction made missing is asked).
8
8
  */
@@ -13,8 +13,9 @@
13
13
  */
14
14
  /** rank_parameters and a choice's parameters: [{ name, description, type?, reads? }] (reads: the input the value answers). */
15
15
  export function validateRankParameters(id: any, name: any, what: any, list: any): void;
16
- /** How firm a form's confirmation is: firm when an action it authorizes declares confirm.exact (charge, cancel, irreversible)
17
- * or it authorizes none (a plain form); soft when it authorizes actions and none is exact (owner 2026-11-08). */
16
+ /** How firm a form's confirmation is: firm ONLY when an action it authorizes is a payment (effect 'charge'); every other
17
+ * confirmation is soft, a plain form's (no action) included (owner 2026-11-09). confirm.exact is separate: it binds the
18
+ * action to the confirmed values, whatever the confirmation's firmness. */
18
19
  export function confirmationFirmness(performs: any): "firm" | "soft";
19
20
  export function formFlow({ id, name, version, description, inputs, after, extraVars, parameters, confirm, confirm_soft, before_confirm, declined, stop, close_on_stop, on_no, resume_on_parent_change, before, done, primary, scope, validator, confirmation_condition, prompt, prompt_es, incomplete_outcome, on_exhausted, must_complete, first_task, first_task_es, acknowledge }: {
20
21
  id: any;
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6GA;;;;;;;GAOG;AACH;;;;GAIG;AACH,8HAA8H;AAC9H,uFAIC;AAiBD;iHACiH;AACjH,qEAEC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAolBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3yBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAogC9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6GA;;;;;;;GAOG;AACH;;;;GAIG;AACH,8HAA8H;AAC9H,uFAIC;AAiBD;;2EAE2E;AAC3E,qEAEC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqEC;AACD;;;GAGG;AACH;;;;;;EAGC;AAslBD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AApzBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6gC9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -105,7 +105,7 @@ const FIELD_COLLECTORS = { 'collect-phone': 'phone', 'collect-email': 'email', '
105
105
  * The form shell: declare inputs, run the loop, hand the result on. `after` = the form's own steps once complete.
106
106
  * `confirm` ({ question, question_es } — SAY-style templates that may read cargo.forms[<id>].values; build them in
107
107
  * `before_confirm` SET steps when they need computing): the confirmation, asked every time after the loop and
108
- * never an input. It is firm unless `confirm_soft` (definePerformer: no action it authorizes declares `confirm.exact`): a soft
108
+ * never an input. It is soft unless `confirm_soft` is false (confirmationFirmness: firm only for a payment): a soft
109
109
  * confirmation's YES is read as a soft input confirmation's and is offered to the reply ranker; a firm one's is neither. YES runs `after`; NO runs `declined`; a correction given there goes through the form's own matching
110
110
  * and the form runs again (so whatever the correction made missing is asked).
111
111
  */
@@ -145,12 +145,13 @@ function validateAcceptanceDeclarations(id, inputs) {
145
145
  else { /* no invites, or a list of names */ }
146
146
  }
147
147
  }
148
- /** How firm a form's confirmation is: firm when an action it authorizes declares confirm.exact (charge, cancel, irreversible)
149
- * or it authorizes none (a plain form); soft when it authorizes actions and none is exact (owner 2026-11-08). */
148
+ /** How firm a form's confirmation is: firm ONLY when an action it authorizes is a payment (effect 'charge'); every other
149
+ * confirmation is soft, a plain form's (no action) included (owner 2026-11-09). confirm.exact is separate: it binds the
150
+ * action to the confirmed values, whatever the confirmation's firmness. */
150
151
  export function confirmationFirmness(performs) {
151
- return Array.isArray(performs) && performs.length && !performs.some((action) => action?.confirm?.exact) ? 'soft' : 'firm';
152
+ return Array.isArray(performs) && performs.some((action) => action?.effect === 'charge') ? 'firm' : 'soft';
152
153
  }
153
- export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, confirm_soft = false, before_confirm = [], declined = [], stop = '', close_on_stop = false, on_no = '', resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false, first_task = '', first_task_es = '', acknowledge = 'all' }) {
154
+ export function formFlow({ id, name, version, description, inputs, after = [], extraVars = {}, parameters = [], confirm = null, confirm_soft = confirmationFirmness([]) === 'soft', before_confirm = [], declined = [], stop = '', close_on_stop = false, on_no = '', resume_on_parent_change = false, before = [], done = true, primary = false, scope = 'isolated', validator = '', confirmation_condition = 'true', prompt = undefined, prompt_es = undefined, incomplete_outcome = null, on_exhausted = null, must_complete = false, first_task = '', first_task_es = '', acknowledge = 'all' }) {
154
155
  validateFormExhaustion(on_exhausted);
155
156
  if (acknowledge !== 'all' && acknowledge !== 'data')
156
157
  throw new Error(`FORM ${id}: acknowledge must be 'all' or 'data'`);
@@ -162,6 +163,12 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
162
163
  if (confirm && typeof confirm === 'object')
163
164
  validateRankParameters(id, 'confirm', 'rank_parameters', confirm.rank_parameters);
164
165
  else { /* no confirmation */ }
166
+ // confirm.confirmation ('firm' | 'soft', the attribute inputs declare) is definite when declared; a template ('{{…}}')
167
+ // is resolved at run time (an inferred value's descriptor); absent, confirm_soft (confirmationFirmness) decides.
168
+ if (confirm && typeof confirm === 'object' && confirm.confirmation !== undefined && !['firm', 'soft'].includes(confirm.confirmation) && !String(confirm.confirmation).includes('{{'))
169
+ throw new Error(`FORM ${id}: confirm.confirmation must be "firm" or "soft"`);
170
+ else { /* declared, or derived below */ }
171
+ const confirmFirmness = confirm && typeof confirm === 'object' && confirm.confirmation !== undefined ? confirm.confirmation : (confirm_soft === true ? 'soft' : 'firm');
165
172
  const incompleteFrom = incomplete_outcome && Object.hasOwn(incomplete_outcome, 'from');
166
173
  if (incompleteFrom) {
167
174
  if (Object.keys(incomplete_outcome).length !== 1 || typeof incomplete_outcome.from !== 'string' || !/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(incomplete_outcome.from) || !parameters.some(p => p.name === incomplete_outcome.from.split('.')[0] && p.type === 'object'))
@@ -174,7 +181,8 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
174
181
  // FLOW parameters are paths only: the question is built here, as a SAY-style template, then passed by name
175
182
  SET(`${id}_confirm_q`, 'form_confirm_q', textValue(confirm.question)),
176
183
  SET(`${id}_confirm_q_es`, 'form_confirm_q_es', textValue(confirm.question_es || confirm.question)),
177
- FLOW(`${id}_confirm`, 'confirm-form', 'call', { form: '{{form_instance}}', confirmation: confirm_soft === true ? 'soft' : 'firm', question: '{{form_confirm_q}}', question_es: '{{form_confirm_q_es}}', phrases: '{{form_confirmation.phrases}}', words: '{{form_confirmation.words}}', clarifications: '{{form_confirmation.clarifications}}', no_keys: '{{form_confirmation.no_keys}}', interrupts: '{{form_confirmation.interrupts}}', invites: '{{form_confirmation.invites}}', named_fields: '{{form_confirmation.named_fields}}', reply_context: '{{form_confirmation.reply_context}}', rank_parameters: '{{form_confirmation.rank_parameters}}', exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
184
+ SET(`${id}_confirm_firmness`, 'form_confirm_firmness', textValue(confirmFirmness)),
185
+ FLOW(`${id}_confirm`, 'confirm-form', 'call', { form: '{{form_instance}}', confirmation: '{{form_confirm_firmness}}', question: '{{form_confirm_q}}', question_es: '{{form_confirm_q_es}}', phrases: '{{form_confirmation.phrases}}', words: '{{form_confirmation.words}}', clarifications: '{{form_confirmation.clarifications}}', no_keys: '{{form_confirmation.no_keys}}', interrupts: '{{form_confirmation.interrupts}}', invites: '{{form_confirmation.invites}}', named_fields: '{{form_confirmation.named_fields}}', reply_context: '{{form_confirmation.reply_context}}', rank_parameters: '{{form_confirmation.rank_parameters}}', exit_context: '{{exit_context}}', exit_context_es: '{{exit_context_es}}' }),
178
186
  { id: `${id}_confirmed`, type: 'CASE', branches: {
179
187
  [`condition: (cargo.formConfirm || {})[form_instance] === 'changed'`]: FLOW(`${id}_again`, id, 'replace', { parent_form: '{{parent_form}}', result_input: '{{result_input}}', form_instance: '{{form_instance}}' }),
180
188
  // NO: the declined flow takes the form's place, so the action below never runs
@@ -184,7 +192,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
184
192
  ] : [];
185
193
  return {
186
194
  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 } : {}),
187
- variables: vars({ form_on_exhausted: on_exhausted, form_on_no: on_no, form_must_complete: must_complete, 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 }),
195
+ variables: vars({ form_on_exhausted: on_exhausted, form_on_no: on_no, form_must_complete: must_complete, 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_confirm_firmness: '', form_confirmation: confirm || {}, form_should_confirm: false, ...extraVars }),
188
196
  steps: [
189
197
  ...contextualResponseRuntimeSteps(id, [], hasFormResponse(inputs) ? [CONTEXTUAL_RESPONSES_REQUIREMENT] : []),
190
198
  ...(hasFormResponse(inputs) ? [{ ...SET(`${id}_response_reader_required`, 'noop', "typeof validateFormResponse === 'function'"), requireTrue: true }] : []),
@@ -684,12 +692,14 @@ const disambiguate = {
684
692
  // ── confirm-form: the exact confirmation of a finished form; a correction is matched against the form chain ───────
685
693
  const confirmForm = {
686
694
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
687
- description: "Worker: ask a form's confirmation (YES / NO, keys 1 and 2; firm when an action it authorizes is exact; otherwise soft: YES is read as a soft confirmation's and offered to the reply ranker). 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 answer with words it cannot read goes to the reply ranker (read-confirm-reply); an unclear one is asked again, and after two the give-up worker takes over. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
695
+ description: "Worker: ask a form's confirmation (YES / NO, keys 1 and 2; firm only when an action it authorizes is a payment; otherwise soft: YES is read as a soft confirmation's and offered to the reply ranker). 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 answer with words it cannot read goes to the reply ranker (read-confirm-reply); an unclear one is asked again, and after two the give-up worker takes over. Result: cargo.formConfirm[form] = 'yes' | 'no' | 'changed'. Params: form, question(_es), exit_context(_es).",
688
696
  parameters: ['form', 'confirmation', 'question', 'question_es', 'exit_context', 'exit_context_es', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites', 'rank_parameters'].map((n) => ({ name: n, type: ['phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'invites', 'rank_parameters'].includes(n) ? 'array' : 'string', description: n })).concat([{ name: 'named_fields', type: 'boolean', description: "true: an answer that names one of the form's inputs (its names) without a value re-asks only that input" }]),
689
697
  variables: vars({ confirmation: '', cf_clarify: null, named_fields: false, reply_context: '', rank_parameters: [], cf_must: false, 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, phrases: null, words: [], clarifications: [], no_keys: ['2'], interrupts: [], invites: [], cf_choices: [], cf_event: '', cf_amount: false, cf_questions: [], cf_provides: null, cf_skip_gate: '' }),
690
698
  steps: [
691
699
  SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', cargo.confirmRanked = '', true)"),
692
- SET('cf_choices', 'cf_choices', `[{key:'yes',phrases:['1'].concat(Array.isArray(phrases)?phrases:Array.isArray(words)?words:[]).concat(confirmation==='soft'?${JSON.stringify(ANSWER_WORDS.yes)}:[]),exact:confirmation!=='soft'},{key:'no',phrases:(Array.isArray(no_keys)?no_keys:['2']).concat(${JSON.stringify(ANSWER_WORDS.no)}),exact:true}].concat((Array.isArray(interrupts)?interrupts:[]).map(function(event){return {key:'event:'+event.key,phrases:event.phrases ?? event.words,exact:event.exact!==false};}))`),
700
+ // YES matches only as the whole answer, firm or soft (as an input confirmation's): soft widens the words and offers the
701
+ // ranker YES; the flow's own YES phrases inside a longer sentence ("quiero cancelar el club, no la cuenta") never confirm.
702
+ SET('cf_choices', 'cf_choices', `[{key:'yes',phrases:['1'].concat(Array.isArray(phrases)?phrases:Array.isArray(words)?words:[]).concat(confirmation==='soft'?${JSON.stringify(ANSWER_WORDS.yes)}:[]),exact:true},{key:'no',phrases:(Array.isArray(no_keys)?no_keys:['2']).concat(${JSON.stringify(ANSWER_WORDS.no)}),exact:true}].concat((Array.isArray(interrupts)?interrupts:[]).map(function(event){return {key:'event:'+event.key,phrases:event.phrases ?? event.words,exact:event.exact!==false};}))`),
693
703
  SET('cf_initial_chain', 'cf_chain', CHAIN_JS),
694
704
  SET('cf_amount_context', 'cf_amount', "cf_chain.some(function(input){return input.profile && input.profile.kind==='amount';})"),
695
705
  SET('cf_prompt', 'cf_prompt', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'en')"),
@@ -752,7 +762,7 @@ const confirmInputProposal = formFlow({ id: 'confirm-input-proposal', name: 'Con
752
762
  { ...SIDE('proposal_bind', '(bindFormProposal(cargo,parent_form,input,form_instance),true)'), requireTrue: true },
753
763
  SET('proposal_confirmation', 'form_confirmation', 'Object.assign({},proposal.descriptor)'),
754
764
  ],
755
- confirm: { question: '{{proposal.descriptor.question}}', question_es: '{{proposal.descriptor.question_es || proposal.descriptor.question}}' },
765
+ confirm: { question: '{{proposal.descriptor.question}}', question_es: '{{proposal.descriptor.question_es || proposal.descriptor.question}}', confirmation: "{{proposal.descriptor.confirmation || 'soft'}}" },
756
766
  after: [{ ...SIDE('proposal_confirmed', "decideFormProposal(cargo,parent_form,input,form_instance,'confirmed')"), requireTrue: true }],
757
767
  declined: [NOOP('proposal_declined_marker')],
758
768
  });
@@ -1 +1 @@
1
- {"version":3,"file":"proposal-contract.d.ts","sourceRoot":"","sources":["../../src/helpers/proposal-contract.js"],"names":[],"mappings":"AAMA,uEAcC;AAUD,4DAWC;AACD,gEAIC;AACD,iGAAiG;AACjG,+EASC;AAzDM,+CAAoC"}
1
+ {"version":3,"file":"proposal-contract.d.ts","sourceRoot":"","sources":["../../src/helpers/proposal-contract.js"],"names":[],"mappings":"AAMA,uEAcC;AAWD,4DAWC;AACD,gEAIC;AACD,iGAAiG;AACjG,+EASC;AA1DM,+CAAoC"}
@@ -26,7 +26,7 @@ export function validateProposalData(value, seen = new Set()) {
26
26
  seen.delete(value);
27
27
  return value;
28
28
  }
29
- const confirmationKeys = ['question', 'question_es', 'binding', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'on_declined'];
29
+ const confirmationKeys = ['question', 'question_es', 'confirmation', 'binding', 'phrases', 'words', 'interrupts', 'no_keys', 'clarifications', 'on_declined'];
30
30
  function confirmation(value, required) {
31
31
  if (required && !string(value.question))
32
32
  fail('needs a nonempty question');
@@ -35,6 +35,8 @@ function confirmation(value, required) {
35
35
  fail(key + ' must be nonempty text');
36
36
  if (value.on_declined !== undefined && !safeName(value.on_declined))
37
37
  fail('on_declined needs a static worker id');
38
+ if (value.confirmation !== undefined && !['firm', 'soft'].includes(value.confirmation))
39
+ fail('confirmation must be "firm" or "soft"');
38
40
  for (const key of ['phrases', 'words', 'no_keys', 'clarifications'])
39
41
  if (value[key] !== undefined && (!Array.isArray(value[key]) || !value[key].every(string)))
40
42
  fail(key + ' must be an array of nonempty phrases');
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:452fe6731c7d9d4b43d0961d3eb33a429f0d3c2d7392acb968f4cbda7f514c95";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:d01edb5b556d1bc3c8825df569b295adf6b359da2293ce9ce009bab01ea7e2b5";
@@ -339,9 +339,10 @@ it.
339
339
  Some options are **never offered**, so the ranker cannot pick them: the YES of a firm confirmation (an input's, or a form's
340
340
  confirmation of an exact action), a `current_only` choice while another question is asked, and text. `exact` on a choice says how words
341
341
  match it, not what it does, so exact choices are offered. A charge or a cancellation is guarded by `confirm: {exact: true}`
342
- on its action, whose form confirmation offers the ranker NO, corrections and a person: a ranker can understand "no thanks,
343
- maybe later" as NO; it can never produce a YES to a payment. A form whose actions are none of them exact (filing a request)
344
- confirms softly: its YES is read as a soft confirmation's and is offered to the ranker too. Options that cost more when wrong need a higher score (a NO that would cancel what the
342
+ on its action. A form confirmation is firm only when it authorizes a payment (effect `charge`), unless the form declares
343
+ `confirm.confirmation`: a firm one offers the ranker NO, corrections and a person (a ranker can understand "no thanks,
344
+ maybe later" as NO; it can never produce a YES to a payment). Every other confirmation is soft: its YES is read as a
345
+ soft confirmation's and is offered to the ranker too. Options that cost more when wrong need a higher score (a NO that would cancel what the
345
346
  caller was confirming, or a new request that leaves the question unanswered). A reply that clears no option is
346
347
  `answers`, and the question is asked again if nothing usable was in it.
347
348
 
@@ -410,8 +411,9 @@ Cancellation and Concurrency" explains each rule and the production incident beh
410
411
 
411
412
  - **Choices.** A choice has a key, phrases in each language and, on a call, keypad keys. A key matches only as the
412
413
  whole answer. Sensitive choices are `exact`.
413
- - **Confirmations.** `confirmation: "firm"` for consent (a charge, a cancellation, storing a card):
414
- only confirmation words. `"soft"` for an offer ("Would you like a link?"): the wider YES vocabulary.
414
+ - **Confirmations.** One attribute, `confirmation: "firm" | "soft"`, on all three confirmations: an input's, a form's
415
+ `confirm`, and a proposal's policy. Declared, it is definite. Absent: firm only for a payment, soft otherwise (a
416
+ proposal is soft). Firm: only confirmation words. Soft: the wider YES vocabulary, and the reply ranker may pick YES.
415
417
  - **Proposals.** When the flow infers a value ("Is this the card ending in 4242?"), a proposal asks the caller to
416
418
  approve that one value. It never authorizes the final action.
417
419
  - **Questions in the middle of a task.** "When is it due?" asked while paying is answered from the provider's catalog,
@@ -214,7 +214,7 @@ Declarations:
214
214
 
215
215
  | Declaration | Meaning |
216
216
  | --- | --- |
217
- | input `confirmation:"firm"` / `"soft"` | A yes/no decision. Firm (consent to a charge, a cancellation, storing a card): YES only by `CONFIRM_WORDS` or the option's own exact phrases. Soft (an offer): the wider YES vocabulary. NO by the NO vocabulary or the option's own phrases. |
217
+ | `confirmation:"firm"` / `"soft"` (an input, a form's `confirm`, a proposal policy) | A yes/no decision; declared, it is definite; absent, firm only for a payment. Firm: YES only by `CONFIRM_WORDS` or the option's own exact phrases. Soft (an offer): the wider YES vocabulary. NO by the NO vocabulary or the option's own phrases. |
218
218
  | input `invites:[names]`, `form.confirm.invites` | The other inputs this question (or the read-back) also asks for ("NEW CARD, LINK, or a different amount" invites `amount`), in its form or an enclosing one; read as in order while it is asked. |
219
219
  | profile `out_of_order:{…}` | A narrower signature for out-of-order reading: the payment card takes a bare last four when asked, but out of order only a card word or "ending in 4242". |
220
220
  | `authenticate-user` `other_whole_answer:true` | At the code question "another account" switches only as the whole answer; inside a sentence the reply ranker decides, offered `another_account` and `no_other_account` ("I understand. We still need to secure access to your account.", then the question). Default off. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.37.1",
3
+ "version": "1.38.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",