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 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-09 15:44:32 +03:00
co-authored by Claude Opus 4.6
parent d05199d048
commit c665800916
6 changed files with 110 additions and 9 deletions
+16 -1
View File
@@ -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
+13
View File
@@ -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` или базовые типы для автономной сборки
@@ -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
+6 -4
View File
@@ -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}` — имена обработчиков
+20 -4
View File
@@ -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",
@@ -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 (<v8:item>). Use top-level 'title' key in form-compile DSL."
} else {
Report-OK "Title: multilingual XML"
}
}
}
# --- Summary ---
Write-Host ""