From 96890598f905394f8a6759f6bf3d6bcb1a65d2bb Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 8 Feb 2026 18:08:04 +0300 Subject: [PATCH] Add mxl-compile skill: DSL compiler for SpreadsheetDocument JSON DSL format compiles to valid 1C Template.xml with correct format palettes, merges, named areas, and rowStyle gap-filling. Tested on simple (4 areas) and invoice (7 areas) templates. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/mxl-compile/SKILL.md | 191 ++++++ .../mxl-compile/scripts/mxl-compile.ps1 | 587 ++++++++++++++++++ README.md | 4 +- 3 files changed, 781 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/mxl-compile/SKILL.md create mode 100644 .claude/skills/mxl-compile/scripts/mxl-compile.ps1 diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md new file mode 100644 index 00000000..6e98ac38 --- /dev/null +++ b/.claude/skills/mxl-compile/SKILL.md @@ -0,0 +1,191 @@ +--- +name: mxl-compile +description: Компиляция табличного документа (MXL) из JSON-определения +argument-hint: +allowed-tools: + - Bash + - Read + - Write + - Glob +--- + +# /mxl-compile — Компилятор макета из DSL + +Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace). + +## Использование + +``` +/mxl-compile +``` + +## Параметры + +| Параметр | Обязательный | Описание | +|------------|:------------:|------------------------------------| +| JsonPath | да | Путь к JSON-определению макета | +| OutputPath | да | Путь для генерации Template.xml | + +## Команда + +```powershell +powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.ps1 -JsonPath "<путь>.json" -OutputPath "<путь>/Template.xml" +``` + +## Рабочий процесс + +1. Claude пишет JSON-определение (Write tool) → файл `.json` +2. Claude вызывает `/mxl-compile` для генерации Template.xml +3. Claude вызывает `/mxl-validate` для проверки корректности +4. Claude вызывает `/mxl-info` для верификации структуры + +## JSON-схема + +### Пример + +```json +{ + "columns": 10, + "defaultWidth": 30, + "columnWidths": { "1": 15, "2-8": 40, "9-10": 50 }, + + "fonts": { + "default": { "face": "Arial", "size": 10 }, + "bold": { "face": "Arial", "size": 10, "bold": true }, + "header": { "face": "Arial", "size": 14, "bold": true } + }, + + "styles": { + "default": {}, + "header": { "font": "header", "align": "center" }, + "label": { "font": "bold" }, + "bordered": { "border": "all" }, + "bordered-right": { "border": "all", "align": "right" }, + "total-right": { "font": "bold", "border": "top", "align": "right" } + }, + + "areas": [ + { + "name": "Заголовок", + "rows": [ + { "height": 20, "cells": [ + { "col": 1, "span": 10, "style": "header", "param": "ТекстЗаголовка" } + ]} + ] + }, + { + "name": "ШапкаТаблицы", + "rows": [ + { "rowStyle": "bordered", "cells": [ + { "col": 1, "text": "№" }, + { "col": 2, "span": 6, "text": "Наименование" }, + { "col": 9, "text": "Кол-во" }, + { "col": 10, "text": "Сумма" } + ]} + ] + }, + { + "name": "Строка", + "rows": [ + { "rowStyle": "bordered", "cells": [ + { "col": 1, "param": "НомерСтроки" }, + { "col": 2, "span": 6, "param": "Товар", "detail": "Номенклатура" }, + { "col": 9, "style": "bordered-right", "param": "Количество" }, + { "col": 10, "style": "bordered-right", "param": "Сумма" } + ]} + ] + }, + { + "name": "Итого", + "rows": [ + { "cells": [ + { "col": 8, "span": 2, "style": "total-right", "text": "Итого:" }, + { "col": 10, "style": "total-right", "param": "Всего" } + ]} + ] + } + ] +} +``` + +### Верхний уровень + +| Поле | Обяз. | По умолч. | Описание | +|------|:-----:|-----------|----------| +| `columns` | да | — | Количество колонок | +| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию | +| `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"` | +| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) | +| `styles` | нет | `{}` | Именованные стили | +| `areas` | да | — | Массив именованных областей (порядок = порядок в документе) | + +### Шрифты (`fonts.`) + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `face` | `"Arial"` | Имя шрифта | +| `size` | `10` | Размер | +| `bold` | `false` | Жирный | +| `italic` | `false` | Курсив | + +Шрифт `"default"` используется когда стиль не указывает шрифт явно. + +### Стили (`styles.`) + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `font` | `"default"` | Ссылка на имя шрифта | +| `align` | — | `left`, `center`, `right` | +| `valign` | — | `top`, `center` | +| `border` | — | `none`, `all`, `bottom`, `top` | +| `wrap` | `false` | Перенос текста | + +### Области (`areas[]`) + +| Поле | Обяз. | Описание | +|------|:-----:|----------| +| `name` | да | Имя области для `Макет.ПолучитьОбласть("Имя")` | +| `rows` | да | Массив строк | + +### Строки (`rows[]`) + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `height` | — | Высота строки (если не задана, используется авто) | +| `rowStyle` | — | Стиль для ВСЕХ колонок (заполняет пустоты рамками) | +| `cells` | `[]` | Массив ячеек | + +Строка без `cells` и `rowStyle` → пустая строка. + +### Ячейки (`cells[]`) + +| Поле | Обяз. | По умолч. | Описание | +|------|:-----:|-----------|----------| +| `col` | да | — | Позиция колонки (1-based) | +| `span` | нет | `1` | Количество объединённых колонок | +| `style` | нет | rowStyle | Стиль ячейки (переопределяет rowStyle) | +| `param` | нет | — | Параметр заполнения | +| `detail` | нет | — | Параметр расшифровки (только с `param`) | +| `text` | нет | — | Статический текст | +| `template` | нет | — | Шаблонный текст с `[Параметр]` | + +Тип заполнения определяется автоматически: +- `param` → fillType=Parameter +- `template` → fillType=Template +- `text` → fillType=Text + +### `rowStyle` — автозаполнение + +Когда задан `rowStyle`, скрипт создаёт ячейки для ВСЕХ колонок строки. Позиции без явных ячеек заполняются пустыми ячейками с указанным стилем. Это обеспечивает сплошные рамки в табличных строках. + +## Ограничения MVP + +Текущая версия не поддерживает: +- Множественные наборы колонок +- Области типа Columns / Rectangle +- Рисунки (штрихкоды, картинки) +- Объединение по строкам (rowspan) +- Числовые форматы +- Фон ячеек + +Эти возможности будут добавлены в будущих версиях без переделки основной архитектуры. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 new file mode 100644 index 00000000..e946637d --- /dev/null +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -0,0 +1,587 @@ +param( + [Parameter(Mandatory)] + [string]$JsonPath, + + [Parameter(Mandatory)] + [string]$OutputPath +) + +$ErrorActionPreference = "Stop" +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +# --- 1. Load and validate JSON --- + +if (-not (Test-Path $JsonPath)) { + Write-Error "File not found: $JsonPath" + exit 1 +} + +$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$def = $json | ConvertFrom-Json + +if (-not $def.columns) { + Write-Error "Required field 'columns' is missing" + exit 1 +} +if (-not $def.areas) { + Write-Error "Required field 'areas' is missing" + exit 1 +} + +$totalColumns = [int]$def.columns +$defaultWidth = if ($def.defaultWidth) { [int]$def.defaultWidth } else { 10 } + +# --- 2. Build font palette --- + +$fontMap = [ordered]@{} # name -> 0-based index +$fontEntries = @() # array of hashtables + +function Add-Font { + param([string]$name, $fontDef) + $face = if ($fontDef.face) { $fontDef.face } else { "Arial" } + $size = if ($fontDef.size) { [int]$fontDef.size } else { 10 } + $bold = if ($fontDef.bold -eq $true) { "true" } else { "false" } + $italic = if ($fontDef.italic -eq $true) { "true" } else { "false" } + + $idx = $script:fontEntries.Count + $script:fontMap[$name] = $idx + $script:fontEntries += @{ + Face = $face + Size = $size + Bold = $bold + Italic = $italic + } +} + +# Add user-defined fonts +$hasDefault = $false +if ($def.fonts) { + foreach ($prop in $def.fonts.PSObject.Properties) { + if ($prop.Name -eq "default") { $hasDefault = $true } + Add-Font -name $prop.Name -fontDef $prop.Value + } +} + +# Ensure default font exists +if (-not $hasDefault) { + $defaultDef = New-Object PSObject -Property @{ face = "Arial"; size = 10 } + Add-Font -name "default" -fontDef $defaultDef +} + +# --- 3. Determine line palette --- + +$hasBorders = $false + +# Scan styles for border usage +if ($def.styles) { + foreach ($prop in $def.styles.PSObject.Properties) { + if ($prop.Value.border -and $prop.Value.border -ne "none") { + $hasBorders = $true + break + } + } +} + +$solidLineIndex = -1 +if ($hasBorders) { + $solidLineIndex = 0 +} + +# --- 4. Parse column width specs --- + +function Parse-ColumnSpec { + param([string]$spec) + $cols = @() + foreach ($part in $spec -split ',') { + $part = $part.Trim() + if ($part -match '^(\d+)-(\d+)$') { + $from = [int]$Matches[1] + $to = [int]$Matches[2] + for ($i = $from; $i -le $to; $i++) { $cols += $i } + } else { + $cols += [int]$part + } + } + return $cols +} + +# Build column width map: 1-based col -> width +$colWidthMap = @{} +if ($def.columnWidths) { + foreach ($prop in $def.columnWidths.PSObject.Properties) { + $width = [int]$prop.Value + $columns = Parse-ColumnSpec $prop.Name + foreach ($c in $columns) { + $colWidthMap[$c] = $width + } + } +} + +# --- 5. Style resolver --- + +function Resolve-Style { + param([string]$styleName, [string]$fillType) + + $fontIdx = $fontMap["default"] + $lb = -1; $tb = -1; $rb = -1; $bb = -1 + $ha = ""; $va = "" + $wrap = $false + + if ($styleName -and $def.styles) { + $style = $def.styles.$styleName + if ($style) { + # Font + if ($style.font -and $fontMap.Contains($style.font)) { + $fontIdx = $fontMap[$style.font] + } + + # Borders + if ($style.border) { + switch ($style.border) { + "all" { + $lb = $solidLineIndex; $tb = $solidLineIndex + $rb = $solidLineIndex; $bb = $solidLineIndex + } + "bottom" { $bb = $solidLineIndex } + "top" { $tb = $solidLineIndex } + "none" { } + } + } + + # Alignment + if ($style.align) { + switch ($style.align) { + "left" { $ha = "Left" } + "center" { $ha = "Center" } + "right" { $ha = "Right" } + } + } + if ($style.valign) { + switch ($style.valign) { + "top" { $va = "Top" } + "center" { $va = "Center" } + } + } + + # Wrap + if ($style.wrap -eq $true) { $wrap = $true } + } + } + + return @{ + FontIdx = $fontIdx + LB = $lb; TB = $tb; RB = $rb; BB = $bb + HA = $ha; VA = $va + Wrap = $wrap + FillType = $fillType + } +} + +# --- 6. Format palette builder --- + +$formatRegistry = [ordered]@{} # key -> hashtable with properties +$formatOrder = @() # ordered keys for index assignment + +function Get-FormatKey { + param( + [int]$fontIdx = -1, + [int]$lb = -1, [int]$tb = -1, [int]$rb = -1, [int]$bb = -1, + [string]$ha = "", [string]$va = "", + [bool]$wrap = $false, + [string]$fillType = "", + [int]$width = -1, + [int]$height = -1 + ) + return "f=$fontIdx|lb=$lb|tb=$tb|rb=$rb|bb=$bb|ha=$ha|va=$va|wr=$wrap|ft=$fillType|w=$width|h=$height" +} + +function Register-Format { + param([string]$key, [hashtable]$props) + if (-not $script:formatRegistry.Contains($key)) { + $script:formatRegistry[$key] = $props + $script:formatOrder += $key + } + # Return 1-based index + $idx = 0 + foreach ($k in $script:formatRegistry.Keys) { + $idx++ + if ($k -eq $key) { return $idx } + } + return $idx +} + +# 6a. Default width format +$defaultFormatKey = Get-FormatKey -width $defaultWidth +$defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth } + +# 6b. Column width formats +$colFormatMap = @{} # 1-based col -> format index +foreach ($col in $colWidthMap.Keys) { + $w = $colWidthMap[$col] + $key = Get-FormatKey -width $w + $idx = Register-Format -key $key -props @{ Width = $w } + $colFormatMap[[int]$col] = $idx +} + +# 6c. Scan areas for row heights and cell formats +# We need to do two passes: first collect all formats, then generate XML + +# Helper: determine fillType from cell content +function Get-FillType { + param($cell) + if ($cell.param) { return "Parameter" } + if ($cell.template) { return "Template" } + if ($cell.text) { return "Text" } + return "" +} + +# Helper: register a cell format and return its index +function Register-CellFormat { + param($styleName, [string]$fillType) + $resolved = Resolve-Style -styleName $styleName -fillType $fillType + $key = Get-FormatKey -fontIdx $resolved.FontIdx ` + -lb $resolved.LB -tb $resolved.TB -rb $resolved.RB -bb $resolved.BB ` + -ha $resolved.HA -va $resolved.VA ` + -wrap $resolved.Wrap -fillType $resolved.FillType + $props = @{ + FontIdx = $resolved.FontIdx + LB = $resolved.LB; TB = $resolved.TB + RB = $resolved.RB; BB = $resolved.BB + HA = $resolved.HA; VA = $resolved.VA + Wrap = $resolved.Wrap + FillType = $resolved.FillType + } + return Register-Format -key $key -props $props +} + +# Pre-register all formats from areas +foreach ($area in $def.areas) { + foreach ($row in $area.rows) { + # Row height format + if ($row.height) { + $hKey = Get-FormatKey -height ([int]$row.height) + Register-Format -key $hKey -props @{ Height = [int]$row.height } | Out-Null + } + + # rowStyle gap-fill format (no content → no fillType) + if ($row.rowStyle) { + Register-CellFormat -styleName $row.rowStyle -fillType "" | Out-Null + } + + # Explicit cell formats + if ($row.cells) { + foreach ($cell in $row.cells) { + $cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" } + $ft = Get-FillType $cell + Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null + } + } + } +} + +# --- 7. Generate XML --- + +$xml = New-Object System.Text.StringBuilder 4096 + +function X { + param([string]$text) + $script:xml.AppendLine($text) | Out-Null +} + +# 7a. Header +X '' +X '' + +# 7b. Language settings +X "`t" +X "`t`tru" +X "`t`tru" +X "`t`t" +X "`t`t`tru" +X "`t`t`tРусский" +X "`t`t`tРусский" +X "`t`t" +X "`t" + +# 7c. Columns +X "`t" +X "`t`t$totalColumns" + +# Emit columnsItem for columns with non-default widths +foreach ($col in ($colFormatMap.Keys | Sort-Object)) { + $fmtIdx = $colFormatMap[$col] + $colIdx = $col - 1 # Convert to 0-based + X "`t`t" + X "`t`t`t$colIdx" + X "`t`t`t" + X "`t`t`t`t$fmtIdx" + X "`t`t`t" + X "`t`t" +} + +X "`t" + +# 7d. Rows — main generation loop +$globalRow = 0 +$merges = @() +$namedItems = @() +$totalRowCount = 0 + +foreach ($area in $def.areas) { + $areaStartRow = $globalRow + $areaName = $area.name + + foreach ($row in $area.rows) { + $rowHasContent = $false + $rowCells = @() # array of { Col(0-based), FormatIdx, Content } + + # Determine row height format + $rowFormatIdx = 0 + if ($row.height) { + $hKey = Get-FormatKey -height ([int]$row.height) + # Find format index for this key + $rIdx = 0 + foreach ($k in $formatRegistry.Keys) { + $rIdx++ + if ($k -eq $hKey) { $rowFormatIdx = $rIdx; break } + } + } + + if ($row.cells -and $row.cells.Count -gt 0) { + $rowHasContent = $true + + # Build set of occupied columns (1-based) + $occupiedCols = @{} + foreach ($cell in $row.cells) { + $colStart = [int]$cell.col + $colSpan = if ($cell.span) { [int]$cell.span } else { 1 } + for ($c = $colStart; $c -lt ($colStart + $colSpan); $c++) { + $occupiedCols[$c] = $true + } + } + + # Generate explicit cells + foreach ($cell in $row.cells) { + $colStart = [int]$cell.col + $colSpan = if ($cell.span) { [int]$cell.span } else { 1 } + $cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" } + $ft = Get-FillType $cell + $fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft + + $cellInfo = @{ + Col = $colStart - 1 # 0-based + FormatIdx = $fmtIdx + Param = $cell.param + Detail = $cell.detail + Text = $cell.text + Template = $cell.template + } + $rowCells += $cellInfo + + # Collect merge + if ($colSpan -gt 1) { + $merges += @{ + R = $globalRow + C = $colStart - 1 + W = $colSpan - 1 + } + } + } + + # Generate gap-fill cells for rowStyle + if ($row.rowStyle) { + $gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType "" + for ($c = 1; $c -le $totalColumns; $c++) { + if (-not $occupiedCols.ContainsKey($c)) { + $rowCells += @{ + Col = $c - 1 # 0-based + FormatIdx = $gapFmtIdx + Param = $null + Detail = $null + Text = $null + Template = $null + } + } + } + } + + # Sort cells by column + $rowCells = $rowCells | Sort-Object { $_.Col } + + } elseif ($row.rowStyle) { + # Row with only rowStyle, no explicit cells — fill all columns + $rowHasContent = $true + $gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType "" + for ($c = 0; $c -lt $totalColumns; $c++) { + $rowCells += @{ + Col = $c + FormatIdx = $gapFmtIdx + Param = $null + Detail = $null + Text = $null + Template = $null + } + } + } + + # Emit rowsItem + X "`t" + X "`t`t$globalRow" + X "`t`t" + + if ($rowFormatIdx -gt 0) { + X "`t`t`t$rowFormatIdx" + } + + if (-not $rowHasContent) { + X "`t`t`ttrue" + } else { + foreach ($cellInfo in $rowCells) { + X "`t`t`t" + X "`t`t`t`t$($cellInfo.Col)" + X "`t`t`t`t" + X "`t`t`t`t`t$($cellInfo.FormatIdx)" + + if ($cellInfo.Param) { + X "`t`t`t`t`t$($cellInfo.Param)" + if ($cellInfo.Detail) { + X "`t`t`t`t`t$($cellInfo.Detail)" + } + } + + if ($cellInfo.Text) { + X "`t`t`t`t`t" + X "`t`t`t`t`t`t" + X "`t`t`t`t`t`t`tru" + X "`t`t`t`t`t`t`t$($cellInfo.Text)" + X "`t`t`t`t`t`t" + X "`t`t`t`t`t" + } + + if ($cellInfo.Template) { + X "`t`t`t`t`t" + X "`t`t`t`t`t`t" + X "`t`t`t`t`t`t`tru" + X "`t`t`t`t`t`t`t$($cellInfo.Template)" + X "`t`t`t`t`t`t" + X "`t`t`t`t`t" + } + + X "`t`t`t`t" + X "`t`t`t" + } + } + + X "`t`t" + X "`t" + + $globalRow++ + } + + $areaEndRow = $globalRow - 1 + $namedItems += @{ + Name = $areaName + BeginRow = $areaStartRow + EndRow = $areaEndRow + } +} + +$totalRowCount = $globalRow + +# 7e. Scalar metadata +X "`ttrue" +X "`t$defaultFormatIndex" +X "`t$totalRowCount" +X "`t$totalRowCount" + +# 7f. Merges +foreach ($m in $merges) { + X "`t" + X "`t`t$($m.R)" + X "`t`t$($m.C)" + X "`t`t$($m.W)" + X "`t" +} + +# 7g. Named items +foreach ($ni in $namedItems) { + X "`t" + X "`t`t$($ni.Name)" + X "`t`t" + X "`t`t`tRows" + X "`t`t`t$($ni.BeginRow)" + X "`t`t`t$($ni.EndRow)" + X "`t`t`t-1" + X "`t`t`t-1" + X "`t`t" + X "`t" +} + +# 7h. Line palette +if ($hasBorders) { + X "`t" + X "`t`tSolid" + X "`t" +} + +# 7i. Font palette +foreach ($fe in $fontEntries) { + X "`t" +} + +# 7j. Format palette +foreach ($key in $formatRegistry.Keys) { + $fmt = $formatRegistry[$key] + X "`t" + + if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) { + X "`t`t$($fmt.FontIdx)" + } + if ($fmt.LB -ne $null -and $fmt.LB -ge 0) { + X "`t`t$($fmt.LB)" + } + if ($fmt.TB -ne $null -and $fmt.TB -ge 0) { + X "`t`t$($fmt.TB)" + } + if ($fmt.RB -ne $null -and $fmt.RB -ge 0) { + X "`t`t$($fmt.RB)" + } + if ($fmt.BB -ne $null -and $fmt.BB -ge 0) { + X "`t`t$($fmt.BB)" + } + if ($fmt.Width) { + X "`t`t$($fmt.Width)" + } + if ($fmt.Height) { + X "`t`t$($fmt.Height)" + } + if ($fmt.HA) { + X "`t`t$($fmt.HA)" + } + if ($fmt.VA) { + X "`t`t$($fmt.VA)" + } + if ($fmt.Wrap -eq $true) { + X "`t`tWrap" + } + if ($fmt.FillType) { + X "`t`t$($fmt.FillType)" + } + + X "`t" +} + +# 7k. Close document +X '' + +# --- 8. Write output --- + +$enc = New-Object System.Text.UTF8Encoding($true) +[System.IO.File]::WriteAllText((Join-Path (Get-Location) $OutputPath), $xml.ToString(), $enc) + +# --- 9. Summary --- + +Write-Host "[OK] Compiled: $OutputPath" +Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns" +Write-Host " Fonts: $($fontEntries.Count), Lines: $(if ($hasBorders) { 1 } else { 0 }), Formats: $($formatRegistry.Count)" +Write-Host " Merges: $($merges.Count)" diff --git a/README.md b/README.md index 00613c99..769922d8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ | `/epf-bsp-add-command` | ` <Идентификатор>` | Добавить команду в обработку БСП | | `/mxl-info` | `` | Анализ структуры табличного документа (области, параметры, колонки) | | `/mxl-validate` | `` | Валидация табличного документа (индексы, ссылки, границы) | +| `/mxl-compile` | ` ` | Компиляция табличного документа из JSON-определения | Навыки удаления (`epf-remove-*`) не вызываются Claude автоматически — только по явной команде пользователя. @@ -142,7 +143,8 @@ src/ ├── epf-bsp-init/ # SKILL.md (шаблоны кода, без скриптов) ├── epf-bsp-add-command/ # SKILL.md (шаблоны кода, без скриптов) ├── mxl-info/ # SKILL.md + scripts/mxl-info.ps1 -└── mxl-validate/ # SKILL.md + scripts/mxl-validate.ps1 +├── mxl-validate/ # SKILL.md + scripts/mxl-validate.ps1 +└── mxl-compile/ # SKILL.md + scripts/mxl-compile.ps1 docs/ ├── 1c-xml-format-spec.md # Спецификация XML-формата выгрузки ├── 1c-help-spec.md # Спецификация встроенной справки