@llm4ts/shell 2.12.0 → 2.13.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 (61) hide show
  1. package/flows/epic-stories.js +9 -3
  2. package/flows/lib/epic-stories.js +24 -4
  3. package/kits/soap-ace/README.md +313 -0
  4. package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +102 -0
  5. package/kits/soap-ace/fixtures/demo-bank-soap/design/api-design.md +620 -0
  6. package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaConti.xml +3 -0
  7. package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaMovimenti.xml +3 -0
  8. package/kits/soap-ace/fixtures/demo-bank-soap/responses/confermaBonifico.xml +3 -0
  9. package/kits/soap-ace/fixtures/demo-bank-soap/responses/dettaglioConto.xml +3 -0
  10. package/kits/soap-ace/fixtures/demo-bank-soap/responses/inserisciBonifico.xml +3 -0
  11. package/kits/soap-ace/fixtures/demo-bank-soap/responses/revocaBonifico.xml +3 -0
  12. package/kits/soap-ace/fixtures/demo-bank-soap/soapui/DemoBank-soapui-project.xml +39 -0
  13. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/DemoBank.wsdl +186 -0
  14. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/LegacyRpc.wsdl +29 -0
  15. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/banking.xsd +10 -0
  16. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/bonifici.xsd +107 -0
  17. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/common.xsd +93 -0
  18. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/conti.xsd +71 -0
  19. package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/movimenti.xsd +55 -0
  20. package/kits/soap-ace/flows/lib/soap/Analysis.js +508 -0
  21. package/kits/soap-ace/flows/lib/soap/Auth.js +179 -0
  22. package/kits/soap-ace/flows/lib/soap/Call.js +186 -0
  23. package/kits/soap-ace/flows/lib/soap/CallPolicy.js +50 -0
  24. package/kits/soap-ace/flows/lib/soap/Catalog.js +157 -0
  25. package/kits/soap-ace/flows/lib/soap/Classification.js +254 -0
  26. package/kits/soap-ace/flows/lib/soap/Design.js +497 -0
  27. package/kits/soap-ace/flows/lib/soap/DesignProposal.js +204 -0
  28. package/kits/soap-ace/flows/lib/soap/Discover.js +139 -0
  29. package/kits/soap-ace/flows/lib/soap/Envelope.js +132 -0
  30. package/kits/soap-ace/flows/lib/soap/Epic.js +239 -0
  31. package/kits/soap-ace/flows/lib/soap/Instance.js +675 -0
  32. package/kits/soap-ace/flows/lib/soap/Mapping.js +130 -0
  33. package/kits/soap-ace/flows/lib/soap/Masking.js +508 -0
  34. package/kits/soap-ace/flows/lib/soap/OpenApi.js +288 -0
  35. package/kits/soap-ace/flows/lib/soap/Samples.js +287 -0
  36. package/kits/soap-ace/flows/lib/soap/Scenarios.js +132 -0
  37. package/kits/soap-ace/flows/lib/soap/SoapUiImport.js +200 -0
  38. package/kits/soap-ace/flows/lib/soap/Stub.js +34 -0
  39. package/kits/soap-ace/flows/lib/soap/Transport.js +167 -0
  40. package/kits/soap-ace/flows/lib/soap/Wsdl.js +533 -0
  41. package/kits/soap-ace/flows/lib/soap/Xml.js +453 -0
  42. package/kits/soap-ace/flows/lib/soap/Xsd.js +471 -0
  43. package/kits/soap-ace/flows/lib/soap/Yaml.js +309 -0
  44. package/kits/soap-ace/flows/soap-design.js +167 -0
  45. package/kits/soap-ace/flows/soap-discover.js +85 -0
  46. package/kits/soap-ace/flows/soap-epic.js +204 -0
  47. package/kits/soap-ace/flows/soap-sample.js +265 -0
  48. package/kits/soap-ace/packs/ace12-rest/api-style.md +63 -0
  49. package/kits/soap-ace/packs/ace12-rest/pack.md +18 -0
  50. package/kits/soap-ace/packs/ace12-rest/prompts/implement.md +8 -0
  51. package/kits/soap-ace/packs/ace12-rest/reviewers/contract.md +5 -0
  52. package/kits/soap-ace/patterns/PAT-ACE-001-esito-to-problem.md +8 -0
  53. package/kits/soap-ace/patterns/PAT-ACE-002-fault-to-problem.md +8 -0
  54. package/kits/soap-ace/patterns/PAT-ACE-003-amounts-and-enums.md +8 -0
  55. package/kits/soap-ace/patterns/PAT-ACE-004-pagination.md +7 -0
  56. package/kits/soap-ace/patterns/PAT-ACE-005-nil-and-absence.md +7 -0
  57. package/kits/soap-ace/scaffolds/ace12-rest-api/CONTRIBUTING.md +56 -0
  58. package/kits/soap-ace/scaffolds/ace12-rest-api/README.md +16 -0
  59. package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates-container.sh +8 -0
  60. package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates.sh +41 -0
  61. package/package.json +4 -4
@@ -0,0 +1,675 @@
1
+ import { ComplexTypeDef, isBuiltin, localName, SimpleTypeDef, splitClark, typeByName } from "./Catalog.js";
2
+ import { kindForName } from "./Masking.js";
3
+ import { escapeAttribute, escapeText, elements, textOf } from "./Xml.js";
4
+ import { isYamlList, isYamlMap, yamlKey, yamlScalar } from "./Yaml.js";
5
+ // Instances of catalog types as plain value trees (the shape request files
6
+ // are written in): the commented YAML skeleton of a request, validation of
7
+ // a value against its XSD type, and conversion both ways between values and
8
+ // XML. Leaves are strings (XML text); `null` is `xsi:nil`. Attributes are
9
+ // keys starting with `@`; the text of a simple-content type is `#text`.
10
+ // Child order on the wire follows the XSD sequence, not the file.
11
+ export const XsiNamespace = "http://www.w3.org/2001/XMLSchema-instance";
12
+ export const simpleView = (catalog, name) => {
13
+ const facets = [];
14
+ const chain = [];
15
+ let current = name;
16
+ for (let depth = 0; depth < 16 && !isBuiltin(current); depth++) {
17
+ const type = typeByName(catalog, current);
18
+ if (!(type instanceof SimpleTypeDef))
19
+ break;
20
+ facets.push(type.facets);
21
+ chain.push(localName(type.name));
22
+ current = type.base;
23
+ }
24
+ return { builtin: isBuiltin(current) ? localName(current) : "string", facets, chain };
25
+ };
26
+ const integerTypes = new Set([
27
+ "int",
28
+ "integer",
29
+ "long",
30
+ "short",
31
+ "byte",
32
+ "nonNegativeInteger",
33
+ "positiveInteger",
34
+ "nonPositiveInteger",
35
+ "negativeInteger",
36
+ "unsignedInt",
37
+ "unsignedLong",
38
+ "unsignedShort",
39
+ "unsignedByte"
40
+ ]);
41
+ const lexical = {
42
+ boolean: /^(true|false|1|0)$/,
43
+ decimal: /^[+-]?(\d+(\.\d*)?|\.\d+)$/,
44
+ float: /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)$/,
45
+ double: /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)$/,
46
+ date: /^-?\d{4,}-\d{2}-\d{2}(Z|[+-]\d{2}:\d{2})?$/,
47
+ dateTime: /^-?\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/,
48
+ time: /^\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/,
49
+ gYear: /^-?\d{4,}(Z|[+-]\d{2}:\d{2})?$/,
50
+ base64Binary: /^[A-Za-z0-9+/\s]*=?=?\s*$/,
51
+ hexBinary: /^([0-9A-Fa-f]{2})*$/
52
+ };
53
+ /** XSD patterns are implicitly anchored and mostly JS-compatible. */
54
+ const xsdRegex = (pattern) => {
55
+ try {
56
+ return new RegExp(`^(?:${pattern.replace(/\\i/g, "[A-Za-z_:]").replace(/\\c/g, "[A-Za-z0-9._:-]")})$`, "u");
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ };
62
+ const digitsOf = (value) => {
63
+ const unsigned = value.replace(/^[+-]/, "");
64
+ const [whole = "", fraction = ""] = unsigned.split(".");
65
+ const trimmedFraction = fraction.replace(/0+$/, "");
66
+ return {
67
+ total: whole.replace(/^0+(?=\d)/, "").length + trimmedFraction.length,
68
+ fraction: trimmedFraction.length
69
+ };
70
+ };
71
+ export const checkSimple = (catalog, type, value) => {
72
+ const view = simpleView(catalog, type);
73
+ const problems = [];
74
+ const builtin = view.builtin;
75
+ if (integerTypes.has(builtin)) {
76
+ if (!/^[+-]?\d+$/.test(value))
77
+ problems.push(`"${value}" is not an ${builtin}`);
78
+ }
79
+ else {
80
+ const shape = lexical[builtin];
81
+ if (shape !== undefined && !shape.test(value))
82
+ problems.push(`"${value}" is not a ${builtin}`);
83
+ }
84
+ const numeric = integerTypes.has(builtin) || ["decimal", "float", "double"].includes(builtin);
85
+ for (const facets of view.facets) {
86
+ if (facets.enumeration !== undefined && !facets.enumeration.includes(value)) {
87
+ problems.push(`"${value}" is not one of ${facets.enumeration.join(", ")}`);
88
+ }
89
+ if (facets.pattern !== undefined && facets.pattern.length > 0) {
90
+ const matches = facets.pattern.some((pattern) => xsdRegex(pattern)?.test(value) ?? true);
91
+ if (!matches)
92
+ problems.push(`"${value}" does not match ${facets.pattern.join(" | ")}`);
93
+ }
94
+ const length = [...value].length;
95
+ if (facets.length !== undefined && length !== facets.length) {
96
+ problems.push(`length ${length}, expected ${facets.length}`);
97
+ }
98
+ if (facets.minLength !== undefined && length < facets.minLength) {
99
+ problems.push(`length ${length} below ${facets.minLength}`);
100
+ }
101
+ if (facets.maxLength !== undefined && length > facets.maxLength) {
102
+ problems.push(`length ${length} above ${facets.maxLength}`);
103
+ }
104
+ if (numeric && /^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(value)) {
105
+ const number = Number(value);
106
+ const digits = digitsOf(value);
107
+ if (facets.totalDigits !== undefined && digits.total > facets.totalDigits) {
108
+ problems.push(`${digits.total} digits, at most ${facets.totalDigits}`);
109
+ }
110
+ if (facets.fractionDigits !== undefined && digits.fraction > facets.fractionDigits) {
111
+ problems.push(`${digits.fraction} fraction digits, at most ${facets.fractionDigits}`);
112
+ }
113
+ if (facets.minInclusive !== undefined && number < Number(facets.minInclusive)) {
114
+ problems.push(`${value} below ${facets.minInclusive}`);
115
+ }
116
+ if (facets.maxInclusive !== undefined && number > Number(facets.maxInclusive)) {
117
+ problems.push(`${value} above ${facets.maxInclusive}`);
118
+ }
119
+ if (facets.minExclusive !== undefined && number <= Number(facets.minExclusive)) {
120
+ problems.push(`${value} not above ${facets.minExclusive}`);
121
+ }
122
+ if (facets.maxExclusive !== undefined && number >= Number(facets.maxExclusive)) {
123
+ problems.push(`${value} not below ${facets.maxExclusive}`);
124
+ }
125
+ }
126
+ }
127
+ return problems;
128
+ };
129
+ // ---------------------------------------------------------------------------
130
+ // Example values
131
+ /** A string matching a simple XSD pattern: literals, classes, groups (first branch), quantifiers. */
132
+ export const patternExample = (pattern) => {
133
+ let index = 0;
134
+ const atom = () => {
135
+ const char = pattern[index];
136
+ if (char === undefined)
137
+ return undefined;
138
+ if (char === "(") {
139
+ index++;
140
+ const start = index;
141
+ let depth = 1;
142
+ while (index < pattern.length && depth > 0) {
143
+ if (pattern[index] === "\\")
144
+ index++;
145
+ else if (pattern[index] === "(")
146
+ depth++;
147
+ else if (pattern[index] === ")")
148
+ depth--;
149
+ index++;
150
+ }
151
+ const inner = pattern.slice(start, index - 1).replace(/^\?:/, "");
152
+ let branchDepth = 0;
153
+ let cut = inner.length;
154
+ for (let position = 0; position < inner.length; position++) {
155
+ const current = inner[position];
156
+ if (current === "\\")
157
+ position++;
158
+ else if (current === "(" || current === "[")
159
+ branchDepth++;
160
+ else if (current === ")" || current === "]")
161
+ branchDepth--;
162
+ else if (current === "|" && branchDepth === 0) {
163
+ cut = position;
164
+ break;
165
+ }
166
+ }
167
+ return patternExample(inner.slice(0, cut));
168
+ }
169
+ if (char === "[") {
170
+ const end = pattern.indexOf("]", index + 1);
171
+ if (end < 0)
172
+ return undefined;
173
+ const body = pattern.slice(index + 1, end);
174
+ index = end + 1;
175
+ if (body.startsWith("^"))
176
+ return undefined;
177
+ if (body.startsWith("\\d") || /^0-9/.test(body))
178
+ return "0";
179
+ const first = body.startsWith("\\") ? body[1] : body[0];
180
+ return first === undefined ? undefined : first;
181
+ }
182
+ if (char === "\\") {
183
+ const escaped = pattern[index + 1];
184
+ index += 2;
185
+ if (escaped === "d")
186
+ return "0";
187
+ if (escaped === "w")
188
+ return "a";
189
+ if (escaped === "s")
190
+ return " ";
191
+ if (escaped === undefined)
192
+ return undefined;
193
+ return /[A-Za-z]/.test(escaped) ? undefined : escaped;
194
+ }
195
+ if (char === ".") {
196
+ index++;
197
+ return "a";
198
+ }
199
+ if ("|)*+?{".includes(char))
200
+ return undefined;
201
+ index++;
202
+ return char;
203
+ };
204
+ let result = "";
205
+ while (index < pattern.length) {
206
+ if (pattern[index] === "|")
207
+ break;
208
+ const piece = atom();
209
+ if (piece === undefined)
210
+ return undefined;
211
+ const quantifier = /^(\{(\d+)(,(\d*))?\}|\*|\+|\?)/.exec(pattern.slice(index));
212
+ let count = 1;
213
+ if (quantifier !== null) {
214
+ index += quantifier[0].length;
215
+ if (quantifier[0] === "*" || quantifier[0] === "?")
216
+ count = 0;
217
+ else if (quantifier[2] !== undefined)
218
+ count = Number(quantifier[2]);
219
+ }
220
+ result += piece.repeat(count);
221
+ }
222
+ return result;
223
+ };
224
+ // Public example identifiers, valid by checksum; no real person or account.
225
+ const kindExamples = {
226
+ iban: "IT60X0542811101000000123456",
227
+ "codice-fiscale": "RSSMRA85T10A562S",
228
+ "partita-iva": "12345678903",
229
+ pan: "4111111111111111",
230
+ email: "mario.rossi@example.invalid",
231
+ phone: "+39 333 1234567",
232
+ "birth-date": "1985-12-10"
233
+ };
234
+ const builtinExamples = {
235
+ boolean: "false",
236
+ decimal: "100.00",
237
+ float: "1.0",
238
+ double: "1.0",
239
+ date: "2026-01-15",
240
+ dateTime: "2026-01-15T10:00:00Z",
241
+ time: "10:00:00",
242
+ gYear: "2026",
243
+ base64Binary: "",
244
+ hexBinary: ""
245
+ };
246
+ /** An example value for a field: a seed, then field meaning, then facets, then the builtin. */
247
+ export const exampleValue = (catalog, field, seeds = new Map()) => {
248
+ const seeded = seeds.get(field.name);
249
+ if (seeded !== undefined)
250
+ return seeded;
251
+ const view = simpleView(catalog, field.type);
252
+ const candidates = [];
253
+ const kind = kindForName(field.name) ?? kindForName(localName(field.type).replace(/Type$/, ""));
254
+ if (kind !== undefined && kindExamples[kind] !== undefined)
255
+ candidates.push(kindExamples[kind] ?? "");
256
+ if (/divisa|currency|valuta$/i.test(field.name) ||
257
+ /divisa|currency/i.test(view.chain.join(" "))) {
258
+ candidates.push("EUR");
259
+ }
260
+ for (const facets of view.facets) {
261
+ if (facets.enumeration?.[0] !== undefined)
262
+ candidates.push(facets.enumeration[0]);
263
+ for (const pattern of facets.pattern ?? []) {
264
+ const example = patternExample(pattern);
265
+ if (example !== undefined)
266
+ candidates.push(example);
267
+ }
268
+ if (facets.minInclusive !== undefined)
269
+ candidates.push(facets.minInclusive);
270
+ }
271
+ if (integerTypes.has(view.builtin))
272
+ candidates.push("1");
273
+ const builtinExample = builtinExamples[view.builtin];
274
+ if (builtinExample !== undefined)
275
+ candidates.push(builtinExample);
276
+ const maxLength = Math.min(...view.facets.map((facets) => facets.maxLength ?? facets.length ?? 64));
277
+ candidates.push(field.name.slice(0, Math.max(1, maxLength)));
278
+ return (candidates.find((candidate) => checkSimple(catalog, field.type, candidate).length === 0) ??
279
+ candidates[0] ??
280
+ "");
281
+ };
282
+ // ---------------------------------------------------------------------------
283
+ // Skeleton
284
+ const describeSimple = (catalog, type) => {
285
+ const view = simpleView(catalog, type);
286
+ const named = view.chain.filter((name) => !name.startsWith("~"));
287
+ const parts = [...named.slice(0, 1), view.builtin];
288
+ for (const facets of view.facets) {
289
+ if (facets.enumeration !== undefined)
290
+ parts.push(`one of ${facets.enumeration.join(" | ")}`);
291
+ for (const pattern of facets.pattern ?? []) {
292
+ if (pattern.length <= 40)
293
+ parts.push(`pattern ${pattern}`);
294
+ }
295
+ if (facets.length !== undefined)
296
+ parts.push(`length ${facets.length}`);
297
+ if (facets.minLength !== undefined || facets.maxLength !== undefined) {
298
+ parts.push(`length ${facets.minLength ?? 0}..${facets.maxLength ?? "*"}`);
299
+ }
300
+ if (facets.totalDigits !== undefined)
301
+ parts.push(`${facets.totalDigits} digits`);
302
+ if (facets.fractionDigits !== undefined)
303
+ parts.push(`${facets.fractionDigits} decimals`);
304
+ if (facets.minInclusive !== undefined || facets.maxInclusive !== undefined) {
305
+ parts.push(`${facets.minInclusive ?? "…"}..${facets.maxInclusive ?? "…"}`);
306
+ }
307
+ }
308
+ return [...new Set(parts)].join(", ");
309
+ };
310
+ const occursText = (field) => field.maxOccurs === "unbounded"
311
+ ? `${field.minOccurs}..*`
312
+ : field.minOccurs === field.maxOccurs
313
+ ? `${field.minOccurs}`
314
+ : `${field.minOccurs}..${field.maxOccurs}`;
315
+ /**
316
+ * The commented YAML body of an instance of `element`: required fields with
317
+ * example values and a type note, optional ones as commented lines, choice
318
+ * alternatives after the first commented as `or`.
319
+ */
320
+ export const skeletonYaml = (catalog, element, indent, options = {}) => {
321
+ const maxDepth = options.maxDepth ?? 8;
322
+ const lines = [];
323
+ const emit = (depth, text) => lines.push(`${" ".repeat(depth)}${text}`);
324
+ const complexBody = (type, depth, level, commented) => {
325
+ const hash = commented ? "# " : "";
326
+ for (const attribute of type.attributes) {
327
+ const value = exampleValue(catalog, attribute, options.seeds);
328
+ const optional = !attribute.required && options.includeOptional !== true;
329
+ emit(depth, `${hash}${optional ? "# " : ""}${yamlKey(`@${attribute.name}`)}: ${yamlScalar(value)} # attribute, ${describeSimple(catalog, attribute.type)}`);
330
+ }
331
+ if (type.textType !== undefined) {
332
+ emit(depth, `${hash}"#text": ${yamlScalar(exampleValue(catalog, { name: localName(type.name), type: type.textType }, options.seeds))}`);
333
+ }
334
+ // The first alternative of each choice is written out; the others (whole
335
+ // branches, not single fields) are commented as `or`.
336
+ const firstBranch = new Map();
337
+ for (const field of type.fields) {
338
+ if (field.choice !== undefined && !firstBranch.has(field.choice))
339
+ firstBranch.set(field.choice, field.branch);
340
+ }
341
+ for (const field of type.fields) {
342
+ const alternative = field.choice !== undefined && firstBranch.get(field.choice) !== field.branch;
343
+ const optional = field.minOccurs === 0 && options.includeOptional !== true;
344
+ const comment = commented || optional || alternative;
345
+ fieldLines(field, depth, level, comment, alternative);
346
+ }
347
+ };
348
+ const fieldLines = (field, depth, level, commented, alternative) => {
349
+ const hash = commented ? "# " : "";
350
+ const note = `${alternative ? "or, " : ""}${occursText(field)}${field.nillable ? ", nillable" : ""}`;
351
+ const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
352
+ const type = typeByName(catalog, field.type);
353
+ const key = yamlKey(field.name);
354
+ if (type instanceof ComplexTypeDef) {
355
+ const doc = field.documentation ?? type.documentation;
356
+ emit(depth, `${hash}${key}: # ${localName(field.type).replace(/^~/, "")} [${note}]${doc === undefined ? "" : ` — ${doc}`}`);
357
+ if (level >= maxDepth) {
358
+ emit(depth + 2, `# … nested deeper than ${maxDepth} levels`);
359
+ return;
360
+ }
361
+ const header = lines.length - 1;
362
+ if (repeated) {
363
+ emit(depth + 2, `${hash}-`);
364
+ complexBody(type, depth + 4, level + 1, commented);
365
+ }
366
+ else {
367
+ complexBody(type, depth + 2, level + 1, commented);
368
+ }
369
+ // A required element whose children are all optional is written as
370
+ // an explicit empty map, never as a key with only comments (null).
371
+ if (!commented &&
372
+ lines
373
+ .slice(header + 1)
374
+ .every((line) => line.trimStart().startsWith("#") || line.trim() === "-")) {
375
+ if (repeated) {
376
+ const dash = lines.findIndex((line, index) => index > header && line.trim() === "-");
377
+ if (dash >= 0)
378
+ lines[dash] = `${" ".repeat(depth + 2)}- {}`;
379
+ }
380
+ else {
381
+ lines[header] = (lines[header] ?? "").replace(/^(\s*[^:]+):/, "$1: {}");
382
+ }
383
+ }
384
+ return;
385
+ }
386
+ const value = exampleValue(catalog, field, options.seeds);
387
+ const description = describeSimple(catalog, field.type);
388
+ const doc = field.documentation === undefined ? "" : ` — ${field.documentation}`;
389
+ if (repeated) {
390
+ emit(depth, `${hash}${key}: # ${description} [${note}]${doc}`);
391
+ emit(depth + 2, `${hash}- ${yamlScalar(value)}`);
392
+ }
393
+ else {
394
+ emit(depth, `${hash}${key}: ${yamlScalar(value)} # ${description} [${note}]${doc}`);
395
+ }
396
+ };
397
+ const root = typeByName(catalog, element.type);
398
+ if (root instanceof ComplexTypeDef)
399
+ complexBody(root, indent, 1, false);
400
+ else
401
+ emit(indent, `"#text": ${yamlScalar(exampleValue(catalog, { name: localName(element.name), type: element.type }, options.seeds))} # ${describeSimple(catalog, element.type)}`);
402
+ return lines;
403
+ };
404
+ // ---------------------------------------------------------------------------
405
+ // Validation of values
406
+ const join = (path, segment) => path === "" ? segment : `${path}.${segment}`;
407
+ const validateComplex = (catalog, type, value, path, issues) => {
408
+ const known = new Set([
409
+ ...type.fields.map((field) => field.name),
410
+ ...type.attributes.map((attribute) => `@${attribute.name}`),
411
+ ...(type.textType === undefined ? [] : ["#text"])
412
+ ]);
413
+ for (const key of Object.keys(value)) {
414
+ if (!known.has(key))
415
+ issues.push({
416
+ path: join(path, key),
417
+ detail: `not a field of ${localName(type.name).replace(/^~/, "")}`
418
+ });
419
+ }
420
+ for (const attribute of type.attributes) {
421
+ const item = value[`@${attribute.name}`];
422
+ if (item === undefined || item === null) {
423
+ if (attribute.required)
424
+ issues.push({
425
+ path: join(path, `@${attribute.name}`),
426
+ detail: "required attribute is missing"
427
+ });
428
+ }
429
+ else if (typeof item !== "string") {
430
+ issues.push({
431
+ path: join(path, `@${attribute.name}`),
432
+ detail: "an attribute holds a single value"
433
+ });
434
+ }
435
+ else {
436
+ for (const problem of checkSimple(catalog, attribute.type, item)) {
437
+ issues.push({ path: join(path, `@${attribute.name}`), detail: problem });
438
+ }
439
+ }
440
+ }
441
+ if (type.textType !== undefined) {
442
+ const text = value["#text"];
443
+ if (typeof text === "string") {
444
+ for (const problem of checkSimple(catalog, type.textType, text))
445
+ issues.push({ path: join(path, "#text"), detail: problem });
446
+ }
447
+ }
448
+ // Choices: at most one alternative (branch) present; inside the chosen
449
+ // one its required fields are required; a required choice needs one.
450
+ const branches = new Map();
451
+ for (const field of type.fields) {
452
+ if (field.choice === undefined)
453
+ continue;
454
+ const byBranch = branches.get(field.choice) ?? new Map();
455
+ byBranch.set(field.branch, [...(byBranch.get(field.branch) ?? []), field.name]);
456
+ branches.set(field.choice, byBranch);
457
+ }
458
+ const taken = new Map();
459
+ for (const field of type.fields) {
460
+ if (field.choice !== undefined && value[field.name] !== undefined) {
461
+ taken.set(field.choice, new Set([...(taken.get(field.choice) ?? []), field.branch]));
462
+ }
463
+ }
464
+ const where = path === "" ? "(root)" : path;
465
+ const label = (choice, branch) => (branches.get(choice)?.get(branch) ?? []).join(" + ");
466
+ for (const [choice, present] of taken) {
467
+ if (present.size > 1) {
468
+ issues.push({
469
+ path: where,
470
+ detail: `choose one of ${[...present].map((branch) => label(choice, branch)).join(", ")}`
471
+ });
472
+ }
473
+ }
474
+ for (const [choice, byBranch] of branches) {
475
+ if ((taken.get(choice)?.size ?? 0) > 0)
476
+ continue;
477
+ const required = type.fields.some((field) => field.choice === choice && field.minOccurs > 0);
478
+ if (required) {
479
+ issues.push({
480
+ path: where,
481
+ detail: `one of ${[...byBranch.keys()].map((branch) => label(choice, branch)).join(", ")} is required`
482
+ });
483
+ }
484
+ }
485
+ for (const field of type.fields) {
486
+ const item = value[field.name];
487
+ const fieldPath = join(path, field.name);
488
+ if (item === undefined) {
489
+ const inTakenBranch = field.choice !== undefined && (taken.get(field.choice)?.has(field.branch) ?? false);
490
+ if (field.minOccurs > 0 && (field.choice === undefined || inTakenBranch)) {
491
+ issues.push({ path: fieldPath, detail: "required field is missing" });
492
+ }
493
+ continue;
494
+ }
495
+ const items = isYamlList(item) ? item : [item];
496
+ const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
497
+ if (isYamlList(item) && !repeated) {
498
+ issues.push({ path: fieldPath, detail: "occurs at most once; write a single value" });
499
+ }
500
+ if (items.length < field.minOccurs)
501
+ issues.push({ path: fieldPath, detail: `at least ${field.minOccurs} occurrences` });
502
+ if (field.maxOccurs !== "unbounded" && items.length > field.maxOccurs) {
503
+ issues.push({ path: fieldPath, detail: `at most ${field.maxOccurs} occurrences` });
504
+ }
505
+ items.forEach((entry, position) => {
506
+ validateValue(catalog, field.type, field.nillable, entry, isYamlList(item) ? `${fieldPath}[${position}]` : fieldPath, issues);
507
+ });
508
+ }
509
+ };
510
+ const validateValue = (catalog, typeName, nillable, value, path, issues) => {
511
+ if (value === null) {
512
+ if (!nillable)
513
+ issues.push({ path, detail: "is not nillable; give a value or remove the field" });
514
+ return;
515
+ }
516
+ const type = typeByName(catalog, typeName);
517
+ if (type instanceof ComplexTypeDef) {
518
+ if (typeof value === "string" && type.textType !== undefined) {
519
+ validateComplex(catalog, type, { "#text": value }, path, issues);
520
+ }
521
+ else if (!isYamlMap(value)) {
522
+ issues.push({ path, detail: `expects fields of ${localName(type.name).replace(/^~/, "")}` });
523
+ }
524
+ else {
525
+ validateComplex(catalog, type, value, path, issues);
526
+ }
527
+ return;
528
+ }
529
+ if (typeof value !== "string") {
530
+ issues.push({ path, detail: "expects a single value" });
531
+ return;
532
+ }
533
+ for (const problem of checkSimple(catalog, typeName, value))
534
+ issues.push({ path, detail: problem });
535
+ };
536
+ /** Validate a value tree against a global element's type. */
537
+ export const validateInstance = (catalog, element, value) => {
538
+ const issues = [];
539
+ validateValue(catalog, element.type, element.nillable, value, "", issues);
540
+ return issues.map((issue) => (issue.path === "" ? { ...issue, path: "(root)" } : issue));
541
+ };
542
+ // ---------------------------------------------------------------------------
543
+ // Value → XML
544
+ /** Serialize an instance of `element` with prefixes `ns1`, `ns2`… declared on the root. */
545
+ export const instanceToXml = (catalog, element, value) => {
546
+ const prefixes = new Map();
547
+ const prefixOf = (namespace) => {
548
+ if (namespace === "")
549
+ return "";
550
+ let prefix = prefixes.get(namespace);
551
+ if (prefix === undefined) {
552
+ prefix = `ns${prefixes.size + 1}`;
553
+ prefixes.set(namespace, prefix);
554
+ }
555
+ return prefix;
556
+ };
557
+ let usesXsi = false;
558
+ const qualified = (namespace, local) => {
559
+ const prefix = prefixOf(namespace);
560
+ return prefix === "" ? local : `${prefix}:${local}`;
561
+ };
562
+ const write = (name, typeName, item) => {
563
+ if (item === null) {
564
+ usesXsi = true;
565
+ return `<${name} xsi:nil="true"/>`;
566
+ }
567
+ const type = typeByName(catalog, typeName);
568
+ if (!(type instanceof ComplexTypeDef)) {
569
+ return `<${name}>${escapeText(typeof item === "string" ? item : "")}</${name}>`;
570
+ }
571
+ const map = typeof item === "string" ? { "#text": item } : isYamlMap(item) ? item : {};
572
+ const attributes = type.attributes
573
+ .flatMap((attribute) => {
574
+ const attributeValue = map[`@${attribute.name}`];
575
+ return typeof attributeValue === "string"
576
+ ? [` ${attribute.name}="${escapeAttribute(attributeValue)}"`]
577
+ : [];
578
+ })
579
+ .join("");
580
+ const text = typeof map["#text"] === "string" ? escapeText(map["#text"]) : "";
581
+ const children = type.fields
582
+ .flatMap((field) => {
583
+ const child = map[field.name];
584
+ if (child === undefined)
585
+ return [];
586
+ const entries = isYamlList(child) ? child : [child];
587
+ return entries.map((entry) => write(qualified(field.namespace, field.name), field.type, entry));
588
+ })
589
+ .join("");
590
+ const content = `${text}${children}`;
591
+ return content === "" ? `<${name}${attributes}/>` : `<${name}${attributes}>${content}</${name}>`;
592
+ };
593
+ const root = splitClark(element.name);
594
+ const rootName = qualified(root.namespace, root.local);
595
+ const body = write(rootName, element.type, value);
596
+ const declarations = [
597
+ ...[...prefixes].map(([namespace, prefix]) => ` xmlns:${prefix}="${escapeAttribute(namespace)}"`),
598
+ ...(usesXsi ? [` xmlns:xsi="${XsiNamespace}"`] : [])
599
+ ].join("");
600
+ return body.replace(/^<([^\s/>]+)/, `<$1${declarations}`);
601
+ };
602
+ // ---------------------------------------------------------------------------
603
+ // XML → value
604
+ /**
605
+ * Read an XML element as an instance of `element`'s type. Unknown children,
606
+ * wrong namespaces, and repeated singletons become issues; values are kept
607
+ * as found so a response can be analysed even when it breaks its schema.
608
+ */
609
+ export const instanceFromXml = (catalog, element, xml) => {
610
+ const issues = [];
611
+ const expected = splitClark(element.name);
612
+ if (xml.name.local !== expected.local || xml.name.namespace !== expected.namespace) {
613
+ issues.push({
614
+ path: "(root)",
615
+ detail: `expected {${expected.namespace}}${expected.local}, found {${xml.name.namespace}}${xml.name.local}`
616
+ });
617
+ }
618
+ const nil = (node) => node.attributes.some((attribute) => attribute.name.namespace === XsiNamespace &&
619
+ attribute.name.local === "nil" &&
620
+ (attribute.value === "true" || attribute.value === "1"));
621
+ const read = (node, typeName, path) => {
622
+ if (nil(node))
623
+ return null;
624
+ const type = typeByName(catalog, typeName);
625
+ if (!(type instanceof ComplexTypeDef))
626
+ return textOf(node);
627
+ const result = {};
628
+ for (const attribute of node.attributes) {
629
+ if (attribute.name.namespace === XsiNamespace)
630
+ continue;
631
+ if (!type.attributes.some((declared) => declared.name === attribute.name.local)) {
632
+ issues.push({
633
+ path: join(path, `@${attribute.name.local}`),
634
+ detail: "attribute not declared"
635
+ });
636
+ }
637
+ result[`@${attribute.name.local}`] = attribute.value;
638
+ }
639
+ if (type.textType !== undefined)
640
+ result["#text"] = textOf(node);
641
+ else if (textOf(node).trim() !== "")
642
+ issues.push({ path: path === "" ? "(root)" : path, detail: "unexpected text content" });
643
+ for (const child of elements(node)) {
644
+ const field = type.fields.find((candidate) => candidate.name === child.name.local);
645
+ const childPath = join(path, child.name.local);
646
+ if (field === undefined) {
647
+ // Kept as text; validation reports it as not a field of the type.
648
+ result[child.name.local] = textOf(child);
649
+ continue;
650
+ }
651
+ if (child.name.namespace !== field.namespace) {
652
+ issues.push({
653
+ path: childPath,
654
+ detail: `namespace ${child.name.namespace === "" ? "(none)" : child.name.namespace}, expected ${field.namespace === "" ? "(none)" : field.namespace}`
655
+ });
656
+ }
657
+ const childValue = read(child, field.type, childPath);
658
+ const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
659
+ const existing = result[field.name];
660
+ if (repeated) {
661
+ const prior = existing !== undefined && isYamlList(existing) ? existing : [];
662
+ result[field.name] = [...prior, childValue];
663
+ }
664
+ else if (existing !== undefined) {
665
+ issues.push({ path: childPath, detail: "occurs more than once" });
666
+ }
667
+ else {
668
+ result[field.name] = childValue;
669
+ }
670
+ }
671
+ return result;
672
+ };
673
+ const value = read(xml, element.type, "");
674
+ return { value, issues: [...issues, ...validateInstance(catalog, element, value)] };
675
+ };