fix(form-validate,cfe-borrow): остаточные ложные ошибки на формах платформы

Корпусный прогон (УТ/БП/ERP, 21 097 форм) после понижения Command/Action
оставлял 8 форм с ошибками. Разобраны все, дефектов оказалось два.

1. Вложенная таблица. Путь Items.<Таблица>.CurrentData.<Поле> разрешался
   ОДНИМ шагом: если таблица сама привязана через Items.*, корнем
   оставался литерал «Items», и типовая форма объявлялась битой
   (НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP). Теперь
   разрешение идёт цепочкой, со страховкой от кольца ссылок.

2. AutoCommandBar с обычным id вместо -1. Это соглашение, а не требование:
   21 094 формы из 21 097 используют -1, но три платформа выгружает с
   обычным id и грузит их без нареканий. Понижено до предупреждения;
   ошибка осталась на случай, когда id вообще не число.

Оставшиеся три формы — дубли id элементов и команд. Это НЕ ложные
срабатывания: измерение по корпусу показало ровно по одному случаю на
21 097 форм, то есть опечатки вендора, а не структурное правило (были бы
пулы id раздельными, пересечений были бы тысячи). Плюс кейс duplicate-id
прямо требует считать дубль ошибкой.

Итого по корпусу: 283 формы с ошибками → 3.

Заодно дооформлена фикстура cfe-borrow/container-types (пространство имён
веб-сервису, документ журналу и последовательности): verify-snapshots по
cfe-borrow теперь 12/12.
This commit is contained in:
Nick Shirokov
2026-08-12 20:20:02 +03:00
parent 71f047291c
commit af00aa4711
32 changed files with 1266 additions and 34 deletions
@@ -1,4 +1,4 @@
# form-validate v1.14 — Validate 1C managed form
# form-validate v1.15 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -201,10 +201,15 @@ if (-not $stopped) {
if ($acb) {
$acbName = $acb.GetAttribute("name")
$acbId = $acb.GetAttribute("id")
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
# предупреждение; ошибка — только если id вовсе не число.
if ($acbId -eq "-1") {
Report-OK "AutoCommandBar: name='$acbName', id=$acbId"
} elseif ($acbId -match '^-?\d+$') {
Report-Warn "AutoCommandBar id='$acbId', usually '-1'"
} else {
Report-Error "AutoCommandBar id='$acbId', expected '-1'"
Report-Error "AutoCommandBar id='$acbId' is not a number"
}
} else {
Report-Error "AutoCommandBar element missing"
@@ -484,11 +489,19 @@ if (-not $stopped) {
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if ($rootAttr -eq 'Items') {
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
# Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
$itemsHops = 0
$itemsBroken = $false
while ($rootAttr -eq 'Items') {
$itemsHops++
if ($itemsHops -gt 10) { $itemsBroken = $true; break } # страховка от кольца ссылок
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': $bTag='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue
$itemsBroken = $true
break
}
$tableName = $segments[1]
$tableEl = $null
@@ -501,17 +514,21 @@ if (-not $stopped) {
if (-not $tableEl) {
Report-Error "[$tag] '$elName': $bTag='$dataPath' — table element '$tableName' not found"
$pathErrors++
continue
$itemsBroken = $true
break
}
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently
continue
$itemsBroken = $true
break
}
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0]
$segments = $tableDp -split '\.'
$rootAttr = $segments[0]
}
if ($itemsBroken) { continue }
if (-not $attrMap.ContainsKey($rootAttr)) {
Report-Error "[$tag] '$elName': $bTag='$dataPath' — attribute '$rootAttr' not found"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.14 — Validate 1C managed form
# form-validate v1.15 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -256,10 +256,15 @@ def main():
if acb is not None:
acb_name = acb.get("name", "")
acb_id = acb.get("id", "")
# id=-1 — соглашение, а не требование: в корпусе УТ/БП/ERP так у 21 094 форм из 21 097,
# но три формы платформа выгружает с обычным id и грузит их без нареканий. Поэтому
# предупреждение; ошибка — только если id вовсе не число.
if acb_id == "-1":
report_ok(f"AutoCommandBar: name='{acb_name}', id={acb_id}")
elif re.match(r'^-?\d+$', acb_id):
report_warn(f"AutoCommandBar id='{acb_id}', usually '-1'")
else:
report_error(f"AutoCommandBar id='{acb_id}', expected '-1'")
report_error(f"AutoCommandBar id='{acb_id}' is not a number")
else:
report_error("AutoCommandBar element missing")
@@ -511,11 +516,21 @@ def main():
segments = clean_path.split(".")
root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if root_attr == 'Items':
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute.
# Разрешаем ЦЕПОЧКОЙ: таблица во вложенной таблице сама привязана через Items.*, и один
# шаг оставлял корнем литерал «Items» — форма платформы объявлялась битой (типовые
# НастройкаПравилОбработкиЗаявокСотрудников в БП и ERP).
items_hops = 0
items_broken = False
while root_attr == 'Items':
items_hops += 1
if items_hops > 10: # страховка от кольца ссылок
items_broken = True
break
if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue
items_broken = True
break
table_name = segments[1]
table_el = None
for candidate in all_elements:
@@ -525,14 +540,19 @@ def main():
if table_el is None:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — table element '{table_name}' not found")
path_errors += 1
continue
items_broken = True
break
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip():
continue
items_broken = True
break
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'):
table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0]
segments = table_dp.split(".")
root_attr = segments[0]
if items_broken:
continue
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': {b_tag}='{data_path}' — attribute '{root_attr}' not found")