@cardenelabs/dragon 0.27.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.
Files changed (60) hide show
  1. package/README.md +119 -12
  2. package/dist/index.cjs +9619 -8023
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +407 -175
  5. package/dist/index.d.ts +407 -175
  6. package/dist/index.js +9617 -8024
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/compile/actor-ids.ts +306 -0
  10. package/src/compile/actors.ts +74 -0
  11. package/src/compile/c4.ts +77 -0
  12. package/src/compile/chain.ts +54 -0
  13. package/src/compile/chart-fields.ts +178 -0
  14. package/src/compile/class.ts +100 -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 +63 -0
  18. package/src/compile/er-relation.ts +200 -0
  19. package/src/compile/er.ts +44 -0
  20. package/src/compile/flow.ts +54 -0
  21. package/src/compile/funnel.ts +75 -0
  22. package/src/compile/gantt.ts +232 -0
  23. package/src/compile/generic.ts +284 -0
  24. package/src/compile/hierarchy.ts +92 -0
  25. package/src/compile/journey.ts +84 -0
  26. package/src/compile/kinds.ts +156 -0
  27. package/src/compile/lanes.ts +56 -0
  28. package/src/compile/mind.ts +343 -0
  29. package/src/compile/node-title.ts +24 -0
  30. package/src/compile/notice.ts +94 -0
  31. package/src/compile/parts-lookup.ts +17 -0
  32. package/src/compile/parts.ts +2635 -0
  33. package/src/compile/placement-apply.ts +673 -0
  34. package/src/compile/placement.ts +15 -0
  35. package/src/compile/quadrant.ts +110 -0
  36. package/src/compile/rows.ts +40 -0
  37. package/src/compile/sequence.ts +226 -0
  38. package/src/compile/slug.ts +29 -0
  39. package/src/compile/solidity.ts +39 -0
  40. package/src/compile/state.ts +71 -0
  41. package/src/compile/subtitle.ts +22 -0
  42. package/src/compile/swimlane.ts +83 -0
  43. package/src/compile/topology.ts +50 -0
  44. package/src/compile/tree.ts +60 -0
  45. package/src/compile/value-chart.ts +183 -0
  46. package/src/compile/values.ts +432 -0
  47. package/src/compile/word-state.ts +82 -0
  48. package/src/compile.ts +596 -5672
  49. package/src/index.ts +13 -10
  50. package/src/json-line-map.ts +30 -0
  51. package/src/json-parser.ts +509 -176
  52. package/src/near-name.ts +42 -0
  53. package/src/notation-lint.ts +112 -74
  54. package/src/relative-pos.ts +71 -2
  55. package/src/schema.ts +1 -1
  56. package/src/schemas/diagram.json +130 -34
  57. package/src/types.ts +60 -41
  58. package/src/v05/input-table.generated.ts +6 -4
  59. package/src/v05/parser-types.ts +12 -1
  60. package/src/v05/parser.ts +503 -149
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 書き間違えた名前に綴りの近い候補を探す (#1295 で JSON の項目名に作り、#2113 で部品の名前と共有した)。
3
+ *
4
+ * 「知らない名前です」 だけだと、書いた人は正しい綴りを探しに行く必要がある。
5
+ * 2 文字の違い (足りない / 多い / 別の字) までを候補とする。 隣どうしの入れ替わりは 2 文字の違いに数える。
6
+ *
7
+ * 遠い名前は勧めない。 無関係な名前を勧めると、書いた人がそちらへ直して二度手間になる。
8
+ * 大文字と小文字は区別しない。
9
+ */
10
+ export function 近い名前(語: string, 候補: Iterable<string>): string | undefined {
11
+ const 小文字 = 語.toLowerCase();
12
+ let 最短: { 名: string; 距離: number } | undefined;
13
+ for (const c of 候補) {
14
+ const d = 編集距離(小文字, c.toLowerCase(), 2);
15
+ if (d <= 2 && (最短 === undefined || d < 最短.距離)) 最短 = { 名: c, 距離: d };
16
+ }
17
+ return 最短?.名;
18
+ }
19
+
20
+ /**
21
+ * 2 つの語の編集距離 (上限付き)。
22
+ *
23
+ * 上限を持つのは、長い語どうしで表を全部埋めないため。 上限を超えた時点で打ち切る。
24
+ */
25
+ function 編集距離(a: string, b: string, 上限: number): number {
26
+ if (Math.abs(a.length - b.length) > 上限) return 上限 + 1;
27
+ let 前 = Array.from({ length: b.length + 1 }, (_, i) => i);
28
+ for (let i = 1; i <= a.length; i += 1) {
29
+ const 今: number[] = [i];
30
+ let 行の最小 = i;
31
+ for (let j = 1; j <= b.length; j += 1) {
32
+ const 費用 = a[i - 1] === b[j - 1] ? 0 : 1;
33
+ const v = Math.min(今[j - 1]! + 1, 前[j]! + 1, 前[j - 1]! + 費用);
34
+ 今.push(v);
35
+ if (v < 行の最小) 行の最小 = v;
36
+ }
37
+ // その行の最小が上限を超えたら、以降どう進んでも上限以下にはならない
38
+ if (行の最小 > 上限) return 上限 + 1;
39
+ 前 = 今;
40
+ }
41
+ return 前[b.length]!;
42
+ }
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Notation lint (author 向け修正システム)。
2
+ * 記法の検査 (図を書く人向けの修正システム)。
3
3
  *
4
- * cdl / dragon 記法で書かれた CdlDiagram を rule-based に検査し、
5
- * 「もっと良い書き方」 を suggestion として返す純粋関数。 LLM 不要、 rule のみ。
4
+ * cdl / dragon の記法で書いた図 (`CdlDiagram`) を決まった規則で検査し、直し方を修正案
5
+ * (`suggestion`) として返す純粋関数。 言語モデルは使わない。
6
6
  *
7
- * 検知システム (`check:cdl` / `check:dragon` / `check:kind`) は開発陣向けで
8
- * 「実装バグ」 を検出するが、 本 notation-lint は **author 向け** で
9
- * 「書き方の癖 / 冗長表現 / 未定義参照 / 空 payload」 等を検出する。
7
+ * 検知システム (`check:cdl` / `check:dragon` / `check:kind`) は開発者向けで実装の不具合を
8
+ * 見つける。 こちらは図を書く人向けで、読む人へ伝わらない書き方 (図の説明に入り込んだ実装の
9
+ * 書き方、無い部品を指す参照、値や項目が空の図表など) を見つける。
10
10
  *
11
11
  * 使い方:
12
- * import { lintDiagram } from "@cardenelabs/dragon";
12
+ * import { lintDiagram, autoFix } from "@cardenelabs/dragon";
13
13
  * const report = lintDiagram(diagram);
14
- * // report.issues[] = LintIssue[]
15
- * // report.fixed = LintIssue[] のうち自動修正で解消される件
16
- * // report.autoFix(diagram) = 修正済 CdlDiagram
14
+ * // report.issues = LintIssue[]
15
+ * // report.autoFixableCount = 自動修正で実際に解消できる指摘の数
16
+ * const patched = autoFix(diagram); // 自動修正を当てた新しい図 (元の図は変えない)
17
17
  *
18
- * CLI:
19
- * pnpm dragon-lint apps/playground-spa/src/topics/catalog/presets.cdl.ts
18
+ * 道具:
19
+ * node packages/dragon/scripts/dragon-lint.mjs apps/playground-spa/src/topics/catalog/presets.cdl.ts
20
20
  */
21
21
  import type { CdlDiagram, CdlNode } from "@cardenelabs/cdl";
22
22
 
@@ -28,9 +28,12 @@ export type LintIssue = {
28
28
  severity: LintSeverity;
29
29
  /** 該当対象 (node id / edge id / diagram id) */
30
30
  target: string;
31
- /** 人間向けメッセージ */
31
+ /**
32
+ * 書き手向けの指摘の文。 書き手が打ち込む識別子と書き手の値は `` ` `` で囲み、 残りは日本語で書く
33
+ * (画面側の `lint-message-words.test.ts` が全規則の文を判定に通す)
34
+ */
32
35
  message: string;
33
- /** 修正案 (自動修正可能なら適用後の値、 手動修正必要なら null) */
36
+ /** 修正案。 自動修正できる指摘は自動修正が書く値そのもの、 できない指摘は直し方の文 */
34
37
  suggestion?: string;
35
38
  /** autoFix() が本 issue を自動解消できるか */
36
39
  autoFixable: boolean;
@@ -43,11 +46,21 @@ export type LintReport = {
43
46
  autoFixableCount: number;
44
47
  };
45
48
 
49
+ /**
50
+ * 図の説明に入り込んだ実装の書き方と、 読む人に何が伝わらないか。
51
+ *
52
+ * 文は理由だけを持ち、 直し方の例を書かない。 直し方は修正案 (`suggestion`) が自動修正の
53
+ * 出力から作る。 例を字で書くと、 自動修正の出力を変えた日に例だけが古い文型で残る
54
+ * (実測 = #1934 で出力を変えた後も `「〜 を示す図」 のように` が残っていた、 #1940)。
55
+ */
46
56
  const REDUNDANT_TOPIC_PATTERNS: Array<{ pattern: RegExp; hint: string }> = [
47
- { pattern: /\bpreset\s*\(/i, hint: "「〜 preset (詳細)」 は実装表現、 「〜 を示す図」 のように読者向け説明に" },
48
- { pattern: /render\s*未実装/, hint: "「render 未実装」 は開発者向け内部メモ、 catalog 表示では省く" },
49
- { pattern: /SVG\s+(polyline|arc|rect|path)/i, hint: "「SVG polyline / arc / rect / path」 は実装詳細、 「〜 を示す図」 に置換" },
50
- { pattern: /\bpolygon\b/i, hint: "「polygon」 は実装用語、 図の意味を説明する自然文に置換" },
57
+ { pattern: /\bpreset\s*\(/i, hint: "`preset (…)` は作り手の書き方で、 読む人には何を示す図かが伝わらない" },
58
+ { pattern: /render\s*未実装/, hint: "`render 未実装` は作り手向けの控えで、 カタログに出す説明には要らない" },
59
+ {
60
+ pattern: /SVG\s+(polyline|arc|rect|path)/i,
61
+ hint: "`SVG` の描き方の名前 (`polyline` / `arc` / `rect` / `path`) は実装の詳細で、 何を示す図かが伝わらない",
62
+ },
63
+ { pattern: /\bpolygon\b/i, hint: "`polygon` は実装の言葉で、 何を示す図かが伝わらない" },
51
64
  ];
52
65
 
53
66
  /**
@@ -85,32 +98,46 @@ export function autoFix(d: CdlDiagram): CdlDiagram {
85
98
  return patched;
86
99
  }
87
100
 
88
- const KIND_TO_JA: Record<string, string> = {
89
- chart: "統計チャート",
90
- "line chart": "折れ線グラフ",
91
- "pie chart": "円グラフ",
92
- "bar chart": "棒グラフ",
93
- flow: "処理の流れ",
94
- swimlane: "スイムレーン (役割別レーン)",
95
- sequence: "時系列のやり取り",
96
- topology: "システム構成",
97
- er: "テーブル関係 (ER 図)",
98
- stateMachine: "状態遷移 (ステート図)",
99
- stateMachine2: "拡張ステート図 (階層状態)",
100
- infrastructure: "クラウド構成",
101
- classDiagram: "UML クラス図",
102
- tree: "階層ツリー",
103
- userJourney: "ユーザージャーニー",
104
- mindMap: "マインドマップ",
105
- funnel: "ファネル (段階別離脱)",
106
- quadrant: "四象限マトリクス",
107
- gantt: "ガントチャート",
108
- flowchart: "分岐フローチャート",
109
- network: "ネットワーク構成",
101
+ /**
102
+ * 図の型ごとに、 自動修正が書く説明「{shows}を示す{name}」 の 2 つの部品。
103
+ *
104
+ * `name` はカタログが同じ型の見本に付けた名前 (`apps/playground-spa/src/lib/i18n.ts` の
105
+ * `ITEM_NAME_JA` の `preset*`) と同じ字にする。 利用者は自動修正で書き換わった説明の名前で
106
+ * カタログを探すため、 呼び名が割れると同じ型の見本に辿り着けない。 照らす検査は画面側の
107
+ * `lint-topic-names.test.ts` が持つ (この package は画面側の file を読まない)。
108
+ *
109
+ * 説明の末尾を型の名前にするのは、 名前の多くが `図` で終わるため。 旧い文型
110
+ * 「{名前} を示す図」 のまま名前を差し替えると `工程表 を示す図` / `状態遷移図 を示す図` の
111
+ * ように図が重なる。 カタログの見本の説明 (`全体に対する内訳の割合を示す円グラフ`) と同じ並びにした。
112
+ *
113
+ * `chart` と `bar chart` はカタログに同じ型の見本が無いので、 図表の日常語で呼ぶ。
114
+ */
115
+ const KIND_TO_JA: Record<string, { shows: string; name: string }> = {
116
+ chart: { shows: "項目ごとの数値", name: "グラフ" },
117
+ "line chart": { shows: "値の移り変わり", name: "折れ線グラフ" },
118
+ "pie chart": { shows: "全体に対する内訳の割合", name: "円グラフ" },
119
+ "bar chart": { shows: "項目ごとの値の大きさ", name: "棒グラフ" },
120
+ flow: { shows: "処理の順番", name: "フロー" },
121
+ swimlane: { shows: "役割ごとの縦列に分けた処理の流れ", name: "スイムレーン" },
122
+ sequence: { shows: "要素どうしのやり取りの順番", name: "シーケンス図" },
123
+ topology: { shows: "システムの構成要素と接続", name: "トポロジー図" },
124
+ er: { shows: "表どうしの関係", name: "ER図" },
125
+ stateMachine: { shows: "状態と遷移の条件", name: "状態遷移図" },
126
+ stateMachine2: { shows: "親の状態の中に置いた子の状態と遷移の条件", name: "入れ子の状態遷移図" },
127
+ infrastructure: { shows: "クラウドとネットワークの構成", name: "階層構成図" },
128
+ classDiagram: { shows: "クラスどうしの関係", name: "クラス図" },
129
+ tree: { shows: "組織や分類の親子の関係", name: "階層図" },
130
+ userJourney: { shows: "利用者の気持ちの移り変わり", name: "体験の道筋" },
131
+ mindMap: { shows: "中心の主題から広がる発想", name: "枝分かれ図" },
132
+ funnel: { shows: "段階ごとに残る数と離れる数", name: "絞り込み図" },
133
+ quadrant: { shows: "2 つの軸で分けた項目の位置", name: "四象限図" },
134
+ gantt: { shows: "作業の期間と前後の関係", name: "工程表" },
135
+ flowchart: { shows: "分岐や判定を含む処理の順番", name: "流れ図" },
136
+ network: { shows: "機器と区画のつながり", name: "ネットワーク図" },
110
137
  };
111
138
 
112
139
  function applyTopicAutoFix(topic: string): string {
113
- // 1. 先頭の kind name を検出、 マッチしたら JA description に置換
140
+ // 1. 先頭の kind name を検出、 マッチしたら「{shows}を示す{name}」 に置換
114
141
  const kindMatch = topic.match(
115
142
  /^\s*(chart|flow|swimlane|sequence|topology|er|stateMachine2?|infrastructure|classDiagram|tree|userJourney|mindMap|funnel|quadrant|gantt|flowchart|network|line chart|pie chart|bar chart)\b/i,
116
143
  );
@@ -118,7 +145,8 @@ function applyTopicAutoFix(topic: string): string {
118
145
  const kind = kindMatch[1]!.toLowerCase();
119
146
  const canonical = Object.keys(KIND_TO_JA).find((k) => k.toLowerCase() === kind);
120
147
  if (canonical) {
121
- return `${KIND_TO_JA[canonical]} を示す図`;
148
+ const { shows, name } = KIND_TO_JA[canonical]!;
149
+ return `${shows}を示す${name}`;
122
150
  }
123
151
  }
124
152
 
@@ -131,21 +159,31 @@ function applyTopicAutoFix(topic: string): string {
131
159
  return out;
132
160
  }
133
161
 
162
+ /**
163
+ * 図の説明に実装の書き方が入っていないかを見る。
164
+ *
165
+ * **自動修正で消えるかは、 直した後の説明にもう 1 度当てて決める** (#1940)。 自動修正が消すのは
166
+ * 先頭の型の名前と、 括弧の中の実装の言葉だけで、 括弧の外の `SVG polyline を使う` は字が変わらない。
167
+ * 当てずに「直せる」 と返すと、 道具が直せる数に入れ、 直したはずの指摘が次の検査でまた出る。
168
+ *
169
+ * 修正案は、 直せる時は自動修正が書く値そのもの (`LintIssue.suggestion` の約束)、 直せない時は
170
+ * 直し方の文にする。 文の例は書き換え先の表から作り、 字で書かない。
171
+ */
134
172
  function ruleTopicRedundancy(d: CdlDiagram): LintIssue[] {
135
- const out: LintIssue[] = [];
136
- for (const { pattern, hint } of REDUNDANT_TOPIC_PATTERNS) {
137
- if (pattern.test(d.topic)) {
138
- out.push({
139
- rule: "topic-redundant-implementation-detail",
140
- severity: "warn",
141
- target: d.id,
142
- message: `topic に実装詳細が含まれる: "${d.topic}"`,
143
- suggestion: hint,
144
- autoFixable: true,
145
- });
146
- }
147
- }
148
- return out;
173
+ const 当たる = REDUNDANT_TOPIC_PATTERNS.filter(({ pattern }) => pattern.test(d.topic));
174
+ if (当たる.length === 0) return [];
175
+ const 直した後 = applyTopicAutoFix(d.topic);
176
+ const 直せる = !REDUNDANT_TOPIC_PATTERNS.some(({ pattern }) => pattern.test(直した後));
177
+ const 例 = KIND_TO_JA.gantt!;
178
+ const 手で直す = `何を示す図かを文で書き直す (「${例.shows}を示す${例.name}」 のように)`;
179
+ return 当たる.map(({ hint }) => ({
180
+ rule: "topic-redundant-implementation-detail",
181
+ severity: "warn",
182
+ target: d.id,
183
+ message: `図の説明 \`${d.topic}\` に実装の書き方が入っている。 ${hint}`,
184
+ suggestion: 直せる ? 直した後 : 手で直す,
185
+ autoFixable: 直せる,
186
+ }));
149
187
  }
150
188
 
151
189
  function ruleEmptyChartData(d: CdlDiagram): LintIssue[] {
@@ -158,8 +196,8 @@ function ruleEmptyChartData(d: CdlDiagram): LintIssue[] {
158
196
  rule: "chart-empty-datum",
159
197
  severity: "warn",
160
198
  target: n.id,
161
- message: `chart node "${n.id}" が datum 0 件、 chart は非表示になる`,
162
- suggestion: `.datum({ id: ..., label: ..., value: ... }) を 1 件以上追加`,
199
+ message: `図表 \`${n.id}\` に値 (\`datum\`) が 1 件も無く、 図表が描かれない`,
200
+ suggestion: `\`.datum({ id, label, value })\` で値を 1 件以上足す`,
163
201
  autoFixable: false,
164
202
  });
165
203
  }
@@ -168,8 +206,8 @@ function ruleEmptyChartData(d: CdlDiagram): LintIssue[] {
168
206
  rule: "chart-single-datum",
169
207
  severity: "info",
170
208
  target: n.id,
171
- message: `chart node "${n.id}" が datum 1 件、 比較 / 推移として意味が薄い`,
172
- suggestion: `2 件以上の datum を推奨 (line 系は 3 件以上で trend が見える)`,
209
+ message: `図表 \`${n.id}\` の値 (\`datum\`) が 1 件だけで、 比べることも移り変わりを追うこともできない`,
210
+ suggestion: `値を 2 件以上にする (折れ線グラフは 3 件以上で傾きが読める)`,
173
211
  autoFixable: false,
174
212
  });
175
213
  }
@@ -190,8 +228,8 @@ function ruleGanttUnknownDependsOn(d: CdlDiagram): LintIssue[] {
190
228
  rule: "gantt-unknown-depends-on",
191
229
  severity: "warn",
192
230
  target: t.id,
193
- message: `task "${t.id}" が未定義 task "${t.dependsOn}" に dependsOn 参照`,
194
- suggestion: `参照先 id を修正 or dependsOn を除去`,
231
+ message: `作業 \`${t.id}\` の \`dependsOn\` が、 無い作業 \`${t.dependsOn}\` を指している`,
232
+ suggestion: `\`dependsOn\` を既にある作業の \`id\` に直すか、 \`dependsOn\` を外す`,
195
233
  autoFixable: false,
196
234
  });
197
235
  }
@@ -225,8 +263,8 @@ function ruleMindMapParentReference(d: CdlDiagram): LintIssue[] {
225
263
  rule: "mindmap-unknown-parent",
226
264
  severity: "warn",
227
265
  target: b.id,
228
- message: `branch "${b.id}" が未定義 parent "${b.parent}" を参照`,
229
- suggestion: `parent を rootId ("${n.mindData.rootId}") または既存 branch id に修正`,
266
+ message: `枝 \`${b.id}\` の \`parent\` が、 無い枝 \`${b.parent}\` を指している`,
267
+ suggestion: `\`parent\` を中心 (\`${n.mindData.rootId}\`) か、 既にある枝の \`id\` に直す`,
230
268
  autoFixable: false,
231
269
  });
232
270
  }
@@ -247,8 +285,8 @@ function ruleTreeParentReference(d: CdlDiagram): LintIssue[] {
247
285
  rule: "tree-unknown-parent",
248
286
  severity: "warn",
249
287
  target: t.id,
250
- message: `tree node "${t.id}" が未定義 parent "${t.parent}" を参照`,
251
- suggestion: `parent id を既存 tree node に修正 or parent 除去 (root にする)`,
288
+ message: `階層図の項目 \`${t.id}\` の \`parent\` が、 無い項目 \`${t.parent}\` を指している`,
289
+ suggestion: `\`parent\` を既にある項目の \`id\` に直すか、 \`parent\` を外して一番上の項目にする`,
252
290
  autoFixable: false,
253
291
  });
254
292
  }
@@ -268,8 +306,8 @@ function ruleQuadrantMissingItems(d: CdlDiagram): LintIssue[] {
268
306
  rule: "quadrant-empty",
269
307
  severity: "warn",
270
308
  target: n.id,
271
- message: `quadrant "${n.id}" が item 0 件、 軸のみ表示される`,
272
- suggestion: `.item({ id: ..., title: ..., quadrant: "topLeft" | ... }) を 1 件以上追加`,
309
+ message: `四象限図 \`${n.id}\` に項目 (\`item\`) が 1 件も無く、 軸だけが描かれる`,
310
+ suggestion: `\`.item({ id, title, quadrant })\` で項目を 1 件以上足す (\`quadrant\` は \`topLeft\` などの 4 区画から選ぶ)`,
273
311
  autoFixable: false,
274
312
  });
275
313
  }
@@ -279,8 +317,8 @@ function ruleQuadrantMissingItems(d: CdlDiagram): LintIssue[] {
279
317
  rule: "quadrant-single-quadrant",
280
318
  severity: "info",
281
319
  target: n.id,
282
- message: `quadrant "${n.id}" の item が 1 象限に集中、 マトリクスの意味が薄い`,
283
- suggestion: `2 象限以上に item を分散 (SWOT / Priority matrix 等は 4 象限 balanced を推奨)`,
320
+ message: `四象限図 \`${n.id}\` の項目がすべて 1 つの区画に集まり、 4 つに分けた意味が薄い`,
321
+ suggestion: `項目を 2 つ以上の区画に分ける。 強みと弱みを並べる分析 (\`SWOT\`) や優先度を決める図は、 4 つの区画に散らして使う`,
284
322
  autoFixable: false,
285
323
  });
286
324
  }
@@ -290,13 +328,13 @@ function ruleQuadrantMissingItems(d: CdlDiagram): LintIssue[] {
290
328
  }
291
329
 
292
330
  /**
293
- * 段の人数が減っていくことを見る。
331
+ * 段階の人数が減っていくことを見る。
294
332
  *
295
333
  * 人数の欄は `{名前}` を書ける (状態から取る形、 cdl の `render/payload-binding.ts` が解く)。
296
334
  * その場合ここでは値が決まらないので、**数どうしの組だけを比べる** (#1194)。
297
335
  *
298
336
  * 素通しで比べると文字列の大小比較になり、`{trial}` が `{signup}` より大きいという理由で
299
- * 発火する。 実際に見本帳の funnel を状態から取る形にした時、その偽発火が出た。
337
+ * 発火する。 実際にカタログの funnel を状態から取る形にした時、その偽発火が出た。
300
338
  */
301
339
  function ruleFunnelMonotonicCount(d: CdlDiagram): LintIssue[] {
302
340
  const out: LintIssue[] = [];
@@ -312,8 +350,8 @@ function ruleFunnelMonotonicCount(d: CdlDiagram): LintIssue[] {
312
350
  rule: "funnel-increasing-count",
313
351
  severity: "warn",
314
352
  target: stages[i]!.id,
315
- message: `stage "${stages[i]!.id}" (${stages[i]!.count}) が前段 (${stages[i - 1]!.count}) より増加、 funnel は単調減少が期待される`,
316
- suggestion: `stage 順を再確認、 増加 pattern なら別 preset (chart-line 等) を検討`,
353
+ message: `段階 \`${stages[i]!.id}\` の数 (${今}) が前の段階 (${前}) より多い。 絞り込み図は段階が進むほど数が減る`,
354
+ suggestion: `段階の順番を確かめる。 数が増える流れを示すなら、 折れ線グラフ (\`chart-line\`) などの別の図にする`,
317
355
  autoFixable: false,
318
356
  });
319
357
  }
@@ -12,8 +12,20 @@
12
12
  * 直らない事故を、 規則を共有することで構造的に防ぐ。
13
13
  */
14
14
 
15
- /** 基準からどちら側に置くか。 */
16
- export type RelativeDirection = "right" | "left" | "above" | "below";
15
+ /**
16
+ * 基準からどちら側に置くか。
17
+ *
18
+ * 語の一覧は下の `RELATIVE_DIRECTIONS` が持つ。 型を並べ直さず、一覧から導く。
19
+ */
20
+ export type RelativeDirection = (typeof RELATIVE_DIRECTIONS)[number];
21
+
22
+ /**
23
+ * 向きとして受ける語の一覧。
24
+ *
25
+ * 読み取りの入口が 2 つある (本文の `位置: Web の右 200` と JSON の `posRel.dir`) ため、
26
+ * 一覧を 1 箇所に置いて両方から引く。 2 度書くと、向きを足した時に片方だけが取り残される。
27
+ */
28
+ export const RELATIVE_DIRECTIONS = ["right", "left", "above", "below"] as const;
17
29
 
18
30
  /** 相対で書かれた位置の指定。 */
19
31
  export type RelativePos = {
@@ -180,3 +192,60 @@ export function orderByDependency(
180
192
  }
181
193
  return { order, cyclic: [...cyclic] };
182
194
  }
195
+
196
+ /** 解けない書き方の種類。 誤りの文は入口ごとに作るので、ここでは種類だけを返す */
197
+ export type RelativeProblemKind = "self" | "missing-anchor" | "cyclic";
198
+
199
+ /** 解けない書き方 1 件。 `anchor` は相手が居ない時だけ持つ */
200
+ export type RelativeProblem = {
201
+ name: string;
202
+ kind: RelativeProblemKind;
203
+ anchor?: string;
204
+ };
205
+
206
+ /**
207
+ * 解けない相対の指定を挙げる (#2039)。
208
+ *
209
+ * 解けない書き方は 3 通りある。 相手が居ない / 自分を基準にした / 基準が輪になっている。
210
+ * どれも「書いたのに図が変わらない」 形で表に出るため、図を出す前に知らせる。
211
+ *
212
+ * **判定は本 file に 1 つだけ置く**。 読み取りの入口が 2 つある (本文と JSON) ため、
213
+ * 片方だけに判定を持つと、同じ書き方が入口によって通ったり通らなかったりする。
214
+ * 実際 JSON 側には判定が無く、相対で置く欄そのものが無かった。
215
+ *
216
+ * 誤りの文は返さない。 本文側は行番号を、JSON 側は欄の場所を添えるため、文の組み立ては
217
+ * 入口に任せる。 ここが返すのは「誰の」「どの種類の」 解けなさか だけ。
218
+ *
219
+ * 呼ぶ側は、挙がった名前から相対の指定を外す。 残したままだと、誤りを無視して読み込んだ
220
+ * 経路で解決できない指定が組み立てまで届く。
221
+ */
222
+ export function findRelativeProblems(
223
+ items: ReadonlyArray<{ name: string; rel?: RelativePos }>,
224
+ ): RelativeProblem[] {
225
+ const named = new Set(items.map((i) => i.name));
226
+ const broken = new Set<string>();
227
+ const out: RelativeProblem[] = [];
228
+
229
+ for (const it of items) {
230
+ const rel = it.rel;
231
+ if (!rel) continue;
232
+ if (rel.anchor === it.name) {
233
+ out.push({ name: it.name, kind: "self" });
234
+ broken.add(it.name);
235
+ continue;
236
+ }
237
+ if (!named.has(rel.anchor)) {
238
+ out.push({ name: it.name, kind: "missing-anchor", anchor: rel.anchor });
239
+ broken.add(it.name);
240
+ }
241
+ }
242
+
243
+ // 輪は、既に解けないと判った分を外してから探す。 外さないと「相手が居ない」 1 件が
244
+ // 「輪になっている」 としても挙がり、同じ 1 つの誤りが 2 通りの直し方で出る
245
+ const { cyclic } = orderByDependency(
246
+ items.map((i) => ({ name: i.name, rel: broken.has(i.name) ? undefined : i.rel })),
247
+ );
248
+ for (const name of cyclic) out.push({ name, kind: "cyclic" });
249
+
250
+ return out;
251
+ }
package/src/schema.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * });
14
14
  * const diagram = jsonToDiagram(res.content[0].input);
15
15
  *
16
- * schema JSON は `packages/dragon/schemas/diagram.json` が SSOT、 本 file は import して再 export するだけ。
16
+ * schema JSON は `packages/dragon/src/schemas/diagram.json` が SSOT、 本 file は import して再 export するだけ。
17
17
  */
18
18
 
19
19
  import schemaJson from "./schemas/diagram.json" with { type: "json" };