jsfe 1.37.0 → 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.
- package/README.md +1 -1
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +4 -3
- package/dist/forms/contracts.d.ts.map +1 -1
- package/dist/forms/contracts.js +3 -4
- package/dist/forms/flows.d.ts +5 -1
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +22 -7
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.js +1 -1
- package/dist/helpers/proposal-contract.d.ts.map +1 -1
- package/dist/helpers/proposal-contract.js +3 -1
- package/dist/runtime-identity.js +1 -1
- package/docs/architecture-guide.md +7 -5
- package/docs/copilot-reference.md +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -387,7 +387,7 @@ means a reply that picks none of them. A picked choice sets `responseVariable` t
|
|
|
387
387
|
`answers` sets `{ key: '' }` unless `responseParameters` are declared too. The request carries the options it offers
|
|
388
388
|
(`request.options`); a host validates a reply against those, never a fixed list. The form readers use it: an unread
|
|
389
389
|
answer with any words goes to the ranker with the question's choices (`exact` ones too, since `exact` is how words match
|
|
390
|
-
a choice; never the YES of a firm confirmation
|
|
390
|
+
a choice; never the YES of a firm confirmation, which a form's confirmation is when an action it authorizes is `confirm.exact`; a NO always) and the escape every question supports, `__agent` (a person);
|
|
391
391
|
text is never offered. **Arguments in an unread answer** (jsfe 1.36): the asked input's `rank_parameters` (`[{ name, description, type?, reads? }]`) are extracted when the ranker says the reply answers the question, and a choice's own `parameters` when it is picked (`responseParameters` may name the array variable that holds them). Each value is read by the form's reader as if said alone: `reads` names the input it answers, else it answers the asked question, so that question's rules decide a number's field. Two readings of one input that differ take nothing, and nothing read asks again, as before. A form's `confirm.rank_parameters` do the same at its confirmation (no `reads`: read as a correction there); a value read changes the form, which is confirmed again. A charge or a cancellation is guarded by the action's `confirm: {exact: true}`, not by the choice's
|
|
392
392
|
`exact`. A confirmation's exhausted attempts go to the `give-up` worker.
|
|
393
393
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;EA+RC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { resolveSetDataReferences } from '../forms/declarations.js';
|
|
18
|
+
import { confirmationFirmness } from '../forms/flows.js';
|
|
18
19
|
export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log = console.log }) {
|
|
19
20
|
// A catalog or value written as {"$data": path} is documented as compilation resolves it.
|
|
20
21
|
const SETS = sets.map((s) => ({ ...s, flows: resolveSetDataReferences(s.flows) }));
|
|
@@ -354,7 +355,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
354
355
|
else { /* no menu */ }
|
|
355
356
|
return c.done();
|
|
356
357
|
}
|
|
357
|
-
function formChart(fm) {
|
|
358
|
+
function formChart(fm, performs) {
|
|
358
359
|
const c = builder();
|
|
359
360
|
if (fm.before && fm.before.length) {
|
|
360
361
|
c.steps(fm.before);
|
|
@@ -365,7 +366,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
365
366
|
c.then({ label: `${i.name}${i.label ? ' (' + i.label + ')' : ''}${extra ? ': ' + extra : ''}`, kind: 'input' });
|
|
366
367
|
}
|
|
367
368
|
if (fm.confirm) {
|
|
368
|
-
c.then({ label:
|
|
369
|
+
c.then({ label: `confirm (${['firm', 'soft'].includes(fm.confirm?.confirmation) ? fm.confirm.confirmation : confirmationFirmness(performs)})`, kind: 'decision' });
|
|
369
370
|
}
|
|
370
371
|
else { /* no confirmation */ }
|
|
371
372
|
if (fm.after && fm.after.length) {
|
|
@@ -414,7 +415,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
414
415
|
}
|
|
415
416
|
else { /* not a provider */ }
|
|
416
417
|
if (flow.form) {
|
|
417
|
-
out.push('', '### Form', '', formChart(flow.form));
|
|
418
|
+
out.push('', '### Form', '', formChart(flow.form, flow.performs));
|
|
418
419
|
}
|
|
419
420
|
else { /* not a form */ }
|
|
420
421
|
// A form the jsfe SDK compiled (the system set) carries its inputs as a variable; its steps are the engine's run-form loop.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EC"}
|
package/dist/forms/contracts.js
CHANGED
|
@@ -5,7 +5,7 @@ import { INPUT_PROPOSALS_REQUIREMENT, hasInputProposals, proposalRuntimeSteps }
|
|
|
5
5
|
import { ACTION_TOOLS } from './action-tools.js';
|
|
6
6
|
import { normalizeHostRequirements } from '../host/capabilities.js';
|
|
7
7
|
import { validateActionProjection, actionProjectionExpression } from './action-projection.js';
|
|
8
|
-
import { formFlow, FIELD_PRESETS } from './flows.js';
|
|
8
|
+
import { formFlow, FIELD_PRESETS, confirmationFirmness } from './flows.js';
|
|
9
9
|
import { resolveFormInputs } from '../helpers/form-state.js';
|
|
10
10
|
const ROLES = new Set(['provider', 'performer', 'collector', 'gate', 'router', 'worker']);
|
|
11
11
|
const EFFECTS = new Set(['read', 'send', 'write', 'charge', 'cancel', 'handoff']);
|
|
@@ -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:
|
|
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 }) {
|
|
@@ -158,8 +158,7 @@ export function definePerformer({ performs, contract, prepare, present, project
|
|
|
158
158
|
when('bad_request', "!(pf_action === null || pf_action && typeof pf_action === 'object' && pf_action.action) || pf_action !== null && (!pf_request || typeof pf_request !== 'object')", { id: `${spec.id}_prepare_failed`, type: 'FLOW', value: 'give-up', callType: 'reboot', recovery: { version: 1, mode: 'none', reason: 'performer_request_invalid' }, parameters: { context: '{{exit_context}}', context_es: '{{exit_context_es}}' } }),
|
|
159
159
|
set('proposal', 'pf_record', "pf_action ? prepareFormAction(cargo, form_instance, pf_action, pf_request) : null"),
|
|
160
160
|
];
|
|
161
|
-
|
|
162
|
-
const flow = formFlow({ ...spec, confirm, confirm_soft: !performs.some(action => action.confirm?.exact), confirmation_condition: "pf_action !== null && pf_action && typeof pf_action === 'object'",
|
|
161
|
+
const flow = formFlow({ ...spec, confirm, confirm_soft: confirmationFirmness(performs) === 'soft', confirmation_condition: "pf_action !== null && pf_action && typeof pf_action === 'object'",
|
|
163
162
|
extraVars: { ...spec.extraVars, ...(provides ? { provides, questions: providerQuestions(provides) } : {}), performs, projection: project, pf_action: null, pf_request: null, pf_record: null, pf_result: null, pf_receipt: null },
|
|
164
163
|
before_confirm: confirm ? [...prepareSteps, ...(spec.before_confirm || [])] : spec.before_confirm || [],
|
|
165
164
|
after: [...(confirm ? [] : prepareSteps),
|
package/dist/forms/flows.d.ts
CHANGED
|
@@ -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
|
|
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,6 +13,10 @@
|
|
|
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 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. */
|
|
19
|
+
export function confirmationFirmness(performs: any): "firm" | "soft";
|
|
16
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 }: {
|
|
17
21
|
id: any;
|
|
18
22
|
name: 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
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"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -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
|
|
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,7 +145,13 @@ function validateAcceptanceDeclarations(id, inputs) {
|
|
|
145
145
|
else { /* no invites, or a list of names */ }
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
|
|
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. */
|
|
151
|
+
export function confirmationFirmness(performs) {
|
|
152
|
+
return Array.isArray(performs) && performs.some((action) => action?.effect === 'charge') ? 'firm' : 'soft';
|
|
153
|
+
}
|
|
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' }) {
|
|
149
155
|
validateFormExhaustion(on_exhausted);
|
|
150
156
|
if (acknowledge !== 'all' && acknowledge !== 'data')
|
|
151
157
|
throw new Error(`FORM ${id}: acknowledge must be 'all' or 'data'`);
|
|
@@ -157,6 +163,12 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
157
163
|
if (confirm && typeof confirm === 'object')
|
|
158
164
|
validateRankParameters(id, 'confirm', 'rank_parameters', confirm.rank_parameters);
|
|
159
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');
|
|
160
172
|
const incompleteFrom = incomplete_outcome && Object.hasOwn(incomplete_outcome, 'from');
|
|
161
173
|
if (incompleteFrom) {
|
|
162
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'))
|
|
@@ -169,7 +181,8 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
169
181
|
// FLOW parameters are paths only: the question is built here, as a SAY-style template, then passed by name
|
|
170
182
|
SET(`${id}_confirm_q`, 'form_confirm_q', textValue(confirm.question)),
|
|
171
183
|
SET(`${id}_confirm_q_es`, 'form_confirm_q_es', textValue(confirm.question_es || confirm.question)),
|
|
172
|
-
|
|
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}}' }),
|
|
173
186
|
{ id: `${id}_confirmed`, type: 'CASE', branches: {
|
|
174
187
|
[`condition: (cargo.formConfirm || {})[form_instance] === 'changed'`]: FLOW(`${id}_again`, id, 'replace', { parent_form: '{{parent_form}}', result_input: '{{result_input}}', form_instance: '{{form_instance}}' }),
|
|
175
188
|
// NO: the declined flow takes the form's place, so the action below never runs
|
|
@@ -179,7 +192,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
|
|
|
179
192
|
] : [];
|
|
180
193
|
return {
|
|
181
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 } : {}),
|
|
182
|
-
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 }),
|
|
183
196
|
steps: [
|
|
184
197
|
...contextualResponseRuntimeSteps(id, [], hasFormResponse(inputs) ? [CONTEXTUAL_RESPONSES_REQUIREMENT] : []),
|
|
185
198
|
...(hasFormResponse(inputs) ? [{ ...SET(`${id}_response_reader_required`, 'noop', "typeof validateFormResponse === 'function'"), requireTrue: true }] : []),
|
|
@@ -679,12 +692,14 @@ const disambiguate = {
|
|
|
679
692
|
// ── confirm-form: the exact confirmation of a finished form; a correction is matched against the form chain ───────
|
|
680
693
|
const confirmForm = {
|
|
681
694
|
id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
|
|
682
|
-
description: "Worker: ask a form's confirmation (YES / NO, keys 1 and 2; firm when an action it authorizes is
|
|
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).",
|
|
683
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" }]),
|
|
684
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: '' }),
|
|
685
698
|
steps: [
|
|
686
699
|
SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', cargo.confirmRanked = '', true)"),
|
|
687
|
-
|
|
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};}))`),
|
|
688
703
|
SET('cf_initial_chain', 'cf_chain', CHAIN_JS),
|
|
689
704
|
SET('cf_amount_context', 'cf_amount', "cf_chain.some(function(input){return input.profile && input.profile.kind==='amount';})"),
|
|
690
705
|
SET('cf_prompt', 'cf_prompt', "formInputPrompt(cargo,form,'__confirm',{question:question,question_es:question_es},{},'en')"),
|
|
@@ -747,7 +762,7 @@ const confirmInputProposal = formFlow({ id: 'confirm-input-proposal', name: 'Con
|
|
|
747
762
|
{ ...SIDE('proposal_bind', '(bindFormProposal(cargo,parent_form,input,form_instance),true)'), requireTrue: true },
|
|
748
763
|
SET('proposal_confirmation', 'form_confirmation', 'Object.assign({},proposal.descriptor)'),
|
|
749
764
|
],
|
|
750
|
-
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'}}" },
|
|
751
766
|
after: [{ ...SIDE('proposal_confirmed', "decideFormProposal(cargo,parent_form,input,form_instance,'confirmed')"), requireTrue: true }],
|
|
752
767
|
declined: [NOOP('proposal_declined_marker')],
|
|
753
768
|
});
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { explainSession } from "./inspection.js";
|
|
|
5
5
|
export { continuationBreaks } from "./continuation-compat.js";
|
|
6
6
|
export { inheritFlowDeclaration } from "./declaration-capture.js";
|
|
7
7
|
export { withSafetyRevision, CONTINUATION_SAFETY_REQUIREMENT } from "./continuation-safety.js";
|
|
8
|
-
export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from "./flows.js";
|
|
8
|
+
export { FIELD_PRESETS, formFlow, declinedFlow, textValue, confirmationFirmness } from "./flows.js";
|
|
9
9
|
export { linkFormSchemas, defineWorker, defineProvider, defineForm, definePerformer, validateFlowContracts, operationWorkers, providerQuestions, providerParameter } from "./contracts.js";
|
|
10
10
|
export { addressDraft, zipLocation, streetLocation, resolvedAddress } from "../helpers/address.js";
|
|
11
11
|
export { Form, Provider, Performer, Worker, compileExpression } from "./authoring.js";
|
package/dist/forms/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { FLOWS as FORM_FLOWS } from './flows.js';
|
|
|
5
5
|
import { FLOWS as CHOICE_FLOWS } from './choice-flow.js';
|
|
6
6
|
import { FLOWS as IDENTITY } from './identity-flows.js';
|
|
7
7
|
export const FLOWS = [...FORM_FLOWS, ...ADDRESS_FLOWS, ...CHOICE_FLOWS, ...IDENTITY].map(flow => withSafetyRevision(flow, 'dispatch-intake-2026-10-22'));
|
|
8
|
-
export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from './flows.js';
|
|
8
|
+
export { FIELD_PRESETS, formFlow, declinedFlow, textValue, confirmationFirmness } from './flows.js';
|
|
9
9
|
export { FLOWS as IDENTITY_FLOWS } from './identity-flows.js';
|
|
10
10
|
export { linkFormSchemas, defineWorker, defineProvider, defineForm, definePerformer, validateFlowContracts, operationWorkers, providerQuestions, providerParameter } from './contracts.js';
|
|
11
11
|
export { compileFormPrompts } from './prompt-compiler.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proposal-contract.d.ts","sourceRoot":"","sources":["../../src/helpers/proposal-contract.js"],"names":[],"mappings":"AAMA,uEAcC;
|
|
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');
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
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"
|
|
414
|
-
|
|
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
|
-
|
|
|
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. |
|
|
@@ -347,7 +347,7 @@ A Performer requires `form`, nonempty `performs`, `prepare` worker and `present`
|
|
|
347
347
|
| `subject` | Ownership path such as `form.data.account` or `form.data.subject`; bind account/service identity, not an unrelated session flag. |
|
|
348
348
|
| `gate:{flow?,verified:[paths]}` | Execution-time verification; rerun the declared flow before each fresh effect. |
|
|
349
349
|
| `flow`, `tool` | Installed executor worker and/or registered tool identity. |
|
|
350
|
-
| `confirm:{exact:true}` | Required for charge, cancel or `reversible:false`. This requires a real form confirmation question. |
|
|
350
|
+
| `confirm:{exact:true}` | Required for charge, cancel or `reversible:false`. This requires a real form confirmation question, and makes it firm (YES only by `CONFIRM_WORDS` or its own exact phrases, never offered to the reply ranker). A performer none of whose actions is exact (filing a request) confirms softly: the wider YES vocabulary, and YES is offered to the ranker. There is no separate attribute for this. |
|
|
351
351
|
| `outcomes` | Map declared executor statuses to business outcomes, including unknown results. |
|
|
352
352
|
| `retry` | Explicit policies: `after:[knownStatuses]`; unknown retry only for SEND with `unknown:"explicit"` and a new accepted recovery choice. |
|
|
353
353
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "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",
|