jsfe 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -4
- package/dist/forms/action-projection.d.ts +4 -0
- package/dist/forms/action-projection.d.ts.map +1 -0
- package/dist/forms/action-projection.js +21 -0
- package/dist/forms/action-recovery.d.ts +12 -0
- package/dist/forms/action-recovery.d.ts.map +1 -0
- package/dist/forms/action-recovery.js +125 -0
- package/dist/forms/action-tools.d.ts +29 -0
- package/dist/forms/action-tools.d.ts.map +1 -0
- package/dist/forms/action-tools.js +6 -0
- package/dist/forms/address-flows.d.ts +2 -0
- package/dist/forms/address-flows.d.ts.map +1 -0
- package/dist/forms/address-flows.js +39 -0
- package/dist/forms/choice-flow.d.ts +419 -0
- package/dist/forms/choice-flow.d.ts.map +1 -0
- package/dist/forms/choice-flow.js +707 -0
- package/dist/forms/contracts.d.ts +237 -0
- package/dist/forms/contracts.d.ts.map +1 -0
- package/dist/forms/contracts.js +278 -0
- package/dist/forms/flows.d.ts +611 -0
- package/dist/forms/flows.d.ts.map +1 -0
- package/dist/forms/flows.js +675 -0
- package/dist/forms/identity-flows.d.ts +17 -0
- package/dist/forms/identity-flows.d.ts.map +1 -0
- package/dist/forms/identity-flows.js +302 -0
- package/dist/forms/index.d.ts +7 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +10 -0
- package/dist/forms/prompt-compiler.d.ts +3 -0
- package/dist/forms/prompt-compiler.d.ts.map +1 -0
- package/dist/forms/prompt-compiler.js +30 -0
- package/dist/forms/session-actions.d.ts +14 -0
- package/dist/forms/session-actions.d.ts.map +1 -0
- package/dist/forms/session-actions.js +40 -0
- package/dist/helpers/address.d.ts +27 -0
- package/dist/helpers/address.d.ts.map +1 -0
- package/dist/helpers/address.js +29 -0
- package/dist/helpers/answer.d.ts +5 -0
- package/dist/helpers/answer.d.ts.map +1 -1
- package/dist/helpers/answer.js +5 -3
- package/dist/helpers/choice-offer.d.ts +3 -0
- package/dist/helpers/choice-offer.d.ts.map +1 -0
- package/dist/helpers/choice-offer.js +6 -0
- package/dist/helpers/form-actions.d.ts +13 -0
- package/dist/helpers/form-actions.d.ts.map +1 -0
- package/dist/helpers/form-actions.js +144 -0
- package/dist/helpers/form-sources.d.ts +7 -0
- package/dist/helpers/form-sources.d.ts.map +1 -0
- package/dist/helpers/form-sources.js +47 -0
- package/dist/helpers/form-state.d.ts +94 -0
- package/dist/helpers/form-state.d.ts.map +1 -0
- package/dist/helpers/form-state.js +870 -0
- package/dist/helpers/index.d.ts +114 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +9 -4
- package/dist/helpers/match-inputs.d.ts +14 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -1
- package/dist/helpers/match-inputs.js +154 -41
- package/dist/helpers/money.d.ts +12 -0
- package/dist/helpers/money.d.ts.map +1 -0
- package/dist/helpers/money.js +54 -0
- package/dist/helpers/spoken-money.d.ts +20 -0
- package/dist/helpers/spoken-money.d.ts.map +1 -0
- package/dist/helpers/spoken-money.js +143 -0
- package/dist/host/action-journal.d.ts +98 -0
- package/dist/host/action-journal.d.ts.map +1 -0
- package/dist/host/action-journal.js +218 -0
- package/dist/host/action-session.d.ts +12 -0
- package/dist/host/action-session.d.ts.map +1 -0
- package/dist/host/action-session.js +29 -0
- package/dist/host/capabilities.d.ts +14 -0
- package/dist/host/capabilities.d.ts.map +1 -0
- package/dist/host/capabilities.js +60 -0
- package/dist/host/checkpoint-codec-error.d.ts +6 -0
- package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
- package/dist/host/checkpoint-codec-error.js +4 -0
- package/dist/host/index.d.ts +6 -0
- package/dist/host/index.d.ts.map +1 -1
- package/dist/host/index.js +6 -0
- package/dist/host/intent-stages.d.ts +1 -0
- package/dist/host/intent-stages.d.ts.map +1 -1
- package/dist/host/intent-stages.js +3 -3
- package/dist/host/node.d.ts +17 -0
- package/dist/host/node.d.ts.map +1 -0
- package/dist/host/node.js +64 -0
- package/dist/host/recovery-identity.d.ts +16 -0
- package/dist/host/recovery-identity.d.ts.map +1 -0
- package/dist/host/recovery-identity.js +73 -0
- package/dist/host/recovery-policy.d.ts +5 -0
- package/dist/host/recovery-policy.d.ts.map +1 -0
- package/dist/host/recovery-policy.js +54 -0
- package/dist/host/runtime-manifest.d.ts +3 -0
- package/dist/host/runtime-manifest.d.ts.map +1 -0
- package/dist/host/runtime-manifest.js +38 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +186 -45
- package/dist/runtime-identity.d.ts +3 -0
- package/dist/runtime-identity.d.ts.map +1 -0
- package/dist/runtime-identity.js +1 -0
- package/docs/development-release.md +25 -0
- package/docs/forms.md +132 -0
- package/package.json +15 -4
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
export function providerQuestions(provides: any): {
|
|
2
|
+
key: any;
|
|
3
|
+
words: any;
|
|
4
|
+
}[];
|
|
5
|
+
export function providerParameter(provides: any): {
|
|
6
|
+
name: string;
|
|
7
|
+
type: string;
|
|
8
|
+
enum: any[];
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
/** Link declarative sub-forms before runtime so any descendant field can receive an early answer. */
|
|
12
|
+
export function linkFormSchemas(flows: any, { externalFlows }?: {
|
|
13
|
+
externalFlows?: never[] | undefined;
|
|
14
|
+
}): any;
|
|
15
|
+
export function defineWorker({ contract, ...flow }: {
|
|
16
|
+
[x: string]: any;
|
|
17
|
+
contract: any;
|
|
18
|
+
}): any;
|
|
19
|
+
export function defineProvider({ provides, contract, parameters, variables, steps, ...flow }: {
|
|
20
|
+
[x: string]: any;
|
|
21
|
+
provides: any;
|
|
22
|
+
contract: any;
|
|
23
|
+
parameters?: never[] | undefined;
|
|
24
|
+
variables?: {} | undefined;
|
|
25
|
+
steps: any;
|
|
26
|
+
}): any;
|
|
27
|
+
export function defineForm({ contract, ...spec }: {
|
|
28
|
+
[x: string]: any;
|
|
29
|
+
contract: any;
|
|
30
|
+
}): any;
|
|
31
|
+
export function definePerformer({ performs, contract, prepare, present, project, ...spec }: {
|
|
32
|
+
[x: string]: any;
|
|
33
|
+
performs: any;
|
|
34
|
+
contract: any;
|
|
35
|
+
prepare: any;
|
|
36
|
+
present: any;
|
|
37
|
+
project?: never[] | undefined;
|
|
38
|
+
}): any;
|
|
39
|
+
/** A registered tool gets one generated adapter with explicit args; no dynamic tool-name interpreter is needed. */
|
|
40
|
+
export function operationWorkers(tools: any): any;
|
|
41
|
+
/** Validate compiled declarations and all static references against the actual combined flow/tool registry. */
|
|
42
|
+
export function validateFlowContracts(flows: any, tools?: any[], { externalFlows, requireAll, presets }?: {
|
|
43
|
+
externalFlows?: never[] | undefined;
|
|
44
|
+
requireAll?: boolean | undefined;
|
|
45
|
+
presets?: {
|
|
46
|
+
phone: {
|
|
47
|
+
label: string;
|
|
48
|
+
label_es: string;
|
|
49
|
+
profile: {
|
|
50
|
+
kind: string;
|
|
51
|
+
};
|
|
52
|
+
digits: {
|
|
53
|
+
min: number;
|
|
54
|
+
max: number;
|
|
55
|
+
};
|
|
56
|
+
question: string;
|
|
57
|
+
question_es: string;
|
|
58
|
+
retry: string;
|
|
59
|
+
retry_es: string;
|
|
60
|
+
};
|
|
61
|
+
email: {
|
|
62
|
+
label: string;
|
|
63
|
+
label_es: string;
|
|
64
|
+
profile: {
|
|
65
|
+
kind: string;
|
|
66
|
+
};
|
|
67
|
+
question: string;
|
|
68
|
+
question_es: string;
|
|
69
|
+
retry: string;
|
|
70
|
+
retry_es: string;
|
|
71
|
+
};
|
|
72
|
+
zip: {
|
|
73
|
+
label: string;
|
|
74
|
+
label_es: string;
|
|
75
|
+
profile: {
|
|
76
|
+
kind: string;
|
|
77
|
+
min: number;
|
|
78
|
+
max: number;
|
|
79
|
+
};
|
|
80
|
+
digits: {
|
|
81
|
+
min: number;
|
|
82
|
+
max: number;
|
|
83
|
+
};
|
|
84
|
+
question: string;
|
|
85
|
+
question_es: string;
|
|
86
|
+
retry: string;
|
|
87
|
+
retry_es: string;
|
|
88
|
+
};
|
|
89
|
+
account_number: {
|
|
90
|
+
label: string;
|
|
91
|
+
label_es: string;
|
|
92
|
+
profile: {
|
|
93
|
+
kind: string;
|
|
94
|
+
pattern: string;
|
|
95
|
+
};
|
|
96
|
+
digits: {
|
|
97
|
+
min: number;
|
|
98
|
+
max: number;
|
|
99
|
+
};
|
|
100
|
+
question: string;
|
|
101
|
+
question_es: string;
|
|
102
|
+
retry: string;
|
|
103
|
+
retry_es: string;
|
|
104
|
+
};
|
|
105
|
+
ssn_last4: {
|
|
106
|
+
label: string;
|
|
107
|
+
label_es: string;
|
|
108
|
+
profile: {
|
|
109
|
+
kind: string;
|
|
110
|
+
min: number;
|
|
111
|
+
max: number;
|
|
112
|
+
};
|
|
113
|
+
digits: {
|
|
114
|
+
min: number;
|
|
115
|
+
max: number;
|
|
116
|
+
};
|
|
117
|
+
pii: boolean;
|
|
118
|
+
question: string;
|
|
119
|
+
question_es: string;
|
|
120
|
+
};
|
|
121
|
+
amount: {
|
|
122
|
+
label: string;
|
|
123
|
+
label_es: string;
|
|
124
|
+
profile: {
|
|
125
|
+
kind: string;
|
|
126
|
+
min: number;
|
|
127
|
+
};
|
|
128
|
+
amount: boolean;
|
|
129
|
+
question: string;
|
|
130
|
+
question_es: string;
|
|
131
|
+
};
|
|
132
|
+
street_address: {
|
|
133
|
+
label: string;
|
|
134
|
+
label_es: string;
|
|
135
|
+
profile: {
|
|
136
|
+
kind: string;
|
|
137
|
+
};
|
|
138
|
+
question: string;
|
|
139
|
+
question_es: string;
|
|
140
|
+
};
|
|
141
|
+
city: {
|
|
142
|
+
label: string;
|
|
143
|
+
label_es: string;
|
|
144
|
+
profile: {
|
|
145
|
+
kind: string;
|
|
146
|
+
};
|
|
147
|
+
question: string;
|
|
148
|
+
question_es: string;
|
|
149
|
+
};
|
|
150
|
+
state: {
|
|
151
|
+
label: string;
|
|
152
|
+
label_es: string;
|
|
153
|
+
profile: {
|
|
154
|
+
kind: string;
|
|
155
|
+
regex: string;
|
|
156
|
+
};
|
|
157
|
+
question: string;
|
|
158
|
+
question_es: string;
|
|
159
|
+
};
|
|
160
|
+
card_number: {
|
|
161
|
+
label: string;
|
|
162
|
+
label_es: string;
|
|
163
|
+
profile: {
|
|
164
|
+
kind: string;
|
|
165
|
+
min: number;
|
|
166
|
+
max: number;
|
|
167
|
+
};
|
|
168
|
+
digits: {
|
|
169
|
+
min: number;
|
|
170
|
+
max: number;
|
|
171
|
+
complete: {
|
|
172
|
+
first: string;
|
|
173
|
+
lengths: number[];
|
|
174
|
+
}[];
|
|
175
|
+
};
|
|
176
|
+
pii: boolean;
|
|
177
|
+
validator: string;
|
|
178
|
+
question: string;
|
|
179
|
+
question_es: string;
|
|
180
|
+
retry: string;
|
|
181
|
+
retry_es: string;
|
|
182
|
+
};
|
|
183
|
+
card_expiration: {
|
|
184
|
+
label: string;
|
|
185
|
+
label_es: string;
|
|
186
|
+
profile: {
|
|
187
|
+
kind: string;
|
|
188
|
+
regex: string;
|
|
189
|
+
};
|
|
190
|
+
digits: {
|
|
191
|
+
min: number;
|
|
192
|
+
max: number;
|
|
193
|
+
};
|
|
194
|
+
validator: string;
|
|
195
|
+
question: string;
|
|
196
|
+
question_es: string;
|
|
197
|
+
retry: string;
|
|
198
|
+
retry_es: string;
|
|
199
|
+
};
|
|
200
|
+
card_cvv: {
|
|
201
|
+
label: string;
|
|
202
|
+
label_es: string;
|
|
203
|
+
profile: {
|
|
204
|
+
kind: string;
|
|
205
|
+
min: number;
|
|
206
|
+
max: number;
|
|
207
|
+
};
|
|
208
|
+
digits: {
|
|
209
|
+
min: number;
|
|
210
|
+
max: number;
|
|
211
|
+
};
|
|
212
|
+
pii: boolean;
|
|
213
|
+
question: string;
|
|
214
|
+
question_es: string;
|
|
215
|
+
};
|
|
216
|
+
card_zip: {
|
|
217
|
+
label: string;
|
|
218
|
+
label_es: string;
|
|
219
|
+
profile: {
|
|
220
|
+
kind: string;
|
|
221
|
+
min: number;
|
|
222
|
+
max: number;
|
|
223
|
+
};
|
|
224
|
+
digits: {
|
|
225
|
+
min: number;
|
|
226
|
+
max: number;
|
|
227
|
+
};
|
|
228
|
+
question: string;
|
|
229
|
+
question_es: string;
|
|
230
|
+
};
|
|
231
|
+
} | undefined;
|
|
232
|
+
}): {
|
|
233
|
+
total: any;
|
|
234
|
+
declared: number;
|
|
235
|
+
missing: any[];
|
|
236
|
+
};
|
|
237
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/** Build-time contracts for conversational flows. Transport operations remain registered tools. */
|
|
2
|
+
import { ACTION_TOOLS } from './action-tools.js';
|
|
3
|
+
import { normalizeHostRequirements } from '../host/capabilities.js';
|
|
4
|
+
import { validateActionProjection, actionProjectionExpression } from './action-projection.js';
|
|
5
|
+
import { formFlow, FIELD_PRESETS } from './flows.js';
|
|
6
|
+
import { resolveFormInputs } from '../helpers/form-state.js';
|
|
7
|
+
const ROLES = new Set(['provider', 'performer', 'collector', 'gate', 'router', 'worker']);
|
|
8
|
+
const EFFECTS = new Set(['read', 'send', 'write', 'charge', 'cancel', 'handoff']);
|
|
9
|
+
const value = v => ({ type: Array.isArray(v) ? 'array' : v !== null && typeof v === 'object' ? 'object' : typeof v, value: v });
|
|
10
|
+
const fail = (id, message) => { throw new Error(`Flow contract ${id}: ${message}`); };
|
|
11
|
+
const stepList = steps => (steps || []).flatMap(step => [step, ...stepList(Object.values(step.branches || {})), ...(step.onFail ? stepList([step.onFail]) : [])]);
|
|
12
|
+
export function providerQuestions(provides) {
|
|
13
|
+
return [...(provides.facts || []), ...(provides.sets || [])].filter(f => (f.words || f.asks || []).length)
|
|
14
|
+
.map(f => ({ key: f.key, words: f.words || f.asks }));
|
|
15
|
+
}
|
|
16
|
+
export function providerParameter(provides) {
|
|
17
|
+
const entries = [...(provides.facts || []).filter(f => f.gloss || f.asks?.length), ...(provides.sets || [])];
|
|
18
|
+
return { name: 'asked', type: 'string', enum: entries.map(f => f.key), description: entries.map(f => `${f.key}: ${f.gloss || (f.asks || []).join('; ') || f.label || f.key}`).join('. ') };
|
|
19
|
+
}
|
|
20
|
+
/** Link declarative sub-forms before runtime so any descendant field can receive an early answer. */
|
|
21
|
+
export function linkFormSchemas(flows, { externalFlows = [] } = {}) {
|
|
22
|
+
const registry = new Map([...externalFlows, ...flows].map(flow => [flow.id, flow])), memo = new Map();
|
|
23
|
+
const inputsFor = (id, active = new Set()) => {
|
|
24
|
+
if (memo.has(id))
|
|
25
|
+
return memo.get(id);
|
|
26
|
+
if (active.has(id))
|
|
27
|
+
fail(id, 'recursive sub-form definitions need an explicit repeated-group protocol');
|
|
28
|
+
const flow = registry.get(id);
|
|
29
|
+
if (!flow || flow.metadata?.formProtocol !== 2)
|
|
30
|
+
return null;
|
|
31
|
+
active = new Set(active);
|
|
32
|
+
active.add(id);
|
|
33
|
+
const inputs = flow.variables.inputs.value.map(input => {
|
|
34
|
+
if (!input.form)
|
|
35
|
+
return input;
|
|
36
|
+
const target = registry.get(input.form);
|
|
37
|
+
if (!target)
|
|
38
|
+
fail(id, `unknown sub-form or worker ${input.form}`);
|
|
39
|
+
const child = inputsFor(input.form, active);
|
|
40
|
+
return child ? { ...input, schema: { definition: target.id, version: target.version, inputs: child, provides: target.variables?.provides?.value || null, questions: target.variables?.questions?.value || [], on_exhausted: target.variables?.form_on_exhausted?.value || null } } : input;
|
|
41
|
+
});
|
|
42
|
+
resolveFormInputs(inputs, FIELD_PRESETS);
|
|
43
|
+
memo.set(id, inputs);
|
|
44
|
+
return inputs;
|
|
45
|
+
};
|
|
46
|
+
const leafSchema = input => {
|
|
47
|
+
if (input.schema)
|
|
48
|
+
return objectSchema(input.schema.inputs);
|
|
49
|
+
if (input.profile?.kind === 'choice' && !input.profile.accept_other)
|
|
50
|
+
return { type: 'string', enum: (input.profile.choices || []).map(c => c.key), description: input.description || input.label || input.name };
|
|
51
|
+
return { type: 'string', description: input.description || input.label || input.name };
|
|
52
|
+
};
|
|
53
|
+
const objectSchema = inputs => {
|
|
54
|
+
const fields = inputs.flatMap(input => input.any_of || [input]).filter(input => input.schema || input.profile);
|
|
55
|
+
return { type: 'object', additionalProperties: false, required: fields.map(i => i.name), properties: Object.fromEntries(fields.map(input => [input.name, { anyOf: [leafSchema(input), { type: 'null' }] }])) };
|
|
56
|
+
};
|
|
57
|
+
return flows.map(flow => {
|
|
58
|
+
const inputs = inputsFor(flow.id);
|
|
59
|
+
if (!inputs)
|
|
60
|
+
return flow;
|
|
61
|
+
const parameters = [...(flow.parameters || [])];
|
|
62
|
+
if (flow.primary)
|
|
63
|
+
for (const input of inputs.flatMap(input => input.any_of || [input])) {
|
|
64
|
+
if (parameters.some(p => p.name === input.name) || !input.profile && !input.schema)
|
|
65
|
+
continue;
|
|
66
|
+
parameters.push(input.schema ? { name: input.name, type: 'object', jsonSchema: objectSchema(input.schema.inputs), description: input.description || input.label || input.name }
|
|
67
|
+
: { 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
|
+
return { ...flow, parameters, variables: { ...flow.variables, inputs: { ...flow.variables.inputs, value: inputs } } };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function declared(flow, contract) {
|
|
73
|
+
if (!ROLES.has(contract.role))
|
|
74
|
+
fail(flow.id, 'unknown role');
|
|
75
|
+
if (!contract.summary)
|
|
76
|
+
fail(flow.id, 'summary is required');
|
|
77
|
+
// Native declarations opt in explicitly; loading legacy flow JSON never changes its steps.
|
|
78
|
+
const replacementDefaults = step => ({ ...step,
|
|
79
|
+
...(step.type === 'FLOW' && step.callType === 'replace' ? { initializeDefaults: step.initializeDefaults ?? true } : {}),
|
|
80
|
+
...(step.branches ? { branches: Object.fromEntries(Object.entries(step.branches).map(([key, branch]) => [key, replacementDefaults(branch)])) } : {}),
|
|
81
|
+
...(step.onFail ? { onFail: replacementDefaults(step.onFail) } : {}) });
|
|
82
|
+
const metadata = { version: 1, ...contract, parametersFrom: 'parameters' };
|
|
83
|
+
if (contract.requires !== undefined)
|
|
84
|
+
Object.defineProperty(metadata, 'requires', {
|
|
85
|
+
value: normalizeHostRequirements(contract.requires, flow.id), enumerable: true, writable: false, configurable: false,
|
|
86
|
+
});
|
|
87
|
+
return { ...flow, steps: flow.steps.map(replacementDefaults), variables: { ...flow.variables, contract: value(metadata) } };
|
|
88
|
+
}
|
|
89
|
+
export function defineWorker({ contract, ...flow }) {
|
|
90
|
+
return declared({ scope: 'isolated', version: '1.0.0', ...flow }, contract);
|
|
91
|
+
}
|
|
92
|
+
export function defineProvider({ provides, contract, parameters = [], variables = {}, steps, ...flow }) {
|
|
93
|
+
const asked = providerParameter(provides);
|
|
94
|
+
return declared({ scope: 'isolated', version: '1.0.0', primary: true, ...flow,
|
|
95
|
+
parameters: [...parameters, asked], variables: { ...variables, asked: value(''), provides: value(provides) },
|
|
96
|
+
steps: steps || [{ id: `${flow.id}_provide`, type: 'FLOW', value: 'provide-data', callType: 'call', parameters: { provides: '{{provides}}', asked: '{{asked}}' } }] }, { role: 'provider', ...contract, catalog: 'provides' });
|
|
97
|
+
}
|
|
98
|
+
export function defineForm({ contract, ...spec }) {
|
|
99
|
+
const flow = formFlow(spec);
|
|
100
|
+
return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' } });
|
|
101
|
+
}
|
|
102
|
+
export function definePerformer({ performs, contract, prepare, present, project = [], ...spec }) {
|
|
103
|
+
validateActionProjection(project, spec.id);
|
|
104
|
+
if (!Array.isArray(performs) || !performs.length || !prepare || !present)
|
|
105
|
+
fail(spec.id, 'performer needs actions, request preparation and outcome presentation');
|
|
106
|
+
const confirm = spec.confirm || (performs.length === 1 && performs[0].confirm?.question ? performs[0].confirm : null);
|
|
107
|
+
if (performs.some(action => action.confirm?.exact) && !confirm)
|
|
108
|
+
fail(spec.id, 'exact action needs a declared form confirmation');
|
|
109
|
+
const set = (id, variable, expression) => ({ id: `${spec.id}_${id}`, type: 'SET', variable, value: expression });
|
|
110
|
+
const call = (id, target, parameters) => ({ id: `${spec.id}_${id}`, type: 'FLOW', value: target, callType: 'call', parameters });
|
|
111
|
+
const when = (id, condition, step) => ({ id: `${spec.id}_${id}`, type: 'CASE', branches: { [`condition: ${condition}`]: step, default: set(`${id}_skip`, 'noop', 'true') } });
|
|
112
|
+
const prepareSteps = [
|
|
113
|
+
set('request_reset', 'noop', '(cargo.actionRequest = null, true)'),
|
|
114
|
+
call('prepare', prepare, { form: '{{form_instance}}' }),
|
|
115
|
+
set('request', 'pf_request', 'cargo.actionRequest'),
|
|
116
|
+
set('select', 'pf_action', 'selectFormAction(cargo, form_instance, performs)'),
|
|
117
|
+
when('bad_request', "!(pf_action === null || pf_action && typeof pf_action === 'object' && pf_action.action) || pf_action !== null && (!pf_request || typeof pf_request !== 'object')", { id: `${spec.id}_prepare_failed`, type: 'FLOW', value: 'give-up', callType: 'reboot', parameters: { context: '{{exit_context}}', context_es: '{{exit_context_es}}' } }),
|
|
118
|
+
set('proposal', 'pf_record', "pf_action ? prepareFormAction(cargo, form_instance, pf_action, pf_request) : null"),
|
|
119
|
+
];
|
|
120
|
+
const flow = formFlow({ ...spec, confirm, confirmation_condition: "pf_action !== null && pf_action && typeof pf_action === 'object'",
|
|
121
|
+
extraVars: { ...spec.extraVars, performs, projection: project, pf_action: null, pf_request: null, pf_record: null, pf_result: null, pf_receipt: null },
|
|
122
|
+
before_confirm: confirm ? [...prepareSteps, ...(spec.before_confirm || [])] : spec.before_confirm || [],
|
|
123
|
+
after: [...(confirm ? [] : prepareSteps),
|
|
124
|
+
when('perform', 'pf_action && typeof pf_action === \'object\'', call('execute', 'perform-action', { form: '{{form_instance}}', action: '{{pf_action}}', request: '{{pf_request}}', defer_receipt: true })),
|
|
125
|
+
when('no_action', 'pf_action === null', set('not_applicable', 'noop', "(cargo.actionResult = {status:'not_applicable'}, cargo.actionReceipt=null, true)")),
|
|
126
|
+
set('result', 'pf_result', 'cargo.actionResult'),
|
|
127
|
+
set('receipt', 'pf_receipt', 'cargo.actionReceipt'),
|
|
128
|
+
{ ...set('project', 'pf_projected', actionProjectionExpression(project, { form: 'form_instance', formSnapshot: 'cargo.forms[form_instance]', request: 'pf_request', result: 'pf_result', key: "pf_record ? (pf_record.durableKey || pf_record.key) : ''" })), requireTrue: true },
|
|
129
|
+
{ ...set('receipt_applied', 'noop', 'pf_receipt ? consumeActionReceipt(cargo,pf_receipt) : true'), requireTrue: true },
|
|
130
|
+
call('present', present, { form: '{{form_instance}}', request: '{{pf_request}}', result: '{{pf_result}}' }),
|
|
131
|
+
...(spec.after || [])] });
|
|
132
|
+
return declared(flow, { role: 'performer', ...contract, requires: ['guardedActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection' });
|
|
133
|
+
}
|
|
134
|
+
/** A registered tool gets one generated adapter with explicit args; no dynamic tool-name interpreter is needed. */
|
|
135
|
+
export function operationWorkers(tools) {
|
|
136
|
+
const workers = tools.map(tool => {
|
|
137
|
+
const schema = tool.schema || tool.parameters || tool;
|
|
138
|
+
const names = Array.isArray(schema) ? schema.map(p => p.name) : Object.keys(schema?.properties || schema || {}).filter(name => schema?.properties || schema[name] && typeof schema[name] === 'object' && !Array.isArray(schema[name]));
|
|
139
|
+
const id = `operation:${tool.id}`;
|
|
140
|
+
return defineWorker({ id, name: `Operation ${tool.id}`, description: `Execute registered tool ${tool.id}; return its transport outcome.`,
|
|
141
|
+
parameters: [{ name: 'request', type: 'object', description: 'Explicit tool arguments' }], variables: { request: value({}), operation_result: value(null) },
|
|
142
|
+
contract: { role: 'worker', summary: `Transport adapter for ${tool.id}`, tools: [tool.id], result: { path: 'cargo.operationResult', type: 'object' } },
|
|
143
|
+
steps: [{ id: `${id}_call`, type: 'CALL-TOOL', tool: tool.id, variable: 'operation_result', args: Object.fromEntries(names.map(name => [name, `{{request.${name}}}`])),
|
|
144
|
+
onFail: { id: `${id}_fail`, type: 'FLOW', value: 'operation-failed', callType: 'replace', parameters: { error: '{{operation_result}}' } } },
|
|
145
|
+
{ id: `${id}_result`, type: 'SET', variable: 'noop', value: "(cargo.operationResult = { transport: 'ok', value: operation_result }, true)" }] });
|
|
146
|
+
});
|
|
147
|
+
workers.push(defineWorker({ id: 'operation-failed', name: 'OperationFailed', description: 'Return an explicit transport failure to the invoking performer or provider.',
|
|
148
|
+
parameters: [{ name: 'error', type: 'string', description: 'Transport failure' }], contract: { role: 'worker', summary: 'Transport failure result', result: { path: 'cargo.operationResult', type: 'object' } },
|
|
149
|
+
steps: [{ id: 'operation_failed_result', type: 'SET', variable: 'noop', value: "(cargo.operationResult = {transport:'failed',error:String(error || '')}, true)" }] }));
|
|
150
|
+
return workers;
|
|
151
|
+
}
|
|
152
|
+
/** Validate compiled declarations and all static references against the actual combined flow/tool registry. */
|
|
153
|
+
export function validateFlowContracts(flows, tools = [], { externalFlows = [], requireAll = true, presets = FIELD_PRESETS } = {}) {
|
|
154
|
+
const ids = new Set(externalFlows), names = new Set(), toolIds = new Set([...tools, ...ACTION_TOOLS].map(t => t.id));
|
|
155
|
+
for (const flow of flows) {
|
|
156
|
+
if (ids.has(flow.id))
|
|
157
|
+
fail(flow.id, 'duplicate id');
|
|
158
|
+
if (names.has(flow.name))
|
|
159
|
+
fail(flow.id, 'duplicate name');
|
|
160
|
+
ids.add(flow.id);
|
|
161
|
+
names.add(flow.name);
|
|
162
|
+
}
|
|
163
|
+
const missing = [];
|
|
164
|
+
for (const flow of flows) {
|
|
165
|
+
const contract = flow.variables?.contract?.value;
|
|
166
|
+
if (!contract) {
|
|
167
|
+
missing.push(flow.id);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (contract.version !== 1 || !ROLES.has(contract.role) || !contract.summary)
|
|
171
|
+
fail(flow.id, 'invalid contract header');
|
|
172
|
+
normalizeHostRequirements(contract.requires, flow.id);
|
|
173
|
+
if (contract.projects)
|
|
174
|
+
validateActionProjection(flow.variables?.[contract.projects]?.value, flow.id);
|
|
175
|
+
const exhaustion = flow.variables?.form_on_exhausted?.value;
|
|
176
|
+
if (exhaustion && !ids.has(exhaustion))
|
|
177
|
+
fail(flow.id, `unknown on_exhausted worker ${exhaustion}`);
|
|
178
|
+
if (flow.primary && !flow.description)
|
|
179
|
+
fail(flow.id, 'primary requires trigger description');
|
|
180
|
+
const inputSpecs = contract.inputs || flow.variables?.inputs?.value;
|
|
181
|
+
if (inputSpecs) {
|
|
182
|
+
// Already resolved field specs are valid without their original preset registry.
|
|
183
|
+
const specs = resolveFormInputs(inputSpecs, presets), fieldNames = new Set(specs.flatMap(i => [i.name, ...(i.any_of || []).map(m => m.name)]));
|
|
184
|
+
const graph = new Map();
|
|
185
|
+
for (const spec of specs.flatMap(i => [i, ...(i.any_of || [])])) {
|
|
186
|
+
const deps = [...(spec.reset_on || []), ...(spec.depends_on || []), ...Object.keys({ ...spec.when, ...spec.required_when }).filter(k => !k.startsWith('cargo.') && !k.startsWith('form.'))];
|
|
187
|
+
for (const name of deps)
|
|
188
|
+
if (!name.startsWith('cargo.') && !name.startsWith('form.') && !fieldNames.has(name))
|
|
189
|
+
fail(flow.id, `unknown dependency ${name}`);
|
|
190
|
+
graph.set(spec.name, deps);
|
|
191
|
+
for (const key of ['form', 'validator', 'prepare', 'refresh', 'after', 'answer_precheck', 'answer_validator', 'on_exhausted'])
|
|
192
|
+
if (spec[key] && !ids.has(spec[key]))
|
|
193
|
+
fail(flow.id, `unknown ${key} worker ${spec[key]}`);
|
|
194
|
+
}
|
|
195
|
+
const visited = new Set(), active = new Set();
|
|
196
|
+
const visit = name => { if (active.has(name))
|
|
197
|
+
fail(flow.id, `cyclic input dependency ${name}`); if (visited.has(name))
|
|
198
|
+
return; active.add(name); for (const dep of graph.get(name) || [])
|
|
199
|
+
visit(dep); active.delete(name); visited.add(name); };
|
|
200
|
+
for (const name of graph.keys())
|
|
201
|
+
visit(name);
|
|
202
|
+
}
|
|
203
|
+
const provides = contract.provides || (contract.catalog ? flow.variables?.[contract.catalog]?.value : null);
|
|
204
|
+
if (contract.role === 'provider' && !provides)
|
|
205
|
+
fail(flow.id, 'provider needs a catalog');
|
|
206
|
+
if (provides) {
|
|
207
|
+
if (!Array.isArray(provides.facts) || !provides.facts.length)
|
|
208
|
+
fail(flow.id, 'provider needs facts');
|
|
209
|
+
const keys = new Set();
|
|
210
|
+
for (const fact of provides.facts) {
|
|
211
|
+
if (!fact.key || keys.has(fact.key))
|
|
212
|
+
fail(flow.id, 'invalid or duplicate fact key');
|
|
213
|
+
keys.add(fact.key);
|
|
214
|
+
if (!fact.path && !fact.flow)
|
|
215
|
+
fail(flow.id, `fact ${fact.key} needs a source path or worker`);
|
|
216
|
+
if (fact.flow && !ids.has(fact.flow))
|
|
217
|
+
fail(flow.id, `unknown fact worker ${fact.flow}`);
|
|
218
|
+
}
|
|
219
|
+
for (const set of provides.sets || []) {
|
|
220
|
+
if (!set.key || keys.has(set.key))
|
|
221
|
+
fail(flow.id, 'invalid or duplicate fact set');
|
|
222
|
+
for (const key of set.facts || [])
|
|
223
|
+
if (!keys.has(key))
|
|
224
|
+
fail(flow.id, `unknown fact ${key}`);
|
|
225
|
+
keys.add(set.key);
|
|
226
|
+
}
|
|
227
|
+
if (provides.facts.some(f => !f.public) && (!provides.gate?.flow || ![].concat(provides.gate.verified || []).filter(Boolean).length))
|
|
228
|
+
fail(flow.id, 'private provider needs an explicit gate and verification condition');
|
|
229
|
+
for (const reference of [provides.gate?.flow, provides.source?.flow])
|
|
230
|
+
if (reference && !ids.has(reference))
|
|
231
|
+
fail(flow.id, `unknown provider worker ${reference}`);
|
|
232
|
+
}
|
|
233
|
+
for (const action of contract.performs || flow.variables?.[contract.actions]?.value || []) {
|
|
234
|
+
if (action.retry?.unknown && !(action.effect === 'send' && action.retry.unknown === 'explicit'))
|
|
235
|
+
fail(flow.id, 'unknown effects may be explicitly repeated only for declared send actions');
|
|
236
|
+
if (!action.action || !EFFECTS.has(action.effect))
|
|
237
|
+
fail(flow.id, 'action needs a name and effect');
|
|
238
|
+
if (!action.tool && !action.flow)
|
|
239
|
+
fail(flow.id, `action ${action.action} needs a tool or execution worker`);
|
|
240
|
+
if (action.tool && !toolIds.has(action.tool))
|
|
241
|
+
fail(flow.id, `unknown action tool ${action.tool}`);
|
|
242
|
+
if (action.flow && !ids.has(action.flow))
|
|
243
|
+
fail(flow.id, `unknown action worker ${action.flow}`);
|
|
244
|
+
if (!action.outcomes || !Object.keys(action.outcomes).length)
|
|
245
|
+
fail(flow.id, `action ${action.action} needs outcomes`);
|
|
246
|
+
if (['charge', 'write', 'send', 'cancel'].includes(action.effect) && !action.idempotency && !action.reconcile)
|
|
247
|
+
fail(flow.id, `action ${action.action} needs idempotency or reconciliation`);
|
|
248
|
+
if ((action.effect === 'charge' || action.effect === 'cancel' || action.reversible === false) && action.confirm?.exact !== true)
|
|
249
|
+
fail(flow.id, `action ${action.action} needs exact confirmation`);
|
|
250
|
+
}
|
|
251
|
+
for (const tool of contract.tools || [])
|
|
252
|
+
if (!toolIds.has(tool))
|
|
253
|
+
fail(flow.id, `unknown tool ${tool}`);
|
|
254
|
+
for (const step of stepList(flow.steps)) {
|
|
255
|
+
if (step.type === 'FLOW' || step.type === 'CALL-TOOL') {
|
|
256
|
+
const inspect = value => {
|
|
257
|
+
if (typeof value === 'string')
|
|
258
|
+
for (const match of value.matchAll(/\{\{(.*?)\}\}/g)) {
|
|
259
|
+
if (!/^[\w$]+(?:\.[\w$]+)*$/.test(match[1].trim()))
|
|
260
|
+
fail(flow.id, `step ${step.id} arguments support dotted paths, not expressions`);
|
|
261
|
+
}
|
|
262
|
+
else if (value && typeof value === 'object')
|
|
263
|
+
Object.values(value).forEach(inspect);
|
|
264
|
+
};
|
|
265
|
+
inspect(step.parameters || step.args);
|
|
266
|
+
}
|
|
267
|
+
if (step.type === 'FLOW' && typeof step.value === 'string' && !step.value.includes('{{') && !ids.has(step.value))
|
|
268
|
+
fail(flow.id, `unknown flow ${step.value}`);
|
|
269
|
+
if (step.type === 'CALL-TOOL' && (!toolIds.has(step.tool) || !step.onFail))
|
|
270
|
+
fail(flow.id, `tool call ${step.id} needs a registered tool and onFail`);
|
|
271
|
+
if (step.type === 'SAY-GET' && contract.role !== 'collector')
|
|
272
|
+
fail(flow.id, 'input collection must use a collector');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (requireAll && missing.length)
|
|
276
|
+
throw new Error(`Missing flow contracts: ${missing.join(', ')}`);
|
|
277
|
+
return { total: flows.length, declared: flows.length - missing.length, missing };
|
|
278
|
+
}
|