@cardenelabs/dragon 0.30.0 → 0.32.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 (55) hide show
  1. package/README.md +314 -12
  2. package/dist/index.cjs +6966 -1438
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +2150 -118
  5. package/dist/index.d.ts +2150 -118
  6. package/dist/index.js +6950 -1440
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -13
  9. package/src/compile/actor-ids.ts +6 -15
  10. package/src/compile/actor-option-notice.ts +379 -0
  11. package/src/compile/actors.ts +32 -3
  12. package/src/compile/c4.ts +0 -3
  13. package/src/compile/chain.ts +43 -4
  14. package/src/compile/chart-fields.ts +16 -1
  15. package/src/compile/direction.ts +50 -11
  16. package/src/compile/edge-option-notice.ts +112 -0
  17. package/src/compile/flowchart.ts +344 -0
  18. package/src/compile/funnel.ts +14 -2
  19. package/src/compile/gantt.ts +141 -36
  20. package/src/compile/generic.ts +473 -61
  21. package/src/compile/journey.ts +14 -5
  22. package/src/compile/kinds.ts +5 -13
  23. package/src/compile/lanes.ts +95 -13
  24. package/src/compile/metro.ts +271 -0
  25. package/src/compile/mind.ts +55 -5
  26. package/src/compile/notice.ts +51 -4
  27. package/src/compile/order.ts +62 -0
  28. package/src/compile/parts.ts +107 -33
  29. package/src/compile/placement-apply.ts +27 -11
  30. package/src/compile/quadrant.ts +171 -16
  31. package/src/compile/record.ts +75 -0
  32. package/src/compile/row-marks.ts +128 -0
  33. package/src/compile/sequence.ts +105 -26
  34. package/src/compile/slug.ts +9 -1
  35. package/src/compile/stages.ts +247 -0
  36. package/src/compile/subtitle.ts +15 -2
  37. package/src/compile/swimlane-marks.ts +87 -0
  38. package/src/compile/swimlane.ts +13 -14
  39. package/src/compile/timeline.ts +213 -0
  40. package/src/compile/topology.ts +3 -9
  41. package/src/compile/tree.ts +13 -4
  42. package/src/compile/value-chart.ts +179 -25
  43. package/src/compile.ts +1550 -370
  44. package/src/focus.ts +2 -2
  45. package/src/index.ts +34 -2
  46. package/src/json-parser.ts +1407 -48
  47. package/src/keywords.ts +251 -17
  48. package/src/notation-lint.ts +41 -64
  49. package/src/schemas/diagram.json +643 -20
  50. package/src/types.ts +336 -35
  51. package/src/v05/parser.ts +2451 -271
  52. package/src/compile/class.ts +0 -100
  53. package/src/compile/er.ts +0 -44
  54. package/src/compile/solidity.ts +0 -39
  55. package/src/compile/state.ts +0 -71
@@ -24,6 +24,7 @@ import {
24
24
  EDGE_HEAD_VALUES,
25
25
  NODE_KIND_VALID,
26
26
  PRESET_TYPES,
27
+ TYPE_ALIASES,
27
28
  STYLE_VALID,
28
29
  resolveNodeKind,
29
30
  resolveTone,
@@ -36,15 +37,67 @@ import {
36
37
  EVENT_KINDS,
37
38
  EVENT_TARGET_KEYS,
38
39
  EVENT_REQUIRED_KEYS,
40
+ CHART_LINE_DASH_VALUES,
41
+ CHART_LINE_MARKER_VALUES,
42
+ CHART_PIE_TABLE_COLUMN_VALUES,
43
+ CHART_PIE_RING_WIDTH_VALUES,
44
+ CHART_STACKED_LEGEND_POSITION_VALUES,
45
+ CHART_WAFFLE_LEGEND_POSITION_VALUES,
46
+ FUNNEL_FORM_VALUES,
47
+ FUNNEL_RATE_VALUES,
48
+ QUADRANT_POINT_LABEL_SIDE_VALUES,
49
+ TREE_NODE_FORM_VALUES,
50
+ TREE_EDGE_TONE_VALUES,
51
+ TREE_EDGE_HEAD_VALUES,
52
+ MIND_FORM_VALUES,
53
+ JOURNEY_FORM_VALUES,
54
+ JOURNEY_LINE_FORM_VALUES,
55
+ JOURNEY_OPPORTUNITY_POSITION_VALUES,
56
+ JOURNEY_LABEL_KEYS,
57
+ CORNER_FORM_VALUES,
58
+ CHART_BAR_SHAPE_VALUES,
59
+ QUADRANT_LABEL_POSITION_VALUES,
60
+ LABEL_COLLISION_VALUES,
61
+ GANTT_TASK_LABEL_ALIGN_VALUES,
62
+ CDL_ICON_VALUES,
63
+ KIND_FORM_VALUES,
64
+ TITLE_ALIGN_VALUES,
65
+ LEGEND_LINE_STYLE_VALUES,
66
+ LEGEND_NOTE_ALIGN_VALUES,
67
+ ROW_MARK_FORM_VALUES,
68
+ ROW_MARK_TONE_VALUES,
69
+ ROW_DIVIDER_FORM_VALUES,
70
+ ROW_WELL_KEYS,
71
+ SEQUENCE_ACTOR_FORM_VALUES,
72
+ SEQUENCE_LABEL_FORM_VALUES,
73
+ SEQUENCE_RETURN_HEAD_VALUES,
74
+ SEQUENCE_KIND_LABEL_KEYS,
75
+ SEQUENCE_SELF_LOOP_KEYS,
39
76
  type 図形の定義,
40
77
  } from "./v05/parser";
41
- // 図の配色 (#1553)。 記法の読み手と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方が
78
+ // 図の意匠 (#1553 / #2790)。 記法の読み手と同じ解決を通す = 別名の受け方が
42
79
  // 記法と JSON でずれない
43
- import { resolvePalette } from "./keywords";
44
- import type { DslPalette } from "./keywords";
80
+ import {
81
+ PHASE_BODY_KEYS,
82
+ DIAGRAM_SHAPES,
83
+ THEMES,
84
+ THEME_ALIAS,
85
+ resolveOrder,
86
+ resolveDiagramShape,
87
+ shapesForDiagramType,
88
+ resolveTheme,
89
+ resolveLegendMark,
90
+ } from "./keywords";
91
+ // 区画の語の表 (#2667)。 記法と同じ表から直す = 同じ区画を 2 通りで呼ばない
92
+ import { 区画 } from "./compile/word-state";
93
+ // 行頭の印の古い語の読み替え (#2782)。 記法と同じ表を通す = JSON だけ古い語が印を持たない
94
+ // 形にならない
95
+ import { 行頭の語へ読み替える } from "./compile/row-marks";
96
+ import type { DslDiagramShape, DslShape } from "./keywords";
45
97
  import type { CompileToCdlOpts } from "./compile";
46
98
  import type {
47
99
  CdlDiagram,
100
+ CdlLegendItem,
48
101
  NodeKind,
49
102
  Tone,
50
103
  EdgeStyle,
@@ -58,6 +111,8 @@ import type {
58
111
  import { extractIdentifiers, parseFormula } from "@cardenelabs/cdl";
59
112
  import type {
60
113
  DslDocument,
114
+ DslStageHeader,
115
+ DslRegions,
61
116
  DslActor,
62
117
  DslStep,
63
118
  DslAnimate,
@@ -83,18 +138,93 @@ import {
83
138
  type RelativePos,
84
139
  } from "./relative-pos";
85
140
 
141
+ /** 古い綴りで書いた図種 (#2655 / #2657)。 受ける値は `TYPE_ALIASES` が SSOT */
142
+ type 図種の別名 = "solidity" | DslShape;
143
+
86
144
  /**
87
145
  * LLM 向け JSON DSL の入力 shape。 YAML DSL と 1:1 対応、 top-level は flat な object。
88
146
  */
89
147
  export interface DragonJson {
90
148
  /** 図の title (必須) */
91
149
  title: string;
92
- /** preset type (必須): sequence / flow / swimlane / er / state / topology / solidity / gantt / class / pie / c4 / mind */
93
- type: PresetType;
150
+ /**
151
+ * preset type (必須)。 受け付ける値は `PRESET_TYPES` と `TYPE_ALIASES` が SSOT。
152
+ *
153
+ * 別名 (#2655 / #2657) は読み取りで図種と語の 2 つに開く = `solidity` と書いた JSON は
154
+ * 順序図 + 種類で並べ替え、`pie` と書いた JSON は 数を描く図 + 円の形 として読まれる。
155
+ */
156
+ type: PresetType | 図種の別名;
157
+ /** 円グラフの見せ方。 */
158
+ form?: string;
159
+ /** `form` の和名。両方あれば `form` を採る。 */
160
+ 見せ方?: string;
161
+ /** ガントチャートの目盛り。 */
162
+ ticks?: string[];
163
+ /** `ticks` の和名。両方あれば `ticks` を採る。 */
164
+ 目盛り?: string[];
165
+ /** ガントチャートの今日の線の位置と札。 */
166
+ ganttToday?: { index: number; label: string };
167
+ /** 工程の端とは独立した目盛り名。 */
168
+ ganttTickLabels?: string[];
169
+ ganttBarEnd?: "position";
170
+ ganttBarThickness?: "thin";
171
+ chartSeriesSkipMuted?: DslDocument["chartSeriesSkipMuted"];
172
+ chartLineSeries?: DslDocument["chartLineSeries"];
173
+ chartPieCenterLabel?: DslDocument["chartPieCenterLabel"];
174
+ chartPieTableColumns?: DslDocument["chartPieTableColumns"];
175
+ chartPieRingWidth?: DslDocument["chartPieRingWidth"];
176
+ chartSlopePeriods?: DslDocument["chartSlopePeriods"];
177
+ chartSlopeEmphasisIds?: DslDocument["chartSlopeEmphasisIds"];
178
+ chartSlopeUnit?: DslDocument["chartSlopeUnit"];
179
+ chartStackedPeriods?: DslDocument["chartStackedPeriods"];
180
+ chartStackedRateId?: DslDocument["chartStackedRateId"];
181
+ chartStackedLegendPosition?: DslDocument["chartStackedLegendPosition"];
182
+ chartGaugeValue?: DslDocument["chartGaugeValue"];
183
+ chartWaffleLegendPosition?: DslDocument["chartWaffleLegendPosition"];
184
+ funnelForm?: DslDocument["funnelForm"];
185
+ funnelRate?: DslDocument["funnelRate"];
186
+ quadrantPointLabelSide?: DslDocument["quadrantPointLabelSide"];
187
+ treeNodeForm?: DslDocument["treeNodeForm"];
188
+ treeSize?: DslDocument["treeSize"];
189
+ treeEdgeTone?: DslDocument["treeEdgeTone"];
190
+ treeEdgeHead?: DslDocument["treeEdgeHead"];
191
+ mindForm?: DslDocument["mindForm"];
192
+ mindSize?: DslDocument["mindSize"];
193
+ edgeEndGap?: DslDocument["edgeEndGap"];
194
+ journeyForm?: DslDocument["journeyForm"];
195
+ journeyLineForm?: DslDocument["journeyLineForm"];
196
+ journeyLabels?: DslDocument["journeyLabels"];
197
+ journeySize?: DslDocument["journeySize"];
198
+ sequenceActorForm?: DslDocument["sequenceActorForm"];
199
+ sequenceLabelForm?: DslDocument["sequenceLabelForm"];
200
+ sequenceReturnHead?: DslDocument["sequenceReturnHead"];
201
+ sequenceKindLabels?: DslDocument["sequenceKindLabels"];
202
+ sequenceRowGap?: DslDocument["sequenceRowGap"];
203
+ sequenceGutter?: DslDocument["sequenceGutter"];
204
+ sequenceBandWidth?: DslDocument["sequenceBandWidth"];
205
+ sequenceSelfLoop?: DslDocument["sequenceSelfLoop"];
206
+ figureCard?: DslDocument["figureCard"];
207
+ figureSize?: DslDocument["figureSize"];
208
+ terminalDecoration?: DslDocument["terminalDecoration"];
209
+ cornerForm?: DslDocument["cornerForm"];
210
+ figureKindLabel?: DslDocument["figureKindLabel"];
211
+ figureInset?: DslDocument["figureInset"];
212
+ chartBarSize?: DslDocument["chartBarSize"];
213
+ chartBarShape?: DslDocument["chartBarShape"];
214
+ chartLineSize?: DslDocument["chartLineSize"];
215
+ chartPieSize?: DslDocument["chartPieSize"];
216
+ chartWaffleSize?: DslDocument["chartWaffleSize"];
217
+ chartSlopeLabelTemplate?: DslDocument["chartSlopeLabelTemplate"];
218
+ chartSlopeSize?: DslDocument["chartSlopeSize"];
219
+ chartStackedLayout?: DslDocument["chartStackedLayout"];
220
+ chartGaugeSize?: DslDocument["chartGaugeSize"];
221
+ ganttSize?: DslDocument["ganttSize"];
222
+ quadrantLabelPosition?: DslDocument["quadrantLabelPosition"];
223
+ quadrantSize?: DslDocument["quadrantSize"];
94
224
  /**
95
225
  * 図表の箱の上に出す小見出し (optional)。 記法の最上位 `eyebrow:` と同じ (#1247)。
96
226
  *
97
- * 効くのは図全体を 1 箱にする図種 (`pie` / `bar` / `line` / `funnel` / `tree` / `journey` /
227
+ * 効くのは図全体を 1 箱にする図種 (`chart` / `funnel` / `tree` / `journey` /
98
228
  * `quadrant` / `mind` / `gantt`) だけ。 箱ごとに分かれる図種では相手が決まらないため、
99
229
  * 組み立て側が知らせを出す。 そちらは `actors[].eyebrow` に書く。
100
230
  */
@@ -108,6 +238,19 @@ export interface DragonJson {
108
238
  * 他の図種には軸が無いため、書かれていたら組み立て側が知らせる。
109
239
  */
110
240
  axes?: JsonAxes;
241
+ /**
242
+ * 2 軸で仕分ける図の区画の名前 (#2667)。 記法の `regions:` と同じ。
243
+ *
244
+ * 鍵は項目の欄と同じ 4 語 (`左上` / `右上` / `左下` / `右下`)。 別の言い方を足すと
245
+ * 同じ区画を 2 通りで呼ぶことになる。
246
+ */
247
+ regions?: JsonRegions;
248
+ /** 図の凡例の字の大きさ。 */
249
+ legendFontSize?: number;
250
+ legendOffset?: DslDocument["legendOffset"];
251
+ /** 段の箱で全意匠に共通して使う見出しの寸法。 */
252
+ stageHeader?: DslStageHeader;
253
+ stageLayout?: DslDocument["stageLayout"];
111
254
  /** 登場人物 (必須): 文字列 or { name, kind, ... } object */
112
255
  actors: (string | JsonActor)[];
113
256
  /** flow step 配列 (必須): { from, to, label, ... } */
@@ -184,6 +327,8 @@ export interface DragonJson {
184
327
  x?: number;
185
328
  width?: number;
186
329
  label?: string;
330
+ /** 路線図の担当の名札で、名前の下へ出す補足。 */
331
+ subtitle?: string;
187
332
  contain?: boolean;
188
333
  lifeline?: boolean;
189
334
  /**
@@ -209,16 +354,16 @@ export interface DragonJson {
209
354
  relations?: RelationFocus;
210
355
  /** 図の並ぶ向き (#1494)。 記法の最上位 `direction:` と同じ。 JSON は英語の語で書く */
211
356
  direction?: "vertical" | "horizontal";
212
- /**
213
- * 図の配色 (#1553)。 記法の最上位 `palette:` と同じ。
214
- *
215
- * 名前だけを図に載せる = cdl は色を持たず、値は `cdl-theme.css` が決める。
216
- * ER 図とクラス図は書かなくても `kinari` (生成りに茶) になる。 どちらも箱の作りが同じ
217
- * (行頭の印 + 左に名前 + 右に型) で、名前と型が離れて並ぶため、行を横に追う目印
218
- * (行の縞) が要る。 縞の色は配色からしか来ないので、既定が無いと縞が箱の面と同じ色に
219
- * 落ちて 1 本も出ない。 書き手が `palette:` を書いた時はそちらが勝つ。
220
- */
221
- palette?: DslPalette;
357
+ /** 箱を並べ替える軸 (#2655)。 記法の最上位 `order:` と同じ。 JSON は英語の語で書く */
358
+ order?: "kind";
359
+ /** 図種ごとに選ぶ形 (#2657 / #2797)。 記法の最上位 `shape:` と同じ */
360
+ shape?: DslDiagramShape;
361
+ /** 図の意匠 (#1553 / #2790)。 記法の最上位 `theme:` と同じ。 */
362
+ theme?: string;
363
+ /** `theme` の別名。 両方書いた時は `theme` を使う。 */
364
+ palette?: string;
365
+ /** 図の下へ置く凡例。 印は記法と同じ英語名または和名で書く。 */
366
+ legend?: CdlLegendItem[];
222
367
  }
223
368
 
224
369
  /**
@@ -276,6 +421,22 @@ export interface JsonActor {
276
421
  */
277
422
  kind?: NodeKind | (string & {});
278
423
  subtitle?: string;
424
+ kindForm?: "icon";
425
+ icon?: DslActor["icon"];
426
+ emptyRowsNote?: string;
427
+ titleAlign?: "center";
428
+ rowRules?: boolean;
429
+ rowStripe?: boolean;
430
+ rowLayout?: DslActor["rowLayout"];
431
+ rowDividerForm?: DslActor["rowDividerForm"];
432
+ rowWell?: DslActor["rowWell"];
433
+ rowMarkForm?: "tone";
434
+ nestIn?: string;
435
+ figureCard?: DslActor["figureCard"];
436
+ stationNamePosition?: "bottom";
437
+ subtitlePlacement?: "right";
438
+ titleFontSize?: number;
439
+ markGap?: number;
279
440
  eyebrow?: string;
280
441
  value?: string;
281
442
  /**
@@ -286,6 +447,8 @@ export interface JsonActor {
286
447
  */
287
448
  previous?: string;
288
449
  rows?: string[];
450
+ /** 段階ごとの箱で、この箱を入れる段階。 記法の `stage` / `段階` と同じ (#2797)。 */
451
+ stage?: string;
289
452
  lane?: string;
290
453
  stack?: number;
291
454
  initial?: boolean;
@@ -341,14 +504,22 @@ export interface JsonActor {
341
504
  * (記法側と同じ扱い)。
342
505
  */
343
506
  end?: string;
507
+ startLabel?: string;
508
+ milestone?: boolean;
509
+ emphasis?: "primary";
510
+ /** 四象限の座標 `[x, y]`。 */
511
+ at?: [number | string, number | string];
512
+ /** `at` の和名。両方あれば `at` を採る。 */
513
+ 点の位置?: [number | string, number | string];
344
514
  /**
345
- * 体験の道筋 (`type: journey`) で、その段階が起きる場所 (#1294)。 記法の `touchpoint:` と同じ。
515
+ * ユーザージャーニー (`type: journey`) で、その段階が起きる場所 (#1294)。 記法の `touchpoint:` と同じ。
346
516
  */
347
517
  touchpoint?: string;
348
518
  /**
349
- * 体験の道筋 (`type: journey`) で、その段階の改善の余地 (#1294)。 記法の `opportunity:` と同じ。
519
+ * ユーザージャーニー (`type: journey`) で、その段階の改善の余地 (#1294)。 記法の `opportunity:` と同じ。
350
520
  */
351
521
  opportunity?: string;
522
+ opportunityPosition?: DslActor["opportunityPosition"];
352
523
  /**
353
524
  * 箱を置く絶対座標と大きさ (#1294)。 記法の `posX:` / `posY:` / `posW:` / `posH:` と同じ。
354
525
  *
@@ -377,13 +548,21 @@ export interface JsonActor {
377
548
  */
378
549
  phase?: boolean;
379
550
  /** 行頭の印 (#1466)。 行と対で読む。 語の意味は図の種類が決める */
380
- marks?: string[];
551
+ marks?: Array<string | { mark: string; tone?: "primary" | "muted" }>;
381
552
  }
382
553
 
383
554
  /** 2 軸で仕分ける図の軸の名前 (#1294)。 記法の `axes:` と同じ形 */
384
555
  export interface JsonAxes {
385
- x?: { left?: string; right?: string };
386
- y?: { bottom?: string; top?: string };
556
+ x?: { left?: string; right?: string; label?: never; direction?: never } | { label: string; direction: true };
557
+ y?: { bottom?: string; top?: string; label?: never; direction?: never } | { label: string; direction: true };
558
+ }
559
+
560
+ /** 2 軸で仕分ける図の区画の名前 (#2667)。 記法の `regions:` と同じ形 */
561
+ export interface JsonRegions {
562
+ 左上?: string;
563
+ 右上?: string;
564
+ 左下?: string;
565
+ 右下?: string;
387
566
  }
388
567
 
389
568
  export interface JsonStep {
@@ -393,6 +572,8 @@ export interface JsonStep {
393
572
  sub?: string;
394
573
  /** 矢印がどの辺から出るか (#1385)。 記法の `side:` と同じ */
395
574
  side?: "top" | "right" | "bottom" | "left";
575
+ fromSide?: "top" | "right" | "bottom" | "left";
576
+ toSide?: "top" | "right" | "bottom" | "left";
396
577
  /**
397
578
  * 矢印の先の形 (#1462)。 記法の `{ head: triangle }` と同じ。
398
579
  *
@@ -404,7 +585,7 @@ export interface JsonStep {
404
585
  tailHead?: EdgeHead;
405
586
  headFill?: EdgeHeadFill;
406
587
  tailHeadFill?: EdgeHeadFill;
407
- /** クラス図の出どころ側の多重度 (#1771)。 記法の `{ tailSub: "1" }` と同じ */
588
+ /** UML 関係の出どころ側の多重度 (#1771 / #2783)。記法の `{ tailSub: "1" }` と同じ */
408
589
  tailSub?: string;
409
590
  /**
410
591
  * 辺の役目 (cdl#618)。 記法の `{ role: main }` と同じ。
@@ -414,7 +595,7 @@ export interface JsonStep {
414
595
  role?: "main";
415
596
  /** 名前の下地を敷くか (cdl#618)。 記法の `{ labelPlate: false }` と同じ。 既定は敷く */
416
597
  labelPlate?: boolean;
417
- /** クラス図の関係の語 (#1466)。 書くと端の形 / 塗り / 線種がまとめて決まる */
598
+ /** record の UML 関係の語 (#1466 / #2783)。書くと端の形 / 塗り / 線種がまとめて決まる */
418
599
  relation?: ClassRelationType;
419
600
  /** 順序図の言づての種類 (#1466)。 `call` / `return` / `fire` */
420
601
  kind?: SequenceMessageKind;
@@ -436,6 +617,13 @@ export interface JsonStep {
436
617
  dashOffsetBind?: string;
437
618
  labelOffsetX?: number;
438
619
  labelOffsetY?: number;
620
+ posX1?: number;
621
+ posY1?: number;
622
+ posX2?: number;
623
+ posY2?: number;
624
+ waypoints?: DslStep["waypoints"];
625
+ labelCollision?: DslStep["labelCollision"];
626
+ labelSize?: DslStep["labelSize"];
439
627
  /** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
440
628
  overlay?: boolean;
441
629
  /**
@@ -459,6 +647,13 @@ export interface JsonPhase {
459
647
  focus?: string[];
460
648
  /** body 説明文 */
461
649
  body?: string;
650
+ /**
651
+ * 説明文の別名 (#2621)。 記法が `description:` を受けるので JSON でも受ける。
652
+ *
653
+ * **正の語は `body`**。 両方書いた段は `body` を採る = 正の語を優先すると決めておくと、
654
+ * 読み手が「どちらが効くのか」 を実装を読まずに判断できる。
655
+ */
656
+ description?: string;
462
657
  /** badge label */
463
658
  badge?: string;
464
659
  /**
@@ -523,6 +718,13 @@ const VALID_KIND_SET: ReadonlySet<string> = NODE_KIND_VALID;
523
718
  */
524
719
  const VALID_PRESETS: readonly PresetType[] = [...PRESET_TYPES];
525
720
 
721
+ /**
722
+ * 受け付ける図種の別名 (#2655)。 **記法側と同じ表を使う** (`v05/parser.ts` の `TYPE_ALIASES`)。
723
+ *
724
+ * 写すと片方だけ古くなる。 図種の一覧を 1 か所に寄せたのと同じ理由。
725
+ */
726
+ const VALID_ALIASES: readonly string[] = [...TYPE_ALIASES.keys()];
727
+
526
728
  /**
527
729
  * 受け付ける項目の一覧 (#1295)。 **知らない項目を誤りにする判定と、公開 schema との
528
730
  * 突き合わせが、どちらもここを見る**。
@@ -540,8 +742,70 @@ export const ACCEPTED_KEYS = {
540
742
  root: [
541
743
  "title",
542
744
  "type",
745
+ "form",
746
+ "見せ方",
747
+ "ticks",
748
+ "目盛り",
749
+ "ganttToday",
750
+ "ganttTickLabels",
751
+ "ganttBarEnd",
752
+ "ganttBarThickness",
753
+ "chartSeriesSkipMuted",
754
+ "chartLineSeries",
755
+ "chartPieCenterLabel",
756
+ "chartPieTableColumns",
757
+ "chartPieRingWidth",
758
+ "chartSlopePeriods",
759
+ "chartSlopeEmphasisIds",
760
+ "chartSlopeUnit",
761
+ "chartStackedPeriods",
762
+ "chartStackedRateId",
763
+ "chartStackedLegendPosition",
764
+ "chartGaugeValue",
765
+ "chartWaffleLegendPosition",
766
+ "funnelForm",
767
+ "funnelRate",
768
+ "quadrantPointLabelSide",
769
+ "treeNodeForm",
770
+ "treeSize",
771
+ "treeEdgeTone",
772
+ "treeEdgeHead",
773
+ "mindForm",
774
+ "mindSize",
775
+ "edgeEndGap",
776
+ "journeyForm",
777
+ "journeyLineForm",
778
+ "journeyLabels",
779
+ "journeySize",
780
+ "sequenceActorForm",
781
+ "sequenceLabelForm",
782
+ "sequenceReturnHead",
783
+ "sequenceKindLabels",
784
+ "sequenceRowGap",
785
+ "sequenceGutter",
786
+ "sequenceBandWidth",
787
+ "sequenceSelfLoop",
788
+ "figureCard",
789
+ "figureSize",
790
+ "terminalDecoration",
791
+ "cornerForm",
792
+ "figureKindLabel",
793
+ "figureInset",
794
+ "chartBarSize",
795
+ "chartBarShape",
796
+ "chartLineSize",
797
+ "chartPieSize",
798
+ "chartWaffleSize",
799
+ "chartSlopeLabelTemplate",
800
+ "chartSlopeSize",
801
+ "chartStackedLayout",
802
+ "chartGaugeSize",
803
+ "ganttSize",
804
+ "quadrantLabelPosition",
805
+ "quadrantSize",
543
806
  "eyebrow",
544
807
  "axes",
808
+ "regions",
545
809
  "actors",
546
810
  "flow",
547
811
  "states",
@@ -567,18 +831,45 @@ export const ACCEPTED_KEYS = {
567
831
  "relations",
568
832
  // 図の並ぶ向き (#1494)
569
833
  "direction",
570
- // 図の配色 (#1553)
834
+ // 箱を並べ替える軸 (#2655)
835
+ "order",
836
+ // 数を描く図の形 (#2657)
837
+ "shape",
838
+ // 図の意匠とその別名 (#1553 / #2790)
839
+ "theme",
571
840
  "palette",
841
+ "legend",
842
+ "legendFontSize",
843
+ "legendOffset",
844
+ "stageHeader",
845
+ "stageLayout",
572
846
  ],
573
847
  actor: [
574
848
  "name",
575
849
  "kind",
576
850
  "subtitle",
851
+ "kindForm",
852
+ "icon",
853
+ "emptyRowsNote",
854
+ "titleAlign",
855
+ "rowRules",
856
+ "rowStripe",
857
+ "rowLayout",
858
+ "rowDividerForm",
859
+ "rowWell",
860
+ "rowMarkForm",
861
+ "nestIn",
862
+ "figureCard",
863
+ "stationNamePosition",
864
+ "subtitlePlacement",
865
+ "titleFontSize",
866
+ "markGap",
577
867
  "eyebrow",
578
868
  "value",
579
869
  // 前の時点の値 (#1450)。 内訳の変化を帯で示す図と、値 1 つを大きく示す図が読む
580
870
  "previous",
581
871
  "rows",
872
+ "stage",
582
873
  "lane",
583
874
  "stack",
584
875
  "initial",
@@ -587,8 +878,14 @@ export const ACCEPTED_KEYS = {
587
878
  "color",
588
879
  "owner",
589
880
  "end",
881
+ "startLabel",
882
+ "milestone",
883
+ "emphasis",
884
+ "at",
885
+ "点の位置",
590
886
  "touchpoint",
591
887
  "opportunity",
888
+ "opportunityPosition",
592
889
  "posX",
593
890
  "posY",
594
891
  "posW",
@@ -622,13 +919,15 @@ export const ACCEPTED_KEYS = {
622
919
  "sub",
623
920
  // 矢印がどの辺から出るか (#1385)
624
921
  "side",
922
+ "fromSide",
923
+ "toSide",
625
924
  // 矢印の先の形 (#1462)
626
925
  "head",
627
926
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
628
927
  "tailHead",
629
928
  "headFill",
630
929
  "tailHeadFill",
631
- // クラス図の出どころ側の多重度 (#1771)
930
+ // UML 関係の出どころ側の多重度 (#1771 / #2783)
632
931
  "tailSub",
633
932
  "relation",
634
933
  // 辺の役目と名前の下地 (cdl#618)
@@ -645,19 +944,29 @@ export const ACCEPTED_KEYS = {
645
944
  "dashOffsetBind",
646
945
  "labelOffsetX",
647
946
  "labelOffsetY",
947
+ "posX1",
948
+ "posY1",
949
+ "posX2",
950
+ "posY2",
951
+ "waypoints",
952
+ "labelCollision",
953
+ "labelSize",
648
954
  "overlay",
649
955
  "pos",
650
956
  // 部品の端で繋ぐ要素の id (#1979)
651
957
  "fromPartNode",
652
958
  "toPartNode",
653
959
  ],
654
- phase: ["step", "duration", "focus", "body", "badge", "tween", "set", "draw", "drawRatio"],
655
- viewport: ["width", "height", "scale", "laneWidth", "gap", "laneGap", "nodeGap", "labelMargin"],
656
- lane: ["x", "width", "label", "contain", "lifeline", "pos"],
960
+ // 説明の語は `PHASE_BODY_KEYS` から導く (#2621)。 書き写すと記法の入口とずれる
961
+ phase: ["step", "duration", "focus", ...PHASE_BODY_KEYS, "badge", "tween", "set", "draw", "drawRatio"],
962
+ viewport: ["width", "height", "contentOffsetX", "scale", "laneWidth", "gap", "laneGap", "nodeGap", "labelMargin"],
963
+ lane: ["x", "width", "label", "subtitle", "contain", "lifeline", "pos"],
657
964
  group: ["label", "lanes"],
658
965
  axes: ["x", "y"],
659
- axesX: ["left", "right"],
660
- axesY: ["bottom", "top"],
966
+ axesX: ["left", "right", "label", "direction"],
967
+ axesY: ["bottom", "top", "label", "direction"],
968
+ // 区画を指す語は項目の欄と同じ 4 語 (#2667)
969
+ regions: ["左上", "右上", "左下", "右下"],
661
970
  layoutPos: ["x", "y"],
662
971
  // 位置を他の要素からの相対で書く指定 (#2039)。 間隔は書かなくてよい
663
972
  posRel: ["anchor", "dir", "gap"],
@@ -706,6 +1015,14 @@ export type 欄の型 =
706
1015
  | "必須の向き"
707
1016
  | "色か色番号"
708
1017
  | "描くもの"
1018
+ | "意匠"
1019
+ | "図の形"
1020
+ | "ガントの帯の終わり"
1021
+ | "ガントの帯の厚み"
1022
+ | "札の角"
1023
+ | "四象限の区画名位置"
1024
+ | "線札の衝突"
1025
+ | "主役"
709
1026
  | "必須の図種"
710
1027
  | "object"
711
1028
  | "並び"
@@ -716,8 +1033,70 @@ export const 欄の型表 = {
716
1033
  root: {
717
1034
  title: "必須の非空文字列",
718
1035
  type: "必須の図種",
1036
+ form: "非空の文字列",
1037
+ 見せ方: "非空の文字列",
1038
+ ticks: "文字列の並び",
1039
+ 目盛り: "文字列の並び",
1040
+ ganttToday: "object",
1041
+ ganttTickLabels: "文字列の並び",
1042
+ ganttBarEnd: "ガントの帯の終わり",
1043
+ ganttBarThickness: "ガントの帯の厚み",
1044
+ chartSeriesSkipMuted: "真偽",
1045
+ chartLineSeries: "並び",
1046
+ chartPieCenterLabel: "文字列",
1047
+ chartPieTableColumns: "文字列",
1048
+ chartPieRingWidth: "文字列",
1049
+ chartSlopePeriods: "文字列の並び",
1050
+ chartSlopeEmphasisIds: "文字列の並び",
1051
+ chartSlopeUnit: "文字列",
1052
+ chartStackedPeriods: "並び",
1053
+ chartStackedRateId: "文字列",
1054
+ chartStackedLegendPosition: "文字列",
1055
+ chartGaugeValue: "object",
1056
+ chartWaffleLegendPosition: "文字列",
1057
+ funnelForm: "文字列",
1058
+ funnelRate: "文字列",
1059
+ quadrantPointLabelSide: "文字列",
1060
+ treeNodeForm: "文字列",
1061
+ treeSize: "object",
1062
+ treeEdgeTone: "文字列",
1063
+ treeEdgeHead: "文字列",
1064
+ mindForm: "文字列",
1065
+ mindSize: "object",
1066
+ edgeEndGap: "数",
1067
+ journeyForm: "文字列",
1068
+ journeyLineForm: "文字列",
1069
+ journeyLabels: "object",
1070
+ journeySize: "object",
1071
+ sequenceActorForm: "非空の文字列",
1072
+ sequenceLabelForm: "非空の文字列",
1073
+ sequenceReturnHead: "非空の文字列",
1074
+ sequenceKindLabels: "object",
1075
+ sequenceRowGap: "数",
1076
+ sequenceGutter: "数",
1077
+ sequenceBandWidth: "数",
1078
+ sequenceSelfLoop: "object",
1079
+ figureCard: "object",
1080
+ figureSize: "object",
1081
+ terminalDecoration: "object",
1082
+ cornerForm: "札の角",
1083
+ figureKindLabel: "object",
1084
+ figureInset: "object",
1085
+ chartBarSize: "object",
1086
+ chartBarShape: "非空の文字列",
1087
+ chartLineSize: "object",
1088
+ chartPieSize: "object",
1089
+ chartWaffleSize: "object",
1090
+ chartSlopeLabelTemplate: "object",
1091
+ chartSlopeSize: "object",
1092
+ chartStackedLayout: "object",
1093
+ chartGaugeSize: "object",
1094
+ ganttSize: "object",
1095
+ quadrantLabelPosition: "四象限の区画名位置",
1096
+ quadrantSize: "object",
719
1097
  eyebrow: "文字列",
720
1098
  axes: "object",
1099
+ regions: "object",
721
1100
  actors: "必須の非空の並び",
722
1101
  flow: "必須の並び",
723
1102
  states: "object",
@@ -743,17 +1122,44 @@ export const 欄の型表 = {
743
1122
  relations: "非空の文字列",
744
1123
  // 図の並ぶ向き (#1494)
745
1124
  direction: "非空の文字列",
746
- // 図の配色 (#1553)
747
- palette: "非空の文字列",
1125
+ // 箱を並べ替える軸 (#2655)
1126
+ order: "非空の文字列",
1127
+ // 図種ごとの形 (#2657 / #2797)
1128
+ shape: "図の形",
1129
+ // 図の意匠とその別名 (#1553 / #2790)
1130
+ theme: "意匠",
1131
+ palette: "意匠",
1132
+ legend: "並び",
1133
+ legendFontSize: "数",
1134
+ legendOffset: "object",
1135
+ stageHeader: "object",
1136
+ stageLayout: "object",
748
1137
  },
749
1138
  actor: {
750
1139
  name: "必須の非空文字列",
751
1140
  kind: "非空の文字列",
752
1141
  subtitle: "文字列",
1142
+ kindForm: "非空の文字列",
1143
+ icon: "非空の文字列",
1144
+ emptyRowsNote: "文字列",
1145
+ titleAlign: "非空の文字列",
1146
+ rowRules: "真偽",
1147
+ rowStripe: "真偽",
1148
+ rowLayout: "object",
1149
+ rowDividerForm: "非空の文字列",
1150
+ rowWell: "object",
1151
+ rowMarkForm: "非空の文字列",
1152
+ nestIn: "非空の文字列",
1153
+ figureCard: "object",
1154
+ stationNamePosition: "非空の文字列",
1155
+ subtitlePlacement: "非空の文字列",
1156
+ titleFontSize: "数",
1157
+ markGap: "数",
753
1158
  eyebrow: "文字列",
754
1159
  value: "文字列",
755
1160
  previous: "文字列",
756
1161
  rows: "文字列の並び",
1162
+ stage: "非空の文字列",
757
1163
  lane: "文字列",
758
1164
  stack: "数",
759
1165
  initial: "真偽",
@@ -762,8 +1168,14 @@ export const 欄の型表 = {
762
1168
  color: "色か色番号",
763
1169
  owner: "文字列",
764
1170
  end: "文字列",
1171
+ startLabel: "文字列",
1172
+ milestone: "真偽",
1173
+ emphasis: "主役",
1174
+ at: "並び",
1175
+ 点の位置: "並び",
765
1176
  touchpoint: "文字列",
766
1177
  opportunity: "文字列",
1178
+ opportunityPosition: "文字列",
767
1179
  posX: "数",
768
1180
  posY: "数",
769
1181
  posW: "数",
@@ -784,7 +1196,7 @@ export const 欄の型表 = {
784
1196
  renderOffsetX: "数か文字列",
785
1197
  renderOffsetY: "数か文字列",
786
1198
  // 行頭の印 (#1466)
787
- marks: "文字列の並び",
1199
+ marks: "並び",
788
1200
  // 相対で置く指定 (#2039)。 中身の形は `validateRelativePos` が見る
789
1201
  posRel: "object",
790
1202
  // 見本が持つ段を使うか (#2125)
@@ -797,12 +1209,14 @@ export const 欄の型表 = {
797
1209
  sub: "文字列",
798
1210
  // 矢印がどの辺から出るか (#1385)
799
1211
  side: "辺",
1212
+ fromSide: "辺",
1213
+ toSide: "辺",
800
1214
  head: "端の形",
801
1215
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
802
1216
  tailHead: "端の形",
803
1217
  headFill: "非空の文字列",
804
1218
  tailHeadFill: "非空の文字列",
805
- // クラス図の出どころ側の多重度 (#1771)
1219
+ // UML 関係の出どころ側の多重度 (#1771 / #2783)
806
1220
  tailSub: "文字列",
807
1221
  relation: "非空の文字列",
808
1222
  // 辺の役目と名前の下地 (cdl#618)
@@ -819,6 +1233,13 @@ export const 欄の型表 = {
819
1233
  dashOffsetBind: "非空の文字列",
820
1234
  labelOffsetX: "数",
821
1235
  labelOffsetY: "数",
1236
+ posX1: "数",
1237
+ posY1: "数",
1238
+ posX2: "数",
1239
+ posY2: "数",
1240
+ waypoints: "並び",
1241
+ labelCollision: "線札の衝突",
1242
+ labelSize: "object",
822
1243
  overlay: "真偽",
823
1244
  pos: "object",
824
1245
  // 部品の端で繋ぐ要素の id (#1979)。 空の id は要素を指せないので受けない
@@ -829,7 +1250,9 @@ export const 欄の型表 = {
829
1250
  step: "必須の非空文字列",
830
1251
  duration: "数",
831
1252
  focus: "文字列の並び",
1253
+ // 説明は正の語と別名のどちらでも同じ型。 表の欄は `ACCEPTED_KEYS.phase` と揃える (#2621)
832
1254
  body: "文字列",
1255
+ description: "文字列",
833
1256
  badge: "文字列",
834
1257
  tween: "object",
835
1258
  set: "object",
@@ -839,6 +1262,7 @@ export const 欄の型表 = {
839
1262
  viewport: {
840
1263
  width: "数",
841
1264
  height: "数",
1265
+ contentOffsetX: "数",
842
1266
  scale: "数",
843
1267
  laneWidth: "数",
844
1268
  gap: "数",
@@ -850,14 +1274,16 @@ export const 欄の型表 = {
850
1274
  x: "数",
851
1275
  width: "数",
852
1276
  label: "文字列",
1277
+ subtitle: "文字列",
853
1278
  contain: "真偽",
854
1279
  lifeline: "真偽",
855
1280
  pos: "object",
856
1281
  },
857
1282
  group: { label: "文字列", lanes: "文字列の並び" },
858
1283
  axes: { x: "object", y: "object" },
859
- axesX: { left: "文字列", right: "文字列" },
860
- axesY: { bottom: "文字列", top: "文字列" },
1284
+ axesX: { left: "文字列", right: "文字列", label: "文字列", direction: "真偽" },
1285
+ axesY: { bottom: "文字列", top: "文字列", label: "文字列", direction: "真偽" },
1286
+ regions: { 左上: "文字列", 右上: "文字列", 左下: "文字列", 右下: "文字列" },
861
1287
  // 位置は書けば x と y の両方が要る。 片方だけでは寄せ幅が決まらない
862
1288
  layoutPos: { x: "必須の数", y: "必須の数" },
863
1289
  // 相対で置く指定 (#2039)。 相手と向きは必ず要り、間隔は書かなければ既定で置く
@@ -929,10 +1355,28 @@ export const 見本にしか効かない欄 = [
929
1355
  */
930
1356
  export const 見本に効かない欄 = [
931
1357
  "tone",
1358
+ "kindForm",
1359
+ "icon",
1360
+ "emptyRowsNote",
1361
+ "titleAlign",
1362
+ "rowRules",
1363
+ "rowStripe",
1364
+ "rowLayout",
1365
+ "rowDividerForm",
1366
+ "rowWell",
1367
+ "rowMarkForm",
1368
+ "nestIn",
1369
+ "figureCard",
1370
+ "at",
932
1371
  "owner",
933
1372
  "end",
1373
+ "startLabel",
1374
+ "milestone",
1375
+ "emphasis",
934
1376
  "touchpoint",
935
1377
  "opportunity",
1378
+ "opportunityPosition",
1379
+ "stage",
936
1380
  // 箱の中に描く図形 (#1374)。 見本は自分の形を持つため、外から図形を差し替えられない
937
1381
  "shape",
938
1382
  // その箱を出すかどうかの条件 (#1381)。 見本は自分の出方を持つ
@@ -1031,6 +1475,36 @@ function 値を検査(
1031
1475
  if (v === undefined) return;
1032
1476
  if (typeof v !== "boolean") 型違い("true or false if present");
1033
1477
  return;
1478
+ case "ガントの帯の終わり":
1479
+ if (v === undefined) return;
1480
+ if (v !== "position") 型違い('"position" if present');
1481
+ return;
1482
+ case "ガントの帯の厚み":
1483
+ if (v === undefined) return;
1484
+ if (v !== "thin") 型違い('"thin" if present');
1485
+ return;
1486
+ case "札の角":
1487
+ if (v === undefined) return;
1488
+ if (!(CORNER_FORM_VALUES as readonly unknown[]).includes(v)) {
1489
+ 型違い(`one of: ${CORNER_FORM_VALUES.join(", ")}`);
1490
+ }
1491
+ return;
1492
+ case "四象限の区画名位置":
1493
+ if (v === undefined) return;
1494
+ if (!(QUADRANT_LABEL_POSITION_VALUES as readonly unknown[]).includes(v)) {
1495
+ 型違い(`one of: ${QUADRANT_LABEL_POSITION_VALUES.join(", ")}`);
1496
+ }
1497
+ return;
1498
+ case "線札の衝突":
1499
+ if (v === undefined) return;
1500
+ if (!(LABEL_COLLISION_VALUES as readonly unknown[]).includes(v)) {
1501
+ 型違い(`one of: ${LABEL_COLLISION_VALUES.join(", ")}`);
1502
+ }
1503
+ return;
1504
+ case "主役":
1505
+ if (v === undefined) return;
1506
+ if (v !== "primary") 型違い('"primary" if present');
1507
+ return;
1034
1508
  case "色":
1035
1509
  if (v === undefined) return;
1036
1510
  if (typeof v !== "string") {
@@ -1100,6 +1574,28 @@ function 値を検査(
1100
1574
  });
1101
1575
  }
1102
1576
  return;
1577
+ case "意匠":
1578
+ if (v === undefined) return;
1579
+ if (typeof v !== "string" || resolveTheme(v) === null) {
1580
+ errors.push({
1581
+ path,
1582
+ message: `${名前} must be one of: ${THEMES.join(", ")}`,
1583
+ hint: `使える語 = ${Object.keys(THEME_ALIAS).join(" / ")} (got ${
1584
+ typeof v === "string" ? JSON.stringify(v) : typeof v
1585
+ })`,
1586
+ });
1587
+ }
1588
+ return;
1589
+ case "図の形":
1590
+ if (v === undefined) return;
1591
+ if (typeof v !== "string" || resolveDiagramShape(v) === null) {
1592
+ errors.push({
1593
+ path,
1594
+ message: `${名前} must be one of: ${Object.values(DIAGRAM_SHAPES).flat().join(", ")}`,
1595
+ hint: `使える語 = ${Object.values(DIAGRAM_SHAPES).flat().join(" / ")}`,
1596
+ });
1597
+ }
1598
+ return;
1103
1599
  case "色か色番号":
1104
1600
  if (v === undefined) return;
1105
1601
  if (typeof v !== "string") {
@@ -1126,10 +1622,13 @@ function 値を検査(
1126
1622
  }
1127
1623
  return;
1128
1624
  case "必須の図種":
1129
- if (typeof v !== "string" || !VALID_PRESETS.includes(v as PresetType)) {
1625
+ if (
1626
+ typeof v !== "string" ||
1627
+ !(VALID_PRESETS.includes(v as PresetType) || VALID_ALIASES.includes(v))
1628
+ ) {
1130
1629
  errors.push({
1131
1630
  path,
1132
- message: `${名前} must be one of: ${VALID_PRESETS.join(", ")}`,
1631
+ message: `${名前} must be one of: ${[...VALID_PRESETS, ...VALID_ALIASES].join(", ")}`,
1133
1632
  hint: typeof v === "string" ? `got "${v}"` : undefined,
1134
1633
  });
1135
1634
  }
@@ -1519,6 +2018,91 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
1519
2018
  });
1520
2019
  }
1521
2020
 
2021
+ const LEGEND_KEYS = ["mark", "text", "tone", "lineStyle", "align", "lead"] as const;
2022
+ const LEGEND_LINE_MARKS = ["line-triangle-hollow", "line-diamond-hollow", "line-diamond-solid"] as const;
2023
+
2024
+ /** JSON の凡例を、外側の並びから各項目の必須値まで検査する。 */
2025
+ function validateLegend(v: unknown, errors: JsonDslError[]): void {
2026
+ if (v === undefined) return;
2027
+ if (!Array.isArray(v)) {
2028
+ errors.push({ path: "$.legend", message: "legend must be an array" });
2029
+ return;
2030
+ }
2031
+ v.forEach((item, index) => {
2032
+ const path = `$.legend[${index}]`;
2033
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
2034
+ errors.push({ path, message: "legend item must be an object" });
2035
+ return;
2036
+ }
2037
+ const o = item as Record<string, unknown>;
2038
+ for (const key of Object.keys(o)) {
2039
+ if ((LEGEND_KEYS as readonly string[]).includes(key)) continue;
2040
+ errors.push({
2041
+ path: `${path}.${key}`,
2042
+ message: `unknown key "${key}"`,
2043
+ hint: `使える項目 = ${LEGEND_KEYS.join(", ")}`,
2044
+ });
2045
+ }
2046
+ const mark = typeof o.mark === "string" ? resolveLegendMark(o.mark) : undefined;
2047
+ if (o.mark !== undefined && (typeof o.mark !== "string" || mark === null)) {
2048
+ errors.push({ path: `${path}.mark`, message: "legend mark is not recognized" });
2049
+ }
2050
+ if (typeof o.text !== "string" || o.text.trim().length === 0) {
2051
+ errors.push({ path: `${path}.text`, message: "legend text must be a non-empty string" });
2052
+ }
2053
+ if (o.mark === undefined) {
2054
+ if (o.tone !== undefined || o.lineStyle !== undefined) errors.push({ path, message: "legend note cannot have tone or lineStyle" });
2055
+ if (o.align !== undefined && !(LEGEND_NOTE_ALIGN_VALUES as readonly unknown[]).includes(o.align)) errors.push({ path: `${path}.align`, message: `align must be ${LEGEND_NOTE_ALIGN_VALUES.join(" or ")}` });
2056
+ if (o.lead !== undefined && typeof o.lead !== "string") errors.push({ path: `${path}.lead`, message: "lead must be a string" });
2057
+ } else {
2058
+ if (o.align !== undefined || o.lead !== undefined) errors.push({ path, message: "marked legend item cannot have align or lead" });
2059
+ if (o.tone !== undefined && (typeof o.tone !== "string" || resolveTone(o.tone) === undefined)) errors.push({ path: `${path}.tone`, message: "tone is not recognized" });
2060
+ if (o.lineStyle !== undefined) {
2061
+ if (!(LEGEND_LINE_STYLE_VALUES as readonly unknown[]).includes(o.lineStyle)) errors.push({ path: `${path}.lineStyle`, message: `lineStyle must be ${LEGEND_LINE_STYLE_VALUES.join(" or ")}` });
2062
+ if (mark === undefined || mark === null || !(LEGEND_LINE_MARKS as readonly string[]).includes(mark)) errors.push({ path: `${path}.lineStyle`, message: "lineStyle requires a line-end legend mark" });
2063
+ }
2064
+ }
2065
+ });
2066
+ }
2067
+
2068
+ const STAGE_HEADER_KEYS = ["leftPad", "topPad", "numberSize", "gap", "nameSize", "bottomPad"] as const;
2069
+ const REQUIRED_STAGE_HEADER_KEYS = ["topPad", "numberSize", "gap", "nameSize", "bottomPad"] as const;
2070
+
2071
+ /** JSON の段の見出し寸法を、閉じた欄・必須の正数まで検査する。 */
2072
+ function validateStageHeader(v: unknown, errors: JsonDslError[]): void {
2073
+ if (v === undefined) return;
2074
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
2075
+ errors.push({ path: "$.stageHeader", message: "stageHeader must be an object" });
2076
+ return;
2077
+ }
2078
+ const path = "$.stageHeader";
2079
+ const o = v as Record<string, unknown>;
2080
+ for (const key of Object.keys(o)) {
2081
+ if ((STAGE_HEADER_KEYS as readonly string[]).includes(key)) continue;
2082
+ errors.push({
2083
+ path: `${path}.${key}`,
2084
+ message: `unknown key "${key}"`,
2085
+ hint: `使える項目 = ${STAGE_HEADER_KEYS.join(", ")}`,
2086
+ });
2087
+ }
2088
+ for (const key of REQUIRED_STAGE_HEADER_KEYS) {
2089
+ if (o[key] === undefined) {
2090
+ errors.push({ path: `${path}.${key}`, message: `${key} is required` });
2091
+ }
2092
+ }
2093
+ for (const key of STAGE_HEADER_KEYS) {
2094
+ const value = o[key];
2095
+ if (value === undefined) continue;
2096
+ const zeroAllowed = key === "leftPad" || key === "topPad" || key === "gap" || key === "bottomPad";
2097
+ if (typeof value !== "number" || !Number.isFinite(value) || (zeroAllowed ? value < 0 : value <= 0)) {
2098
+ errors.push({
2099
+ path: `${path}.${key}`,
2100
+ message: `${key} must be ${zeroAllowed ? "a non-negative" : "a positive"} number`,
2101
+ });
2102
+ }
2103
+ }
2104
+ }
2105
+
1522
2106
  /**
1523
2107
  * 値を見せる部品の並びを検査する (#1374)。
1524
2108
  *
@@ -2019,7 +2603,448 @@ function validateAxes(v: unknown, errors: JsonDslError[]): void {
2019
2603
  const o = 一方 as Record<string, unknown>;
2020
2604
  checkUnknownKeys(o, 層の名[名], `$.axes.${名}`, errors);
2021
2605
  表で検査(o, 層の名[名], `$.axes.${名}`, `axes.${名}`, errors);
2606
+ if (o.direction === true) {
2607
+ if (typeof o.label !== "string") {
2608
+ errors.push({ path: `$.axes.${名}.label`, message: `axes.${名}.label must be a string for a directional axis` });
2609
+ }
2610
+ } else if (o.label !== undefined || o.direction !== undefined) {
2611
+ errors.push({ path: `$.axes.${名}.direction`, message: `axes.${名}.direction must be true when label is used` });
2612
+ }
2613
+ }
2614
+ }
2615
+
2616
+ /** #2854 で足した図表の複合欄と閉じた語を検査する。 */
2617
+ function validateChartPresentationFields(j: Record<string, unknown>, errors: JsonDslError[]): void {
2618
+ const enumField = (name: string, allowed: readonly string[]): void => {
2619
+ const value = j[name];
2620
+ if (value === undefined || typeof value !== "string") return;
2621
+ if (!allowed.includes(value)) {
2622
+ errors.push({ path: `$.${name}`, message: `${name} must be one of: ${allowed.join(", ")}` });
2623
+ }
2624
+ };
2625
+ enumField("chartPieTableColumns", CHART_PIE_TABLE_COLUMN_VALUES);
2626
+ enumField("chartPieRingWidth", CHART_PIE_RING_WIDTH_VALUES);
2627
+ enumField("chartStackedLegendPosition", CHART_STACKED_LEGEND_POSITION_VALUES);
2628
+ enumField("chartWaffleLegendPosition", CHART_WAFFLE_LEGEND_POSITION_VALUES);
2629
+ enumField("funnelForm", FUNNEL_FORM_VALUES);
2630
+ enumField("funnelRate", FUNNEL_RATE_VALUES);
2631
+ enumField("quadrantPointLabelSide", QUADRANT_POINT_LABEL_SIDE_VALUES);
2632
+ enumField("treeNodeForm", TREE_NODE_FORM_VALUES);
2633
+ enumField("treeEdgeTone", TREE_EDGE_TONE_VALUES);
2634
+ enumField("treeEdgeHead", TREE_EDGE_HEAD_VALUES);
2635
+ enumField("mindForm", MIND_FORM_VALUES);
2636
+ enumField("journeyForm", JOURNEY_FORM_VALUES);
2637
+ enumField("journeyLineForm", JOURNEY_LINE_FORM_VALUES);
2638
+ enumField("sequenceActorForm", SEQUENCE_ACTOR_FORM_VALUES);
2639
+ enumField("sequenceLabelForm", SEQUENCE_LABEL_FORM_VALUES);
2640
+ enumField("sequenceReturnHead", SEQUENCE_RETURN_HEAD_VALUES);
2641
+ enumField("cornerForm", CORNER_FORM_VALUES);
2642
+ enumField("chartBarShape", CHART_BAR_SHAPE_VALUES);
2643
+ enumField("quadrantLabelPosition", QUADRANT_LABEL_POSITION_VALUES);
2644
+
2645
+ const closedObject = (
2646
+ name: "journeyLabels" | "figureCard" | "figureSize" | "treeSize" | "mindSize" |
2647
+ "terminalDecoration" | "figureKindLabel" | "figureInset" | "journeySize" | "chartBarSize" |
2648
+ "chartLineSize" | "chartPieSize" | "chartWaffleSize" | "chartSlopeLabelTemplate" | "chartSlopeSize" |
2649
+ "chartStackedLayout" | "chartGaugeSize" | "ganttSize" | "quadrantSize" | "legendOffset" | "stageLayout",
2650
+ keys: readonly string[],
2651
+ ): Record<string, unknown> | undefined => {
2652
+ const value = j[name];
2653
+ if (value === undefined) return undefined;
2654
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2655
+ errors.push({ path: `$.${name}`, message: `${name} must be an object` });
2656
+ return undefined;
2657
+ }
2658
+ const object = value as Record<string, unknown>;
2659
+ for (const key of Object.keys(object)) {
2660
+ if (!keys.includes(key)) errors.push({ path: `$.${name}.${key}`, message: `unknown key "${key}"` });
2661
+ }
2662
+ return object;
2663
+ };
2664
+ const labels = closedObject("journeyLabels", JOURNEY_LABEL_KEYS);
2665
+ if (labels !== undefined) {
2666
+ for (const key of JOURNEY_LABEL_KEYS) {
2667
+ if (typeof labels[key] !== "string" || labels[key].trim() === "") {
2668
+ errors.push({ path: `$.journeyLabels.${key}`, message: `${key} must be a non-empty string` });
2669
+ }
2670
+ }
2671
+ }
2672
+ if (j.sequenceKindLabels !== undefined) {
2673
+ if (!j.sequenceKindLabels || typeof j.sequenceKindLabels !== "object" || Array.isArray(j.sequenceKindLabels)) {
2674
+ errors.push({ path: "$.sequenceKindLabels", message: "sequenceKindLabels must be an object" });
2675
+ } else {
2676
+ const labels = j.sequenceKindLabels as Record<string, unknown>;
2677
+ for (const [key, value] of Object.entries(labels)) {
2678
+ if (!(SEQUENCE_KIND_LABEL_KEYS as readonly string[]).includes(key)) errors.push({ path: `$.sequenceKindLabels.${key}`, message: `unknown key "${key}"` });
2679
+ else if (typeof value !== "string") errors.push({ path: `$.sequenceKindLabels.${key}`, message: `${key} must be a string` });
2680
+ }
2681
+ }
2682
+ }
2683
+ if (
2684
+ j.sequenceRowGap !== undefined &&
2685
+ (typeof j.sequenceRowGap !== "number" || !Number.isFinite(j.sequenceRowGap) || j.sequenceRowGap <= 0)
2686
+ ) {
2687
+ errors.push({ path: "$.sequenceRowGap", message: "sequenceRowGap must be a positive finite number" });
2688
+ }
2689
+ for (const key of ["sequenceGutter", "sequenceBandWidth"] as const) {
2690
+ if (j[key] !== undefined && (typeof j[key] !== "number" || !Number.isFinite(j[key]) || j[key] <= 0)) {
2691
+ errors.push({ path: `$.${key}`, message: `${key} must be a positive finite number` });
2692
+ }
2693
+ }
2694
+ if (j.sequenceSelfLoop !== undefined) {
2695
+ if (!j.sequenceSelfLoop || typeof j.sequenceSelfLoop !== "object" || Array.isArray(j.sequenceSelfLoop)) {
2696
+ errors.push({ path: "$.sequenceSelfLoop", message: "sequenceSelfLoop must be an object" });
2697
+ } else {
2698
+ const loop = j.sequenceSelfLoop as Record<string, unknown>;
2699
+ for (const key of Object.keys(loop)) {
2700
+ if (!(SEQUENCE_SELF_LOOP_KEYS as readonly string[]).includes(key)) errors.push({ path: `$.sequenceSelfLoop.${key}`, message: `unknown key "${key}"` });
2701
+ }
2702
+ if (loop.form !== undefined && loop.form !== "orthogonal") errors.push({ path: "$.sequenceSelfLoop.form", message: "form must be orthogonal" });
2703
+ for (const key of ["width", "drop"] as const) {
2704
+ if (loop[key] !== undefined && (typeof loop[key] !== "number" || !Number.isFinite(loop[key]) || loop[key] <= 0)) errors.push({ path: `$.sequenceSelfLoop.${key}`, message: "must be a positive finite number" });
2705
+ }
2706
+ }
2707
+ }
2708
+ const card = closedObject("figureCard", ["label", "note"]);
2709
+ if (card !== undefined) {
2710
+ if (typeof card.label !== "string")
2711
+ errors.push({ path: "$.figureCard.label", message: "label must be a string" });
2712
+ if (card.note !== undefined && typeof card.note !== "string")
2713
+ errors.push({ path: "$.figureCard.note", message: "note must be a string" });
2714
+ }
2715
+ const size = closedObject("figureSize", ["width", "height"]);
2716
+ if (size !== undefined) {
2717
+ for (const key of ["width", "height"] as const) {
2718
+ if (typeof size[key] !== "number" || !Number.isFinite(size[key]) || size[key] <= 0)
2719
+ errors.push({ path: `$.figureSize.${key}`, message: `${key} must be a positive finite number` });
2720
+ }
2721
+ }
2722
+ const positiveNumber = (value: unknown, path: string): boolean => {
2723
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return true;
2724
+ errors.push({ path, message: "must be a positive finite number" });
2725
+ return false;
2726
+ };
2727
+ const nonNegativeNumber = (value: unknown, path: string): boolean => {
2728
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) return true;
2729
+ errors.push({ path, message: "must be a non-negative finite number" });
2730
+ return false;
2731
+ };
2732
+ if (j.edgeEndGap !== undefined) nonNegativeNumber(j.edgeEndGap, "$.edgeEndGap");
2733
+ const treeSize = closedObject("treeSize", [
2734
+ "nodeWidths",
2735
+ "nodeHeight",
2736
+ "titleFontSize",
2737
+ "subtitleFontSize",
2738
+ "siblingGap",
2739
+ "levelGap",
2740
+ ]);
2741
+ if (treeSize !== undefined) {
2742
+ if (treeSize.nodeWidths !== undefined) {
2743
+ if (!Array.isArray(treeSize.nodeWidths) || treeSize.nodeWidths.length === 0) {
2744
+ errors.push({ path: "$.treeSize.nodeWidths", message: "must be a non-empty array" });
2745
+ } else {
2746
+ treeSize.nodeWidths.forEach((value, index) => {
2747
+ positiveNumber(value, `$.treeSize.nodeWidths[${index}]`);
2748
+ });
2749
+ }
2750
+ }
2751
+ for (const key of ["nodeHeight", "titleFontSize", "subtitleFontSize"] as const) {
2752
+ if (treeSize[key] !== undefined) positiveNumber(treeSize[key], `$.treeSize.${key}`);
2753
+ }
2754
+ for (const key of ["siblingGap", "levelGap"] as const) {
2755
+ if (treeSize[key] !== undefined) nonNegativeNumber(treeSize[key], `$.treeSize.${key}`);
2756
+ }
2757
+ }
2758
+ const mindSize = closedObject("mindSize", [
2759
+ "contentOffset",
2760
+ "root",
2761
+ "branch",
2762
+ "leaf",
2763
+ "rootBranchGap",
2764
+ "branchLeafGap",
2765
+ "branchRowGap",
2766
+ "branchRowOffset",
2767
+ "leafRowGap",
2768
+ "leafRowOffset",
2769
+ ]);
2770
+ if (mindSize !== undefined) {
2771
+ if (mindSize.contentOffset !== undefined) {
2772
+ const value = mindSize.contentOffset;
2773
+ const path = "$.mindSize.contentOffset";
2774
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2775
+ errors.push({ path, message: "contentOffset must be an object" });
2776
+ } else {
2777
+ const offset = value as Record<string, unknown>;
2778
+ for (const key of Object.keys(offset)) {
2779
+ if (key !== "x" && key !== "y")
2780
+ errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
2781
+ }
2782
+ for (const key of ["x", "y"] as const) {
2783
+ if (typeof offset[key] !== "number" || !Number.isFinite(offset[key]))
2784
+ errors.push({ path: `${path}.${key}`, message: "must be a finite number" });
2785
+ }
2786
+ }
2787
+ }
2788
+ const nestedSize = (
2789
+ key: "root" | "branch" | "leaf",
2790
+ allowed: readonly string[],
2791
+ ): Record<string, unknown> | undefined => {
2792
+ const value = mindSize[key];
2793
+ if (value === undefined) return undefined;
2794
+ const path = `$.mindSize.${key}`;
2795
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2796
+ errors.push({ path, message: `${key} must be an object` });
2797
+ return undefined;
2798
+ }
2799
+ const object = value as Record<string, unknown>;
2800
+ for (const nestedKey of Object.keys(object)) {
2801
+ if (!allowed.includes(nestedKey))
2802
+ errors.push({ path: `${path}.${nestedKey}`, message: `unknown key "${nestedKey}"` });
2803
+ }
2804
+ return object;
2805
+ };
2806
+ for (const [key, allowed] of [
2807
+ ["root", ["width", "height", "fontSize"]],
2808
+ ["branch", ["width", "height", "fontSize"]],
2809
+ ["leaf", ["fontSize"]],
2810
+ ] as const) {
2811
+ const nested = nestedSize(key, allowed);
2812
+ if (nested === undefined) continue;
2813
+ for (const nestedKey of allowed) {
2814
+ if (nested[nestedKey] !== undefined)
2815
+ positiveNumber(nested[nestedKey], `$.mindSize.${key}.${nestedKey}`);
2816
+ }
2817
+ }
2818
+ for (const key of ["rootBranchGap", "branchLeafGap", "branchRowGap", "leafRowGap"] as const) {
2819
+ if (mindSize[key] !== undefined) nonNegativeNumber(mindSize[key], `$.mindSize.${key}`);
2820
+ }
2821
+ for (const key of ["branchRowOffset", "leafRowOffset"] as const) {
2822
+ const value = mindSize[key];
2823
+ if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value))) {
2824
+ errors.push({ path: `$.mindSize.${key}`, message: "must be a finite number" });
2825
+ }
2826
+ }
2827
+ }
2828
+
2829
+ const specs = {
2830
+ terminalDecoration: ["windowControls", "cursor", "rowOrdinal"],
2831
+ figureKindLabel: ["text", "x", "y", "fontSize", "fontWeight", "letterSpacing"],
2832
+ figureInset: ["top", "right", "bottom", "left", "radius"],
2833
+ journeySize: ["plotPadding", "pointPadding"],
2834
+ chartBarSize: ["tickFontSize", "valueFontSize", "labelFontSize", "plotPadding", "barWidth", "cornerRadius"],
2835
+ chartLineSize: ["valueLabelOffset", "tickFontSize", "periodFontSize", "pointRadius", "plotPadding", "tickGap", "pointPadding", "periodY"],
2836
+ chartPieSize: ["tableWidth", "tableLabelFontSize", "tableValueFontSize", "tableRowGap", "tablePaddingInline", "outerRadius", "centerX"],
2837
+ chartWaffleSize: ["gridLeft", "cellPitch"],
2838
+ chartSlopeLabelTemplate: ["left", "right"],
2839
+ chartSlopeSize: ["pointRadius", "periodFontSize", "labelFontSize", "leftAxisX", "axisGap", "labelAxisGap", "plotPadding", "pointPadding"],
2840
+ chartStackedLayout: ["legendColumns", "legendColumnGap", "segmentGap", "barThickness", "barWidth", "legendMarkSize", "legendFontSize", "periodFontSize", "rateFontSize", "legendOffsetY"],
2841
+ chartGaugeSize: ["arcThickness", "outerRadius", "plotTop", "valueFontSize"],
2842
+ ganttSize: ["taskLabelFontSize", "taskLabelAlign", "taskLabelX", "tickFontSize", "tickCount", "barThickness", "milestoneFontSize", "milestoneRadius", "dependencyStrokeWidth", "taskColumnWidth", "timelinePaddingRight"],
2843
+ quadrantSize: ["plotOffset", "plotSize", "pointRadius", "pointFontSize", "pointLabelGap", "quadrantFontSize", "axisFontSize"],
2844
+ legendOffset: ["x", "y"],
2845
+ stageLayout: ["columnWidths", "columnGap", "backDetourTopGap", "backDetourSideGap", "backDetourTargetInset"],
2846
+ } as const;
2847
+ const objects = Object.fromEntries(Object.entries(specs).map(([name, keys]) => [name, closedObject(name as keyof typeof specs, keys)])) as Record<keyof typeof specs, Record<string, unknown> | undefined>;
2848
+ const positiveKeys: Partial<Record<keyof typeof specs, readonly string[]>> = {
2849
+ figureKindLabel: ["fontSize", "fontWeight"],
2850
+ chartBarSize: ["tickFontSize", "valueFontSize", "labelFontSize", "barWidth"],
2851
+ chartLineSize: ["tickFontSize", "periodFontSize", "pointRadius"],
2852
+ chartPieSize: ["tableWidth", "tableLabelFontSize", "tableValueFontSize", "outerRadius"],
2853
+ chartWaffleSize: ["cellPitch"],
2854
+ chartSlopeSize: ["pointRadius", "periodFontSize", "labelFontSize", "axisGap"],
2855
+ chartStackedLayout: ["barThickness", "barWidth", "legendMarkSize", "legendFontSize", "periodFontSize", "rateFontSize"],
2856
+ chartGaugeSize: ["arcThickness", "outerRadius", "valueFontSize"],
2857
+ ganttSize: ["taskLabelFontSize", "tickFontSize", "barThickness", "milestoneFontSize", "milestoneRadius", "dependencyStrokeWidth"],
2858
+ quadrantSize: ["plotSize", "pointRadius", "pointFontSize", "quadrantFontSize", "axisFontSize"],
2859
+ };
2860
+ const nonNegativeKeys: Partial<Record<keyof typeof specs, readonly string[]>> = {
2861
+ figureKindLabel: ["letterSpacing"], figureInset: ["top", "right", "bottom", "left", "radius"],
2862
+ chartBarSize: ["cornerRadius"], chartLineSize: ["tickGap", "periodY"], chartPieSize: ["tableRowGap", "tablePaddingInline", "centerX"], chartWaffleSize: ["gridLeft"], chartSlopeSize: ["leftAxisX", "labelAxisGap"],
2863
+ chartStackedLayout: ["legendColumnGap", "segmentGap"], chartGaugeSize: ["plotTop"],
2864
+ ganttSize: ["taskLabelX", "taskColumnWidth", "timelinePaddingRight"], quadrantSize: ["pointLabelGap"],
2865
+ stageLayout: ["columnGap", "backDetourTopGap", "backDetourSideGap", "backDetourTargetInset"],
2866
+ };
2867
+ for (const [name, object] of Object.entries(objects) as [keyof typeof specs, Record<string, unknown> | undefined][]) {
2868
+ if (object === undefined) continue;
2869
+ for (const key of positiveKeys[name] ?? []) if (object[key] !== undefined) positiveNumber(object[key], `$.${name}.${key}`);
2870
+ for (const key of nonNegativeKeys[name] ?? []) if (object[key] !== undefined) nonNegativeNumber(object[key], `$.${name}.${key}`);
2871
+ }
2872
+ const finite = (value: unknown, path: string): void => { if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value))) errors.push({ path, message: "must be a finite number" }); };
2873
+ if (objects.terminalDecoration) for (const key of specs.terminalDecoration) if (objects.terminalDecoration[key] !== undefined && typeof objects.terminalDecoration[key] !== "boolean") errors.push({ path: `$.terminalDecoration.${key}`, message: "must be boolean" });
2874
+ if (objects.figureKindLabel) {
2875
+ if (typeof objects.figureKindLabel.text !== "string" || objects.figureKindLabel.text.trim() === "") errors.push({ path: "$.figureKindLabel.text", message: "must be a non-empty string" });
2876
+ finite(objects.figureKindLabel.x, "$.figureKindLabel.x"); finite(objects.figureKindLabel.y, "$.figureKindLabel.y");
2877
+ }
2878
+ if (objects.chartSlopeLabelTemplate) for (const key of specs.chartSlopeLabelTemplate) if (objects.chartSlopeLabelTemplate[key] !== undefined && typeof objects.chartSlopeLabelTemplate[key] !== "string") errors.push({ path: `$.chartSlopeLabelTemplate.${key}`, message: "must be a string" });
2879
+ if (objects.chartStackedLayout) finite(objects.chartStackedLayout.legendOffsetY, "$.chartStackedLayout.legendOffsetY");
2880
+ if (objects.legendOffset) {
2881
+ finite(objects.legendOffset.x, "$.legendOffset.x");
2882
+ finite(objects.legendOffset.y, "$.legendOffset.y");
2883
+ }
2884
+ const nestedNumberObject = (parent: keyof typeof specs, key: string, keys: readonly string[], positive: boolean): void => {
2885
+ const value = objects[parent]?.[key]; if (value === undefined) return;
2886
+ if (!value || typeof value !== "object" || Array.isArray(value)) { errors.push({ path: `$.${parent}.${key}`, message: "must be an object" }); return; }
2887
+ const object = value as Record<string, unknown>;
2888
+ for (const child of Object.keys(object)) if (!keys.includes(child)) errors.push({ path: `$.${parent}.${key}.${child}`, message: `unknown key "${child}"` });
2889
+ for (const child of keys) if (object[child] !== undefined) (positive ? positiveNumber : nonNegativeNumber)(object[child], `$.${parent}.${key}.${child}`);
2890
+ };
2891
+ nestedNumberObject("chartBarSize", "plotPadding", ["top", "right", "bottom", "left"], false);
2892
+ nestedNumberObject("chartLineSize", "valueLabelOffset", ["above", "below"], false);
2893
+ nestedNumberObject("chartLineSize", "plotPadding", ["top", "right", "bottom", "left"], false);
2894
+ nestedNumberObject("chartLineSize", "pointPadding", ["left", "right"], false);
2895
+ nestedNumberObject("chartSlopeSize", "plotPadding", ["top", "bottom"], false);
2896
+ nestedNumberObject("chartSlopeSize", "pointPadding", ["top", "bottom"], false);
2897
+ nestedNumberObject("journeySize", "plotPadding", ["left", "right"], false);
2898
+ nestedNumberObject("journeySize", "pointPadding", ["left", "right"], false);
2899
+ nestedNumberObject("quadrantSize", "plotOffset", ["x", "y"], false);
2900
+ const columnWidths = objects.stageLayout?.columnWidths;
2901
+ if (columnWidths !== undefined) {
2902
+ if (!columnWidths || typeof columnWidths !== "object" || Array.isArray(columnWidths)) errors.push({ path: "$.stageLayout.columnWidths", message: "must be an object" });
2903
+ else for (const [key, value] of Object.entries(columnWidths)) nonNegativeNumber(value, `$.stageLayout.columnWidths.${key}`);
2904
+ }
2905
+ const stackedColumns = objects.chartStackedLayout?.legendColumns;
2906
+ if (stackedColumns !== undefined && (!Number.isInteger(stackedColumns) || typeof stackedColumns !== "number" || stackedColumns <= 0)) errors.push({ path: "$.chartStackedLayout.legendColumns", message: "must be a positive integer" });
2907
+ if (
2908
+ objects.ganttSize?.taskLabelAlign !== undefined &&
2909
+ !(GANTT_TASK_LABEL_ALIGN_VALUES as readonly unknown[]).includes(objects.ganttSize.taskLabelAlign)
2910
+ ) errors.push({ path: "$.ganttSize.taskLabelAlign", message: "must be start or end" });
2911
+ const tickCount = objects.ganttSize?.tickCount;
2912
+ if (tickCount !== undefined && (!Number.isInteger(tickCount) || typeof tickCount !== "number" || tickCount < 2)) errors.push({ path: "$.ganttSize.tickCount", message: "must be an integer greater than or equal to 2" });
2913
+
2914
+ if (Array.isArray(j.chartSlopePeriods) &&
2915
+ (j.chartSlopePeriods.length !== 2 || j.chartSlopePeriods.some((v) => typeof v !== "string" || v.trim() === ""))) {
2916
+ errors.push({ path: "$.chartSlopePeriods", message: "chartSlopePeriods must contain exactly two non-empty strings" });
2917
+ }
2918
+ if (Array.isArray(j.chartSlopeEmphasisIds) && j.chartSlopeEmphasisIds.length === 0) {
2919
+ errors.push({ path: "$.chartSlopeEmphasisIds", message: "chartSlopeEmphasisIds must contain at least one id" });
2920
+ }
2921
+
2922
+ const boundNumber = (value: unknown): boolean =>
2923
+ (typeof value === "number" && Number.isFinite(value)) ||
2924
+ (typeof value === "string" && value.trim() !== "");
2925
+ if (j.chartLineSeries !== undefined && !Array.isArray(j.chartLineSeries)) {
2926
+ errors.push({ path: "$.chartLineSeries", message: "chartLineSeries must be an array" });
2927
+ } else if (Array.isArray(j.chartLineSeries)) {
2928
+ if (j.chartLineSeries.length === 0) {
2929
+ errors.push({ path: "$.chartLineSeries", message: "chartLineSeries must contain at least one series" });
2930
+ }
2931
+ j.chartLineSeries.forEach((series, i) => {
2932
+ const path = `$.chartLineSeries[${i}]`;
2933
+ if (!series || typeof series !== "object" || Array.isArray(series)) {
2934
+ errors.push({ path, message: "chartLineSeries item must be an object" });
2935
+ return;
2936
+ }
2937
+ const value = series as Record<string, unknown>;
2938
+ for (const key of Object.keys(value)) {
2939
+ if (!["label", "points", "tone", "dash", "marker", "valueIndexes"].includes(key))
2940
+ errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
2941
+ }
2942
+ if (value.label !== undefined && typeof value.label !== "string")
2943
+ errors.push({ path: `${path}.label`, message: "label must be a string" });
2944
+ if (value.tone !== undefined && (typeof value.tone !== "string" || resolveTone(value.tone) === undefined))
2945
+ errors.push({ path: `${path}.tone`, message: "tone must be a known tone" });
2946
+ if (
2947
+ value.dash !== undefined &&
2948
+ (typeof value.dash !== "string" ||
2949
+ !(CHART_LINE_DASH_VALUES as readonly string[]).includes(value.dash))
2950
+ )
2951
+ errors.push({ path: `${path}.dash`, message: "dash must be solid or dotted" });
2952
+ if (
2953
+ value.marker !== undefined &&
2954
+ (typeof value.marker !== "string" ||
2955
+ !(CHART_LINE_MARKER_VALUES as readonly string[]).includes(value.marker))
2956
+ )
2957
+ errors.push({ path: `${path}.marker`, message: "marker must be dot, hollow, or none" });
2958
+ if (value.valueIndexes !== undefined &&
2959
+ (!Array.isArray(value.valueIndexes) || value.valueIndexes.some((v) => typeof v !== "number" || !Number.isInteger(v) || v < 0)))
2960
+ errors.push({ path: `${path}.valueIndexes`, message: "valueIndexes must contain non-negative integers" });
2961
+ if (!Array.isArray(value.points) || value.points.length === 0) {
2962
+ errors.push({ path: `${path}.points`, message: "points must be a non-empty array" });
2963
+ } else value.points.forEach((point, p) => {
2964
+ const pointPath = `${path}.points[${p}]`;
2965
+ if (!point || typeof point !== "object" || Array.isArray(point)) {
2966
+ errors.push({ path: pointPath, message: "point must be an object" });
2967
+ return;
2968
+ }
2969
+ const item = point as Record<string, unknown>;
2970
+ for (const key of Object.keys(item)) if (key !== "label" && key !== "value")
2971
+ errors.push({ path: `${pointPath}.${key}`, message: `unknown key "${key}"` });
2972
+ if (typeof item.label !== "string") errors.push({ path: `${pointPath}.label`, message: "label must be a string" });
2973
+ if (!boundNumber(item.value)) errors.push({ path: `${pointPath}.value`, message: "value must be a finite number or non-empty string" });
2974
+ });
2975
+ });
2976
+ }
2977
+
2978
+ if (j.chartStackedPeriods !== undefined && !Array.isArray(j.chartStackedPeriods)) {
2979
+ errors.push({ path: "$.chartStackedPeriods", message: "chartStackedPeriods must be an array" });
2980
+ } else if (Array.isArray(j.chartStackedPeriods)) {
2981
+ if (j.chartStackedPeriods.length === 0)
2982
+ errors.push({ path: "$.chartStackedPeriods", message: "chartStackedPeriods must contain at least one period" });
2983
+ j.chartStackedPeriods.forEach((period, i) => {
2984
+ const path = `$.chartStackedPeriods[${i}]`;
2985
+ if (!period || typeof period !== "object" || Array.isArray(period)) {
2986
+ errors.push({ path, message: "chartStackedPeriods item must be an object" });
2987
+ return;
2988
+ }
2989
+ const value = period as Record<string, unknown>;
2990
+ for (const key of Object.keys(value)) if (key !== "label" && key !== "values")
2991
+ errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
2992
+ if (typeof value.label !== "string") errors.push({ path: `${path}.label`, message: "label must be a string" });
2993
+ if (!Array.isArray(value.values) || value.values.some((entry) => !boundNumber(entry)))
2994
+ errors.push({ path: `${path}.values`, message: "values must be an array of finite numbers or non-empty strings" });
2995
+ });
2996
+ }
2997
+
2998
+ if (
2999
+ j.chartGaugeValue !== undefined &&
3000
+ (!j.chartGaugeValue || typeof j.chartGaugeValue !== "object" || Array.isArray(j.chartGaugeValue))
3001
+ ) {
3002
+ errors.push({ path: "$.chartGaugeValue", message: "chartGaugeValue must be an object" });
3003
+ } else if (j.chartGaugeValue && typeof j.chartGaugeValue === "object") {
3004
+ const value = j.chartGaugeValue as Record<string, unknown>;
3005
+ for (const key of Object.keys(value)) if (!["max", "current", "target", "previous", "previousLabel"].includes(key))
3006
+ errors.push({ path: `$.chartGaugeValue.${key}`, message: `unknown key "${key}"` });
3007
+ const finite = (entry: unknown): boolean => typeof entry === "number" && Number.isFinite(entry);
3008
+ if (!finite(value.max) || Number(value.max) <= 0) errors.push({ path: "$.chartGaugeValue.max", message: "max must be a positive finite number" });
3009
+ if (!boundNumber(value.current)) errors.push({ path: "$.chartGaugeValue.current", message: "current must be a finite number or non-empty string" });
3010
+ for (const key of ["target", "previous"] as const) if (value[key] !== undefined && !boundNumber(value[key]))
3011
+ errors.push({ path: `$.chartGaugeValue.${key}`, message: `${key} must be a finite number or non-empty string` });
3012
+ if (value.previousLabel !== undefined && typeof value.previousLabel !== "string")
3013
+ errors.push({ path: "$.chartGaugeValue.previousLabel", message: "previousLabel must be a string" });
3014
+ }
3015
+ }
3016
+
3017
+ /**
3018
+ * JSON の区画の名前を、図の側の綴りへ直す (#2667)。
3019
+ *
3020
+ * 記法と JSON はどちらも `左上` のような語で書き、図の側は `topLeft` で受ける。
3021
+ * 直す先の表は項目の欄と共有する (`区画`) = 同じ区画を 2 通りで呼ばない。
3022
+ *
3023
+ * 中身の無い形は「書かなかった」 と同じにする (`axes` と同じ扱い)。
3024
+ */
3025
+ function 区画の名前を直す(r: JsonRegions | undefined): DslRegions | undefined {
3026
+ if (r === undefined) return undefined;
3027
+ const 出: DslRegions = {};
3028
+ for (const [語, key] of 区画) {
3029
+ const v = (r as Record<string, unknown>)[語];
3030
+ if (typeof v === "string" && v.trim() !== "") 出[key] = v;
3031
+ }
3032
+ return Object.keys(出).length > 0 ? 出 : undefined;
3033
+ }
3034
+
3035
+ /** 2 軸で仕分ける図の区画の名前を見る (#2667) */
3036
+ function validateRegions(v: unknown, errors: JsonDslError[]): void {
3037
+ if (v === undefined) return;
3038
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
3039
+ errors.push({
3040
+ path: "$.regions",
3041
+ message: 'regions must be a plain object like { "左上": "すぐやる" }',
3042
+ });
3043
+ return;
2022
3044
  }
3045
+ const o = v as Record<string, unknown>;
3046
+ checkUnknownKeys(o, "regions", "$.regions", errors);
3047
+ 表で検査(o, "regions", "$.regions", "regions", errors);
2023
3048
  }
2024
3049
 
2025
3050
  /**
@@ -2288,6 +3313,8 @@ function validateJson(
2288
3313
  // 値を見せる部品の中身を、記法と同じ表で見る (#1374)
2289
3314
  validateReadouts(j.readouts, errors);
2290
3315
  validateBands(j.bands, errors);
3316
+ validateLegend(j.legend, errors);
3317
+ validateStageHeader(j.stageHeader, errors);
2291
3318
  // 読む人が動かすつまみの中身も、記法と同じ表で見る (#1389)
2292
3319
  validateInputs(j.inputs, errors);
2293
3320
  // 式は描画側の parser に通す (#1391)
@@ -2299,6 +3326,29 @@ function validateJson(
2299
3326
  // 値そのものの型は表が見る (#1304)。 図表の箱の上の小見出し (#1247) の空文字は
2300
3327
  // 「書かなかった」 と同じ扱いにするため通す (記法側の `eyebrow:` と揃える。 落とすのは `jsonToDoc`)
2301
3328
  表で検査(j, "root", "$", "", errors);
3329
+ validateDiagramShape(j, errors);
3330
+ for (const 欄 of ["ticks", "目盛り"] as const) {
3331
+ if (Array.isArray(j[欄]) && j[欄].length === 0) {
3332
+ errors.push({ path: `$.${欄}`, message: `${欄} must contain at least one label` });
3333
+ }
3334
+ }
3335
+ if (j.ganttToday !== undefined) {
3336
+ const today = j.ganttToday;
3337
+ if (!today || typeof today !== "object" || Array.isArray(today)) {
3338
+ errors.push({ path: "$.ganttToday", message: "ganttToday must be an object" });
3339
+ } else {
3340
+ const value = today as Record<string, unknown>;
3341
+ for (const key of Object.keys(value)) {
3342
+ if (key !== "index" && key !== "label") errors.push({ path: `$.ganttToday.${key}`, message: `unknown key "${key}"` });
3343
+ }
3344
+ if (typeof value.index !== "number" || !Number.isFinite(value.index)) {
3345
+ errors.push({ path: "$.ganttToday.index", message: "ganttToday.index must be a finite number" });
3346
+ }
3347
+ if (typeof value.label !== "string") {
3348
+ errors.push({ path: "$.ganttToday.label", message: "ganttToday.label must be a string" });
3349
+ }
3350
+ }
3351
+ }
2302
3352
 
2303
3353
  if (!Array.isArray(j.actors) || j.actors.length === 0) {
2304
3354
  errors.push({ path: "$.actors", message: "actors must be a non-empty array" });
@@ -2314,6 +3364,93 @@ function validateJson(
2314
3364
  // 値そのものの型は表が見る (#1304)。 `kind` は見本 (parts) の名前も受けるため
2315
3365
  // 非空の文字列までしか縛らない (CAR-1657 の unified syntax)
2316
3366
  表で検査(ao, "actor", `$.actors[${i}]`, "actor", errors);
3367
+ const actorEnum = (key: string, allowed: readonly string[]): void => {
3368
+ const value = ao[key];
3369
+ if (value === undefined || typeof value !== "string") return;
3370
+ if (!allowed.includes(value)) errors.push({ path: `$.actors[${i}].${key}`, message: `${key} must be one of: ${allowed.join(", ")}` });
3371
+ };
3372
+ actorEnum("kindForm", KIND_FORM_VALUES);
3373
+ actorEnum("icon", CDL_ICON_VALUES);
3374
+ actorEnum("titleAlign", TITLE_ALIGN_VALUES);
3375
+ actorEnum("rowMarkForm", ROW_MARK_FORM_VALUES);
3376
+ actorEnum("rowDividerForm", ROW_DIVIDER_FORM_VALUES);
3377
+ if (ao.rowWell !== undefined) {
3378
+ const path = `$.actors[${i}].rowWell`;
3379
+ if (!ao.rowWell || typeof ao.rowWell !== "object" || Array.isArray(ao.rowWell)) {
3380
+ errors.push({ path, message: "rowWell must be an object" });
3381
+ } else {
3382
+ const well = ao.rowWell as Record<string, unknown>;
3383
+ for (const key of Object.keys(well)) {
3384
+ if (!(ROW_WELL_KEYS as readonly string[]).includes(key)) errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
3385
+ }
3386
+ for (const key of ROW_WELL_KEYS) {
3387
+ const value = well[key];
3388
+ if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || value < 0)) errors.push({ path: `${path}.${key}`, message: "must be a non-negative finite number" });
3389
+ }
3390
+ }
3391
+ }
3392
+ if (ao.rowLayout !== undefined) {
3393
+ const path = `$.actors[${i}].rowLayout`;
3394
+ if (!ao.rowLayout || typeof ao.rowLayout !== "object" || Array.isArray(ao.rowLayout)) {
3395
+ errors.push({ path, message: "rowLayout must be an object" });
3396
+ } else {
3397
+ const layout = ao.rowLayout as Record<string, unknown>;
3398
+ for (const key of Object.keys(layout)) if (!["titleBaseline", "rowTop", "rowPitch"].includes(key)) errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
3399
+ for (const key of ["titleBaseline", "rowTop", "rowPitch"] as const) {
3400
+ const value = layout[key];
3401
+ if (value !== undefined && (typeof value !== "number" || !Number.isFinite(value) || (key === "rowPitch" ? value <= 0 : value < 0))) errors.push({ path: `${path}.${key}`, message: key === "rowPitch" ? "must be a positive finite number" : "must be a non-negative finite number" });
3402
+ }
3403
+ }
3404
+ }
3405
+ if (ao.figureCard !== undefined) {
3406
+ const path = `$.actors[${i}].figureCard`;
3407
+ if (!ao.figureCard || typeof ao.figureCard !== "object" || Array.isArray(ao.figureCard)) errors.push({ path, message: "figureCard must be an object" });
3408
+ else {
3409
+ const card = ao.figureCard as Record<string, unknown>;
3410
+ for (const key of Object.keys(card)) if (key !== "label" && key !== "note") errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
3411
+ if (typeof card.label !== "string") errors.push({ path: `${path}.label`, message: "label must be a string" });
3412
+ if (card.note !== undefined && typeof card.note !== "string") errors.push({ path: `${path}.note`, message: "note must be a string" });
3413
+ }
3414
+ }
3415
+ if (ao.marks !== undefined && !Array.isArray(ao.marks)) {
3416
+ errors.push({ path: `$.actors[${i}].marks`, message: "marks must be an array" });
3417
+ } else if (Array.isArray(ao.marks)) {
3418
+ ao.marks.forEach((mark, markIndex) => {
3419
+ if (typeof mark === "string") return;
3420
+ const path = `$.actors[${i}].marks[${markIndex}]`;
3421
+ if (!mark || typeof mark !== "object" || Array.isArray(mark)) {
3422
+ errors.push({ path, message: "mark must be a string or object" });
3423
+ return;
3424
+ }
3425
+ const entry = mark as Record<string, unknown>;
3426
+ for (const key of Object.keys(entry)) if (key !== "mark" && key !== "tone") errors.push({ path: `${path}.${key}`, message: `unknown key "${key}"` });
3427
+ if (typeof entry.mark !== "string") errors.push({ path: `${path}.mark`, message: "mark must be a string" });
3428
+ if (entry.tone !== undefined && !(ROW_MARK_TONE_VALUES as readonly unknown[]).includes(entry.tone)) errors.push({ path: `${path}.tone`, message: `tone must be one of: ${ROW_MARK_TONE_VALUES.join(", ")}` });
3429
+ });
3430
+ }
3431
+ if (
3432
+ typeof ao.opportunityPosition === "string" &&
3433
+ !(JOURNEY_OPPORTUNITY_POSITION_VALUES as readonly string[]).includes(ao.opportunityPosition)
3434
+ ) {
3435
+ errors.push({
3436
+ path: `$.actors[${i}].opportunityPosition`,
3437
+ message: `opportunityPosition must be one of: ${JOURNEY_OPPORTUNITY_POSITION_VALUES.join(", ")}`,
3438
+ });
3439
+ }
3440
+ for (const 欄 of ["at", "点の位置"] as const) {
3441
+ if (ao[欄] === undefined) continue;
3442
+ const at = ao[欄];
3443
+ if (
3444
+ !Array.isArray(at) ||
3445
+ at.length !== 2 ||
3446
+ at.some((v) => typeof v !== "number" && typeof v !== "string")
3447
+ ) {
3448
+ errors.push({
3449
+ path: `$.actors[${i}].${欄}`,
3450
+ message: `actor.${欄} must be [x, y] with number or state reference values`,
3451
+ });
3452
+ }
3453
+ }
2317
3454
  // 箱の中に描く図形の中身を、記法と同じ表で見る (#1374)
2318
3455
  validateActorShape(ao.shape, `$.actors[${i}].shape`, errors);
2319
3456
  // 見本 (parts) にしか効かない項目は、見本でない箱に書かれたら誤りにする (#1294)。
@@ -2387,6 +3524,44 @@ function validateJson(
2387
3524
  checkUnknownKeys(so, "step", `$.flow[${i}]`, errors);
2388
3525
  // 値そのものの型は表が見る (#1304)。 色と線種は engine の一覧と突き合わせる
2389
3526
  表で検査(so, "step", `$.flow[${i}]`, "step", errors);
3527
+ if (so.labelCollision !== undefined && !(LABEL_COLLISION_VALUES as readonly unknown[]).includes(so.labelCollision)) {
3528
+ errors.push({ path: `$.flow[${i}].labelCollision`, message: `labelCollision must be one of: ${LABEL_COLLISION_VALUES.join(", ")}` });
3529
+ }
3530
+ if (
3531
+ so.labelSize !== undefined &&
3532
+ (!so.labelSize || typeof so.labelSize !== "object" || Array.isArray(so.labelSize))
3533
+ ) {
3534
+ errors.push({ path: `$.flow[${i}].labelSize`, message: "labelSize must be an object" });
3535
+ } else if (so.labelSize !== undefined) {
3536
+ const size = so.labelSize as Record<string, unknown>;
3537
+ for (const key of Object.keys(size)) if (!["width", "height", "fontSize"].includes(key)) errors.push({ path: `$.flow[${i}].labelSize.${key}`, message: `unknown key "${key}"` });
3538
+ for (const key of ["width", "height", "fontSize"]) if (size[key] !== undefined && (typeof size[key] !== "number" || !Number.isFinite(size[key]) || size[key] <= 0)) errors.push({ path: `$.flow[${i}].labelSize.${key}`, message: "must be a positive finite number" });
3539
+ }
3540
+ if (so.waypoints !== undefined) {
3541
+ const path = `$.flow[${i}].waypoints`;
3542
+ if (!Array.isArray(so.waypoints)) errors.push({ path, message: "waypoints must be an array" });
3543
+ else {
3544
+ const points: Array<{ x: number; y: number }> = [];
3545
+ so.waypoints.forEach((point, pointIndex) => {
3546
+ const pointPath = `${path}[${pointIndex}]`;
3547
+ if (!point || typeof point !== "object" || Array.isArray(point)) {
3548
+ errors.push({ path: pointPath, message: "waypoint must be an object" });
3549
+ return;
3550
+ }
3551
+ const value = point as Record<string, unknown>;
3552
+ for (const key of Object.keys(value)) if (key !== "x" && key !== "y") errors.push({ path: `${pointPath}.${key}`, message: `unknown key "${key}"` });
3553
+ if (typeof value.x !== "number" || !Number.isFinite(value.x) || typeof value.y !== "number" || !Number.isFinite(value.y)) errors.push({ path: pointPath, message: "waypoint x and y must be finite numbers" });
3554
+ else points.push({ x: value.x, y: value.y });
3555
+ });
3556
+ const ends = [so.posX1, so.posY1, so.posX2, so.posY2];
3557
+ if (ends.every((value) => typeof value === "number" && Number.isFinite(value)) && points.length === so.waypoints.length) {
3558
+ const all = [{ x: so.posX1 as number, y: so.posY1 as number }, ...points, { x: so.posX2 as number, y: so.posY2 as number }];
3559
+ all.forEach((point, pointIndex) => {
3560
+ if (pointIndex > 0 && point.x !== all[pointIndex - 1]!.x && point.y !== all[pointIndex - 1]!.y) errors.push({ path: `${path}[${Math.min(pointIndex - 1, points.length - 1)}]`, message: "each waypoint segment must be orthogonal" });
3561
+ });
3562
+ }
3563
+ }
3564
+ }
2390
3565
  // CAR-1693 Phase 1: step DSL 表面 pos の validation
2391
3566
  validateLayoutPos(so.pos, `$.flow[${i}].pos`, errors);
2392
3567
  });
@@ -2423,11 +3598,38 @@ function validateJson(
2423
3598
  }
2424
3599
  validateStates(j.states, errors);
2425
3600
  validateValues(j.values, errors);
3601
+ validateChartPresentationFields(j, errors);
2426
3602
  validateAxes(j.axes, errors);
3603
+ validateRegions(j.regions, errors);
2427
3604
  if (errors.length > 0) return { ok: false, errors };
2428
3605
  return { ok: true, data: j as unknown as DragonJson };
2429
3606
  }
2430
3607
 
3608
+ /** 最上位の `shape` を記法と同じ図種別の集合で検査する (#2797)。 */
3609
+ function validateDiagramShape(j: Record<string, unknown>, errors: JsonDslError[]): void {
3610
+ if (j.shape === undefined) return;
3611
+ if (typeof j.shape !== "string" || typeof j.type !== "string") return;
3612
+ const 別名 = TYPE_ALIASES.get(j.type);
3613
+ const type = 別名?.type ?? (PRESET_TYPES.has(j.type as PresetType) ? (j.type as PresetType) : null);
3614
+ if (type === null) return;
3615
+ const shape = resolveDiagramShape(j.shape);
3616
+ const 図種の語 = shapesForDiagramType(type);
3617
+ // 語そのものの誤りは `欄の型表` が同じ集合から伝える。 ここでは図種との組だけを見る。
3618
+ if (shape === null) return;
3619
+ if (shape !== null && (type !== "chart" || 図種の語.includes(shape))) return;
3620
+ const hint =
3621
+ 図種の語.length > 0
3622
+ ? `使える語 = ${図種の語.join(" / ")}`
3623
+ : `shape を書ける図種と語 = ${Object.entries(DIAGRAM_SHAPES)
3624
+ .map(([図種, 語]) => `type: ${図種} (${語.join(" / ")})`)
3625
+ .join(" / ")}`;
3626
+ errors.push({
3627
+ path: "$.shape",
3628
+ message: `shape is not valid for type: ${type}`,
3629
+ hint,
3630
+ });
3631
+ }
3632
+
2431
3633
  /**
2432
3634
  * 段の中で値を動かす指定を見る (#1186)。
2433
3635
  *
@@ -2553,6 +3755,20 @@ function validateValues(v: unknown, errors: JsonDslError[]): void {
2553
3755
  }
2554
3756
  }
2555
3757
 
3758
+ /**
3759
+ * 段の説明を取り出す (#2621)。 受ける語は `PHASE_BODY_KEYS` が持つ。
3760
+ *
3761
+ * **正の語 (`body`) を先に見る**。 表の先頭が正の語なので、並び順のまま最初に値を持つ語を
3762
+ * 採れば「どちらが効くか」 の規則を別に書かなくて済む。
3763
+ */
3764
+ function 段の説明(p: JsonPhase): string | undefined {
3765
+ for (const key of PHASE_BODY_KEYS) {
3766
+ const v = p[key];
3767
+ if (typeof v === "string") return v;
3768
+ }
3769
+ return undefined;
3770
+ }
3771
+
2556
3772
  /**
2557
3773
  * 図表の箱の上の小見出しを、 記法側と同じ形に整える (#1247)。
2558
3774
  *
@@ -2606,6 +3822,7 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2606
3822
  // 色は記法と同じ振り分けを通す (#1294)。 `#` で始まれば色番号、それ以外は色の名前。
2607
3823
  // 別々に書くと、同じ値が入口によって色番号にも色名にもなる
2608
3824
  const 色 = a.color !== undefined ? splitColorValue(a.color) : {};
3825
+ const 書いた座標 = a.at ?? a.点の位置;
2609
3826
  return {
2610
3827
  name: a.name,
2611
3828
  kind: isPart ? "actor" : resolveNodeKind(kindStr),
@@ -2613,12 +3830,33 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2613
3830
  // 「書かなかった」 と同じ扱いにする (#1058)
2614
3831
  kindWritten: a.kind !== undefined && !isPart,
2615
3832
  subtitle: a.subtitle,
3833
+ kindForm: isPart ? undefined : a.kindForm,
3834
+ icon: isPart ? undefined : a.icon,
3835
+ emptyRowsNote: isPart ? undefined : a.emptyRowsNote,
3836
+ titleAlign: isPart ? undefined : a.titleAlign,
3837
+ rowRules: isPart ? undefined : a.rowRules,
3838
+ rowStripe: isPart ? undefined : a.rowStripe,
3839
+ rowLayout: isPart ? undefined : a.rowLayout,
3840
+ rowDividerForm: isPart ? undefined : a.rowDividerForm,
3841
+ rowWell: isPart ? undefined : a.rowWell,
3842
+ rowMarkForm: isPart ? undefined : a.rowMarkForm,
3843
+ nestIn: isPart ? undefined : a.nestIn,
3844
+ figureCard: isPart ? undefined : a.figureCard,
3845
+ stationNamePosition: a.stationNamePosition,
3846
+ subtitlePlacement: a.subtitlePlacement,
3847
+ titleFontSize: a.titleFontSize,
3848
+ markGap: a.markGap,
2616
3849
  eyebrow: a.eyebrow,
2617
3850
  value: a.value,
2618
3851
  previous: a.previous,
2619
3852
  rows: a.rows,
2620
- // 行頭の印 (#1466)。 行と対で読む
2621
- marks: a.marks,
3853
+ // 行頭の印 (#1466)。 行と対で読む。 古い語は記法と同じ関数で読み替える (#2782)
3854
+ marks: a.marks?.map((m) =>
3855
+ typeof m === "string"
3856
+ ? 行頭の語へ読み替える(m)
3857
+ : { mark: 行頭の語へ読み替える(m.mark), ...(m.tone === undefined ? {} : { tone: m.tone }) },
3858
+ ),
3859
+ stage: isPart ? undefined : a.stage,
2622
3860
  lane: a.lane,
2623
3861
  stack: a.stack,
2624
3862
  initial: a.initial,
@@ -2642,8 +3880,17 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2642
3880
  tone: resolveTone(a.tone) ?? 色.tone,
2643
3881
  owner: a.owner,
2644
3882
  end: a.end,
3883
+ startLabel: a.startLabel,
3884
+ milestone: a.milestone,
3885
+ emphasis: a.emphasis,
3886
+ at:
3887
+ 書いた座標 === undefined
3888
+ ? undefined
3889
+ : { x: 書いた座標[0], y: 書いた座標[1], raw: `[${書いた座標.join(", ")}]` },
3890
+ atPos: 書いた座標 === undefined ? undefined : 位置("actors", i, a.at ? "at" : "点の位置"),
2645
3891
  touchpoint: a.touchpoint,
2646
3892
  opportunity: a.opportunity,
3893
+ opportunityPosition: a.opportunityPosition,
2647
3894
  }),
2648
3895
  colorHex: 色.hex,
2649
3896
  // 部品に書いた色の名前は効かない。 記法と同じく書いた名前として残し、組み立て側が知らせる (#1973)
@@ -2666,8 +3913,17 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2666
3913
  pos: 位置("actors", i),
2667
3914
  };
2668
3915
  });
2669
- // 図の配色 (#1553)。 解くのは 1 度だけにする
2670
- const 配色 = json.palette === undefined ? null : resolvePalette(json.palette);
3916
+ // 図の意匠 (#1553 / #2790)。 正の語と別名を別々に解き、順番に依らず `theme` を優先する
3917
+ const 意匠 = json.theme === undefined ? null : resolveTheme(json.theme);
3918
+ const 別名の意匠 = json.palette === undefined ? null : resolveTheme(json.palette);
3919
+ const 選んだ意匠 = 意匠 ?? 別名の意匠;
3920
+ const 並び順 = json.order === undefined ? null : resolveOrder(json.order);
3921
+ const 形 = json.shape === undefined ? null : resolveDiagramShape(json.shape);
3922
+ const legend = json.legend?.flatMap((item) => {
3923
+ if (item.mark === undefined) return [item];
3924
+ const mark = resolveLegendMark(item.mark);
3925
+ return mark === null ? [] : [{ ...item, mark } as CdlLegendItem];
3926
+ });
2671
3927
 
2672
3928
  const flow: DslStep[] = json.flow.map((s, i) => ({
2673
3929
  no: i + 1,
@@ -2676,13 +3932,15 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2676
3932
  label: s.label,
2677
3933
  sub: s.sub,
2678
3934
  side: s.side,
3935
+ fromSide: s.fromSide,
3936
+ toSide: s.toSide,
2679
3937
  // 矢印の先の形 (#1462)。 読めない語は組み立てが落とす
2680
3938
  head: s.head,
2681
3939
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
2682
3940
  tailHead: s.tailHead,
2683
3941
  headFill: s.headFill,
2684
3942
  tailHeadFill: s.tailHeadFill,
2685
- // クラス図の出どころ側の多重度 (#1771)
3943
+ // UML 関係の出どころ側の多重度 (#1771 / #2783)
2686
3944
  tailSub: s.tailSub,
2687
3945
  relation: s.relation,
2688
3946
  // 辺の役目と名前の下地 (cdl#618)
@@ -2701,6 +3959,13 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2701
3959
  dashOffsetBind: s.dashOffsetBind,
2702
3960
  labelOffsetX: s.labelOffsetX,
2703
3961
  labelOffsetY: s.labelOffsetY,
3962
+ posX1: s.posX1,
3963
+ posY1: s.posY1,
3964
+ posX2: s.posX2,
3965
+ posY2: s.posY2,
3966
+ waypoints: s.waypoints,
3967
+ labelCollision: s.labelCollision,
3968
+ labelSize: s.labelSize,
2704
3969
  overlay: s.overlay,
2705
3970
  // 部品の端で繋ぐ要素の id (#1979)。 書かない矢印には欄を足さない = 記法と同じ形になる
2706
3971
  ...(s.fromPartNode !== undefined ? { fromPartNode: s.fromPartNode } : {}),
@@ -2763,7 +4028,7 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2763
4028
  name: p.step,
2764
4029
  durationMs: Math.round((p.duration ?? 1.4) * 1000),
2765
4030
  highlight: p.focus,
2766
- body: p.body,
4031
+ body: 段の説明(p),
2767
4032
  badge: p.badge,
2768
4033
  // 書いた段だけが欄を持つ。 空文字を置くと「書いた」 と「書いていない」 が同じ形になる
2769
4034
  ...(p.draw !== undefined ? { draw: p.draw, drawPos: 位置("animation", i, "draw") } : {}),
@@ -2799,9 +4064,82 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2799
4064
  // `states` を指す = どちらも「動きを書き始めた場所」 になる
2800
4065
  { states, phases, pos: phases.length > 0 ? 位置("animation") : 位置("states") }
2801
4066
  : undefined;
4067
+ // **別名は記法と同じ表で開く** (#2655 / #2657)。 開いた後は古い綴りが残らない。
4068
+ //
4069
+ // **開く語を 1 つずつ書く** = 表の値をそのまま広げると、持たない語が `undefined` の
4070
+ // まま欄に載り、後ろの `...(形 !== null ? ...)` を打ち消す (実測 = `type: "pie"` の
4071
+ // JSON が形を持たず、書かない時の形 (棒) の図になった)
4072
+ const 別名 = TYPE_ALIASES.get(json.type);
4073
+ const form = json.form ?? json.見せ方;
4074
+ const ticks = json.ticks ?? json.目盛り;
2802
4075
  return {
2803
4076
  title: json.title,
2804
- type: json.type,
4077
+ type: 別名?.type ?? (json.type as PresetType),
4078
+ ...(form !== undefined
4079
+ ? { form, formPos: 位置(json.form !== undefined ? "form" : "見せ方") }
4080
+ : {}),
4081
+ ...(ticks !== undefined
4082
+ ? { ticks: [...ticks], ticksPos: 位置(json.ticks !== undefined ? "ticks" : "目盛り") }
4083
+ : {}),
4084
+ ...(json.ganttToday !== undefined ? { ganttToday: json.ganttToday } : {}),
4085
+ ...(json.ganttTickLabels !== undefined ? { ganttTickLabels: [...json.ganttTickLabels] } : {}),
4086
+ ...(json.ganttBarEnd !== undefined ? { ganttBarEnd: json.ganttBarEnd } : {}),
4087
+ ...(json.ganttBarThickness !== undefined ? { ganttBarThickness: json.ganttBarThickness } : {}),
4088
+ ...(json.chartSeriesSkipMuted !== undefined ? { chartSeriesSkipMuted: json.chartSeriesSkipMuted } : {}),
4089
+ ...(json.chartLineSeries !== undefined ? { chartLineSeries: json.chartLineSeries } : {}),
4090
+ ...(json.chartPieCenterLabel !== undefined ? { chartPieCenterLabel: json.chartPieCenterLabel } : {}),
4091
+ ...(json.chartPieTableColumns !== undefined ? { chartPieTableColumns: json.chartPieTableColumns } : {}),
4092
+ ...(json.chartPieRingWidth !== undefined ? { chartPieRingWidth: json.chartPieRingWidth } : {}),
4093
+ ...(json.chartSlopePeriods !== undefined ? { chartSlopePeriods: json.chartSlopePeriods } : {}),
4094
+ ...(json.chartSlopeEmphasisIds !== undefined ? { chartSlopeEmphasisIds: json.chartSlopeEmphasisIds } : {}),
4095
+ ...(json.chartSlopeUnit !== undefined ? { chartSlopeUnit: json.chartSlopeUnit } : {}),
4096
+ ...(json.chartStackedPeriods !== undefined ? { chartStackedPeriods: json.chartStackedPeriods } : {}),
4097
+ ...(json.chartStackedRateId !== undefined ? { chartStackedRateId: json.chartStackedRateId } : {}),
4098
+ ...(json.chartStackedLegendPosition !== undefined ? { chartStackedLegendPosition: json.chartStackedLegendPosition } : {}),
4099
+ ...(json.chartGaugeValue !== undefined ? { chartGaugeValue: json.chartGaugeValue } : {}),
4100
+ ...(json.chartWaffleLegendPosition !== undefined ? { chartWaffleLegendPosition: json.chartWaffleLegendPosition } : {}),
4101
+ ...(json.funnelForm !== undefined ? { funnelForm: json.funnelForm } : {}),
4102
+ ...(json.funnelRate !== undefined ? { funnelRate: json.funnelRate } : {}),
4103
+ ...(json.quadrantPointLabelSide !== undefined ? { quadrantPointLabelSide: json.quadrantPointLabelSide } : {}),
4104
+ ...(json.treeNodeForm !== undefined ? { treeNodeForm: json.treeNodeForm } : {}),
4105
+ ...(json.treeSize !== undefined ? { treeSize: json.treeSize } : {}),
4106
+ ...(json.treeEdgeTone !== undefined ? { treeEdgeTone: json.treeEdgeTone } : {}),
4107
+ ...(json.treeEdgeHead !== undefined ? { treeEdgeHead: json.treeEdgeHead } : {}),
4108
+ ...(json.mindForm !== undefined ? { mindForm: json.mindForm } : {}),
4109
+ ...(json.mindSize !== undefined ? { mindSize: json.mindSize } : {}),
4110
+ ...(json.edgeEndGap !== undefined ? { edgeEndGap: json.edgeEndGap } : {}),
4111
+ ...(json.journeyForm !== undefined ? { journeyForm: json.journeyForm } : {}),
4112
+ ...(json.journeyLineForm !== undefined ? { journeyLineForm: json.journeyLineForm } : {}),
4113
+ ...(json.journeyLabels !== undefined ? { journeyLabels: json.journeyLabels } : {}),
4114
+ ...(json.journeySize !== undefined ? { journeySize: json.journeySize } : {}),
4115
+ ...(json.sequenceActorForm !== undefined ? { sequenceActorForm: json.sequenceActorForm } : {}),
4116
+ ...(json.sequenceLabelForm !== undefined ? { sequenceLabelForm: json.sequenceLabelForm } : {}),
4117
+ ...(json.sequenceReturnHead !== undefined ? { sequenceReturnHead: json.sequenceReturnHead } : {}),
4118
+ ...(json.sequenceKindLabels !== undefined ? { sequenceKindLabels: json.sequenceKindLabels } : {}),
4119
+ ...(json.sequenceRowGap !== undefined ? { sequenceRowGap: json.sequenceRowGap } : {}),
4120
+ ...(json.sequenceGutter !== undefined ? { sequenceGutter: json.sequenceGutter } : {}),
4121
+ ...(json.sequenceBandWidth !== undefined ? { sequenceBandWidth: json.sequenceBandWidth } : {}),
4122
+ ...(json.sequenceSelfLoop !== undefined ? { sequenceSelfLoop: json.sequenceSelfLoop } : {}),
4123
+ ...(json.figureCard !== undefined ? { figureCard: json.figureCard } : {}),
4124
+ ...(json.figureSize !== undefined ? { figureSize: json.figureSize } : {}),
4125
+ ...(json.terminalDecoration !== undefined ? { terminalDecoration: json.terminalDecoration } : {}),
4126
+ ...(json.cornerForm !== undefined ? { cornerForm: json.cornerForm } : {}),
4127
+ ...(json.figureKindLabel !== undefined ? { figureKindLabel: json.figureKindLabel } : {}),
4128
+ ...(json.figureInset !== undefined ? { figureInset: json.figureInset } : {}),
4129
+ ...(json.chartBarSize !== undefined ? { chartBarSize: json.chartBarSize } : {}),
4130
+ ...(json.chartBarShape !== undefined ? { chartBarShape: json.chartBarShape } : {}),
4131
+ ...(json.chartLineSize !== undefined ? { chartLineSize: json.chartLineSize } : {}),
4132
+ ...(json.chartPieSize !== undefined ? { chartPieSize: json.chartPieSize } : {}),
4133
+ ...(json.chartWaffleSize !== undefined ? { chartWaffleSize: json.chartWaffleSize } : {}),
4134
+ ...(json.chartSlopeLabelTemplate !== undefined ? { chartSlopeLabelTemplate: json.chartSlopeLabelTemplate } : {}),
4135
+ ...(json.chartSlopeSize !== undefined ? { chartSlopeSize: json.chartSlopeSize } : {}),
4136
+ ...(json.chartStackedLayout !== undefined ? { chartStackedLayout: json.chartStackedLayout } : {}),
4137
+ ...(json.chartGaugeSize !== undefined ? { chartGaugeSize: json.chartGaugeSize } : {}),
4138
+ ...(json.ganttSize !== undefined ? { ganttSize: json.ganttSize } : {}),
4139
+ ...(json.quadrantLabelPosition !== undefined ? { quadrantLabelPosition: json.quadrantLabelPosition } : {}),
4140
+ ...(json.quadrantSize !== undefined ? { quadrantSize: json.quadrantSize } : {}),
4141
+ ...(別名?.order !== undefined ? { order: 別名.order, orderPos: 位置("type") } : {}),
4142
+ ...(別名?.shape !== undefined ? { shape: 別名.shape, shapePos: 位置("type") } : {}),
2805
4143
  // 前後の空白を落としてから見る。 記法側 (`v05/parser.ts`) が `trim()` してから
2806
4144
  // 空かどうかを判定するため、 揃えないと **空白だけの値で入口ごとに図が変わる**
2807
4145
  // (記法は書かなかった扱い、 JSON は中身のない帯を描く。 Round 2 の指摘で実測)
@@ -2813,6 +4151,14 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2813
4151
  ...(json.axes && (json.axes.x !== undefined || json.axes.y !== undefined)
2814
4152
  ? { axes: json.axes, axesPos: 位置("axes") }
2815
4153
  : {}),
4154
+ // 区画の名前 (#2667)。 鍵は記法と同じ 4 語なので、描画側の綴りへここで直す
4155
+ ...(区画の名前を直す(json.regions) === undefined
4156
+ ? {}
4157
+ : { regions: 区画の名前を直す(json.regions) }),
4158
+ ...(json.legendFontSize !== undefined ? { legendFontSize: json.legendFontSize } : {}),
4159
+ ...(json.legendOffset !== undefined ? { legendOffset: json.legendOffset } : {}),
4160
+ ...(json.stageHeader !== undefined ? { stageHeader: json.stageHeader } : {}),
4161
+ ...(json.stageLayout !== undefined ? { stageLayout: json.stageLayout } : {}),
2816
4162
  actors,
2817
4163
  flow,
2818
4164
  animate,
@@ -2863,9 +4209,22 @@ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): Dsl
2863
4209
  directionPos: 位置("direction"),
2864
4210
  }
2865
4211
  : {}),
2866
- // 図の配色 (#1553)。 記法と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方がずれない。
2867
- // 読めない語は渡さない = 上流の型検査が語を絞っているので、ここに来るのは書き間違いだけ
2868
- ...(配色 !== null ? { palette: 配色 } : {}),
4212
+ // 箱を並べ替える軸 (#2655)。 向きと同じく英語で書くので、記法と同じ解決を通して日本語に直す。
4213
+ // 読めない語は渡さない = 公開 schema の語の一覧が絞っているので、ここに来るのは書き間違いだけ
4214
+ ...(並び順 !== null ? { order: 並び順, orderPos: 位置("order") } : {}),
4215
+ // 数を描く図の形 (#2657)。 記法と同じ解決を通す = 受ける語がずれない
4216
+ ...(形 !== null ? { shape: 形, shapePos: 位置("shape") } : {}),
4217
+ // 図の意匠 (#1553 / #2790)。 dragon の文書では `theme`、cdl の図へ載せる時は `palette` とする
4218
+ ...(選んだ意匠 !== null ? { theme: 選んだ意匠 } : {}),
4219
+ ...(legend && legend.length > 0 ? { legend } : {}),
4220
+ ...(意匠 !== null && 別名の意匠 !== null
4221
+ ? {
4222
+ themeAlsoPalettePos: {
4223
+ themeLine: 位置("theme").line,
4224
+ paletteLine: 位置("palette").line,
4225
+ },
4226
+ }
4227
+ : {}),
2869
4228
  pos: 位置(),
2870
4229
  };
2871
4230
  }