План міграції формату проєкту на текстовий, придатний для агентного редагування
Створено: 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(), тож плагіни бачать уже цілісну модель
і нічого не добудовують.
Наслідки для формату виявилися більшими, ніж очікувалося:
- Числові ключі відновлюються точно, тому реєстр відповідностей зник: ідентифікатор — оборотна функція ключа, і цього досить.
- Системні класифікатори більше не виняток.
QualifiersQualifier,AttributesQualifier, базові функції, звіти — усе відновлюється дослівно, і винятки в тестах прибрано. - Системні атрибути не потребують позначки
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>
Вони потрібні для пакетного перетворення; сам застосунок працює з каталогом проєкту напряму.
Дефекти, знайдені й виправлені попутно
- Застарілі дати не читалися взагалі на сучасній JDK. Формат брався з
DateFormat.getDateTimeInstance(SHORT, SHORT, ENGLISH), а він залежить від набору локальних даних: до переходу JDK на CLDR це булоM/d/yy h:mm a, після —M/d/yy, h:mm a. Старі файли переставали парситися, виняток ковтався, дати губилися мовчки. Тепер шаблони задані явно, з переліком застарілих. - Невстановлений параметр
PreparedStatement. При невдалому розборі дати колонка лишалася незаданою, і драйвер валив запис усієї таблиці. Тепер ставитьсяnullз повідомленням, що називає таблицю й значення. - Обірвані посилання в даних. У зразках трапляються посилання на видалені
елементи (
previousElementIdвказує в нікуди). Старий формат зберігав числовий ключ, і це ніяк не проявлялося. Новий не вигадує посилання: якщо сутності немає, записується «посилання відсутнє».
Уроки, які змінили конструкцію
- Рівень читання важливіший за все інше. Поки читач працював через
Engine, кожна вада мала вигляд окремої: то дублікат класифікатора, то зниклий елемент, то перейменований чужий атрибут. Причина була одна — плагіни добудовували модель у відповідь на виклики. Перехід наIEngineзняв усі ці випадки разом, а заразом і половину коду читача: зникли реєстр відповідностей, пошук за іменем, прапорець автодобудови й перелік «дзеркальних» класифікаторів. - Атрибути зручніше адресувати іменем, а не ключем.
F_BOUNDSу файлі зрозуміле без словника,vakjk8— ні. Ім’я унікальне майже завжди, а рідкісний збіг знімається суфіксом#<id>. - Властивості атрибутів — частина моделі. Значення з {@code elementId = -1}
тримають конфігурацію плагіна (для
Core.ElementList— пов’язані класифікатори). Без них значення елементів не відновлюються. - Імпорт має бути двопрохідним. Значення посилаються на елементи інших класифікаторів, тож спершу створюються всі сутності, потім властивості атрибутів, і лише тоді значення.
- Системні класифікатори містять реальні дані. Базові функції моделей
IDEF0, звіти й сектори живуть у класифікаторах із познакою «системний».
Пропустити їх означало б експортувати модель без кореневої функції. Коли
читання відбувається нижче плагінів, винятків не потрібно взагалі — навіть
QualifiersQualifierіAttributesQualifierвідновлюються дослівно. - Потоки — теж модель.
/properties/*тримає налаштування IDEF0,/elements/*— вкладення користувача. Без них модель не будується./user/*не експортується свідомо: це персональний стан інтерфейсу.
Етап 2: обидва блоби винесені
У моделі було два двійкових поля. Обидва замінені іменованими полями, зі збереженням читання старих файлів.
| Було | Стало |
|---|---|
F_VISUAL_DATA — вільні текстові підписи діаграми |
таблиця text_labels: текст, межі, шрифт, колір, порядок |
SectorPersistent.visualAttributes — обведення, шрифт і колір стрілки |
іменовані стовпчики в sectors |
BIN_VERSION піднято до 3. Версії 1 і 2 читаються як і раніше, тож наявні
файли відкриваються; при першому збереженні дані переїжджають самі.
Перевірено трьома тестами:
TextLabelStorageTest— модель створюється з нуля, підпис зберігається, файл перечитується; двійкове поле після цього ≤ 8 байт (лише номер версії), тексту в ньому немає;BlobMigrationTest— підписи зі зразків версії 1 переносяться без утрат і без зміни вигляду;SectorVisualMigrationTest— після перенесення вигляду стрілок жоден сектор на діаграмах не має двійкових даних, а діаграми виглядають так само.
Два зауваження.
- Поля
@Binaryформально лишились у класах —SectorPersistentпозначене@Deprecatedі очищується при збереженні, аF_VISUAL_DATAу версії 3 містить лише номер версії. Прибирати стовпчики зі схеми варто окремо, коли не лишиться файлів старих версій. - Обгорнуті типи (
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-дамп таблиць) на дерево текстових файлів,
яке:
- читається й редагується людиною та LLM-агентом без спецінструментів;
- дає осмислений
git diff— зміна однієї діаграми чіпає один файл; - мержиться без ручного розгрібання;
- не містить бінарних блобів у моделі;
- не змінюється, якщо модель не змінилась (детермінізм);
- відділяє зміст від відображення, щоб агент правив зв’язки, не торкаючись геометрії, а розкладку рахував підключений рушій.
Не є метою: заміна рушія БД, відмова від 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
-
Дані Ramus глибоко вкладені та гетерогенні. Модель — EAV з розширюваними плагінами типами атрибутів (
Core.Text,Core.OtherElement,Core.ElementList,Core.Variant,IDEF0.Sector…). Значення атрибута — це відкрита мапа довільної форми. TOML на такому вироджується в ліс[[a.b.c]]-заголовків. -
Norway problem усувається двічі. По-перше, лапками. По-друге і головніше: у YAML 1.2 його просто немає — core schema трактує
no,yes,on,offяк рядки. Проблема належить YAML 1.1. Тому обов’язково snakeyaml-engine 2.x (YAML 1.2), а не SnakeYAML 1.x. -
Зрілий емітер під Java. Нам потрібен не стільки парсер, скільки канонічний писар з контролем відступів, стилю скалярів і перенесення рядків.
snakeyaml-engineдаєDumpSettingsз усім необхідним. Бібліотеки TOML під Java (tomlj,night-config) сильні на читанні й слабкі на записі — довелось би писати власний емітер. -
Менше костилів. Коментарі, багаторядкові рядки, списки, вкладені мапи — усе рідне.
Обов’язкові запобіжники
Ці пункти не опційні — без них 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) роблять повний дамп координат поганим вибором:
-
Бік прив’язки стрілки — це не геометрія.
borderType(RIGHT/BOTTOM/LEFT/TOP) в IDEF0 однозначно виводиться з ролі ICOM. Зберігаємоrole, бік рахуємо. Одне поле переїжджає з геометрії в семантику безкоштовно. -
Точки не мають власних координат.
SectorPointPersistentпосилається на спільніOrdinate— координатні напрямні, які ділять кілька точок і які рухаються разом. Дамп сирихx/yвтратить цей зв’язок. -
Рушій розкладки частково вже написаний.
pb/dmaster/AbstractClassicTemplateбудує топологію стрілок за списком функцій. Треба перетворити разову генерацію шаблону на перерахунок при відображенні.
Чому IDEF0 добре піддається автотрасуванню
Нотація незвично жорстка, і це нам на руку:
- функції в декомпозиції розташовані «сходинкою» з лівого верхнього кута;
- за стандартом їх 3–6 на діаграму;
- бік прив’язки заданий роллю ICOM, а не вибором користувача;
- траси ортогональні.
Тобто для 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 — Тестова інфраструктура ✅
Зараз тестів нуль. Без них переписування серіалізатора — політ наосліп.
- створити
core/src/test/java/; - лишити
junit 4.11(уже оголошений) або перейти на JUnit 5; - тест
RsfRoundTripTest: для кожного файлу зdest/doc/load → save → load → save, порівняти другий і третій артефакти; - зберегти свіжий еталонний
.rsfзастосунком версії 2.0.2 (наявні файли — з 2009 року,BIN_VERSION=1, покривають застарілий шлях); - зафіксувати baseline: тест на цьому етапі червоний — і це очікувано.
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: Hashtable → TreeMap |
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).
- створити
TextLabelPersistent(@Table(name = "text_labels")); - перевести читання/запис підписів на нього;
- підняти
BIN_VERSIONдо 3; лишити код читання v1/v2; SectorPersistent.visualAttributes— з’ясувати, чи взагалі використовується (у коді знайдено лише копіювання, жодного розбору) і за можливості видалити.
Журнал і гілки (рішення 8.1). Історію віддаємо git.
branches,attributes_history,qualifiers_history,*_data_metadataбільше не серіалізуються у файловому режимі;- журнал (
common/.../journal/) лишається в пам’яті, в межах сеансу, обслуговує undo/redo; - мережевий режим не чіпаємо — там історія лишається в БД як є;
- при конвертації старого
.rsfісторію відкидаємо; попередити користувача в діалозі конвертації.
DoD: у моделі не лишається полів @Binary (крім вкладень користувача);
у новому форматі немає таблиць історії; undo/redo працює як раніше.
Етап 3 — Стабільні ідентифікатори: slug + id ✅
Рішення 8.2. Числовий ID лишається як внутрішній ключ БД; поруч додається стабільний зовнішній.
- нова колонка
stable_idвelements,qualifiers,attributes; - генерація при створенні (
IEngineImpl.java:264та сусідні); - формат: короткий ULID/base32, 6–8 символів;
- ім’я файлу —
<slug>--<id>.yaml: slug для людини, id для машини; перейменування сутності змінює slug,git mvзберігає історію, а всі посилання йдуть через незмінний id; - у посиланнях усередині файлів — тільки id;
- міграція: старим файлам роздати id детерміновано з числового, щоб конвертація одного файлу двічі давала однаковий результат.
DoD: конвертація .rsf → новий формат двічі поспіль дає ідентичний результат;
перейменування функції не ламає жодного посилання.
Етап 4 — Розділення семантики й розкладки ⛔ лишилось
Найризикованіший етап — тут змінюється не лише формат, а й модель.
- Роль ICOM стає явною. Додати
role(input/control/output/mechanism) доSectorBorderPersistent;borderTypeрахувати з неї. Міграція: вивести роль зі старогоborderType(MovingPanel.java:24-30) — відображення однозначне. - Контракт
LayoutEngineза розділом 4. idef0-classic@1— перша реалізація, на базі логікиpb/dmaster/AbstractClassicTemplate, перетвореної з разової генерації на перерахунок.none— рушій-заглушка для DFD і для повністю закріплених діаграм.- Модель pin/guide/route поверх наявних
Ordinate. - Команда «нормалізувати розкладку» (зняти надлишкові закріплення).
DoD: діаграму IDEF0 можна намалювати з самої лише семантики; відкриття старого проєкту дає піксельно ту саму картинку.
Етап 5 — Схемний шар ✅
Generic-серіалізатор поверх PersistentWrapper замість ручного опису кожного типу.
PersistentWrapper.getFields()уже віддає відсортований список (Arrays.sortвinitFields()) — детермінізм безкоштовно;getAnnotationType(field)дає тип → мапиться на YAML-скаляр;- плагіни оголошують персистенти через
AttributePlugin.getAttributePersistents(), реєстр збирається автоматично; - правило збереження невідомих полів: поле, якого поточна версія не знає, має пережити round-trip, а не зникнути.
DoD: додавання нового @Table-персистента не потребує правок серіалізатора.
Етап 6 — YAML-читач і писар ✅
Зроблено, але не так, як планувалося. Задум був «YamlStorageWriter поруч із
TableToXML», тобто заміна серіалізатора таблиць. Натомість писар і читач
працюють з моделлю — класифікаторами, елементами, значеннями, — а не з
рядками таблиць, і живуть на рівні IEngine.
Різниця істотна: дамп таблиць у YAML лишався б дампом таблиць, лише з іншими
дужками. Обхід через IEngine дає файл, у якому видно модель, і при цьому
не менш точний — цикл проєкт → .rsf → проєкт дослівний.
- залежність
org.snakeyaml:snakeyaml-engine:2.7уcore/build.gradle; ProjectWriter/ProjectReader, обидва наIEngine;DumpSettingsза всіма 10 правилами розділу 3;FileIEngineImpl.openProject()/saveProject();MemoryDatabaseобирає реалізацію за наявністюproject.ramus;- версія схеми,
minimum-versionі перелік критичних плагінів уproject.ramus; ProjectRoundTripTest— цикл через.rsfі повторне збереження;diagramsLookIdenticalAfterRoundTrip— діаграми з оригіналу й із зібраного назад архіву збігаються піксельно.
Етап 7 — Розділення шарів і .rsf лише на читання ✅
/user/*→.local/, а.gitignoreпроєкту його виключає;attachments/— бінарні вкладення як є, без hex-кодування;.rsfлише на читання: відкритий архів не перезаписується, перше збереження веде до діалогу з іменем нового проєкту;- CLI-конвертери
rsfToYaml/yamlToRsfдля пакетного перетворення; - тип файлу
application/x-ramus-projectзареєстровано в інсталяторі; точкою входу єproject.ramus, бо каталог із програмою не пов’яжеш.
DoD виконано: resavingProjectChangesNothing доводить, що відкриття й
збереження без змін не чіпає жодного файлу.
8. Ухвалені рішення
8.1 Історію та гілки віддаємо git ✅ (з поправкою)
branches і *_history не серіалізуються у новому форматі. Журнал
лишається в пам’яті лише для undo/redo в межах сеансу.
Поправка після рев’ю коду. Формулювання «прибрати журнал» було надто широким і небезпечним:
attributes_data_metadata,formulas_data_metadata,formula_dependences_data_metadata— не історія. Це індекс, який визначає, яке саме значення атрибута є поточним:IEngineImpl.java:968вибираєMAX(branch_id)з нього і лише за відсутності рядка відкочується доvalue_branch_id = 0. Просто перестати їх записувати — це не втрата історії, а тиха підміна значень.- Гілки досяжні і у файловому режимі:
local-clientпідключаєQualifierPluginSuit, аQualifierPlugin.java:98реєструєBranchView. Тобто реальний.rsfможе містити кілька гілок із живими даними.
Тому: правило діє для нового формату, а не як зміна .rsf, і лише коли
проєкт використовує саму лише кореневу гілку. Якщо гілок більше, конвертер
має або зберегти їх, або явно запитати про зведення до однієї.
Обидва зразки в dest/doc гілок не використовують — таблиці порожні.
Ціна: історія в наявних .rsf при конвертації втрачається. Потрібне
явне попередження в діалозі конвертації.
8.2 Іменування: <slug>--<id> ✅
Slug для людини й агента, id для машини. Посилання всередині файлів — лише id,
тому перейменування не ламає нічого, а git mv зберігає історію файлу.
8.3 Координати: розріджені перевизначення поверх обчисленої розкладки ✅
Див. розділ 4. Коротко:
- семантика (зв’язки + ролі ICOM) — єдине джерело істини;
- розкладка — окремий файл, що містить лише свідомі рішення користувача;
- решту рахує підключений рушій, названий і версійований у файлі;
engine: 'none'для DFD і повністю ручних діаграм.
Це задовольняє обидві вимоги з питання: рушій автотрасування підключається за побудовою, а координати, що лишаються, малочисленні й змістовні, тому редагуються легше за повний дамп.
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
└─ самодостатня зупинка ─┘
- Етап 0 — блокер для всіх наступних.
- Етапи 1 і 2 незалежні між собою, можна робити паралельно.
- Етап 3 має завершитись до 6, щоб не переписувати формат посилань двічі.
- Етап 4 — найризикованіший; його можна відкласти після 6, зберігаючи координати повним дампом, і зробити розділення другим заходом. Але тоді формат посилань на геометрію доведеться міняти двічі.