Skip to the content.

План міграції формату проєкту на текстовий, придатний для агентного редагування

Створено: 2026-09-06 Оновлено: 2026-09-06 — перехід завершено, крім етапу 4 (розділ 0) Вихідний стан: START_REVIEW.md Опис формату: PROJECT_FORMAT.md — повний довідник


0. Статус виконання

Оновлено: 2026-09-06.

Застосунок зберігає проєкти в новому форматі. .rsf лишається лише на читання. Опис формату — PROJECT_FORMAT.md.

Етап Стан Перевірка
0. Тестова інфраструктура ✅ виконано storage-test і local-client — 49 тестів
1. Детермінізм у .rsf ✅ виконано resaveIsByteIdentical — повторне збереження побайтово однакове
2. Прибрати блоб ✅ виконано підписи й вигляд стрілок винесені; діаграми не змінилися
3. Стабільні ID ✅ виконано StableIds; ідентифікатор оборотний, реєстр не потрібен
4. Семантика vs розкладка ⛔ не зроблено розблоковано еталонними знімками
5. Схемний шар ✅ виконано PersistentCodec поверх PersistentWrapper
6. Читач і писар ✅ виконано цикл проєкт → .rsf → проєкт дослівний, діаграми піксельно ті самі
7. Розділення шарів, .rsf на читання ✅ виконано «Зберегти» пише каталог; /user/* у .local/; тип файлу зареєстровано

Чому читач нарешті працює

Перша спроба створювала сутності через Engine API. Плагіни реагують на такі виклики власною добудовою моделі — створюють базові функції для нових класифікаторів, перебудовують ієрархію — і імпорт давав то дублікати, то пропуски. Прапорець disableAutoupdate гасив частину поведінки, але не всю.

Читач переписано нижче Engine, на IEngine — той самий рівень, на якому завантажується .rsf. У MemoryDatabase це відбувається до створення журнального рушія й до plugin.init(), тож плагіни бачать уже цілісну модель і нічого не добудовують.

Наслідки для формату виявилися більшими, ніж очікувалося:

  1. Числові ключі відновлюються точно, тому реєстр відповідностей зник: ідентифікатор — оборотна функція ключа, і цього досить.
  2. Системні класифікатори більше не виняток. QualifiersQualifier, AttributesQualifier, базові функції, звіти — усе відновлюється дослівно, і винятки в тестах прибрано.
  3. Системні атрибути не потребують позначки sys: — усі атрибути адресуються однаково, за іменем.

Що змінилося у гарантіях

Було Стало
цикл зберігає користувацькі дані, з переліком винятків цикл зберігає всі файли проєкту дослівно
діаграми після циклу не перевіряються (тест вимкнено) діаграми порівнюються піксельно, без допуску
еталон діаграм звіряється з допуском 1.0 точне порівняння: відмальовування детерміноване

Еталонні знімки діаграм

Головна перешкода етапів 2 і 4 була не в складності, а в перевірці: зміни в коді відображення не ловилися жодним тестом. Тепер ловляться.

DiagramRenderer малює кожну діаграму кожного зразка без екрана (через наявний PIDEF0painter), а DiagramGoldenTest порівнює з еталоном у storage-test/src/test/resources/diagram-golden.properties — 56 діаграм. Перед малюванням з моделі прибирається все, що не залежить від коду: дати в рамці замінюються сталою, а шрифти — вбудованим RamusGoldenSans.ttf. Інакше еталон ловив би сьогоднішнє число та набір шрифтів машини.

Оновити еталон свідомо:

./gradlew :storage-test:test -Dramus.golden.update=1

Порівнюється стиснутий відбиток 48×48 у відтінках сірого, а не байти PNG: відбиток не залежить від версії кодувальника. Допуск — нульовий.

Що з’явилося в коді

Файл Призначення
storage-test/ новий модуль з тестами формату
core/.../impl/XmlDates.java дати ISO-8601 UTC + читання застарілих форматів
core/.../impl/PropertiesXml.java детермінований запис Properties у XML
core/.../format/PersistentCodec.java персистент ↔ мапа через анотації
core/.../format/StableIds.java оборотні <slug>--<id>
core/.../format/ProjectWriter.java запис проєкту в дерево YAML
core/.../format/ProjectReader.java читання проєкту нижче рівня плагінів
storage-test/.../DumpSchemaTest.java друк схеми типів атрибутів для звірки з докою
core/.../format/yaml/YamlFormat.java канонічні налаштування YAML 1.2
common/.../PropertiesXml.java детермінований запис Properties
ramus-core-demo/.../RsfToYaml.java CLI: .rsf → YAML
ramus-core-demo/.../YamlToRsf.java CLI: YAML → .rsf

Запуск конвертерів:

./gradlew :ramus-core-demo:rsfToYaml -Prsf=<файл.rsf> -Pout=<каталог>
./gradlew :ramus-core-demo:yamlToRsf -Pin=<каталог> -Prsf=<файл.rsf>

Вони потрібні для пакетного перетворення; сам застосунок працює з каталогом проєкту напряму.

Дефекти, знайдені й виправлені попутно

  1. Застарілі дати не читалися взагалі на сучасній JDK. Формат брався з DateFormat.getDateTimeInstance(SHORT, SHORT, ENGLISH), а він залежить від набору локальних даних: до переходу JDK на CLDR це було M/d/yy h:mm a, після — M/d/yy, h:mm a. Старі файли переставали парситися, виняток ковтався, дати губилися мовчки. Тепер шаблони задані явно, з переліком застарілих.
  2. Невстановлений параметр PreparedStatement. При невдалому розборі дати колонка лишалася незаданою, і драйвер валив запис усієї таблиці. Тепер ставиться null з повідомленням, що називає таблицю й значення.
  3. Обірвані посилання в даних. У зразках трапляються посилання на видалені елементи (previousElementId вказує в нікуди). Старий формат зберігав числовий ключ, і це ніяк не проявлялося. Новий не вигадує посилання: якщо сутності немає, записується «посилання відсутнє».

Уроки, які змінили конструкцію

  1. Рівень читання важливіший за все інше. Поки читач працював через Engine, кожна вада мала вигляд окремої: то дублікат класифікатора, то зниклий елемент, то перейменований чужий атрибут. Причина була одна — плагіни добудовували модель у відповідь на виклики. Перехід на IEngine зняв усі ці випадки разом, а заразом і половину коду читача: зникли реєстр відповідностей, пошук за іменем, прапорець автодобудови й перелік «дзеркальних» класифікаторів.
  2. Атрибути зручніше адресувати іменем, а не ключем. F_BOUNDS у файлі зрозуміле без словника, vakjk8 — ні. Ім’я унікальне майже завжди, а рідкісний збіг знімається суфіксом #<id>.
  3. Властивості атрибутів — частина моделі. Значення з {@code elementId = -1} тримають конфігурацію плагіна (для Core.ElementList — пов’язані класифікатори). Без них значення елементів не відновлюються.
  4. Імпорт має бути двопрохідним. Значення посилаються на елементи інших класифікаторів, тож спершу створюються всі сутності, потім властивості атрибутів, і лише тоді значення.
  5. Системні класифікатори містять реальні дані. Базові функції моделей IDEF0, звіти й сектори живуть у класифікаторах із познакою «системний». Пропустити їх означало б експортувати модель без кореневої функції. Коли читання відбувається нижче плагінів, винятків не потрібно взагалі — навіть QualifiersQualifier і AttributesQualifier відновлюються дослівно.
  6. Потоки — теж модель. /properties/* тримає налаштування IDEF0, /elements/* — вкладення користувача. Без них модель не будується. /user/* не експортується свідомо: це персональний стан інтерфейсу.

Етап 2: обидва блоби винесені

У моделі було два двійкових поля. Обидва замінені іменованими полями, зі збереженням читання старих файлів.

Було Стало
F_VISUAL_DATA — вільні текстові підписи діаграми таблиця text_labels: текст, межі, шрифт, колір, порядок
SectorPersistent.visualAttributes — обведення, шрифт і колір стрілки іменовані стовпчики в sectors

BIN_VERSION піднято до 3. Версії 1 і 2 читаються як і раніше, тож наявні файли відкриваються; при першому збереженні дані переїжджають самі.

Перевірено трьома тестами:

Два зауваження.

  1. Поля @Binary формально лишились у класах — SectorPersistent позначене @Deprecated і очищується при збереженні, а F_VISUAL_DATA у версії 3 містить лише номер версії. Прибирати стовпчики зі схеми варто окремо, коли не лишиться файлів старих версій.
  2. Обгорнуті типи (java.lang.Integer, java.lang.Double), а не примітиви: у наявних файлах нових стовпчиків немає, читання віддає null, і примітивний сеттер на такому падає. Це виявилось одразу, тестом.

Знахідка: перезбереження кореневої діаграми файлу версії 1 псує стрілки

Під час роботи над етапом 2 виявилося, що повний saveToFunction на кореневій діаграмі файлу версії 1 руйнує розкладку: граничні стрілки злипаються в одну лінію, підписи накладаються.

Це не наслідок цих змін. Перевірено прямо: та сама поломка відтворюється на коді до них (git stash лише SectorRefactor.java, той самий прогін, той самий результат). Тобто вада в міграції секторів версія 1 → версія 2 і існує давно.

Через це перенесення підписів зроблено окремою операцією SectorRefactor.saveTextLabels(Function), яка не чіпає геометрію.

Що варто перевірити далі: відтворення йшло через шлях друку (PIDEF0painter.createMovingArea), а не через інтерактивний редактор. Можливо, в GUI діаграма готується повніше й вади немає. Це вимагає прогону в застосунку.

Знахідка: відмальовування діаграм було недетермінованим ✅ виправлено

Одна з діаграм у dest/doc/ru/Model example.rsf між запусками JVM давала різну картинку. Причина — PaintSector.toArray: він повертав множину в порядку обходу HashSet, тобто в порядку, що залежить від адрес об’єктів. А саме за цим порядком обидва місця, куди йде результат, вирішують, котрий із з’єднаних секторів залишить собі підпис.

Тепер масив упорядкований за ключем сектора. Наслідок: еталон діаграм порівнюється точно, без допуску.


1. Мета

Замінити .rsf (ZIP + позиційний XML-дамп таблиць) на дерево текстових файлів, яке:

Не є метою: заміна рушія БД, відмова від H2, зміна Engine API, переписування GUI.


2. Архітектурне рішення: міняємо лише серіалізатор

 файли на диску  ←→  [СЕРІАЛІЗАТОР]  ←→  H2 in-memory  ←→  Engine API  ←→  ~175k рядків GUI
        ▲                   ▲
    міняємо           переписуємо              ← не чіпаємо

H2 in-memory виконує роль робочого індексу під час сеансу: на ньому тримаються запити findElements, формули, залежності. Спроба його прибрати означає переписування IEngineImpl і всього, що вище.

Межа вже проведена в коді — це FileIEngineImpl. Мережевий режим (server, PostgreSQL) при такому зрізі не зачіпається взагалі.


3. Рішення щодо синтаксису: YAML 1.2

Обрано YAML. Початково розглядався TOML; аргумент на користь YAML прийнято й посилено.

Чому YAML, а не TOML

  1. Дані Ramus глибоко вкладені та гетерогенні. Модель — EAV з розширюваними плагінами типами атрибутів (Core.Text, Core.OtherElement, Core.ElementList, Core.Variant, IDEF0.Sector…). Значення атрибута — це відкрита мапа довільної форми. TOML на такому вироджується в ліс [[a.b.c]]-заголовків.

  2. Norway problem усувається двічі. По-перше, лапками. По-друге і головніше: у YAML 1.2 його просто немає — core schema трактує no, yes, on, off як рядки. Проблема належить YAML 1.1. Тому обов’язково snakeyaml-engine 2.x (YAML 1.2), а не SnakeYAML 1.x.

  3. Зрілий емітер під Java. Нам потрібен не стільки парсер, скільки канонічний писар з контролем відступів, стилю скалярів і перенесення рядків. snakeyaml-engine дає DumpSettings з усім необхідним. Бібліотеки TOML під Java (tomlj, night-config) сильні на читанні й слабкі на записі — довелось би писати власний емітер.

  4. Менше костилів. Коментарі, багаторядкові рядки, списки, вкладені мапи — усе рідне.

Обов’язкові запобіжники

Ці пункти не опційні — без них YAML справді стріляє в ногу.

# Правило Навіщо
1 snakeyaml-engine 2.x, YAML 1.2 усуває Norway problem на рівні специфікації
2 Читання лише безпечним Load (без інстанціювання довільних класів) SnakeYAML-подібні RCE через !!javax… — відомий клас вразливостей
3 На записі всі рядкові скаляри в лапках захист від майбутніх сюрпризів типізації
4 setSplitLines(false) інакше емітер сам переносить довгі рядки → фантомні diff
5 Anchors/aliases не емітуються; на читанні — відхиляти інакше diff бреше, а агент не бачить справжніх даних
6 Глибина вкладеності ≤ 3 значущі відступи ламаються тим важче, чим глибше
7 Багаторядковий текст — лише літеральний блок \| один стиль замість п’яти
8 Ключі мап у детермінованому порядку LinkedHashMap, заповнена в стабільному порядку
9 Locale.ROOT + канонічний формат double інакше кома/крапка залежно від локалі
10 Дати — ISO-8601 з UTC-зсувом, з секундами див. дефекти 7.1 і 7.2 у START_REVIEW

4. Три шари даних

Це ключове рішення формату.

Шар Де У git Хто редагує
Семантика — що з чим пов’язане, ролі ICOM, назви model/…/<fn>.yaml так людина, агент
Наміри розкладки — що користувач закріпив вручну model/…/<fn>.layout.yaml так, але розріджено людина мишею, зрідка агент
Обчислена геометрія — усі координати й траси ніде ні рушій розкладки

Головний принцип: розкладка — це набір перевизначень, а не істина. Усе, що користувач не закріпив явно, рахує рушій при завантаженні.

Чому саме так, а не «просто окремий файл координат»

Три знахідки з коду (див. START_REVIEW, розділ 5) роблять повний дамп координат поганим вибором:

  1. Бік прив’язки стрілки — це не геометрія. borderType (RIGHT/BOTTOM/LEFT/TOP) в IDEF0 однозначно виводиться з ролі ICOM. Зберігаємо role, бік рахуємо. Одне поле переїжджає з геометрії в семантику безкоштовно.

  2. Точки не мають власних координат. SectorPointPersistent посилається на спільні Ordinate — координатні напрямні, які ділять кілька точок і які рухаються разом. Дамп сирих x/y втратить цей зв’язок.

  3. Рушій розкладки частково вже написаний. pb/dmaster/AbstractClassicTemplate будує топологію стрілок за списком функцій. Треба перетворити разову генерацію шаблону на перерахунок при відображенні.

Чому IDEF0 добре піддається автотрасуванню

Нотація незвично жорстка, і це нам на руку:

Тобто для IDEF0 достатньо порядкового номера функції (який і так семантичний — це нумерація A11, A12…), щоб отримати придатну розкладку без жодних координат.

Застереження щодо DFD. Там немає ані конвенції боків, ані сходинки. Контракт рушія мусить допускати engine: 'none' — усе закріплено вручну, поведінка як зараз. Інакше DFD-діаграми поламаються.

Контракт рушія розкладки

public interface LayoutEngine {
    String  id();       // 'idef0-classic'
    int     version();  // 1
    Layout  compute(DiagramSemantics semantics, LayoutPins pins);
}

Рушій зобов’язаний бути детермінованим, а його id@version — записаним у файл. Без цього оновлення Ramus мовчки перетасує розкладку всіх наявних діаграм. Нова версія алгоритму = новий version; старі файли й далі рендеряться старою.


5. Цільова розкладка файлів

project.yaml                    метадані, версія схеми, перелік плагінів
attributes.yaml                 визначення атрибутів (спільні)
qualifiers/
  <slug>--<id>.yaml             класифікатор + його атрибути
model/
  <model-slug>--<id>/
    model.yaml                  налаштування моделі, коренева функція
    <fn-slug>--<id>.yaml        семантика однієї декомпозиції
    <fn-slug>--<id>.layout.yaml розкладка тієї ж декомпозиції (розріджена)
attachments/
  <id>/…                        бінарні вкладення як є (звіти, файли)
.local/                         ⟵ у .gitignore
  ui-state.yaml                 колишній user/gui/*
  layouts/

Один файл на декомпозицію — головне рішення для merge і для агента. Семантика й розкладка розділені, тому змістова правка ніколи не конфліктує з посуванням блоків.


6. Ескіз формату

Семантика

# model/enterprise-activity--k3d9/produce-goods--a3f2c1.yaml
schema: 3
model: "enterprise-activity--k3d9"

function:
  id: "a3f2c1"
  name: "Виготовлення продукції"
  parent: "root"
  number: "A1"
  type: "process"

children:
  - id: "d41a09"
    name: "Вхідний контроль"
    order: 1 # ⟵ нумерація IDEF0; замінює координати
  - id: "e77b13"
    name: "Складання"
    order: 2

arrows:
  - id: "b7e4d2"
    name: "Сировина"
    from: { fn: "parent", role: "input" } # ⟵ роль ICOM, не бік
    to: { fn: "d41a09", role: "input" }
    tunnel: false

  - id: "c8f5e3"
    name: "Перевірені матеріали"
    from: { fn: "d41a09", role: "output" }
    to: { fn: "e77b13", role: "input" }

  - id: "f19a44"
    name: "Технологічна карта"
    from: { fn: "parent", role: "control" }
    to: { fn: "e77b13", role: "control" }

Жодної координати. Цього достатньо, щоб намалювати коректну діаграму IDEF0 — і рівно це редагує агент.

Розкладка

# model/enterprise-activity--k3d9/produce-goods--a3f2c1.layout.yaml
schema: 3
engine: "idef0-classic@1" # ⟵ обов'язково з версією

# Спільні напрямні: точки на одній напрямній рухаються разом.
# Відповідає Ordinate у моделі.
guides:
  "gx-1": { axis: "x", at: 220.0 }

# Лише те, що користувач посунув вручну. Решту рахує рушій.
pins:
  "d41a09": { x: 40.0, y: 40.0 }

routes:
  "c8f5e3": { via: ["gx-1"] }
  "f19a44": { via: ["gx-1"] } # ділять напрямну → лишаються вирівняними

texts:
  - text: "Примітка до діаграми"
    at: { x: 20.0, y: 240.0 }

Порівняйте з поточним:

<row><f id="0">768</f><f id="1">17</f><f id="2">120.0</f><f id="3">80.0</f></row>

Координати, які лишаються, тепер нечисленні й змістовні — кожна означає «користувач свідомо це посунув». Це редагується незрівнянно легше за повний дамп.

Міграція наявних проєктів

Конвертація старого .rsf дасть layout.yaml, де закріплено все — бо в старому файлі кожна координата задана вручну і ми не можемо знати, що з цього було свідомим рішенням. Це чесна деградація до повного розділення, без втрати даних.

Далі — команда «нормалізувати розкладку»: прогнати рушій, зняти ті закріплення, що збігаються з обчисленими в межах допуску. Діаграма не зміниться, файл схудне.


7. Етапи

Етапи 0–2 самодостатні: навіть якщо зупинитись там, формат уже стане diff-абельним, а модель — повністю структурованою.

Етап 0 — Тестова інфраструктура ✅

Зараз тестів нуль. Без них переписування серіалізатора — політ наосліп.

DoD: ./gradlew :core:test запускається; round-trip тест існує й падає з осмисленим повідомленням про недетермінізм.


Етап 1 — Детермінізм у наявному форматі ✅

Найдешевший крок з найбільшою віддачею. Формат не змінюється — змінюється лише те, що git бачить осмислений diff.

Файл Зміна
core/.../TableToXML.java:132 прибрати атрибут generate-time
core/.../TableToXML.java:143 додати ORDER BY за первинним ключем
core/.../FileIEngineImpl.java:546-548 прибрати CurrentTimeMillis, CurrentDateTime
core/.../FileIEngineImpl.createMetadata() замінити Properties.storeToXML на впорядкований запис
core/.../FileIEngineImpl.java:503 extractedFiles: HashtableTreeMap
core/.../FileIEngineImpl (ZIP) фіксований час ZIP-записів (наприклад, epoch 0)
core/.../XMLToTable.java:40 DATE_FORMAT → ISO-8601, UTC, з секундами (дефекти 7.1, 7.2)

Зворотна сумісність: читання старих дат лишити — приймати обидва формати, писати лише новий.

DoD: round-trip тест з етапу 0 зелений. Повторне збереження без змін дає байт-в-байт ідентичний файл.


Етап 2 — Прибрати блоб і журнал ✅

Блоб. SectorRefactor.java:758 для BIN_VERSION = 2 пише в блоб лише вільні текстові підписи (font, color, bounds, text).

Журнал і гілки (рішення 8.1). Історію віддаємо git.

DoD: у моделі не лишається полів @Binary (крім вкладень користувача); у новому форматі немає таблиць історії; undo/redo працює як раніше.


Етап 3 — Стабільні ідентифікатори: slug + id ✅

Рішення 8.2. Числовий ID лишається як внутрішній ключ БД; поруч додається стабільний зовнішній.

DoD: конвертація .rsf → новий формат двічі поспіль дає ідентичний результат; перейменування функції не ламає жодного посилання.


Етап 4 — Розділення семантики й розкладки ⛔ лишилось

Найризикованіший етап — тут змінюється не лише формат, а й модель.

  1. Роль ICOM стає явною. Додати role (input/control/output/mechanism) до SectorBorderPersistent; borderType рахувати з неї. Міграція: вивести роль зі старого borderType (MovingPanel.java:24-30) — відображення однозначне.
  2. Контракт LayoutEngine за розділом 4.
  3. idef0-classic@1 — перша реалізація, на базі логіки pb/dmaster/AbstractClassicTemplate, перетвореної з разової генерації на перерахунок.
  4. none — рушій-заглушка для DFD і для повністю закріплених діаграм.
  5. Модель pin/guide/route поверх наявних Ordinate.
  6. Команда «нормалізувати розкладку» (зняти надлишкові закріплення).

DoD: діаграму IDEF0 можна намалювати з самої лише семантики; відкриття старого проєкту дає піксельно ту саму картинку.


Етап 5 — Схемний шар ✅

Generic-серіалізатор поверх PersistentWrapper замість ручного опису кожного типу.

DoD: додавання нового @Table-персистента не потребує правок серіалізатора.


Етап 6 — YAML-читач і писар ✅

Зроблено, але не так, як планувалося. Задум був «YamlStorageWriter поруч із TableToXML», тобто заміна серіалізатора таблиць. Натомість писар і читач працюють з моделлю — класифікаторами, елементами, значеннями, — а не з рядками таблиць, і живуть на рівні IEngine.

Різниця істотна: дамп таблиць у YAML лишався б дампом таблиць, лише з іншими дужками. Обхід через IEngine дає файл, у якому видно модель, і при цьому не менш точний — цикл проєкт → .rsf → проєкт дослівний.


Етап 7 — Розділення шарів і .rsf лише на читання ✅

DoD виконано: resavingProjectChangesNothing доводить, що відкриття й збереження без змін не чіпає жодного файлу.


8. Ухвалені рішення

8.1 Історію та гілки віддаємо git ✅ (з поправкою)

branches і *_history не серіалізуються у новому форматі. Журнал лишається в пам’яті лише для undo/redo в межах сеансу.

Поправка після рев’ю коду. Формулювання «прибрати журнал» було надто широким і небезпечним:

Тому: правило діє для нового формату, а не як зміна .rsf, і лише коли проєкт використовує саму лише кореневу гілку. Якщо гілок більше, конвертер має або зберегти їх, або явно запитати про зведення до однієї. Обидва зразки в dest/doc гілок не використовують — таблиці порожні.

Ціна: історія в наявних .rsf при конвертації втрачається. Потрібне явне попередження в діалозі конвертації.

8.2 Іменування: <slug>--<id>

Slug для людини й агента, id для машини. Посилання всередині файлів — лише id, тому перейменування не ламає нічого, а git mv зберігає історію файлу.

8.3 Координати: розріджені перевизначення поверх обчисленої розкладки ✅

Див. розділ 4. Коротко:

Це задовольняє обидві вимоги з питання: рушій автотрасування підключається за побудовою, а координати, що лишаються, малочисленні й змістовні, тому редагуються легше за повний дамп.

8.4 Зворотна сумісність: лише читання ✅

.rsf — формат імпорту. Запис не підтримуємо.

Наслідок: економія на етапі 7 і, важливіше, свобода в рішенні 8.1 — не треба вміти покласти назад те, що ми свідомо викидаємо (історію).


9. Ризики

Ризик Пом’якшення
Тестові файли з 2009 не покривають поточний шлях коду етап 0: зберегти свіжий еталон версією 2.0.2
Недетермінований рушій розкладки перетасує наявні діаграми engine: 'id@version' у файлі; тест «стара версія рушія дає стару картинку»
DFD не має конвенції боків → автотрасування не працює engine: 'none', повне закріплення; поведінка як зараз
Втрата спільності Ordinate при дампі координат модель guides, а не сирі x/y (розділ 6)
Втрата історії при конвертації явне попередження; рекомендувати зберегти оригінальний .rsf
Зміна схеми зачіпає мережевий режим stable_id і role додаються як нові колонки; наявні не чіпаються
Значущі відступи YAML ламаються агентом глибина ≤ 3, запобіжники розділу 3, схемна валідація при читанні
Locale-залежність чисел і дат Locale.ROOT, ISO-8601 UTC; тест із підміною локалі та TZ
Мовчазне поглинання винятків при читанні дефект 7.3: loadTable має падати гучно, а не printStackTrace

10. Порядок виконання

0 ──▶ 1 ──▶ 2 ──▶ 3 ──▶ 4 ──▶ 5 ──▶ 6 ──▶ 7
      └─ самодостатня зупинка ─┘