feat(xdto-compile,xdto-decompile,xdto-validate): пакеты XDTO из XML-схемы

Три навыка для работы с пакетами XDTO. Формат описания — обычная XSD,
своего DSL нет: рутину снимает конвертер (локальные объявления префиксов
dNpM на каждой ссылке, инвертированная кратность lowerBound/upperBound,
фасеты атрибутами вместо дочерних элементов, обязательный порядок
элементов верхнего уровня). То, чего XSD выразить не может — nillable
у атрибута, qualified у свойства, «атрибут записан явно» — едет
атрибутами из пространства имён модели XDTO по правилу «то же имя,
что в Package.bin». Свойства объекта метаданных живут в xs:appinfo,
поэтому пара decompile → compile замыкается без потерь.

Инвариант bin → xsd → bin проверен побайтово на 760 пакетах выгрузок
Бухгалтерии и ERP 8.3.24 (харнесс debug/xdto/roundtrip-corpus.mjs).
Сборка из рукописной XSD проверена загрузкой в базу 8.3.24 — именно
она вскрыла обязательный порядок import→property→valueType→objectType,
невидимый для корпусной сверки: все выгрузки уже канонические.

xdto-validate ловит два класса тихих дефектов, которые платформа не
диагностирует: подмену неразрешённого чужого типа на xs:anyType при
импорте XML-схемы и nillable у свойства-атрибута, теряемый экспортом
схемы в Конфигураторе.

Тесты: 18 снэпшот-кейсов на синтетических схемах (типовые конфигурации
в репозиторий не тащим), паритет PS↔PY на общих эталонах. Раннер
получил caseFiles — копирование файлов кейса в workDir для навыков
с файловым, а не JSON входом.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-07-25 21:21:57 +03:00
co-authored by Claude Opus 5
parent d544071b1e
commit d05aef54b4
140 changed files with 9601 additions and 1 deletions
+18
View File
@@ -603,6 +603,7 @@ async function runCaseAsync(testCase, opts) {
const isExternal = typeof setupName === 'string' && setupName.startsWith('external:');
workspace = createWorkspace(fixturePath, isExternal);
workDir = workspace.path;
copyCaseFiles(caseData, workDir, skillCasesDir);
// Pre-run steps
if (caseData.preRun) {
@@ -742,6 +743,22 @@ async function runCaseAsync(testCase, opts) {
}
}
// Скопировать файлы из каталога кейса в workDir — для навыков, вход которых
// не JSON, а файл (например XSD для xdto-compile). Так эталонные входы остаются
// читаемыми файлами, а не строками внутри JSON кейса.
function copyCaseFiles(caseData, workDir, skillCasesDir) {
if (!caseData.caseFiles) return;
for (const rel of caseData.caseFiles) {
const src = join(skillCasesDir, rel);
if (!existsSync(src)) throw new Error(`caseFiles: файл не найден: ${src}`);
// Путь со слэшем сохраняет структуру каталогов (напр. дерево пакета),
// простое имя кладётся в корень workDir
const dst = rel.includes('/') ? join(workDir, rel) : join(workDir, basename(rel));
mkdirSync(dirname(dst), { recursive: true });
copyFileSync(src, dst);
}
}
function runCase(testCase, opts) {
const { skillConfig, caseData, snapshotDir } = testCase;
const t0 = performance.now();
@@ -769,6 +786,7 @@ function runCase(testCase, opts) {
const isExternal = typeof setupName === 'string' && setupName.startsWith('external:');
workspace = createWorkspace(fixturePath, isExternal);
workDir = workspace.path;
copyCaseFiles(caseData, workDir, skillCasesDir);
// 2. Pre-run steps (setup prerequisites like creating objects)
if (caseData.preRun) {