From c66580091611035986ca68f313602c44a12026db Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Mon, 9 Feb 2026 15:44:32 +0300 Subject: [PATCH] Improve form skills: validation, docs, round-trip consistency form-compile: warn about unknown DSL keys in element definitions, document stdCommand/command button keys and EPF-specific notes. form-validate: check that form-level Title uses multilingual XML, not plain text (which causes XDTO errors at build time). form-add: warn about duplicate element names, clarify after-not-found message when using into+after together. form-info: show Title in header instead of Properties line, display commands as DSL-friendly format (-> Name [cmd], -> Close [std]) instead of raw Form.Command/Form.StandardCommand paths. Co-Authored-By: Claude Opus 4.6 --- .claude/skills/form-add/scripts/form-add.ps1 | 17 +++++++- .claude/skills/form-compile/SKILL.md | 13 ++++++ .../form-compile/scripts/form-compile.ps1 | 41 +++++++++++++++++++ .claude/skills/form-info/SKILL.md | 10 +++-- .../skills/form-info/scripts/form-info.ps1 | 24 +++++++++-- .../form-validate/scripts/form-validate.ps1 | 14 +++++++ 6 files changed, 110 insertions(+), 9 deletions(-) diff --git a/.claude/skills/form-add/scripts/form-add.ps1 b/.claude/skills/form-add/scripts/form-add.ps1 index ad6ee781..76ad3e28 100644 --- a/.claude/skills/form-add/scripts/form-add.ps1 +++ b/.claude/skills/form-add/scripts/form-add.ps1 @@ -634,7 +634,7 @@ function Insert-IntoContainer($container, $newNode, $afterName, $childIndent) { if ($afterElem) { $refNode = $afterElem.NextSibling } else { - Write-Host "[WARN] Element '$afterName' not found in target container, appending at end" + Write-Host "[WARN] after='$afterName' not found in target container, appending at end" } } @@ -737,6 +737,21 @@ if ($def.elements -and $def.elements.Count -gt 0) { # Detect indent level $childIndent = Get-ChildIndent $targetCI + # Check for duplicate element names + foreach ($el in $def.elements) { + $typeKey = $null + foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) { + if ($el.$key -ne $null) { $typeKey = $key; break } + } + if ($typeKey) { + $elName = Get-ElementName -el $el -typeKey $typeKey + $existing = Find-Element $rootCI $elName + if ($existing) { + Write-Host "[WARN] Element '$elName' already exists in form (id=$($existing.GetAttribute('id')))" + } + } + } + # Remember starting element ID for companion counting $startElemId = $script:nextElemId diff --git a/.claude/skills/form-compile/SKILL.md b/.claude/skills/form-compile/SKILL.md index 6791f941..68011a39 100644 --- a/.claude/skills/form-compile/SKILL.md +++ b/.claude/skills/form-compile/SKILL.md @@ -63,6 +63,13 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile - `"readOnly": true` — ReadOnly=true - `"on": ["OnChange", "StartChoice"]` — события с автоименованием обработчиков +### Свойства кнопок + +- `"command": "ИмяКоманды"` — привязка к команде формы → `Form.Command.ИмяКоманды` +- `"stdCommand": "Close"` — привязка к стандартной команде → `Form.StandardCommand.Close` +- `"defaultButton": true` — кнопка по умолчанию +- `"type": "hyperlink"` — тип кнопки (`"usual"` / `"hyperlink"` / `"commandBar"`) + ### Система типов (shorthand) | DSL | XML | @@ -119,3 +126,9 @@ powershell.exe -NoProfile -File .claude\skills\form-compile\scripts\form-compile Структура в сводке должна совпадать с определением в JSON. +## Особенности для внешних обработок (EPF) + +- **Тип главного реквизита**: `ExternalDataProcessorObject.ИмяОбработки` (не `DataProcessorObject`) +- **DataPath**: используйте реквизиты формы (`ИмяРеквизита`), а не `Объект.ИмяРеквизита` — у внешних обработок нет реквизитов объекта в метаданных +- **Ссылочные типы**: `CatalogRef.XXX`, `DocumentRef.XXX` и т.д. могут не собраться в пустой базе — используйте `string` или базовые типы для автономной сборки + diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index d7bc8f13..3d0adc66 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -272,6 +272,47 @@ function Emit-Element { return } + # Validate known keys — warn about typos and unknown properties + $knownKeys = @{ + # type keys + "group"=1;"input"=1;"check"=1;"label"=1;"labelField"=1;"table"=1;"pages"=1;"page"=1 + "button"=1;"picture"=1;"picField"=1;"calendar"=1;"cmdBar"=1;"popup"=1 + # naming & binding + "name"=1;"path"=1;"title"=1 + # visibility & state + "visible"=1;"hidden"=1;"enabled"=1;"disabled"=1;"readOnly"=1 + # events + "on"=1;"handlers"=1 + # layout + "titleLocation"=1;"representation"=1;"width"=1;"height"=1 + "horizontalStretch"=1;"verticalStretch"=1;"autoMaxWidth"=1;"autoMaxHeight"=1 + # input-specific + "multiLine"=1;"passwordMode"=1;"choiceButton"=1;"clearButton"=1 + "spinButton"=1;"dropListButton"=1;"markIncomplete"=1;"skipOnInput"=1;"inputHint"=1 + # label/hyperlink + "hyperlink"=1 + # group-specific + "showTitle"=1;"united"=1 + # hierarchy + "children"=1;"columns"=1 + # table-specific + "changeRowSet"=1;"changeRowOrder"=1;"header"=1;"footer"=1 + "commandBarLocation"=1;"searchStringLocation"=1 + # pages-specific + "pagesRepresentation"=1 + # button-specific + "type"=1;"command"=1;"stdCommand"=1;"defaultButton"=1;"locationInCommandBar"=1 + # picture/decoration + "src"=1 + # cmdBar-specific + "autofill"=1 + } + foreach ($p in $el.PSObject.Properties) { + if (-not $knownKeys.ContainsKey($p.Name)) { + Write-Warning "Element '$($el.$typeKey)': unknown key '$($p.Name)' — ignored. Check SKILL.md for valid keys." + } + } + $name = Get-ElementName -el $el -typeKey $typeKey $id = New-Id diff --git a/.claude/skills/form-info/SKILL.md b/.claude/skills/form-info/SKILL.md index 8f61e1f5..9a564f47 100644 --- a/.claude/skills/form-info/SKILL.md +++ b/.claude/skills/form-info/SKILL.md @@ -42,14 +42,14 @@ powershell.exe -NoProfile -File .claude\skills\form-info\scripts\form-info.ps1 - ### Заголовок ``` -=== Form: ФормаДокумента (Documents.РеализацияТоваровУслуг) === +=== Form: ФормаДокумента — "Реализация товаров и услуг" (Documents.РеализацияТоваровУслуг) === ``` -Имя формы и контекст объекта определяются из пути к файлу. +Имя формы, заголовок (Title) и контекст объекта определяются из пути к файлу и XML. ### Properties — свойства формы -Только нестандартные свойства (отличающиеся от умолчания): +Только нестандартные свойства (отличающиеся от умолчания). Title показывается в заголовке, не здесь: ``` Properties: AutoTitle=false, WindowOpeningMode=LockOwnerWindow, CommandBarLocation=Bottom @@ -110,7 +110,9 @@ Elements: - `[ro]` — ReadOnly=true - `,collapse` — Behavior=Collapsible (для групп) -**Привязка**: `-> Объект.Поле` — DataPath или CommandName +**Привязка к данным**: `-> Объект.Поле` — DataPath + +**Привязка к команде**: `-> ИмяКоманды [cmd]` — команда формы, `-> Close [std]` — стандартная команда **События**: `{OnChange, StartChoice}` — имена обработчиков diff --git a/.claude/skills/form-info/scripts/form-info.ps1 b/.claude/skills/form-info/scripts/form-info.ps1 index 12f84674..61ef72da 100644 --- a/.claude/skills/form-info/scripts/form-info.ps1 +++ b/.claude/skills/form-info/scripts/form-info.ps1 @@ -253,7 +253,16 @@ function Build-Tree($childItemsNode, [string]$prefix, [bool]$isLast) { $binding = " -> $($dp.InnerText)" } else { $cn = $child.SelectSingleNode("d:CommandName", $ns) - if ($cn) { $binding = " -> $($cn.InnerText)" } + if ($cn) { + $cnVal = $cn.InnerText + if ($cnVal -match '^Form\.StandardCommand\.(.+)$') { + $binding = " -> $($Matches[1]) [std]" + } elseif ($cnVal -match '^Form\.Command\.(.+)$') { + $binding = " -> $($Matches[1]) [cmd]" + } else { + $binding = " -> $cnVal" + } + } } # Title differs? @@ -322,16 +331,23 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) { $lines = @() -# Header +# Header — include Title if present +$titleNode = $root.SelectSingleNode("d:Title", $ns) +$formTitle = $null +if ($titleNode) { + $formTitle = Get-MLText $titleNode + if (-not $formTitle) { $formTitle = $titleNode.InnerText } +} $header = "=== Form: $formName" +if ($formTitle) { $header += " — `"$formTitle`"" } if ($objectContext) { $header += " ($objectContext)" } $header += " ===" $lines += $header -# --- Form properties --- +# --- Form properties (Title excluded — shown in header) --- $propNames = @( - "Title", "Width", "Height", "Group", + "Width", "Height", "Group", "WindowOpeningMode", "EnterKeyBehavior", "AutoTitle", "AutoURL", "AutoFillCheck", "Customizable", "CommandBarLocation", "SaveDataInSettings", "AutoSaveDataInSettings", diff --git a/.claude/skills/form-validate/scripts/form-validate.ps1 b/.claude/skills/form-validate/scripts/form-validate.ps1 index ca13dcdd..696dd310 100644 --- a/.claude/skills/form-validate/scripts/form-validate.ps1 +++ b/.claude/skills/form-validate/scripts/form-validate.ps1 @@ -463,6 +463,20 @@ if (-not $stopped) { } } +# --- Check 10: Title must be multilingual XML (not plain text) --- + +if (-not $stopped) { + $titleNode = $root.SelectSingleNode("f:Title", $nsMgr) + if ($titleNode) { + $v8items = $titleNode.SelectNodes("v8:item", $nsMgr) + if ($v8items.Count -eq 0 -and $titleNode.InnerText.Trim() -ne "") { + Report-Error "Form Title is plain text ('$($titleNode.InnerText.Trim())') — must be multilingual XML (). Use top-level 'title' key in form-compile DSL." + } else { + Report-OK "Title: multilingual XML" + } + } +} + # --- Summary --- Write-Host ""