@joymerrevent/porters-connect 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,68 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.19.1] - 2026-09-20
9
+
10
+ **定期レビューで見つけた 7 件を塞いだ版**です。破壊的変更はありません。公開 API の形は変わらず、
11
+ **壊れたときの倒れ方**が変わります。
12
+
13
+ いちばん重いのは **Option の選択肢 alias から書き込み XML を注入できた**こと。PORTERS は Option の値を
14
+ **タグ名**として書くため(`<FieldAlias><OptionAlias/></FieldAlias>`)、その値を検証していないと
15
+ **呼び出し側が指定していないレコードが書き換わり**ます。要素名はエスケープできないので、検証して弾く形にしました
16
+ ([ADR-0085][adr85])。
17
+
18
+ ### Security
19
+
20
+ - **Option の選択肢 alias と書き込み項目 alias を、XML の名前として妥当か検証するようになりました**
21
+ ([ADR-0085][adr85])。公開型が `string[]` なので cast なしで到達でき、`<Item>` を閉じて開き直す文字列を
22
+ 渡すと **well-formed な XML に別レコードを名指す `<Item>` を注入**できていました(更新先が
23
+ すり替わる/頼んでいない項目が書き足される)。
24
+
25
+ ```ts
26
+ await t.candidate.update(10001, { P_Phase: [userInput] });
27
+ // 不正なら送信前に PortersConfigError(category: "validation")
28
+ ```
29
+
30
+ 通るのは **XML の `Name`**(英数字・`_`・`-`・`.`・日本語など。先頭に数字や `-` は置けません)。
31
+ 出典が alias の書式を定めていないので**XML が許すものはすべて許します** — `Option.P_東京` のような
32
+ alias も従来どおり書けます。**本文になる値(テキスト項目など)の扱いは変わりません。**
33
+
34
+ ### Fixed
35
+
36
+ - **`createThrottle` が実質 0 件の上限を受け付けて永久に待つのをやめました**。バケットのトークンは
37
+ `floor(上限 × safety)` 個で、既定 `safety` は 0.9。そのため `createThrottle({ readPerMin: 1 })` は
38
+ 容量 0 になり、**すべての呼び出しが返らなく**なっていました(例外もログも無し)。
39
+
40
+ ```ts
41
+ createThrottle({ readPerMin: 1 }); // PortersConfigError(floor(1 × 0.9) = 0)
42
+ createThrottle({ readPerMin: 2 }); // OK(floor(1.8) = 1)
43
+ ```
44
+
45
+ `readPerMin` / `writePerMin` は**正の整数**、`safety` は **0 より大きく 1 以下**。加えて
46
+ **積が 1 以上**であることを見ます。「1 件も通さない」は `take()` が解決しない `Throttle` を
47
+ 自分で渡してください。
48
+
49
+ - **XML の解析に失敗したとき、例外が必ず `PortersError` になるようになりました**。
50
+ `fast-xml-parser` は `prototype` / `constructor` / `__proto__` をタグ名として拒否します。
51
+ これらは妥当な XML Name なので書き込みは通り、**読み取りだけ**が素の `Error` で落ちていました
52
+ — `catch (e) { if (e instanceof PortersError) … }` に**引っかからず**、アプリの最上位まで
53
+ 素通りします。Read は `PortersResourceError`、認証は `PortersAuthError` に包み、パーサ自身の
54
+ 説明は `cause` に残します。壊れた XML も同じ経路になりました。
55
+
56
+ - **公開 API リファレンス**(`docs/usage/api/`)に残っていた日本語を英語に直しました。
57
+ 公開サーフェスの JSDoc は英語、README とガイドは日本語ファースト、という切り分けは変わりません。
58
+
59
+ ### Changed
60
+
61
+ - **CI が `engines` の下限そのものを走らせるようになりました**。`engines.node` は `>=22.12.0` を
62
+ 約束していますが、テストの Node マトリクスは `22`(その時点の 22 系最新に解決される)だったため、
63
+ **22.12.0 は一度も走っていません**でした。Node 22.12.0 で動かしている場合、そのバージョンが
64
+ 実際に検証されるようになります。
65
+
66
+ - 内部の検査を 2 本増やしました(利用者への影響はありません)。契約後に実機確認する仮定と
67
+ コード側のコメントの対応を双方向で見る検査と、リファレンスへの日本語混入を見る検査です。
68
+ 前者では**コードに仮定があるのに一覧に無いもの**が 3 件見つかり、起票しました。
69
+
8
70
  ## [0.19.0] - 2026-09-18
9
71
 
10
72
  **CJS からの入口を開け、Node の下限を 22.12 に上げた版**です。**破壊的変更を 1 つ**含みます
@@ -1062,11 +1124,13 @@ Attachment)あるのに、受け口の形が 3 つとも違っていました
1062
1124
  [adr82]: docs/adr/0082-module-format-and-node-baseline.md
1063
1125
  [limits]: docs/usage/concepts/limits.md
1064
1126
  [failures]: docs/usage/howto/handle-failures.md
1127
+ [adr85]: docs/adr/0085-option-alias-validation.md
1065
1128
  [lv]: docs/live-verification.md
1066
1129
  [ref]: docs/usage/reference/README.md
1067
1130
  [kac]: https://keepachangelog.com/en/1.1.0/
1068
1131
  [semver]: https://semver.org/
1069
- [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.19.0...HEAD
1132
+ [unreleased]: https://github.com/Joymerrevent/porters-connect/compare/v0.19.1...HEAD
1133
+ [0.19.1]: https://github.com/Joymerrevent/porters-connect/compare/v0.19.0...v0.19.1
1070
1134
  [0.19.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.18.0...v0.19.0
1071
1135
  [0.18.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.17.0...v0.18.0
1072
1136
  [0.17.0]: https://github.com/Joymerrevent/porters-connect/compare/v0.16.0...v0.17.0
package/dist/index.d.cts CHANGED
@@ -110,9 +110,22 @@ type Throttle = {
110
110
  take(write: boolean): Promise<void>;
111
111
  };
112
112
  type ThrottleOptions = {
113
+ /**
114
+ * Reads allowed per minute before headroom. Default 2000 (PORTERS' own cap). A positive
115
+ * integer, and **`readPerMin * safety` must still leave at least one token** — see
116
+ * {@link ThrottleOptions.safety}.
117
+ */
113
118
  readPerMin?: number;
119
+ /** Writes allowed per minute before headroom. Default 500. Same rules as `readPerMin`. */
114
120
  writePerMin?: number;
115
- /** Fraction of the limit to actually use (headroom). Default 0.9. */
121
+ /**
122
+ * Fraction of the limit to actually use (headroom). Default 0.9. Greater than 0, at most 1.
123
+ *
124
+ * The bucket holds `floor(limit * safety)` tokens, so a small limit and a small `safety`
125
+ * multiply into **zero capacity** — `{ readPerMin: 1 }` at the default 0.9 already does.
126
+ * A bucket that can never hold a token would make every call wait forever, so the
127
+ * combination is rejected at construction rather than hanging (RV-49).
128
+ */
116
129
  safety?: number;
117
130
  now?: () => number;
118
131
  };
@@ -1736,8 +1749,8 @@ type SalesPage = ResourcePage<typeof FIELDS$7>;
1736
1749
  type SalesSearchQuery = SearchQuery<typeof FIELDS$7, typeof REFERENCES$2>;
1737
1750
  /**
1738
1751
  * Fields for `create`: only `P_Owner` is unconditionally required. The six references are
1739
- * required *conditionally* (a dependency chain PORTERS validates server-side) — see the
1740
- * module comment and docs/usage/concepts/limits.md.
1752
+ * required *conditionally* (a dependency chain PORTERS validates server-side), so they stay
1753
+ * optional here (ADR-0083) — see the module comment and docs/usage/concepts/limits.md.
1741
1754
  */
1742
1755
  type SalesCreateInput = CreateInput<typeof FIELDS$7, (typeof REQUIRED_ON_CREATE$3)[number]>;
1743
1756
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -2337,8 +2350,8 @@ type AttachmentUpdate = {
2337
2350
  *
2338
2351
  * ```ts
2339
2352
  * const files = t.attachment.of("resume");
2340
- * await files.search({ resourceId: 10006 }); // メタデータだけ
2341
- * await files.get(900); // 本体つき
2353
+ * await files.search({ resourceId: 10006 }); // metadata only
2354
+ * await files.get(900); // with the file body
2342
2355
  * ```
2343
2356
  *
2344
2357
  * PORTERS requires `resource=` on every Attachment Read, and the same value goes into the
@@ -2646,7 +2659,7 @@ type ReadCustomCatalogOptions = {
2646
2659
  * The default is `-1` deliberately. Narrowing to `1` would hide fields that exist but are
2647
2660
  * unused, and a comparison against a declaration would then report them as **missing** — a
2648
2661
  * false alarm. `generateFieldDecls` overrides it to `1`, where "only what is in use" is what
2649
- * you want in a template (ADR-0069, accept 時の決定).
2662
+ * you want in a template (ADR-0069, decided on accept).
2650
2663
  */
2651
2664
  readonly active?: -1 | 0 | 1;
2652
2665
  };
@@ -2669,7 +2682,7 @@ type GenerateFieldDeclsOptions = {
2669
2682
  * Field Read's `active` filter. Defaults to `1` — **in-use fields only**, which is what belongs
2670
2683
  * in a template; there is no reason to declare a field the tenant is not using. This is the
2671
2684
  * opposite of `verifyFields`, where narrowing would cause false "missing" reports
2672
- * (ADR-0069, accept 時の決定).
2685
+ * (ADR-0069, decided on accept).
2673
2686
  */
2674
2687
  readonly active?: -1 | 0 | 1;
2675
2688
  /**
@@ -2758,7 +2771,7 @@ type VerifyFieldsOptions = {
2758
2771
  /**
2759
2772
  * Field Read's `active` filter. Defaults to `-1` (every field) and should stay there: with `1`
2760
2773
  * a field that exists but is currently unused is absent from the response, and its declaration
2761
- * would be reported as {@link MissingField} — a false alarm (ADR-0069, accept 時の決定).
2774
+ * would be reported as {@link MissingField} — a false alarm (ADR-0069, decided on accept).
2762
2775
  */
2763
2776
  readonly active?: -1 | 0 | 1;
2764
2777
  };
package/dist/index.d.ts CHANGED
@@ -110,9 +110,22 @@ type Throttle = {
110
110
  take(write: boolean): Promise<void>;
111
111
  };
112
112
  type ThrottleOptions = {
113
+ /**
114
+ * Reads allowed per minute before headroom. Default 2000 (PORTERS' own cap). A positive
115
+ * integer, and **`readPerMin * safety` must still leave at least one token** — see
116
+ * {@link ThrottleOptions.safety}.
117
+ */
113
118
  readPerMin?: number;
119
+ /** Writes allowed per minute before headroom. Default 500. Same rules as `readPerMin`. */
114
120
  writePerMin?: number;
115
- /** Fraction of the limit to actually use (headroom). Default 0.9. */
121
+ /**
122
+ * Fraction of the limit to actually use (headroom). Default 0.9. Greater than 0, at most 1.
123
+ *
124
+ * The bucket holds `floor(limit * safety)` tokens, so a small limit and a small `safety`
125
+ * multiply into **zero capacity** — `{ readPerMin: 1 }` at the default 0.9 already does.
126
+ * A bucket that can never hold a token would make every call wait forever, so the
127
+ * combination is rejected at construction rather than hanging (RV-49).
128
+ */
116
129
  safety?: number;
117
130
  now?: () => number;
118
131
  };
@@ -1736,8 +1749,8 @@ type SalesPage = ResourcePage<typeof FIELDS$7>;
1736
1749
  type SalesSearchQuery = SearchQuery<typeof FIELDS$7, typeof REFERENCES$2>;
1737
1750
  /**
1738
1751
  * Fields for `create`: only `P_Owner` is unconditionally required. The six references are
1739
- * required *conditionally* (a dependency chain PORTERS validates server-side) — see the
1740
- * module comment and docs/usage/concepts/limits.md.
1752
+ * required *conditionally* (a dependency chain PORTERS validates server-side), so they stay
1753
+ * optional here (ADR-0083) — see the module comment and docs/usage/concepts/limits.md.
1741
1754
  */
1742
1755
  type SalesCreateInput = CreateInput<typeof FIELDS$7, (typeof REQUIRED_ON_CREATE$3)[number]>;
1743
1756
  /** Fields for `update`: all optional (`null` omits, `""` clears a text field). */
@@ -2337,8 +2350,8 @@ type AttachmentUpdate = {
2337
2350
  *
2338
2351
  * ```ts
2339
2352
  * const files = t.attachment.of("resume");
2340
- * await files.search({ resourceId: 10006 }); // メタデータだけ
2341
- * await files.get(900); // 本体つき
2353
+ * await files.search({ resourceId: 10006 }); // metadata only
2354
+ * await files.get(900); // with the file body
2342
2355
  * ```
2343
2356
  *
2344
2357
  * PORTERS requires `resource=` on every Attachment Read, and the same value goes into the
@@ -2646,7 +2659,7 @@ type ReadCustomCatalogOptions = {
2646
2659
  * The default is `-1` deliberately. Narrowing to `1` would hide fields that exist but are
2647
2660
  * unused, and a comparison against a declaration would then report them as **missing** — a
2648
2661
  * false alarm. `generateFieldDecls` overrides it to `1`, where "only what is in use" is what
2649
- * you want in a template (ADR-0069, accept 時の決定).
2662
+ * you want in a template (ADR-0069, decided on accept).
2650
2663
  */
2651
2664
  readonly active?: -1 | 0 | 1;
2652
2665
  };
@@ -2669,7 +2682,7 @@ type GenerateFieldDeclsOptions = {
2669
2682
  * Field Read's `active` filter. Defaults to `1` — **in-use fields only**, which is what belongs
2670
2683
  * in a template; there is no reason to declare a field the tenant is not using. This is the
2671
2684
  * opposite of `verifyFields`, where narrowing would cause false "missing" reports
2672
- * (ADR-0069, accept 時の決定).
2685
+ * (ADR-0069, decided on accept).
2673
2686
  */
2674
2687
  readonly active?: -1 | 0 | 1;
2675
2688
  /**
@@ -2758,7 +2771,7 @@ type VerifyFieldsOptions = {
2758
2771
  /**
2759
2772
  * Field Read's `active` filter. Defaults to `-1` (every field) and should stay there: with `1`
2760
2773
  * a field that exists but is currently unused is absent from the response, and its declaration
2761
- * would be reported as {@link MissingField} — a false alarm (ADR-0069, accept 時の決定).
2774
+ * would be reported as {@link MissingField} — a false alarm (ADR-0069, decided on accept).
2762
2775
  */
2763
2776
  readonly active?: -1 | 0 | 1;
2764
2777
  };
package/dist/index.js CHANGED
@@ -265,6 +265,30 @@ var createMockTransport = (handler, options = {}) => {
265
265
 
266
266
  // src/http/throttle.ts
267
267
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
268
+ var capacityOf = (limit, safety, option) => {
269
+ if (!Number.isInteger(limit) || limit <= 0) {
270
+ throw new PortersConfigError(
271
+ `${option} must be a positive integer, got ${JSON.stringify(limit)}`,
272
+ {
273
+ category: "config",
274
+ hint: "Pass requests per minute (defaults: readPerMin 2000, writePerMin 500)."
275
+ }
276
+ );
277
+ }
278
+ const capacity = Math.floor(limit * safety);
279
+ if (capacity < 1) {
280
+ throw new PortersConfigError(
281
+ `${option} ${limit} with safety ${safety} leaves no capacity (floor(${limit * safety}) = 0), so every call would wait forever`,
282
+ {
283
+ category: "config",
284
+ // 「まったく通さない」を表現したいなら、それ専用の Throttle を書くのが正しい道。
285
+ // 上限 0 を黙って受けて永久に待つのは、許可と沈黙を混ぜる形(ADR-0047 と同じ考え)。
286
+ hint: `Raise ${option} to at least ${Math.ceil(1 / safety)} at this safety, or raise safety. To block every request on purpose, pass your own Throttle whose take() never resolves.`
287
+ }
288
+ );
289
+ }
290
+ return capacity;
291
+ };
268
292
  var makeBucket = (capacity, now) => {
269
293
  const ratePerMs = capacity / 6e4;
270
294
  let tokens = capacity;
@@ -284,9 +308,24 @@ var makeBucket = (capacity, now) => {
284
308
  };
285
309
  var createThrottle = (opts = {}) => {
286
310
  const safety = opts.safety ?? 0.9;
311
+ if (!(safety > 0) || safety > 1) {
312
+ throw new PortersConfigError(
313
+ `safety must be greater than 0 and at most 1, got ${JSON.stringify(opts.safety)}`,
314
+ {
315
+ category: "config",
316
+ hint: "safety is the fraction of the limit to use (default 0.9 = 90%). Use 1 to run right at PORTERS' cap."
317
+ }
318
+ );
319
+ }
287
320
  const now = opts.now ?? (() => Date.now());
288
- const read = makeBucket(Math.floor((opts.readPerMin ?? 2e3) * safety), now);
289
- const write = makeBucket(Math.floor((opts.writePerMin ?? 500) * safety), now);
321
+ const read = makeBucket(
322
+ capacityOf(opts.readPerMin ?? 2e3, safety, "readPerMin"),
323
+ now
324
+ );
325
+ const write = makeBucket(
326
+ capacityOf(opts.writePerMin ?? 500, safety, "writePerMin"),
327
+ now
328
+ );
290
329
  return { take: (isWrite) => isWrite ? write() : read() };
291
330
  };
292
331
  var createThrottleRegistry = (make = () => createThrottle()) => {
@@ -415,19 +454,28 @@ var parser = new XMLParser({
415
454
  // Stryker disable next-line ArrowFunction,ConditionalExpression,StringLiteral: equivalent — asArray() normalizes regardless
416
455
  isArray: (name) => name === "Item"
417
456
  });
457
+ var parseXml = (xml, unparseable) => {
458
+ try {
459
+ return parser.parse(xml);
460
+ } catch (cause) {
461
+ throw unparseable(cause);
462
+ }
463
+ };
418
464
  var toInt = (v) => {
419
465
  const s = asString(v);
420
466
  return s === void 0 ? 0 : Number(s);
421
467
  };
422
468
  var MIDDLEBOX_HINT = "A middlebox (proxy, captive portal, SSO login page, WAF notice) may be answering instead of PORTERS. Check PORTERS_HOST and the network path from this process to the API.";
423
469
  var parseResourcePage = (xml, resource) => {
424
- const root = asRecord(parser.parse(xml));
470
+ const unparseable = (cause) => new PortersResourceError("unparseable resource response", {
471
+ category: "unknown",
472
+ cause
473
+ });
474
+ const root = asRecord(parseXml(xml, unparseable));
425
475
  const rootKey = root ? Object.keys(root)[0] : void 0;
426
476
  const body = root && rootKey ? asRecord(root[rootKey]) : void 0;
427
477
  if (!body) {
428
- throw new PortersResourceError("unparseable resource response", {
429
- category: "unknown"
430
- });
478
+ throw unparseable();
431
479
  }
432
480
  if (rootKey !== resource) {
433
481
  throw new PortersResourceError(
@@ -453,13 +501,15 @@ var parseResourcePage = (xml, resource) => {
453
501
  };
454
502
  };
455
503
  var parseWriteResult = (xml) => {
456
- const root = asRecord(parser.parse(xml));
504
+ const unparseable = (cause) => new PortersResourceError("unparseable write response", {
505
+ category: "unknown",
506
+ cause
507
+ });
508
+ const root = asRecord(parseXml(xml, unparseable));
457
509
  const rootKey = root ? Object.keys(root)[0] : void 0;
458
510
  const body = root && rootKey ? asRecord(root[rootKey]) : void 0;
459
511
  if (!body) {
460
- throw new PortersResourceError("unparseable write response", {
461
- category: "unknown"
462
- });
512
+ throw unparseable();
463
513
  }
464
514
  const code = toInt(body.Code);
465
515
  if (code !== 0) {
@@ -474,12 +524,14 @@ var parseWriteResult = (xml) => {
474
524
  });
475
525
  };
476
526
  var parseAuthentication = (xml) => {
477
- const root = asRecord(parser.parse(xml));
527
+ const unparseable = (cause) => new PortersAuthError("unparseable authentication response", {
528
+ category: "unknown",
529
+ cause
530
+ });
531
+ const root = asRecord(parseXml(xml, unparseable));
478
532
  const body = root ? asRecord(root.Authentication) : void 0;
479
533
  if (!body) {
480
- throw new PortersAuthError("unparseable authentication response", {
481
- category: "unknown"
482
- });
534
+ throw unparseable();
483
535
  }
484
536
  const error = toInt(body.Error);
485
537
  if (error !== 0) {
@@ -978,6 +1030,12 @@ var paginateOnce = async function* (prepare) {
978
1030
  yield* paginate(prepare());
979
1031
  };
980
1032
 
1033
+ // src/util/xml-name.ts
1034
+ var NAME_START_CHAR = ":A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\u{10000}-\\u{EFFFF}";
1035
+ var NAME_CHAR = NAME_START_CHAR + "\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
1036
+ var XML_NAME = new RegExp(`^[${NAME_START_CHAR}][${NAME_CHAR}]*$`, "u");
1037
+ var isXmlName = (value) => XML_NAME.test(value);
1038
+
981
1039
  // src/xml/encode.ts
982
1040
  var IMAGE_CONTENT_TYPES = [
983
1041
  "image/jpeg",
@@ -989,6 +1047,19 @@ var escapeXml = (s) => s.replace(
989
1047
  /[&<>]/g,
990
1048
  (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"
991
1049
  );
1050
+ var assertTagName = (name, kind, alias) => {
1051
+ if (isXmlName(name)) return;
1052
+ throw new PortersConfigError(
1053
+ `${alias}: ${kind} ${JSON.stringify(name)} is not a valid XML element name`,
1054
+ {
1055
+ category: "validation",
1056
+ // PORTERS writes this value as a tag (`<FieldAlias><OptionAlias/></FieldAlias>`), so an
1057
+ // arbitrary string cannot be sent. Say that, and say where a real one comes from.
1058
+ hint: "PORTERS writes it as an XML element name, so it must be a valid XML Name (letters, digits, `_`, `-`, `.`, no spaces or markup). Option aliases come from the Option master (`t.option`) or the field's option list in the reference.",
1059
+ context: { operation: "encode" }
1060
+ }
1061
+ );
1062
+ };
992
1063
  var text = (v) => typeof v === "object" && !Array.isArray(v) ? JSON.stringify(v) : String(v);
993
1064
  var scalar = (v) => escapeXml(text(v));
994
1065
  var IMAGE_SUBFIELDS = [
@@ -1025,7 +1096,10 @@ var encodeField = (type, value, alias) => {
1025
1096
  // array (ADR-0017, symmetric with read); a lone string is wrapped as a 1-element
1026
1097
  // selection (fail-safe).
1027
1098
  case "Option":
1028
- return (Array.isArray(value) ? value : [text(value)]).map((alias2) => `<${alias2}/>`).join("");
1099
+ return (Array.isArray(value) ? value : [text(value)]).map((selected) => {
1100
+ assertTagName(selected, "option alias", alias);
1101
+ return `<${selected}/>`;
1102
+ }).join("");
1029
1103
  // System[DateTime] (registration/update) is Write-restricted by PORTERS; we still
1030
1104
  // serialize it identically — rejecting the write is the input type's job (SD-3).
1031
1105
  case "DateTime":
@@ -1065,6 +1139,7 @@ var encodeItem = (prefix, fields, item) => {
1065
1139
  const parts = [];
1066
1140
  for (const [alias, value] of Object.entries(item)) {
1067
1141
  if (value === null || value === void 0) continue;
1142
+ assertTagName(alias, "field alias", alias);
1068
1143
  const type = fields.get(alias);
1069
1144
  const inner = type === void 0 || type === null ? scalar(value) : encodeField(type, value, alias);
1070
1145
  const tag2 = qualify(prefix, alias);
@@ -1860,7 +1935,8 @@ var FIELDS7 = {
1860
1935
  P_Memo: "MultilineText",
1861
1936
  // VERIFY(live): participants is a `User` field that can hold several people. Whether the
1862
1937
  // Read response repeats the nested <User> element (and how) is unconfirmed; the decoder
1863
- // takes the first, like every other User field. See docs/live-verification.md.
1938
+ // takes the first, like every other User field — so a second participant would be dropped
1939
+ // silently. docs/live-verification.md (LV-27).
1864
1940
  P_EventParticipants: "User",
1865
1941
  P_EventResources: "Option",
1866
1942
  // 削除状態("0" / "1")。PORTERS が Data Type を与えていない項目=`null`(ADR-0056。
@@ -2454,6 +2530,10 @@ var createFieldAccessor = (deps) => ({
2454
2530
  var FIELDS16 = {
2455
2531
  P_Id: "System[Id]",
2456
2532
  P_Name: "SinglelineText",
2533
+ // VERIFY(live): LV-26。選択肢 alias は Write でタグ名になるので ADR-0085 が XML Name を要求するが、
2534
+ // ここは**スカラのテキスト**として読むだけで、読めた値がその範囲に収まる保証は無い
2535
+ // (Read 応答の入れ子タグ名から来る値は定義上 Name — 保証が効かないのはこの経路だけ)。
2536
+ // docs/live-verification.md(LV-26)。
2457
2537
  P_Alias: "SinglelineText",
2458
2538
  P_ParentId: "Number",
2459
2539
  P_Type: "Number",