@llm4ts/shell 2.12.0 → 2.13.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.
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 +283 -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 +1339 -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 +455 -0
  42. package/kits/soap-ace/flows/lib/soap/Xsd.js +720 -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,1339 @@
1
+ import { ComplexTypeDef, contentModel, emptiable, isBuiltin, localName, particleFields, qname, SimpleTypeDef, splitClark, typeByName, XsdNamespace } 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
+ /** Built-in list types and their item types. */
13
+ const builtinLists = {
14
+ NMTOKENS: "NMTOKEN",
15
+ IDREFS: "IDREF",
16
+ ENTITIES: "ENTITY"
17
+ };
18
+ export const simpleView = (catalog, name) => {
19
+ const facets = [];
20
+ const chain = [];
21
+ let current = name;
22
+ let itemType;
23
+ let union = false;
24
+ for (let depth = 0; depth < 16 && !isBuiltin(current); depth++) {
25
+ const type = typeByName(catalog, current);
26
+ if (!(type instanceof SimpleTypeDef))
27
+ break;
28
+ chain.push(localName(type.name));
29
+ if (type.variety === "list") {
30
+ itemType = type.itemType ?? qname(XsdNamespace, "string");
31
+ break;
32
+ }
33
+ if (type.variety === "union")
34
+ union = true;
35
+ facets.push(type.facets);
36
+ current = type.base;
37
+ }
38
+ const builtin = isBuiltin(current) ? localName(current) : "string";
39
+ const listItem = builtinLists[builtin];
40
+ if (itemType === undefined && listItem !== undefined)
41
+ itemType = qname(XsdNamespace, listItem);
42
+ return { builtin, facets, chain, itemType, union };
43
+ };
44
+ /** Inclusive bounds of the integer built-ins; `undefined` is unbounded. */
45
+ const integerRanges = {
46
+ integer: [undefined, undefined],
47
+ nonNegativeInteger: [0n, undefined],
48
+ positiveInteger: [1n, undefined],
49
+ nonPositiveInteger: [undefined, 0n],
50
+ negativeInteger: [undefined, -1n],
51
+ long: [-(2n ** 63n), 2n ** 63n - 1n],
52
+ int: [-(2n ** 31n), 2n ** 31n - 1n],
53
+ short: [-32768n, 32767n],
54
+ byte: [-128n, 127n],
55
+ unsignedLong: [0n, 2n ** 64n - 1n],
56
+ unsignedInt: [0n, 2n ** 32n - 1n],
57
+ unsignedShort: [0n, 65535n],
58
+ unsignedByte: [0n, 255n]
59
+ };
60
+ const integerTypes = new Set(Object.keys(integerRanges));
61
+ const isDecimal = (builtin) => builtin === "decimal" || integerTypes.has(builtin);
62
+ const isFloating = (builtin) => builtin === "float" || builtin === "double";
63
+ /** Built-ins derived from xs:string by whitespace: token and its descendants collapse. */
64
+ const replacing = new Set(["normalizedString"]);
65
+ const preserving = new Set(["string", "anySimpleType", "anyType"]);
66
+ /** The effective whiteSpace facet: the most derived one, else the built-in's. */
67
+ const whiteSpaceOf = (view) => {
68
+ for (const facets of view.facets)
69
+ if (facets.whiteSpace !== undefined)
70
+ return facets.whiteSpace;
71
+ if (view.itemType !== undefined)
72
+ return "collapse";
73
+ if (view.union || preserving.has(view.builtin))
74
+ return "preserve";
75
+ return replacing.has(view.builtin) ? "replace" : "collapse";
76
+ };
77
+ const normalizeSpace = (value, mode) => mode === "preserve"
78
+ ? value
79
+ : mode === "replace"
80
+ ? value.replace(/[\t\n\r]/g, " ")
81
+ : value.replace(/[\t\n\r ]+/g, " ").trim();
82
+ const zone = "(Z|[+-]\\d{2}:\\d{2})?";
83
+ const validZone = (text) => {
84
+ if (text === undefined || text === "Z")
85
+ return true;
86
+ const hours = Number(text.slice(1, 3));
87
+ const minutes = Number(text.slice(4, 6));
88
+ return minutes < 60 && (hours < 14 || (hours === 14 && minutes === 0));
89
+ };
90
+ const leapYear = (year) => {
91
+ // The leap rule only needs the year modulo 400, which its last four digits give.
92
+ const last = Number(year.replace(/^-/, "").slice(-4));
93
+ return last % 4 === 0 && (last % 100 !== 0 || last % 400 === 0);
94
+ };
95
+ const daysIn = (year, month) => month === 2 ? (leapYear(year) ? 29 : 28) : [4, 6, 9, 11].includes(month) ? 30 : 31;
96
+ const validMonth = (month) => Number(month) >= 1 && Number(month) <= 12;
97
+ const validDate = (year, month, day) => validMonth(month) && Number(day) >= 1 && Number(day) <= daysIn(year, Number(month));
98
+ const validTime = (hours, minutes, seconds, fraction) => (Number(hours) < 24 && Number(minutes) < 60 && Number(seconds) < 60) ||
99
+ (hours === "24" && minutes === "00" && seconds === "00" && /^(\.0+)?$/.test(fraction));
100
+ const shapes = {
101
+ date: new RegExp(`^(-?\\d{4,})-(\\d{2})-(\\d{2})${zone}$`),
102
+ dateTime: new RegExp(`^(-?\\d{4,})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?${zone}$`),
103
+ time: new RegExp(`^(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?${zone}$`),
104
+ gYearMonth: new RegExp(`^(-?\\d{4,})-(\\d{2})${zone}$`),
105
+ gYear: new RegExp(`^(-?\\d{4,})${zone}$`),
106
+ gMonth: new RegExp(`^--(\\d{2})(?:--)?${zone}$`),
107
+ gDay: new RegExp(`^---(\\d{2})${zone}$`),
108
+ gMonthDay: new RegExp(`^--(\\d{2})-(\\d{2})${zone}$`)
109
+ };
110
+ const lexical = {
111
+ boolean: (value) => /^(true|false|1|0)$/.test(value),
112
+ decimal: (value) => /^[+-]?(\d+(\.\d*)?|\.\d+)$/.test(value),
113
+ float: (value) => /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)$/.test(value),
114
+ double: (value) => /^([+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?|INF|-INF|NaN)$/.test(value),
115
+ duration: (value) => /^-?P(?=\d|T\d)(\d+Y)?(\d+M)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(value),
116
+ date: (value) => {
117
+ const match = shapes.date?.exec(value);
118
+ return (match != null &&
119
+ validDate(match[1] ?? "", match[2] ?? "", match[3] ?? "") &&
120
+ validZone(match[4]));
121
+ },
122
+ dateTime: (value) => {
123
+ const match = shapes.dateTime?.exec(value);
124
+ return (match != null &&
125
+ validDate(match[1] ?? "", match[2] ?? "", match[3] ?? "") &&
126
+ validTime(match[4] ?? "", match[5] ?? "", match[6] ?? "", match[7] ?? "") &&
127
+ validZone(match[8]));
128
+ },
129
+ time: (value) => {
130
+ const match = shapes.time?.exec(value);
131
+ return (match != null &&
132
+ validTime(match[1] ?? "", match[2] ?? "", match[3] ?? "", match[4] ?? "") &&
133
+ validZone(match[5]));
134
+ },
135
+ gYearMonth: (value) => {
136
+ const match = shapes.gYearMonth?.exec(value);
137
+ return match != null && validMonth(match[2] ?? "") && validZone(match[3]);
138
+ },
139
+ gYear: (value) => {
140
+ const match = shapes.gYear?.exec(value);
141
+ return match != null && validZone(match[2]);
142
+ },
143
+ gMonth: (value) => {
144
+ const match = shapes.gMonth?.exec(value);
145
+ return match != null && validMonth(match[1] ?? "") && validZone(match[2]);
146
+ },
147
+ gDay: (value) => {
148
+ const match = shapes.gDay?.exec(value);
149
+ return match != null && Number(match[1]) >= 1 && Number(match[1]) <= 31 && validZone(match[2]);
150
+ },
151
+ gMonthDay: (value) => {
152
+ const match = shapes.gMonthDay?.exec(value);
153
+ return (match != null &&
154
+ validMonth(match[1] ?? "") &&
155
+ Number(match[2]) >= 1 &&
156
+ // A leap year: --02-29 is a valid month-day.
157
+ Number(match[2]) <= daysIn("2000", Number(match[1])) &&
158
+ validZone(match[3]));
159
+ },
160
+ // Whole quanta of four characters, padding only at the end.
161
+ base64Binary: (value) => /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value.replace(/[\t\n\r ]/g, "")),
162
+ hexBinary: (value) => /^([0-9A-Fa-f]{2})*$/.test(value)
163
+ };
164
+ // XSD regular expressions: implicitly anchored, no anchors of their own,
165
+ // `.` excludes only line ends, \d \w \s \i \c and \p{Is<Block>} mean more
166
+ // (or less) than in JavaScript, and classes can be subtracted. Translated
167
+ // token by token; anything without a faithful translation is refused.
168
+ const unicodeBlocks = {
169
+ BasicLatin: "\\u{0}-\\u{7F}",
170
+ "Latin-1Supplement": "\\u{80}-\\u{FF}",
171
+ "LatinExtended-A": "\\u{100}-\\u{17F}",
172
+ "LatinExtended-B": "\\u{180}-\\u{24F}",
173
+ IPAExtensions: "\\u{250}-\\u{2AF}",
174
+ SpacingModifierLetters: "\\u{2B0}-\\u{2FF}",
175
+ CombiningDiacriticalMarks: "\\u{300}-\\u{36F}",
176
+ Greek: "\\u{370}-\\u{3FF}",
177
+ GreekandCoptic: "\\u{370}-\\u{3FF}",
178
+ Cyrillic: "\\u{400}-\\u{4FF}",
179
+ Armenian: "\\u{530}-\\u{58F}",
180
+ Hebrew: "\\u{590}-\\u{5FF}",
181
+ Arabic: "\\u{600}-\\u{6FF}",
182
+ Devanagari: "\\u{900}-\\u{97F}",
183
+ Thai: "\\u{E00}-\\u{E7F}",
184
+ LatinExtendedAdditional: "\\u{1E00}-\\u{1EFF}",
185
+ GreekExtended: "\\u{1F00}-\\u{1FFF}",
186
+ GeneralPunctuation: "\\u{2000}-\\u{206F}",
187
+ SuperscriptsandSubscripts: "\\u{2070}-\\u{209F}",
188
+ CurrencySymbols: "\\u{20A0}-\\u{20CF}",
189
+ LetterlikeSymbols: "\\u{2100}-\\u{214F}",
190
+ NumberForms: "\\u{2150}-\\u{218F}",
191
+ Arrows: "\\u{2190}-\\u{21FF}",
192
+ MathematicalOperators: "\\u{2200}-\\u{22FF}",
193
+ BoxDrawing: "\\u{2500}-\\u{257F}",
194
+ CJKSymbolsandPunctuation: "\\u{3000}-\\u{303F}",
195
+ Hiragana: "\\u{3040}-\\u{309F}",
196
+ Katakana: "\\u{30A0}-\\u{30FF}",
197
+ CJKUnifiedIdeographs: "\\u{4E00}-\\u{9FFF}",
198
+ HangulSyllables: "\\u{AC00}-\\u{D7AF}",
199
+ PrivateUse: "\\u{E000}-\\u{F8FF}",
200
+ AlphabeticPresentationForms: "\\u{FB00}-\\u{FB4F}",
201
+ HalfwidthandFullwidthForms: "\\u{FF00}-\\u{FFEF}",
202
+ Specials: "\\u{FFF0}-\\u{FFFF}"
203
+ };
204
+ const set = (members) => ({ inside: members, atom: `[${members}]` });
205
+ const complement = (members) => ({ inside: undefined, atom: `[^${members}]` });
206
+ const classEscapes = {
207
+ d: { inside: "\\p{Nd}", atom: "\\p{Nd}" },
208
+ D: { inside: "\\P{Nd}", atom: "\\P{Nd}" },
209
+ s: set(" \\t\\n\\r"),
210
+ S: complement(" \\t\\n\\r"),
211
+ w: set("\\p{L}\\p{M}\\p{N}\\p{S}"),
212
+ W: set("\\p{P}\\p{Z}\\p{C}"),
213
+ i: set("\\p{L}_:"),
214
+ I: complement("\\p{L}_:"),
215
+ c: set("\\p{L}\\p{M}\\p{N}._:\\-"),
216
+ C: complement("\\p{L}\\p{M}\\p{N}._:\\-")
217
+ };
218
+ const categories = /^(L[ultmo]?|M[nce]?|N[dlo]?|P[cdseifo]?|Z[slp]?|S[mcko]?|C[cfons]?)$/;
219
+ /** Translate an XSD pattern to an anchored JavaScript source; undefined when impossible. */
220
+ export const translatePattern = (pattern) => {
221
+ let index = 0;
222
+ const escape = () => {
223
+ const char = pattern[index + 1];
224
+ index += 2;
225
+ if (char === undefined)
226
+ return undefined;
227
+ if (char === "n")
228
+ return { inside: "\\n", atom: "\\n" };
229
+ if (char === "r")
230
+ return { inside: "\\r", atom: "\\r" };
231
+ if (char === "t")
232
+ return { inside: "\\t", atom: "\\t" };
233
+ if (char === "-")
234
+ return { inside: "\\-", atom: "-" };
235
+ if ("\\|.?*+(){}[]^".includes(char))
236
+ return { inside: `\\${char}`, atom: `\\${char}` };
237
+ const known = classEscapes[char];
238
+ if (known !== undefined)
239
+ return known;
240
+ if (char !== "p" && char !== "P")
241
+ return undefined;
242
+ const close = pattern.indexOf("}", index);
243
+ if (pattern[index] !== "{" || close < 0)
244
+ return undefined;
245
+ const property = pattern.slice(index + 1, close);
246
+ index = close + 1;
247
+ if (property.startsWith("Is")) {
248
+ const block = unicodeBlocks[property.slice(2)];
249
+ if (block === undefined)
250
+ return undefined;
251
+ return char === "p" ? set(block) : complement(block);
252
+ }
253
+ if (!categories.test(property))
254
+ return undefined;
255
+ const atom = `\\${char}{${property}}`;
256
+ return { inside: atom, atom };
257
+ };
258
+ // A class body after its `[`, through its `]`; subtraction as a lookahead.
259
+ const characterClass = () => {
260
+ const negated = pattern[index] === "^";
261
+ if (negated)
262
+ index++;
263
+ let members = "";
264
+ let subtracted;
265
+ for (;;) {
266
+ const char = pattern[index];
267
+ if (char === undefined)
268
+ return undefined;
269
+ if (char === "]") {
270
+ index++;
271
+ break;
272
+ }
273
+ if (char === "-" && pattern[index + 1] === "[") {
274
+ index += 2;
275
+ subtracted = characterClass();
276
+ if (subtracted === undefined || pattern[index] !== "]")
277
+ return undefined;
278
+ index++;
279
+ break;
280
+ }
281
+ if (char === "\\") {
282
+ const escaped = escape();
283
+ if (escaped?.inside === undefined)
284
+ return undefined;
285
+ members += escaped.inside;
286
+ }
287
+ else if (char === "[") {
288
+ return undefined;
289
+ }
290
+ else {
291
+ members += char;
292
+ index++;
293
+ }
294
+ }
295
+ if (members === "")
296
+ return undefined;
297
+ const base = `[${negated ? "^" : ""}${members}]`;
298
+ return subtracted === undefined ? base : `(?:(?!${subtracted})${base})`;
299
+ };
300
+ let source = "";
301
+ while (index < pattern.length) {
302
+ const char = pattern[index] ?? "";
303
+ if (char === "\\") {
304
+ const escaped = escape();
305
+ if (escaped === undefined)
306
+ return undefined;
307
+ source += escaped.atom;
308
+ }
309
+ else if (char === "[") {
310
+ index++;
311
+ const translated = characterClass();
312
+ if (translated === undefined)
313
+ return undefined;
314
+ source += translated;
315
+ }
316
+ else if (char === ".") {
317
+ source += "[^\\n\\r]";
318
+ index++;
319
+ }
320
+ else if (char === "^" || char === "$") {
321
+ source += `\\${char}`;
322
+ index++;
323
+ }
324
+ else {
325
+ source += char;
326
+ index++;
327
+ }
328
+ }
329
+ return `^(?:${source})$`;
330
+ };
331
+ const xsdRegex = (pattern) => {
332
+ const source = translatePattern(pattern);
333
+ if (source === undefined)
334
+ return undefined;
335
+ try {
336
+ return new RegExp(source, "u");
337
+ }
338
+ catch {
339
+ return undefined;
340
+ }
341
+ };
342
+ const decimalParts = (value) => {
343
+ const match = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(value);
344
+ if (match === null || (match[2] === "" && (match[3] ?? "") === ""))
345
+ return undefined;
346
+ return {
347
+ negative: match[1] === "-",
348
+ whole: (match[2] ?? "").replace(/^0+/, ""),
349
+ fraction: (match[3] ?? "").replace(/0+$/, "")
350
+ };
351
+ };
352
+ /** Exact comparison of two decimal literals: -1, 0, 1, or undefined when either is not one. */
353
+ const compareDecimal = (left, right) => {
354
+ const a = decimalParts(left);
355
+ const b = decimalParts(right);
356
+ if (a === undefined || b === undefined)
357
+ return undefined;
358
+ const scale = Math.max(a.fraction.length, b.fraction.length);
359
+ const scaled = (parts) => {
360
+ const magnitude = BigInt(`${parts.whole}${parts.fraction.padEnd(scale, "0")}` || "0");
361
+ return parts.negative ? -magnitude : magnitude;
362
+ };
363
+ const x = scaled(a);
364
+ const y = scaled(b);
365
+ return x < y ? -1 : x > y ? 1 : 0;
366
+ };
367
+ const floating = (value) => value === "INF" ? Infinity : value === "-INF" ? -Infinity : Number(value);
368
+ /** Order two values of a numeric built-in; undefined when not comparable. */
369
+ const compareValues = (builtin, left, right) => {
370
+ if (isDecimal(builtin))
371
+ return compareDecimal(left, right);
372
+ if (isFloating(builtin)) {
373
+ const x = floating(left);
374
+ const y = floating(right);
375
+ if (Number.isNaN(x) || Number.isNaN(y))
376
+ return undefined;
377
+ return x < y ? -1 : x > y ? 1 : 0;
378
+ }
379
+ return undefined;
380
+ };
381
+ /** Equality in the value space of the built-in (`1.0` is `1.00` as a decimal). */
382
+ const sameValue = (builtin, left, right) => {
383
+ if (isDecimal(builtin) || isFloating(builtin)) {
384
+ const order = compareValues(builtin, left, right);
385
+ if (order !== undefined)
386
+ return order === 0;
387
+ return isFloating(builtin) && left === "NaN" && right === "NaN";
388
+ }
389
+ if (builtin === "boolean") {
390
+ const truth = (value) => (value === "1" ? "true" : value === "0" ? "false" : value);
391
+ return truth(left) === truth(right);
392
+ }
393
+ return left === right;
394
+ };
395
+ /** Length in the facet's unit: octets for binaries, code points otherwise. */
396
+ const lengthOf = (builtin, value) => {
397
+ if (builtin === "hexBinary")
398
+ return Math.floor(value.length / 2);
399
+ if (builtin === "base64Binary") {
400
+ const compact = value.replace(/[\t\n\r ]/g, "");
401
+ return Math.floor(compact.length / 4) * 3 - (compact.match(/=+$/)?.[0].length ?? 0);
402
+ }
403
+ return [...value].length;
404
+ };
405
+ const digitsOf = (value) => {
406
+ const parts = decimalParts(value);
407
+ const whole = parts?.whole ?? "";
408
+ const fraction = parts?.fraction ?? "";
409
+ return { total: whole.length + fraction.length, fraction: fraction.length };
410
+ };
411
+ export const checkSimple = (catalog, type, raw) => {
412
+ const view = simpleView(catalog, type);
413
+ // Every check sees the value after its whiteSpace facet normalizes it.
414
+ const value = normalizeSpace(raw, whiteSpaceOf(view));
415
+ const problems = [];
416
+ const builtin = view.builtin;
417
+ const items = view.itemType === undefined ? undefined : value === "" ? [] : value.split(" ");
418
+ if (items !== undefined) {
419
+ for (const item of items) {
420
+ for (const problem of checkSimple(catalog, view.itemType ?? type, item)) {
421
+ problems.push(`item ${problem}`);
422
+ }
423
+ }
424
+ }
425
+ else if (view.union) {
426
+ // Member types are not modelled; any string is accepted.
427
+ }
428
+ else if (integerTypes.has(builtin)) {
429
+ const [low, high] = integerRanges[builtin] ?? [undefined, undefined];
430
+ if (!/^[+-]?\d+$/.test(value))
431
+ problems.push(`"${value}" is not an ${builtin}`);
432
+ else {
433
+ const number = BigInt(value.replace(/^\+/, ""));
434
+ if ((low !== undefined && number < low) || (high !== undefined && number > high)) {
435
+ problems.push(`${value} is out of the ${builtin} range`);
436
+ }
437
+ }
438
+ }
439
+ else {
440
+ const shape = lexical[builtin];
441
+ if (shape !== undefined && !shape(value))
442
+ problems.push(`"${value}" is not a ${builtin}`);
443
+ }
444
+ const lexicallyValid = problems.length === 0;
445
+ for (const facets of view.facets) {
446
+ if (facets.enumeration !== undefined &&
447
+ !facets.enumeration.some((option) => sameValue(builtin, normalizeSpace(option, whiteSpaceOf(view)), value))) {
448
+ problems.push(`"${value}" is not one of ${facets.enumeration.join(", ")}`);
449
+ }
450
+ if (facets.pattern !== undefined && facets.pattern.length > 0) {
451
+ const regexes = facets.pattern.map(xsdRegex);
452
+ const untranslatable = facets.pattern.filter((_, position) => regexes[position] === undefined);
453
+ if (untranslatable.length > 0) {
454
+ problems.push(`pattern ${untranslatable.join(" | ")} cannot be checked`);
455
+ }
456
+ else if (!regexes.some((regex) => regex?.test(value) ?? false)) {
457
+ problems.push(`"${value}" does not match ${facets.pattern.join(" | ")}`);
458
+ }
459
+ }
460
+ const length = items === undefined ? lengthOf(builtin, value) : items.length;
461
+ const unit = items === undefined ? "length" : "items";
462
+ if (facets.length !== undefined && length !== facets.length) {
463
+ problems.push(`${unit} ${length}, expected ${facets.length}`);
464
+ }
465
+ if (facets.minLength !== undefined && length < facets.minLength) {
466
+ problems.push(`${unit} ${length} below ${facets.minLength}`);
467
+ }
468
+ if (facets.maxLength !== undefined && length > facets.maxLength) {
469
+ problems.push(`${unit} ${length} above ${facets.maxLength}`);
470
+ }
471
+ if (items !== undefined || !lexicallyValid)
472
+ continue;
473
+ if (isDecimal(builtin)) {
474
+ const digits = digitsOf(value);
475
+ if (facets.totalDigits !== undefined && digits.total > facets.totalDigits) {
476
+ problems.push(`${digits.total} digits, at most ${facets.totalDigits}`);
477
+ }
478
+ if (facets.fractionDigits !== undefined && digits.fraction > facets.fractionDigits) {
479
+ problems.push(`${digits.fraction} fraction digits, at most ${facets.fractionDigits}`);
480
+ }
481
+ }
482
+ const compare = (bound) => bound === undefined ? undefined : compareValues(builtin, value, bound.trim());
483
+ const minInclusive = compare(facets.minInclusive);
484
+ const maxInclusive = compare(facets.maxInclusive);
485
+ const minExclusive = compare(facets.minExclusive);
486
+ const maxExclusive = compare(facets.maxExclusive);
487
+ if (minInclusive !== undefined && minInclusive < 0) {
488
+ problems.push(`${value} below ${facets.minInclusive}`);
489
+ }
490
+ if (maxInclusive !== undefined && maxInclusive > 0) {
491
+ problems.push(`${value} above ${facets.maxInclusive}`);
492
+ }
493
+ if (minExclusive !== undefined && minExclusive <= 0) {
494
+ problems.push(`${value} not above ${facets.minExclusive}`);
495
+ }
496
+ if (maxExclusive !== undefined && maxExclusive >= 0) {
497
+ problems.push(`${value} not below ${facets.maxExclusive}`);
498
+ }
499
+ }
500
+ return problems;
501
+ };
502
+ /** A `fixed=` value: the only one allowed (an empty element takes it). */
503
+ const checkFixed = (catalog, type, raw, fixed, emptyTakesFixed) => {
504
+ const view = simpleView(catalog, type);
505
+ const mode = whiteSpaceOf(view);
506
+ const value = normalizeSpace(raw, mode);
507
+ if (emptyTakesFixed && raw === "")
508
+ return [];
509
+ return sameValue(view.builtin, value, normalizeSpace(fixed, mode))
510
+ ? []
511
+ : [`"${value}" is fixed to "${fixed}"`];
512
+ };
513
+ // ---------------------------------------------------------------------------
514
+ // Example values
515
+ /** A string matching a simple XSD pattern: literals, classes, groups (first branch), quantifiers. */
516
+ export const patternExample = (pattern) => {
517
+ let index = 0;
518
+ const atom = () => {
519
+ const char = pattern[index];
520
+ if (char === undefined)
521
+ return undefined;
522
+ if (char === "(") {
523
+ index++;
524
+ const start = index;
525
+ let depth = 1;
526
+ while (index < pattern.length && depth > 0) {
527
+ if (pattern[index] === "\\")
528
+ index++;
529
+ else if (pattern[index] === "(")
530
+ depth++;
531
+ else if (pattern[index] === ")")
532
+ depth--;
533
+ index++;
534
+ }
535
+ const inner = pattern.slice(start, index - 1).replace(/^\?:/, "");
536
+ let branchDepth = 0;
537
+ let cut = inner.length;
538
+ for (let position = 0; position < inner.length; position++) {
539
+ const current = inner[position];
540
+ if (current === "\\")
541
+ position++;
542
+ else if (current === "(" || current === "[")
543
+ branchDepth++;
544
+ else if (current === ")" || current === "]")
545
+ branchDepth--;
546
+ else if (current === "|" && branchDepth === 0) {
547
+ cut = position;
548
+ break;
549
+ }
550
+ }
551
+ return patternExample(inner.slice(0, cut));
552
+ }
553
+ if (char === "[") {
554
+ const end = pattern.indexOf("]", index + 1);
555
+ if (end < 0)
556
+ return undefined;
557
+ const body = pattern.slice(index + 1, end);
558
+ index = end + 1;
559
+ if (body.startsWith("^"))
560
+ return undefined;
561
+ if (body.startsWith("\\d") || /^0-9/.test(body))
562
+ return "0";
563
+ const first = body.startsWith("\\") ? body[1] : body[0];
564
+ return first === undefined ? undefined : first;
565
+ }
566
+ if (char === "\\") {
567
+ const escaped = pattern[index + 1];
568
+ index += 2;
569
+ if (escaped === "d")
570
+ return "0";
571
+ if (escaped === "w")
572
+ return "a";
573
+ if (escaped === "s")
574
+ return " ";
575
+ if (escaped === undefined)
576
+ return undefined;
577
+ return /[A-Za-z]/.test(escaped) ? undefined : escaped;
578
+ }
579
+ if (char === ".") {
580
+ index++;
581
+ return "a";
582
+ }
583
+ if ("|)*+?{".includes(char))
584
+ return undefined;
585
+ index++;
586
+ return char;
587
+ };
588
+ let result = "";
589
+ while (index < pattern.length) {
590
+ if (pattern[index] === "|")
591
+ break;
592
+ const piece = atom();
593
+ if (piece === undefined)
594
+ return undefined;
595
+ const quantifier = /^(\{(\d+)(,(\d*))?\}|\*|\+|\?)/.exec(pattern.slice(index));
596
+ let count = 1;
597
+ if (quantifier !== null) {
598
+ index += quantifier[0].length;
599
+ if (quantifier[0] === "*" || quantifier[0] === "?")
600
+ count = 0;
601
+ else if (quantifier[2] !== undefined)
602
+ count = Number(quantifier[2]);
603
+ }
604
+ result += piece.repeat(count);
605
+ }
606
+ return result;
607
+ };
608
+ // Public example identifiers, valid by checksum; no real person or account.
609
+ const kindExamples = {
610
+ iban: "IT60X0542811101000000123456",
611
+ "codice-fiscale": "RSSMRA85T10A562S",
612
+ "partita-iva": "12345678903",
613
+ pan: "4111111111111111",
614
+ email: "mario.rossi@example.invalid",
615
+ phone: "+39 333 1234567",
616
+ "birth-date": "1985-12-10"
617
+ };
618
+ const builtinExamples = {
619
+ boolean: "false",
620
+ decimal: "100.00",
621
+ float: "1.0",
622
+ double: "1.0",
623
+ date: "2026-01-15",
624
+ dateTime: "2026-01-15T10:00:00Z",
625
+ time: "10:00:00",
626
+ gYear: "2026",
627
+ gYearMonth: "2026-01",
628
+ gMonth: "--01",
629
+ gDay: "---15",
630
+ gMonthDay: "--01-15",
631
+ duration: "P1D",
632
+ base64Binary: "",
633
+ hexBinary: ""
634
+ };
635
+ /** An example value for a field: its fixed value, a seed, field meaning, facets, the builtin. */
636
+ export const exampleValue = (catalog, field, seeds = new Map()) => {
637
+ if (field.fixed !== undefined)
638
+ return field.fixed;
639
+ const seeded = seeds.get(field.name);
640
+ if (seeded !== undefined)
641
+ return seeded;
642
+ const view = simpleView(catalog, field.type);
643
+ const candidates = [];
644
+ const kind = kindForName(field.name) ?? kindForName(localName(field.type).replace(/Type$/, ""));
645
+ if (kind !== undefined && kindExamples[kind] !== undefined)
646
+ candidates.push(kindExamples[kind] ?? "");
647
+ if (/divisa|currency|valuta$/i.test(field.name) ||
648
+ /divisa|currency/i.test(view.chain.join(" "))) {
649
+ candidates.push("EUR");
650
+ }
651
+ for (const facets of view.facets) {
652
+ if (facets.enumeration?.[0] !== undefined)
653
+ candidates.push(facets.enumeration[0]);
654
+ for (const pattern of facets.pattern ?? []) {
655
+ const example = patternExample(pattern);
656
+ if (example !== undefined)
657
+ candidates.push(example);
658
+ }
659
+ if (facets.minInclusive !== undefined)
660
+ candidates.push(facets.minInclusive);
661
+ }
662
+ if (integerTypes.has(view.builtin))
663
+ candidates.push("1");
664
+ const builtinExample = builtinExamples[view.builtin];
665
+ if (builtinExample !== undefined)
666
+ candidates.push(builtinExample);
667
+ const maxLength = Math.min(...view.facets.map((facets) => facets.maxLength ?? facets.length ?? 64));
668
+ candidates.push(field.name.slice(0, Math.max(1, maxLength)));
669
+ return (candidates.find((candidate) => checkSimple(catalog, field.type, candidate).length === 0) ??
670
+ candidates[0] ??
671
+ "");
672
+ };
673
+ // ---------------------------------------------------------------------------
674
+ // Skeleton
675
+ const describeSimple = (catalog, type) => {
676
+ const view = simpleView(catalog, type);
677
+ const named = view.chain.filter((name) => !name.startsWith("~"));
678
+ const parts = [...named.slice(0, 1), view.builtin];
679
+ for (const facets of view.facets) {
680
+ if (facets.enumeration !== undefined)
681
+ parts.push(`one of ${facets.enumeration.join(" | ")}`);
682
+ for (const pattern of facets.pattern ?? []) {
683
+ if (pattern.length <= 40)
684
+ parts.push(`pattern ${pattern}`);
685
+ }
686
+ if (facets.length !== undefined)
687
+ parts.push(`length ${facets.length}`);
688
+ if (facets.minLength !== undefined || facets.maxLength !== undefined) {
689
+ parts.push(`length ${facets.minLength ?? 0}..${facets.maxLength ?? "*"}`);
690
+ }
691
+ if (facets.totalDigits !== undefined)
692
+ parts.push(`${facets.totalDigits} digits`);
693
+ if (facets.fractionDigits !== undefined)
694
+ parts.push(`${facets.fractionDigits} decimals`);
695
+ if (facets.minInclusive !== undefined || facets.maxInclusive !== undefined) {
696
+ parts.push(`${facets.minInclusive ?? "…"}..${facets.maxInclusive ?? "…"}`);
697
+ }
698
+ }
699
+ return [...new Set(parts)].join(", ");
700
+ };
701
+ const occursText = (field) => field.maxOccurs === "unbounded"
702
+ ? `${field.minOccurs}..*`
703
+ : field.minOccurs === field.maxOccurs
704
+ ? `${field.minOccurs}`
705
+ : `${field.minOccurs}..${field.maxOccurs}`;
706
+ /**
707
+ * The commented YAML body of an instance of `element`: required fields with
708
+ * example values and a type note, optional ones as commented lines, choice
709
+ * alternatives after the first commented as `or`.
710
+ */
711
+ export const skeletonYaml = (catalog, element, indent, options = {}) => {
712
+ const maxDepth = options.maxDepth ?? 8;
713
+ const lines = [];
714
+ const emit = (depth, text) => lines.push(`${" ".repeat(depth)}${text}`);
715
+ const complexBody = (type, depth, level, commented) => {
716
+ const hash = commented ? "# " : "";
717
+ for (const attribute of type.attributes) {
718
+ const value = exampleValue(catalog, attribute, options.seeds);
719
+ const optional = !attribute.required && options.includeOptional !== true;
720
+ emit(depth, `${hash}${optional ? "# " : ""}${yamlKey(`@${attribute.name}`)}: ${yamlScalar(value)} # attribute, ${describeSimple(catalog, attribute.type)}`);
721
+ }
722
+ if (type.textType !== undefined) {
723
+ emit(depth, `${hash}"#text": ${yamlScalar(exampleValue(catalog, { name: localName(type.name), type: type.textType }, options.seeds))}`);
724
+ }
725
+ // The first alternative of each choice is written out; the others (whole
726
+ // branches, not single fields) are commented as `or`.
727
+ const alternatives = new Set();
728
+ const markAlternatives = (particle) => {
729
+ if (particle.kind === "element")
730
+ return;
731
+ particle.children.forEach((child, position) => {
732
+ if (particle.kind === "choice" && position > 0) {
733
+ for (const field of particleFields(child))
734
+ alternatives.add(field);
735
+ }
736
+ markAlternatives(child);
737
+ });
738
+ };
739
+ markAlternatives(contentModel(type));
740
+ for (const field of type.fields) {
741
+ const alternative = alternatives.has(field);
742
+ const optional = field.minOccurs === 0 && options.includeOptional !== true;
743
+ const comment = commented || optional || alternative;
744
+ fieldLines(field, depth, level, comment, alternative);
745
+ }
746
+ };
747
+ const fieldLines = (field, depth, level, commented, alternative) => {
748
+ const hash = commented ? "# " : "";
749
+ const note = `${alternative ? "or, " : ""}${occursText(field)}${field.nillable ? ", nillable" : ""}`;
750
+ const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
751
+ const type = typeByName(catalog, field.type);
752
+ const key = yamlKey(field.name);
753
+ if (type instanceof ComplexTypeDef) {
754
+ const doc = field.documentation ?? type.documentation;
755
+ emit(depth, `${hash}${key}: # ${localName(field.type).replace(/^~/, "")} [${note}]${doc === undefined ? "" : ` — ${doc}`}`);
756
+ if (level >= maxDepth) {
757
+ emit(depth + 2, `# … nested deeper than ${maxDepth} levels`);
758
+ return;
759
+ }
760
+ const header = lines.length - 1;
761
+ if (repeated) {
762
+ emit(depth + 2, `${hash}-`);
763
+ complexBody(type, depth + 4, level + 1, commented);
764
+ }
765
+ else {
766
+ complexBody(type, depth + 2, level + 1, commented);
767
+ }
768
+ // A required element whose children are all optional is written as
769
+ // an explicit empty map, never as a key with only comments (null).
770
+ if (!commented &&
771
+ lines
772
+ .slice(header + 1)
773
+ .every((line) => line.trimStart().startsWith("#") || line.trim() === "-")) {
774
+ if (repeated) {
775
+ const dash = lines.findIndex((line, index) => index > header && line.trim() === "-");
776
+ if (dash >= 0)
777
+ lines[dash] = `${" ".repeat(depth + 2)}- {}`;
778
+ }
779
+ else {
780
+ lines[header] = (lines[header] ?? "").replace(/^(\s*[^:]+):/, "$1: {}");
781
+ }
782
+ }
783
+ return;
784
+ }
785
+ const value = exampleValue(catalog, field, options.seeds);
786
+ const description = describeSimple(catalog, field.type);
787
+ const doc = field.documentation === undefined ? "" : ` — ${field.documentation}`;
788
+ if (repeated) {
789
+ emit(depth, `${hash}${key}: # ${description} [${note}]${doc}`);
790
+ emit(depth + 2, `${hash}- ${yamlScalar(value)}`);
791
+ }
792
+ else {
793
+ emit(depth, `${hash}${key}: ${yamlScalar(value)} # ${description} [${note}]${doc}`);
794
+ }
795
+ };
796
+ const root = typeByName(catalog, element.type);
797
+ if (root instanceof ComplexTypeDef)
798
+ complexBody(root, indent, 1, false);
799
+ else
800
+ emit(indent, `"#text": ${yamlScalar(exampleValue(catalog, { name: localName(element.name), type: element.type }, options.seeds))} # ${describeSimple(catalog, element.type)}`);
801
+ return lines;
802
+ };
803
+ // ---------------------------------------------------------------------------
804
+ // Content models over value maps
805
+ const join = (path, segment) => path === "" ? segment : `${path}.${segment}`;
806
+ const unbounded = (occurs) => occurs === "unbounded" ? Infinity : occurs;
807
+ /** How many elements a value map holds for a field. */
808
+ const countIn = (value, name) => {
809
+ const item = value[name];
810
+ return item === undefined ? 0 : isYamlList(item) ? item.length : 1;
811
+ };
812
+ const labelOf = (particle, nested = false) => {
813
+ if (particle.kind === "element")
814
+ return particle.field.name;
815
+ const parts = particle.children.map((child) => labelOf(child, true));
816
+ if (particle.kind !== "choice")
817
+ return parts.join(" + ");
818
+ return nested ? `(${parts.join(" | ")})` : parts.join(" | ");
819
+ };
820
+ /**
821
+ * Whether the element counts of a value map can come from `particle`
822
+ * occurring `times` times. A value map keeps no order, so this is the
823
+ * question XSD asks of it: a repeating sequence needs balanced counts, a
824
+ * repeating choice any mix of its alternatives. A term is tried up to as
825
+ * many repetitions as there are elements (a minimal repetition is never
826
+ * empty; beyond it an emptiable term stays satisfiable).
827
+ */
828
+ const fitsCounts = (value, particle, times) => {
829
+ const memo = new Map();
830
+ const sizes = new Map();
831
+ const sizeOf = (node) => {
832
+ const known = sizes.get(node);
833
+ if (known !== undefined)
834
+ return known;
835
+ const size = particleFields(node).reduce((sum, field) => sum + countIn(value, field.name), 0);
836
+ sizes.set(node, size);
837
+ return size;
838
+ };
839
+ const term = (node, repetitions) => {
840
+ if (node.kind === "element")
841
+ return countIn(value, node.field.name) === repetitions;
842
+ if (node.kind !== "choice")
843
+ return node.children.every((child) => occurs(child, repetitions));
844
+ // Split the repetitions among the alternatives.
845
+ let reachable = new Set([0]);
846
+ for (const child of node.children) {
847
+ const next = new Set();
848
+ for (const done of reachable) {
849
+ for (let share = 0; done + share <= repetitions; share++) {
850
+ if (occurs(child, share))
851
+ next.add(done + share);
852
+ }
853
+ }
854
+ reachable = next;
855
+ }
856
+ return reachable.has(repetitions);
857
+ };
858
+ const occurs = (node, count) => {
859
+ const cached = memo.get(node)?.get(count);
860
+ if (cached !== undefined)
861
+ return cached;
862
+ const from = count * node.min;
863
+ const to = count === 0 ? 0 : Math.min(count * unbounded(node.max), Math.max(from, sizeOf(node)));
864
+ let result = false;
865
+ for (let repetitions = from; repetitions <= to && !result; repetitions++) {
866
+ result = term(node, repetitions);
867
+ }
868
+ memo.set(node, (memo.get(node) ?? new Map()).set(count, result));
869
+ return result;
870
+ };
871
+ return occurs(particle, times);
872
+ };
873
+ /**
874
+ * Check a value map against a complex type's content model: required
875
+ * fields and choices, all-or-nothing optional groups, balanced repeating
876
+ * groups, and each field's number of occurrences. Values are not descended.
877
+ */
878
+ const checkContent = (type, value, path) => {
879
+ const issues = [];
880
+ const where = path === "" ? "(root)" : path;
881
+ const present = (particle) => particleFields(particle).some((field) => countIn(value, field.name) > 0);
882
+ const counts = (particle) => [...new Set(particleFields(particle).map((field) => field.name))]
883
+ .map((name) => `${name} ${countIn(value, name)}`)
884
+ .join(", ");
885
+ // `repeating`: inside a group that repeats, where different occurrences
886
+ // may take different alternatives of a choice.
887
+ const walk = (particle, repeating) => {
888
+ if (particle.kind === "element") {
889
+ const field = particle.field;
890
+ const count = countIn(value, field.name);
891
+ if (count === 0 && particle.min > 0) {
892
+ issues.push({ path: join(path, field.name), detail: "required field is missing" });
893
+ }
894
+ else if (!repeating &&
895
+ count > 0 &&
896
+ count < particle.min &&
897
+ field.minOccurs < particle.min) {
898
+ issues.push({
899
+ path: join(path, field.name),
900
+ detail: `at least ${particle.min} occurrences`
901
+ });
902
+ }
903
+ return;
904
+ }
905
+ const here = present(particle);
906
+ if (!here && (particle.min === 0 || emptiable(particle)))
907
+ return;
908
+ if (!here && particle.kind === "choice") {
909
+ issues.push({
910
+ path: where,
911
+ detail: `one of ${particle.children.map((child) => labelOf(child, true)).join(", ")} is required`
912
+ });
913
+ return;
914
+ }
915
+ const repeats = unbounded(particle.max) > 1;
916
+ const before = issues.length;
917
+ if (particle.kind === "choice" && !(repeating || repeats)) {
918
+ const taken = particle.children.filter(present);
919
+ if (taken.length > 1) {
920
+ issues.push({
921
+ path: where,
922
+ detail: `choose one of ${taken.map((child) => labelOf(child, true)).join(", ")}`
923
+ });
924
+ return;
925
+ }
926
+ for (const child of taken)
927
+ walk(child, false);
928
+ }
929
+ else if (particle.kind === "choice") {
930
+ for (const child of particle.children.filter(present))
931
+ walk(child, true);
932
+ }
933
+ else {
934
+ for (const child of particle.children)
935
+ walk(child, repeating || repeats);
936
+ }
937
+ if (repeats && here && issues.length === before && !fitsCounts(value, particle, 1)) {
938
+ issues.push({
939
+ path: where,
940
+ detail: `${labelOf(particle)} repeat${particle.kind === "choice" ? "s" : " together"} as a group; counts ${counts(particle)} do not fit`
941
+ });
942
+ }
943
+ };
944
+ walk(contentModel(type), false);
945
+ for (const field of type.fields) {
946
+ const item = value[field.name];
947
+ if (item === undefined)
948
+ continue;
949
+ const fieldPath = join(path, field.name);
950
+ const count = isYamlList(item) ? item.length : 1;
951
+ const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
952
+ if (isYamlList(item) && !repeated) {
953
+ issues.push({ path: fieldPath, detail: "occurs at most once; write a single value" });
954
+ }
955
+ if (count < field.minOccurs)
956
+ issues.push({ path: fieldPath, detail: `at least ${field.minOccurs} occurrences` });
957
+ if (field.maxOccurs !== "unbounded" && count > field.maxOccurs) {
958
+ issues.push({ path: fieldPath, detail: `at most ${field.maxOccurs} occurrences` });
959
+ }
960
+ }
961
+ return issues;
962
+ };
963
+ // ---------------------------------------------------------------------------
964
+ // Validation of values
965
+ const validateComplex = (catalog, type, value, path, issues, fixed) => {
966
+ const known = new Set([
967
+ ...type.fields.map((field) => field.name),
968
+ ...type.attributes.map((attribute) => `@${attribute.name}`),
969
+ ...(type.textType === undefined ? [] : ["#text"])
970
+ ]);
971
+ for (const key of Object.keys(value)) {
972
+ if (!known.has(key))
973
+ issues.push({
974
+ path: join(path, key),
975
+ detail: `not a field of ${localName(type.name).replace(/^~/, "")}`
976
+ });
977
+ }
978
+ for (const attribute of type.attributes) {
979
+ const item = value[`@${attribute.name}`];
980
+ const attributePath = join(path, `@${attribute.name}`);
981
+ if (item === undefined || item === null) {
982
+ if (attribute.required)
983
+ issues.push({ path: attributePath, detail: "required attribute is missing" });
984
+ }
985
+ else if (typeof item !== "string") {
986
+ issues.push({ path: attributePath, detail: "an attribute holds a single value" });
987
+ }
988
+ else {
989
+ const problems = [
990
+ ...checkSimple(catalog, attribute.type, item),
991
+ ...(attribute.fixed === undefined
992
+ ? []
993
+ : checkFixed(catalog, attribute.type, item, attribute.fixed, false))
994
+ ];
995
+ for (const problem of problems)
996
+ issues.push({ path: attributePath, detail: problem });
997
+ }
998
+ }
999
+ if (type.textType !== undefined) {
1000
+ const text = value["#text"];
1001
+ if (typeof text === "string") {
1002
+ const problems = [
1003
+ ...checkSimple(catalog, type.textType, text),
1004
+ ...(fixed === undefined ? [] : checkFixed(catalog, type.textType, text, fixed, true))
1005
+ ];
1006
+ for (const problem of problems)
1007
+ issues.push({ path: join(path, "#text"), detail: problem });
1008
+ }
1009
+ }
1010
+ issues.push(...checkContent(type, value, path));
1011
+ for (const field of type.fields) {
1012
+ const item = value[field.name];
1013
+ if (item === undefined)
1014
+ continue;
1015
+ const fieldPath = join(path, field.name);
1016
+ const items = isYamlList(item) ? item : [item];
1017
+ items.forEach((entry, position) => {
1018
+ validateValue(catalog, field.type, field.nillable, entry, isYamlList(item) ? `${fieldPath}[${position}]` : fieldPath, issues, field.fixed);
1019
+ });
1020
+ }
1021
+ };
1022
+ const validateValue = (catalog, typeName, nillable, value, path, issues, fixed) => {
1023
+ if (value === null) {
1024
+ if (!nillable)
1025
+ issues.push({ path, detail: "is not nillable; give a value or remove the field" });
1026
+ return;
1027
+ }
1028
+ const type = typeByName(catalog, typeName);
1029
+ if (type instanceof ComplexTypeDef) {
1030
+ if (typeof value === "string" && type.textType !== undefined) {
1031
+ validateComplex(catalog, type, { "#text": value }, path, issues, fixed);
1032
+ }
1033
+ else if (!isYamlMap(value)) {
1034
+ issues.push({ path, detail: `expects fields of ${localName(type.name).replace(/^~/, "")}` });
1035
+ }
1036
+ else {
1037
+ validateComplex(catalog, type, value, path, issues, fixed);
1038
+ }
1039
+ return;
1040
+ }
1041
+ // A simple value may also be written as `"#text": value` (a simple-typed root).
1042
+ let text = value;
1043
+ if (isYamlMap(value) && typeof value["#text"] === "string") {
1044
+ for (const key of Object.keys(value)) {
1045
+ if (key !== "#text")
1046
+ issues.push({ path: join(path, key), detail: "not a field of a simple value" });
1047
+ }
1048
+ text = value["#text"];
1049
+ }
1050
+ if (typeof text !== "string") {
1051
+ issues.push({ path, detail: "expects a single value" });
1052
+ return;
1053
+ }
1054
+ const problems = [
1055
+ ...checkSimple(catalog, typeName, text),
1056
+ ...(fixed === undefined ? [] : checkFixed(catalog, typeName, text, fixed, true))
1057
+ ];
1058
+ for (const problem of problems)
1059
+ issues.push({ path, detail: problem });
1060
+ };
1061
+ /** Validate a value tree against a global element's type. */
1062
+ export const validateInstance = (catalog, element, value) => {
1063
+ const issues = [];
1064
+ validateValue(catalog, element.type, element.nillable, value, "", issues, element.fixed);
1065
+ return issues.map((issue) => (issue.path === "" ? { ...issue, path: "(root)" } : issue));
1066
+ };
1067
+ // ---------------------------------------------------------------------------
1068
+ // Value → XML
1069
+ /**
1070
+ * The children of a value map in wire order: the content model is walked
1071
+ * and each occurrence takes the next entries, so a repeating sequence is
1072
+ * written as interleaved groups and a repeating choice as its alternatives.
1073
+ * Entries the model leaves over (an invalid value) follow in field order.
1074
+ */
1075
+ const wireOrder = (type, value) => {
1076
+ const queues = new Map();
1077
+ for (const field of type.fields) {
1078
+ const item = value[field.name];
1079
+ if (item !== undefined && !queues.has(field.name)) {
1080
+ queues.set(field.name, isYamlList(item) ? [...item] : [item]);
1081
+ }
1082
+ }
1083
+ const out = [];
1084
+ const remaining = (particle) => particleFields(particle).some((field) => (queues.get(field.name)?.length ?? 0) > 0);
1085
+ const take = (field) => {
1086
+ const queue = queues.get(field.name);
1087
+ if (queue !== undefined && queue.length > 0)
1088
+ out.push({ field, entry: queue.shift() ?? null });
1089
+ };
1090
+ const occurrence = (particle) => {
1091
+ const limit = unbounded(particle.max);
1092
+ for (let count = 0; count < limit && remaining(particle); count++) {
1093
+ if (particle.kind === "element")
1094
+ take(particle.field);
1095
+ else if (particle.kind === "choice") {
1096
+ const branch = particle.children.find(remaining);
1097
+ if (branch !== undefined)
1098
+ occurrence(branch);
1099
+ }
1100
+ else
1101
+ particle.children.forEach(occurrence);
1102
+ }
1103
+ };
1104
+ occurrence(contentModel(type));
1105
+ for (const field of type.fields) {
1106
+ const queue = queues.get(field.name) ?? [];
1107
+ while (queue.length > 0)
1108
+ take(field);
1109
+ }
1110
+ return out;
1111
+ };
1112
+ /** Serialize an instance of `element` with prefixes `ns1`, `ns2`… declared on the root. */
1113
+ export const instanceToXml = (catalog, element, value) => {
1114
+ const prefixes = new Map();
1115
+ const prefixOf = (namespace) => {
1116
+ if (namespace === "")
1117
+ return "";
1118
+ let prefix = prefixes.get(namespace);
1119
+ if (prefix === undefined) {
1120
+ prefix = `ns${prefixes.size + 1}`;
1121
+ prefixes.set(namespace, prefix);
1122
+ }
1123
+ return prefix;
1124
+ };
1125
+ let usesXsi = false;
1126
+ const qualified = (namespace, local) => {
1127
+ const prefix = prefixOf(namespace);
1128
+ return prefix === "" ? local : `${prefix}:${local}`;
1129
+ };
1130
+ const write = (name, typeName, item) => {
1131
+ if (item === null) {
1132
+ usesXsi = true;
1133
+ return `<${name} xsi:nil="true"/>`;
1134
+ }
1135
+ const type = typeByName(catalog, typeName);
1136
+ if (!(type instanceof ComplexTypeDef)) {
1137
+ const text = typeof item === "string"
1138
+ ? item
1139
+ : isYamlMap(item) && typeof item["#text"] === "string"
1140
+ ? item["#text"]
1141
+ : "";
1142
+ return `<${name}>${escapeText(text)}</${name}>`;
1143
+ }
1144
+ const map = typeof item === "string" ? { "#text": item } : isYamlMap(item) ? item : {};
1145
+ const attributes = type.attributes
1146
+ .flatMap((attribute) => {
1147
+ const attributeValue = map[`@${attribute.name}`];
1148
+ return typeof attributeValue === "string"
1149
+ ? [
1150
+ ` ${qualified(attribute.namespace ?? "", attribute.name)}="${escapeAttribute(attributeValue)}"`
1151
+ ]
1152
+ : [];
1153
+ })
1154
+ .join("");
1155
+ const text = typeof map["#text"] === "string" ? escapeText(map["#text"]) : "";
1156
+ const children = wireOrder(type, map)
1157
+ .map(({ field, entry }) => write(qualified(field.namespace, field.name), field.type, entry))
1158
+ .join("");
1159
+ const content = `${text}${children}`;
1160
+ return content === "" ? `<${name}${attributes}/>` : `<${name}${attributes}>${content}</${name}>`;
1161
+ };
1162
+ const root = splitClark(element.name);
1163
+ const rootName = qualified(root.namespace, root.local);
1164
+ const body = write(rootName, element.type, value);
1165
+ const declarations = [
1166
+ ...[...prefixes].map(([namespace, prefix]) => ` xmlns:${prefix}="${escapeAttribute(namespace)}"`),
1167
+ ...(usesXsi ? [` xmlns:xsi="${XsiNamespace}"`] : [])
1168
+ ].join("");
1169
+ return body.replace(/^<([^\s/>]+)/, `<$1${declarations}`);
1170
+ };
1171
+ // ---------------------------------------------------------------------------
1172
+ // XML → value
1173
+ /** Whether a sequence of child names can be read in order by the content model. */
1174
+ const followsModel = (model, names) => {
1175
+ // Every position reachable after one occurrence of a particle from `from`.
1176
+ const ends = (particle, from) => {
1177
+ const result = new Set();
1178
+ if (particle.min === 0)
1179
+ result.add(from);
1180
+ const seen = new Set();
1181
+ let frontier = new Set([from]);
1182
+ const limit = unbounded(particle.max);
1183
+ for (let count = 1; count <= limit && frontier.size > 0; count++) {
1184
+ const next = new Set();
1185
+ for (const position of frontier)
1186
+ for (const end of termEnds(particle, position))
1187
+ next.add(end);
1188
+ if (count < particle.min) {
1189
+ frontier = next;
1190
+ continue;
1191
+ }
1192
+ for (const end of next)
1193
+ result.add(end);
1194
+ frontier = new Set([...next].filter((end) => !seen.has(end)));
1195
+ for (const end of next)
1196
+ seen.add(end);
1197
+ }
1198
+ return result;
1199
+ };
1200
+ const termEnds = (particle, from) => {
1201
+ if (particle.kind === "element") {
1202
+ return names[from] === particle.field.name ? new Set([from + 1]) : new Set();
1203
+ }
1204
+ if (particle.kind === "choice") {
1205
+ return new Set(particle.children.flatMap((child) => [...ends(child, from)]));
1206
+ }
1207
+ if (particle.kind === "sequence") {
1208
+ let positions = new Set([from]);
1209
+ for (const child of particle.children) {
1210
+ positions = new Set([...positions].flatMap((position) => [...ends(child, position)]));
1211
+ }
1212
+ return positions;
1213
+ }
1214
+ // xs:all: every child at most once, in any order.
1215
+ const result = new Set();
1216
+ const visited = new Set();
1217
+ const explore = (position, used) => {
1218
+ const key = `${position}:${[...used].join(",")}`;
1219
+ if (visited.has(key))
1220
+ return;
1221
+ visited.add(key);
1222
+ const complete = particle.children.every((child, index) => used.has(index) || emptiable(child));
1223
+ if (complete)
1224
+ result.add(position);
1225
+ particle.children.forEach((child, index) => {
1226
+ if (used.has(index))
1227
+ return;
1228
+ for (const end of ends(child, position)) {
1229
+ if (end !== position)
1230
+ explore(end, new Set([...used, index]));
1231
+ }
1232
+ });
1233
+ };
1234
+ explore(from, new Set());
1235
+ return result;
1236
+ };
1237
+ return ends(model, 0).has(names.length);
1238
+ };
1239
+ /**
1240
+ * Read an XML element as an instance of `element`'s type. Unknown children,
1241
+ * wrong namespaces, repeated singletons, and children out of the schema's
1242
+ * order become issues; values are kept as found so a response can be
1243
+ * analysed even when it breaks its schema.
1244
+ */
1245
+ export const instanceFromXml = (catalog, element, xml) => {
1246
+ const issues = [];
1247
+ const expected = splitClark(element.name);
1248
+ if (xml.name.local !== expected.local || xml.name.namespace !== expected.namespace) {
1249
+ issues.push({
1250
+ path: "(root)",
1251
+ detail: `expected {${expected.namespace}}${expected.local}, found {${xml.name.namespace}}${xml.name.local}`
1252
+ });
1253
+ }
1254
+ const nil = (node) => node.attributes.some((attribute) => attribute.name.namespace === XsiNamespace &&
1255
+ attribute.name.local === "nil" &&
1256
+ (attribute.value === "true" || attribute.value === "1"));
1257
+ const noChildren = (node, path) => {
1258
+ const children = elements(node);
1259
+ if (children.length > 0) {
1260
+ issues.push({
1261
+ path: path === "" ? "(root)" : path,
1262
+ detail: `unexpected child element ${children.map((child) => child.name.local).join(", ")} in a simple value`
1263
+ });
1264
+ }
1265
+ };
1266
+ const read = (node, typeName, path) => {
1267
+ if (nil(node))
1268
+ return null;
1269
+ const type = typeByName(catalog, typeName);
1270
+ if (!(type instanceof ComplexTypeDef)) {
1271
+ noChildren(node, path);
1272
+ return textOf(node);
1273
+ }
1274
+ const result = {};
1275
+ for (const attribute of node.attributes) {
1276
+ if (attribute.name.namespace === XsiNamespace)
1277
+ continue;
1278
+ const attributePath = join(path, `@${attribute.name.local}`);
1279
+ const declared = type.attributes.find((candidate) => candidate.name === attribute.name.local);
1280
+ if (declared === undefined) {
1281
+ issues.push({ path: attributePath, detail: "attribute not declared" });
1282
+ }
1283
+ else if ((declared.namespace ?? "") !== attribute.name.namespace) {
1284
+ issues.push({
1285
+ path: attributePath,
1286
+ detail: `namespace ${attribute.name.namespace === "" ? "(none)" : attribute.name.namespace}, expected ${declared.namespace ?? "(none)"}`
1287
+ });
1288
+ }
1289
+ result[`@${attribute.name.local}`] = attribute.value;
1290
+ }
1291
+ if (type.textType !== undefined) {
1292
+ noChildren(node, path);
1293
+ result["#text"] = textOf(node);
1294
+ }
1295
+ else if (textOf(node).trim() !== "")
1296
+ issues.push({ path: path === "" ? "(root)" : path, detail: "unexpected text content" });
1297
+ const order = [];
1298
+ for (const child of type.textType === undefined ? elements(node) : []) {
1299
+ const field = type.fields.find((candidate) => candidate.name === child.name.local);
1300
+ const childPath = join(path, child.name.local);
1301
+ if (field === undefined) {
1302
+ // Kept as text; validation reports it as not a field of the type.
1303
+ result[child.name.local] = textOf(child);
1304
+ continue;
1305
+ }
1306
+ order.push(field.name);
1307
+ if (child.name.namespace !== field.namespace) {
1308
+ issues.push({
1309
+ path: childPath,
1310
+ detail: `namespace ${child.name.namespace === "" ? "(none)" : child.name.namespace}, expected ${field.namespace === "" ? "(none)" : field.namespace}`
1311
+ });
1312
+ }
1313
+ const childValue = read(child, field.type, childPath);
1314
+ const repeated = field.maxOccurs === "unbounded" || field.maxOccurs > 1;
1315
+ const existing = result[field.name];
1316
+ if (repeated) {
1317
+ const prior = existing !== undefined && isYamlList(existing) ? existing : [];
1318
+ result[field.name] = [...prior, childValue];
1319
+ }
1320
+ else if (existing !== undefined) {
1321
+ issues.push({ path: childPath, detail: "occurs more than once" });
1322
+ }
1323
+ else {
1324
+ result[field.name] = childValue;
1325
+ }
1326
+ }
1327
+ // Order only matters once the counts fit: otherwise validation says why.
1328
+ const model = contentModel(type);
1329
+ if (!followsModel(model, order) && checkContent(type, result, path).length === 0) {
1330
+ issues.push({
1331
+ path: path === "" ? "(root)" : path,
1332
+ detail: `children ${order.join(", ")} are out of the schema's order ${[...new Set(particleFields(model).map((field) => field.name))].join(", ")}`
1333
+ });
1334
+ }
1335
+ return result;
1336
+ };
1337
+ const value = read(xml, element.type, "");
1338
+ return { value, issues: [...issues, ...validateInstance(catalog, element, value)] };
1339
+ };