feat(skd-decompile): авто-генерация skd-styles.json для custom appearance

Категория C — закрыта для однородных шаблонов с custom appearance.

Refactor fingerprint → preset shape (11 полей: font/fontSize/bold/italic/
hAlign/vAlign/wrap/bgColor/textColor/borderColor/borders). vAlign теперь
учитывается в matching (раньше игнорировался).

Алгоритм:
1. При -OutputPath загружается existing skd-styles.json рядом (если есть);
   user presets накладываются на built-in по той же логике что и compile.
2. Каждая ячейка → Extract-CellPreset → Match-PresetByShape против
   effectivePresets (built-in + user).
3. Если не match — Allocate-CustomStyle: новый customN, регистрируется
   в effectivePresets и accumulator.
4. По окончании Save-UserStyles пишет skd-styles.json рядом с outputPath
   (preserved existing + новые customN).
5. Compile подхватит файл по своим search-путям (cwd/dirname/scan-up).

В SKILL.md не добавляем (custom стили — для round-trip, не для написания
модель с нуля; built-in `data/header/subheader/total/none` остаются
основным интерфейсом для модели).

- runner.mjs: новый preRun step `writeFile` для подготовки fixture-файлов
  в workDir (нужен для теста с предзаписанным skd-styles.json).
- Новый тест template-custom-style: preRun пишет myHeader preset,
  скомпилирует темплейт, decompile reverse'ит → переиспользует имя
  myHeader (не создаёт customN).
- v0.14 → v0.15.

Метрики:
- ERP-сэмпл 30: 24 → 0 sentinel'ов, clean 26 → 30/30
- Целевой корпус 40 отчётов: 39 → 25 sentinel'ов (часть закрыта), clean
  19 → 20/40. Остаточные — шаблоны с разными стилями в разных ячейках
  одного шаблона (нужно per-cell style override — отдельная задача).
This commit is contained in:
Nick Shirokov
2026-05-21 19:32:17 +03:00
parent 4bd8f27dec
commit 3119700c71
6 changed files with 400 additions and 104 deletions
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
<dataSet xsi:type="DataSetQuery">
<name>Тест</name>
<field xsi:type="DataSetFieldField">
<dataPath>Поле</dataPath>
<field>Поле</field>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>0</v8:Length>
<v8:AllowedLength>Variable</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
</field>
<dataSource>ИсточникДанных1</dataSource>
<query>ВЫБРАТЬ * ИЗ Справочник.Сотрудники</query>
</dataSet>
<template>
<name>Заголовок</name>
<template xmlns:dcsat="http://v8.1c.ru/8.1/data-composition-system/area-template" xsi:type="dcsat:AreaTemplate">
<dcsat:item xsi:type="dcsat:TableRow">
<dcsat:tableCell>
<dcsat:item xsi:type="dcsat:Field">
<dcsat:value xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>A</v8:content>
</v8:item>
</dcsat:value>
</dcsat:item>
<dcsat:appearance>
<dcscor:item>
<dcscor:parameter>ЦветФона</dcscor:parameter>
<dcscor:value xmlns:d8p1="http://v8.1c.ru/8.1/data/ui/style" xsi:type="v8ui:Color">d8p1:ReportHeaderBackColor</dcscor:value>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>ЦветГраницы</dcscor:parameter>
<dcscor:value xmlns:d8p1="http://v8.1c.ru/8.1/data/ui/style" xsi:type="v8ui:Color">d8p1:ReportLineColor</dcscor:value>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>СтильГраницы</dcscor:parameter>
<dcscor:value xsi:type="v8ui:Line" width="0" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">None</v8ui:style>
</dcscor:value>
<dcscor:item>
<dcscor:parameter>СтильГраницы.Слева</dcscor:parameter>
<dcscor:value xsi:type="v8ui:Line" width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>
</dcscor:value>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>СтильГраницы.Сверху</dcscor:parameter>
<dcscor:value xsi:type="v8ui:Line" width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>
</dcscor:value>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>СтильГраницы.Справа</dcscor:parameter>
<dcscor:value xsi:type="v8ui:Line" width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>
</dcscor:value>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>СтильГраницы.Снизу</dcscor:parameter>
<dcscor:value xsi:type="v8ui:Line" width="1" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>
</dcscor:value>
</dcscor:item>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>Шрифт</dcscor:parameter>
<dcscor:value xsi:type="v8ui:Font" faceName="Calibri" height="11" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>ГоризонтальноеПоложение</dcscor:parameter>
<dcscor:value xsi:type="v8ui:HorizontalAlign">Center</dcscor:value>
</dcscor:item>
<dcscor:item>
<dcscor:parameter>ВертикальноеПоложение</dcscor:parameter>
<dcscor:value xsi:type="v8ui:VerticalAlign">Center</dcscor:value>
</dcscor:item>
</dcsat:appearance>
</dcsat:tableCell>
</dcsat:item>
</template>
</template>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основной</v8:content>
</v8:item>
</dcsset:presentation>
<dcsset:settings xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows">
<dcsset:selection>
</dcsset:selection>
<dcsset:item xsi:type="dcsset:StructureItemGroup">
<dcsset:order>
<dcsset:item xsi:type="dcsset:OrderItemAuto"/>
</dcsset:order>
<dcsset:selection>
<dcsset:item xsi:type="dcsset:SelectedItemAuto"/>
</dcsset:selection>
</dcsset:item>
</dcsset:settings>
</settingsVariant>
</DataCompositionSchema>
@@ -0,0 +1,22 @@
{
"dataSets": [
{
"name": "Тест",
"query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники",
"fields": [
"Поле: string"
]
}
],
"templates": [
{
"name": "Заголовок",
"style": "myHeader",
"rows": [
[
"A"
]
]
}
]
}
@@ -0,0 +1,15 @@
{
"myHeader": {
"font": "Calibri",
"fontSize": 11,
"bold": false,
"italic": false,
"hAlign": "Center",
"vAlign": "Center",
"wrap": false,
"bgColor": "style:ReportHeaderBackColor",
"textColor": null,
"borderColor": "style:ReportLineColor",
"borders": true
}
}
@@ -0,0 +1,42 @@
{
"name": "Шаблон с custom стилем (Calibri 11) — переиспользование user preset из skd-styles.json",
"preRun": [
{
"writeFile": {
"path": "skd-styles.json",
"content": {
"myHeader": {
"font": "Calibri",
"fontSize": 11,
"bold": false,
"italic": false,
"hAlign": "Center",
"vAlign": "Center",
"wrap": false,
"bgColor": "style:ReportHeaderBackColor",
"textColor": null,
"borderColor": "style:ReportLineColor",
"borders": true
}
}
}
},
{
"script": "skd-compile/scripts/skd-compile",
"input": {
"dataSets": [{
"name": "Тест",
"query": "ВЫБРАТЬ * ИЗ Справочник.Сотрудники",
"fields": ["Поле: string"]
}],
"templates": [
{ "name": "Заголовок", "style": "myHeader", "rows": [["A"]] }
]
},
"args": { "-DefinitionFile": "{inputFile}", "-OutputPath": "Template.xml" },
"cwd": "{workDir}"
}
],
"params": { "templatePath": "Template.xml" },
"outputPath": "decompiled.json"
}
+9
View File
@@ -517,6 +517,15 @@ async function runCaseAsync(testCase, opts) {
// Pre-run steps
if (caseData.preRun) {
for (const step of caseData.preRun) {
// writeFile step — записать произвольный файл в workDir перед запуском скрипта
if (step.writeFile) {
const wfPath = join(workDir, step.writeFile.path);
const wfContent = typeof step.writeFile.content === 'string'
? step.writeFile.content
: JSON.stringify(step.writeFile.content, null, 2);
writeFileSync(wfPath, wfContent, 'utf8');
continue;
}
const preScript = resolveScript(step.script, opts.runtime);
const preArgs = [];
for (const [flag, value] of Object.entries(step.args || {})) {