@cardenelabs/dragon 0.28.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 (66) hide show
  1. package/README.md +391 -16
  2. package/dist/index.cjs +13479 -7403
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +2360 -208
  5. package/dist/index.d.ts +2360 -208
  6. package/dist/index.js +13462 -7405
  7. package/dist/index.js.map +1 -1
  8. package/package.json +12 -13
  9. package/src/compile/actor-ids.ts +297 -0
  10. package/src/compile/actor-option-notice.ts +379 -0
  11. package/src/compile/actors.ts +103 -0
  12. package/src/compile/c4.ts +74 -0
  13. package/src/compile/chain.ts +93 -0
  14. package/src/compile/chart-fields.ts +193 -0
  15. package/src/compile/deep-rewrite.ts +30 -0
  16. package/src/compile/derived-source.ts +18 -0
  17. package/src/compile/direction.ts +102 -0
  18. package/src/compile/edge-option-notice.ts +112 -0
  19. package/src/compile/er-relation.ts +200 -0
  20. package/src/compile/flow.ts +54 -0
  21. package/src/compile/flowchart.ts +344 -0
  22. package/src/compile/funnel.ts +87 -0
  23. package/src/compile/gantt.ts +337 -0
  24. package/src/compile/generic.ts +696 -0
  25. package/src/compile/hierarchy.ts +92 -0
  26. package/src/compile/journey.ts +93 -0
  27. package/src/compile/kinds.ts +148 -0
  28. package/src/compile/lanes.ts +138 -0
  29. package/src/compile/metro.ts +271 -0
  30. package/src/compile/mind.ts +393 -0
  31. package/src/compile/node-title.ts +24 -0
  32. package/src/compile/notice.ts +141 -0
  33. package/src/compile/order.ts +62 -0
  34. package/src/compile/parts-lookup.ts +17 -0
  35. package/src/compile/parts.ts +2709 -0
  36. package/src/compile/placement-apply.ts +689 -0
  37. package/src/compile/placement.ts +15 -0
  38. package/src/compile/quadrant.ts +265 -0
  39. package/src/compile/record.ts +75 -0
  40. package/src/compile/row-marks.ts +128 -0
  41. package/src/compile/rows.ts +40 -0
  42. package/src/compile/sequence.ts +305 -0
  43. package/src/compile/slug.ts +37 -0
  44. package/src/compile/stages.ts +247 -0
  45. package/src/compile/subtitle.ts +35 -0
  46. package/src/compile/swimlane-marks.ts +87 -0
  47. package/src/compile/swimlane.ts +82 -0
  48. package/src/compile/timeline.ts +213 -0
  49. package/src/compile/topology.ts +44 -0
  50. package/src/compile/tree.ts +69 -0
  51. package/src/compile/value-chart.ts +337 -0
  52. package/src/compile/values.ts +432 -0
  53. package/src/compile/word-state.ts +82 -0
  54. package/src/compile.ts +1806 -6971
  55. package/src/focus.ts +2 -2
  56. package/src/index.ts +42 -9
  57. package/src/json-line-map.ts +30 -0
  58. package/src/json-parser.ts +1760 -168
  59. package/src/keywords.ts +251 -17
  60. package/src/near-name.ts +42 -0
  61. package/src/notation-lint.ts +41 -64
  62. package/src/relative-pos.ts +71 -2
  63. package/src/schema.ts +1 -1
  64. package/src/schemas/diagram.json +680 -23
  65. package/src/types.ts +337 -36
  66. package/src/v05/parser.ts +2553 -354
@@ -6,7 +6,7 @@
6
6
  * で確実に生成できるよう、 flat な object array を優先した shape になっている。
7
7
  *
8
8
  * 使い方 (LLM):
9
- * 1. `packages/dragon/schemas/diagram.json` の JSON Schema を LLM の tool schema に注入
9
+ * 1. `packages/dragon/src/schemas/diagram.json` の JSON Schema を LLM の tool schema に注入
10
10
  * 2. LLM が JSON を返す
11
11
  * 3. `jsonToDiagram(json)` で CdlDiagram に変換
12
12
  * 4. validation error は throw、 retry loop で LLM に修正させる
@@ -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,
@@ -34,15 +35,69 @@ import {
34
35
  部品の組の表,
35
36
  つまみの表,
36
37
  EVENT_KINDS,
38
+ EVENT_TARGET_KEYS,
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,
37
76
  type 図形の定義,
38
77
  } from "./v05/parser";
39
- // 図の配色 (#1553)。 記法の読み手と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方が
78
+ // 図の意匠 (#1553 / #2790)。 記法の読み手と同じ解決を通す = 別名の受け方が
40
79
  // 記法と JSON でずれない
41
- import { resolvePalette } from "./keywords";
42
- 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";
43
97
  import type { CompileToCdlOpts } from "./compile";
44
98
  import type {
45
99
  CdlDiagram,
100
+ CdlLegendItem,
46
101
  NodeKind,
47
102
  Tone,
48
103
  EdgeStyle,
@@ -56,6 +111,8 @@ import type {
56
111
  import { extractIdentifiers, parseFormula } from "@cardenelabs/cdl";
57
112
  import type {
58
113
  DslDocument,
114
+ DslStageHeader,
115
+ DslRegions,
59
116
  DslActor,
60
117
  DslStep,
61
118
  DslAnimate,
@@ -71,7 +128,18 @@ import type {
71
128
  DslDynShape,
72
129
  } from "./types";
73
130
  import { checkValueExpression, isValueName, valueNameIssue } from "./value-syntax";
131
+ import { 近い名前 } from "./near-name";
132
+ import { 書いた場所の鍵, type 書いた行の表 } from "./json-line-map";
74
133
  import { compileToCdl } from "./compile";
134
+ import {
135
+ RELATIVE_DIRECTIONS,
136
+ findRelativeProblems,
137
+ type RelativeDirection,
138
+ type RelativePos,
139
+ } from "./relative-pos";
140
+
141
+ /** 古い綴りで書いた図種 (#2655 / #2657)。 受ける値は `TYPE_ALIASES` が SSOT */
142
+ type 図種の別名 = "solidity" | DslShape;
75
143
 
76
144
  /**
77
145
  * LLM 向け JSON DSL の入力 shape。 YAML DSL と 1:1 対応、 top-level は flat な object。
@@ -79,12 +147,84 @@ import { compileToCdl } from "./compile";
79
147
  export interface DragonJson {
80
148
  /** 図の title (必須) */
81
149
  title: string;
82
- /** preset type (必須): sequence / flow / swimlane / er / state / topology / solidity / gantt / class / pie / c4 / mind */
83
- 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"];
84
224
  /**
85
225
  * 図表の箱の上に出す小見出し (optional)。 記法の最上位 `eyebrow:` と同じ (#1247)。
86
226
  *
87
- * 効くのは図全体を 1 箱にする図種 (`pie` / `bar` / `line` / `funnel` / `tree` / `journey` /
227
+ * 効くのは図全体を 1 箱にする図種 (`chart` / `funnel` / `tree` / `journey` /
88
228
  * `quadrant` / `mind` / `gantt`) だけ。 箱ごとに分かれる図種では相手が決まらないため、
89
229
  * 組み立て側が知らせを出す。 そちらは `actors[].eyebrow` に書く。
90
230
  */
@@ -98,6 +238,19 @@ export interface DragonJson {
98
238
  * 他の図種には軸が無いため、書かれていたら組み立て側が知らせる。
99
239
  */
100
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"];
101
254
  /** 登場人物 (必須): 文字列 or { name, kind, ... } object */
102
255
  actors: (string | JsonActor)[];
103
256
  /** flow step 配列 (必須): { from, to, label, ... } */
@@ -174,6 +327,8 @@ export interface DragonJson {
174
327
  x?: number;
175
328
  width?: number;
176
329
  label?: string;
330
+ /** 路線図の担当の名札で、名前の下へ出す補足。 */
331
+ subtitle?: string;
177
332
  contain?: boolean;
178
333
  lifeline?: boolean;
179
334
  /**
@@ -199,16 +354,34 @@ export interface DragonJson {
199
354
  relations?: RelationFocus;
200
355
  /** 図の並ぶ向き (#1494)。 記法の最上位 `direction:` と同じ。 JSON は英語の語で書く */
201
356
  direction?: "vertical" | "horizontal";
202
- /**
203
- * 図の配色 (#1553)。 記法の最上位 `palette:` と同じ。
204
- *
205
- * 名前だけを図に載せる = cdl は色を持たず、値は `cdl-theme.css` が決める。
206
- * ER 図とクラス図は書かなくても `kinari` (生成りに茶) になる。 どちらも箱の作りが同じ
207
- * (行頭の印 + 左に名前 + 右に型) で、名前と型が離れて並ぶため、行を横に追う目印
208
- * (行の縞) が要る。 縞の色は配色からしか来ないので、既定が無いと縞が箱の面と同じ色に
209
- * 落ちて 1 本も出ない。 書き手が `palette:` を書いた時はそちらが勝つ。
210
- */
211
- 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[];
367
+ }
368
+
369
+ /**
370
+ * 位置を他の要素からの相対で書く指定の、JSON 側の形 (#2039)。
371
+ *
372
+ * 記法の `位置: Web の右 200` を 3 つの欄に開いたもの。 内側の値は `RelativePos` と同じで、
373
+ * 向きの語も同じ 4 つ (`right` / `left` / `above` / `below`) を使う。
374
+ *
375
+ * **向きの語を並べ直さない**。 `RelativeDirection` をそのまま使うことで、本文側に向きを
376
+ * 足した時に JSON 側が黙って取り残される形を作らない。
377
+ */
378
+ export interface JsonRelativePos {
379
+ /** 基準にする相手の名前。 `actors:` に書かれた名前をそのまま書く */
380
+ anchor: string;
381
+ /** 基準からどちら側に置くか */
382
+ dir: RelativeDirection;
383
+ /** 相手との間隔。 書かなければ `RELATIVE_GAP_DEFAULT` */
384
+ gap?: number;
212
385
  }
213
386
 
214
387
  export interface JsonActor {
@@ -239,14 +412,31 @@ export interface JsonActor {
239
412
  renderOffsetX?: number | string;
240
413
  renderOffsetY?: number | string;
241
414
  /**
242
- * CAR-1657 unified syntax = 既存 NodeKind (28 個) に加えて parts identifier (arc-gauge 等) を
243
- * accept する。 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
415
+ * CAR-1657 unified syntax = 既存 NodeKind (一覧は `v05/parser.ts` の `NODE_KIND_VALID`) に
416
+ * 加えて parts identifier (arc-gauge 等) を accept する。
417
+ * 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
244
418
  * LLM structured output の typing 制約を緩めるため union に string 追加。
245
419
  * `string & {}` = NodeKind の候補を IDE 補完で提示しつつ任意 string も許容する idiom。
246
420
  * 素の `NodeKind | string` は no-redundant-type-constituents に抵触し補完も潰れる (#865)。
247
421
  */
248
422
  kind?: NodeKind | (string & {});
249
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;
250
440
  eyebrow?: string;
251
441
  value?: string;
252
442
  /**
@@ -257,6 +447,8 @@ export interface JsonActor {
257
447
  */
258
448
  previous?: string;
259
449
  rows?: string[];
450
+ /** 段階ごとの箱で、この箱を入れる段階。 記法の `stage` / `段階` と同じ (#2797)。 */
451
+ stage?: string;
260
452
  lane?: string;
261
453
  stack?: number;
262
454
  initial?: boolean;
@@ -266,6 +458,17 @@ export interface JsonActor {
266
458
  * と同じで、読み方は `DslActor.layoutPos` が持つ。
267
459
  */
268
460
  pos?: LayoutPos;
461
+ /**
462
+ * 位置を他の要素からの相対で書く指定 (#2039)。 記法の `位置: Web の右 200` と同じ。
463
+ *
464
+ * 記法は 1 本の文字列に「誰の」「どちら側に」「どれだけ離して」 を詰めるが、JSON では
465
+ * 3 つの欄に分ける。 文字列を組み立てて渡す形にすると、書く側が記法の並び順と区切りを
466
+ * 覚える必要が出る = JSON を書くのは主に LLM で、読み取りの規則は欄の名前で示す方が短い。
467
+ *
468
+ * 組み立ての段階で基準の実座標から `posX` / `posY` に直すため、解決した後は座標を直接
469
+ * 書いた時と同じ経路を通る。 `pos` (配置後のずらし幅) とは別の欄。
470
+ */
471
+ posRel?: JsonRelativePos;
269
472
  /**
270
473
  * CAR-1657 parts state override (kind = parts identifier 時のみ有効)。
271
474
  * LLM JSON DSL では nested 明示 = `{ "state": { "v": 50 } }` が natural、 human 側の
@@ -301,14 +504,22 @@ export interface JsonActor {
301
504
  * (記法側と同じ扱い)。
302
505
  */
303
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];
304
514
  /**
305
- * 体験の道筋 (`type: journey`) で、その段階が起きる場所 (#1294)。 記法の `touchpoint:` と同じ。
515
+ * ユーザージャーニー (`type: journey`) で、その段階が起きる場所 (#1294)。 記法の `touchpoint:` と同じ。
306
516
  */
307
517
  touchpoint?: string;
308
518
  /**
309
- * 体験の道筋 (`type: journey`) で、その段階の改善の余地 (#1294)。 記法の `opportunity:` と同じ。
519
+ * ユーザージャーニー (`type: journey`) で、その段階の改善の余地 (#1294)。 記法の `opportunity:` と同じ。
310
520
  */
311
521
  opportunity?: string;
522
+ opportunityPosition?: DslActor["opportunityPosition"];
312
523
  /**
313
524
  * 箱を置く絶対座標と大きさ (#1294)。 記法の `posX:` / `posY:` / `posW:` / `posH:` と同じ。
314
525
  *
@@ -325,14 +536,33 @@ export interface JsonActor {
325
536
  * 読めない項目名として知らせる)。
326
537
  */
327
538
  scale?: number;
539
+ /**
540
+ * 見本が持つ段を使うか (#2125)。 記法の `{ kind: wave-gauge, phase: false }` と同じ。
541
+ *
542
+ * 既定 (書かない) は見本の段を書いた段と番号ごとに並べて再生する。 `false` を書くと
543
+ * 見本の段の中身 (点灯と値の変化) を空にし、値は書いた段だけが動かす。
544
+ *
545
+ * 見本 (parts) にしか効かない。 中身は状態の上書き (`state`) に `phase` として入る =
546
+ * `"state": { "phase": false }` と同じ所へ届く。 記法が中括弧の中に 1 つの名前として
547
+ * 書ける以上、JSON でも同じ名前で書けるようにする (入口で書き方が分かれない)。
548
+ */
549
+ phase?: boolean;
328
550
  /** 行頭の印 (#1466)。 行と対で読む。 語の意味は図の種類が決める */
329
- marks?: string[];
551
+ marks?: Array<string | { mark: string; tone?: "primary" | "muted" }>;
330
552
  }
331
553
 
332
554
  /** 2 軸で仕分ける図の軸の名前 (#1294)。 記法の `axes:` と同じ形 */
333
555
  export interface JsonAxes {
334
- x?: { left?: string; right?: string };
335
- 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;
336
566
  }
337
567
 
338
568
  export interface JsonStep {
@@ -342,6 +572,8 @@ export interface JsonStep {
342
572
  sub?: string;
343
573
  /** 矢印がどの辺から出るか (#1385)。 記法の `side:` と同じ */
344
574
  side?: "top" | "right" | "bottom" | "left";
575
+ fromSide?: "top" | "right" | "bottom" | "left";
576
+ toSide?: "top" | "right" | "bottom" | "left";
345
577
  /**
346
578
  * 矢印の先の形 (#1462)。 記法の `{ head: triangle }` と同じ。
347
579
  *
@@ -353,7 +585,7 @@ export interface JsonStep {
353
585
  tailHead?: EdgeHead;
354
586
  headFill?: EdgeHeadFill;
355
587
  tailHeadFill?: EdgeHeadFill;
356
- /** クラス図の出どころ側の多重度 (#1771)。 記法の `{ tailSub: "1" }` と同じ */
588
+ /** UML 関係の出どころ側の多重度 (#1771 / #2783)。記法の `{ tailSub: "1" }` と同じ */
357
589
  tailSub?: string;
358
590
  /**
359
591
  * 辺の役目 (cdl#618)。 記法の `{ role: main }` と同じ。
@@ -363,7 +595,7 @@ export interface JsonStep {
363
595
  role?: "main";
364
596
  /** 名前の下地を敷くか (cdl#618)。 記法の `{ labelPlate: false }` と同じ。 既定は敷く */
365
597
  labelPlate?: boolean;
366
- /** クラス図の関係の語 (#1466)。 書くと端の形 / 塗り / 線種がまとめて決まる */
598
+ /** record の UML 関係の語 (#1466 / #2783)。書くと端の形 / 塗り / 線種がまとめて決まる */
367
599
  relation?: ClassRelationType;
368
600
  /** 順序図の言づての種類 (#1466)。 `call` / `return` / `fire` */
369
601
  kind?: SequenceMessageKind;
@@ -385,6 +617,13 @@ export interface JsonStep {
385
617
  dashOffsetBind?: string;
386
618
  labelOffsetX?: number;
387
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"];
388
627
  /** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
389
628
  overlay?: boolean;
390
629
  /**
@@ -408,6 +647,13 @@ export interface JsonPhase {
408
647
  focus?: string[];
409
648
  /** body 説明文 */
410
649
  body?: string;
650
+ /**
651
+ * 説明文の別名 (#2621)。 記法が `description:` を受けるので JSON でも受ける。
652
+ *
653
+ * **正の語は `body`**。 両方書いた段は `body` を採る = 正の語を優先すると決めておくと、
654
+ * 読み手が「どちらが効くのか」 を実装を読まずに判断できる。
655
+ */
656
+ description?: string;
411
657
  /** badge label */
412
658
  badge?: string;
413
659
  /**
@@ -451,11 +697,13 @@ export interface JsonDslError {
451
697
  * 受け付ける種類 (`kind`)。 **記法側と同じ集合を使う** (`v05/parser.ts` の `NODE_KIND_VALID`)。
452
698
  *
453
699
  * ここに無い値は見本 (parts) の名前とみなし、`partId` へ退避して箱を `actor` に倒す
454
- * (CAR-1657 の unified syntax)。 つまりこの集合が小さいほど、**書いた種類が黙って消える**。
700
+ * (CAR-1657 の unified syntax)。 つまりこの集合が小さいほど、**書いた種類が箱の見た目に効かなくなる**。
701
+ * 部品を持つ一覧を渡した組み立てでは一覧に無い名前を知らせる (`part-not-found`、#2113) が、
702
+ * 一覧を渡さない組み立てと部品を持たない一覧の組み立てでは `console.warn` にしか残らない。
455
703
  *
456
704
  * 以前はここに一覧を写していたため、記法に種類が増えても JSON 経路が古い一覧のまま
457
705
  * 見本の名前として扱った (実測 = 記法が知る 108 種のうち 77 種が該当、`shape-*` は全滅)。
458
- * カタログに無い名前は `console.warn` にしか残らず `onNotice` を呼ばないため、
706
+ * 当時はカタログに無い名前が `console.warn` にしか残らず `onNotice` を呼ばなかったため、
459
707
  * 画面には何も出ないまま `actor` の箱が描かれていた (#1293)。
460
708
  *
461
709
  * 型が 2 箇所に散らばると、必ずどちらかが古くなる。 `VALID_PRESETS` (図種) と同じ扱いにする。
@@ -470,6 +718,13 @@ const VALID_KIND_SET: ReadonlySet<string> = NODE_KIND_VALID;
470
718
  */
471
719
  const VALID_PRESETS: readonly PresetType[] = [...PRESET_TYPES];
472
720
 
721
+ /**
722
+ * 受け付ける図種の別名 (#2655)。 **記法側と同じ表を使う** (`v05/parser.ts` の `TYPE_ALIASES`)。
723
+ *
724
+ * 写すと片方だけ古くなる。 図種の一覧を 1 か所に寄せたのと同じ理由。
725
+ */
726
+ const VALID_ALIASES: readonly string[] = [...TYPE_ALIASES.keys()];
727
+
473
728
  /**
474
729
  * 受け付ける項目の一覧 (#1295)。 **知らない項目を誤りにする判定と、公開 schema との
475
730
  * 突き合わせが、どちらもここを見る**。
@@ -487,8 +742,70 @@ export const ACCEPTED_KEYS = {
487
742
  root: [
488
743
  "title",
489
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",
490
806
  "eyebrow",
491
807
  "axes",
808
+ "regions",
492
809
  "actors",
493
810
  "flow",
494
811
  "states",
@@ -514,18 +831,45 @@ export const ACCEPTED_KEYS = {
514
831
  "relations",
515
832
  // 図の並ぶ向き (#1494)
516
833
  "direction",
517
- // 図の配色 (#1553)
834
+ // 箱を並べ替える軸 (#2655)
835
+ "order",
836
+ // 数を描く図の形 (#2657)
837
+ "shape",
838
+ // 図の意匠とその別名 (#1553 / #2790)
839
+ "theme",
518
840
  "palette",
841
+ "legend",
842
+ "legendFontSize",
843
+ "legendOffset",
844
+ "stageHeader",
845
+ "stageLayout",
519
846
  ],
520
847
  actor: [
521
848
  "name",
522
849
  "kind",
523
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",
524
867
  "eyebrow",
525
868
  "value",
526
869
  // 前の時点の値 (#1450)。 内訳の変化を帯で示す図と、値 1 つを大きく示す図が読む
527
870
  "previous",
528
871
  "rows",
872
+ "stage",
529
873
  "lane",
530
874
  "stack",
531
875
  "initial",
@@ -534,8 +878,14 @@ export const ACCEPTED_KEYS = {
534
878
  "color",
535
879
  "owner",
536
880
  "end",
881
+ "startLabel",
882
+ "milestone",
883
+ "emphasis",
884
+ "at",
885
+ "点の位置",
537
886
  "touchpoint",
538
887
  "opportunity",
888
+ "opportunityPosition",
539
889
  "posX",
540
890
  "posY",
541
891
  "posW",
@@ -557,6 +907,10 @@ export const ACCEPTED_KEYS = {
557
907
  "renderOffsetY",
558
908
  // 行頭の印 (#1466)。 行ごとに 1 つ、図の種類ごとの語で書く
559
909
  "marks",
910
+ // 位置を他の要素からの相対で書く指定 (#2039)
911
+ "posRel",
912
+ // 見本が持つ段を使うか (#2125)
913
+ "phase",
560
914
  ],
561
915
  step: [
562
916
  "from",
@@ -565,13 +919,15 @@ export const ACCEPTED_KEYS = {
565
919
  "sub",
566
920
  // 矢印がどの辺から出るか (#1385)
567
921
  "side",
922
+ "fromSide",
923
+ "toSide",
568
924
  // 矢印の先の形 (#1462)
569
925
  "head",
570
926
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
571
927
  "tailHead",
572
928
  "headFill",
573
929
  "tailHeadFill",
574
- // クラス図の出どころ側の多重度 (#1771)
930
+ // UML 関係の出どころ側の多重度 (#1771 / #2783)
575
931
  "tailSub",
576
932
  "relation",
577
933
  // 辺の役目と名前の下地 (cdl#618)
@@ -588,20 +944,32 @@ export const ACCEPTED_KEYS = {
588
944
  "dashOffsetBind",
589
945
  "labelOffsetX",
590
946
  "labelOffsetY",
947
+ "posX1",
948
+ "posY1",
949
+ "posX2",
950
+ "posY2",
951
+ "waypoints",
952
+ "labelCollision",
953
+ "labelSize",
591
954
  "overlay",
592
955
  "pos",
593
956
  // 部品の端で繋ぐ要素の id (#1979)
594
957
  "fromPartNode",
595
958
  "toPartNode",
596
959
  ],
597
- phase: ["step", "duration", "focus", "body", "badge", "tween", "set", "draw", "drawRatio"],
598
- viewport: ["width", "height", "scale", "laneWidth", "gap", "laneGap", "nodeGap", "labelMargin"],
599
- 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"],
600
964
  group: ["label", "lanes"],
601
965
  axes: ["x", "y"],
602
- axesX: ["left", "right"],
603
- axesY: ["bottom", "top"],
966
+ axesX: ["left", "right", "label", "direction"],
967
+ axesY: ["bottom", "top", "label", "direction"],
968
+ // 区画を指す語は項目の欄と同じ 4 語 (#2667)
969
+ regions: ["左上", "右上", "左下", "右下"],
604
970
  layoutPos: ["x", "y"],
971
+ // 位置を他の要素からの相対で書く指定 (#2039)。 間隔は書かなくてよい
972
+ posRel: ["anchor", "dir", "gap"],
605
973
  } as const satisfies Record<string, readonly string[]>;
606
974
 
607
975
  /** 受ける項目を持つ階層の名前 */
@@ -643,8 +1011,18 @@ export type 欄の型 =
643
1011
  | "線種"
644
1012
  | "辺"
645
1013
  | "端の形"
1014
+ // 相対で置く時の向き (#2039)。 書けば必ず要る = 向きが無いとどちら側か決まらない
1015
+ | "必須の向き"
646
1016
  | "色か色番号"
647
1017
  | "描くもの"
1018
+ | "意匠"
1019
+ | "図の形"
1020
+ | "ガントの帯の終わり"
1021
+ | "ガントの帯の厚み"
1022
+ | "札の角"
1023
+ | "四象限の区画名位置"
1024
+ | "線札の衝突"
1025
+ | "主役"
648
1026
  | "必須の図種"
649
1027
  | "object"
650
1028
  | "並び"
@@ -655,8 +1033,70 @@ export const 欄の型表 = {
655
1033
  root: {
656
1034
  title: "必須の非空文字列",
657
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",
658
1097
  eyebrow: "文字列",
659
1098
  axes: "object",
1099
+ regions: "object",
660
1100
  actors: "必須の非空の並び",
661
1101
  flow: "必須の並び",
662
1102
  states: "object",
@@ -682,17 +1122,44 @@ export const 欄の型表 = {
682
1122
  relations: "非空の文字列",
683
1123
  // 図の並ぶ向き (#1494)
684
1124
  direction: "非空の文字列",
685
- // 図の配色 (#1553)
686
- 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",
687
1137
  },
688
1138
  actor: {
689
1139
  name: "必須の非空文字列",
690
1140
  kind: "非空の文字列",
691
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: "数",
692
1158
  eyebrow: "文字列",
693
1159
  value: "文字列",
694
1160
  previous: "文字列",
695
1161
  rows: "文字列の並び",
1162
+ stage: "非空の文字列",
696
1163
  lane: "文字列",
697
1164
  stack: "数",
698
1165
  initial: "真偽",
@@ -701,8 +1168,14 @@ export const 欄の型表 = {
701
1168
  color: "色か色番号",
702
1169
  owner: "文字列",
703
1170
  end: "文字列",
1171
+ startLabel: "文字列",
1172
+ milestone: "真偽",
1173
+ emphasis: "主役",
1174
+ at: "並び",
1175
+ 点の位置: "並び",
704
1176
  touchpoint: "文字列",
705
1177
  opportunity: "文字列",
1178
+ opportunityPosition: "文字列",
706
1179
  posX: "数",
707
1180
  posY: "数",
708
1181
  posW: "数",
@@ -723,7 +1196,11 @@ export const 欄の型表 = {
723
1196
  renderOffsetX: "数か文字列",
724
1197
  renderOffsetY: "数か文字列",
725
1198
  // 行頭の印 (#1466)
726
- marks: "文字列の並び",
1199
+ marks: "並び",
1200
+ // 相対で置く指定 (#2039)。 中身の形は `validateRelativePos` が見る
1201
+ posRel: "object",
1202
+ // 見本が持つ段を使うか (#2125)
1203
+ phase: "真偽",
727
1204
  },
728
1205
  step: {
729
1206
  from: "必須の文字列",
@@ -732,12 +1209,14 @@ export const 欄の型表 = {
732
1209
  sub: "文字列",
733
1210
  // 矢印がどの辺から出るか (#1385)
734
1211
  side: "辺",
1212
+ fromSide: "辺",
1213
+ toSide: "辺",
735
1214
  head: "端の形",
736
1215
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
737
1216
  tailHead: "端の形",
738
1217
  headFill: "非空の文字列",
739
1218
  tailHeadFill: "非空の文字列",
740
- // クラス図の出どころ側の多重度 (#1771)
1219
+ // UML 関係の出どころ側の多重度 (#1771 / #2783)
741
1220
  tailSub: "文字列",
742
1221
  relation: "非空の文字列",
743
1222
  // 辺の役目と名前の下地 (cdl#618)
@@ -754,6 +1233,13 @@ export const 欄の型表 = {
754
1233
  dashOffsetBind: "非空の文字列",
755
1234
  labelOffsetX: "数",
756
1235
  labelOffsetY: "数",
1236
+ posX1: "数",
1237
+ posY1: "数",
1238
+ posX2: "数",
1239
+ posY2: "数",
1240
+ waypoints: "並び",
1241
+ labelCollision: "線札の衝突",
1242
+ labelSize: "object",
757
1243
  overlay: "真偽",
758
1244
  pos: "object",
759
1245
  // 部品の端で繋ぐ要素の id (#1979)。 空の id は要素を指せないので受けない
@@ -764,7 +1250,9 @@ export const 欄の型表 = {
764
1250
  step: "必須の非空文字列",
765
1251
  duration: "数",
766
1252
  focus: "文字列の並び",
1253
+ // 説明は正の語と別名のどちらでも同じ型。 表の欄は `ACCEPTED_KEYS.phase` と揃える (#2621)
767
1254
  body: "文字列",
1255
+ description: "文字列",
768
1256
  badge: "文字列",
769
1257
  tween: "object",
770
1258
  set: "object",
@@ -774,6 +1262,7 @@ export const 欄の型表 = {
774
1262
  viewport: {
775
1263
  width: "数",
776
1264
  height: "数",
1265
+ contentOffsetX: "数",
777
1266
  scale: "数",
778
1267
  laneWidth: "数",
779
1268
  gap: "数",
@@ -785,16 +1274,20 @@ export const 欄の型表 = {
785
1274
  x: "数",
786
1275
  width: "数",
787
1276
  label: "文字列",
1277
+ subtitle: "文字列",
788
1278
  contain: "真偽",
789
1279
  lifeline: "真偽",
790
1280
  pos: "object",
791
1281
  },
792
1282
  group: { label: "文字列", lanes: "文字列の並び" },
793
1283
  axes: { x: "object", y: "object" },
794
- axesX: { left: "文字列", right: "文字列" },
795
- axesY: { bottom: "文字列", top: "文字列" },
1284
+ axesX: { left: "文字列", right: "文字列", label: "文字列", direction: "真偽" },
1285
+ axesY: { bottom: "文字列", top: "文字列", label: "文字列", direction: "真偽" },
1286
+ regions: { 左上: "文字列", 右上: "文字列", 左下: "文字列", 右下: "文字列" },
796
1287
  // 位置は書けば x と y の両方が要る。 片方だけでは寄せ幅が決まらない
797
1288
  layoutPos: { x: "必須の数", y: "必須の数" },
1289
+ // 相対で置く指定 (#2039)。 相手と向きは必ず要り、間隔は書かなければ既定で置く
1290
+ posRel: { anchor: "必須の非空文字列", dir: "必須の向き", gap: "数" },
798
1291
  } as const satisfies {
799
1292
  [層 in 階層]: { [欄 in (typeof ACCEPTED_KEYS)[層][number]]: 欄の型 };
800
1293
  };
@@ -839,6 +1332,8 @@ function JSONの色名か(v: string): boolean {
839
1332
  export const 見本にしか効かない欄 = [
840
1333
  "state",
841
1334
  "scale",
1335
+ // 見本が持つ段を使うか (#2125)。 普通の箱は自分の段を持たないので効かない
1336
+ "phase",
842
1337
  ] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
843
1338
 
844
1339
  /**
@@ -860,10 +1355,28 @@ export const 見本にしか効かない欄 = [
860
1355
  */
861
1356
  export const 見本に効かない欄 = [
862
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",
863
1371
  "owner",
864
1372
  "end",
1373
+ "startLabel",
1374
+ "milestone",
1375
+ "emphasis",
865
1376
  "touchpoint",
866
1377
  "opportunity",
1378
+ "opportunityPosition",
1379
+ "stage",
867
1380
  // 箱の中に描く図形 (#1374)。 見本は自分の形を持つため、外から図形を差し替えられない
868
1381
  "shape",
869
1382
  // その箱を出すかどうかの条件 (#1381)。 見本は自分の出方を持つ
@@ -962,6 +1475,36 @@ function 値を検査(
962
1475
  if (v === undefined) return;
963
1476
  if (typeof v !== "boolean") 型違い("true or false if present");
964
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;
965
1508
  case "色":
966
1509
  if (v === undefined) return;
967
1510
  if (typeof v !== "string") {
@@ -997,6 +1540,17 @@ function 値を検査(
997
1540
  });
998
1541
  }
999
1542
  return;
1543
+ case "必須の向き":
1544
+ // 受ける語は記法と同じ一覧を見る (`RELATIVE_DIRECTIONS`)。 写すと向きが増えた時に
1545
+ // 片方だけ古くなる
1546
+ if (typeof v !== "string" || !(RELATIVE_DIRECTIONS as readonly string[]).includes(v)) {
1547
+ errors.push({
1548
+ path,
1549
+ message: `${名前} must be one of: ${RELATIVE_DIRECTIONS.join(", ")}`,
1550
+ hint: typeof v === "string" ? `got "${v}"` : `got ${v === undefined ? "nothing" : typeof v}`,
1551
+ });
1552
+ }
1553
+ return;
1000
1554
  case "端の形":
1001
1555
  if (v === undefined) return;
1002
1556
  // 受ける語は記法と同じ一覧を見る (`EDGE_HEAD_VALUES`)。 写すと描画側が形を増やした時に
@@ -1020,6 +1574,28 @@ function 値を検査(
1020
1574
  });
1021
1575
  }
1022
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;
1023
1599
  case "色か色番号":
1024
1600
  if (v === undefined) return;
1025
1601
  if (typeof v !== "string") {
@@ -1046,10 +1622,13 @@ function 値を検査(
1046
1622
  }
1047
1623
  return;
1048
1624
  case "必須の図種":
1049
- 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
+ ) {
1050
1629
  errors.push({
1051
1630
  path,
1052
- message: `${名前} must be one of: ${VALID_PRESETS.join(", ")}`,
1631
+ message: `${名前} must be one of: ${[...VALID_PRESETS, ...VALID_ALIASES].join(", ")}`,
1053
1632
  hint: typeof v === "string" ? `got "${v}"` : undefined,
1054
1633
  });
1055
1634
  }
@@ -1086,48 +1665,6 @@ function 表で検査(
1086
1665
  }
1087
1666
  }
1088
1667
 
1089
- /**
1090
- * 綴り違いの候補を返す (#1295)。
1091
- *
1092
- * 「知らない項目です」 だけだと、`animations` と書いた人は正しい綴りを探しに行く必要がある。
1093
- * 1 文字の違い (足りない / 多い / 入れ替わり / 別の字) までを候補とする。
1094
- *
1095
- * 遠い名前は勧めない。 無関係な項目名を勧めると、書いた人がそちらへ直して二度手間になる。
1096
- */
1097
- function 近い項目名(key: string, 候補: readonly string[]): string | undefined {
1098
- const 小文字 = key.toLowerCase();
1099
- let 最短: { 名: string; 距離: number } | undefined;
1100
- for (const c of 候補) {
1101
- const d = 編集距離(小文字, c.toLowerCase(), 2);
1102
- if (d <= 2 && (最短 === undefined || d < 最短.距離)) 最短 = { 名: c, 距離: d };
1103
- }
1104
- return 最短?.名;
1105
- }
1106
-
1107
- /**
1108
- * 2 つの語の編集距離 (上限付き)。
1109
- *
1110
- * 上限を持つのは、長い語どうしで表を全部埋めないため。 上限を超えた時点で打ち切る。
1111
- */
1112
- function 編集距離(a: string, b: string, 上限: number): number {
1113
- if (Math.abs(a.length - b.length) > 上限) return 上限 + 1;
1114
- let 前 = Array.from({ length: b.length + 1 }, (_, i) => i);
1115
- for (let i = 1; i <= a.length; i += 1) {
1116
- const 今: number[] = [i];
1117
- let 行の最小 = i;
1118
- for (let j = 1; j <= b.length; j += 1) {
1119
- const 費用 = a[i - 1] === b[j - 1] ? 0 : 1;
1120
- const v = Math.min(今[j - 1]! + 1, 前[j]! + 1, 前[j - 1]! + 費用);
1121
- 今.push(v);
1122
- if (v < 行の最小) 行の最小 = v;
1123
- }
1124
- // その行の最小が上限を超えたら、以降どう進んでも上限以下にはならない
1125
- if (行の最小 > 上限) return 上限 + 1;
1126
- 前 = 今;
1127
- }
1128
- return 前[b.length]!;
1129
- }
1130
-
1131
1668
  /**
1132
1669
  * 知らない項目を誤りとして積む (#1295)。
1133
1670
  *
@@ -1139,7 +1676,7 @@ function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDsl
1139
1676
  const 受ける = ACCEPTED_KEYS[層] as readonly string[];
1140
1677
  for (const key of Object.keys(v)) {
1141
1678
  if (受ける.includes(key)) continue;
1142
- const 候補 = 近い項目名(key, 受ける);
1679
+ const 候補 = 近い名前(key, 受ける);
1143
1680
  errors.push({
1144
1681
  path: `${path}.${key}`,
1145
1682
  message: `unknown key "${key}"`,
@@ -1148,10 +1685,6 @@ function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDsl
1148
1685
  }
1149
1686
  }
1150
1687
 
1151
- /**
1152
- * shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
1153
- * fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
1154
- */
1155
1688
  /**
1156
1689
  * CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` の型 check helper。 finite number pair を必須にし、
1157
1690
  * `NaN` / `Infinity` / non-number は reject する (組み立ての出口 `applyLayoutOffsets` が配置後の位置に足すため)。
@@ -1167,6 +1700,77 @@ function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): vo
1167
1700
  表で検査(p, "layoutPos", path, "pos", errors);
1168
1701
  }
1169
1702
 
1703
+ /**
1704
+ * 相対で置く指定の形を見る (#2039)。
1705
+ *
1706
+ * 相手の名前が実在するかはここでは見ない。 組み立て側 (`resolveRelativeDoc`) が figure 全体を
1707
+ * 組んだ後に照合し、いない相手を指した時は知らせを出す。 入口ごとに別の場所で判定すると、
1708
+ * 本文で書いた時と JSON で書いた時で返るものが変わる。
1709
+ */
1710
+ function validateRelativePos(v: unknown, path: string, errors: JsonDslError[]): void {
1711
+ if (v === undefined) return;
1712
+ if (!v || typeof v !== "object" || Array.isArray(v)) {
1713
+ errors.push({ path, message: "posRel must be an object with anchor and dir" });
1714
+ return;
1715
+ }
1716
+ const p = v as Record<string, unknown>;
1717
+ checkUnknownKeys(p, "posRel", path, errors);
1718
+ 表で検査(p, "posRel", path, "posRel", errors);
1719
+ }
1720
+
1721
+ /**
1722
+ * 図全体として相対の指定が解けるかを見る (#2039)。
1723
+ *
1724
+ * 判定は `relative-pos.ts` の `findRelativeProblems` が持つ。 記法側も同じ判定を呼ぶので、
1725
+ * 同じ書き方が入口によって通ったり通らなかったりしない。
1726
+ *
1727
+ * 誤りの文だけをここで作る = 記法側は行番号を、こちらは欄の場所を添えるため。
1728
+ */
1729
+ function validateRelativeAnchors(actors: readonly unknown[], errors: JsonDslError[]): void {
1730
+ const items: Array<{ name: string; rel?: RelativePos }> = [];
1731
+ const 場所 = new Map<string, number>();
1732
+ actors.forEach((a, i) => {
1733
+ if (!a || typeof a !== "object" || Array.isArray(a)) return;
1734
+ const o = a as Record<string, unknown>;
1735
+ if (typeof o.name !== "string") return;
1736
+ const rel = o.posRel;
1737
+ // 形の誤りは `validateRelativePos` が既に返している。 ここは形の整った指定だけを見る
1738
+ const 整った =
1739
+ rel !== null &&
1740
+ typeof rel === "object" &&
1741
+ !Array.isArray(rel) &&
1742
+ typeof (rel as Record<string, unknown>).anchor === "string" &&
1743
+ typeof (rel as Record<string, unknown>).dir === "string";
1744
+ items.push({ name: o.name, rel: 整った ? (rel as RelativePos) : undefined });
1745
+ if (!場所.has(o.name)) 場所.set(o.name, i);
1746
+ });
1747
+
1748
+ const named = items.map((i) => i.name);
1749
+ for (const p of findRelativeProblems(items)) {
1750
+ const i = 場所.get(p.name) ?? 0;
1751
+ errors.push({
1752
+ path: `$.actors[${i}].posRel`,
1753
+ ...(p.kind === "self"
1754
+ ? {
1755
+ message: `posRel.anchor must not be the actor itself: "${p.name}"`,
1756
+ hint: "別の登場人物の名前を書く",
1757
+ }
1758
+ : p.kind === "missing-anchor"
1759
+ ? {
1760
+ message: `posRel.anchor must name an actor: "${p.anchor ?? ""}"`,
1761
+ hint:
1762
+ named.length > 0
1763
+ ? `actors に書かれている名前 = ${named.join(", ")}`
1764
+ : "actors に基準にする登場人物を書く",
1765
+ }
1766
+ : {
1767
+ message: `posRel.anchor must not form a cycle: "${p.name}"`,
1768
+ hint: "どれか 1 つは posX / posY か自動配置にする",
1769
+ }),
1770
+ });
1771
+ }
1772
+ }
1773
+
1170
1774
  /**
1171
1775
  * 見本 (parts) の名前として扱う値かどうか (#1294)。
1172
1776
  *
@@ -1196,22 +1800,13 @@ function validateViewport(v: unknown, errors: JsonDslError[]): void {
1196
1800
  }
1197
1801
 
1198
1802
  /**
1199
- * 鍵を利用者が決める入れ物 (縦列 / 群) を見る (#1304)。
1803
+ * 選択肢の組が受ける項目。 どれも必ず書く (#2258)。
1200
1804
  *
1201
- * `lanes` / `groups` は id を鍵に持つため、鍵そのものは縛れない。 縛れるのは
1202
- * 「入れ物が plain object か」 と「各 id の中身の項目名と値の型」 の 2 つ。
1203
- *
1204
- * #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
1205
- * 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
1206
- */
1207
- /**
1208
- * 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
1209
- *
1210
- * 表を 2 つ持つと片方だけ直してずれる。 記法側 (`v05/parser.ts`) が持つ表をそのまま引く。
1211
- *
1212
- * 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
1213
- * 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
1805
+ * **判定と知らせが同じ集合を読む**。 別々に並べていた間、判定に項目を足しても
1806
+ * 知らせは古い一覧を教え続ける形だった。
1214
1807
  */
1808
+ const 選択肢の項目 = ["value", "label"] as const;
1809
+
1215
1810
  /**
1216
1811
  * 選択肢の並び (`["green", { "value": "red", "label": "失敗" }]`) を要素ごとに検査する (#1920)。
1217
1812
  *
@@ -1240,13 +1835,21 @@ function 選択肢の並びを検査する(値: unknown, path: string, errors: J
1240
1835
  }
1241
1836
  const 組 = x as Record<string, unknown>;
1242
1837
  for (const k of Object.keys(組)) {
1243
- if (k === "value" || k === "label") continue;
1244
- errors.push({ path: `${here}.${k}`, message: `unknown key "${k}"`, hint: "使える項目 = value, label" });
1838
+ if ((選択肢の項目 as readonly string[]).includes(k)) continue;
1839
+ errors.push({
1840
+ path: `${here}.${k}`,
1841
+ message: `unknown key "${k}"`,
1842
+ hint: `使える項目 = ${選択肢の項目.join(", ")}`,
1843
+ });
1245
1844
  }
1246
- for (const k of ["value", "label"] as const) {
1845
+ for (const k of 選択肢の項目) {
1247
1846
  const v = 組[k];
1248
1847
  if (v === undefined) {
1249
- errors.push({ path: `${here}.${k}`, message: `${k} is required`, hint: "必須の項目 = value, label" });
1848
+ errors.push({
1849
+ path: `${here}.${k}`,
1850
+ message: `${k} is required`,
1851
+ hint: `必須の項目 = ${選択肢の項目.join(", ")}`,
1852
+ });
1250
1853
  } else if (typeof v !== "string") {
1251
1854
  errors.push({ path: `${here}.${k}`, message: `${k} must be 文字列`, hint: `got ${v === null ? "null" : typeof v}` });
1252
1855
  } else if (v === "") {
@@ -1260,6 +1863,14 @@ function 選択肢の並びを検査する(値: unknown, path: string, errors: J
1260
1863
  });
1261
1864
  }
1262
1865
 
1866
+ /**
1867
+ * 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
1868
+ *
1869
+ * 表を 2 つ持つと片方だけ直してずれる。 記法側 (`v05/parser.ts`) が持つ表をそのまま引く。
1870
+ *
1871
+ * 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
1872
+ * 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
1873
+ */
1263
1874
  function 表で中身を検査する(
1264
1875
  o: Record<string, unknown>,
1265
1876
  表: Record<string, 図形の定義>,
@@ -1344,18 +1955,30 @@ function 表で中身を検査する(
1344
1955
  }
1345
1956
  }
1346
1957
 
1958
+ /** 帯に書く相手の項目。 値が文字列で、他の 2 つと型が違う */
1959
+ const 帯の相手の項目 = ["actor"] as const;
1960
+ /** 帯に書く段の項目。 値が 0 以上の整数 */
1961
+ const 帯の段の項目 = ["from", "to"] as const;
1347
1962
  /**
1348
- * 値を見せる部品の並びを検査する (#1374)。
1963
+ * 帯が受ける項目。 判定と知らせの両方がここから作る (#2258)。
1349
1964
  *
1350
- * **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
1351
- * 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `readouts: 1` が
1352
- * 素通りする。
1965
+ * 公開 schema の `$.bands` も同じ 3 語を閉じた集合として宣言しており、
1966
+ * `closed-level-coverage.test.ts` が両者の一致を見る。
1353
1967
  */
1968
+ export const 帯の項目: readonly string[] = [...帯の相手の項目, ...帯の段の項目];
1969
+
1354
1970
  /**
1355
1971
  * 順序図の帯の並びを検査する (#1466)。
1356
1972
  *
1357
1973
  * **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
1358
1974
  * 委ねる作りなので、ここで見ないと `bands: 1` が素通りする。
1975
+ *
1976
+ * ## 知らない項目も見る (#2262)
1977
+ *
1978
+ * 書いてある 3 つの型だけを見て、**知らない項目があるかを確かめていなかった**。
1979
+ * 公開 schema は `$.bands` を閉じた階層として宣言しているので、
1980
+ * schema を信じて書いた入力がこちらでは通り、schema 検証器では落ちる状態だった。
1981
+ * 公開 schema が閉じた階層のうち、素通りしていたのはここだけだった。
1359
1982
  */
1360
1983
  function validateBands(v: unknown, errors: JsonDslError[]): void {
1361
1984
  if (v === undefined) return;
@@ -1374,10 +1997,20 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
1374
1997
  return;
1375
1998
  }
1376
1999
  const o = b as Record<string, unknown>;
1377
- if (typeof o.actor !== "string" || o.actor === "") {
1378
- errors.push({ path: `${path}.actor`, message: "band.actor must be a non-empty string" });
2000
+ for (const k of Object.keys(o)) {
2001
+ if (帯の項目.includes(k)) continue;
2002
+ errors.push({
2003
+ path: `${path}.${k}`,
2004
+ message: `unknown key "${k}"`,
2005
+ hint: `使える項目 = ${帯の項目.join(", ")}`,
2006
+ });
2007
+ }
2008
+ for (const k of 帯の相手の項目) {
2009
+ if (typeof o[k] !== "string" || o[k] === "") {
2010
+ errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-empty string` });
2011
+ }
1379
2012
  }
1380
- for (const k of ["from", "to"] as const) {
2013
+ for (const k of 帯の段の項目) {
1381
2014
  if (typeof o[k] !== "number" || !Number.isInteger(o[k]) || o[k] < 0) {
1382
2015
  errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-negative integer` });
1383
2016
  }
@@ -1385,6 +2018,98 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
1385
2018
  });
1386
2019
  }
1387
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
+
2106
+ /**
2107
+ * 値を見せる部品の並びを検査する (#1374)。
2108
+ *
2109
+ * **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
2110
+ * 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `readouts: 1` が
2111
+ * 素通りする。
2112
+ */
1388
2113
  function validateReadouts(v: unknown, errors: JsonDslError[]): void {
1389
2114
  if (v === undefined) return;
1390
2115
  if (!Array.isArray(v)) {
@@ -1578,6 +2303,11 @@ function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]):
1578
2303
  */
1579
2304
  type 式の検査結果 = { readonly 読めた: true; readonly 値: unknown } | { readonly 読めた: false };
1580
2305
 
2306
+ /**
2307
+ * 式の組が受ける項目 (#2258)。 判定と知らせが同じ集合を読む。
2308
+ */
2309
+ const 式の組の項目 = ["expression", "label"] as const;
2310
+
1581
2311
  /**
1582
2312
  * 式の値が組の形 (`{ expression, label }`) なら組を検査し、式の文字列を返す (#1916)。
1583
2313
  *
@@ -1589,12 +2319,12 @@ function 式の組を検査する(値: unknown, path: string, errors: JsonDslErr
1589
2319
  const 組 = 値 as Record<string, unknown>;
1590
2320
  let 読めた = true;
1591
2321
  for (const 鍵 of Object.keys(組)) {
1592
- if (鍵 === "expression" || 鍵 === "label") continue;
2322
+ if ((式の組の項目 as readonly string[]).includes(鍵)) continue;
1593
2323
  読めた = false;
1594
2324
  errors.push({
1595
2325
  path: `${path}.${鍵}`,
1596
2326
  message: `unknown formula field: ${鍵}`,
1597
- hint: "使える項目 = expression, label",
2327
+ hint: `使える項目 = ${式の組の項目.join(", ")}`,
1598
2328
  });
1599
2329
  }
1600
2330
  if (typeof 組.expression !== "string" || 組.expression.trim() === "") {
@@ -1632,7 +2362,11 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
1632
2362
  });
1633
2363
  return;
1634
2364
  }
1635
- const 相手の鍵 = ["box", "lane", "arrow", "diagram"];
2365
+ // 相手の指し方は記法の側が 1 か所で決める (#2256)。 同じ 4 語をここにも並べていた間、
2366
+ // 1:1 を名乗りながら受ける項目が両側で別々に決まっていた。
2367
+ // 必ず書く 2 つも同じ定数から取る (#2258) = 知らせが判定と別に並べる形をやめる
2368
+ const 相手の鍵: readonly string[] = EVENT_TARGET_KEYS;
2369
+ const 出来事の項目: readonly string[] = [...EVENT_REQUIRED_KEYS, ...EVENT_TARGET_KEYS];
1636
2370
  v.forEach((e, i) => {
1637
2371
  const path = `$.events[${i}]`;
1638
2372
  if (!e || typeof e !== "object" || Array.isArray(e)) {
@@ -1641,11 +2375,11 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
1641
2375
  }
1642
2376
  const o = e as Record<string, unknown>;
1643
2377
  for (const k of Object.keys(o)) {
1644
- if (k === "on" || k === "handler" || 相手の鍵.includes(k)) continue;
2378
+ if (出来事の項目.includes(k)) continue;
1645
2379
  errors.push({
1646
2380
  path: `${path}.${k}`,
1647
2381
  message: `unknown key "${k}"`,
1648
- hint: `使える項目 = on, handler, ${相手の鍵.join(", ")}`,
2382
+ hint: `使える項目 = ${出来事の項目.join(", ")}`,
1649
2383
  });
1650
2384
  }
1651
2385
  if (typeof o.on !== "string" || !(EVENT_KINDS as readonly string[]).includes(o.on)) {
@@ -1700,6 +2434,16 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
1700
2434
  });
1701
2435
  }
1702
2436
 
2437
+ /**
2438
+ * 巻き上げに応じて進む値が受ける項目 (#2258)。
2439
+ *
2440
+ * **数を書く項目と文字を書く項目を分けて持つ** = 受け方が違うので判定が分かれるが、
2441
+ * 知らせは 2 つを繋いで 1 つの一覧として出す。 別々に並べていた間、判定が 3 語で
2442
+ * 知らせが 4 語という食い違いが同じ関数の中にあった。
2443
+ */
2444
+ const 送りの数の項目 = ["start", "end", "scrub"] as const;
2445
+ const 送りの文の項目 = ["label"] as const;
2446
+
1703
2447
  /**
1704
2448
  * 巻き上げに応じて進む値を検査する (#1393)。
1705
2449
  *
@@ -1749,17 +2493,17 @@ function validateScrolls(
1749
2493
  continue;
1750
2494
  }
1751
2495
  for (const [k, x] of Object.entries(spec as Record<string, unknown>)) {
1752
- if (k === "label") {
2496
+ if ((送りの文の項目 as readonly string[]).includes(k)) {
1753
2497
  if (typeof x !== "string") {
1754
- errors.push({ path: `${path}.label`, message: "label must be a string" });
2498
+ errors.push({ path: `${path}.${k}`, message: `${k} must be a string` });
1755
2499
  }
1756
2500
  continue;
1757
2501
  }
1758
- if (k !== "start" && k !== "end" && k !== "scrub") {
2502
+ if (!(送りの数の項目 as readonly string[]).includes(k)) {
1759
2503
  errors.push({
1760
2504
  path: `${path}.${k}`,
1761
2505
  message: `unknown key "${k}"`,
1762
- hint: "使える項目 = start, end, scrub, label",
2506
+ hint: `使える項目 = ${[...送りの数の項目, ...送りの文の項目].join(", ")}`,
1763
2507
  });
1764
2508
  continue;
1765
2509
  }
@@ -1794,6 +2538,15 @@ function validateActorShape(v: unknown, path: string, errors: JsonDslError[]): v
1794
2538
  表で中身を検査する(v as Record<string, unknown>, 図形の表, path, "shape", errors);
1795
2539
  }
1796
2540
 
2541
+ /**
2542
+ * 鍵を利用者が決める入れ物 (縦列 / 群) を見る (#1304)。
2543
+ *
2544
+ * `lanes` / `groups` は id を鍵に持つため、鍵そのものは縛れない。 縛れるのは
2545
+ * 「入れ物が plain object か」 と「各 id の中身の項目名と値の型」 の 2 つ。
2546
+ *
2547
+ * #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
2548
+ * 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
2549
+ */
1797
2550
  function validateIdMap(
1798
2551
  v: unknown,
1799
2552
  欄: "lanes" | "groups",
@@ -1850,9 +2603,450 @@ function validateAxes(v: unknown, errors: JsonDslError[]): void {
1850
2603
  const o = 一方 as Record<string, unknown>;
1851
2604
  checkUnknownKeys(o, 層の名[名], `$.axes.${名}`, errors);
1852
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" });
1853
3014
  }
1854
3015
  }
1855
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;
3044
+ }
3045
+ const o = v as Record<string, unknown>;
3046
+ checkUnknownKeys(o, "regions", "$.regions", errors);
3047
+ 表で検査(o, "regions", "$.regions", "regions", errors);
3048
+ }
3049
+
1856
3050
  /**
1857
3051
  * 写しを作る時の入れ子の深さの上限。
1858
3052
  *
@@ -2075,6 +3269,10 @@ export function 素のデータに写す(
2075
3269
  }
2076
3270
  }
2077
3271
 
3272
+ /**
3273
+ * shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
3274
+ * fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
3275
+ */
2078
3276
  function validateJson(
2079
3277
  json: unknown,
2080
3278
  ): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
@@ -2115,6 +3313,8 @@ function validateJson(
2115
3313
  // 値を見せる部品の中身を、記法と同じ表で見る (#1374)
2116
3314
  validateReadouts(j.readouts, errors);
2117
3315
  validateBands(j.bands, errors);
3316
+ validateLegend(j.legend, errors);
3317
+ validateStageHeader(j.stageHeader, errors);
2118
3318
  // 読む人が動かすつまみの中身も、記法と同じ表で見る (#1389)
2119
3319
  validateInputs(j.inputs, errors);
2120
3320
  // 式は描画側の parser に通す (#1391)
@@ -2126,6 +3326,29 @@ function validateJson(
2126
3326
  // 値そのものの型は表が見る (#1304)。 図表の箱の上の小見出し (#1247) の空文字は
2127
3327
  // 「書かなかった」 と同じ扱いにするため通す (記法側の `eyebrow:` と揃える。 落とすのは `jsonToDoc`)
2128
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
+ }
2129
3352
 
2130
3353
  if (!Array.isArray(j.actors) || j.actors.length === 0) {
2131
3354
  errors.push({ path: "$.actors", message: "actors must be a non-empty array" });
@@ -2141,6 +3364,93 @@ function validateJson(
2141
3364
  // 値そのものの型は表が見る (#1304)。 `kind` は見本 (parts) の名前も受けるため
2142
3365
  // 非空の文字列までしか縛らない (CAR-1657 の unified syntax)
2143
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
+ }
2144
3454
  // 箱の中に描く図形の中身を、記法と同じ表で見る (#1374)
2145
3455
  validateActorShape(ao.shape, `$.actors[${i}].shape`, errors);
2146
3456
  // 見本 (parts) にしか効かない項目は、見本でない箱に書かれたら誤りにする (#1294)。
@@ -2150,6 +3460,7 @@ function validateJson(
2150
3460
  const 代わりに使う欄: Record<(typeof 見本にしか効かない欄)[number], string> = {
2151
3461
  state: "箱の見た目を変えるなら tone / color を使う",
2152
3462
  scale: "大きさを変えるなら posW / posH を使う",
3463
+ phase: "普通の箱は自分の段を持たない。 動かす段は animation に書く",
2153
3464
  };
2154
3465
  if (!見本か) {
2155
3466
  for (const 欄 of 見本にしか効かない欄) {
@@ -2173,8 +3484,8 @@ function validateJson(
2173
3484
  });
2174
3485
  }
2175
3486
  }
2176
- // codex-review MAJOR fix = state override は plain object + 値は primitive (number / string / boolean) 限定、
2177
- // `{ v: {} }` 等 nested object や null が流入すると CdlState.initial に不正な型が入り compile 崩れる。
3487
+ // 値の上書き (`state`) は素の object に限り、中の値も数 / 文字 / 真偽のいずれかに限る。
3488
+ // `{ v: {} }` のような入れ子や null を通すと、値の初期値に扱えない型が入って組み立てが崩れる。
2178
3489
  if (ao.state !== undefined) {
2179
3490
  if (!ao.state || typeof ao.state !== "object" || Array.isArray(ao.state)) {
2180
3491
  errors.push({
@@ -2195,7 +3506,11 @@ function validateJson(
2195
3506
  }
2196
3507
  // CAR-1693 Phase 1: actor DSL 表面 pos の validation
2197
3508
  validateLayoutPos(ao.pos, `$.actors[${i}].pos`, errors);
3509
+ // 相対で置く指定 (#2039)
3510
+ validateRelativePos(ao.posRel, `$.actors[${i}].posRel`, errors);
2198
3511
  });
3512
+ // 解けない相対の指定 (#2039)。 欄ごとの形を見た後に、図全体として解けるかを見る
3513
+ validateRelativeAnchors(j.actors, errors);
2199
3514
  }
2200
3515
  if (!Array.isArray(j.flow)) {
2201
3516
  errors.push({ path: "$.flow", message: "flow must be an array" });
@@ -2209,6 +3524,44 @@ function validateJson(
2209
3524
  checkUnknownKeys(so, "step", `$.flow[${i}]`, errors);
2210
3525
  // 値そのものの型は表が見る (#1304)。 色と線種は engine の一覧と突き合わせる
2211
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
+ }
2212
3565
  // CAR-1693 Phase 1: step DSL 表面 pos の validation
2213
3566
  validateLayoutPos(so.pos, `$.flow[${i}].pos`, errors);
2214
3567
  });
@@ -2245,11 +3598,38 @@ function validateJson(
2245
3598
  }
2246
3599
  validateStates(j.states, errors);
2247
3600
  validateValues(j.values, errors);
3601
+ validateChartPresentationFields(j, errors);
2248
3602
  validateAxes(j.axes, errors);
3603
+ validateRegions(j.regions, errors);
2249
3604
  if (errors.length > 0) return { ok: false, errors };
2250
3605
  return { ok: true, data: j as unknown as DragonJson };
2251
3606
  }
2252
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
+
2253
3633
  /**
2254
3634
  * 段の中で値を動かす指定を見る (#1186)。
2255
3635
  *
@@ -2376,11 +3756,19 @@ function validateValues(v: unknown, errors: JsonDslError[]): void {
2376
3756
  }
2377
3757
 
2378
3758
  /**
2379
- * JSON DSL → DslDocument (AST) 変換。 pos は JSON なので line 情報なし、 全て line 0。
3759
+ * 段の説明を取り出す (#2621)。 受ける語は `PHASE_BODY_KEYS` が持つ。
2380
3760
  *
2381
- * CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
2382
- * test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
3761
+ * **正の語 (`body`) を先に見る**。 表の先頭が正の語なので、並び順のまま最初に値を持つ語を
3762
+ * 採れば「どちらが効くか」 の規則を別に書かなくて済む。
2383
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
+
2384
3772
  /**
2385
3773
  * 図表の箱の上の小見出しを、 記法側と同じ形に整える (#1247)。
2386
3774
  *
@@ -2393,11 +3781,40 @@ function 整えた小見出し(v: string | undefined): string | undefined {
2393
3781
  return t.length > 0 ? t : undefined;
2394
3782
  }
2395
3783
 
2396
- export function jsonToDoc(json: DragonJson): DslDocument {
2397
- const p0 = { line: 0 };
2398
- const actors: DslActor[] = json.actors.map((a) => {
3784
+ /**
3785
+ * 見本の状態の上書きに、段を使うかどうか (`phase`) を混ぜる (#2125)。
3786
+ *
3787
+ * 組み立て側は段を外す指定を状態の上書きの中の `phase` として読む
3788
+ * (`compile/parts.ts` の `段を外す`)。 記法は中括弧に書いた名前をすべて上書きへ入れるため
3789
+ * `{ kind: wave-gauge, phase: false }` がそのまま届くが、JSON は欄ごとに型を宣言する形なので、
3790
+ * 欄として受けたものを同じ場所へ入れ直す。
3791
+ *
3792
+ * **書かなかった時は欄を作らない**。 `{ phase: undefined }` を混ぜると、上書きの有無で
3793
+ * 分岐する側から見て「書いた」 ことになる。
3794
+ */
3795
+ function 段を含めた上書き(a: JsonActor): Record<string, number | string | boolean> | undefined {
3796
+ if (a.phase === undefined) return a.state;
3797
+ return { ...(a.state ?? {}), phase: a.phase };
3798
+ }
3799
+
3800
+ /**
3801
+ * JSON DSL → DslDocument (AST) 変換。
3802
+ *
3803
+ * 行番号は `行の表` を渡した時だけ入る (#2117)。 JSON そのものは書いた場所を持たないため、
3804
+ * 本文の行が分かる入口 (編集画面の YAML 欄) が場所ごとの行を集めて渡す。 渡さない呼出は
3805
+ * 全ての要素が 0 行になる = 「行が分からない」 ことを 0 で表す。
3806
+ *
3807
+ * CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
3808
+ * test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
3809
+ */
3810
+ export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): DslDocument {
3811
+ // 書いた場所から行を引く。 引けない場所は 0 行 = 表を渡さない呼出と同じ状態になる
3812
+ const 位置 = (...道: readonly (string | number)[]): { line: number } => ({
3813
+ line: 行の表?.get(書いた場所の鍵(...道)) ?? 0,
3814
+ });
3815
+ const actors: DslActor[] = json.actors.map((a, i) => {
2399
3816
  if (typeof a === "string") {
2400
- return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: p0 };
3817
+ return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: 位置("actors", i) };
2401
3818
  }
2402
3819
  // CAR-1657 = kind が既存 NodeKind に無い値なら parts identifier 候補、 partId に格納
2403
3820
  const kindStr = a.kind ?? "actor";
@@ -2405,6 +3822,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2405
3822
  // 色は記法と同じ振り分けを通す (#1294)。 `#` で始まれば色番号、それ以外は色の名前。
2406
3823
  // 別々に書くと、同じ値が入口によって色番号にも色名にもなる
2407
3824
  const 色 = a.color !== undefined ? splitColorValue(a.color) : {};
3825
+ const 書いた座標 = a.at ?? a.点の位置;
2408
3826
  return {
2409
3827
  name: a.name,
2410
3828
  kind: isPart ? "actor" : resolveNodeKind(kindStr),
@@ -2412,12 +3830,33 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2412
3830
  // 「書かなかった」 と同じ扱いにする (#1058)
2413
3831
  kindWritten: a.kind !== undefined && !isPart,
2414
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,
2415
3849
  eyebrow: a.eyebrow,
2416
3850
  value: a.value,
2417
3851
  previous: a.previous,
2418
3852
  rows: a.rows,
2419
- // 行頭の印 (#1466)。 行と対で読む
2420
- 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,
2421
3860
  lane: a.lane,
2422
3861
  stack: a.stack,
2423
3862
  initial: a.initial,
@@ -2441,8 +3880,17 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2441
3880
  tone: resolveTone(a.tone) ?? 色.tone,
2442
3881
  owner: a.owner,
2443
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" : "点の位置"),
2444
3891
  touchpoint: a.touchpoint,
2445
3892
  opportunity: a.opportunity,
3893
+ opportunityPosition: a.opportunityPosition,
2446
3894
  }),
2447
3895
  colorHex: 色.hex,
2448
3896
  // 部品に書いた色の名前は効かない。 記法と同じく書いた名前として残し、組み立て側が知らせる (#1973)
@@ -2457,14 +3905,25 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2457
3905
  scale: isPart ? a.scale : undefined,
2458
3906
  scaleKeys: isPart && a.scale !== undefined ? ["scale"] : undefined,
2459
3907
  partId: isPart ? kindStr : undefined,
2460
- stateOverride: isPart ? a.state : undefined,
3908
+ stateOverride: isPart ? 段を含めた上書き(a) : undefined,
2461
3909
  // CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping (naming collision 回避)
2462
3910
  layoutPos: a.pos,
2463
- pos: p0,
3911
+ // 相対で置く指定 (#2039)。 欄の名前が内部と同じなのでそのまま渡す
3912
+ posRel: a.posRel,
3913
+ pos: 位置("actors", i),
2464
3914
  };
2465
3915
  });
2466
- // 図の配色 (#1553)。 解くのは 1 度だけにする
2467
- 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
+ });
2468
3927
 
2469
3928
  const flow: DslStep[] = json.flow.map((s, i) => ({
2470
3929
  no: i + 1,
@@ -2473,13 +3932,15 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2473
3932
  label: s.label,
2474
3933
  sub: s.sub,
2475
3934
  side: s.side,
3935
+ fromSide: s.fromSide,
3936
+ toSide: s.toSide,
2476
3937
  // 矢印の先の形 (#1462)。 読めない語は組み立てが落とす
2477
3938
  head: s.head,
2478
3939
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
2479
3940
  tailHead: s.tailHead,
2480
3941
  headFill: s.headFill,
2481
3942
  tailHeadFill: s.tailHeadFill,
2482
- // クラス図の出どころ側の多重度 (#1771)
3943
+ // UML 関係の出どころ側の多重度 (#1771 / #2783)
2483
3944
  tailSub: s.tailSub,
2484
3945
  relation: s.relation,
2485
3946
  // 辺の役目と名前の下地 (cdl#618)
@@ -2498,13 +3959,20 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2498
3959
  dashOffsetBind: s.dashOffsetBind,
2499
3960
  labelOffsetX: s.labelOffsetX,
2500
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,
2501
3969
  overlay: s.overlay,
2502
3970
  // 部品の端で繋ぐ要素の id (#1979)。 書かない矢印には欄を足さない = 記法と同じ形になる
2503
3971
  ...(s.fromPartNode !== undefined ? { fromPartNode: s.fromPartNode } : {}),
2504
3972
  ...(s.toPartNode !== undefined ? { toPartNode: s.toPartNode } : {}),
2505
3973
  // CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos
2506
3974
  layoutPos: s.pos,
2507
- pos: p0,
3975
+ pos: 位置("flow", i),
2508
3976
  }));
2509
3977
  // 状態は段が無くても図に載る (#1162 で組み立ての出口が載せる)。 **段の有無で分けない** =
2510
3978
  // 分けると `states` だけを書いた JSON で値が 1 つも届かない (記法側で起きていた形、 #1181)
@@ -2517,7 +3985,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2517
3985
  *
2518
3986
  * 相手は名前のまま持ち、識別子への読み替えは組み立てが行う = 2 つの入口で同じ経路を通る。
2519
3987
  */
2520
- const events: DslEventBinding[] | undefined = json.events?.map((e) => ({
3988
+ const events: DslEventBinding[] | undefined = json.events?.map((e, i) => ({
2521
3989
  event: e.on as DslEventBinding["event"],
2522
3990
  target:
2523
3991
  e.diagram === true
@@ -2532,7 +4000,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2532
4000
  ? ({ kind: "lane" as const, name: e.lane } as const)
2533
4001
  : ({ kind: "node" as const, name: e.box ?? "" } as const),
2534
4002
  handlerId: e.handler.trim(),
2535
- pos: p0,
4003
+ pos: 位置("events", i),
2536
4004
  }));
2537
4005
  const scrolls: DslScrollTrigger[] | undefined = json.scrolls
2538
4006
  ? Object.entries(json.scrolls).map(([id, spec]) => ({ id, ...spec }))
@@ -2542,28 +4010,28 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2542
4010
  const formulas: DslFormula[] | undefined = json.formulas
2543
4011
  ? Object.entries(json.formulas).map(([id, 値]) =>
2544
4012
  typeof 値 === "string"
2545
- ? { id, expression: 値, pos: p0 }
4013
+ ? { id, expression: 値, pos: 位置("formulas", id) }
2546
4014
  : {
2547
4015
  id,
2548
4016
  expression: 値.expression,
2549
4017
  ...(値.label !== undefined ? { label: 値.label } : {}),
2550
- pos: p0,
4018
+ pos: 位置("formulas", id),
2551
4019
  },
2552
4020
  )
2553
4021
  : undefined;
2554
4022
  const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
2555
4023
  name,
2556
4024
  initial,
2557
- pos: p0,
4025
+ pos: 位置("states", name),
2558
4026
  }));
2559
- const phases: DslPhase[] = (json.animation ?? []).map((p) => ({
4027
+ const phases: DslPhase[] = (json.animation ?? []).map((p, i) => ({
2560
4028
  name: p.step,
2561
4029
  durationMs: Math.round((p.duration ?? 1.4) * 1000),
2562
4030
  highlight: p.focus,
2563
- body: p.body,
4031
+ body: 段の説明(p),
2564
4032
  badge: p.badge,
2565
4033
  // 書いた段だけが欄を持つ。 空文字を置くと「書いた」 と「書いていない」 が同じ形になる
2566
- ...(p.draw !== undefined ? { draw: p.draw, drawPos: p0 } : {}),
4034
+ ...(p.draw !== undefined ? { draw: p.draw, drawPos: 位置("animation", i, "draw") } : {}),
2567
4035
  // 割合は `draw` がある段にだけ写す。 単独で書いても描く相手が決まらず何も起きないので、
2568
4036
  // 記法側 (同じ行に書かせる形) と同じ状態に揃える
2569
4037
  ...(p.draw !== undefined && p.drawRatio !== undefined ? { drawRatio: p.drawRatio } : {}),
@@ -2575,47 +4043,142 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2575
4043
  state,
2576
4044
  from,
2577
4045
  to,
2578
- pos: p0,
4046
+ pos: 位置("animation", i, "tween", state),
2579
4047
  })),
2580
4048
  }
2581
4049
  : {}),
2582
4050
  ...(p.set && Object.keys(p.set).length > 0
2583
- ? { sets: Object.entries(p.set).map(([state, value]) => ({ state, value, pos: p0 })) }
4051
+ ? {
4052
+ sets: Object.entries(p.set).map(([state, value]) => ({
4053
+ state,
4054
+ value,
4055
+ pos: 位置("animation", i, "set", state),
4056
+ })),
4057
+ }
2584
4058
  : {}),
2585
- pos: p0,
4059
+ pos: 位置("animation", i),
2586
4060
  }));
2587
4061
  const animate: DslAnimate | undefined =
2588
- states.length > 0 || phases.length > 0 ? { states, phases, pos: p0 } : undefined;
4062
+ states.length > 0 || phases.length > 0
4063
+ ? // 動きの塊そのものには書いた場所が無い。 段がある文書は `animation`、無い文書は
4064
+ // `states` を指す = どちらも「動きを書き始めた場所」 になる
4065
+ { states, phases, pos: phases.length > 0 ? 位置("animation") : 位置("states") }
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.目盛り;
2589
4075
  return {
2590
4076
  title: json.title,
2591
- 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") } : {}),
2592
4143
  // 前後の空白を落としてから見る。 記法側 (`v05/parser.ts`) が `trim()` してから
2593
4144
  // 空かどうかを判定するため、 揃えないと **空白だけの値で入口ごとに図が変わる**
2594
4145
  // (記法は書かなかった扱い、 JSON は中身のない帯を描く。 Round 2 の指摘で実測)
2595
4146
  ...(整えた小見出し(json.eyebrow) !== undefined
2596
- ? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos: p0 }
4147
+ ? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos: 位置("eyebrow") }
2597
4148
  : {}),
2598
4149
  // 2 軸で仕分ける図の軸の名前 (#1294)。 中身の無い形は「書かなかった」 と同じにする =
2599
4150
  // 空の軸を渡すと、書いていない側の名前が空文字で描かれる (記法側と同じ扱い)
2600
4151
  ...(json.axes && (json.axes.x !== undefined || json.axes.y !== undefined)
2601
- ? { axes: json.axes, axesPos: p0 }
4152
+ ? { axes: json.axes, axesPos: 位置("axes") }
2602
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 } : {}),
2603
4162
  actors,
2604
4163
  flow,
2605
4164
  animate,
2606
4165
  // 他の値から決まる値 (#1181)。 書いた順に並べる = 解く順は参照から決まるので順序に
2607
4166
  // 意味は無いが、知らせの並びが書いた順になる
2608
4167
  values: json.values
2609
- ? Object.entries(json.values).map(([name, expression]) => ({ name, expression, pos: p0 }))
4168
+ ? Object.entries(json.values).map(([name, expression]) => ({
4169
+ name,
4170
+ expression,
4171
+ pos: 位置("values", name),
4172
+ }))
2610
4173
  : undefined,
2611
- viewport: json.viewport ? { ...json.viewport, pos: p0 } : undefined,
4174
+ viewport: json.viewport ? { ...json.viewport, pos: 位置("viewport") } : undefined,
2612
4175
  lanes: json.lanes
2613
4176
  ? Object.fromEntries(
2614
4177
  Object.entries(json.lanes).map(([id, l]) => {
2615
4178
  // CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping。
2616
4179
  // JSON input の { pos, x, width, ... } を分離し、 pos のみ layoutPos に rename する。
2617
4180
  const { pos: layoutPos, ...laneRest } = l;
2618
- return [id, { id, ...laneRest, layoutPos, pos: p0 }];
4181
+ return [id, { id, ...laneRest, layoutPos, pos: 位置("lanes", id) }];
2619
4182
  }),
2620
4183
  )
2621
4184
  : undefined,
@@ -2623,7 +4186,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2623
4186
  ? Object.fromEntries(
2624
4187
  Object.entries(json.groups).map(([id, g]) => [
2625
4188
  id,
2626
- { id, label: g.label, lanes: g.lanes, pos: p0 },
4189
+ { id, label: g.label, lanes: g.lanes, pos: 位置("groups", id) },
2627
4190
  ]),
2628
4191
  )
2629
4192
  : undefined,
@@ -2638,12 +4201,31 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2638
4201
  reveal: json.reveal,
2639
4202
  // 箱に触れると関係する線だけを光らせるか (#1757)
2640
4203
  relations: json.relations,
2641
- // 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す
2642
- ...(json.direction !== undefined ? { direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const) } : {}),
2643
- // 図の配色 (#1553)。 記法と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方がずれない。
2644
- // 読めない語は渡さない = 上流の型検査が語を絞っているので、ここに来るのは書き間違いだけ
2645
- ...(配色 !== null ? { palette: 配色 } : {}),
2646
- pos: p0,
4204
+ // 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す。
4205
+ // 書いた行も記法と同じく持つ = 持たないと、向きの知らせが文書の行 (1 行目) を指す
4206
+ ...(json.direction !== undefined
4207
+ ? {
4208
+ direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const),
4209
+ directionPos: 位置("direction"),
4210
+ }
4211
+ : {}),
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
+ : {}),
4228
+ pos: 位置(),
2647
4229
  };
2648
4230
  }
2649
4231
 
@@ -2673,7 +4255,17 @@ export function jsonToDiagram(
2673
4255
  // **`onNotice` も通す**。 記法経路だけに通知を付けていたため、 同じ型を受ける JSON / YAML
2674
4256
  // 経路では読めない値や捨てた矢印が利用者へ届かなかった (review 指摘)。 エディタの YAML タブは
2675
4257
  // ここを通る
2676
- opts?: { partsCatalog?: Record<string, CdlDiagram>; onNotice?: CompileToCdlOpts["onNotice"] },
4258
+ opts?: {
4259
+ partsCatalog?: Record<string, CdlDiagram>;
4260
+ onNotice?: CompileToCdlOpts["onNotice"];
4261
+ /**
4262
+ * 本文の場所から書いた行を引く表 (#2117)。 渡すと知らせが書いた行を指す。
4263
+ *
4264
+ * JSON そのものは書いた場所を持たないため、本文の行が分かる入口 (編集画面の YAML 欄) が
4265
+ * 集めて渡す。 渡さない呼出は今までどおり全ての知らせが 0 行になる。
4266
+ */
4267
+ 行の表?: 書いた行の表;
4268
+ },
2677
4269
  ): CdlDiagram {
2678
4270
  const v = validateJson(json);
2679
4271
  if (!v.ok) {
@@ -2682,7 +4274,7 @@ export function jsonToDiagram(
2682
4274
  .join("\n");
2683
4275
  throw new Error(`Dragon JSON DSL validation error:\n${msg}`);
2684
4276
  }
2685
- const doc = jsonToDoc(v.data);
4277
+ const doc = jsonToDoc(v.data, opts?.行の表);
2686
4278
  return compileToCdl(doc, opts);
2687
4279
  }
2688
4280