@cardenelabs/dragon 0.26.0 → 0.28.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.
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@ export type { CdlDiagram } from "@cardenelabs/cdl";
25
25
  export { parseTextDslV05 } from "./v05";
26
26
  // 記法一覧が「実際に受け付ける値」 を実装から引くための公開。 手書きすると説明と実装がずれる。
27
27
  export { DRAW_TARGETS, DRAW_WORDS, PHASE_ITEM_WORDS, PRESET_TYPES, TOP_LEVEL_KEYS } from "./v05/parser";
28
- export { TONE_ALIAS, NODE_KIND_ALIAS } from "./keywords";
28
+ export { TONE_ALIAS, NODE_KIND_ALIAS, DIRECTION_ALIAS } from "./keywords";
29
29
  export { lintDiagram, autoFix } from "./notation-lint";
30
30
  export type { LintIssue, LintReport, LintSeverity } from "./notation-lint";
31
31
  // canvas pivot 新 spec 図境界計算 helper (§diagram-boundary SSOT)
@@ -46,6 +46,9 @@ export type { FocusEntry } from "./focus";
46
46
  export { writeActorPosition } from "./write-position";
47
47
  // 図の上での位置を測る。 editor が現在位置を出すのと、 相対指定を解くので同じ規則を使う。
48
48
  export { measureActorBoxes } from "./compile";
49
+ // 本文に書いた部品の状態の上書きと色番号を、部品の図へ当てる。 編集画面が重ねて描く部品にも
50
+ // 組み立て側と同じ値を使う (#1973)
51
+ export { 部品に上書きを当てる } from "./compile";
49
52
  // パーツの見た目の大きさと、 位置を書かなかった時の格子。 画面側と組み立て側で同じ規則を使う。
50
53
  // 記法が受理する種類の全体。 画面側が「本文が見本を使っているか」 を判定するのに使う (#1022)。
51
54
  export { NODE_KIND_VALID } from "./v05/parser";
@@ -76,7 +79,7 @@ export {
76
79
  MAX_PART_SCALE,
77
80
  partDrawsInDiagram,
78
81
  partIsMeasurable,
79
- partsBaseRows,
82
+ partsBaseBottom,
80
83
  partsGridCenters,
81
84
  } from "./compile";
82
85
  // 色として読めるかの判定と、 図の外を指す値かの判定。 状態の上書きを受け取る側 / 画面が色欄を
@@ -108,7 +111,6 @@ export { diagramJsonSchema } from "./schema";
108
111
  export type {
109
112
  DragonJson,
110
113
  JsonActor,
111
- JsonActorNodeOverride,
112
114
  JsonAxes,
113
115
  JsonStep,
114
116
  JsonPhase,
@@ -49,6 +49,7 @@ import type {
49
49
  EdgeHead,
50
50
  EdgeHeadFill,
51
51
  EdgeReveal,
52
+ RelationFocus,
52
53
  ClassRelationType,
53
54
  SequenceMessageKind,
54
55
  } from "@cardenelabs/cdl";
@@ -117,8 +118,11 @@ export interface DragonJson {
117
118
  * つまみの値から決まる値 (optional、 #1391)。 記法の最上位 `formulas:` と同じ。
118
119
  *
119
120
  * 形は `{ 名前: "式" }`。 `values:` と経路が別で、式は名前を中括弧で囲わない。
121
+ *
122
+ * 名札を付ける時は `{ 名前: { expression: "式", label: "名札" } }` の組で書く (#1916)。
123
+ * 描画側は名札を操作部の式の名前に描く (cdl#853)。
120
124
  */
121
- formulas?: Record<string, string>;
125
+ formulas?: Record<string, string | { expression: string; label?: string }>;
122
126
  /**
123
127
  * 押下などの出来事で動く仕掛け (optional、 #1393)。 記法の最上位 `events:` と同じ。
124
128
  *
@@ -173,8 +177,8 @@ export interface DragonJson {
173
177
  contain?: boolean;
174
178
  lifeline?: boolean;
175
179
  /**
176
- * canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` = auto layout offset。 未指定は
177
- * backward compat、 set 済は Phase 2 の applyPosOffset で lane 位置を shift する。
180
+ * 縦列の位置のずらし (#1971)。 配置後の縦列の左上を (x, y) だけ動かす。 記法の `offsetX` / `offsetY`
181
+ * と同じで、読み方は `DslLane.layoutPos` が持つ。
178
182
  */
179
183
  pos?: LayoutPos;
180
184
  }
@@ -191,6 +195,8 @@ export interface DragonJson {
191
195
  bands?: { actor: string; from: number; to: number }[];
192
196
  /** 矢印をいつ出すか (#1470)。 記法の最上位 `reveal:` と同じ */
193
197
  reveal?: EdgeReveal;
198
+ /** 箱に触れると関係する線だけを光らせるか (#1757)。 記法の最上位 `relations:` と同じ */
199
+ relations?: RelationFocus;
194
200
  /** 図の並ぶ向き (#1494)。 記法の最上位 `direction:` と同じ。 JSON は英語の語で書く */
195
201
  direction?: "vertical" | "horizontal";
196
202
  /**
@@ -256,8 +262,8 @@ export interface JsonActor {
256
262
  initial?: boolean;
257
263
  final?: boolean;
258
264
  /**
259
- * canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` = auto layout offset。 未指定は
260
- * backward compat、 set 済は Phase 2 の applyPosOffset で actor 由来 lane / node の位置を shift。
265
+ * 箱の位置のずらし (#1971)。 配置後の箱の中心を (x, y) だけ動かす。 記法の `offsetX` / `offsetY`
266
+ * と同じで、読み方は `DslActor.layoutPos` が持つ。
261
267
  */
262
268
  pos?: LayoutPos;
263
269
  /**
@@ -312,12 +318,6 @@ export interface JsonActor {
312
318
  posY?: number;
313
319
  posW?: number;
314
320
  posH?: number;
315
- /**
316
- * 箱の中の要素ごとに位置と大きさを固定する (#1294)。 記法の `nodes:` と同じ。
317
- *
318
- * key は箱が作る要素の名前 (`header` / `footer` / `spacer` / `s0` 等)。
319
- */
320
- nodes?: Record<string, JsonActorNodeOverride>;
321
321
  /**
322
322
  * 見本を何倍で描くか (#1294)。 記法の `scale:` / `倍率:` と同じ。
323
323
  *
@@ -335,14 +335,6 @@ export interface JsonAxes {
335
335
  y?: { bottom?: string; top?: string };
336
336
  }
337
337
 
338
- /** 箱の中の要素 1 つ分の位置と大きさ (#1294)。 記法の `nodes: { header: { ... } }` と同じ */
339
- export interface JsonActorNodeOverride {
340
- posX?: number;
341
- posY?: number;
342
- posW?: number;
343
- posH?: number;
344
- }
345
-
346
338
  export interface JsonStep {
347
339
  from: string;
348
340
  to: string;
@@ -353,7 +345,7 @@ export interface JsonStep {
353
345
  /**
354
346
  * 矢印の先の形 (#1462)。 記法の `{ head: triangle }` と同じ。
355
347
  *
356
- * 三角 (継ぐ) / 菱 (持つ) / 開いた矢 (使う) / 鳥の足 (多)。
348
+ * 三角 (継承・実装) / 菱 (集約・コンポジション) / 開いた矢 (関連・依存) / 鳥の足 (多)。
357
349
  * 書かなければ従来どおり塗った三角になる。
358
350
  */
359
351
  head?: EdgeHead;
@@ -361,6 +353,8 @@ export interface JsonStep {
361
353
  tailHead?: EdgeHead;
362
354
  headFill?: EdgeHeadFill;
363
355
  tailHeadFill?: EdgeHeadFill;
356
+ /** クラス図の出どころ側の多重度 (#1771)。 記法の `{ tailSub: "1" }` と同じ */
357
+ tailSub?: string;
364
358
  /**
365
359
  * 辺の役目 (cdl#618)。 記法の `{ role: main }` と同じ。
366
360
  *
@@ -394,10 +388,15 @@ export interface JsonStep {
394
388
  /** true で説明文を矢印の線の上に重ねる。 分岐図の条件ラベル用。 */
395
389
  overlay?: boolean;
396
390
  /**
397
- * canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` = edge label offset。 未指定は
398
- * backward compat、 set 済は Phase 2 の applyPosOffset で edge label 位置を shift する。
391
+ * 矢印の位置のずらし (#1971)。 名前のずらし (`labelOffsetX` / `labelOffsetY`) に足して名前を動かす。
399
392
  */
400
393
  pos?: LayoutPos;
394
+ /**
395
+ * 部品の端で矢印を繋ぐ部品の中の要素の id (#1979)。 記法の `{ fromPartNode: rC }` / `{ toPartNode: gC }` と同じ。
396
+ * 要素を 1 つだけ持つ部品は書かなくてもその要素に繋ぐ
397
+ */
398
+ fromPartNode?: string;
399
+ toPartNode?: string;
401
400
  }
402
401
 
403
402
  export interface JsonPhase {
@@ -456,7 +455,7 @@ export interface JsonDslError {
456
455
  *
457
456
  * 以前はここに一覧を写していたため、記法に種類が増えても JSON 経路が古い一覧のまま
458
457
  * 見本の名前として扱った (実測 = 記法が知る 108 種のうち 77 種が該当、`shape-*` は全滅)。
459
- * 見本帳に無い名前は `console.warn` にしか残らず `onNotice` を呼ばないため、
458
+ * カタログに無い名前は `console.warn` にしか残らず `onNotice` を呼ばないため、
460
459
  * 画面には何も出ないまま `actor` の箱が描かれていた (#1293)。
461
460
  *
462
461
  * 型が 2 箇所に散らばると、必ずどちらかが古くなる。 `VALID_PRESETS` (図種) と同じ扱いにする。
@@ -511,6 +510,8 @@ export const ACCEPTED_KEYS = {
511
510
  "bands",
512
511
  // 矢印をいつ出すか (#1470)
513
512
  "reveal",
513
+ // 箱に触れると関係する線だけを光らせるか (#1757)
514
+ "relations",
514
515
  // 図の並ぶ向き (#1494)
515
516
  "direction",
516
517
  // 図の配色 (#1553)
@@ -539,7 +540,6 @@ export const ACCEPTED_KEYS = {
539
540
  "posY",
540
541
  "posW",
541
542
  "posH",
542
- "nodes",
543
543
  "scale",
544
544
  "state",
545
545
  "pos",
@@ -571,6 +571,8 @@ export const ACCEPTED_KEYS = {
571
571
  "tailHead",
572
572
  "headFill",
573
573
  "tailHeadFill",
574
+ // クラス図の出どころ側の多重度 (#1771)
575
+ "tailSub",
574
576
  "relation",
575
577
  // 辺の役目と名前の下地 (cdl#618)
576
578
  "role",
@@ -588,12 +590,14 @@ export const ACCEPTED_KEYS = {
588
590
  "labelOffsetY",
589
591
  "overlay",
590
592
  "pos",
593
+ // 部品の端で繋ぐ要素の id (#1979)
594
+ "fromPartNode",
595
+ "toPartNode",
591
596
  ],
592
597
  phase: ["step", "duration", "focus", "body", "badge", "tween", "set", "draw", "drawRatio"],
593
598
  viewport: ["width", "height", "scale", "laneWidth", "gap", "laneGap", "nodeGap", "labelMargin"],
594
599
  lane: ["x", "width", "label", "contain", "lifeline", "pos"],
595
600
  group: ["label", "lanes"],
596
- actorNode: ["posX", "posY", "posW", "posH"],
597
601
  axes: ["x", "y"],
598
602
  axesX: ["left", "right"],
599
603
  axesY: ["bottom", "top"],
@@ -674,6 +678,8 @@ export const 欄の型表 = {
674
678
  bands: "並び",
675
679
  // 矢印をいつ出すか (#1470)
676
680
  reveal: "非空の文字列",
681
+ // 箱に触れると関係する線だけを光らせるか (#1757)
682
+ relations: "非空の文字列",
677
683
  // 図の並ぶ向き (#1494)
678
684
  direction: "非空の文字列",
679
685
  // 図の配色 (#1553)
@@ -701,7 +707,6 @@ export const 欄の型表 = {
701
707
  posY: "数",
702
708
  posW: "数",
703
709
  posH: "数",
704
- nodes: "object",
705
710
  scale: "数",
706
711
  state: "object",
707
712
  pos: "object",
@@ -732,6 +737,8 @@ export const 欄の型表 = {
732
737
  tailHead: "端の形",
733
738
  headFill: "非空の文字列",
734
739
  tailHeadFill: "非空の文字列",
740
+ // クラス図の出どころ側の多重度 (#1771)
741
+ tailSub: "文字列",
735
742
  relation: "非空の文字列",
736
743
  // 辺の役目と名前の下地 (cdl#618)
737
744
  role: "非空の文字列",
@@ -749,6 +756,9 @@ export const 欄の型表 = {
749
756
  labelOffsetY: "数",
750
757
  overlay: "真偽",
751
758
  pos: "object",
759
+ // 部品の端で繋ぐ要素の id (#1979)。 空の id は要素を指せないので受けない
760
+ fromPartNode: "非空の文字列",
761
+ toPartNode: "非空の文字列",
752
762
  },
753
763
  phase: {
754
764
  step: "必須の非空文字列",
@@ -780,7 +790,6 @@ export const 欄の型表 = {
780
790
  pos: "object",
781
791
  },
782
792
  group: { label: "文字列", lanes: "文字列の並び" },
783
- actorNode: { posX: "数", posY: "数", posW: "数", posH: "数" },
784
793
  axes: { x: "object", y: "object" },
785
794
  axesX: { left: "文字列", right: "文字列" },
786
795
  axesY: { bottom: "文字列", top: "文字列" },
@@ -1128,7 +1137,7 @@ function 編集距離(a: string, b: string, 上限: number): number {
1128
1137
  function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDslError[]): void {
1129
1138
  if (!v || typeof v !== "object" || Array.isArray(v)) return;
1130
1139
  const 受ける = ACCEPTED_KEYS[層] as readonly string[];
1131
- for (const key of Object.keys(v as Record<string, unknown>)) {
1140
+ for (const key of Object.keys(v)) {
1132
1141
  if (受ける.includes(key)) continue;
1133
1142
  const 候補 = 近い項目名(key, 受ける);
1134
1143
  errors.push({
@@ -1145,7 +1154,7 @@ function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDsl
1145
1154
  */
1146
1155
  /**
1147
1156
  * CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` の型 check helper。 finite number pair を必須にし、
1148
- * `NaN` / `Infinity` / non-number は reject する (Phase 2 の applyPosOffset で数値演算するため)。
1157
+ * `NaN` / `Infinity` / non-number は reject する (組み立ての出口 `applyLayoutOffsets` が配置後の位置に足すため)。
1149
1158
  */
1150
1159
  function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): void {
1151
1160
  if (v === undefined) return;
@@ -1169,28 +1178,6 @@ function 見本の名前か(kind: unknown): boolean {
1169
1178
  return kind !== "actor" && !VALID_KIND_SET.has(kind);
1170
1179
  }
1171
1180
 
1172
- /** 箱の中の要素ごとの位置と大きさを見る (#1294) */
1173
- function validateActorNodes(v: unknown, path: string, errors: JsonDslError[]): void {
1174
- if (v === undefined) return;
1175
- if (!v || typeof v !== "object" || Array.isArray(v)) {
1176
- errors.push({
1177
- path,
1178
- message: "actor.nodes must be a plain object of name -> { posX, posY, posW, posH }",
1179
- });
1180
- return;
1181
- }
1182
- for (const [name, o] of Object.entries(v as Record<string, unknown>)) {
1183
- const nodePath = `${path}.${name}`;
1184
- if (!o || typeof o !== "object" || Array.isArray(o)) {
1185
- errors.push({ path: nodePath, message: "actor.nodes entry must be a plain object" });
1186
- continue;
1187
- }
1188
- const n = o as Record<string, unknown>;
1189
- checkUnknownKeys(n, "actorNode", nodePath, errors);
1190
- 表で検査(n, "actorNode", nodePath, `nodes.${name}`, errors);
1191
- }
1192
- }
1193
-
1194
1181
  /**
1195
1182
  * 図全体の大きさと間隔を見る (#1295)。
1196
1183
  *
@@ -1225,6 +1212,54 @@ function validateViewport(v: unknown, errors: JsonDslError[]): void {
1225
1212
  * 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
1226
1213
  * 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
1227
1214
  */
1215
+ /**
1216
+ * 選択肢の並び (`["green", { "value": "red", "label": "失敗" }]`) を要素ごとに検査する (#1920)。
1217
+ *
1218
+ * 要素は文字列か、`value` と `label` の 2 項目を持つ組。 誤りは要素の場所 (`options[1].label`) で
1219
+ * 返す = 並び全体の型違いとして返すと、どの組のどの項目を直せばよいかが読み手に届かない。
1220
+ */
1221
+ function 選択肢の並びを検査する(値: unknown, path: string, errors: JsonDslError[]): void {
1222
+ if (!Array.isArray(値)) {
1223
+ errors.push({
1224
+ path,
1225
+ message: "options must be 選択肢の並び",
1226
+ hint: `got ${値 === null ? "null" : typeof 値}`,
1227
+ });
1228
+ return;
1229
+ }
1230
+ 値.forEach((x, i) => {
1231
+ const here = `${path}[${i}]`;
1232
+ if (typeof x === "string") return;
1233
+ if (typeof x !== "object" || x === null || Array.isArray(x)) {
1234
+ errors.push({
1235
+ path: here,
1236
+ message: "option must be a string or { value, label }",
1237
+ hint: `got ${Array.isArray(x) ? "array" : x === null ? "null" : typeof x}`,
1238
+ });
1239
+ return;
1240
+ }
1241
+ const 組 = x as Record<string, unknown>;
1242
+ 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" });
1245
+ }
1246
+ for (const k of ["value", "label"] as const) {
1247
+ const v = 組[k];
1248
+ if (v === undefined) {
1249
+ errors.push({ path: `${here}.${k}`, message: `${k} is required`, hint: "必須の項目 = value, label" });
1250
+ } else if (typeof v !== "string") {
1251
+ errors.push({ path: `${here}.${k}`, message: `${k} must be 文字列`, hint: `got ${v === null ? "null" : typeof v}` });
1252
+ } else if (v === "") {
1253
+ errors.push({
1254
+ path: `${here}.${k}`,
1255
+ message: `${k} must not be empty`,
1256
+ hint: k === "label" ? "名前を書かないなら文字列だけを並べる" : "値を 1 文字以上書く",
1257
+ });
1258
+ }
1259
+ }
1260
+ });
1261
+ }
1262
+
1228
1263
  function 表で中身を検査する(
1229
1264
  o: Record<string, unknown>,
1230
1265
  表: Record<string, 図形の定義>,
@@ -1258,6 +1293,10 @@ function 表で中身を検査する(
1258
1293
  });
1259
1294
  continue;
1260
1295
  }
1296
+ if (形 === "選択肢の並び") {
1297
+ 選択肢の並びを検査する(値, `${path}.${欄}`, errors);
1298
+ continue;
1299
+ }
1261
1300
  const 型が合う =
1262
1301
  形 === "数"
1263
1302
  ? typeof 値 === "number" && Number.isFinite(値)
@@ -1339,7 +1378,7 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
1339
1378
  errors.push({ path: `${path}.actor`, message: "band.actor must be a non-empty string" });
1340
1379
  }
1341
1380
  for (const k of ["from", "to"] as const) {
1342
- if (typeof o[k] !== "number" || !Number.isInteger(o[k]) || (o[k] as number) < 0) {
1381
+ if (typeof o[k] !== "number" || !Number.isInteger(o[k]) || o[k] < 0) {
1343
1382
  errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-negative integer` });
1344
1383
  }
1345
1384
  }
@@ -1478,12 +1517,15 @@ function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]):
1478
1517
  }
1479
1518
  }
1480
1519
  const 先に書かれた式 = new Set<string>();
1481
- for (const [名前, 式] of Object.entries(v as Record<string, unknown>)) {
1520
+ for (const [名前, 値] of Object.entries(v as Record<string, unknown>)) {
1482
1521
  const path = `$.formulas.${名前}`;
1483
1522
  if (!isValueName(名前)) {
1484
1523
  errors.push({ path, ...valueNameIssue(名前) });
1485
1524
  continue;
1486
1525
  }
1526
+ const 検査結果 = 式の組を検査する(値, path, errors);
1527
+ if (!検査結果.読めた) continue;
1528
+ const 式 = 検査結果.値;
1487
1529
  if (typeof 式 !== "string" || 式.trim() === "") {
1488
1530
  errors.push({
1489
1531
  path,
@@ -1525,6 +1567,55 @@ function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]):
1525
1567
  }
1526
1568
  }
1527
1569
 
1570
+ /**
1571
+ * 式の組を検査した結果 (#1916 / #2006)。
1572
+ *
1573
+ * **読めたかどうかを別の欄に持つ**。 値そのものに合図を混ぜると、`{ a: null }` の `null` が
1574
+ * 合図と区別が付かず、呼び手が「組の誤りは知らせ済み」 と読んで何も知らせずに通してしまう (実測)。
1575
+ *
1576
+ * 印 (`unique symbol`) を値に混ぜる形も採らない。 返す型が `unknown` を含むと印が飲み込まれ、
1577
+ * 呼び手は型で見分けられないまま等値比較に頼ることになる。
1578
+ */
1579
+ type 式の検査結果 = { readonly 読めた: true; readonly 値: unknown } | { readonly 読めた: false };
1580
+
1581
+ /**
1582
+ * 式の値が組の形 (`{ expression, label }`) なら組を検査し、式の文字列を返す (#1916)。
1583
+ *
1584
+ * 文字列やそれ以外の形は **そのまま返す** = 式の文字列の検査は呼び手が文字列の形と同じ経路で行う。
1585
+ * 組に誤りがあれば知らせて `読めた: false` を返す。
1586
+ */
1587
+ function 式の組を検査する(値: unknown, path: string, errors: JsonDslError[]): 式の検査結果 {
1588
+ if (!値 || typeof 値 !== "object" || Array.isArray(値)) return { 読めた: true, 値 };
1589
+ const 組 = 値 as Record<string, unknown>;
1590
+ let 読めた = true;
1591
+ for (const 鍵 of Object.keys(組)) {
1592
+ if (鍵 === "expression" || 鍵 === "label") continue;
1593
+ 読めた = false;
1594
+ errors.push({
1595
+ path: `${path}.${鍵}`,
1596
+ message: `unknown formula field: ${鍵}`,
1597
+ hint: "使える項目 = expression, label",
1598
+ });
1599
+ }
1600
+ if (typeof 組.expression !== "string" || 組.expression.trim() === "") {
1601
+ 読めた = false;
1602
+ errors.push({
1603
+ path: `${path}.expression`,
1604
+ message: "expression must be a non-empty expression string",
1605
+ hint: `got ${組.expression === undefined ? "nothing" : JSON.stringify(組.expression)}`,
1606
+ });
1607
+ }
1608
+ if (組.label !== undefined && (typeof 組.label !== "string" || 組.label.trim() === "")) {
1609
+ 読めた = false;
1610
+ errors.push({
1611
+ path: `${path}.label`,
1612
+ message: "label must be a non-empty string",
1613
+ hint: "名札を付けないなら label を書かない",
1614
+ });
1615
+ }
1616
+ return 読めた ? { 読めた: true, 値: 組.expression } : { 読めた: false };
1617
+ }
1618
+
1528
1619
  /**
1529
1620
  * 押下などの出来事で動く仕掛けを検査する (#1393)。
1530
1621
  *
@@ -1592,7 +1683,7 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
1592
1683
  }
1593
1684
  return;
1594
1685
  }
1595
- if (typeof o[鍵] !== "string" || (o[鍵] as string).trim() === "") {
1686
+ if (typeof o[鍵] !== "string" || o[鍵].trim() === "") {
1596
1687
  errors.push({ path: `${path}.${鍵}`, message: `${鍵} must be a non-empty string` });
1597
1688
  return;
1598
1689
  }
@@ -1927,7 +2018,7 @@ export function 素のデータに写す(
1927
2018
  // **名前も数に入れる** (Round 5 の指摘)。 値を読む前に名前の一覧を作るため、 値だけを
1928
2019
  // 数えると「名前が 20,000 個ある段を 63 回降りる」 形で 126 万個を並べられる = 上限を
1929
2020
  // 見る前に資源を使い切れる
1930
- const 名前の並び = Object.keys(v as Record<string, unknown>);
2021
+ const 名前の並び = Object.keys(v);
1931
2022
  項目数 += 名前の並び.length;
1932
2023
  if (項目数 > 写しの最大の項目数) {
1933
2024
  throw new 写せない(path, `項目が多すぎる (上限 ${写しの最大の項目数})`);
@@ -2082,7 +2173,6 @@ function validateJson(
2082
2173
  });
2083
2174
  }
2084
2175
  }
2085
- validateActorNodes(ao.nodes, `$.actors[${i}].nodes`, errors);
2086
2176
  // codex-review MAJOR fix = state override は plain object + 値は primitive (number / string / boolean) 限定、
2087
2177
  // `{ v: {} }` 等 nested object や null が流入すると CdlState.initial に不正な型が入り compile 崩れる。
2088
2178
  if (ao.state !== undefined) {
@@ -2210,7 +2300,7 @@ function validatePhaseMotion(po: Record<string, unknown>, i: number, errors: Jso
2210
2300
  const t = typeof value;
2211
2301
  if (t !== "number" && t !== "string") {
2212
2302
  errors.push({ path, message: "set value must be a number or string", hint: `got ${t}` });
2213
- } else if (t === "number" && !Number.isFinite(value as number)) {
2303
+ } else if (t === "number" && !Number.isFinite(value)) {
2214
2304
  errors.push({ path, message: "set value must be a finite number" });
2215
2305
  }
2216
2306
  }
@@ -2244,7 +2334,7 @@ function validateStates(v: unknown, errors: JsonDslError[]): void {
2244
2334
  message: "state initial must be a number or string",
2245
2335
  hint: `got ${t}`,
2246
2336
  });
2247
- } else if (t === "number" && !Number.isFinite(initial as number)) {
2337
+ } else if (t === "number" && !Number.isFinite(initial)) {
2248
2338
  // `NaN` / `Infinity` は JSON には書けないが、object を直接渡す経路では届く。
2249
2339
  // 描画側は文字列に直して式に流すため、そのまま通すと計算が全て壊れる
2250
2340
  errors.push({ path: `$.states.${name}`, message: "state initial must be a finite number" });
@@ -2310,7 +2400,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2310
2400
  return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: p0 };
2311
2401
  }
2312
2402
  // CAR-1657 = kind が既存 NodeKind に無い値なら parts identifier 候補、 partId に格納
2313
- const kindStr = (a.kind ?? "actor") as string;
2403
+ const kindStr = a.kind ?? "actor";
2314
2404
  const isPart = kindStr !== "actor" && !VALID_KIND_SET.has(kindStr);
2315
2405
  // 色は記法と同じ振り分けを通す (#1294)。 `#` で始まれば色番号、それ以外は色の名前。
2316
2406
  // 別々に書くと、同じ値が入口によって色番号にも色名にもなる
@@ -2355,20 +2445,13 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2355
2445
  opportunity: a.opportunity,
2356
2446
  }),
2357
2447
  colorHex: 色.hex,
2448
+ // 部品に書いた色の名前は効かない。 記法と同じく書いた名前として残し、組み立て側が知らせる (#1973)
2449
+ partColorName: isPart ? 色.tone : undefined,
2358
2450
  // 絶対座標と大きさ (#1294)。 `pos` (ずらし幅) とは別経路
2359
2451
  posX: a.posX,
2360
2452
  posY: a.posY,
2361
2453
  posW: a.posW,
2362
2454
  posH: a.posH,
2363
- // 箱の中の要素ごとの固定 (#1294)。 写しを作って外から書き換えられないようにする
2364
- nodes: a.nodes
2365
- ? Object.fromEntries(
2366
- Object.entries(a.nodes).map(([id, o]) => [
2367
- id,
2368
- { posX: o.posX, posY: o.posY, posW: o.posW, posH: o.posH },
2369
- ]),
2370
- )
2371
- : undefined,
2372
2455
  // 倍率は見本にしか効かない (検査が見本でない箱を弾く)。 書かれた名前は JSON では
2373
2456
  // 常に `scale` で、記法の別名 (`倍率`) は JSON に持ち込まない
2374
2457
  scale: isPart ? a.scale : undefined,
@@ -2389,13 +2472,15 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2389
2472
  to: s.to,
2390
2473
  label: s.label,
2391
2474
  sub: s.sub,
2392
- side: s.side as "top" | "right" | "bottom" | "left" | undefined,
2475
+ side: s.side,
2393
2476
  // 矢印の先の形 (#1462)。 読めない語は組み立てが落とす
2394
2477
  head: s.head,
2395
2478
  // 端の印の残り 3 欄と、関係の語 / 言づての種類 (#1466)
2396
2479
  tailHead: s.tailHead,
2397
2480
  headFill: s.headFill,
2398
2481
  tailHeadFill: s.tailHeadFill,
2482
+ // クラス図の出どころ側の多重度 (#1771)
2483
+ tailSub: s.tailSub,
2399
2484
  relation: s.relation,
2400
2485
  // 辺の役目と名前の下地 (cdl#618)
2401
2486
  role: s.role,
@@ -2414,6 +2499,9 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2414
2499
  labelOffsetX: s.labelOffsetX,
2415
2500
  labelOffsetY: s.labelOffsetY,
2416
2501
  overlay: s.overlay,
2502
+ // 部品の端で繋ぐ要素の id (#1979)。 書かない矢印には欄を足さない = 記法と同じ形になる
2503
+ ...(s.fromPartNode !== undefined ? { fromPartNode: s.fromPartNode } : {}),
2504
+ ...(s.toPartNode !== undefined ? { toPartNode: s.toPartNode } : {}),
2417
2505
  // CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos
2418
2506
  layoutPos: s.pos,
2419
2507
  pos: p0,
@@ -2449,9 +2537,19 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2449
2537
  const scrolls: DslScrollTrigger[] | undefined = json.scrolls
2450
2538
  ? Object.entries(json.scrolls).map(([id, spec]) => ({ id, ...spec }))
2451
2539
  : undefined;
2452
- // 式は `{ 名前: "式" }` から並びへ写す (#1391)。 記法側と同じ形にして組み立てを 1 本にする
2540
+ // 式は `{ 名前: "式" }` から並びへ写す (#1391)。 記法側と同じ形にして組み立てを 1 本にする。
2541
+ // 組の形 (`{ expression, label }`) は名札を持ったまま写す (#1916)
2453
2542
  const formulas: DslFormula[] | undefined = json.formulas
2454
- ? Object.entries(json.formulas).map(([id, expression]) => ({ id, expression, pos: p0 }))
2543
+ ? Object.entries(json.formulas).map(([id, 値]) =>
2544
+ typeof 値 === "string"
2545
+ ? { id, expression: 値, pos: p0 }
2546
+ : {
2547
+ id,
2548
+ expression: 値.expression,
2549
+ ...(値.label !== undefined ? { label: 値.label } : {}),
2550
+ pos: p0,
2551
+ },
2552
+ )
2455
2553
  : undefined;
2456
2554
  const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
2457
2555
  name,
@@ -2538,6 +2636,8 @@ export function jsonToDoc(json: DragonJson): DslDocument {
2538
2636
  bands: json.bands,
2539
2637
  // 矢印をいつ出すか (#1470)
2540
2638
  reveal: json.reveal,
2639
+ // 箱に触れると関係する線だけを光らせるか (#1757)
2640
+ relations: json.relations,
2541
2641
  // 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す
2542
2642
  ...(json.direction !== undefined ? { direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const) } : {}),
2543
2643
  // 図の配色 (#1553)。 記法と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方がずれない。