jsfe 1.7.2 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAaA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAmCC;AAeD;;;QAEC;AACD;;;;;;;QAMC;AACD;;;QAGC;AACD;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsEC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAcA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAqCC;AAgBD;;;QAEC;AACD;;;;;;;QAMC;AACD;;;QAGC;AACD;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwEC"}
@@ -1,4 +1,5 @@
1
1
  /** Build-time contracts for conversational flows. Transport operations remain registered tools. */
2
+ import { INPUT_PROPOSALS_REQUIREMENT, hasInputProposals, proposalRuntimeSteps } from './proposal-runtime.js';
2
3
  import { ACTION_TOOLS } from './action-tools.js';
3
4
  import { normalizeHostRequirements } from '../host/capabilities.js';
4
5
  import { validateActionProjection, actionProjectionExpression } from './action-projection.js';
@@ -66,7 +67,9 @@ export function linkFormSchemas(flows, { externalFlows = [] } = {}) {
66
67
  parameters.push(input.schema ? { name: input.name, type: 'object', jsonSchema: objectSchema(input.schema.inputs), description: input.description || input.label || input.name }
67
68
  : { name: input.name, type: 'string', ...(input.profile.kind === 'choice' && !input.profile.accept_other ? { enum: input.profile.choices.map(c => c.key) } : {}), description: input.description || input.label || input.name });
68
69
  }
69
- return { ...flow, parameters, variables: { ...flow.variables, inputs: { ...flow.variables.inputs, value: inputs } } };
70
+ const linked = { ...flow, parameters, variables: { ...flow.variables, inputs: { ...flow.variables.inputs, value: inputs } } };
71
+ // Descendant schemas are linked after declaration; propagate their runtime requirement to the root entry.
72
+ return flow.variables?.contract ? declared(linked, flow.variables.contract.value) : { ...linked, steps: proposalRuntimeSteps(flow.id, linked.steps, hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []) };
70
73
  });
71
74
  }
72
75
  function declared(flow, contract) {
@@ -79,12 +82,13 @@ function declared(flow, contract) {
79
82
  ...(step.type === 'FLOW' && step.callType === 'replace' ? { initializeDefaults: step.initializeDefaults ?? true } : {}),
80
83
  ...(step.branches ? { branches: Object.fromEntries(Object.entries(step.branches).map(([key, branch]) => [key, replacementDefaults(branch)])) } : {}),
81
84
  ...(step.onFail ? { onFail: replacementDefaults(step.onFail) } : {}) });
85
+ const requirements = [...normalizeHostRequirements(contract.requires, flow.id), ...(hasInputProposals(flow.variables?.inputs?.value) ? [INPUT_PROPOSALS_REQUIREMENT] : [])];
82
86
  const metadata = { version: 1, ...contract, parametersFrom: 'parameters' };
83
- if (contract.requires !== undefined)
87
+ if (requirements.length || contract.requires !== undefined)
84
88
  Object.defineProperty(metadata, 'requires', {
85
- value: normalizeHostRequirements(contract.requires, flow.id), enumerable: true, writable: false, configurable: false,
89
+ value: normalizeHostRequirements(requirements, flow.id), enumerable: true, writable: false, configurable: false,
86
90
  });
87
- return { ...flow, steps: flow.steps.map(replacementDefaults), variables: { ...flow.variables, contract: value(metadata) } };
91
+ return { ...flow, steps: proposalRuntimeSteps(flow.id, flow.steps.map(replacementDefaults), requirements), variables: { ...flow.variables, contract: value(metadata) } };
88
92
  }
89
93
  export function defineWorker({ contract, ...flow }) {
90
94
  return declared({ scope: 'isolated', version: '1.0.0', ...flow }, contract);
@@ -187,10 +191,16 @@ export function validateFlowContracts(flows, tools = [], { externalFlows = [], r
187
191
  for (const name of deps)
188
192
  if (!name.startsWith('cargo.') && !name.startsWith('form.') && !fieldNames.has(name))
189
193
  fail(flow.id, `unknown dependency ${name}`);
194
+ for (const name of [...Object.keys(spec.proposal?.when || {}), ...(spec.proposal?.unless || [])])
195
+ if (!name.startsWith('cargo.') && !name.startsWith('form.') && !fieldNames.has(name))
196
+ fail(flow.id, `unknown proposal condition ${name}`);
190
197
  graph.set(spec.name, deps);
191
198
  for (const key of ['form', 'validator', 'prepare', 'refresh', 'after', 'answer_precheck', 'answer_validator', 'on_exhausted'])
192
199
  if (spec[key] && !ids.has(spec[key]))
193
200
  fail(flow.id, `unknown ${key} worker ${spec[key]}`);
201
+ for (const reference of [spec.proposal?.prepare, spec.proposal?.on_declined, ...(spec.proposal?.interrupts || []).map(event => event.flow)])
202
+ if (reference && !ids.has(reference))
203
+ fail(flow.id, `unknown proposal worker ${reference}`);
194
204
  }
195
205
  const visited = new Set(), active = new Set();
196
206
  const visit = name => { if (active.has(name))
@@ -417,8 +417,8 @@ export const FLOWS: ({
417
417
  };
418
418
  })[];
419
419
  } | {
420
+ steps: any;
420
421
  variables: any;
421
- steps: any[];
422
422
  prompt?: undefined;
423
423
  prompt_es?: undefined;
424
424
  primary?: boolean | undefined;
@@ -432,6 +432,7 @@ export const FLOWS: ({
432
432
  };
433
433
  parameters: any[];
434
434
  } | {
435
+ steps: any;
435
436
  variables: any;
436
437
  id: string;
437
438
  name: string;
@@ -442,136 +443,8 @@ export const FLOWS: ({
442
443
  type: string;
443
444
  description: string;
444
445
  }[];
445
- steps: ({
446
- id: any;
447
- type: string;
448
- variable: any;
449
- value: any;
450
- } | {
451
- parameters?: any;
452
- initializeDefaults?: boolean | undefined;
453
- id: any;
454
- type: string;
455
- value: any;
456
- callType: string;
457
- } | {
458
- id: any;
459
- type: string;
460
- branches: {
461
- [x: string]: any;
462
- default: {
463
- id: any;
464
- type: string;
465
- variable: any;
466
- value: any;
467
- };
468
- };
469
- } | {
470
- id: string;
471
- type: string;
472
- branches: {
473
- 'condition: rf_next_form': {
474
- parameters?: any;
475
- initializeDefaults?: boolean | undefined;
476
- id: any;
477
- type: string;
478
- value: any;
479
- callType: string;
480
- };
481
- default: {
482
- parameters?: any;
483
- initializeDefaults?: boolean | undefined;
484
- id: any;
485
- type: string;
486
- value: any;
487
- callType: string;
488
- };
489
- };
490
- })[];
491
- } | {
492
- variables: any;
493
- id: string;
494
- name: string;
495
- version: string;
496
- scope: string;
497
- description: string;
498
- parameters: {
499
- name: string;
500
- type: string;
501
- description: string;
502
- }[];
503
- steps: ({
504
- id: any;
505
- type: string;
506
- variable: any;
507
- value: any;
508
- } | {
509
- parameters?: any;
510
- initializeDefaults?: boolean | undefined;
511
- id: any;
512
- type: string;
513
- value: any;
514
- callType: string;
515
- } | {
516
- id: any;
517
- type: string;
518
- branches: {
519
- [x: string]: any;
520
- default: {
521
- id: any;
522
- type: string;
523
- variable: any;
524
- value: any;
525
- };
526
- };
527
- } | {
528
- id: string;
529
- type: string;
530
- branches: {
531
- 'condition: pm_keys.length > 0': {
532
- parameters?: any;
533
- initializeDefaults?: boolean | undefined;
534
- id: any;
535
- type: string;
536
- value: any;
537
- callType: string;
538
- };
539
- 'condition: pm_flow': {
540
- parameters?: any;
541
- initializeDefaults?: boolean | undefined;
542
- id: any;
543
- type: string;
544
- value: any;
545
- callType: string;
546
- };
547
- "condition: pm_key === 'done'": {
548
- id: string;
549
- type: string;
550
- value: string;
551
- value_es: string;
552
- outcome: string;
553
- };
554
- "condition: /[a-z\u00E1\u00E9\u00ED\u00F3\u00FA\u00FC\u00F1]/i.test(String(cargo.choiceText || ''))": {
555
- id: string;
556
- type: string;
557
- };
558
- "condition: retry !== '1'": {
559
- parameters?: any;
560
- initializeDefaults?: boolean | undefined;
561
- id: any;
562
- type: string;
563
- value: any;
564
- callType: string;
565
- };
566
- default: {
567
- id: string;
568
- type: string;
569
- value: string;
570
- value_es: string;
571
- };
572
- };
573
- })[];
574
446
  } | {
447
+ steps: any;
575
448
  variables: any;
576
449
  id: string;
577
450
  name: string;
@@ -582,30 +455,5 @@ export const FLOWS: ({
582
455
  name: string;
583
456
  type: string;
584
457
  }[];
585
- steps: ({
586
- id: any;
587
- type: string;
588
- variable: any;
589
- value: any;
590
- } | {
591
- parameters?: any;
592
- initializeDefaults?: boolean | undefined;
593
- id: any;
594
- type: string;
595
- value: any;
596
- callType: string;
597
- } | {
598
- id: any;
599
- type: string;
600
- branches: {
601
- [x: string]: any;
602
- default: {
603
- id: any;
604
- type: string;
605
- variable: any;
606
- value: any;
607
- };
608
- };
609
- })[];
610
458
  })[];
611
459
  //# sourceMappingURL=flows.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAyFA;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAoTD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAtdM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsoB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA0FA;;;;;;GAMG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DC;AACD;;;GAGG;AACH;;;;;;EAGC;AA8VD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAjgBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkrB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -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 });
@@ -114,6 +115,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
114
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 } : {}),
115
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 }),
116
117
  steps: [
118
+ ...proposalRuntimeSteps(id, [], hasInputProposals(inputs) ? [INPUT_PROPOSALS_REQUIREMENT] : []),
117
119
  SET(`${id}_resume_kind`, 'form_internal_resume', '!!form_instance'),
118
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 })`),
119
121
  SIDE(`${id}_task`, `(!parent_form ? (cargo.formTasks = cargo.formTasks || {}, cargo.formTasks[${JSON.stringify(id)}] = form_instance) : true, true)`),
@@ -131,7 +133,7 @@ export function formFlow({ id, name, version, description, inputs, after = [], e
131
133
  WHEN(`${id}_parent_resume`, "rf_status === 'parent_changed'", FLOW(`${id}_parent_reenter`, 'resume-form', 'reboot', { form: '{{form_resume_owner}}' })),
132
134
  ] : []),
133
135
  ...(close_on_stop ? [WHEN(`${id}_close_stopped`, "rf_status === 'stopped'", SIDE(`${id}_stop_close`, '(closeFormInstance(cargo, form_instance, false), true)'))] : []),
134
- 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`)),
135
137
  ...(validator ? [
136
138
  SET(`${id}_values`, 'form_values', 'cargo.forms[form_instance].values'),
137
139
  SIDE(`${id}_validation_reset`, '(cargo.formCheck = null, true)'),
@@ -166,7 +168,7 @@ const runForm = {
166
168
  id: 'run-form', name: 'RunForm', version: '1.0.0',
167
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)`.",
168
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' }],
169
- 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: [] }),
170
172
  steps: [
171
173
  SIDE('rf_pass_count', '(cargo.forms[rf_form] ? (cargo.forms[rf_form].replays = Number(cargo.forms[rf_form].replays || 0) + 1) : true, true)'),
172
174
  SET('rf_pass', 'rf_status', "formInstanceApplies(cargo, rf_form) ? '' : (closeFormInstance(cargo, rf_form, false), 'unwound')"),
@@ -182,6 +184,12 @@ const runForm = {
182
184
  SET('rf_pending', 'rf_pending', 'pendingFormReadings(cargo, rf_form)'),
183
185
  WHEN('rf_pending_accept', 'rf_pending.length > 0', FLOW('rf_pending_batch', 'collect-input-accept', 'call', { hits: '{{rf_pending}}' })),
184
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}}' })),
185
193
  SET('rf_find', 'rf_next', 'nextFormInput(cargo.forms[rf_form], cargo)'),
186
194
  WHEN('rf_blocked', "rf_next === '__blocked__'", incompleteDispatch('rf_blocked_end', 'rf_form', "'form_dependencies_blocked'")),
187
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 || '' : ''"),
@@ -275,14 +283,15 @@ const prepareInput = {
275
283
  const validateBatch = {
276
284
  id: 'validate-input-batch', name: 'ValidateInputBatch', version: '2.0.0', scope: 'isolated',
277
285
  description: 'Worker: validate dependency-ordered candidates; return accepted, rejected and pending readings before a single commit.',
278
- parameters: ['hits', 'accepted', 'rejected', 'pending'].map(name => ({ name, type: 'array' })),
279
- 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 }),
280
288
  steps: [
281
289
  SET('vb_init_accepted', 'accepted', 'Array.isArray(accepted) ? accepted : []'),
282
290
  SET('vb_init_rejected', 'rejected', 'Array.isArray(rejected) ? rejected : []'),
283
291
  SET('vb_init_pending', 'pending', 'Array.isArray(pending) ? pending : []'),
292
+ SET('vb_init_ignored', 'ignored', 'Array.isArray(ignored) ? ignored : []'),
284
293
  SET('vb_first', 'vb_h', 'hits.length ? hits[0] : null'),
285
- 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)')),
286
295
  WHEN('vb_end', '!vb_h', END('vb_end_done')),
287
296
  SET('vb_rest', 'vb_rest', 'hits.slice(1)'),
288
297
  SET('vb_relevant', 'vb_relevant', 'formReadingRelevant(cargo, vb_h, accepted)'),
@@ -294,8 +303,15 @@ const validateBatch = {
294
303
  SIDE('vb_reset', "(cargo.inputCheck = vb_h.invalid ? { ok: false } : (vb_h.validator && !vb_known ? null : { ok: true, value: vb_h.value }), true)"),
295
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}}' })),
296
305
  WHEN('vb_refused', 'vb_h.refused === true', SIDE('vb_withhold_refusal', "(cargo.inputCheck=Object.assign({},cargo.inputCheck || {},{ok:false}),true)")),
297
- SIDE('vb_take', "(vb_relevant !== true ? true : vb_ready !== true ? pending.push(Object.assign({}, vb_h, {outcome:'pending'})) : cargo.inputCheck && cargo.inputCheck.ok === true ? accepted.push(Object.assign({}, vb_h, { ok: true, value: cargo.inputCheck.value !== undefined && cargo.inputCheck.value !== null ? cargo.inputCheck.value : vb_h.value, outcome: vb_outcome })) : rejected.push(Object.assign({}, vb_h, { outcome: 'rejected', message: cargo.inputCheck && cargo.inputCheck.message, message_es: cargo.inputCheck && cargo.inputCheck.message_es })), true)"),
298
- FLOW('vb_next', 'validate-input-batch', 'replace', { hits: '{{vb_rest}}', accepted: '{{accepted}}', rejected: '{{rejected}}', pending: '{{pending}}' }),
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}}' }),
299
315
  ],
300
316
  };
301
317
  const accept = {
@@ -311,7 +327,9 @@ const accept = {
311
327
  SET('ca_order', 'ca_ordered', 'orderFormReadings(cargo, hits)'),
312
328
  FLOW('ca_validate', 'validate-input-batch', 'call', { hits: '{{ca_ordered}}' }),
313
329
  SIDE('ca_settle', '(cargo.inputBatch = settleFormActivations(cargo.inputBatch, cargo), true)'),
330
+ { ...SIDE('ca_proposal_validation', '(settleFormProposalValidation(cargo,cargo.inputBatch),true)'), requireTrue: true },
314
331
  SIDE('ca_rejections', 'rejectFormReadings(cargo, cargo.inputBatch.rejected)'),
332
+ { ...SIDE('ca_proposal_commit', 'commitFormProposals(cargo,cargo.inputBatch.accepted)'), requireTrue: true },
315
333
  SIDE('ca_commit', 'acceptFormReadings(cargo, cargo.inputBatch.accepted, cargo.inputBatch.pending)'),
316
334
  SIDE('ca_pending', 'queueFormReadings(cargo, cargo.inputBatch.pending)'),
317
335
  SET('ca_results', 'ca_results', 'cargo.inputBatch.accepted.concat(cargo.inputBatch.rejected, cargo.inputBatch.pending)'),
@@ -372,7 +390,7 @@ const confirmForm = {
372
390
  id: 'confirm-form', name: 'ConfirmForm', version: '1.0.0', scope: 'isolated',
373
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).",
374
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 })),
375
- variables: vars({ cf_presets: FIELD_PRESETS, cf_chain: [], cf_m: 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: '' }),
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: '' }),
376
394
  steps: [
377
395
  SIDE('cf_reset', "(cargo.formConfirm = cargo.formConfirm || {}, cargo.formConfirm[form] = '', true)"),
378
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};}))"),
@@ -389,7 +407,8 @@ const confirmForm = {
389
407
  SET('cf_question_catalog', 'cf_questions', 'cargo.forms[form].questions || []'),
390
408
  SET('cf_provider_catalog', 'cf_provides', 'cargo.forms[form].providers'),
391
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' : ''"),
392
- 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, { form: form, name: '' }, '',cf_questions)"),
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)"),
393
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))"),
394
413
  SET('cf_count_words', 'cf_words', "String(cargo.choiceText || '').split(/\\s+/).filter(function (w) { return /[a-záéíóúüñ]/i.test(w); }).length"),
395
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}}' })),
@@ -412,6 +431,29 @@ const confirmForm = {
412
431
  SIDE('cf_store', "(cargo.formConfirm[form] = cf_result === 'unclear' || cf_result === 'clarification' ? 'changed' : cf_result, true)"),
413
432
  ],
414
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
+ ] };
415
457
  // ── form-done: the result goes to the parent (or cargo.formResults); the form is cleared ─────────────────────────────
416
458
  const formDone = {
417
459
  id: 'form-done', name: 'FormDone', version: '1.0.0', scope: 'isolated',
@@ -667,8 +709,9 @@ const resumeForm = { id: 'resume-form', name: 'ResumeForm', version: '1.0.0', sc
667
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 || ''"),
668
710
  FLOW('resume_owned_form', '{{target}}', 'replace', { form_instance: '{{form}}', parent_form: '{{parent}}', result_input: '{{slot}}' })
669
711
  ] };
670
- 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];
671
713
  const LIBRARY_CONTRACTS = {
714
+ 'confirm-input-proposal': { role: 'collector', requires: [INPUT_PROPOSALS_REQUIREMENT] }, 'confirm-input-proposal-declined': { role: 'worker', requires: [INPUT_PROPOSALS_REQUIREMENT] },
672
715
  'resume-form': { role: 'worker' },
673
716
  'perform-action': { role: 'worker', tools: ['jsfe:action-claim', 'jsfe:action-complete'], result: { path: 'cargo.actionResult', type: 'object' } },
674
717
  'send-text-action': { role: 'worker', result: { path: 'cargo.actionOutcome', type: 'object' } },
@@ -693,6 +736,6 @@ export const FLOWS = LIBRARY_FLOWS.map(flow => {
693
736
  throw new Error('Shared worker needs an explicit contract: ' + flow.id);
694
737
  if ((declaration.role === 'collector') !== (flow.metadata?.formProtocol === 2))
695
738
  throw new Error('Shared collector contract mismatch: ' + flow.id);
696
- 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,
697
740
  parametersFrom: 'parameters', ...declaration, ...(declaration.role === 'collector' ? { form: { inputs: 'inputs' } } : {}) } } } };
698
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,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"}
@@ -0,0 +1,292 @@
1
+ /** Owned input proposals. Candidates and their consent remain separate from canonical values and action proposals. */
2
+ import { formInputApplies, formValuesEqual, hasFormValue } from './form-state.js';
3
+ import { formSignature, actionConfirmationValid } from './form-actions.js';
4
+ import { validateFormProposalPolicy, validateFormProposalDescriptor, validateProposalData } from './proposal-contract.js';
5
+ const own = (value, key) => Object.prototype.hasOwnProperty.call(value || {}, key);
6
+ const statuses = new Set(['pending', 'confirmed', 'declined', 'superseded']);
7
+ const fail = message => { throw new Error('FORM input proposal ' + message); };
8
+ const text = value => typeof value === 'string' && !!value.trim();
9
+ function data(value) {
10
+ const seen = new Set();
11
+ const inspect = item => {
12
+ if (!item || typeof item !== 'object')
13
+ return;
14
+ if (seen.has(item))
15
+ fail('requires acyclic JSON data');
16
+ seen.add(item);
17
+ if (Object.getOwnPropertySymbols(item).length)
18
+ fail('requires JSON data without symbols');
19
+ for (const [key, property] of Object.entries(Object.getOwnPropertyDescriptors(item))) {
20
+ if (Array.isArray(item) && key === 'length')
21
+ continue;
22
+ if (Array.isArray(item) && (!/^(0|[1-9]\d*)$/.test(key) || Number(key) >= item.length))
23
+ fail('requires JSON arrays without extra properties');
24
+ if (property.get || property.set || !property.enumerable)
25
+ fail('requires enumerable JSON data properties');
26
+ inspect(property.value);
27
+ }
28
+ seen.delete(item);
29
+ };
30
+ inspect(value);
31
+ validateProposalData(value);
32
+ return structuredClone(value);
33
+ }
34
+ function inputOwner(cargo, id, name) {
35
+ const state = cargo?.forms?.[id], spec = state?.specs?.flatMap(input => input.any_of || [input]).find(input => input.name === name);
36
+ if (!state || state.id !== id || !spec || ['__proto__', 'prototype', 'constructor'].includes(name))
37
+ fail('requires a live owning input');
38
+ if (!Number.isSafeInteger(state.revision) || state.revision < 0 || !text(state.definition) || !text(state.task))
39
+ fail('has invalid owner identity or revision');
40
+ return { state, spec };
41
+ }
42
+ function candidate(cargo, reading) {
43
+ if (!reading || reading.ok !== true || !hasFormValue(reading.value))
44
+ fail('requires a validated nonempty candidate');
45
+ const owner = inputOwner(cargo, reading.form, reading.name);
46
+ data(reading.value);
47
+ return owner;
48
+ }
49
+ const identity = state => ({ id: state.id, definition: state.definition, task: state.task });
50
+ const signature = (owner, input, value, revision, descriptor) => formSignature({ owner, input, value, revision, binding: descriptor.binding ?? null, descriptor });
51
+ const token = (value, record) => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).every(key => ['id', 'signature'].includes(key)) && value.id === record?.id && value.signature === record?.signature;
52
+ const sourceReading = reading => Object.fromEntries(['raw', 'utterance', 'origin', 'current', 'asked'].filter(key => reading[key] !== undefined).map(key => [key, data(reading[key])]));
53
+ function recordFor(cargo, state, name) {
54
+ const record = state.inputProposals?.[name];
55
+ if (!record)
56
+ return null;
57
+ if (!text(record.id) || record.form !== state.id || record.input !== name || !statuses.has(record.status) || typeof record.committed !== 'boolean' || !Number.isSafeInteger(record.owner_revision) || record.owner_revision < 0 || !Number.isSafeInteger(record.revision) || record.revision < 1 || typeof record.child !== 'string' || !hasFormValue(record.value) || !record.reading || typeof record.reading !== 'object' || Array.isArray(record.reading))
58
+ fail('has invalid retained state');
59
+ data(record.value);
60
+ data(record.reading);
61
+ const descriptor = validateFormProposalDescriptor(record.descriptor);
62
+ if (!formValuesEqual(record.owner, identity(state)) || !formValuesEqual(record.binding, descriptor.binding ?? null) || record.signature !== signature(record.owner, name, record.value, record.owner_revision, descriptor))
63
+ fail('retained identity or binding changed');
64
+ return record;
65
+ }
66
+ const active = record => !!record && !record.committed && ['pending', 'confirmed'].includes(record.status);
67
+ function contextChild(cargo, state, id) {
68
+ const child = cargo.forms?.[id];
69
+ return child && child.id === id && child.context_only === true && child.parent === state.id && child.task === state.task && state.contexts?.[child.definition] === id ? child : null;
70
+ }
71
+ function withdraw(cargo, state, record) {
72
+ const child = contextChild(cargo, state, record.child);
73
+ if (child && child.proposalPrompt?.id === record.id && child.proposalPrompt?.signature === record.signature && child.proposalPrompt?.input === record.input) {
74
+ child.confirmation = null;
75
+ delete child.proposalPrompt;
76
+ }
77
+ delete record.approval;
78
+ }
79
+ const current = (state, record) => record.owner_revision === state.revision && record.signature === signature(identity(state), record.input, record.value, state.revision, record.descriptor);
80
+ function proofValid(cargo, state, record) {
81
+ if (!record || record.status !== 'confirmed' || record.committed || record.deferred || !current(state, record) || !record.approval)
82
+ return false;
83
+ const proof = record.approval;
84
+ if (proof.id !== record.id || proof.signature !== record.signature || proof.child !== record.child || !Number.isSafeInteger(proof.revision) || proof.confirmation?.revision !== proof.revision)
85
+ return false;
86
+ const child = cargo.forms?.[record.child];
87
+ return !child || !!contextChild(cargo, state, record.child) && child.proposalPrompt?.id === record.id && child.proposalPrompt?.signature === record.signature && child.revision === proof.revision && formValuesEqual(child.confirmation, proof.confirmation) && actionConfirmationValid(cargo, child.id, { signature: child.proposal?.signature || null });
88
+ }
89
+ function approved(cargo, state, record, reading) {
90
+ return token(reading.proposalResume, record) && token(reading.proposalApproval, record) && formValuesEqual(reading.value, record.value) && proofValid(cargo, state, record);
91
+ }
92
+ /** Static policy selectors use this batch's proposed values; a validator's explicit descriptor independently applies. */
93
+ export function formProposalPolicy(cargo, reading, check, accepted = []) {
94
+ const { state, spec } = inputOwner(cargo, reading.form, reading.name);
95
+ const declared = spec.proposal === undefined ? null : validateFormProposalPolicy(spec.proposal);
96
+ const dynamic = check && own(check, 'proposal') && check.proposal !== undefined ? validateFormProposalDescriptor(check.proposal) : null;
97
+ if (check?.ok === false)
98
+ return null;
99
+ const record = recordFor(cargo, state, reading.name), values = { ...state.values, ...Object.fromEntries(accepted.filter(hit => hit.form === state.id && hit.ok === true).map(hit => [hit.name, hit.value])), [reading.name]: reading.value };
100
+ const origin = reading.origin || 'answer';
101
+ const matches = declared && formInputApplies(declared, values, cargo, state) && (!declared.origins || declared.origins.includes(origin) || active(record));
102
+ return matches || dynamic ? { ...(matches ? declared : {}), ...(dynamic || {}) } : null;
103
+ }
104
+ /** A null descriptor admits ordinary input, but cannot silently waive the same retained candidate's consent. */
105
+ export function stageFormProposal(cargo, reading, descriptor) {
106
+ const { state } = candidate(cargo, reading), decision = descriptor === null ? null : validateFormProposalDescriptor(descriptor);
107
+ let old = recordFor(cargo, state, reading.name);
108
+ const resumed = own(reading, 'proposalResume');
109
+ if (resumed && !token(reading.proposalResume, old))
110
+ return false;
111
+ if (old?.committed && resumed)
112
+ return false;
113
+ if (decision === null) {
114
+ if (!old || old.status === 'superseded' || old.committed)
115
+ return !own(reading, 'proposalApproval');
116
+ if (old.status === 'declined') {
117
+ if (!resumed && !own(reading, 'proposalApproval')) {
118
+ withdraw(cargo, state, old);
119
+ old.status = 'superseded';
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+ if (formValuesEqual(old.value, reading.value)) {
125
+ if (approved(cargo, state, old, reading))
126
+ return true;
127
+ withdraw(cargo, state, old);
128
+ old.status = 'superseded';
129
+ return false;
130
+ }
131
+ if (resumed)
132
+ return false;
133
+ withdraw(cargo, state, old);
134
+ old.status = 'superseded';
135
+ return !own(reading, 'proposalApproval');
136
+ }
137
+ const binding = data(decision.binding ?? null), value = data(reading.value), nextSignature = signature(identity(state), reading.name, value, state.revision, decision);
138
+ const same = old && !old.committed && ['pending', 'confirmed', 'declined'].includes(old.status) && formValuesEqual(old.value, value) && formValuesEqual(old.binding, binding);
139
+ if (same && old.status === 'declined' && resumed)
140
+ return false;
141
+ if (!same || old.status === 'declined') {
142
+ if (old) {
143
+ withdraw(cargo, state, old);
144
+ old.status = 'superseded';
145
+ }
146
+ const sequence = state.inputProposalSequence ?? 0;
147
+ if (!Number.isSafeInteger(sequence) || sequence < 0 || !Number.isSafeInteger(sequence + 1))
148
+ fail('has an invalid proposal sequence');
149
+ state.inputProposalSequence = sequence + 1;
150
+ old = { id: `${state.id}:proposal:${state.inputProposalSequence}`, form: state.id, input: reading.name, owner: identity(state), value, reading: sourceReading(reading), descriptor: decision, binding, signature: nextSignature, owner_revision: state.revision, revision: 1, status: 'pending', committed: false, deferred: false, child: '' };
151
+ state.inputProposals ||= {};
152
+ state.inputProposals[reading.name] = old;
153
+ }
154
+ else if (old.signature !== nextSignature) {
155
+ withdraw(cargo, state, old);
156
+ Object.assign(old, { value, descriptor: decision, binding, signature: nextSignature, owner_revision: state.revision, revision: old.revision + 1, status: 'pending' });
157
+ }
158
+ old.deferred = false;
159
+ return approved(cargo, state, old, reading);
160
+ }
161
+ /** Only actionable records enter the confirmation worker; dependency-deferred records await the normal batch. */
162
+ export function nextFormProposal(cargo, owner) {
163
+ const state = cargo.forms?.[owner];
164
+ if (!state)
165
+ return null;
166
+ for (const spec of state.specs.flatMap(input => input.any_of || [input])) {
167
+ const record = recordFor(cargo, state, spec.name);
168
+ if (active(record) && !record.deferred && formInputApplies(spec, { ...state.values, [spec.name]: record.value }, cargo, state))
169
+ return record;
170
+ }
171
+ return null;
172
+ }
173
+ /** Owner replay issues approval only for confirmed records; named child refresh never invents approval. */
174
+ export function formProposalReadings(cargo, owner, input = '', approve = true) {
175
+ const state = cargo.forms?.[owner];
176
+ if (!state)
177
+ return [];
178
+ const names = input ? [input] : Object.keys(state.inputProposals || {}), out = [];
179
+ for (const name of names) {
180
+ const { spec } = inputOwner(cargo, owner, name), record = recordFor(cargo, state, name);
181
+ if (!record || record.committed || record.status === 'superseded' || record.deferred || approve && record.status !== 'confirmed' || !input && record.status === 'declined')
182
+ continue;
183
+ const hit = { ...spec, ...data(record.reading), form: owner, name, value: data(record.value), revalidate: true, proposalResume: { id: record.id, signature: record.signature } };
184
+ delete hit.proposalApproval;
185
+ if (approve && proofValid(cargo, state, record))
186
+ hit.proposalApproval = { id: record.id, signature: record.signature };
187
+ out.push(hit);
188
+ }
189
+ return out;
190
+ }
191
+ /** Bind only a real context child; ordinary child slots and unrelated owners cannot collect consent. */
192
+ export function bindFormProposal(cargo, owner, input, childId) {
193
+ const { state } = inputOwner(cargo, owner, input), record = recordFor(cargo, state, input), child = contextChild(cargo, state, childId);
194
+ if (!active(record) || record.deferred || !current(state, record) || !child)
195
+ fail('cannot bind a stale proposal or foreign confirmation child');
196
+ if (record.child && record.child !== childId)
197
+ withdraw(cargo, state, record);
198
+ if (child.proposalPrompt?.id !== record.id || child.proposalPrompt?.signature !== record.signature)
199
+ child.confirmation = null;
200
+ record.child = childId;
201
+ child.proposalPrompt = { id: record.id, signature: record.signature, input };
202
+ return record;
203
+ }
204
+ /** Exact YES must have passed the ordinary confirm-form revision check; NO never admits the candidate. */
205
+ export function decideFormProposal(cargo, owner, input, childId, decision) {
206
+ if (!['confirmed', 'declined'].includes(decision))
207
+ fail('has an invalid decision');
208
+ const { state } = inputOwner(cargo, owner, input), record = recordFor(cargo, state, input), child = contextChild(cargo, state, childId);
209
+ if (!active(record) || record.deferred || !current(state, record) || !child || record.child !== childId || child.proposalPrompt?.id !== record.id || child.proposalPrompt?.signature !== record.signature || child.proposalPrompt?.input !== input)
210
+ return false;
211
+ if (decision === 'confirmed' && !actionConfirmationValid(cargo, childId, { signature: child.proposal?.signature || null }))
212
+ return false;
213
+ record.status = decision;
214
+ if (decision === 'confirmed')
215
+ record.approval = { id: record.id, signature: record.signature, child: childId, revision: child.revision, confirmation: data(child.confirmation) };
216
+ else {
217
+ withdraw(cargo, state, record);
218
+ child.confirmation = null;
219
+ }
220
+ return true;
221
+ }
222
+ /** Validate the entire final batch before consuming any proof; invoke immediately before acceptFormReadings. */
223
+ export function commitFormProposals(cargo, accepted = []) {
224
+ const changes = [], readings = [];
225
+ for (const hit of accepted) {
226
+ const { state } = candidate(cargo, hit);
227
+ readings.push({ state, hit });
228
+ const record = recordFor(cargo, state, hit.name);
229
+ if (!record) {
230
+ if (own(hit, 'proposalResume') || own(hit, 'proposalApproval'))
231
+ fail('approval has no live record');
232
+ continue;
233
+ }
234
+ const tagged = own(hit, 'proposalResume') || own(hit, 'proposalApproval');
235
+ if (tagged) {
236
+ if (!approved(cargo, state, record, hit))
237
+ fail('requires current unconsumed approval before commitment');
238
+ if (changes.some(change => change.record === record))
239
+ fail('approval cannot be consumed twice in one batch');
240
+ changes.push({ state, record, commit: true });
241
+ }
242
+ else if (!record.committed && record.status !== 'superseded' && formValuesEqual(record.value, hit.value))
243
+ fail('cannot commit a retained candidate without approval');
244
+ else if (!formValuesEqual(record.value, hit.value) && record.status !== 'superseded')
245
+ changes.push({ state, record, commit: false });
246
+ }
247
+ // Canonical exclusive selection also replaces uncommitted peer candidates, even if this input has no proposal.
248
+ // Plan every withdrawal after validation, so an invalid later reading cannot partially consume consent.
249
+ for (const { state, hit } of readings)
250
+ for (const group of state.specs.filter(spec => spec.exclusive === true && spec.any_of?.some(member => member.name === hit.name))) {
251
+ const peers = group.any_of.filter(member => member.name !== hit.name);
252
+ if (readings.some(other => other.state.id === state.id && peers.some(peer => peer.name === other.hit.name)))
253
+ fail('exclusive alternatives require clarification before commitment');
254
+ for (const peer of peers) {
255
+ const record = recordFor(cargo, state, peer.name);
256
+ if (active(record) && !changes.some(change => change.record === record))
257
+ changes.push({ state, record, commit: false });
258
+ }
259
+ }
260
+ for (const { state, record, commit } of changes) {
261
+ if (commit) {
262
+ record.committed = true;
263
+ record.deferred = false;
264
+ }
265
+ else
266
+ record.status = 'superseded';
267
+ withdraw(cargo, state, record);
268
+ }
269
+ return true;
270
+ }
271
+ /** Failed validation withdraws the candidate; unmet prerequisites wait for the normal dependency replay. */
272
+ export function settleFormProposalValidation(cargo, batch = {}) {
273
+ for (const hit of [...(batch.rejected || []), ...(batch.ignored || [])]) {
274
+ const state = cargo.forms?.[hit.form];
275
+ if (!state)
276
+ continue;
277
+ const record = recordFor(cargo, state, hit.name);
278
+ if (active(record)) {
279
+ withdraw(cargo, state, record);
280
+ record.status = 'superseded';
281
+ }
282
+ }
283
+ for (const hit of batch.pending || []) {
284
+ const state = cargo.forms?.[hit.form];
285
+ if (!state)
286
+ continue;
287
+ const record = recordFor(cargo, state, hit.name);
288
+ if (active(record))
289
+ record.deferred = true;
290
+ }
291
+ return batch;
292
+ }
@@ -12,6 +12,7 @@ export function enrichFormMatches(matches: any, chain: any, form: any, names?: a
12
12
  export function expandFormActivations(cargo: any, hits?: any[]): any[];
13
13
  /** No partial activation: validation failure suppresses inferred changes; pending prerequisites retain the complete group. */
14
14
  export function settleFormActivations(batch?: {}, cargo?: null): {
15
+ ignored: any[];
15
16
  accepted: any[];
16
17
  rejected: any[];
17
18
  pending: any[];
@@ -1 +1 @@
1
- {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAMA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEAuBC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEAuCC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;QA8BC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AACD,qFASC;AACD,oGAoCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGA4CC;AACD,8DAQC;AACD,8EAeC;AAnjBM,8CAAmE"}
1
+ {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAOA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA2BC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEAuCC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;QA8BC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AACD,qFASC;AACD,oGAoCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGA4CC;AACD,8DAQC;AACD,8EAeC;AAvjBM,8CAAmE"}
@@ -1,3 +1,4 @@
1
+ import { validateFormProposalPolicy } from './proposal-contract.js';
1
2
  /** Shared FORM semantics. Pure readers plus explicit mutations of the supplied session container; no host I/O. */
2
3
  import { readInputProfile } from './match-inputs.js';
3
4
  import { validateMoneyOptions } from './money.js';
@@ -35,6 +36,11 @@ export function resolveFormInputs(inputs, presets = {}) {
35
36
  validateMoneyOptions(input.profile.money);
36
37
  }
37
38
  validateFormExhaustion(input.on_exhausted);
39
+ if (input.proposal !== undefined) {
40
+ if (!input.profile || input.form || input.any_of || input.bind)
41
+ throw new Error('FORM proposal requires a readable leaf input; declare it on each group member or inside the subform');
42
+ validateFormProposalPolicy(input.proposal);
43
+ }
38
44
  if (input.max_misses !== undefined && (!Number.isInteger(input.max_misses) || input.max_misses < 1))
39
45
  throw new Error('FORM max_misses must be a positive integer');
40
46
  if (input.max_attempts !== undefined && (!Number.isInteger(input.max_attempts) || input.max_attempts < 1))
@@ -264,7 +270,7 @@ export function settleFormActivations(batch = {}, cargo = null) {
264
270
  for (const [name, hit] of Object.entries(state.pending || {}))
265
271
  if (discarded.has(hit.activation_group))
266
272
  delete state.pending[name];
267
- return { accepted: accepted.filter(h => !held.has(h.activation_group) && !discarded.has(h.activation_group)),
273
+ return { ignored: [...(batch.ignored || [])], accepted: accepted.filter(h => !held.has(h.activation_group) && !discarded.has(h.activation_group)),
268
274
  rejected: rejected.map(h => h.activation_inferred ? { ...h, ...h.activation_entry, activation_inferred: false } : h), pending: pending.concat(accepted.filter(h => held.has(h.activation_group))).filter(h => !discarded.has(h.activation_group)).map(h => held.has(h.activation_group) ? { ...h, ok: undefined, outcome: 'pending', silent: true } : h) };
269
275
  }
270
276
  /** Explicit ownership: unrelated tasks and sibling inputs never share an instance by definition id. */
@@ -1,4 +1,16 @@
1
1
  export namespace STANDARD_FUNCTIONS {
2
+ export { formProposalPolicy };
3
+ export { stageFormProposal };
4
+ export { nextFormProposal };
5
+ export { formProposalReadings };
6
+ export { bindFormProposal };
7
+ export { decideFormProposal };
8
+ export { commitFormProposals };
9
+ export { settleFormProposalValidation };
10
+ export { formProposalProtocol };
11
+ export { validateFormProposalPolicy };
12
+ export { validateFormProposalDescriptor };
13
+ export { formProposalDescriptor };
2
14
  export { clearFormMisses };
3
15
  export { missFormInput };
4
16
  export { formInputPrompt };
@@ -93,6 +105,18 @@ export namespace STANDARD_FUNCTIONS {
93
105
  export { urlToSpeech };
94
106
  export { textWithUrlToSpeech };
95
107
  }
108
+ import { formProposalPolicy } from './form-proposals.js';
109
+ import { stageFormProposal } from './form-proposals.js';
110
+ import { nextFormProposal } from './form-proposals.js';
111
+ import { formProposalReadings } from './form-proposals.js';
112
+ import { bindFormProposal } from './form-proposals.js';
113
+ import { decideFormProposal } from './form-proposals.js';
114
+ import { commitFormProposals } from './form-proposals.js';
115
+ import { settleFormProposalValidation } from './form-proposals.js';
116
+ import { formProposalProtocol } from './proposal-contract.js';
117
+ import { validateFormProposalPolicy } from './proposal-contract.js';
118
+ import { validateFormProposalDescriptor } from './proposal-contract.js';
119
+ import { formProposalDescriptor } from './proposal-contract.js';
96
120
  import { clearFormMisses } from './form-conversation.js';
97
121
  import { missFormInput } from './form-conversation.js';
98
122
  import { formInputPrompt } from './form-conversation.js';
@@ -191,5 +215,5 @@ import { validatePhone } from './validators.js';
191
215
  import { validateEmail } from './validators.js';
192
216
  import { urlToSpeech } from './url-speech.js';
193
217
  import { textWithUrlToSpeech } from './url-speech.js';
194
- export { clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
218
+ export { formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech };
195
219
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gCAAwG,wBAAwB;8BAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB7C,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;qCAAZ,YAAY;gCAlB/D,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUtD,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;kCAC6mB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzoB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/helpers/index.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCAA8K,qBAAqB;kCAArB,qBAAqB;iCAArB,qBAAqB;qCAArB,qBAAqB;iCAArB,qBAAqB;mCAArB,qBAAqB;oCAArB,qBAAqB;6CAArB,qBAAqB;qCAC/E,wBAAwB;2CAAxB,wBAAwB;+CAAxB,wBAAwB;uCAAxB,wBAAwB;gCACpC,wBAAwB;8BAAxB,wBAAwB;gCAAxB,wBAAwB;oCAAxB,wBAAwB;wCAAxB,wBAAwB;gCAmB7C,YAAY;8BAAZ,YAAY;kCAAZ,YAAY;qCAAZ,YAAY;gCAlB/D,mBAAmB;8BAiBrB,qBAAqB;6BAEkG,aAAa;8BAAb,aAAa;yBAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;kCAAb,aAAa;+BAAb,aAAa;+BAAb,aAAa;2BAAb,aAAa;0BACT,cAAc;0BAAd,cAAc;uCAAd,cAAc;+BAAd,cAAc;sBAAd,cAAc;0BAAd,cAAc;iCAAd,cAAc;gCAAd,cAAc;sCAAd,cAAc;wCAC/H,cAAc;6BACP,cAAc;wBAAd,cAAc;wBAAd,cAAc;4BACjC,iBAAiB;+BACd,uBAAuB;mCACC,kBAAkB;mCAAlB,kBAAkB;qCAxB+I,mBAAmB;yCAAnB,mBAAmB;8BAAnB,mBAAmB;iCAAnB,mBAAmB;oCAAnB,mBAAmB;kCAAnB,mBAAmB;wCAAnB,mBAAmB;gCAAnB,mBAAmB;iCAAnB,mBAAmB;mCAAnB,mBAAmB;gCAe1I,mBAAmB;mCAAnB,mBAAmB;sCAAnB,mBAAmB;qCAAnB,mBAAmB;4BAUtD,mBAAmB;+BAAnB,mBAAmB;iCAAnB,mBAAmB;kCAC6mB,iBAAiB;iCAAjB,iBAAiB;gCAAjB,iBAAiB;8BAAjB,iBAAiB;+BAAjB,iBAAiB;iCAAjB,iBAAiB;8CAAjB,iBAAiB;sCAAjB,iBAAiB;uCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;wCAAjB,iBAAiB;sCAAjB,iBAAiB;kCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;sCAAjB,iBAAiB;gCAAjB,iBAAiB;6BAAjB,iBAAiB;yBAAjB,iBAAiB;gCAAjB,iBAAiB;iCAAjB,iBAAiB;oCAAjB,iBAAiB;gCAAjB,iBAAiB;kCAAjB,iBAAiB;oCAAjB,iBAAiB;8BAAjB,iBAAiB;mCAAjB,iBAAiB;qCAAjB,iBAAiB;mCAAjB,iBAAiB;oCAAjB,iBAAiB;kCAAjB,iBAAiB;yBACzoB,gBAAgB;gCAAhB,gBAAgB;4BAAhB,gBAAgB;8BAAhB,gBAAgB;+BACzB,iBAAiB;8BAAjB,iBAAiB;8BAAjB,iBAAiB;4BAC7B,iBAAiB;oCAAjB,iBAAiB"}
@@ -1,3 +1,5 @@
1
+ import { formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation } from './form-proposals.js';
2
+ import { formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor } from './proposal-contract.js';
1
3
  import { clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome } from './form-conversation.js';
2
4
  import { takeChoiceOffer } from './choice-offer.js';
3
5
  import { consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified } from './form-actions.js';
@@ -30,9 +32,10 @@ import { resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, fo
30
32
  import { checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS } from './otp-check.js';
31
33
  import { validateDigits, validatePhone, validateEmail } from './validators.js';
32
34
  import { urlToSpeech, textWithUrlToSpeech } from './url-speech.js';
33
- export { clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
35
+ export { formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor, clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome, readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, ANSWER_WORDS, CONFIRM_WORDS, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice, readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational, normalizeAndFindCapture, anythingElse, goodbye, sayName, dateToWords, amountToSpeech, validateCardNumber, validateExpiration, consumeActionReceipt, authorizeFormActionRetry, formSignature, selectFormAction, confirmFormRevision, prepareFormAction, actionConfirmationValid, beginFormAction, finishFormAction, actionGateVerified, sourceNeedsLoad, beginSourceRefresh, completeSourceRefresh, pickProviderQuestion, matchInputs, readFormAnswer, readInputProfile, resolveFormInputs, formInputApplies, formInputFilled, nextFormInput, formInputChain, openFormInstance, validateFormIncompleteOutcome, formIncompleteOutcome, validateFormExhaustion, exhaustFormInput, formInitialReadings, orderFormReadings, proposedFormValues, applyFormValidation, formReadingDependencies, exclusiveFormReadings, enrichFormMatches, expandFormActivations, settleFormActivations, rememberFormArguments, resetFormInputs, hasFormValue, formPath, formValuesEqual, formReadingReady, formReadingRelevant, formPreparation, queueFormReadings, pendingFormReadings, formWorkLimit, rejectFormReadings, invalidateFormInputs, acceptFormReadings, formInstanceApplies, closeFormInstance, checkOtp, maskDestination, validateOTP, OTP_EXPIRY_MS, validateDigits, validatePhone, validateEmail, urlToSpeech, textWithUrlToSpeech, };
34
36
  /** The functions flows call by name, ready to spread into a host's approved functions. */
35
37
  export const STANDARD_FUNCTIONS = {
38
+ formProposalPolicy, stageFormProposal, nextFormProposal, formProposalReadings, bindFormProposal, decideFormProposal, commitFormProposals, settleFormProposalValidation, formProposalProtocol, validateFormProposalPolicy, validateFormProposalDescriptor, formProposalDescriptor,
36
39
  clearFormMisses, missFormInput, formInputPrompt, consumeFormFeedback, formConversationOutcome,
37
40
  readMoneyAmount, isMoneyAmount, moneyIntentAllows, validateMoneyOptions, takeChoiceOffer, matchesChoice, isAnswer, isAgentRequest, isOtherRequest, isSpelledFragment, isAmountAnswer, amountInAnswer, pickChoice,
38
41
  readEmail, readPhone, readInternationalPhone, readIdentifier, last4, maskEmail, callerTextNumber, textDestination, callerIsInternational,
@@ -232,7 +232,7 @@ export function readFormAnswer(answer, candidates, current, choiceKey, questions
232
232
  let consumed = false;
233
233
  const remaining = parts.map(part => {
234
234
  const text = part.trim();
235
- if (!/^[¿\s]*(?:how\b|what\b|when\b|where\b|which\b|why\b|who\b|cu[aá]nt[oa]s?\b|qu[eé]\b|cu[aá]ndo\b|d[oó]nde\b|c[oó]mo\b|(?:can|could) you (?:tell|show)\b|(?:i (?:want|would like) to know|tell me|show me|quiero saber|quisiera saber|me gustar[ií]a saber|d[ií]game|dime|mu[eé]strame)\b)/i.test(text))
235
+ if (!/^[¿\s]*(?:how\b|what\b|when\b|where\b|which\b|why\b|who\b|cu[aá]nt[oa]s?\b|cu[aá]l(?:es)?\b|qu[eé](?=$|[^\p{L}\p{N}_])|cu[aá]ndo\b|d[oó]nde\b|c[oó]mo\b|(?:can|could) you (?:tell|show)\b|(?:i (?:want|would like) to know|tell me|show me|quiero saber|quisiera saber|me gustar[ií]a saber|d[ií]game|dime|mu[eé]strame)\b)/iu.test(text))
236
236
  return part;
237
237
  const key = pickProviderQuestion(text, questions);
238
238
  if (!key || key.startsWith('__'))
@@ -0,0 +1,7 @@
1
+ export function validateProposalData(value: any, seen?: Set<any>): any;
2
+ export function validateFormProposalPolicy(value: any): any;
3
+ export function validateFormProposalDescriptor(value: any): any;
4
+ /** Keep policy selectors out of the runtime decision descriptor; null is an explicit opt-out. */
5
+ export function formProposalDescriptor(policy: any, prepared?: undefined): any;
6
+ export function formProposalProtocol(): number;
7
+ //# sourceMappingURL=proposal-contract.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,95 @@
1
+ export const formProposalProtocol = () => 1;
2
+ /** Data-only authoring and runtime contract for input proposal confirmation. */
3
+ const safeName = value => typeof value === 'string' && /^[A-Za-z0-9][\w.:-]*$/.test(value);
4
+ const plain = value => !!value && typeof value === 'object' && !Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
5
+ const string = value => typeof value === 'string' && !!value.trim();
6
+ const fail = message => { throw new Error('FORM proposal ' + message); };
7
+ export function validateProposalData(value, seen = new Set()) {
8
+ if (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value))
9
+ return value;
10
+ if (!value || typeof value !== 'object' || seen.has(value) || !Array.isArray(value) && !plain(value))
11
+ fail('binding must be finite, acyclic JSON data');
12
+ seen.add(value);
13
+ const array = Array.isArray(value), keys = Reflect.ownKeys(value);
14
+ for (const key of keys) {
15
+ const property = Object.getOwnPropertyDescriptor(value, key);
16
+ if (typeof key !== 'string' || !Object.hasOwn(property, 'value') || !property.enumerable && !(array && key === 'length'))
17
+ fail('must contain only enumerable JSON data properties');
18
+ if (array && key === 'length')
19
+ continue;
20
+ if (['__proto__', 'prototype', 'constructor'].includes(key) || array && !/^(0|[1-9]\d*)$/.test(key))
21
+ fail('binding contains an unsafe key');
22
+ validateProposalData(property.value, seen);
23
+ }
24
+ if (array && keys.length !== value.length + 1)
25
+ fail('binding cannot contain array holes');
26
+ seen.delete(value);
27
+ return value;
28
+ }
29
+ const confirmationKeys = ['question', 'question_es', 'binding', 'words', 'interrupts', 'no_keys', 'clarifications', 'on_declined'];
30
+ function confirmation(value, required) {
31
+ if (required && !string(value.question))
32
+ fail('needs a nonempty question');
33
+ for (const key of ['question', 'question_es'])
34
+ if (value[key] !== undefined && !string(value[key]))
35
+ fail(key + ' must be nonempty text');
36
+ if (value.on_declined !== undefined && !safeName(value.on_declined))
37
+ fail('on_declined needs a static worker id');
38
+ for (const key of ['words', 'no_keys', 'clarifications'])
39
+ if (value[key] !== undefined && (!Array.isArray(value[key]) || !value[key].every(string)))
40
+ fail(key + ' must be an array of nonempty words');
41
+ if (value.interrupts !== undefined) {
42
+ if (!Array.isArray(value.interrupts))
43
+ fail('interrupts must be an array');
44
+ const keys = new Set();
45
+ for (const event of value.interrupts) {
46
+ if (!plain(event) || Object.keys(event).some(k => !['key', 'words', 'flow', 'exact'].includes(k)) || !safeName(event.key) || keys.has(event.key) || !safeName(event.flow) || !Array.isArray(event.words) || !event.words.length || !event.words.every(string) || event.exact !== undefined && typeof event.exact !== 'boolean')
47
+ fail('invalid interrupt declaration');
48
+ keys.add(event.key);
49
+ }
50
+ }
51
+ if (value.binding !== undefined)
52
+ validateProposalData(value.binding);
53
+ }
54
+ export function validateFormProposalPolicy(value) {
55
+ validateProposalData(value);
56
+ if (!plain(value) || Object.keys(value).some(key => !['prepare', 'origins', 'when', 'unless', ...confirmationKeys].includes(key)))
57
+ fail('invalid input policy');
58
+ if (value.prepare !== undefined && !safeName(value.prepare))
59
+ fail('prepare needs a static worker id');
60
+ if (!value.prepare && !string(value.question))
61
+ fail('policy needs a prepare worker or question');
62
+ if (value.origins !== undefined && (!Array.isArray(value.origins) || !value.origins.length || new Set(value.origins).size !== value.origins.length || !value.origins.every(origin => ['activation', 'answer'].includes(origin))))
63
+ fail('invalid origins');
64
+ const path = key => typeof key === 'string' && key.split('.').every(part => /^[A-Za-z_$][\w$-]*$/.test(part) && !['__proto__', 'prototype', 'constructor'].includes(part));
65
+ const scalar = item => item === null || typeof item === 'string' || typeof item === 'boolean' || typeof item === 'number' && Number.isFinite(item);
66
+ if (value.when !== undefined && (!plain(value.when) || Object.entries(value.when).some(([key, wanted]) => !path(key) || !(Array.isArray(wanted) ? wanted.length && wanted.every(scalar) : scalar(wanted)))))
67
+ fail('when must map safe paths to scalar conditions or nonempty scalar arrays');
68
+ if (value.unless !== undefined && (!Array.isArray(value.unless) || !value.unless.every(path)))
69
+ fail('unless must be an array of safe paths');
70
+ confirmation(value, false);
71
+ return structuredClone(value);
72
+ }
73
+ export function validateFormProposalDescriptor(value) {
74
+ validateProposalData(value);
75
+ if (!plain(value) || Object.keys(value).some(key => !confirmationKeys.includes(key)))
76
+ fail('invalid decision descriptor');
77
+ confirmation(value, true);
78
+ return structuredClone(value);
79
+ }
80
+ /** Keep policy selectors out of the runtime decision descriptor; null is an explicit opt-out. */
81
+ export function formProposalDescriptor(policy, prepared = undefined) {
82
+ if (!policy)
83
+ return null;
84
+ validateFormProposalPolicy(policy);
85
+ if (policy.prepare && prepared === undefined)
86
+ fail('prepare must explicitly return a descriptor or null');
87
+ if (prepared === null)
88
+ return null;
89
+ if (prepared !== undefined)
90
+ validateProposalData(prepared);
91
+ if (prepared !== undefined && (!prepared || typeof prepared !== 'object' || Array.isArray(prepared)))
92
+ fail('prepare must return a descriptor or null');
93
+ const base = Object.fromEntries(confirmationKeys.filter(key => policy[key] !== undefined).map(key => [key, policy[key]]));
94
+ return validateFormProposalDescriptor({ ...base, ...prepared });
95
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/host/capabilities.js"],"names":[],"mappings":"AAWA,8FAA8F;AAC9F,0FASC;AAED,2FAA2F;AAC3F,iFAKC;AAED,gHAAgH;AAChH,mFA0BC;AA1DD,kGAAkG;AAClG;IACG,uDAMC;IAHE,aAA2C;IAC3C,YAA0B;IAC1B,gBAAkC;CAEvC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/host/capabilities.js"],"names":[],"mappings":"AAYA,8FAA8F;AAC9F,0FASC;AAED,2FAA2F;AAC3F,iFAKC;AAED,gHAAgH;AAChH,mFA6BC;AA7DD,kGAAkG;AAClG;IACG,uDAMC;IAHE,aAA2C;IAC3C,YAA0B;IAC1B,gBAAkC;CAEvC"}
@@ -1,3 +1,4 @@
1
+ import { INPUT_PROPOSALS_REQUIREMENT } from '../forms/proposal-runtime.js';
1
2
  /** Declarative flow admission. No engine imports, I/O, or implicit host capability activation. */
2
3
  export class JSFEHostCapabilityError extends Error {
3
4
  constructor(flowId, capability, reason) {
@@ -40,6 +41,11 @@ export function assertFlowHostCapabilities(flows, capabilities = {}) {
40
41
  for (const flow of flows) {
41
42
  const requirements = normalizeHostRequirements(flow?.variables?.contract?.value?.requires, flow?.id);
42
43
  for (const capability of requirements) {
44
+ // Intrinsic runtime support, not a host option or an implicit host permission.
45
+ if (capability === INPUT_PROPOSALS_REQUIREMENT)
46
+ continue;
47
+ if (capability.startsWith('jsfe:'))
48
+ throw new JSFEHostCapabilityError(flow?.id, capability, 'unsupported intrinsic runtime contract');
43
49
  if (!capabilities || typeof capabilities !== 'object' || Array.isArray(capabilities)) {
44
50
  throw new JSFEHostCapabilityError(flow?.id, capability, 'explicit host capabilities must be an object');
45
51
  }
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:8bda7ef27c22853a43dfc6734c548115e2b5855e6bb56eeb48eddc5acdf6a820";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:258637780da663b142f69744745b18173dfadebefac54d7b71fa5717ff12823d";
@@ -29,3 +29,9 @@ Publishing the package, deploying the host or uploading tenant/system flow sets
29
29
  Native contextual misses default to one localized re-ask then DISPATCH of the second current utterance. Validation attempts and declared exhaustion callbacks are independent. Dynamic question refresh, explicit localized retries, nested/out-of-order input, provider detours and recognized clarification questions require behavioral assertions. A required draft yields pending; a failed service stays unresolved; a completed optional follow-up stays completed; unknown effects remain held. The host must consume explicit recovery rather than infer restartability from `unresolved`. Unannotated legacy JSON retains its historical path.
30
30
 
31
31
  Acceptance must include real engine-produced states through the actual host outcome and consume-once guidance adapter, not only helper snapshots. Verify English/Spanish and voice/text, “imposible” never confirms, exact price-reference utterance handoff, same root/accepted values on resume, fresh consent after correction, validator-specific limits, completed receipts, unknown-action holds, matched dispatch suppression and next-turn clearing. Run full engine and tenant matrices and clean consumer installs; Shopify behavior is outside this release and its source/artifact bytes must remain unchanged.
32
+
33
+ ## 1.7.3 input-proposal confirmation
34
+
35
+ The shared proposal contract is opt-in through input declarations or validator descriptors. It retains candidates before canonical acceptance, uses an owned exact-confirmation context and revalidates proof before commit. No host or website changes are part of this implementation. The contract is included in JSFE 1.7.3. Publishing and pinning the package do not activate regenerated flow definitions or deploy a host.
36
+
37
+ `contract.requires:['jsfe:input-proposals:v1']` describes intrinsic runtime support, not host configuration. Current JSFE admits that version directly; unknown `jsfe:*` contracts fail closed. Static and linked child declarations derive the requirement and strict entry guard; dynamic proposal workers declare it explicitly. Older runtimes must reject the matched definitions before tenant behavior. Release/pin the accepted runtime before activating regenerated shared and tenant sets. Both consumers must pin the same exact published runtime before the matched shared and tenant flow definitions are activated.
package/docs/forms.md CHANGED
@@ -51,6 +51,29 @@ The common collector passes `form_context:{form,current,names,questions}` to `as
51
51
 
52
52
  Private provider questions during collection preserve the owner and pending verification. Repeated questions join the pending provider request instead of stacking another authentication flow. Independent field clauses are accepted before a provider suspends for verification. Questions cannot become invalid OTP attempts or arbitrary text answers.
53
53
 
54
+ ## Input proposal confirmation (unpublished candidate)
55
+
56
+ Use `input.proposal` when an inferred or ambiguous **field value** needs the caller's approval before acceptance. This is independent of a performer's final action confirmation: accepting a payment option never authorizes a charge. Declare it on readable leaf inputs, including members of an `any_of` group; group, bound-value and subform declarations reject this attribute instead of silently ignoring it. Ordinary inputs without this declaration retain their existing path.
57
+
58
+ ```js
59
+ {
60
+ name:'reason', profile:{kind:'choice',choices:[/* canonical choices */]},
61
+ proposal:{prepare:'describe-reason',origins:['activation'],when:{reason:['lost','stolen']}}
62
+ }
63
+ ```
64
+
65
+ An optional pure `prepare` worker receives `{form,input,value,values,reading}`. `value` is validator-normalized; `values` includes validated peers and this candidate. It writes `cargo.proposalCheck={question,question_es,binding?,on_declined?,words?,no_keys?,clarifications?,interrupts?}`, or explicitly `null` when no proposal is required. A static policy may supply the question directly. An ordinary validator can independently return `cargo.inputCheck={ok:true,value,proposal:{question,question_es,binding,...}}`. Missing or malformed descriptors fail closed. Declare dynamic proposal workers with `contract.requires:['jsfe:input-proposals:v1']`; static policies and linked descendant schemas derive that requirement automatically.
66
+
67
+ Formatters must be pure apart from their output slot: no authentication, business updates or external effects. Bind the semantic subject and facts that make the question true (account, option, plan, prior receipt). Avoid volatile unrelated history. `origins` restricts initial triggering to `activation` or `answer`; corrections to a pending proposal still require a fresh applicable decision. `when`/`unless` use normal form conditions against candidate values.
68
+
69
+ JSFE owns `owner.inputProposals[input]`, separate from canonical `values` and the existing **action** `proposal`. Candidates survive contextual yield and bare reentry. An owned `confirm-input-proposal` context uses ordinary exact confirmation and treats the proposed field as current for corrections. Independent inputs commit normally. Accepting a member of an exclusive `any_of` group withdraws pending or confirmed proposals for its peer members. YES is provisional proof bound to owner identity, revision, candidate, prompt and business binding; ordinary validation reruns before canonical acceptance and post-commit callbacks. A changed candidate, policy, subject or binding revokes old proof. Rejected/inapplicable candidates are withdrawn; unmet prerequisites defer them. Approval is consumed once. Do not set proposal state or approval tokens in tenant expressions.
70
+
71
+ NO never commits the candidate or hands an incomplete value to a parent. It closes the confirmation context and optionally calls `on_declined` with `{form,input,value,reading}` once; this worker chooses the business alternative and may close its owner. Yield retains the proposal and has no decline effect. Do not add separate yes/no collector children, cargo permission flags or ad hoc stack restoration.
72
+
73
+ The versioned intrinsic requirement is supported by JSFE itself, with no host feature switch. Unknown `jsfe:*` versions reject admission even if a caller supplies a matching boolean. Compiled proposal entries also include a strict `formProposalProtocol() === 1` guard, so an older direct engine cannot silently skip consent. Deploy matched runtime, shared workers and tenant declarations; these candidate artifacts must not be activated with published 1.7.2.
74
+
75
+ Test actual EN/ES voice/text conversations: activation, unreadable-answer yield, no-argument root reentry, explicit NO, mixed YES plus correction, provider detours, independent fields, nested/foreign owners, stale bindings and consumed proof. Assert canonical values, callbacks, authentication and effects, then run the complete engine and tenant acceptance matrices against installed package bytes.
76
+
54
77
  ## Providers
55
78
 
56
79
  Declare a `provides` catalog: subject, gate (`flow`, verified paths), source (`flow`, result path, subject/freshness policy), facts and fact sets. Each fact has a key, phrasings, source path or dedicated worker, rendering rules and explicit missing-data text. A private catalog requires an explicit gate. Derive `asked` and mid-form question choices with the builder; do not maintain another list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
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",