{
  "id": "P08-C05",
  "title": "Левый и правый стол: артикул связан с изображением",
  "kind": "data",
  "brand": "ЧИСТЫЕ ДАННЫЕ / P08-C05",
  "role": "Проектирование правил, реализация преобразователя и проверка исключений",
  "objective": "Получить воспроизводимое преобразование товарных строк с сохранением исходных данных.",
  "problem": "В карточках офисных столов сторона тумбы терялась при нормализации. Красивый снимок противоположной версии делал карточку неверной.",
  "choice": "Сторону определяю только по явному суффиксу. Каждая нормализованная запись получает точный ключ собственной схемы; неподдерживаемые ширины и X не пропускаю.",
  "result": "Рабочий преобразователь обработал 6 строк: 4 записи разнесены по полям, 2 отправлены в журнал исключений. Исходные значения сохранены; результат можно пересчитать на странице и сопоставить с JSON.",
  "limit": "Использован собственный ограниченный набор строк и явно заданные справочники. Преобразователь не угадывает отсутствующие свойства; до импорта реального каталога правила нужно согласовать с его схемой.",
  "contribution": "Рабочий преобразователь JavaScript, входной набор, результат, журнал исключений и страница для повторного запуска.",
  "panels": [
    {
      "mode": "table",
      "title": "Нормализованные поля",
      "headers": [
        "id",
        "sku",
        "widthMm",
        "side"
      ],
      "rows": [
        [
          "F-101",
          "T-140-L",
          "1400",
          "left"
        ],
        [
          "F-102",
          "T-140-R",
          "1400",
          "right"
        ],
        [
          "F-103",
          "T-160-L",
          "1600",
          "left"
        ],
        [
          "F-104",
          "T-160-R",
          "1600",
          "right"
        ]
      ],
      "caption": "4 готовые записи; остальные 2 требуют отдельного решения."
    },
    {
      "mode": "table",
      "title": "Исключения не потеряны",
      "headers": [
        "ID",
        "Исходная строка"
      ],
      "rows": [
        [
          "F-105",
          "T-140-X"
        ],
        [
          "F-106",
          "T-180-L"
        ]
      ],
      "caption": "Причины каждого исключения доступны в полном JSON."
    }
  ],
  "table": {
    "headers": [
      "id",
      "sku",
      "widthMm",
      "side",
      "imageKey"
    ],
    "rows": [
      [
        "F-101",
        "T-140-L",
        "1400",
        "left",
        "t-140-l.svg"
      ],
      [
        "F-102",
        "T-140-R",
        "1400",
        "right",
        "t-140-r.svg"
      ],
      [
        "F-103",
        "T-160-L",
        "1600",
        "left",
        "t-160-l.svg"
      ],
      [
        "F-104",
        "T-160-R",
        "1600",
        "right",
        "t-160-r.svg"
      ]
    ]
  },
  "extraFiles": [
    {
      "file": "normalizer.js",
      "content": "function normalize(rows){if(!Array.isArray(rows))throw new TypeError(\"Нужен массив строк\");const records=[],exceptions=[];for(const r of rows){if(!r||typeof r.id!==\"string\"||typeof r.raw!==\"string\"){exceptions.push({input:r,reason:\"Нужны текстовые id и raw\"});continue}const sku=r.raw.trim().toUpperCase(),m=sku.match(/^T-(140|160)-(L|R)$/);if(!m){exceptions.push({...r,reason:'Нужен точный размер и сторона L/R'});continue}const widthMm=+m[1]*10,side=m[2]==='L'?'left':'right';records.push({id:r.id,raw:r.raw,sku,widthMm,side,imageKey:sku.toLowerCase()+'.svg'})}return {records,exceptions}}\nif(typeof module!==\"undefined\")module.exports={normalize};\n",
      "label": "Исходник преобразования",
      "mimeType": "text/javascript"
    },
    {
      "file": "input.json",
      "content": "[\n  {\n    \"id\": \"F-101\",\n    \"raw\": \"T-140-L\"\n  },\n  {\n    \"id\": \"F-102\",\n    \"raw\": \"T-140-R\"\n  },\n  {\n    \"id\": \"F-103\",\n    \"raw\": \"T-160-L\"\n  },\n  {\n    \"id\": \"F-104\",\n    \"raw\": \"T-160-R\"\n  },\n  {\n    \"id\": \"F-105\",\n    \"raw\": \"T-140-X\"\n  },\n  {\n    \"id\": \"F-106\",\n    \"raw\": \"T-180-L\"\n  }\n]",
      "label": "Исходные записи",
      "mimeType": "application/json"
    },
    {
      "file": "output.json",
      "content": "{\n  \"records\": [\n    {\n      \"id\": \"F-101\",\n      \"raw\": \"T-140-L\",\n      \"sku\": \"T-140-L\",\n      \"widthMm\": 1400,\n      \"side\": \"left\",\n      \"imageKey\": \"t-140-l.svg\"\n    },\n    {\n      \"id\": \"F-102\",\n      \"raw\": \"T-140-R\",\n      \"sku\": \"T-140-R\",\n      \"widthMm\": 1400,\n      \"side\": \"right\",\n      \"imageKey\": \"t-140-r.svg\"\n    },\n    {\n      \"id\": \"F-103\",\n      \"raw\": \"T-160-L\",\n      \"sku\": \"T-160-L\",\n      \"widthMm\": 1600,\n      \"side\": \"left\",\n      \"imageKey\": \"t-160-l.svg\"\n    },\n    {\n      \"id\": \"F-104\",\n      \"raw\": \"T-160-R\",\n      \"sku\": \"T-160-R\",\n      \"widthMm\": 1600,\n      \"side\": \"right\",\n      \"imageKey\": \"t-160-r.svg\"\n    }\n  ],\n  \"exceptions\": [\n    {\n      \"id\": \"F-105\",\n      \"raw\": \"T-140-X\",\n      \"reason\": \"Нужен точный размер и сторона L/R\"\n    },\n    {\n      \"id\": \"F-106\",\n      \"raw\": \"T-180-L\",\n      \"reason\": \"Нужен точный размер и сторона L/R\"\n    }\n  ]\n}",
      "label": "Результат и исключения",
      "mimeType": "application/json"
    },
    {
      "file": "test-report.json",
      "content": "[\n  {\n    \"name\": \"Входные строки сохранены и распределены\",\n    \"pass\": true\n  },\n  {\n    \"name\": \"Исходный raw сохранён\",\n    \"pass\": true\n  },\n  {\n    \"name\": \"Повторный запуск детерминирован\",\n    \"pass\": true\n  },\n  {\n    \"name\": \"Неверная структура не теряется\",\n    \"pass\": true\n  },\n  {\n    \"name\": \"Верхний уровень должен быть массивом\",\n    \"pass\": true\n  },\n  {\n    \"name\": \"Все числовые поля конечны\",\n    \"pass\": true\n  }\n]",
      "label": "Протокол проверок",
      "mimeType": "application/json"
    },
    {
      "file": "t-140-l.svg",
      "label": "T-140-L · схема",
      "mimeType": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"500\"><rect width=\"900\" height=\"500\" fill=\"#f5f2e8\"/><text x=\"40\" y=\"50\" font-family=\"sans-serif\" font-size=\"30\">T-140-L · 1400 мм</text><rect x=\"150\" y=\"140\" width=\"600\" height=\"60\" fill=\"#b58d60\"/><rect x=\"170\" y=\"200\" width=\"170\" height=\"240\" fill=\"#31524a\"/><path d=\"M720 200 V440\" stroke=\"#31524a\" stroke-width=\"20\"/><text x=\"40\" y=\"480\" font-family=\"sans-serif\" font-size=\"24\">Тумба слева при взгляде со стороны сидящего</text></svg>"
    },
    {
      "file": "t-140-r.svg",
      "label": "T-140-R · схема",
      "mimeType": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"500\"><rect width=\"900\" height=\"500\" fill=\"#f5f2e8\"/><text x=\"40\" y=\"50\" font-family=\"sans-serif\" font-size=\"30\">T-140-R · 1400 мм</text><rect x=\"150\" y=\"140\" width=\"600\" height=\"60\" fill=\"#b58d60\"/><rect x=\"560\" y=\"200\" width=\"170\" height=\"240\" fill=\"#31524a\"/><path d=\"M180 200 V440\" stroke=\"#31524a\" stroke-width=\"20\"/><text x=\"40\" y=\"480\" font-family=\"sans-serif\" font-size=\"24\">Тумба справа при взгляде со стороны сидящего</text></svg>"
    },
    {
      "file": "t-160-l.svg",
      "label": "T-160-L · схема",
      "mimeType": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"500\"><rect width=\"900\" height=\"500\" fill=\"#f5f2e8\"/><text x=\"40\" y=\"50\" font-family=\"sans-serif\" font-size=\"30\">T-160-L · 1600 мм</text><rect x=\"150\" y=\"140\" width=\"600\" height=\"60\" fill=\"#b58d60\"/><rect x=\"170\" y=\"200\" width=\"170\" height=\"240\" fill=\"#31524a\"/><path d=\"M720 200 V440\" stroke=\"#31524a\" stroke-width=\"20\"/><text x=\"40\" y=\"480\" font-family=\"sans-serif\" font-size=\"24\">Тумба слева при взгляде со стороны сидящего</text></svg>"
    },
    {
      "file": "t-160-r.svg",
      "label": "T-160-R · схема",
      "mimeType": "image/svg+xml",
      "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"900\" height=\"500\"><rect width=\"900\" height=\"500\" fill=\"#f5f2e8\"/><text x=\"40\" y=\"50\" font-family=\"sans-serif\" font-size=\"30\">T-160-R · 1600 мм</text><rect x=\"150\" y=\"140\" width=\"600\" height=\"60\" fill=\"#b58d60\"/><rect x=\"560\" y=\"200\" width=\"170\" height=\"240\" fill=\"#31524a\"/><path d=\"M180 200 V440\" stroke=\"#31524a\" stroke-width=\"20\"/><text x=\"40\" y=\"480\" font-family=\"sans-serif\" font-size=\"24\">Тумба справа при взгляде со стороны сидящего</text></svg>"
    }
  ],
  "interactiveHTML": "<section class=\"box\"><h2>Проверить свою строку</h2><p>Вход — JSON-массив с полями id и raw. Измените пример и выполните преобразование.</p><textarea id=\"source\" aria-label=\"Исходные записи\">[\n  {\n    \"id\": \"F-101\",\n    \"raw\": \"T-140-L\"\n  },\n  {\n    \"id\": \"F-102\",\n    \"raw\": \"T-140-R\"\n  },\n  {\n    \"id\": \"F-103\",\n    \"raw\": \"T-160-L\"\n  },\n  {\n    \"id\": \"F-104\",\n    \"raw\": \"T-160-R\"\n  },\n  {\n    \"id\": \"F-105\",\n    \"raw\": \"T-140-X\"\n  },\n  {\n    \"id\": \"F-106\",\n    \"raw\": \"T-180-L\"\n  }\n]</textarea><button id=\"run\">Преобразовать</button><output id=\"result\" aria-live=\"polite\"></output><script src=\"normalizer.js\"></script><script>document.getElementById('run').onclick=function(){try{const data=JSON.parse(document.getElementById('source').value);if(!Array.isArray(data)||data.some(r=>typeof r.id!=='string'||typeof r.raw!=='string'))throw Error('Нужен массив объектов с текстовыми id и raw');document.getElementById('result').textContent=JSON.stringify(normalize(data),null,2)}catch(e){document.getElementById('result').textContent='Ошибка входных данных: '+e.message}};document.getElementById('run').click();</script></section>",
  "checks": [
    "4 записи, 2 исключения. Для каждого imageKey приложен соответствующий SVG с тумбой слева или справа.",
    "Изменить один входной объект и проверить, что результат пересчитался.",
    "Проверить, что неверный JSON даёт сообщение об ошибке, не стирая поле."
  ],
  "closes": [
    "Вместо описания очистки создан и запущен преобразователь.",
    "Поля реально разделены; исходная строка и исключения сохранены."
  ]
}
