@cardenelabs/dragon 0.7.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/index.cjs +4916 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1451 -0
- package/dist/index.d.ts +1451 -0
- package/dist/index.js +4870 -0
- package/dist/index.js.map +1 -0
- package/examples/quick-start.md +142 -0
- package/package.json +79 -0
- package/src/canvas-bounds.ts +52 -0
- package/src/color.ts +172 -0
- package/src/compile.ts +3739 -0
- package/src/focus.ts +46 -0
- package/src/index.ts +226 -0
- package/src/input-size.ts +154 -0
- package/src/json-parser.ts +434 -0
- package/src/keywords.ts +114 -0
- package/src/notation-lint.ts +304 -0
- package/src/parser.ts +354 -0
- package/src/relative-pos.ts +182 -0
- package/src/schema.ts +24 -0
- package/src/schemas/diagram.json +133 -0
- package/src/types.ts +294 -0
- package/src/v05/index.ts +9 -0
- package/src/v05/parser.ts +1678 -0
- package/src/write-position.ts +270 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 向け JSON DSL parser。
|
|
3
|
+
*
|
|
4
|
+
* dragon の YAML DSL と 1:1 対応する JSON 記法を提供する。
|
|
5
|
+
* LLM (Anthropic Claude / OpenAI GPT) が structured output (tool call / response_format)
|
|
6
|
+
* で確実に生成できるよう、 flat な object array を優先した shape になっている。
|
|
7
|
+
*
|
|
8
|
+
* 使い方 (LLM):
|
|
9
|
+
* 1. `packages/dragon/schemas/diagram.json` の JSON Schema を LLM の tool schema に注入
|
|
10
|
+
* 2. LLM が JSON を返す
|
|
11
|
+
* 3. `jsonToDiagram(json)` で CdlDiagram に変換
|
|
12
|
+
* 4. validation error は throw、 retry loop で LLM に修正させる
|
|
13
|
+
*
|
|
14
|
+
* YAML との対応:
|
|
15
|
+
* YAML `title: "..."` ⇔ JSON `{title: "..."}`
|
|
16
|
+
* YAML `actors: [A, B: kind]` ⇔ JSON `{actors: [{name: "A"}, {name: "B", kind: "storage"}]}`
|
|
17
|
+
* YAML `flow: [- A -> B: "label"]` ⇔ JSON `{flow: [{from: "A", to: "B", label: "label"}]}`
|
|
18
|
+
* YAML `animation: [step: "..."]` ⇔ JSON `{animation: [{step: "...", duration: 1.4, focus: [...]}]}`
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { CdlDiagram, NodeKind, Tone, EdgeStyle } from "@cardenelabs/cdl";
|
|
22
|
+
import type { DslDocument, DslActor, DslStep, DslAnimate, DslPhase, PresetType, LayoutMode, LayoutPos } from "./types";
|
|
23
|
+
import { compileToCdl } from "./compile";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* LLM 向け JSON DSL の入力 shape。 YAML DSL と 1:1 対応、 top-level は flat な object。
|
|
27
|
+
*/
|
|
28
|
+
export interface DragonJson {
|
|
29
|
+
/** 図の title (必須) */
|
|
30
|
+
title: string;
|
|
31
|
+
/** preset type (必須): sequence / flow / swimlane / er / state / topology / solidity / gantt / class / pie / c4 / mind */
|
|
32
|
+
type: PresetType;
|
|
33
|
+
/** 登場人物 (必須): 文字列 or { name, kind, ... } object */
|
|
34
|
+
actors: (string | JsonActor)[];
|
|
35
|
+
/** flow step 配列 (必須): { from, to, label, ... } */
|
|
36
|
+
flow: JsonStep[];
|
|
37
|
+
/** animation phase 配列 (optional) */
|
|
38
|
+
animation?: JsonPhase[];
|
|
39
|
+
/** viewport (optional): 全体 canvas size / gap */
|
|
40
|
+
viewport?: {
|
|
41
|
+
width?: number;
|
|
42
|
+
height?: number;
|
|
43
|
+
laneWidth?: number;
|
|
44
|
+
gap?: number;
|
|
45
|
+
laneGap?: number;
|
|
46
|
+
nodeGap?: number;
|
|
47
|
+
labelMargin?: number;
|
|
48
|
+
};
|
|
49
|
+
/** lanes (optional): topology / swimlane preset で使う lane 宣言 */
|
|
50
|
+
lanes?: Record<string, {
|
|
51
|
+
x?: number;
|
|
52
|
+
width?: number;
|
|
53
|
+
label?: string;
|
|
54
|
+
contain?: boolean;
|
|
55
|
+
lifeline?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` = auto layout offset。 未指定は
|
|
58
|
+
* backward compat、 set 済は Phase 2 の applyPosOffset で lane 位置を shift する。
|
|
59
|
+
*/
|
|
60
|
+
pos?: LayoutPos;
|
|
61
|
+
}>;
|
|
62
|
+
/** groups (optional): topology preset で使う group 宣言 */
|
|
63
|
+
groups?: Record<string, {
|
|
64
|
+
label?: string;
|
|
65
|
+
lanes: string[];
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* canvas pivot (CAR-1693 Phase 1) diagram-level layout mode。 "auto" (default) は catalog 100+
|
|
69
|
+
* backward compat、 "manual" は Phase 4 で drag → pos: 保存の完全 manual mode として使う予定。
|
|
70
|
+
*/
|
|
71
|
+
layout?: LayoutMode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface JsonActor {
|
|
75
|
+
name: string;
|
|
76
|
+
/**
|
|
77
|
+
* CAR-1657 unified syntax = 既存 NodeKind (28 個) に加えて parts identifier (arc-gauge 等) を
|
|
78
|
+
* accept する。 未知 kind 値は parts 候補として partId に格納、 compile 側 partsCatalog で解決。
|
|
79
|
+
* LLM structured output の typing 制約を緩めるため union に string 追加。
|
|
80
|
+
* `string & {}` = NodeKind の候補を IDE 補完で提示しつつ任意 string も許容する idiom。
|
|
81
|
+
* 素の `NodeKind | string` は no-redundant-type-constituents に抵触し補完も潰れる (#865)。
|
|
82
|
+
*/
|
|
83
|
+
kind?: NodeKind | (string & {});
|
|
84
|
+
subtitle?: string;
|
|
85
|
+
eyebrow?: string;
|
|
86
|
+
value?: string;
|
|
87
|
+
rows?: string[];
|
|
88
|
+
lane?: string;
|
|
89
|
+
stack?: number;
|
|
90
|
+
initial?: boolean;
|
|
91
|
+
final?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` = auto layout offset。 未指定は
|
|
94
|
+
* backward compat、 set 済は Phase 2 の applyPosOffset で actor 由来 lane / node の位置を shift。
|
|
95
|
+
*/
|
|
96
|
+
pos?: LayoutPos;
|
|
97
|
+
/**
|
|
98
|
+
* CAR-1657 parts state override (kind = parts identifier 時のみ有効)。
|
|
99
|
+
* LLM JSON DSL では nested 明示 = `{ "state": { "v": 50 } }` が natural、 human 側の
|
|
100
|
+
* inline 拡散 pattern (`- arc1: { kind: arc-gauge, v: 50 }`) とは記述形式が分岐する
|
|
101
|
+
* (spec § 2.3 分岐設計、 human = YAML 手書き最適 / LLM = JSON structured 最適)。
|
|
102
|
+
*/
|
|
103
|
+
state?: Record<string, number | string | boolean>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface JsonStep {
|
|
107
|
+
from: string;
|
|
108
|
+
to: string;
|
|
109
|
+
label: string;
|
|
110
|
+
sub?: string;
|
|
111
|
+
tone?: Tone;
|
|
112
|
+
style?: EdgeStyle;
|
|
113
|
+
guard?: string;
|
|
114
|
+
cardinality?: string;
|
|
115
|
+
labelOffsetX?: number;
|
|
116
|
+
labelOffsetY?: number;
|
|
117
|
+
/**
|
|
118
|
+
* canvas pivot (CAR-1693 Phase 1) DSL 表面 `pos: {x, y}` = edge label offset。 未指定は
|
|
119
|
+
* backward compat、 set 済は Phase 2 の applyPosOffset で edge label 位置を shift する。
|
|
120
|
+
*/
|
|
121
|
+
pos?: LayoutPos;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface JsonPhase {
|
|
125
|
+
/** phase name (必須) */
|
|
126
|
+
step: string;
|
|
127
|
+
/** duration in seconds (default 1.4) */
|
|
128
|
+
duration?: number;
|
|
129
|
+
/** highlight 対象 (actor name / edge "A -> B") */
|
|
130
|
+
focus?: string[];
|
|
131
|
+
/** body 説明文 */
|
|
132
|
+
body?: string;
|
|
133
|
+
/** badge label */
|
|
134
|
+
badge?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* JSON DSL error。 line 概念がないため、 field path (JSON pointer style) で位置を示す。
|
|
139
|
+
*/
|
|
140
|
+
export interface JsonDslError {
|
|
141
|
+
path: string;
|
|
142
|
+
message: string;
|
|
143
|
+
hint?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* CAR-1657 = 既存 NodeKind list (v05/parser.ts の NODE_KIND_VALID と揃える必要あり)。
|
|
148
|
+
* 未知 kind 値は parts identifier 候補として partId に格納する経路の判定基準。
|
|
149
|
+
* v05 parser との drift 防止のため、 別 PR で共通化検討 (`packages/dragon/src/kinds.ts` etc)。
|
|
150
|
+
*/
|
|
151
|
+
const VALID_KIND_SET: ReadonlySet<string> = new Set([
|
|
152
|
+
"actor", "function", "storage", "event", "cdn", "service", "database",
|
|
153
|
+
"cache", "queue", "api", "person", "entity", "state", "container", "card",
|
|
154
|
+
"lambda", "kms", "secret", "alb", "ecs", "rds", "s3", "iam", "user", "browser",
|
|
155
|
+
"contract", "eoa", "multisig", "proxy", "library", "interface",
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
const VALID_PRESETS: readonly PresetType[] = [
|
|
159
|
+
"sequence",
|
|
160
|
+
"flow",
|
|
161
|
+
"swimlane",
|
|
162
|
+
"er",
|
|
163
|
+
"state",
|
|
164
|
+
"topology",
|
|
165
|
+
"solidity",
|
|
166
|
+
"gantt",
|
|
167
|
+
"class",
|
|
168
|
+
"pie",
|
|
169
|
+
"c4",
|
|
170
|
+
"mind",
|
|
171
|
+
] as const;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* shape validation。 layer 1 = 必須 field + 型 check、 layer 2 は compile 側の validation に委譲。
|
|
175
|
+
* fail-fast ではなく全 error 収集して返す (LLM に一括で修正させるため)。
|
|
176
|
+
*/
|
|
177
|
+
/**
|
|
178
|
+
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` の型 check helper。 finite number pair を必須にし、
|
|
179
|
+
* `NaN` / `Infinity` / non-number は reject する (Phase 2 の applyPosOffset で数値演算するため)。
|
|
180
|
+
*/
|
|
181
|
+
function validateLayoutPos(v: unknown, path: string, errors: JsonDslError[]): void {
|
|
182
|
+
if (v === undefined) return;
|
|
183
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) {
|
|
184
|
+
errors.push({ path, message: "pos must be an object with x and y numbers" });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const p = v as Record<string, unknown>;
|
|
188
|
+
if (typeof p.x !== "number" || !Number.isFinite(p.x)) {
|
|
189
|
+
errors.push({ path: `${path}.x`, message: "pos.x must be a finite number" });
|
|
190
|
+
}
|
|
191
|
+
if (typeof p.y !== "number" || !Number.isFinite(p.y)) {
|
|
192
|
+
errors.push({ path: `${path}.y`, message: "pos.y must be a finite number" });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function validateJson(json: unknown): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
|
|
197
|
+
const errors: JsonDslError[] = [];
|
|
198
|
+
if (!json || typeof json !== "object" || Array.isArray(json)) {
|
|
199
|
+
return { ok: false, errors: [{ path: "$", message: "root must be a JSON object" }] };
|
|
200
|
+
}
|
|
201
|
+
const j = json as Record<string, unknown>;
|
|
202
|
+
|
|
203
|
+
if (typeof j.title !== "string" || j.title.length === 0) {
|
|
204
|
+
errors.push({ path: "$.title", message: "title must be a non-empty string" });
|
|
205
|
+
}
|
|
206
|
+
// CAR-1693 Phase 1: diagram-level layout mode の validation (未指定 = auto default で backward compat)
|
|
207
|
+
if (j.layout !== undefined && j.layout !== "auto" && j.layout !== "manual") {
|
|
208
|
+
errors.push({ path: "$.layout", message: 'layout must be "auto" or "manual" if present' });
|
|
209
|
+
}
|
|
210
|
+
if (typeof j.type !== "string" || !VALID_PRESETS.includes(j.type as PresetType)) {
|
|
211
|
+
errors.push({
|
|
212
|
+
path: "$.type",
|
|
213
|
+
message: `type must be one of: ${VALID_PRESETS.join(", ")}`,
|
|
214
|
+
hint: typeof j.type === "string" ? `got "${j.type}"` : undefined,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (!Array.isArray(j.actors) || j.actors.length === 0) {
|
|
218
|
+
errors.push({ path: "$.actors", message: "actors must be a non-empty array" });
|
|
219
|
+
} else {
|
|
220
|
+
j.actors.forEach((a, i) => {
|
|
221
|
+
if (typeof a === "string") return;
|
|
222
|
+
if (!a || typeof a !== "object" || Array.isArray(a)) {
|
|
223
|
+
errors.push({ path: `$.actors[${i}]`, message: "actor must be string or object" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const ao = a as Record<string, unknown>;
|
|
227
|
+
if (typeof ao.name !== "string" || ao.name.length === 0) {
|
|
228
|
+
errors.push({ path: `$.actors[${i}].name`, message: "actor.name must be a non-empty string" });
|
|
229
|
+
}
|
|
230
|
+
// CAR-1657 (+ codex-review MAJOR fix) = kind の validation、 non-empty string 必須。
|
|
231
|
+
// parts identifier or existing NodeKind のどちらかを想定、 空文字 or 非 string は reject。
|
|
232
|
+
if (ao.kind !== undefined && (typeof ao.kind !== "string" || ao.kind.length === 0)) {
|
|
233
|
+
errors.push({ path: `$.actors[${i}].kind`, message: "actor.kind must be a non-empty string" });
|
|
234
|
+
}
|
|
235
|
+
// codex-review MAJOR fix = state override は plain object + 値は primitive (number / string / boolean) 限定、
|
|
236
|
+
// `{ v: {} }` 等 nested object や null が流入すると CdlState.initial に不正な型が入り compile 崩れる。
|
|
237
|
+
if (ao.state !== undefined) {
|
|
238
|
+
if (!ao.state || typeof ao.state !== "object" || Array.isArray(ao.state)) {
|
|
239
|
+
errors.push({ path: `$.actors[${i}].state`, message: "actor.state must be a plain object" });
|
|
240
|
+
} else {
|
|
241
|
+
for (const [sk, sv] of Object.entries(ao.state as Record<string, unknown>)) {
|
|
242
|
+
const svType = typeof sv;
|
|
243
|
+
if (svType !== "number" && svType !== "string" && svType !== "boolean") {
|
|
244
|
+
errors.push({
|
|
245
|
+
path: `$.actors[${i}].state.${sk}`,
|
|
246
|
+
message: `actor.state.${sk} must be number / string / boolean (got ${sv === null ? "null" : svType})`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// CAR-1693 Phase 1: actor DSL 表面 pos の validation
|
|
253
|
+
validateLayoutPos(ao.pos, `$.actors[${i}].pos`, errors);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (!Array.isArray(j.flow)) {
|
|
257
|
+
errors.push({ path: "$.flow", message: "flow must be an array" });
|
|
258
|
+
} else {
|
|
259
|
+
j.flow.forEach((s, i) => {
|
|
260
|
+
if (!s || typeof s !== "object" || Array.isArray(s)) {
|
|
261
|
+
errors.push({ path: `$.flow[${i}]`, message: "step must be an object" });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const so = s as Record<string, unknown>;
|
|
265
|
+
if (typeof so.from !== "string") errors.push({ path: `$.flow[${i}].from`, message: "step.from must be a string" });
|
|
266
|
+
if (typeof so.to !== "string") errors.push({ path: `$.flow[${i}].to`, message: "step.to must be a string" });
|
|
267
|
+
if (typeof so.label !== "string") errors.push({ path: `$.flow[${i}].label`, message: "step.label must be a string" });
|
|
268
|
+
// CAR-1693 Phase 1: step DSL 表面 pos の validation
|
|
269
|
+
validateLayoutPos(so.pos, `$.flow[${i}].pos`, errors);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// CAR-1693 Phase 1: lane DSL 表面 pos の validation
|
|
273
|
+
if (j.lanes !== undefined && j.lanes && typeof j.lanes === "object" && !Array.isArray(j.lanes)) {
|
|
274
|
+
for (const [laneId, lane] of Object.entries(j.lanes as Record<string, unknown>)) {
|
|
275
|
+
if (lane && typeof lane === "object" && !Array.isArray(lane)) {
|
|
276
|
+
validateLayoutPos((lane as Record<string, unknown>).pos, `$.lanes.${laneId}.pos`, errors);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (j.animation !== undefined) {
|
|
281
|
+
if (!Array.isArray(j.animation)) {
|
|
282
|
+
errors.push({ path: "$.animation", message: "animation must be an array if present" });
|
|
283
|
+
} else {
|
|
284
|
+
j.animation.forEach((p, i) => {
|
|
285
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
286
|
+
errors.push({ path: `$.animation[${i}]`, message: "phase must be an object" });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const po = p as Record<string, unknown>;
|
|
290
|
+
if (typeof po.step !== "string" || po.step.length === 0) {
|
|
291
|
+
errors.push({ path: `$.animation[${i}].step`, message: "phase.step must be a non-empty string" });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (errors.length > 0) return { ok: false, errors };
|
|
297
|
+
return { ok: true, data: j as unknown as DragonJson };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* JSON DSL → DslDocument (AST) 変換。 pos は JSON なので line 情報なし、 全て line 0。
|
|
302
|
+
*
|
|
303
|
+
* CAR-1693 Phase 1: DSL 表面 `pos: {x, y}` → 内部 AST `layoutPos:` の 2 層 mapping の実装 core。
|
|
304
|
+
* test で mapping logic を実 execute するため export する (pos-field.test.ts の regression guard)。
|
|
305
|
+
*/
|
|
306
|
+
export function jsonToDoc(json: DragonJson): DslDocument {
|
|
307
|
+
const p0 = { line: 0 };
|
|
308
|
+
const actors: DslActor[] = json.actors.map((a) => {
|
|
309
|
+
if (typeof a === "string") {
|
|
310
|
+
return { name: a, kind: "actor" as NodeKind, kindWritten: false, pos: p0 };
|
|
311
|
+
}
|
|
312
|
+
// CAR-1657 = kind が既存 NodeKind に無い値なら parts identifier 候補、 partId に格納
|
|
313
|
+
const kindStr = (a.kind ?? "actor") as string;
|
|
314
|
+
const isPart = kindStr !== "actor" && !VALID_KIND_SET.has(kindStr);
|
|
315
|
+
return {
|
|
316
|
+
name: a.name,
|
|
317
|
+
kind: isPart ? "actor" as NodeKind : (a.kind ?? "actor") as NodeKind,
|
|
318
|
+
// parts 候補は `kind` を `actor` に倒して `partId` へ退避するため、 名札に載せる種類としては
|
|
319
|
+
// 「書かなかった」 と同じ扱いにする (#1058)
|
|
320
|
+
kindWritten: a.kind !== undefined && !isPart,
|
|
321
|
+
subtitle: a.subtitle,
|
|
322
|
+
eyebrow: a.eyebrow,
|
|
323
|
+
value: a.value,
|
|
324
|
+
rows: a.rows,
|
|
325
|
+
lane: a.lane,
|
|
326
|
+
stack: a.stack,
|
|
327
|
+
initial: a.initial,
|
|
328
|
+
final: a.final,
|
|
329
|
+
partId: isPart ? kindStr : undefined,
|
|
330
|
+
stateOverride: isPart ? a.state : undefined,
|
|
331
|
+
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping (naming collision 回避)
|
|
332
|
+
layoutPos: a.pos,
|
|
333
|
+
pos: p0,
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
const flow: DslStep[] = json.flow.map((s, i) => ({
|
|
337
|
+
no: i + 1,
|
|
338
|
+
from: s.from,
|
|
339
|
+
to: s.to,
|
|
340
|
+
label: s.label,
|
|
341
|
+
sub: s.sub,
|
|
342
|
+
tone: s.tone,
|
|
343
|
+
style: s.style,
|
|
344
|
+
guard: s.guard,
|
|
345
|
+
cardinality: s.cardinality,
|
|
346
|
+
labelOffsetX: s.labelOffsetX,
|
|
347
|
+
labelOffsetY: s.labelOffsetY,
|
|
348
|
+
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos
|
|
349
|
+
layoutPos: s.pos,
|
|
350
|
+
pos: p0,
|
|
351
|
+
}));
|
|
352
|
+
let animate: DslAnimate | undefined;
|
|
353
|
+
if (json.animation && json.animation.length > 0) {
|
|
354
|
+
const phases: DslPhase[] = json.animation.map((p) => ({
|
|
355
|
+
name: p.step,
|
|
356
|
+
durationMs: Math.round((p.duration ?? 1.4) * 1000),
|
|
357
|
+
highlight: p.focus,
|
|
358
|
+
body: p.body,
|
|
359
|
+
badge: p.badge,
|
|
360
|
+
pos: p0,
|
|
361
|
+
}));
|
|
362
|
+
animate = { states: [], phases, pos: p0 };
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
title: json.title,
|
|
366
|
+
type: json.type,
|
|
367
|
+
actors,
|
|
368
|
+
flow,
|
|
369
|
+
animate,
|
|
370
|
+
viewport: json.viewport ? { ...json.viewport, pos: p0 } : undefined,
|
|
371
|
+
lanes: json.lanes
|
|
372
|
+
? Object.fromEntries(
|
|
373
|
+
Object.entries(json.lanes).map(([id, l]) => {
|
|
374
|
+
// CAR-1693 Phase 1: DSL 表面 pos → 内部 AST layoutPos の 2 層 mapping。
|
|
375
|
+
// JSON input の { pos, x, width, ... } を分離し、 pos のみ layoutPos に rename する。
|
|
376
|
+
const { pos: layoutPos, ...laneRest } = l;
|
|
377
|
+
return [id, { id, ...laneRest, layoutPos, pos: p0 }];
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
380
|
+
: undefined,
|
|
381
|
+
groups: json.groups
|
|
382
|
+
? Object.fromEntries(
|
|
383
|
+
Object.entries(json.groups).map(([id, g]) => [id, { id, label: g.label, lanes: g.lanes, pos: p0 }]),
|
|
384
|
+
)
|
|
385
|
+
: undefined,
|
|
386
|
+
// CAR-1693 Phase 1: diagram-level layout mode (auto|manual)、 未指定は undefined = auto default
|
|
387
|
+
layout: json.layout,
|
|
388
|
+
pos: p0,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* LLM 向け JSON DSL の parse + compile 一発変換。
|
|
394
|
+
*
|
|
395
|
+
* @param json - DragonJson shape の object (parsed JSON、 not string)
|
|
396
|
+
* @returns CdlDiagram (@cardenelabs/cdl の CdlDiagramView 等に渡せる)
|
|
397
|
+
* @throws Error - validation error + hint 付きの詳細メッセージ、 LLM に retry させるための情報を含む
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* const diagram = jsonToDiagram({
|
|
401
|
+
* title: "ログインAPI",
|
|
402
|
+
* type: "sequence",
|
|
403
|
+
* actors: ["User", "API", { name: "DB", kind: "storage" }],
|
|
404
|
+
* flow: [
|
|
405
|
+
* { from: "User", to: "API", label: "login" },
|
|
406
|
+
* { from: "API", to: "DB", label: "SELECT" },
|
|
407
|
+
* ],
|
|
408
|
+
* animation: [
|
|
409
|
+
* { step: "call", duration: 1.4, focus: ["User", "API"] },
|
|
410
|
+
* ],
|
|
411
|
+
* });
|
|
412
|
+
*/
|
|
413
|
+
export function jsonToDiagram(
|
|
414
|
+
json: unknown,
|
|
415
|
+
opts?: { partsCatalog?: Record<string, CdlDiagram> },
|
|
416
|
+
): CdlDiagram {
|
|
417
|
+
const v = validateJson(json);
|
|
418
|
+
if (!v.ok) {
|
|
419
|
+
const msg = v.errors.map((e) => ` ${e.path}: ${e.message}${e.hint ? ` (${e.hint})` : ""}`).join("\n");
|
|
420
|
+
throw new Error(`Dragon JSON DSL validation error:\n${msg}`);
|
|
421
|
+
}
|
|
422
|
+
const doc = jsonToDoc(v.data);
|
|
423
|
+
return compileToCdl(doc, opts);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* JSON DSL を validate だけ実施 (compile しない)。 error 詳細を配列で取得したい場合に使う。
|
|
428
|
+
* LLM の structured output の retry loop で、 error path を prompt に注入する用途。
|
|
429
|
+
*/
|
|
430
|
+
export function validateDragonJson(
|
|
431
|
+
json: unknown,
|
|
432
|
+
): { ok: true; data: DragonJson } | { ok: false; errors: JsonDslError[] } {
|
|
433
|
+
return validateJson(json);
|
|
434
|
+
}
|
package/src/keywords.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text DSL i18n キーワード一覧
|
|
3
|
+
* 日本語 / 英語両対応 (大文字小文字無視)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { NodeKind, Tone } from "@cardenelabs/cdl";
|
|
7
|
+
|
|
8
|
+
/** ブロック ヘッダー */
|
|
9
|
+
export const HEADERS = {
|
|
10
|
+
title: ["タイトル", "title"],
|
|
11
|
+
type: ["種類", "type"],
|
|
12
|
+
actors: ["登場人物", "actors"],
|
|
13
|
+
flow: ["流れ", "flow", "steps"],
|
|
14
|
+
animate: ["アニメーション", "animate", "animation"],
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
/** preset 名 */
|
|
18
|
+
export const PRESET_NAMES = ["sequence", "flow", "swimlane", "er", "state", "topology"] as const;
|
|
19
|
+
|
|
20
|
+
/** NodeKind 別名 (日本語 → English) */
|
|
21
|
+
export const NODE_KIND_ALIAS: Record<string, NodeKind> = {
|
|
22
|
+
// 日本語
|
|
23
|
+
人: "actor",
|
|
24
|
+
関数: "function",
|
|
25
|
+
ストレージ: "storage",
|
|
26
|
+
イベント: "event",
|
|
27
|
+
// 英語 (NodeKind そのまま)
|
|
28
|
+
actor: "actor",
|
|
29
|
+
function: "function",
|
|
30
|
+
storage: "storage",
|
|
31
|
+
event: "event",
|
|
32
|
+
cdn: "cdn",
|
|
33
|
+
service: "service",
|
|
34
|
+
database: "database",
|
|
35
|
+
cache: "cache",
|
|
36
|
+
queue: "queue",
|
|
37
|
+
// ... 残り 29 NodeKind は parser 内で types.NodeKind を直接受理
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Tone 別名 */
|
|
41
|
+
export const TONE_ALIAS: Record<string, Tone> = {
|
|
42
|
+
// 日本語
|
|
43
|
+
成功: "success",
|
|
44
|
+
失敗: "error",
|
|
45
|
+
警告: "warning",
|
|
46
|
+
情報: "info",
|
|
47
|
+
中立: "accent",
|
|
48
|
+
// 英語
|
|
49
|
+
success: "success",
|
|
50
|
+
error: "error",
|
|
51
|
+
warning: "warning",
|
|
52
|
+
info: "info",
|
|
53
|
+
neutral: "accent",
|
|
54
|
+
accent: "accent",
|
|
55
|
+
teal: "teal",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** アニメ サブキー */
|
|
59
|
+
export const ANIM_SUBKEYS = {
|
|
60
|
+
state: ["状態", "state"],
|
|
61
|
+
step: ["ステップ", "step"],
|
|
62
|
+
highlight: ["強調", "highlight", "active", "activate"],
|
|
63
|
+
tween: ["遷移", "tween"],
|
|
64
|
+
set: ["切替", "set"],
|
|
65
|
+
body: ["説明", "body", "description"],
|
|
66
|
+
badge: ["バッジ", "badge"],
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
/** 矢印記号 全変種を統一形 → に正規化 */
|
|
70
|
+
export const ARROW_PATTERNS = ["→", "->", "=>", ">>", "->>", "-->>", "->>"];
|
|
71
|
+
|
|
72
|
+
export function normalizeArrow(s: string): string {
|
|
73
|
+
let r = s;
|
|
74
|
+
for (const p of ARROW_PATTERNS) {
|
|
75
|
+
r = r.split(p).join("→");
|
|
76
|
+
}
|
|
77
|
+
return r;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** ヘッダー名 (日本語 or 英語) を canonical name に解決 */
|
|
81
|
+
export function resolveHeader(s: string): keyof typeof HEADERS | null {
|
|
82
|
+
const lower = s.toLowerCase().trim();
|
|
83
|
+
for (const [canon, aliases] of Object.entries(HEADERS)) {
|
|
84
|
+
if ((aliases as readonly string[]).some((a) => a.toLowerCase() === lower)) {
|
|
85
|
+
return canon as keyof typeof HEADERS;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveAnimSubkey(s: string): keyof typeof ANIM_SUBKEYS | null {
|
|
92
|
+
const lower = s.toLowerCase().trim();
|
|
93
|
+
for (const [canon, aliases] of Object.entries(ANIM_SUBKEYS)) {
|
|
94
|
+
if ((aliases as readonly string[]).some((a) => a.toLowerCase() === lower)) {
|
|
95
|
+
return canon as keyof typeof ANIM_SUBKEYS;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** duration 文字列 (1.5 秒 / 1500ms / 2 秒) を ms に変換 */
|
|
102
|
+
export function parseDuration(s: string): number | null {
|
|
103
|
+
const trimmed = s.trim();
|
|
104
|
+
// "1.5 秒" / "2 秒"
|
|
105
|
+
const sec = trimmed.match(/^([\d.]+)\s*秒$/);
|
|
106
|
+
if (sec) return Math.round(parseFloat(sec[1]!) * 1000);
|
|
107
|
+
// "1500ms" / "1500 ms"
|
|
108
|
+
const ms = trimmed.match(/^([\d.]+)\s*ms$/i);
|
|
109
|
+
if (ms) return Math.round(parseFloat(ms[1]!));
|
|
110
|
+
// "1.5s" / "2s"
|
|
111
|
+
const en = trimmed.match(/^([\d.]+)\s*s$/i);
|
|
112
|
+
if (en) return Math.round(parseFloat(en[1]!) * 1000);
|
|
113
|
+
return null;
|
|
114
|
+
}
|