@cardenelabs/dragon 0.28.0 → 0.30.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/README.md +78 -5
- package/dist/index.cjs +9246 -8698
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +186 -66
- package/dist/index.d.ts +186 -66
- package/dist/index.js +9247 -8700
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/compile/actor-ids.ts +306 -0
- package/src/compile/actors.ts +74 -0
- package/src/compile/c4.ts +77 -0
- package/src/compile/chain.ts +54 -0
- package/src/compile/chart-fields.ts +178 -0
- package/src/compile/class.ts +100 -0
- package/src/compile/deep-rewrite.ts +30 -0
- package/src/compile/derived-source.ts +18 -0
- package/src/compile/direction.ts +63 -0
- package/src/compile/er-relation.ts +200 -0
- package/src/compile/er.ts +44 -0
- package/src/compile/flow.ts +54 -0
- package/src/compile/funnel.ts +75 -0
- package/src/compile/gantt.ts +232 -0
- package/src/compile/generic.ts +284 -0
- package/src/compile/hierarchy.ts +92 -0
- package/src/compile/journey.ts +84 -0
- package/src/compile/kinds.ts +156 -0
- package/src/compile/lanes.ts +56 -0
- package/src/compile/mind.ts +343 -0
- package/src/compile/node-title.ts +24 -0
- package/src/compile/notice.ts +94 -0
- package/src/compile/parts-lookup.ts +17 -0
- package/src/compile/parts.ts +2635 -0
- package/src/compile/placement-apply.ts +673 -0
- package/src/compile/placement.ts +15 -0
- package/src/compile/quadrant.ts +110 -0
- package/src/compile/rows.ts +40 -0
- package/src/compile/sequence.ts +226 -0
- package/src/compile/slug.ts +29 -0
- package/src/compile/solidity.ts +39 -0
- package/src/compile/state.ts +71 -0
- package/src/compile/subtitle.ts +22 -0
- package/src/compile/swimlane.ts +83 -0
- package/src/compile/topology.ts +50 -0
- package/src/compile/tree.ts +60 -0
- package/src/compile/value-chart.ts +183 -0
- package/src/compile/values.ts +432 -0
- package/src/compile/word-state.ts +82 -0
- package/src/compile.ts +513 -6858
- package/src/index.ts +8 -7
- package/src/json-line-map.ts +30 -0
- package/src/json-parser.ts +356 -123
- package/src/near-name.ts +42 -0
- package/src/relative-pos.ts +71 -2
- package/src/schema.ts +1 -1
- package/src/schemas/diagram.json +38 -4
- package/src/types.ts +1 -1
- package/src/v05/parser.ts +91 -72
package/src/json-parser.ts
CHANGED
|
@@ -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 に修正させる
|
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
部品の組の表,
|
|
35
35
|
つまみの表,
|
|
36
36
|
EVENT_KINDS,
|
|
37
|
+
EVENT_TARGET_KEYS,
|
|
38
|
+
EVENT_REQUIRED_KEYS,
|
|
37
39
|
type 図形の定義,
|
|
38
40
|
} from "./v05/parser";
|
|
39
41
|
// 図の配色 (#1553)。 記法の読み手と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方が
|
|
@@ -71,7 +73,15 @@ import type {
|
|
|
71
73
|
DslDynShape,
|
|
72
74
|
} from "./types";
|
|
73
75
|
import { checkValueExpression, isValueName, valueNameIssue } from "./value-syntax";
|
|
76
|
+
import { 近い名前 } from "./near-name";
|
|
77
|
+
import { 書いた場所の鍵, type 書いた行の表 } from "./json-line-map";
|
|
74
78
|
import { compileToCdl } from "./compile";
|
|
79
|
+
import {
|
|
80
|
+
RELATIVE_DIRECTIONS,
|
|
81
|
+
findRelativeProblems,
|
|
82
|
+
type RelativeDirection,
|
|
83
|
+
type RelativePos,
|
|
84
|
+
} from "./relative-pos";
|
|
75
85
|
|
|
76
86
|
/**
|
|
77
87
|
* LLM 向け JSON DSL の入力 shape。 YAML DSL と 1:1 対応、 top-level は flat な object。
|
|
@@ -211,6 +221,24 @@ export interface DragonJson {
|
|
|
211
221
|
palette?: DslPalette;
|
|
212
222
|
}
|
|
213
223
|
|
|
224
|
+
/**
|
|
225
|
+
* 位置を他の要素からの相対で書く指定の、JSON 側の形 (#2039)。
|
|
226
|
+
*
|
|
227
|
+
* 記法の `位置: Web の右 200` を 3 つの欄に開いたもの。 内側の値は `RelativePos` と同じで、
|
|
228
|
+
* 向きの語も同じ 4 つ (`right` / `left` / `above` / `below`) を使う。
|
|
229
|
+
*
|
|
230
|
+
* **向きの語を並べ直さない**。 `RelativeDirection` をそのまま使うことで、本文側に向きを
|
|
231
|
+
* 足した時に JSON 側が黙って取り残される形を作らない。
|
|
232
|
+
*/
|
|
233
|
+
export interface JsonRelativePos {
|
|
234
|
+
/** 基準にする相手の名前。 `actors:` に書かれた名前をそのまま書く */
|
|
235
|
+
anchor: string;
|
|
236
|
+
/** 基準からどちら側に置くか */
|
|
237
|
+
dir: RelativeDirection;
|
|
238
|
+
/** 相手との間隔。 書かなければ `RELATIVE_GAP_DEFAULT` */
|
|
239
|
+
gap?: number;
|
|
240
|
+
}
|
|
241
|
+
|
|
214
242
|
export interface JsonActor {
|
|
215
243
|
name: string;
|
|
216
244
|
/**
|
|
@@ -239,8 +267,9 @@ export interface JsonActor {
|
|
|
239
267
|
renderOffsetX?: number | string;
|
|
240
268
|
renderOffsetY?: number | string;
|
|
241
269
|
/**
|
|
242
|
-
* CAR-1657 unified syntax = 既存 NodeKind (
|
|
243
|
-
*
|
|
270
|
+
* CAR-1657 unified syntax = 既存 NodeKind (一覧は `v05/parser.ts` の `NODE_KIND_VALID`) に
|
|
271
|
+
* 加えて parts identifier (arc-gauge 等) を accept する。
|
|
272
|
+
* 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
|
|
244
273
|
* LLM structured output の typing 制約を緩めるため union に string 追加。
|
|
245
274
|
* `string & {}` = NodeKind の候補を IDE 補完で提示しつつ任意 string も許容する idiom。
|
|
246
275
|
* 素の `NodeKind | string` は no-redundant-type-constituents に抵触し補完も潰れる (#865)。
|
|
@@ -266,6 +295,17 @@ export interface JsonActor {
|
|
|
266
295
|
* と同じで、読み方は `DslActor.layoutPos` が持つ。
|
|
267
296
|
*/
|
|
268
297
|
pos?: LayoutPos;
|
|
298
|
+
/**
|
|
299
|
+
* 位置を他の要素からの相対で書く指定 (#2039)。 記法の `位置: Web の右 200` と同じ。
|
|
300
|
+
*
|
|
301
|
+
* 記法は 1 本の文字列に「誰の」「どちら側に」「どれだけ離して」 を詰めるが、JSON では
|
|
302
|
+
* 3 つの欄に分ける。 文字列を組み立てて渡す形にすると、書く側が記法の並び順と区切りを
|
|
303
|
+
* 覚える必要が出る = JSON を書くのは主に LLM で、読み取りの規則は欄の名前で示す方が短い。
|
|
304
|
+
*
|
|
305
|
+
* 組み立ての段階で基準の実座標から `posX` / `posY` に直すため、解決した後は座標を直接
|
|
306
|
+
* 書いた時と同じ経路を通る。 `pos` (配置後のずらし幅) とは別の欄。
|
|
307
|
+
*/
|
|
308
|
+
posRel?: JsonRelativePos;
|
|
269
309
|
/**
|
|
270
310
|
* CAR-1657 parts state override (kind = parts identifier 時のみ有効)。
|
|
271
311
|
* LLM JSON DSL では nested 明示 = `{ "state": { "v": 50 } }` が natural、 human 側の
|
|
@@ -325,6 +365,17 @@ export interface JsonActor {
|
|
|
325
365
|
* 読めない項目名として知らせる)。
|
|
326
366
|
*/
|
|
327
367
|
scale?: number;
|
|
368
|
+
/**
|
|
369
|
+
* 見本が持つ段を使うか (#2125)。 記法の `{ kind: wave-gauge, phase: false }` と同じ。
|
|
370
|
+
*
|
|
371
|
+
* 既定 (書かない) は見本の段を書いた段と番号ごとに並べて再生する。 `false` を書くと
|
|
372
|
+
* 見本の段の中身 (点灯と値の変化) を空にし、値は書いた段だけが動かす。
|
|
373
|
+
*
|
|
374
|
+
* 見本 (parts) にしか効かない。 中身は状態の上書き (`state`) に `phase` として入る =
|
|
375
|
+
* `"state": { "phase": false }` と同じ所へ届く。 記法が中括弧の中に 1 つの名前として
|
|
376
|
+
* 書ける以上、JSON でも同じ名前で書けるようにする (入口で書き方が分かれない)。
|
|
377
|
+
*/
|
|
378
|
+
phase?: boolean;
|
|
328
379
|
/** 行頭の印 (#1466)。 行と対で読む。 語の意味は図の種類が決める */
|
|
329
380
|
marks?: string[];
|
|
330
381
|
}
|
|
@@ -451,11 +502,13 @@ export interface JsonDslError {
|
|
|
451
502
|
* 受け付ける種類 (`kind`)。 **記法側と同じ集合を使う** (`v05/parser.ts` の `NODE_KIND_VALID`)。
|
|
452
503
|
*
|
|
453
504
|
* ここに無い値は見本 (parts) の名前とみなし、`partId` へ退避して箱を `actor` に倒す
|
|
454
|
-
* (CAR-1657 の unified syntax)。
|
|
505
|
+
* (CAR-1657 の unified syntax)。 つまりこの集合が小さいほど、**書いた種類が箱の見た目に効かなくなる**。
|
|
506
|
+
* 部品を持つ一覧を渡した組み立てでは一覧に無い名前を知らせる (`part-not-found`、#2113) が、
|
|
507
|
+
* 一覧を渡さない組み立てと部品を持たない一覧の組み立てでは `console.warn` にしか残らない。
|
|
455
508
|
*
|
|
456
509
|
* 以前はここに一覧を写していたため、記法に種類が増えても JSON 経路が古い一覧のまま
|
|
457
510
|
* 見本の名前として扱った (実測 = 記法が知る 108 種のうち 77 種が該当、`shape-*` は全滅)。
|
|
458
|
-
*
|
|
511
|
+
* 当時はカタログに無い名前が `console.warn` にしか残らず `onNotice` を呼ばなかったため、
|
|
459
512
|
* 画面には何も出ないまま `actor` の箱が描かれていた (#1293)。
|
|
460
513
|
*
|
|
461
514
|
* 型が 2 箇所に散らばると、必ずどちらかが古くなる。 `VALID_PRESETS` (図種) と同じ扱いにする。
|
|
@@ -557,6 +610,10 @@ export const ACCEPTED_KEYS = {
|
|
|
557
610
|
"renderOffsetY",
|
|
558
611
|
// 行頭の印 (#1466)。 行ごとに 1 つ、図の種類ごとの語で書く
|
|
559
612
|
"marks",
|
|
613
|
+
// 位置を他の要素からの相対で書く指定 (#2039)
|
|
614
|
+
"posRel",
|
|
615
|
+
// 見本が持つ段を使うか (#2125)
|
|
616
|
+
"phase",
|
|
560
617
|
],
|
|
561
618
|
step: [
|
|
562
619
|
"from",
|
|
@@ -602,6 +659,8 @@ export const ACCEPTED_KEYS = {
|
|
|
602
659
|
axesX: ["left", "right"],
|
|
603
660
|
axesY: ["bottom", "top"],
|
|
604
661
|
layoutPos: ["x", "y"],
|
|
662
|
+
// 位置を他の要素からの相対で書く指定 (#2039)。 間隔は書かなくてよい
|
|
663
|
+
posRel: ["anchor", "dir", "gap"],
|
|
605
664
|
} as const satisfies Record<string, readonly string[]>;
|
|
606
665
|
|
|
607
666
|
/** 受ける項目を持つ階層の名前 */
|
|
@@ -643,6 +702,8 @@ export type 欄の型 =
|
|
|
643
702
|
| "線種"
|
|
644
703
|
| "辺"
|
|
645
704
|
| "端の形"
|
|
705
|
+
// 相対で置く時の向き (#2039)。 書けば必ず要る = 向きが無いとどちら側か決まらない
|
|
706
|
+
| "必須の向き"
|
|
646
707
|
| "色か色番号"
|
|
647
708
|
| "描くもの"
|
|
648
709
|
| "必須の図種"
|
|
@@ -724,6 +785,10 @@ export const 欄の型表 = {
|
|
|
724
785
|
renderOffsetY: "数か文字列",
|
|
725
786
|
// 行頭の印 (#1466)
|
|
726
787
|
marks: "文字列の並び",
|
|
788
|
+
// 相対で置く指定 (#2039)。 中身の形は `validateRelativePos` が見る
|
|
789
|
+
posRel: "object",
|
|
790
|
+
// 見本が持つ段を使うか (#2125)
|
|
791
|
+
phase: "真偽",
|
|
727
792
|
},
|
|
728
793
|
step: {
|
|
729
794
|
from: "必須の文字列",
|
|
@@ -795,6 +860,8 @@ export const 欄の型表 = {
|
|
|
795
860
|
axesY: { bottom: "文字列", top: "文字列" },
|
|
796
861
|
// 位置は書けば x と y の両方が要る。 片方だけでは寄せ幅が決まらない
|
|
797
862
|
layoutPos: { x: "必須の数", y: "必須の数" },
|
|
863
|
+
// 相対で置く指定 (#2039)。 相手と向きは必ず要り、間隔は書かなければ既定で置く
|
|
864
|
+
posRel: { anchor: "必須の非空文字列", dir: "必須の向き", gap: "数" },
|
|
798
865
|
} as const satisfies {
|
|
799
866
|
[層 in 階層]: { [欄 in (typeof ACCEPTED_KEYS)[層][number]]: 欄の型 };
|
|
800
867
|
};
|
|
@@ -839,6 +906,8 @@ function JSONの色名か(v: string): boolean {
|
|
|
839
906
|
export const 見本にしか効かない欄 = [
|
|
840
907
|
"state",
|
|
841
908
|
"scale",
|
|
909
|
+
// 見本が持つ段を使うか (#2125)。 普通の箱は自分の段を持たないので効かない
|
|
910
|
+
"phase",
|
|
842
911
|
] as const satisfies readonly (typeof ACCEPTED_KEYS.actor)[number][];
|
|
843
912
|
|
|
844
913
|
/**
|
|
@@ -997,6 +1066,17 @@ function 値を検査(
|
|
|
997
1066
|
});
|
|
998
1067
|
}
|
|
999
1068
|
return;
|
|
1069
|
+
case "必須の向き":
|
|
1070
|
+
// 受ける語は記法と同じ一覧を見る (`RELATIVE_DIRECTIONS`)。 写すと向きが増えた時に
|
|
1071
|
+
// 片方だけ古くなる
|
|
1072
|
+
if (typeof v !== "string" || !(RELATIVE_DIRECTIONS as readonly string[]).includes(v)) {
|
|
1073
|
+
errors.push({
|
|
1074
|
+
path,
|
|
1075
|
+
message: `${名前} must be one of: ${RELATIVE_DIRECTIONS.join(", ")}`,
|
|
1076
|
+
hint: typeof v === "string" ? `got "${v}"` : `got ${v === undefined ? "nothing" : typeof v}`,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1000
1080
|
case "端の形":
|
|
1001
1081
|
if (v === undefined) return;
|
|
1002
1082
|
// 受ける語は記法と同じ一覧を見る (`EDGE_HEAD_VALUES`)。 写すと描画側が形を増やした時に
|
|
@@ -1086,48 +1166,6 @@ function 表で検査(
|
|
|
1086
1166
|
}
|
|
1087
1167
|
}
|
|
1088
1168
|
|
|
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
1169
|
/**
|
|
1132
1170
|
* 知らない項目を誤りとして積む (#1295)。
|
|
1133
1171
|
*
|
|
@@ -1139,7 +1177,7 @@ function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDsl
|
|
|
1139
1177
|
const 受ける = ACCEPTED_KEYS[層] as readonly string[];
|
|
1140
1178
|
for (const key of Object.keys(v)) {
|
|
1141
1179
|
if (受ける.includes(key)) continue;
|
|
1142
|
-
const 候補 =
|
|
1180
|
+
const 候補 = 近い名前(key, 受ける);
|
|
1143
1181
|
errors.push({
|
|
1144
1182
|
path: `${path}.${key}`,
|
|
1145
1183
|
message: `unknown key "${key}"`,
|
|
@@ -1148,10 +1186,6 @@ function checkUnknownKeys(v: unknown, 層: 階層, path: string, errors: JsonDsl
|
|
|
1148
1186
|
}
|
|
1149
1187
|
}
|
|
1150
1188
|
|
|
1151
|
-
/**
|
|
1152
|
-
* shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
|
|
1153
|
-
* fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
|
|
1154
|
-
*/
|
|
1155
1189
|
/**
|
|
1156
1190
|
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` の型 check helper。 finite number pair を必須にし、
|
|
1157
1191
|
* `NaN` / `Infinity` / non-number は reject する (組み立ての出口 `applyLayoutOffsets` が配置後の位置に足すため)。
|
|
@@ -1167,6 +1201,77 @@ function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): vo
|
|
|
1167
1201
|
表で検査(p, "layoutPos", path, "pos", errors);
|
|
1168
1202
|
}
|
|
1169
1203
|
|
|
1204
|
+
/**
|
|
1205
|
+
* 相対で置く指定の形を見る (#2039)。
|
|
1206
|
+
*
|
|
1207
|
+
* 相手の名前が実在するかはここでは見ない。 組み立て側 (`resolveRelativeDoc`) が figure 全体を
|
|
1208
|
+
* 組んだ後に照合し、いない相手を指した時は知らせを出す。 入口ごとに別の場所で判定すると、
|
|
1209
|
+
* 本文で書いた時と JSON で書いた時で返るものが変わる。
|
|
1210
|
+
*/
|
|
1211
|
+
function validateRelativePos(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
1212
|
+
if (v === undefined) return;
|
|
1213
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
1214
|
+
errors.push({ path, message: "posRel must be an object with anchor and dir" });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
const p = v as Record<string, unknown>;
|
|
1218
|
+
checkUnknownKeys(p, "posRel", path, errors);
|
|
1219
|
+
表で検査(p, "posRel", path, "posRel", errors);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* 図全体として相対の指定が解けるかを見る (#2039)。
|
|
1224
|
+
*
|
|
1225
|
+
* 判定は `relative-pos.ts` の `findRelativeProblems` が持つ。 記法側も同じ判定を呼ぶので、
|
|
1226
|
+
* 同じ書き方が入口によって通ったり通らなかったりしない。
|
|
1227
|
+
*
|
|
1228
|
+
* 誤りの文だけをここで作る = 記法側は行番号を、こちらは欄の場所を添えるため。
|
|
1229
|
+
*/
|
|
1230
|
+
function validateRelativeAnchors(actors: readonly unknown[], errors: JsonDslError[]): void {
|
|
1231
|
+
const items: Array<{ name: string; rel?: RelativePos }> = [];
|
|
1232
|
+
const 場所 = new Map<string, number>();
|
|
1233
|
+
actors.forEach((a, i) => {
|
|
1234
|
+
if (!a || typeof a !== "object" || Array.isArray(a)) return;
|
|
1235
|
+
const o = a as Record<string, unknown>;
|
|
1236
|
+
if (typeof o.name !== "string") return;
|
|
1237
|
+
const rel = o.posRel;
|
|
1238
|
+
// 形の誤りは `validateRelativePos` が既に返している。 ここは形の整った指定だけを見る
|
|
1239
|
+
const 整った =
|
|
1240
|
+
rel !== null &&
|
|
1241
|
+
typeof rel === "object" &&
|
|
1242
|
+
!Array.isArray(rel) &&
|
|
1243
|
+
typeof (rel as Record<string, unknown>).anchor === "string" &&
|
|
1244
|
+
typeof (rel as Record<string, unknown>).dir === "string";
|
|
1245
|
+
items.push({ name: o.name, rel: 整った ? (rel as RelativePos) : undefined });
|
|
1246
|
+
if (!場所.has(o.name)) 場所.set(o.name, i);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const named = items.map((i) => i.name);
|
|
1250
|
+
for (const p of findRelativeProblems(items)) {
|
|
1251
|
+
const i = 場所.get(p.name) ?? 0;
|
|
1252
|
+
errors.push({
|
|
1253
|
+
path: `$.actors[${i}].posRel`,
|
|
1254
|
+
...(p.kind === "self"
|
|
1255
|
+
? {
|
|
1256
|
+
message: `posRel.anchor must not be the actor itself: "${p.name}"`,
|
|
1257
|
+
hint: "別の登場人物の名前を書く",
|
|
1258
|
+
}
|
|
1259
|
+
: p.kind === "missing-anchor"
|
|
1260
|
+
? {
|
|
1261
|
+
message: `posRel.anchor must name an actor: "${p.anchor ?? ""}"`,
|
|
1262
|
+
hint:
|
|
1263
|
+
named.length > 0
|
|
1264
|
+
? `actors に書かれている名前 = ${named.join(", ")}`
|
|
1265
|
+
: "actors に基準にする登場人物を書く",
|
|
1266
|
+
}
|
|
1267
|
+
: {
|
|
1268
|
+
message: `posRel.anchor must not form a cycle: "${p.name}"`,
|
|
1269
|
+
hint: "どれか 1 つは posX / posY か自動配置にする",
|
|
1270
|
+
}),
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1170
1275
|
/**
|
|
1171
1276
|
* 見本 (parts) の名前として扱う値かどうか (#1294)。
|
|
1172
1277
|
*
|
|
@@ -1196,22 +1301,13 @@ function validateViewport(v: unknown, errors: JsonDslError[]): void {
|
|
|
1196
1301
|
}
|
|
1197
1302
|
|
|
1198
1303
|
/**
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
* `lanes` / `groups` は id を鍵に持つため、鍵そのものは縛れない。 縛れるのは
|
|
1202
|
-
* 「入れ物が plain object か」 と「各 id の中身の項目名と値の型」 の 2 つ。
|
|
1203
|
-
*
|
|
1204
|
-
* #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
|
|
1205
|
-
* 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
|
|
1206
|
-
*/
|
|
1207
|
-
/**
|
|
1208
|
-
* 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
|
|
1304
|
+
* 選択肢の組が受ける項目。 どれも必ず書く (#2258)。
|
|
1209
1305
|
*
|
|
1210
|
-
*
|
|
1211
|
-
*
|
|
1212
|
-
* 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
|
|
1213
|
-
* 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
|
|
1306
|
+
* **判定と知らせが同じ集合を読む**。 別々に並べていた間、判定に項目を足しても
|
|
1307
|
+
* 知らせは古い一覧を教え続ける形だった。
|
|
1214
1308
|
*/
|
|
1309
|
+
const 選択肢の項目 = ["value", "label"] as const;
|
|
1310
|
+
|
|
1215
1311
|
/**
|
|
1216
1312
|
* 選択肢の並び (`["green", { "value": "red", "label": "失敗" }]`) を要素ごとに検査する (#1920)。
|
|
1217
1313
|
*
|
|
@@ -1240,13 +1336,21 @@ function 選択肢の並びを検査する(値: unknown, path: string, errors: J
|
|
|
1240
1336
|
}
|
|
1241
1337
|
const 組 = x as Record<string, unknown>;
|
|
1242
1338
|
for (const k of Object.keys(組)) {
|
|
1243
|
-
if (
|
|
1244
|
-
errors.push({
|
|
1339
|
+
if ((選択肢の項目 as readonly string[]).includes(k)) continue;
|
|
1340
|
+
errors.push({
|
|
1341
|
+
path: `${here}.${k}`,
|
|
1342
|
+
message: `unknown key "${k}"`,
|
|
1343
|
+
hint: `使える項目 = ${選択肢の項目.join(", ")}`,
|
|
1344
|
+
});
|
|
1245
1345
|
}
|
|
1246
|
-
for (const k of
|
|
1346
|
+
for (const k of 選択肢の項目) {
|
|
1247
1347
|
const v = 組[k];
|
|
1248
1348
|
if (v === undefined) {
|
|
1249
|
-
errors.push({
|
|
1349
|
+
errors.push({
|
|
1350
|
+
path: `${here}.${k}`,
|
|
1351
|
+
message: `${k} is required`,
|
|
1352
|
+
hint: `必須の項目 = ${選択肢の項目.join(", ")}`,
|
|
1353
|
+
});
|
|
1250
1354
|
} else if (typeof v !== "string") {
|
|
1251
1355
|
errors.push({ path: `${here}.${k}`, message: `${k} must be 文字列`, hint: `got ${v === null ? "null" : typeof v}` });
|
|
1252
1356
|
} else if (v === "") {
|
|
@@ -1260,6 +1364,14 @@ function 選択肢の並びを検査する(値: unknown, path: string, errors: J
|
|
|
1260
1364
|
});
|
|
1261
1365
|
}
|
|
1262
1366
|
|
|
1367
|
+
/**
|
|
1368
|
+
* 図形と部品の中身を、記法と **同じ表** で検査する (#1374)。
|
|
1369
|
+
*
|
|
1370
|
+
* 表を 2 つ持つと片方だけ直してずれる。 記法側 (`v05/parser.ts`) が持つ表をそのまま引く。
|
|
1371
|
+
*
|
|
1372
|
+
* 記法は値が全て文字列で届くため読み替えが要るが、JSON は型のまま届く。 ここでは
|
|
1373
|
+
* 「知らない種類」 「知らない欄」 「足りない必須の欄」 「欄の型違い」 の 4 つを見る。
|
|
1374
|
+
*/
|
|
1263
1375
|
function 表で中身を検査する(
|
|
1264
1376
|
o: Record<string, unknown>,
|
|
1265
1377
|
表: Record<string, 図形の定義>,
|
|
@@ -1344,18 +1456,30 @@ function 表で中身を検査する(
|
|
|
1344
1456
|
}
|
|
1345
1457
|
}
|
|
1346
1458
|
|
|
1459
|
+
/** 帯に書く相手の項目。 値が文字列で、他の 2 つと型が違う */
|
|
1460
|
+
const 帯の相手の項目 = ["actor"] as const;
|
|
1461
|
+
/** 帯に書く段の項目。 値が 0 以上の整数 */
|
|
1462
|
+
const 帯の段の項目 = ["from", "to"] as const;
|
|
1347
1463
|
/**
|
|
1348
|
-
*
|
|
1464
|
+
* 帯が受ける項目。 判定と知らせの両方がここから作る (#2258)。
|
|
1349
1465
|
*
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
* 素通りする。
|
|
1466
|
+
* 公開 schema の `$.bands` も同じ 3 語を閉じた集合として宣言しており、
|
|
1467
|
+
* `closed-level-coverage.test.ts` が両者の一致を見る。
|
|
1353
1468
|
*/
|
|
1469
|
+
export const 帯の項目: readonly string[] = [...帯の相手の項目, ...帯の段の項目];
|
|
1470
|
+
|
|
1354
1471
|
/**
|
|
1355
1472
|
* 順序図の帯の並びを検査する (#1466)。
|
|
1356
1473
|
*
|
|
1357
1474
|
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
1358
1475
|
* 委ねる作りなので、ここで見ないと `bands: 1` が素通りする。
|
|
1476
|
+
*
|
|
1477
|
+
* ## 知らない項目も見る (#2262)
|
|
1478
|
+
*
|
|
1479
|
+
* 書いてある 3 つの型だけを見て、**知らない項目があるかを確かめていなかった**。
|
|
1480
|
+
* 公開 schema は `$.bands` を閉じた階層として宣言しているので、
|
|
1481
|
+
* schema を信じて書いた入力がこちらでは通り、schema 検証器では落ちる状態だった。
|
|
1482
|
+
* 公開 schema が閉じた階層のうち、素通りしていたのはここだけだった。
|
|
1359
1483
|
*/
|
|
1360
1484
|
function validateBands(v: unknown, errors: JsonDslError[]): void {
|
|
1361
1485
|
if (v === undefined) return;
|
|
@@ -1374,10 +1498,20 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
|
|
|
1374
1498
|
return;
|
|
1375
1499
|
}
|
|
1376
1500
|
const o = b as Record<string, unknown>;
|
|
1377
|
-
|
|
1378
|
-
|
|
1501
|
+
for (const k of Object.keys(o)) {
|
|
1502
|
+
if (帯の項目.includes(k)) continue;
|
|
1503
|
+
errors.push({
|
|
1504
|
+
path: `${path}.${k}`,
|
|
1505
|
+
message: `unknown key "${k}"`,
|
|
1506
|
+
hint: `使える項目 = ${帯の項目.join(", ")}`,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
for (const k of 帯の相手の項目) {
|
|
1510
|
+
if (typeof o[k] !== "string" || o[k] === "") {
|
|
1511
|
+
errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-empty string` });
|
|
1512
|
+
}
|
|
1379
1513
|
}
|
|
1380
|
-
for (const k of
|
|
1514
|
+
for (const k of 帯の段の項目) {
|
|
1381
1515
|
if (typeof o[k] !== "number" || !Number.isInteger(o[k]) || o[k] < 0) {
|
|
1382
1516
|
errors.push({ path: `${path}.${k}`, message: `band.${k} must be a non-negative integer` });
|
|
1383
1517
|
}
|
|
@@ -1385,6 +1519,13 @@ function validateBands(v: unknown, errors: JsonDslError[]): void {
|
|
|
1385
1519
|
});
|
|
1386
1520
|
}
|
|
1387
1521
|
|
|
1522
|
+
/**
|
|
1523
|
+
* 値を見せる部品の並びを検査する (#1374)。
|
|
1524
|
+
*
|
|
1525
|
+
* **外側の形もここで見る**。 `欄の型表` は「並び」 とだけ宣言し、中身の検査は専用の検査に
|
|
1526
|
+
* 委ねる作りなので (`checkFieldType` の `case "並び"`)、ここで見ないと `readouts: 1` が
|
|
1527
|
+
* 素通りする。
|
|
1528
|
+
*/
|
|
1388
1529
|
function validateReadouts(v: unknown, errors: JsonDslError[]): void {
|
|
1389
1530
|
if (v === undefined) return;
|
|
1390
1531
|
if (!Array.isArray(v)) {
|
|
@@ -1578,6 +1719,11 @@ function validateFormulas(v: unknown, inputs: unknown, errors: JsonDslError[]):
|
|
|
1578
1719
|
*/
|
|
1579
1720
|
type 式の検査結果 = { readonly 読めた: true; readonly 値: unknown } | { readonly 読めた: false };
|
|
1580
1721
|
|
|
1722
|
+
/**
|
|
1723
|
+
* 式の組が受ける項目 (#2258)。 判定と知らせが同じ集合を読む。
|
|
1724
|
+
*/
|
|
1725
|
+
const 式の組の項目 = ["expression", "label"] as const;
|
|
1726
|
+
|
|
1581
1727
|
/**
|
|
1582
1728
|
* 式の値が組の形 (`{ expression, label }`) なら組を検査し、式の文字列を返す (#1916)。
|
|
1583
1729
|
*
|
|
@@ -1589,12 +1735,12 @@ function 式の組を検査する(値: unknown, path: string, errors: JsonDslErr
|
|
|
1589
1735
|
const 組 = 値 as Record<string, unknown>;
|
|
1590
1736
|
let 読めた = true;
|
|
1591
1737
|
for (const 鍵 of Object.keys(組)) {
|
|
1592
|
-
if (
|
|
1738
|
+
if ((式の組の項目 as readonly string[]).includes(鍵)) continue;
|
|
1593
1739
|
読めた = false;
|
|
1594
1740
|
errors.push({
|
|
1595
1741
|
path: `${path}.${鍵}`,
|
|
1596
1742
|
message: `unknown formula field: ${鍵}`,
|
|
1597
|
-
hint:
|
|
1743
|
+
hint: `使える項目 = ${式の組の項目.join(", ")}`,
|
|
1598
1744
|
});
|
|
1599
1745
|
}
|
|
1600
1746
|
if (typeof 組.expression !== "string" || 組.expression.trim() === "") {
|
|
@@ -1632,7 +1778,11 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1632
1778
|
});
|
|
1633
1779
|
return;
|
|
1634
1780
|
}
|
|
1635
|
-
|
|
1781
|
+
// 相手の指し方は記法の側が 1 か所で決める (#2256)。 同じ 4 語をここにも並べていた間、
|
|
1782
|
+
// 1:1 を名乗りながら受ける項目が両側で別々に決まっていた。
|
|
1783
|
+
// 必ず書く 2 つも同じ定数から取る (#2258) = 知らせが判定と別に並べる形をやめる
|
|
1784
|
+
const 相手の鍵: readonly string[] = EVENT_TARGET_KEYS;
|
|
1785
|
+
const 出来事の項目: readonly string[] = [...EVENT_REQUIRED_KEYS, ...EVENT_TARGET_KEYS];
|
|
1636
1786
|
v.forEach((e, i) => {
|
|
1637
1787
|
const path = `$.events[${i}]`;
|
|
1638
1788
|
if (!e || typeof e !== "object" || Array.isArray(e)) {
|
|
@@ -1641,11 +1791,11 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1641
1791
|
}
|
|
1642
1792
|
const o = e as Record<string, unknown>;
|
|
1643
1793
|
for (const k of Object.keys(o)) {
|
|
1644
|
-
if (
|
|
1794
|
+
if (出来事の項目.includes(k)) continue;
|
|
1645
1795
|
errors.push({
|
|
1646
1796
|
path: `${path}.${k}`,
|
|
1647
1797
|
message: `unknown key "${k}"`,
|
|
1648
|
-
hint: `使える項目 =
|
|
1798
|
+
hint: `使える項目 = ${出来事の項目.join(", ")}`,
|
|
1649
1799
|
});
|
|
1650
1800
|
}
|
|
1651
1801
|
if (typeof o.on !== "string" || !(EVENT_KINDS as readonly string[]).includes(o.on)) {
|
|
@@ -1700,6 +1850,16 @@ function validateEvents(v: unknown, errors: JsonDslError[]): void {
|
|
|
1700
1850
|
});
|
|
1701
1851
|
}
|
|
1702
1852
|
|
|
1853
|
+
/**
|
|
1854
|
+
* 巻き上げに応じて進む値が受ける項目 (#2258)。
|
|
1855
|
+
*
|
|
1856
|
+
* **数を書く項目と文字を書く項目を分けて持つ** = 受け方が違うので判定が分かれるが、
|
|
1857
|
+
* 知らせは 2 つを繋いで 1 つの一覧として出す。 別々に並べていた間、判定が 3 語で
|
|
1858
|
+
* 知らせが 4 語という食い違いが同じ関数の中にあった。
|
|
1859
|
+
*/
|
|
1860
|
+
const 送りの数の項目 = ["start", "end", "scrub"] as const;
|
|
1861
|
+
const 送りの文の項目 = ["label"] as const;
|
|
1862
|
+
|
|
1703
1863
|
/**
|
|
1704
1864
|
* 巻き上げに応じて進む値を検査する (#1393)。
|
|
1705
1865
|
*
|
|
@@ -1749,17 +1909,17 @@ function validateScrolls(
|
|
|
1749
1909
|
continue;
|
|
1750
1910
|
}
|
|
1751
1911
|
for (const [k, x] of Object.entries(spec as Record<string, unknown>)) {
|
|
1752
|
-
if (
|
|
1912
|
+
if ((送りの文の項目 as readonly string[]).includes(k)) {
|
|
1753
1913
|
if (typeof x !== "string") {
|
|
1754
|
-
errors.push({ path: `${path}
|
|
1914
|
+
errors.push({ path: `${path}.${k}`, message: `${k} must be a string` });
|
|
1755
1915
|
}
|
|
1756
1916
|
continue;
|
|
1757
1917
|
}
|
|
1758
|
-
if (
|
|
1918
|
+
if (!(送りの数の項目 as readonly string[]).includes(k)) {
|
|
1759
1919
|
errors.push({
|
|
1760
1920
|
path: `${path}.${k}`,
|
|
1761
1921
|
message: `unknown key "${k}"`,
|
|
1762
|
-
hint:
|
|
1922
|
+
hint: `使える項目 = ${[...送りの数の項目, ...送りの文の項目].join(", ")}`,
|
|
1763
1923
|
});
|
|
1764
1924
|
continue;
|
|
1765
1925
|
}
|
|
@@ -1794,6 +1954,15 @@ function validateActorShape(v: unknown, path: string, errors: JsonDslError[]): v
|
|
|
1794
1954
|
表で中身を検査する(v as Record<string, unknown>, 図形の表, path, "shape", errors);
|
|
1795
1955
|
}
|
|
1796
1956
|
|
|
1957
|
+
/**
|
|
1958
|
+
* 鍵を利用者が決める入れ物 (縦列 / 群) を見る (#1304)。
|
|
1959
|
+
*
|
|
1960
|
+
* `lanes` / `groups` は id を鍵に持つため、鍵そのものは縛れない。 縛れるのは
|
|
1961
|
+
* 「入れ物が plain object か」 と「各 id の中身の項目名と値の型」 の 2 つ。
|
|
1962
|
+
*
|
|
1963
|
+
* #1304 まで外側の形が違う入力 (`lanes: 5`) は走査ごと飛ばされ、誤りが 1 件も返らなかった。
|
|
1964
|
+
* 形が違うものを黙って捨てると、書いた縦列が 1 つも効かない図が知らせなしで出る。
|
|
1965
|
+
*/
|
|
1797
1966
|
function validateIdMap(
|
|
1798
1967
|
v: unknown,
|
|
1799
1968
|
欄: "lanes" | "groups",
|
|
@@ -2075,6 +2244,10 @@ export function 素のデータに写す(
|
|
|
2075
2244
|
}
|
|
2076
2245
|
}
|
|
2077
2246
|
|
|
2247
|
+
/**
|
|
2248
|
+
* shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
|
|
2249
|
+
* fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
|
|
2250
|
+
*/
|
|
2078
2251
|
function validateJson(
|
|
2079
2252
|
json: unknown,
|
|
2080
2253
|
): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
|
|
@@ -2150,6 +2323,7 @@ function validateJson(
|
|
|
2150
2323
|
const 代わりに使う欄: Record<(typeof 見本にしか効かない欄)[number], string> = {
|
|
2151
2324
|
state: "箱の見た目を変えるなら tone / color を使う",
|
|
2152
2325
|
scale: "大きさを変えるなら posW / posH を使う",
|
|
2326
|
+
phase: "普通の箱は自分の段を持たない。 動かす段は animation に書く",
|
|
2153
2327
|
};
|
|
2154
2328
|
if (!見本か) {
|
|
2155
2329
|
for (const 欄 of 見本にしか効かない欄) {
|
|
@@ -2173,8 +2347,8 @@ function validateJson(
|
|
|
2173
2347
|
});
|
|
2174
2348
|
}
|
|
2175
2349
|
}
|
|
2176
|
-
//
|
|
2177
|
-
// `{ v: {} }`
|
|
2350
|
+
// 値の上書き (`state`) は素の object に限り、中の値も数 / 文字 / 真偽のいずれかに限る。
|
|
2351
|
+
// `{ v: {} }` のような入れ子や null を通すと、値の初期値に扱えない型が入って組み立てが崩れる。
|
|
2178
2352
|
if (ao.state !== undefined) {
|
|
2179
2353
|
if (!ao.state || typeof ao.state !== "object" || Array.isArray(ao.state)) {
|
|
2180
2354
|
errors.push({
|
|
@@ -2195,7 +2369,11 @@ function validateJson(
|
|
|
2195
2369
|
}
|
|
2196
2370
|
// CAR-1693 Phase 1: actor DSL 表面 pos の validation
|
|
2197
2371
|
validateLayoutPos(ao.pos, `$.actors[${i}].pos`, errors);
|
|
2372
|
+
// 相対で置く指定 (#2039)
|
|
2373
|
+
validateRelativePos(ao.posRel, `$.actors[${i}].posRel`, errors);
|
|
2198
2374
|
});
|
|
2375
|
+
// 解けない相対の指定 (#2039)。 欄ごとの形を見た後に、図全体として解けるかを見る
|
|
2376
|
+
validateRelativeAnchors(j.actors, errors);
|
|
2199
2377
|
}
|
|
2200
2378
|
if (!Array.isArray(j.flow)) {
|
|
2201
2379
|
errors.push({ path: "$.flow", message: "flow must be an array" });
|
|
@@ -2375,12 +2553,6 @@ function validateValues(v: unknown, errors: JsonDslError[]): void {
|
|
|
2375
2553
|
}
|
|
2376
2554
|
}
|
|
2377
2555
|
|
|
2378
|
-
/**
|
|
2379
|
-
* JSON DSL → DslDocument (AST) 変換。 pos は JSON なので line 情報なし、 全て line 0。
|
|
2380
|
-
*
|
|
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)。
|
|
2383
|
-
*/
|
|
2384
2556
|
/**
|
|
2385
2557
|
* 図表の箱の上の小見出しを、 記法側と同じ形に整える (#1247)。
|
|
2386
2558
|
*
|
|
@@ -2393,11 +2565,40 @@ function 整えた小見出し(v: string | undefined): string | undefined {
|
|
|
2393
2565
|
return t.length > 0 ? t : undefined;
|
|
2394
2566
|
}
|
|
2395
2567
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2568
|
+
/**
|
|
2569
|
+
* 見本の状態の上書きに、段を使うかどうか (`phase`) を混ぜる (#2125)。
|
|
2570
|
+
*
|
|
2571
|
+
* 組み立て側は段を外す指定を状態の上書きの中の `phase` として読む
|
|
2572
|
+
* (`compile/parts.ts` の `段を外す`)。 記法は中括弧に書いた名前をすべて上書きへ入れるため
|
|
2573
|
+
* `{ kind: wave-gauge, phase: false }` がそのまま届くが、JSON は欄ごとに型を宣言する形なので、
|
|
2574
|
+
* 欄として受けたものを同じ場所へ入れ直す。
|
|
2575
|
+
*
|
|
2576
|
+
* **書かなかった時は欄を作らない**。 `{ phase: undefined }` を混ぜると、上書きの有無で
|
|
2577
|
+
* 分岐する側から見て「書いた」 ことになる。
|
|
2578
|
+
*/
|
|
2579
|
+
function 段を含めた上書き(a: JsonActor): Record<string, number | string | boolean> | undefined {
|
|
2580
|
+
if (a.phase === undefined) return a.state;
|
|
2581
|
+
return { ...(a.state ?? {}), phase: a.phase };
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
/**
|
|
2585
|
+
* JSON DSL → DslDocument (AST) 変換。
|
|
2586
|
+
*
|
|
2587
|
+
* 行番号は `行の表` を渡した時だけ入る (#2117)。 JSON そのものは書いた場所を持たないため、
|
|
2588
|
+
* 本文の行が分かる入口 (編集画面の YAML 欄) が場所ごとの行を集めて渡す。 渡さない呼出は
|
|
2589
|
+
* 全ての要素が 0 行になる = 「行が分からない」 ことを 0 で表す。
|
|
2590
|
+
*
|
|
2591
|
+
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
|
|
2592
|
+
* test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
|
|
2593
|
+
*/
|
|
2594
|
+
export function jsonToDoc(json: DragonJson, 行の表?: 書いた行の表): DslDocument {
|
|
2595
|
+
// 書いた場所から行を引く。 引けない場所は 0 行 = 表を渡さない呼出と同じ状態になる
|
|
2596
|
+
const 位置 = (...道: readonly (string | number)[]): { line: number } => ({
|
|
2597
|
+
line: 行の表?.get(書いた場所の鍵(...道)) ?? 0,
|
|
2598
|
+
});
|
|
2599
|
+
const actors: DslActor[] = json.actors.map((a, i) => {
|
|
2399
2600
|
if (typeof a === "string") {
|
|
2400
|
-
return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos:
|
|
2601
|
+
return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: 位置("actors", i) };
|
|
2401
2602
|
}
|
|
2402
2603
|
// CAR-1657 = kind が既存 NodeKind に無い値なら parts identifier 候補、 partId に格納
|
|
2403
2604
|
const kindStr = a.kind ?? "actor";
|
|
@@ -2457,10 +2658,12 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2457
2658
|
scale: isPart ? a.scale : undefined,
|
|
2458
2659
|
scaleKeys: isPart && a.scale !== undefined ? ["scale"] : undefined,
|
|
2459
2660
|
partId: isPart ? kindStr : undefined,
|
|
2460
|
-
stateOverride: isPart ? a
|
|
2661
|
+
stateOverride: isPart ? 段を含めた上書き(a) : undefined,
|
|
2461
2662
|
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping (naming collision 回避)
|
|
2462
2663
|
layoutPos: a.pos,
|
|
2463
|
-
|
|
2664
|
+
// 相対で置く指定 (#2039)。 欄の名前が内部と同じなのでそのまま渡す
|
|
2665
|
+
posRel: a.posRel,
|
|
2666
|
+
pos: 位置("actors", i),
|
|
2464
2667
|
};
|
|
2465
2668
|
});
|
|
2466
2669
|
// 図の配色 (#1553)。 解くのは 1 度だけにする
|
|
@@ -2504,7 +2707,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2504
2707
|
...(s.toPartNode !== undefined ? { toPartNode: s.toPartNode } : {}),
|
|
2505
2708
|
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos
|
|
2506
2709
|
layoutPos: s.pos,
|
|
2507
|
-
pos:
|
|
2710
|
+
pos: 位置("flow", i),
|
|
2508
2711
|
}));
|
|
2509
2712
|
// 状態は段が無くても図に載る (#1162 で組み立ての出口が載せる)。 **段の有無で分けない** =
|
|
2510
2713
|
// 分けると `states` だけを書いた JSON で値が 1 つも届かない (記法側で起きていた形、 #1181)
|
|
@@ -2517,7 +2720,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2517
2720
|
*
|
|
2518
2721
|
* 相手は名前のまま持ち、識別子への読み替えは組み立てが行う = 2 つの入口で同じ経路を通る。
|
|
2519
2722
|
*/
|
|
2520
|
-
const events: DslEventBinding[] | undefined = json.events?.map((e) => ({
|
|
2723
|
+
const events: DslEventBinding[] | undefined = json.events?.map((e, i) => ({
|
|
2521
2724
|
event: e.on as DslEventBinding["event"],
|
|
2522
2725
|
target:
|
|
2523
2726
|
e.diagram === true
|
|
@@ -2532,7 +2735,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2532
2735
|
? ({ kind: "lane" as const, name: e.lane } as const)
|
|
2533
2736
|
: ({ kind: "node" as const, name: e.box ?? "" } as const),
|
|
2534
2737
|
handlerId: e.handler.trim(),
|
|
2535
|
-
pos:
|
|
2738
|
+
pos: 位置("events", i),
|
|
2536
2739
|
}));
|
|
2537
2740
|
const scrolls: DslScrollTrigger[] | undefined = json.scrolls
|
|
2538
2741
|
? Object.entries(json.scrolls).map(([id, spec]) => ({ id, ...spec }))
|
|
@@ -2542,28 +2745,28 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2542
2745
|
const formulas: DslFormula[] | undefined = json.formulas
|
|
2543
2746
|
? Object.entries(json.formulas).map(([id, 値]) =>
|
|
2544
2747
|
typeof 値 === "string"
|
|
2545
|
-
? { id, expression: 値, pos:
|
|
2748
|
+
? { id, expression: 値, pos: 位置("formulas", id) }
|
|
2546
2749
|
: {
|
|
2547
2750
|
id,
|
|
2548
2751
|
expression: 値.expression,
|
|
2549
2752
|
...(値.label !== undefined ? { label: 値.label } : {}),
|
|
2550
|
-
pos:
|
|
2753
|
+
pos: 位置("formulas", id),
|
|
2551
2754
|
},
|
|
2552
2755
|
)
|
|
2553
2756
|
: undefined;
|
|
2554
2757
|
const states: DslState[] = Object.entries(json.states ?? {}).map(([name, initial]) => ({
|
|
2555
2758
|
name,
|
|
2556
2759
|
initial,
|
|
2557
|
-
pos:
|
|
2760
|
+
pos: 位置("states", name),
|
|
2558
2761
|
}));
|
|
2559
|
-
const phases: DslPhase[] = (json.animation ?? []).map((p) => ({
|
|
2762
|
+
const phases: DslPhase[] = (json.animation ?? []).map((p, i) => ({
|
|
2560
2763
|
name: p.step,
|
|
2561
2764
|
durationMs: Math.round((p.duration ?? 1.4) * 1000),
|
|
2562
2765
|
highlight: p.focus,
|
|
2563
2766
|
body: p.body,
|
|
2564
2767
|
badge: p.badge,
|
|
2565
2768
|
// 書いた段だけが欄を持つ。 空文字を置くと「書いた」 と「書いていない」 が同じ形になる
|
|
2566
|
-
...(p.draw !== undefined ? { draw: p.draw, drawPos:
|
|
2769
|
+
...(p.draw !== undefined ? { draw: p.draw, drawPos: 位置("animation", i, "draw") } : {}),
|
|
2567
2770
|
// 割合は `draw` がある段にだけ写す。 単独で書いても描く相手が決まらず何も起きないので、
|
|
2568
2771
|
// 記法側 (同じ行に書かせる形) と同じ状態に揃える
|
|
2569
2772
|
...(p.draw !== undefined && p.drawRatio !== undefined ? { drawRatio: p.drawRatio } : {}),
|
|
@@ -2575,17 +2778,27 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2575
2778
|
state,
|
|
2576
2779
|
from,
|
|
2577
2780
|
to,
|
|
2578
|
-
pos:
|
|
2781
|
+
pos: 位置("animation", i, "tween", state),
|
|
2579
2782
|
})),
|
|
2580
2783
|
}
|
|
2581
2784
|
: {}),
|
|
2582
2785
|
...(p.set && Object.keys(p.set).length > 0
|
|
2583
|
-
? {
|
|
2786
|
+
? {
|
|
2787
|
+
sets: Object.entries(p.set).map(([state, value]) => ({
|
|
2788
|
+
state,
|
|
2789
|
+
value,
|
|
2790
|
+
pos: 位置("animation", i, "set", state),
|
|
2791
|
+
})),
|
|
2792
|
+
}
|
|
2584
2793
|
: {}),
|
|
2585
|
-
pos:
|
|
2794
|
+
pos: 位置("animation", i),
|
|
2586
2795
|
}));
|
|
2587
2796
|
const animate: DslAnimate | undefined =
|
|
2588
|
-
states.length > 0 || phases.length > 0
|
|
2797
|
+
states.length > 0 || phases.length > 0
|
|
2798
|
+
? // 動きの塊そのものには書いた場所が無い。 段がある文書は `animation`、無い文書は
|
|
2799
|
+
// `states` を指す = どちらも「動きを書き始めた場所」 になる
|
|
2800
|
+
{ states, phases, pos: phases.length > 0 ? 位置("animation") : 位置("states") }
|
|
2801
|
+
: undefined;
|
|
2589
2802
|
return {
|
|
2590
2803
|
title: json.title,
|
|
2591
2804
|
type: json.type,
|
|
@@ -2593,12 +2806,12 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2593
2806
|
// 空かどうかを判定するため、 揃えないと **空白だけの値で入口ごとに図が変わる**
|
|
2594
2807
|
// (記法は書かなかった扱い、 JSON は中身のない帯を描く。 Round 2 の指摘で実測)
|
|
2595
2808
|
...(整えた小見出し(json.eyebrow) !== undefined
|
|
2596
|
-
? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos:
|
|
2809
|
+
? { eyebrow: 整えた小見出し(json.eyebrow), eyebrowPos: 位置("eyebrow") }
|
|
2597
2810
|
: {}),
|
|
2598
2811
|
// 2 軸で仕分ける図の軸の名前 (#1294)。 中身の無い形は「書かなかった」 と同じにする =
|
|
2599
2812
|
// 空の軸を渡すと、書いていない側の名前が空文字で描かれる (記法側と同じ扱い)
|
|
2600
2813
|
...(json.axes && (json.axes.x !== undefined || json.axes.y !== undefined)
|
|
2601
|
-
? { axes: json.axes, axesPos:
|
|
2814
|
+
? { axes: json.axes, axesPos: 位置("axes") }
|
|
2602
2815
|
: {}),
|
|
2603
2816
|
actors,
|
|
2604
2817
|
flow,
|
|
@@ -2606,16 +2819,20 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2606
2819
|
// 他の値から決まる値 (#1181)。 書いた順に並べる = 解く順は参照から決まるので順序に
|
|
2607
2820
|
// 意味は無いが、知らせの並びが書いた順になる
|
|
2608
2821
|
values: json.values
|
|
2609
|
-
? Object.entries(json.values).map(([name, expression]) => ({
|
|
2822
|
+
? Object.entries(json.values).map(([name, expression]) => ({
|
|
2823
|
+
name,
|
|
2824
|
+
expression,
|
|
2825
|
+
pos: 位置("values", name),
|
|
2826
|
+
}))
|
|
2610
2827
|
: undefined,
|
|
2611
|
-
viewport: json.viewport ? { ...json.viewport, pos:
|
|
2828
|
+
viewport: json.viewport ? { ...json.viewport, pos: 位置("viewport") } : undefined,
|
|
2612
2829
|
lanes: json.lanes
|
|
2613
2830
|
? Object.fromEntries(
|
|
2614
2831
|
Object.entries(json.lanes).map(([id, l]) => {
|
|
2615
2832
|
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping。
|
|
2616
2833
|
// JSON input の { pos, x, width, ... } を分離し、 pos のみ layoutPos に rename する。
|
|
2617
2834
|
const { pos: layoutPos, ...laneRest } = l;
|
|
2618
|
-
return [id, { id, ...laneRest, layoutPos, pos:
|
|
2835
|
+
return [id, { id, ...laneRest, layoutPos, pos: 位置("lanes", id) }];
|
|
2619
2836
|
}),
|
|
2620
2837
|
)
|
|
2621
2838
|
: undefined,
|
|
@@ -2623,7 +2840,7 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2623
2840
|
? Object.fromEntries(
|
|
2624
2841
|
Object.entries(json.groups).map(([id, g]) => [
|
|
2625
2842
|
id,
|
|
2626
|
-
{ id, label: g.label, lanes: g.lanes, pos:
|
|
2843
|
+
{ id, label: g.label, lanes: g.lanes, pos: 位置("groups", id) },
|
|
2627
2844
|
]),
|
|
2628
2845
|
)
|
|
2629
2846
|
: undefined,
|
|
@@ -2638,12 +2855,18 @@ export function jsonToDoc(json: DragonJson): DslDocument {
|
|
|
2638
2855
|
reveal: json.reveal,
|
|
2639
2856
|
// 箱に触れると関係する線だけを光らせるか (#1757)
|
|
2640
2857
|
relations: json.relations,
|
|
2641
|
-
// 図の並ぶ向き (#1494)。 JSON
|
|
2642
|
-
|
|
2858
|
+
// 図の並ぶ向き (#1494)。 JSON は英語で書くので、記法と同じ語に直してから渡す。
|
|
2859
|
+
// 書いた行も記法と同じく持つ = 持たないと、向きの知らせが文書の行 (1 行目) を指す
|
|
2860
|
+
...(json.direction !== undefined
|
|
2861
|
+
? {
|
|
2862
|
+
direction: json.direction === "horizontal" ? ("横" as const) : ("縦" as const),
|
|
2863
|
+
directionPos: 位置("direction"),
|
|
2864
|
+
}
|
|
2865
|
+
: {}),
|
|
2643
2866
|
// 図の配色 (#1553)。 記法と同じ解決を通す = 別名 (`生成り` / `青磁`) の受け方がずれない。
|
|
2644
2867
|
// 読めない語は渡さない = 上流の型検査が語を絞っているので、ここに来るのは書き間違いだけ
|
|
2645
2868
|
...(配色 !== null ? { palette: 配色 } : {}),
|
|
2646
|
-
pos:
|
|
2869
|
+
pos: 位置(),
|
|
2647
2870
|
};
|
|
2648
2871
|
}
|
|
2649
2872
|
|
|
@@ -2673,7 +2896,17 @@ export function jsonToDiagram(
|
|
|
2673
2896
|
// **`onNotice` も通す**。 記法経路だけに通知を付けていたため、 同じ型を受ける JSON / YAML
|
|
2674
2897
|
// 経路では読めない値や捨てた矢印が利用者へ届かなかった (review 指摘)。 エディタの YAML タブは
|
|
2675
2898
|
// ここを通る
|
|
2676
|
-
opts?: {
|
|
2899
|
+
opts?: {
|
|
2900
|
+
partsCatalog?: Record<string, CdlDiagram>;
|
|
2901
|
+
onNotice?: CompileToCdlOpts["onNotice"];
|
|
2902
|
+
/**
|
|
2903
|
+
* 本文の場所から書いた行を引く表 (#2117)。 渡すと知らせが書いた行を指す。
|
|
2904
|
+
*
|
|
2905
|
+
* JSON そのものは書いた場所を持たないため、本文の行が分かる入口 (編集画面の YAML 欄) が
|
|
2906
|
+
* 集めて渡す。 渡さない呼出は今までどおり全ての知らせが 0 行になる。
|
|
2907
|
+
*/
|
|
2908
|
+
行の表?: 書いた行の表;
|
|
2909
|
+
},
|
|
2677
2910
|
): CdlDiagram {
|
|
2678
2911
|
const v = validateJson(json);
|
|
2679
2912
|
if (!v.ok) {
|
|
@@ -2682,7 +2915,7 @@ export function jsonToDiagram(
|
|
|
2682
2915
|
.join("\n");
|
|
2683
2916
|
throw new Error(`Dragon JSON DSL validation error:\n${msg}`);
|
|
2684
2917
|
}
|
|
2685
|
-
const doc = jsonToDoc(v.data);
|
|
2918
|
+
const doc = jsonToDoc(v.data, opts?.行の表);
|
|
2686
2919
|
return compileToCdl(doc, opts);
|
|
2687
2920
|
}
|
|
2688
2921
|
|