{
  "$comment": "よくある間違い → 正しい書き方の対照表。綴り間違いは strict パースが拾うが、構造の間違い（書ける場所を間違える・別の種別のキーを使う）と『落ちないけど意図と違う』間違いはここに置く。bad は strict で必ず落ち、good は必ず通ることを CI で確認している（＝この表は嘘をつけない）。keys は hatake_validate / hatake pitfalls が未知キーから引くための索引。",
  "pitfalls": [
    {
      "id": "columns-outside-table",
      "keys": [
        "columns"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "ページ直下に `columns` を書く",
        "en": "Putting `columns` directly on the page"
      },
      "why": {
        "ja": "列は一覧（`table`）の持ち物。`columns` が有効なのは `table` / `dashboardItem` / `field`（明細）の中だけ。",
        "en": "Columns belong to a table. `columns` is only valid inside `table`, a dashboard card, or a subTable `field`."
      },
      "fix": {
        "ja": "`table.columns` に入れる。`layout.columns`（1行あたりの項目数）とは別物なので混同しないこと。",
        "en": "Nest it under `table.columns`. Do not confuse it with `layout.columns`, which is the number of items per row."
      },
      "bad": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  columns:",
        "    - { field: orderNo, label: 受注番号 }"
      ],
      "good": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  table:",
        "    columns:",
        "      - { field: orderNo, label: 受注番号 }"
      ]
    },
    {
      "id": "fields-outside-sections",
      "keys": [
        "fields"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "`form` の直下に `fields` を書く",
        "en": "Putting `fields` directly under `form`"
      },
      "why": {
        "ja": "フォームは必ず section で区切る（`form` が持てるキーは `sections` だけ）。見出しが要らなければ `title` を省いた section を1つ置く。",
        "en": "A form is always divided into sections — `sections` is the only key `form` accepts. If you do not want a heading, use one section without a `title`."
      },
      "fix": {
        "ja": "`form.sections[].fields` に入れる。",
        "en": "Nest it under `form.sections[].fields`."
      },
      "bad": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    fields:",
        "      - { field: code, label: コード }"
      ],
      "good": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: code, label: コード }"
      ]
    },
    {
      "id": "filters-outside-search",
      "keys": [
        "filters"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "ページ直下に `filters` を書く",
        "en": "Putting `filters` directly on the page"
      },
      "why": {
        "ja": "検索条件は `search` の持ち物。ページ直下の `filters` は無い（ダッシュボードのカードの `filters` は「固定の絞り込み値」で別物）。",
        "en": "Filters belong to `search`. There is no page-level `filters`; a dashboard card's `filters` is a different thing (fixed filter values)."
      },
      "fix": {
        "ja": "`search.filters` に入れる。",
        "en": "Nest it under `search.filters`."
      },
      "bad": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  filters:",
        "    - { field: status, label: 状態 }"
      ],
      "good": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  search:",
        "    filters:",
        "      - { field: status, label: 状態 }"
      ]
    },
    {
      "id": "form-on-page-without-form",
      "keys": [
        "form"
      ],
      "pageKinds": [
        "search",
        "wizard",
        "dashboard",
        "report"
      ],
      "wrong": {
        "ja": "`search` / `wizard` / `dashboard` / `report` に `form` を書く",
        "en": "Adding `form` to a `search` / `wizard` / `dashboard` / `report` page"
      },
      "why": {
        "ja": "`form` を持つのは `crud` / `master` / `detail` / `form` だけ。`search` は読み取り専用、`wizard` は `form` ではなく `steps`、`dashboard` と `report` は入力しない。",
        "en": "Only `crud` / `master` / `detail` / `form` have a `form`. A `search` page is read-only, a `wizard` uses `steps` instead, and dashboards and reports do not take input."
      },
      "fix": {
        "ja": "入力もさせたいなら `crud`（または `master`）にする。照会と入力を分けたいなら `search` ページ＋`detail`／`form` ページにして `navigate` で繋ぐ。",
        "en": "Use `crud` (or `master`) if the page should also edit. To keep them apart, use a `search` page plus a `detail` / `form` page and link them with a `navigate` action."
      },
      "bad": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: orderNo, label: 受注番号 }"
      ],
      "good": [
        "page:",
        "  type: crud",
        "  id: order_master",
        "  title: 受注保守",
        "  repository: orderRepository",
        "  table:",
        "    columns:",
        "      - { field: orderNo, label: 受注番号 }",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: orderNo, label: 受注番号 }"
      ]
    },
    {
      "id": "table-on-page-without-table",
      "keys": [
        "table"
      ],
      "pageKinds": [
        "detail",
        "form",
        "wizard",
        "dashboard"
      ],
      "wrong": {
        "ja": "`form` / `detail` / `wizard` / `dashboard` に `table` を書く",
        "en": "Adding `table` to a `form` / `detail` / `wizard` / `dashboard` page"
      },
      "why": {
        "ja": "`table`（一覧）を持つのは `crud` / `master` / `search` / `report` だけ。単票やステップ入力は1件を扱うので一覧を持たない。",
        "en": "Only `crud` / `master` / `search` / `report` have a `table`. Single-record and stepped pages address one record, so they have no list."
      },
      "fix": {
        "ja": "一覧も要るなら `crud` にする。1件の中に明細行を持ちたいだけなら、`field` を `type: subTable` にして `columns` を書く（親子・明細）。",
        "en": "Use `crud` if you need a list. If you only want child rows inside one record, make a `field` of `type: subTable` and give it `columns` (master-detail)."
      },
      "bad": [
        "page:",
        "  type: form",
        "  id: order_entry",
        "  title: 受注入力",
        "  repository: orderRepository",
        "  table:",
        "    columns:",
        "      - { field: productName, label: 商品 }"
      ],
      "good": [
        "page:",
        "  type: form",
        "  id: order_entry",
        "  title: 受注入力",
        "  repository: orderRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - field: lines",
        "            label: 明細",
        "            type: subTable",
        "            columns:",
        "              - { field: productName, label: 商品 }",
        "              - { field: qty, label: 数量, type: number }"
      ]
    },
    {
      "id": "steps-on-page-without-steps",
      "keys": [
        "steps"
      ],
      "pageKinds": [
        "crud",
        "master",
        "search",
        "detail",
        "form",
        "dashboard",
        "report"
      ],
      "wrong": {
        "ja": "`wizard` 以外に `steps` を書く",
        "en": "Adding `steps` to a page that is not a `wizard`"
      },
      "why": {
        "ja": "`steps` は `wizard` だけのキー。逆に `wizard` は `form` を持たない（`steps` が section の役をする）。",
        "en": "`steps` belongs to `wizard` only. Conversely a `wizard` has no `form` — its steps play the part of sections."
      },
      "fix": {
        "ja": "入力をステップに分けたいなら `type: wizard` にして、`steps` に `id` と `title` を持つステップを並べる。",
        "en": "Use `type: wizard` and list steps, each with an `id` and a `title`."
      },
      "bad": [
        "page:",
        "  type: form",
        "  id: customer_wizard",
        "  title: 顧客登録",
        "  repository: customerRepository",
        "  steps:",
        "    - { id: basic, title: 基本情報 }"
      ],
      "good": [
        "page:",
        "  type: wizard",
        "  id: customer_wizard",
        "  title: 顧客登録",
        "  repository: customerRepository",
        "  steps:",
        "    - id: basic",
        "      title: 基本情報",
        "      fields:",
        "        - { field: code, label: コード, required: true }"
      ]
    },
    {
      "id": "key-on-page-without-record",
      "keys": [
        "key"
      ],
      "pageKinds": [
        "dashboard",
        "report"
      ],
      "wrong": {
        "ja": "`dashboard` / `report` に `key` を書く",
        "en": "Adding `key` to a `dashboard` or `report` page"
      },
      "why": {
        "ja": "`key` は「1件のレコードの主キー項目名」。ダッシュボードと帳票は単一レコードを指さないので持たない。",
        "en": "`key` names the primary-key field of one record. Dashboards and reports do not address a single record, so they have none."
      },
      "fix": {
        "ja": "消す。行から1件を開きたいなら `navigate` アクションで `detail` ページへ渡す（`params: { id: \"$row.id\" }`）。",
        "en": "Remove it. To open one record from a row, use a `navigate` action to a `detail` page with `params: { id: \"$row.id\" }`."
      },
      "bad": [
        "page:",
        "  type: report",
        "  id: sales_report",
        "  title: 売上明細表",
        "  repository: orderRepository",
        "  key: id",
        "  table:",
        "    columns:",
        "      - { field: amount, label: 金額, type: number }"
      ],
      "good": [
        "page:",
        "  type: report",
        "  id: sales_report",
        "  title: 売上明細表",
        "  repository: orderRepository",
        "  table:",
        "    columns:",
        "      - { field: amount, label: 金額, type: number }"
      ]
    },
    {
      "id": "theme-on-page",
      "keys": [
        "theme"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "ページに `theme` を書く",
        "en": "Putting `theme` on a page"
      },
      "why": {
        "ja": "見た目はアプリ全体の性質なので `app.theme` にしかない。画面ごとに色が変わる業務システムは、ふつう作らない。",
        "en": "Look and feel belongs to the whole application, so it exists only as `app.theme`. A business system rarely wants a different palette per screen."
      },
      "fix": {
        "ja": "`app:` の直下に移す。1画面だけ違う見た目にしたいなら Renderer 側（Flutter なら `Theme`）で包む。",
        "en": "Move it directly under `app:`. To restyle a single screen, wrap it on the renderer side (a `Theme` in Flutter)."
      },
      "bad": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  theme:",
        "    primaryColor: \"#1B5E20\""
      ],
      "good": [
        "app:",
        "  id: sales_admin",
        "  title: 販売管理",
        "  theme:",
        "    primaryColor: \"#1B5E20\"",
        "    density: compact",
        "  pages:",
        "    - type: search",
        "      id: order_search",
        "      title: 受注照会",
        "      repository: orderRepository"
      ]
    },
    {
      "id": "confirm-written-in-code",
      "keys": [
        "confirm",
        "onSuccess"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "確認ダイアログや「保存できたら戻る」を Dart / プラグインで書く",
        "en": "Coding the confirmation dialog, or \"go back once saved\", in Dart or a plugin"
      },
      "why": {
        "ja": "「削除前に確認する」「終わったら一覧に戻る」は**その操作の業務ルール**なので、画面ごとに実装すると画面ごとにズレる。`delete` は宣言が無くても必ず確認する。",
        "en": "\"Confirm before deleting\" and \"return to the list afterwards\" are business rules about that operation, so implementing them per screen makes them differ per screen. A `delete` asks even with nothing declared."
      },
      "fix": {
        "ja": "アクションに `confirm` / `onSuccess` を書く。`onSuccess` は**成功したときだけ**動くので、失敗時に「保存しました」と出る事故も防げる。",
        "en": "Declare `confirm` / `onSuccess` on the action. `onSuccess` runs only on success, so it cannot announce \"saved\" after a failure."
      },
      "good": [
        "page:",
        "  type: crud",
        "  id: customer_master",
        "  title: 顧客マスタ",
        "  repository: customerRepository",
        "  table:",
        "    rowActions: [edit, delete]",
        "    columns: [{ field: code, label: コード }]",
        "  actions:",
        "    - id: delete",
        "      type: delete",
        "      label: 削除",
        "      confirm:",
        "        message: 受注履歴から辿れなくなります。よろしいですか？",
        "        okLabel: 削除する",
        "        danger: true",
        "      onSuccess:",
        "        message: 顧客を削除しました"
      ]
    },
    {
      "id": "edit-detected-by-key",
      "keys": [
        "enabledWhen",
        "visibleWhen",
        "mode"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "「編集のとき」をキー項目の有無で判定する",
        "en": "Detecting \"while editing\" by looking at the key field"
      },
      "why": {
        "ja": "`{ field: id, operator: isEmpty }` は動くが、**なぜ id を見ているのかが定義から読み取れない**。キー項目名を変えたら黙って壊れるし、`key` を持たないページでは成り立たない。",
        "en": "`{ field: id, operator: isEmpty }` works, but **nothing in the definition says why `id` is being inspected**. Rename the key field and it breaks silently, and pages without a `key` cannot do it at all."
      },
      "fix": {
        "ja": "`{ mode: create }` / `{ mode: edit }` と書く。フォームの状態そのものなので、キー項目に依存しない。",
        "en": "Write `{ mode: create }` / `{ mode: edit }` — it is the form's own state, independent of the key field."
      },
      "good": [
        "page:",
        "  type: crud",
        "  id: customer_master",
        "  title: 顧客マスタ",
        "  repository: customerRepository",
        "  table:",
        "    columns: [{ field: code, label: コード }]",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: code, label: コード, enabledWhen: { mode: create } }",
        "          - { field: updatedBy, label: 更新者, readOnly: true,",
        "              visibleWhen: { mode: edit } }"
      ]
    },
    {
      "id": "cascade-by-visiblewhen",
      "keys": [
        "optionsFrom",
        "when",
        "optionsSource"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "選択肢の連動を、子の項目を親の値ごとに並べて `visibleWhen` で出し分けて作る",
        "en": "Building a cascade by repeating the child field once per parent value and switching with `visibleWhen`"
      },
      "why": {
        "ja": "親の値が3つなら項目が3つになり、保存される項目名も3つに分かれる（`cityTokyo` / `cityOsaka` …）。組み合わせが増えると破綻し、データの形まで歪む。",
        "en": "Three parent values mean three fields and three stored names (`cityTokyo`, `cityOsaka`, …). It collapses as combinations grow, and it distorts the data shape."
      },
      "fix": {
        "ja": "子は1項目のままにして、`optionsFrom` で親を指し、各選択肢に `when` を書く。選択肢がデータなら `optionsSource` で Repository から引く。",
        "en": "Keep one child field: point at the parent with `optionsFrom` and mark each option with `when`. When the choices are data, fetch them with `optionsSource`."
      },
      "good": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: prefecture, label: 都道府県, type: select,",
        "              options: [{ value: tokyo, label: 東京都 }, { value: osaka, label: 大阪府 }] }",
        "          - field: city",
        "            label: 市区町村",
        "            type: select",
        "            optionsFrom: prefecture",
        "            options:",
        "              - { value: shibuya, label: 渋谷区, when: tokyo }",
        "              - { value: kita,    label: 北区,   when: osaka }"
      ]
    },
    {
      "id": "groupby-without-sort",
      "keys": [
        "groupBy"
      ],
      "pageKinds": [
        "report"
      ],
      "wrong": {
        "ja": "`groupBy` を書いて `sort` を書かない",
        "en": "Using `groupBy` without `sort`"
      },
      "why": {
        "ja": "グループはコントロールブレイク（並んで来た行を上から見て、キーが変わったら小計）。Framework は並べ替えないので、同じ顧客が離れて届くとグループが分裂して小計が何度も出る。",
        "en": "Grouping is a control break: rows are read in order and a subtotal is emitted when the key changes. The framework never sorts, so if rows of the same group arrive apart, the group splits and its subtotal repeats."
      },
      "fix": {
        "ja": "`report.sort` に「印刷したい並び」を書く（Repository に渡る）。並べ替えは DB の責務。",
        "en": "Declare the print order in `report.sort` — it is passed to the repository. Sorting is the database's job."
      },
      "good": [
        "page:",
        "  type: report",
        "  id: sales_report",
        "  title: 売上明細表",
        "  repository: orderRepository",
        "  table:",
        "    columns:",
        "      - { field: customer, label: 顧客名 }",
        "      - { field: amount, label: 金額, type: number, format: currency }",
        "  report:",
        "    sort: { field: customer }",
        "    groupBy: [ { field: customer, label: 顧客 } ]",
        "    totals: [ { field: amount, aggregate: sum } ]"
      ]
    },
    {
      "id": "metric-without-aggregate",
      "keys": [
        "value"
      ],
      "pageKinds": [
        "dashboard"
      ],
      "wrong": {
        "ja": "合計を出したいのに `value` を省く",
        "en": "Omitting `value` when you want a sum"
      },
      "why": {
        "ja": "`value` を省いた `metric` カードは **件数（count）**。金額を足したいのに件数が出る、という間違いは画面を見ても気づきにくい。",
        "en": "A `metric` card without `value` counts rows. Getting a count where you wanted a sum is hard to notice by looking at the screen."
      },
      "fix": {
        "ja": "`value: { aggregate: sum, field: amount }` のように、畳み込み方と対象項目を書く。`count` 以外は `field` が必須。",
        "en": "Declare the reduction and the field, e.g. `value: { aggregate: sum, field: amount }`. Everything except `count` needs a `field`."
      },
      "good": [
        "page:",
        "  type: dashboard",
        "  id: sales_dashboard",
        "  title: 売上ダッシュボード",
        "  repository: orderRepository",
        "  items:",
        "    - { id: orderCount, title: 受注件数 }",
        "    - id: total",
        "      title: 受注金額",
        "      value: { aggregate: sum, field: amount }",
        "      format: currency"
      ]
    },
    {
      "id": "between-in-condition",
      "keys": [
        "visibleWhen",
        "enabledWhen"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "条件（`visibleWhen` / `enabledWhen`）で `between` を使う",
        "en": "Using `between` in a condition (`visibleWhen` / `enabledWhen`)"
      },
      "why": {
        "ja": "`between` は検索条件専用の演算子。条件式が知っているのは `equals` `notEquals` `gt` `gte` `lt` `lte` `contains` `in` `isEmpty` `isNotEmpty` だけで、知らない演算子は**黙って false**になる（＝項目が出てこない）。",
        "en": "`between` is a search-only operator. A condition understands only `equals` `notEquals` `gt` `gte` `lt` `lte` `contains` `in` `isEmpty` `isNotEmpty`; an unknown operator silently evaluates to false, so the field never appears."
      },
      "fix": {
        "ja": "`all` で `gte` と `lte` を組み合わせる。",
        "en": "Combine `gte` and `lte` with `all`."
      },
      "good": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: age, label: 年齢, type: number }",
        "          - field: note",
        "            label: 備考",
        "            type: textarea",
        "            visibleWhen:",
        "              all:",
        "                - { field: age, operator: gte, value: 20 }",
        "                - { field: age, operator: lte, value: 65 }"
      ]
    },
    {
      "id": "rowactions-as-objects",
      "keys": [
        "rowActions"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "`rowActions` にオブジェクトを並べる",
        "en": "Listing objects in `rowActions`"
      },
      "why": {
        "ja": "`rowActions` は**アクション id の文字列配列**。中身（ラベル・型）は `actions` に1回書く。`edit` / `delete` は組み込みなので宣言も要らない。",
        "en": "`rowActions` is an array of action *ids* as strings. The action itself is declared once in `actions`. `edit` and `delete` are built in and need no declaration."
      },
      "fix": {
        "ja": "`rowActions: [edit, delete]` のように書く。独自の行アクションは `actions` に `{ id, type, label }` を足して、その `id` を並べる。",
        "en": "Write `rowActions: [edit, delete]`. For your own row action, add `{ id, type, label }` to `actions` and list that `id`."
      },
      "good": [
        "page:",
        "  type: crud",
        "  id: customer_master",
        "  title: 顧客マスタ",
        "  repository: customerRepository",
        "  table:",
        "    rowActions: [edit, delete, openDetail]",
        "    columns:",
        "      - { field: code, label: コード }",
        "  actions:",
        "    - { id: openDetail, type: navigate, label: 詳細, page: customer_detail,",
        "        params: { id: \"$row.id\" } }"
      ]
    },
    {
      "id": "required-as-validator-only",
      "keys": [
        "validators",
        "required"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "`validators` に文字列を並べる（`validators: [required, email]`）",
        "en": "Listing bare strings in `validators` (`validators: [required, email]`)"
      },
      "why": {
        "ja": "`validators` の要素は `{ type, ...params }` のオブジェクト。文字列だけ書いても検証は増えない（`validators` の中は自由な入れ物なので strict でも落ちない＝気づけない）。",
        "en": "Each entry in `validators` is an object `{ type, ...params }`. Bare strings add no validation, and because `validators` is a free-form container even strict parsing will not complain."
      },
      "fix": {
        "ja": "`- { type: email }` の形で書く。必須は `required: true`（項目直下）でも `- { type: required }` でもよい。",
        "en": "Write `- { type: email }`. For required you may use either `required: true` on the field or `- { type: required }`."
      },
      "good": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - field: mail",
        "            label: メール",
        "            required: true",
        "            validators:",
        "              - { type: email }",
        "              - { type: maxLength, value: 120 }"
      ]
    },
    {
      "id": "conditional-required-by-validators",
      "keys": [
        "requiredWhen",
        "validators",
        "required"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "条件によって必須にしたいので、`validators` に条件を書こうとする（`- { type: required, when: ... }`）",
        "en": "Trying to express a conditional requirement inside `validators` (`- { type: required, when: ... }`)"
      },
      "why": {
        "ja": "`validators` の要素はその項目の値だけを見る規則で、他の項目は見えない。`when` のような余分なキーは黙って無視されるので、**いつでも必須**になる（`validators` は自由な入れ物なので strict でも落ちない＝気づけない）。",
        "en": "A `validators` entry only looks at that field's own value; it cannot see other fields. An extra key like `when` is silently ignored, so the field ends up **always required** — and because `validators` is a free-form container, strict parsing will not complain."
      },
      "fix": {
        "ja": "項目直下の `requiredWhen` に条件を書く。判定は3言語の `FormValidator` が同じ定義で行うので、サーバ側でも効く。",
        "en": "Put the condition in `requiredWhen` on the field. The same definition drives `FormValidator` in all three editions, so it holds server-side too."
      },
      "good": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: kind, label: 区分, type: select,",
        "              options: [{ value: personal, label: 個人 }, { value: corp, label: 法人 }] }",
        "          - field: invoiceNo",
        "            label: 登録番号",
        "            requiredWhen: { field: kind, value: corp }"
      ]
    },
    {
      "id": "hidden-field-still-required",
      "keys": [
        "visibleWhen",
        "required",
        "requiredWhen"
      ],
      "pageKinds": [],
      "wrong": {
        "ja": "条件で隠す項目に `required: true` を残したまま、必須が効き続けると思っている（あるいは効かないように条件を二重に書く）",
        "en": "Assuming `required: true` still applies to a field hidden by a condition — or writing the condition twice to work around it"
      },
      "why": {
        "ja": "**隠れている項目は検証しない**（`required` も他のバリデータも飛ぶ）。入力できない項目を必須にすると、直せないのに保存できない画面になるため。この規則を知らないと、`requiredWhen` に同じ条件を書き足して二重管理になる。",
        "en": "**A hidden field is not validated** — `required` and every other validator are skipped, because requiring input nobody can give produces a form that cannot be saved or fixed. Not knowing this leads to duplicating the condition into `requiredWhen`."
      },
      "fix": {
        "ja": "「出たら必須」は `visibleWhen` ＋ `required: true` でよい（条件は1回だけ）。`requiredWhen` は「**出ているのに**条件で必須が変わる」ときに使う。",
        "en": "\"Required once shown\" is just `visibleWhen` + `required: true` — the condition appears once. `requiredWhen` is for a field that is **visible** but only sometimes required."
      },
      "good": [
        "page:",
        "  type: form",
        "  id: customer_form",
        "  title: 顧客入力",
        "  repository: customerRepository",
        "  form:",
        "    sections:",
        "      - fields:",
        "          - { field: kind, label: 区分, type: select,",
        "              options: [{ value: personal, label: 個人 }, { value: corp, label: 法人 }] }",
        "          - field: corpName",
        "            label: 法人名",
        "            required: true",
        "            visibleWhen: { field: kind, value: corp }"
      ]
    },
    {
      "id": "error-message-written-in-code",
      "keys": ["onError", "onSuccess"],
      "pageKinds": [],
      "wrong": {
        "ja": "失敗したときの文言を、プラグインのハンドラの中で `ScaffoldMessenger` に出す",
        "en": "Showing the failure message from inside the plugin handler"
      },
      "why": {
        "ja": "同じ失敗が画面ごとに違う意味を持つ（「在庫が足りません」/「締め済みなので直せません」）ので、文言はその画面の業務の話。ハンドラの中に書くと、画面ごとに文言が散り、定義を読んでも何と言うのか分からない。一括の「5件中1件失敗」も、件数だけ報告すれば文言は定義側で組める。",
        "en": "The same failure means different things per screen, so the wording belongs to that screen's business. Put it in the handler and it scatters: reading the definition no longer tells you what the user is told. For a bulk run the handler only needs to report counts; the sentence is assembled from the definition."
      },
      "fix": {
        "ja": "アクションに `onError: { message: … }` を書く（`{error}` で理由も出せる）。ハンドラは**投げるか、件数を `ctx.report` で返すか**だけにする。`onError` に遷移先は無い＝失敗した画面から離れない。",
        "en": "Declare `onError: { message: … }` on the action (`{error}` interpolates the reason). The handler only throws, or reports counts through `ctx.report`. `onError` has no target page: a failure does not move the screen."
      },
      "good": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  key: orderNo",
        "  table:",
        "    columns: [{ field: orderNo, label: 受注番号 }]",
        "  actions:",
        "    - id: approveSelected",
        "      type: plugin",
        "      plugin: approveOrders",
        "      label: 一括承認",
        "      scope: selection",
        "      confirm: { message: 選んだ受注を承認します }",
        "      onSuccess: { message: '{count} 件を承認しました' }",
        "      onError: { message: '{count} 件を承認しました（{failed} 件は出荷済み）' }"
      ]
    },
    {
      "id": "bulk-delete",
      "keys": ["scope", "actions"],
      "pageKinds": ["crud", "master", "search"],
      "wrong": {
        "ja": "`scope: selection` に `type: delete` を書いて「一括削除」にする",
        "en": "Writing `type: delete` with `scope: selection` to get a bulk delete"
      },
      "why": {
        "ja": "選んだ行にまとめて実行できるのは `type: plugin` だけなので、押しても実行されない。用意していないのは、取り消せない操作は**事故が件数ぶん大きくなる**から。1件ずつなら「押し間違えた」で済むが、全選択のあとの1回は戻せない。",
        "en": "Only `type: plugin` runs over a selection, so the button does nothing. It is missing on purpose: an irreversible action scales its accidents with the row count. One row is a slip; one press after select-all cannot be undone."
      },
      "fix": {
        "ja": "消すのは1件ずつ（`table.rowActions` の `delete`）。どうしてもまとめて消すなら `type: plugin` にして、消す条件と件数の上限をアプリ側で持つ（そこで確認と記録も残せる）。",
        "en": "Delete one row at a time (`delete` in `table.rowActions`). If a batch really is needed, make it a `type: plugin` and let the application own the conditions, the row limit, the confirmation and the audit trail."
      },
      "good": [
        "page:",
        "  type: search",
        "  id: order_search",
        "  title: 受注照会",
        "  repository: orderRepository",
        "  key: orderNo",
        "  table:",
        "    rowActions: [delete]",
        "    columns:",
        "      - { field: orderNo, label: 受注番号 }",
        "  actions:",
        "    - { id: remove, type: delete, label: 削除, roles: [admin] }",
        "    - id: approveSelected",
        "      type: plugin",
        "      plugin: approveOrders",
        "      label: 一括承認",
        "      scope: selection",
        "      roles: [manager]",
        "      confirm: { message: 選んだ受注を承認します }"
      ]
    },
    {
      "id": "print-without-report",
      "keys": [
        "actions",
        "report"
      ],
      "pageKinds": [
        "crud",
        "master",
        "search",
        "detail",
        "form",
        "wizard",
        "dashboard"
      ],
      "wrong": {
        "ja": "一覧の画面に `type: print` の印刷ボタンを置く",
        "en": "Putting a `type: print` button on a list screen"
      },
      "why": {
        "ja": "紙の形（用紙・1枚の行数・グループ・小計）を決めているのは `report` なので、`report` の無い画面には刷るものが無い。定義は通り、ボタンも出るが、押すと「このページでは刷れません」と言われる（押すまで分からない）。",
        "en": "The shape of the paper — sheet size, lines per sheet, groups, totals — comes from `report`, so a screen without one has nothing to print. The definition parses and the button appears, but pressing it only reports that this page cannot print."
      },
      "fix": {
        "ja": "印刷は帳票（`type: report`）に置く。一覧をそのまま持ち出したいなら `type: export`（CSV）で、こちらはどの画面でも動く。",
        "en": "Put printing on a report page (`type: report`). To take a list away as a file, use `type: export` (CSV) instead — that works on any screen."
      },
      "good": [
        "page:",
        "  type: report",
        "  id: sales_report",
        "  title: 売上明細表",
        "  repository: orderRepository",
        "  table:",
        "    columns:",
        "      - { field: amount, label: 金額, type: number }",
        "  report:",
        "    paper: { size: A4 }",
        "    rowsPerPage: 30",
        "  actions:",
        "    - { id: printPdf, type: print, label: 印刷 }"
      ]
    }
  ]
}
