jsfe 1.5.0 → 1.7.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.
Files changed (100) hide show
  1. package/README.md +45 -4
  2. package/dist/forms/action-projection.d.ts +4 -0
  3. package/dist/forms/action-projection.d.ts.map +1 -0
  4. package/dist/forms/action-projection.js +21 -0
  5. package/dist/forms/action-recovery.d.ts +12 -0
  6. package/dist/forms/action-recovery.d.ts.map +1 -0
  7. package/dist/forms/action-recovery.js +125 -0
  8. package/dist/forms/action-tools.d.ts +29 -0
  9. package/dist/forms/action-tools.d.ts.map +1 -0
  10. package/dist/forms/action-tools.js +6 -0
  11. package/dist/forms/address-flows.d.ts +2 -0
  12. package/dist/forms/address-flows.d.ts.map +1 -0
  13. package/dist/forms/address-flows.js +39 -0
  14. package/dist/forms/choice-flow.d.ts +419 -0
  15. package/dist/forms/choice-flow.d.ts.map +1 -0
  16. package/dist/forms/choice-flow.js +707 -0
  17. package/dist/forms/contracts.d.ts +237 -0
  18. package/dist/forms/contracts.d.ts.map +1 -0
  19. package/dist/forms/contracts.js +278 -0
  20. package/dist/forms/flows.d.ts +611 -0
  21. package/dist/forms/flows.d.ts.map +1 -0
  22. package/dist/forms/flows.js +675 -0
  23. package/dist/forms/identity-flows.d.ts +17 -0
  24. package/dist/forms/identity-flows.d.ts.map +1 -0
  25. package/dist/forms/identity-flows.js +302 -0
  26. package/dist/forms/index.d.ts +7 -0
  27. package/dist/forms/index.d.ts.map +1 -0
  28. package/dist/forms/index.js +10 -0
  29. package/dist/forms/prompt-compiler.d.ts +3 -0
  30. package/dist/forms/prompt-compiler.d.ts.map +1 -0
  31. package/dist/forms/prompt-compiler.js +30 -0
  32. package/dist/helpers/address.d.ts +27 -0
  33. package/dist/helpers/address.d.ts.map +1 -0
  34. package/dist/helpers/address.js +29 -0
  35. package/dist/helpers/answer.d.ts +5 -0
  36. package/dist/helpers/answer.d.ts.map +1 -1
  37. package/dist/helpers/answer.js +5 -3
  38. package/dist/helpers/choice-offer.d.ts +3 -0
  39. package/dist/helpers/choice-offer.d.ts.map +1 -0
  40. package/dist/helpers/choice-offer.js +6 -0
  41. package/dist/helpers/form-actions.d.ts +13 -0
  42. package/dist/helpers/form-actions.d.ts.map +1 -0
  43. package/dist/helpers/form-actions.js +144 -0
  44. package/dist/helpers/form-sources.d.ts +7 -0
  45. package/dist/helpers/form-sources.d.ts.map +1 -0
  46. package/dist/helpers/form-sources.js +47 -0
  47. package/dist/helpers/form-state.d.ts +94 -0
  48. package/dist/helpers/form-state.d.ts.map +1 -0
  49. package/dist/helpers/form-state.js +870 -0
  50. package/dist/helpers/index.d.ts +114 -1
  51. package/dist/helpers/index.d.ts.map +1 -1
  52. package/dist/helpers/index.js +9 -4
  53. package/dist/helpers/match-inputs.d.ts +14 -0
  54. package/dist/helpers/match-inputs.d.ts.map +1 -1
  55. package/dist/helpers/match-inputs.js +154 -41
  56. package/dist/helpers/money.d.ts +12 -0
  57. package/dist/helpers/money.d.ts.map +1 -0
  58. package/dist/helpers/money.js +54 -0
  59. package/dist/helpers/spoken-money.d.ts +20 -0
  60. package/dist/helpers/spoken-money.d.ts.map +1 -0
  61. package/dist/helpers/spoken-money.js +143 -0
  62. package/dist/host/action-journal.d.ts +98 -0
  63. package/dist/host/action-journal.d.ts.map +1 -0
  64. package/dist/host/action-journal.js +218 -0
  65. package/dist/host/action-session.d.ts +12 -0
  66. package/dist/host/action-session.d.ts.map +1 -0
  67. package/dist/host/action-session.js +29 -0
  68. package/dist/host/capabilities.d.ts +12 -0
  69. package/dist/host/capabilities.d.ts.map +1 -0
  70. package/dist/host/capabilities.js +43 -0
  71. package/dist/host/checkpoint-codec-error.d.ts +6 -0
  72. package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
  73. package/dist/host/checkpoint-codec-error.js +4 -0
  74. package/dist/host/index.d.ts +7 -0
  75. package/dist/host/index.d.ts.map +1 -1
  76. package/dist/host/index.js +7 -0
  77. package/dist/host/intent-stages.d.ts +155 -0
  78. package/dist/host/intent-stages.d.ts.map +1 -0
  79. package/dist/host/intent-stages.js +256 -0
  80. package/dist/host/node.d.ts +17 -0
  81. package/dist/host/node.d.ts.map +1 -0
  82. package/dist/host/node.js +64 -0
  83. package/dist/host/recovery-identity.d.ts +16 -0
  84. package/dist/host/recovery-identity.d.ts.map +1 -0
  85. package/dist/host/recovery-identity.js +73 -0
  86. package/dist/host/recovery-policy.d.ts +5 -0
  87. package/dist/host/recovery-policy.d.ts.map +1 -0
  88. package/dist/host/recovery-policy.js +54 -0
  89. package/dist/host/runtime-manifest.d.ts +3 -0
  90. package/dist/host/runtime-manifest.d.ts.map +1 -0
  91. package/dist/host/runtime-manifest.js +38 -0
  92. package/dist/index.d.ts +75 -19
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +239 -46
  95. package/dist/runtime-identity.d.ts +3 -0
  96. package/dist/runtime-identity.d.ts.map +1 -0
  97. package/dist/runtime-identity.js +1 -0
  98. package/docs/development-release.md +23 -0
  99. package/docs/forms.md +122 -0
  100. package/package.json +15 -4
package/README.md CHANGED
@@ -13,6 +13,8 @@ npm i jsfe
13
13
  - Comprehensive tutorials, examples, and best practices
14
14
  - **[README.md](README.md)** - Technical API reference (this document)
15
15
  - **[Flow author reference](docs/copilot-reference.md)** - the engine in one page, for flow authors and AI flow generators (shipped in the package; the Flow Designer's CoPilot reads it from the published version, so change it with the engine)
16
+ - **[Native forms, providers and performers](docs/forms.md)** - owned reentrant collection, fact catalogs, confirmed effects and durable recovery
17
+ - **[Development candidate acceptance](docs/development-release.md)** - reproducible package and cross-repository verification
16
18
  - **[System Flows](docs/system-flows.md)** and **[Shopify Flows](docs/shopify-flows.md)** - the bundled flow and tool libraries in `flows/`, step by step
17
19
 
18
20
  *For detailed tutorials, step-by-step examples, and comprehensive workflow patterns, see the **[User Guide](user-guide.md)**.*
@@ -342,6 +344,24 @@ const body = buildAiRequest(
342
344
  - `mergeTextParameters` adds a text model's free-text parameters only when the text model chose
343
345
  the same flow. The classifier's enum and boolean values always win.
344
346
 
347
+ **Two-stage intent detection (opt-in: `engine.intentDetection = 'two-stage'`):**
348
+ Stage 1 picks which PRIMARY flow, if any, the message starts, reading only each primary's `description`.
349
+ Stage 2 extracts the selected flow's parameters, reading each parameter's name, type, `enum` and `description`,
350
+ plus the flow's optional `parameterInstructions` (rules that span its parameters). A parameter marked
351
+ `internal: true` is the flow's own and is never asked. Each stage is ONE aiCallback call: the positional
352
+ arguments are the LLM instructions, and the 4th argument (`task: 'detect_flow'` with `stage: 1`, or
353
+ `task: 'extract_parameters'` with `stage: 2`) carries both executors' instructions, `llm` and `systemOne`.
354
+ The host runs either, or both as a hedge, and replies with the canonical JSON: `{ "flowName": … }` for
355
+ stage 1, `{ "parameters": { … } }` for stage 2. `jsfe/host` exports `buildFlowStage`, `buildParameterStage`
356
+ and the decoders (`decodeFlowLlm`, `decodeFlowSystemOne`, `decodeParametersLlm`, `decodeParametersSystemOne`,
357
+ `isNearMiss`), so a host never interprets an answer itself. Only primaries are ever activated by detection;
358
+ a stage-2 failure starts the flow without its parameters (it asks for them). The default, `'single-call'`,
359
+ is the original single call.
360
+
361
+ **Host start (`ContextEntry.start`):** `{ role: 'user', content, start: { flow, arguments } }` starts that
362
+ flow (any flow, by id or name) with those arguments on this turn, without asking the AI. Only the host sets it;
363
+ it is never derived from what the user typed.
364
+
345
365
  **Flow helpers — `jsfe/helpers`:**
346
366
  The functions the bundled system flows (`ask-choice`, `collect-input`, `authenticate-user`…) call from
347
367
  expressions, so every host runs the same ones. Pure: no credentials, no network. The engine does not
@@ -367,6 +387,12 @@ const engine = new WorkflowEngine(logger, aiCallback, flows, tools,
367
387
  - Session-aware helpers read only the cargo a caller passes: `callerTextNumber`, `textDestination`,
368
388
  `callerIsInternational`.
369
389
 
390
+ `amountInAnswer` and `isAmountAnswer` retain their legacy parsing behavior. Explicit USD parsing uses
391
+ `readMoneyAmount(text, {currency:'USD',cardinals:true,intent:'payment'})` or `isMoneyAmount` with the
392
+ same options. `cardinals` defaults to false and `intent` is optional. FORM amount profiles opt in with
393
+ `{kind:'amount',money:{currency:'USD',cardinals:true,intent:'payment'}}`; ordinary amount profiles retain
394
+ legacy parsing. No currency conversion is inferred.
395
+
370
396
  **Alternative AI Services:**
371
397
  You can integrate any AI service (Claude, Gemini, local LLMs, etc.) by implementing this same interface. The engine only requires a function that takes system instructions and user input, then returns an AI response.
372
398
 
@@ -422,6 +448,14 @@ const flowsMenu = [
422
448
  - **`variables`** (optional): Flow-specific variable definitions with types and descriptions
423
449
  - **`version`** (optional): Flow version for compatibility tracking (defaults to "1.0")
424
450
 
451
+ Mutable object defaults and authored step data are copied for each activation; changing one session's
452
+ default data does not mutate a flow definition or another activation. This does not change the shared
453
+ variable-container behavior above. A shared `FLOW` step with `callType:'replace'` preserves that container
454
+ and does not initialize target defaults unless it declares `initializeDefaults:true`; that opt-in fills
455
+ only undefined defaults before explicit parameters. Isolated replacement initializes its own scope.
456
+ `SET` steps may declare `requireTrue:true` to throw `JSFERequiredSetFailed` unless their result is literal
457
+ true; absent or false keeps ordinary SET behavior.
458
+
425
459
  ##### Primary vs. Sub-Flow Architecture
426
460
 
427
461
  The `primary` property enables a clean separation between:
@@ -738,10 +772,17 @@ return aiResponse;
738
772
  ### Host Responsibilities: Tool Calls, Cancellation and Concurrency
739
773
 
740
774
  A flow can take actions in the outside world through `CALL-TOOL` — charge a card, send an SMS,
741
- create a ticket. **The session returned by `updateActivity` is the only record that those steps
742
- already ran.** The engine cannot protect that record once it leaves `updateActivity`; the host
743
- owns it from there. Getting any of the rules below wrong lets a flow repeat an action the user
744
- asked for once.
775
+ create a ticket. For ordinary `CALL-TOOL` flows, **the session returned by `updateActivity` records
776
+ which steps already ran**; the host owns its persistence. Native performers can additionally use an
777
+ explicitly attached durable action journal. Neither mode removes the host responsibilities below.
778
+ Getting these rules wrong can repeat an action the user asked for once.
779
+
780
+ The reference host defaults `config.jsfe` to `{durableActions:false,sessionEncoding:'plain'}`. Durable
781
+ journaling and `sessionEncoding:'br-v1'` conversation writes are separate opt-ins. Encoded storage
782
+ requires a reader-first rollout and a lossless conversion before an older-reader rollback. Durable
783
+ recovery requires matching runtime/definition identities; a missing or different identity holds recovery
784
+ without replaying effects or acknowledging receipts. See [development release boundaries](docs/development-release.md)
785
+ for exact policy keys, storage limits and rollback requirements. Passing local tests does not approve deployment.
745
786
 
746
787
  This is not hypothetical. In production a caller confirmed a payment and repeated "confirm" a
747
788
  second later. The host cancelled the first turn while its charge was in flight and restored the
@@ -0,0 +1,4 @@
1
+ export function validateActionProjection(steps: any, id?: string): any[];
2
+ /** Compile both ordinary and late-result paths from exactly the same validated declaration. */
3
+ export function actionProjectionExpression(steps: any, bindings: any): string;
4
+ //# sourceMappingURL=action-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-projection.d.ts","sourceRoot":"","sources":["../../src/forms/action-projection.js"],"names":[],"mappings":"AAGA,yEAQC;AACD,+FAA+F;AAC/F,8EAKC"}
@@ -0,0 +1,21 @@
1
+ /** A performer projection updates current session facts only; it never orchestrates another flow or tool. */
2
+ const LOCALS = ['form', 'formSnapshot', 'request', 'result', 'key'];
3
+ const identifier = /^[A-Za-z_$][\w$]*$/;
4
+ export function validateActionProjection(steps, id = 'performer') {
5
+ if (!Array.isArray(steps))
6
+ throw new Error(`Flow contract ${id}: project must be a SET-step array`);
7
+ for (const step of steps) {
8
+ if (!step || step.type !== 'SET' || step.onFail || step.branches || !identifier.test(step.variable || '') || LOCALS.concat(['cargo', 'arguments', 'eval']).includes(step.variable) || step.value === undefined)
9
+ throw new Error(`Flow contract ${id}: project permits only SET expressions with local temporary variables`);
10
+ if (typeof step.value === 'string' && step.value.includes('{{'))
11
+ throw new Error(`Flow contract ${id}: project uses expressions, not message templates`);
12
+ }
13
+ return steps;
14
+ }
15
+ /** Compile both ordinary and late-result paths from exactly the same validated declaration. */
16
+ export function actionProjectionExpression(steps, bindings) {
17
+ validateActionProjection(steps);
18
+ const variables = [...new Set(steps.map(step => step.variable))];
19
+ const expressions = steps.map(step => `${step.variable}=(${typeof step.value === 'string' ? step.value : JSON.stringify(step.value)});`).join('\n');
20
+ return `(function(${LOCALS.join(',')}){${variables.length ? 'var ' + variables.join(',') + ';' : ''}${expressions}return true;})(${LOCALS.map(name => bindings[name]).join(',')})`;
21
+ }
@@ -0,0 +1,12 @@
1
+ export function resumableActionSession(session: any, flows: any, identity: any): any;
2
+ /** Mutate the supplied session so the host retains its original object reference. */
3
+ export function restoreActionSession(session: any, recovery: any, contextEntry: any, identity: any): any;
4
+ /** Update only the exact action identity; later requests and draft values remain owned by their current task. */
5
+ export function reconcileRecoveredAction(cargo: any, key: any, result: any): boolean;
6
+ /**
7
+ * Recover a late result against CURRENT cargo. Only its effect-free projector runs: no
8
+ * original presenter, caller continuation, confirmation, or nested performer is replayed.
9
+ * The engine must restore the returned suspended stacks in a finally block after playing.
10
+ */
11
+ export function projectActionRecoverySession(session: any, recovery: any, contextEntry: any, flows: any, identity: any): any;
12
+ //# sourceMappingURL=action-recovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-recovery.d.ts","sourceRoot":"","sources":["../../src/forms/action-recovery.js"],"names":[],"mappings":"AAKA,qFAuBC;AAOD,qFAAqF;AACrF,yGAQC;AACD,iHAAiH;AACjH,qFAMC;AACD;;;;GAIG;AACH,6HAkCC"}
@@ -0,0 +1,125 @@
1
+ /** Durable resume snapshots contain engine data only, never a live engine or transport. */
2
+ import { applyRecoveryStatePolicy } from '../host/recovery-policy.js';
3
+ import { assertActionRecoveryIdentity, actionRecoveryIdentityMatches } from '../host/recovery-identity.js';
4
+ import { actionProjectionExpression, validateActionProjection } from './action-projection.js';
5
+ const clone = value => JSON.parse(JSON.stringify(value));
6
+ export function resumableActionSession(session, flows, identity) {
7
+ if (!session)
8
+ throw new Error('Action journal requires an active engine session');
9
+ const recoveryIdentity = assertActionRecoveryIdentity(identity);
10
+ const snapshot = clone(session);
11
+ snapshot.actionRecoveryIdentity = recoveryIdentity;
12
+ let owner;
13
+ for (let s = snapshot.flowStacks.length - 1; s >= 0 && !owner; s--) {
14
+ const frames = snapshot.flowStacks[s];
15
+ for (let f = frames.length - 1; f >= 0; f--) {
16
+ if (frames[f].flowId === 'perform-action') {
17
+ owner = { s, f, frame: frames[f] };
18
+ break;
19
+ }
20
+ }
21
+ }
22
+ const definition = flows.find(flow => flow.id === 'perform-action');
23
+ if (!owner || !definition)
24
+ throw new Error('Durable action ports require the shared perform-action worker');
25
+ snapshot.flowStacks.length = owner.s + 1;
26
+ snapshot.flowStacks[owner.s].length = owner.f + 1;
27
+ owner.frame.flowStepsStack = clone(definition.steps).reverse();
28
+ owner.frame.inputStack = [];
29
+ delete owner.frame.pendingVariable;
30
+ delete owner.frame.pendingInterruption;
31
+ // Earlier completed actions in this turn may still have unsent messages.
32
+ snapshot.response = null;
33
+ return snapshot;
34
+ }
35
+ function matchingSnapshot(session, recovery, identity) {
36
+ const snapshot = recovery?.session;
37
+ if (!snapshot || snapshot.sessionId !== session.sessionId || snapshot.userId !== session.userId)
38
+ throw new Error('Durable action checkpoint belongs to a different session');
39
+ if (!actionRecoveryIdentityMatches(identity, snapshot.actionRecoveryIdentity))
40
+ throw new Error('Durable action checkpoint requires a compatible runtime and flow contract');
41
+ return snapshot;
42
+ }
43
+ /** Mutate the supplied session so the host retains its original object reference. */
44
+ export function restoreActionSession(session, recovery, contextEntry, identity) {
45
+ const restored = clone(matchingSnapshot(session, recovery, identity));
46
+ applyRecoveryStatePolicy(restored, session);
47
+ for (const key of Object.keys(session))
48
+ delete session[key];
49
+ Object.assign(session, restored);
50
+ session.cargo ||= {};
51
+ session.lastTurnRecovery = { key: recovery.key, sequence: recovery.sequence, deferredInput: contextEntry };
52
+ return session;
53
+ }
54
+ /** Update only the exact action identity; later requests and draft values remain owned by their current task. */
55
+ export function reconcileRecoveredAction(cargo, key, result) {
56
+ const matches = record => record && (record.durableKey || record.key) === key;
57
+ const update = record => { if (matches(record)) {
58
+ record.result = JSON.parse(JSON.stringify(result));
59
+ record.status = result.status === 'unknown' ? 'unknown' : 'completed';
60
+ } };
61
+ for (const state of Object.values(cargo.forms || {})) {
62
+ for (const record of Object.values(state.actions || {}))
63
+ update(record);
64
+ update(state.proposal);
65
+ }
66
+ for (const [localKey, record] of Object.entries(cargo.actionJournal || {}))
67
+ if (matches(record)) {
68
+ update(record);
69
+ if (record.status === 'completed')
70
+ delete cargo.actionJournal[localKey];
71
+ }
72
+ return true;
73
+ }
74
+ /**
75
+ * Recover a late result against CURRENT cargo. Only its effect-free projector runs: no
76
+ * original presenter, caller continuation, confirmation, or nested performer is replayed.
77
+ * The engine must restore the returned suspended stacks in a finally block after playing.
78
+ */
79
+ export function projectActionRecoverySession(session, recovery, contextEntry, flows, identity) {
80
+ const snapshot = matchingSnapshot(session, recovery, identity), record = recovery.record;
81
+ if (!record || record.key !== recovery.key || record.sequence !== recovery.sequence)
82
+ throw new Error('Late action recovery requires its exact durable record');
83
+ const frames = snapshot.flowStacks.flat().filter(Boolean), actionIndex = frames.findLastIndex(frame => frame.flowId === 'perform-action');
84
+ const registry = new Map(flows.map(flow => [flow.id, flow]));
85
+ let owner, definition;
86
+ for (let i = actionIndex - 1; i >= 0; i--) {
87
+ const candidate = registry.get(frames[i].flowId);
88
+ if (candidate?.variables?.contract?.value?.role === 'performer') {
89
+ owner = frames[i];
90
+ definition = candidate;
91
+ break;
92
+ }
93
+ }
94
+ if (!owner || !definition)
95
+ throw new Error('Late action recovery requires an owning declarative performer');
96
+ const project = definition.variables?.[definition.variables.contract.value.projects || 'projection']?.value || [];
97
+ validateActionProjection(project, definition.id);
98
+ const variables = owner.variables || {}, form = variables.form_instance;
99
+ const formSnapshot = snapshot.cargo?.forms?.[form];
100
+ if (!formSnapshot)
101
+ throw new Error('Late action recovery is missing its owned form snapshot');
102
+ const frame = clone(owner), result = clone(record.result || { status: 'unknown', reason: 'result_pending' });
103
+ frame.variables = { ...frame.variables, pf_recovery_form: clone(formSnapshot), pf_recovery_request: clone(variables.pf_request || frames[actionIndex]?.variables?.request || {}), pf_recovery_result: result, pf_recovery_key: record.key,
104
+ pf_recovery_receipt: { key: record.key, sequence: record.sequence, attemptId: record.attemptId, scopeId: record.scopeId, signature: record.signature } };
105
+ const set = (id, variable, value) => ({ id, type: 'SET', variable, value, requireTrue: true });
106
+ const notice = result.status === 'unknown'
107
+ ? { en: 'I still could not confirm whether your earlier request completed. I have not repeated it.', es: 'Todavía no pude confirmar si su solicitud anterior se completó. No la he repetido.' }
108
+ : ['refused', 'failed', 'invalid_email'].includes(result.status)
109
+ ? { en: 'Your earlier request did not complete.', es: 'Su solicitud anterior no se completó.' }
110
+ : { en: 'I recovered a result for your earlier request.', es: 'Recuperé un resultado de su solicitud anterior.' };
111
+ frame.flowStepsStack = [
112
+ set('recovery_project', 'pf_projected', actionProjectionExpression(project, { form: 'form_instance', formSnapshot: 'pf_recovery_form', request: 'pf_recovery_request', result: 'pf_recovery_result', key: 'pf_recovery_key' })),
113
+ set('recovery_reconcile', 'noop', `(${reconcileRecoveredAction.toString()})(cargo,pf_recovery_key,pf_recovery_result)`),
114
+ set('recovery_receipt', 'noop', 'consumeActionReceipt(cargo,pf_recovery_receipt)'),
115
+ { id: 'recovery_notice', type: 'SAY', value: notice.en, value_es: notice.es },
116
+ ].reverse();
117
+ frame.inputStack = [];
118
+ frame.contextStack = [];
119
+ for (const key of ['pendingVariable', 'pendingVariableContext', 'pendingVariableAsSpoken', 'pendingInterruption'])
120
+ delete frame[key];
121
+ const suspendedStacks = session.flowStacks;
122
+ session.flowStacks = [[frame]];
123
+ session.lastTurnRecovery = { key: record.key, sequence: record.sequence, deferredInput: contextEntry, projection: true };
124
+ return suspendedStacks;
125
+ }
@@ -0,0 +1,29 @@
1
+ /** Internal transport ports. Registered by WorkflowEngine; no network implementation lives in JSFE. */
2
+ export const ACTION_TOOLS: {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ parameters: {
7
+ type: string;
8
+ properties: {
9
+ outcome?: {
10
+ type: string[];
11
+ } | undefined;
12
+ record: {
13
+ type: string;
14
+ };
15
+ action: {
16
+ type: string;
17
+ };
18
+ };
19
+ required: string[];
20
+ additionalProperties: boolean;
21
+ };
22
+ implementation: {
23
+ type: string;
24
+ function: string;
25
+ retries: number;
26
+ timeout: number;
27
+ };
28
+ }[];
29
+ //# sourceMappingURL=action-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-tools.d.ts","sourceRoot":"","sources":["../../src/forms/action-tools.js"],"names":[],"mappings":"AAAA,uGAAuG;AACvG;;;;;;;;;;;;;;;;;;;;;;;;;;IAII"}
@@ -0,0 +1,6 @@
1
+ /** Internal transport ports. Registered by WorkflowEngine; no network implementation lives in JSFE. */
2
+ export const ACTION_TOOLS = ['claim', 'complete'].map(operation => ({
3
+ id: `jsfe:action-${operation}`, name: `JSFE action ${operation}`, description: `Await the configured durable action journal ${operation} port.`,
4
+ parameters: { type: 'object', properties: { record: { type: 'object' }, action: { type: 'object' }, ...(operation === 'complete' ? { outcome: { type: ['object', 'null'] } } : {}) }, required: ['record', 'action'], additionalProperties: false },
5
+ implementation: { type: 'local', function: operation === 'claim' ? 'jsfeClaimAction' : 'jsfeCompleteAction', retries: 0, timeout: 10000 },
6
+ }));
@@ -0,0 +1,2 @@
1
+ export const FLOWS: any[];
2
+ //# sourceMappingURL=address-flows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address-flows.d.ts","sourceRoot":"","sources":["../../src/forms/address-flows.js"],"names":[],"mappings":"AAsCA,0BAAwE"}
@@ -0,0 +1,39 @@
1
+ /** Shared geocoded mailing-address collector over host-provided geocode tools. */
2
+ import { defineForm, defineWorker } from './contracts.js';
3
+ import { addressDraft, zipLocation, streetLocation, resolvedAddress } from '../helpers/address.js';
4
+ const S = (id, variable, value) => ({ id, type: 'SET', variable, value });
5
+ const F = (id, value, parameters = {}, callType = 'call') => ({ id, type: 'FLOW', value, parameters, callType });
6
+ const W = (id, condition, step) => ({ id, type: 'CASE', branches: { ['condition: ' + condition]: step, default: S(id + '_skip', 'noop', 'true') } });
7
+ const V = o => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, { type: Array.isArray(v) ? 'array' : v === null || typeof v === 'object' ? 'object' : typeof v, value: v }]));
8
+ const E = (fn, ...args) => `(${fn.toString()})(${args.join(',')})`;
9
+ const P = (name, type = 'string') => ({ name, type, description: name });
10
+ const C = (id, summary, steps, extra = {}) => defineWorker({ id, name: id.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join(''), description: summary, ...extra, contract: { role: 'worker', summary, ...extra.contract }, steps });
11
+ const streetPattern = '\\b\\d{1,6}\\s+(?:[A-Za-z0-9.\\xC0-\\xFF-]+\\s+){1,8}(?:st(?:reet)?|ave(?:nue)?|blvd|boulevard|rd|road|ln|lane|dr(?:ive)?|way|ct|court|pl(?:ace)?|pkwy|parkway|cir(?:cle)?|ter(?:race)?)\\b';
12
+ const address = defineForm({ id: 'collect-geocoded-address', name: 'CollectGeocodedAddress', version: '1.0.0', description: 'Collect a mailing address in any order, project geocoder evidence, and read the complete address back for confirmation.', contract: { summary: 'Collect and confirm a geocoded mailing address', result: { path: 'form result.resolved', type: 'object' } }, parameters: [P('given')], extraVars: { given: '', zip: '', street: '', unit: '', draft: null, address_result: null, exit_context: 'your address', exit_context_es: 'su dirección' },
13
+ before: [S('address_given', 'given', "given || (cargo.forms[cargo.forms[form_instance].parent] || {}).context && (cargo.forms[cargo.forms[form_instance].parent].context.given || '') || ''"), S('address_draft', 'draft', E(addressDraft, "given || (cargo.forms[form_instance].seeds && cargo.forms[form_instance].seeds.given) || ''")), S('address_seed_zip', 'zip', "zip || draft.zip"), S('address_seed_street', 'street', "street || draft.street"), S('address_seed_unit', 'unit', "unit || draft.unit"), S('address_data', 'noop', "(cargo.forms[form_instance].data=Object.assign({need_city:false},cargo.forms[form_instance].data || {}), draft.street ? (cargo.addressDraft={said:draft.street}) : null, true)")],
14
+ inputs: [{ name: 'zip', field: 'zip', validator: 'address-zip-validate', prepare: 'address-zip-prompt', question_from: 'zip_question', question: 'What is the 5-digit zip code of your new address?', question_es: '¿Cuál es el código postal de 5 dígitos de su nueva dirección?' },
15
+ { name: 'city_state', label: 'city and state', label_es: 'ciudad y estado', profile: { kind: 'text' }, when: { 'form.data.need_city': true }, depends_on: ['zip'], question: 'What city and state is that in?', question_es: '¿En qué ciudad y estado está?' },
16
+ { name: 'street', label: 'street address', label_es: 'dirección', profile: { kind: 'pattern', regex: streetPattern, accept_other: true }, depends_on: ['zip'], validator: 'address-street-validate', prepare: 'address-street-prompt', question_from: 'street_question', question: 'What is the street address?', question_es: '¿Cuál es la dirección?' },
17
+ { name: 'unit', reset_on: ['street'], label: 'apartment or unit', label_es: 'apartamento o unidad', profile: { kind: 'pattern', regex: '(?:\\bapt\\.?|\\bunit|#)\\s*([\\w-]+)', group: 1, accept_other: true }, question: 'Is there an apartment or unit number? Type the number, or NONE.', question_es: '¿Hay un número de apartamento o unidad? Escriba el número, o NINGUNO.' }],
18
+ before_confirm: [S('address_resolved', 'address_result', E(resolvedAddress, 'cargo.forms[form_instance].values', 'cargo.forms[form_instance].data'))],
19
+ confirm: { no_keys: ['2', 'not right', 'not correct', 'incorrect', 'wrong', 'no es correcto', 'no es correcta', 'incorrecta'], question: 'Your new address is {{address_result.line}}. Is that correct?', question_es: 'Su nueva dirección es {{address_result.line}}. ¿Es correcta?' },
20
+ after: [S('address_result_store', 'noop', '(cargo.forms[form_instance].values.resolved = address_result, cargo.address = address_result, cargo.addressDraft = null, true)')], declined: [S('address_retry_noop', 'noop', 'true')] });
21
+ const zip = C('address-zip-validate', 'Read city and state from geocoder evidence for the proposed ZIP; missing geocoder data requires an explicit city.', [
22
+ S('address_zip_components', 'components', "'postal_code:'+value+'|country:US'"), { id: 'address_zip_tool', type: 'CALL-TOOL', tool: 'geocode-zip', variable: 'result', args: { components: '{{components}}', address: '' }, onFail: S('address_zip_failed', 'result', 'null') },
23
+ S('address_zip_location', 'location', E(zipLocation, 'result')),
24
+ S('address_zip_result', 'noop', "(cargo.forms[form].data=Object.assign({},cargo.forms[form].data,{location:location,need_city:!location}), cargo.inputCheck={ok:true,value:String(value)}, true)")
25
+ ], { parameters: [P('form'), P('value')], variables: V({ components: '', result: null, location: null }), contract: { tools: ['geocode-zip'], result: { path: 'cargo.inputCheck', type: 'object' } } });
26
+ const street = C('address-street-validate', 'Validate a proposed street against its ZIP; retain unverified wording when geocoding is inconclusive.', [
27
+ W('address_street_malformed', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", S('address_street_invalid', 'noop', "(cargo.inputCheck={ok:false,message:'Please give the street number and street name.',message_es:'Indique el número y nombre de la calle.'},true)")), W('address_street_stop', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", { id: 'address_street_invalid_end', type: 'END' }),
28
+ S('address_street_components', 'components', "'postal_code:'+values.zip+'|country:US'"), { id: 'address_street_tool', type: 'CALL-TOOL', tool: 'geocode-street', variable: 'result', args: { address: '{{value}}', components: '{{components}}' }, onFail: S('address_street_failed', 'result', 'null') },
29
+ S('address_street_projection', 'location', E(streetLocation, 'result', 'value', 'values.zip')),
30
+ S('address_street_result', 'noop', "(cargo.forms[form].data.street=location, cargo.inputCheck={ok:true,value:String(value)}, true)")
31
+ ], { parameters: [P('form'), P('value'), P('values', 'object')], variables: V({ components: '', result: null, location: null }), contract: { tools: ['geocode-street'], result: { path: 'cargo.inputCheck', type: 'object' } } });
32
+ const zipPrompt = C('address-zip-prompt', 'Render the ZIP retry wording from the owned address instance.', [S('address_zip_prompt', 'noop', "(cargo.forms[form].data.zip_question=(cargo.forms[form].data.retry ? \"Let's try again. \" : '')+'What is the 5-digit zip code of your new address?',cargo.forms[form].data.zip_question_es=(cargo.forms[form].data.retry?'Intentémoslo otra vez. ':'')+'¿Cuál es el código postal de 5 dígitos de su nueva dirección?',true)")], { parameters: [P('form')], contract: { result: { path: 'cargo.forms[form].data', type: 'object' } } });
33
+ const streetPrompt = C('address-street-prompt', 'Include the ZIP-derived location when asking for the street.', [S('address_street_prompt', 'noop', "(cargo.forms[form].data.street_question=(cargo.forms[form].data.location?'Got it, '+cargo.forms[form].data.location.line+'. ':'')+'What is the street address?',cargo.forms[form].data.street_question_es=(cargo.forms[form].data.location?'Entendido, '+cargo.forms[form].data.location.line+'. ':'')+'¿Cuál es la dirección?',true)")], { parameters: [P('form')], contract: { result: { path: 'cargo.forms[form].data', type: 'object' } } });
34
+ const declined = C('collect-geocoded-address-declined', 'Start a fresh address pass when the complete address is declined.', [
35
+ S('address_retry_owner', 'parent_form', 'cargo.forms[form_instance].parent'), S('address_retry_input', 'result_input', 'cargo.forms[form_instance].result_input'),
36
+ S('address_retry_clear', 'noop', "(resetFormInputs(cargo,form_instance,['zip','city_state','street','unit']),cargo.forms[form_instance].data={retry:true,need_city:false},true)"),
37
+ F('address_retry', 'collect-geocoded-address', { form_instance: '{{form_instance}}', parent_form: '{{parent_form}}', result_input: '{{result_input}}' }, 'replace')
38
+ ], { parameters: [P('form_instance')], variables: V({ parent_form: '', result_input: '' }) });
39
+ export const FLOWS = [address, zip, street, zipPrompt, streetPrompt, declined];