diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 index 0b526b35..22a6cff2 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.ps1 @@ -1,4 +1,4 @@ -# cfe-borrow v1.0 — Borrow objects from configuration into extension (CFE) +# cfe-borrow v1.1 — Borrow objects from configuration into extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][string]$ExtensionPath, @@ -476,36 +476,209 @@ function Borrow-Form { $formVersion = $srcFormEl.GetAttribute("version") if (-not $formVersion) { $formVersion = "2.17" } - # Find direct children: AutoCommandBar, ChildItems (visual elements only) + # Find direct children: form properties, AutoCommandBar, ChildItems $srcAutoCmd = $null $srcChildItems = $null + $formProps = @() + $reachedVisual = $false foreach ($fc in $srcFormEl.ChildNodes) { if ($fc.NodeType -ne 'Element') { continue } - if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { $srcAutoCmd = $fc } - elseif ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { $srcChildItems = $fc } + if ($fc.LocalName -eq 'AutoCommandBar' -and -not $srcAutoCmd) { + $reachedVisual = $true; $srcAutoCmd = $fc; continue + } + if ($fc.LocalName -eq 'ChildItems' -and -not $srcChildItems) { + $reachedVisual = $true; $srcChildItems = $fc; continue + } + if ($fc.LocalName -eq 'Events' -or $fc.LocalName -eq 'Attributes' -or $fc.LocalName -eq 'Commands' -or $fc.LocalName -eq 'Parameters') { + $reachedVisual = $true; continue + } + if (-not $reachedVisual) { + $formProps += $fc.OuterXml + } } # Get OuterXml and strip redundant namespace redeclarations (they're on root
) $nsStripPattern = '\s+xmlns(?::\w+)?="[^"]*"' + # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false $autoCmdXml = "" if ($srcAutoCmd) { $autoCmdXml = $srcAutoCmd.OuterXml $autoCmdXml = [regex]::Replace($autoCmdXml, $nsStripPattern, '') - # Replace all CommandName values with 0 (base form buttons lose command refs) $autoCmdXml = [regex]::Replace($autoCmdXml, '[^<]*', '0') - # Replace Autofill true → false $autoCmdXml = $autoCmdXml -replace 'true', 'false' } + # ChildItems: copy full tree, clean up base-config references $childItemsXml = "" if ($srcChildItems) { $childItemsXml = $srcChildItems.OuterXml $childItemsXml = [regex]::Replace($childItemsXml, $nsStripPattern, '') - # Replace all CommandName values with 0 in ChildItems too + # Replace all CommandName values with 0 $childItemsXml = [regex]::Replace($childItemsXml, '[^<]*', '0') - } else { - $childItemsXml = "" + # Strip DataPath (references base form attributes not in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip TitleDataPath (e.g. Объект.Товары.RowsCount — invalid without base attributes) + $childItemsXml = [regex]::Replace($childItemsXml, '\s*[^<]*', '') + # Strip TypeLink blocks with human-readable DataPath (Items.XXX — can't convert to UUID) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*Items\.[^<]*.*?', '') + # Strip element-level Events (base form handlers not in extension) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*.*?', '') + + # Collect CommonPicture references from ChildItems + $picRefs = [regex]::Matches($childItemsXml, 'CommonPicture\.(\w+)') + $referencedPictures = @{} + foreach ($m in $picRefs) { $referencedPictures[$m.Groups[1].Value] = $true } + + # Auto-borrow referenced CommonPictures (if not already borrowed) + $autoBorrowedPics = @() + foreach ($picName in $referencedPictures.Keys) { + if (-not (Test-ObjectBorrowed "CommonPicture" $picName)) { + $picSrcFile = Join-Path (Join-Path $cfgDir "CommonPictures") "${picName}.xml" + if (Test-Path $picSrcFile) { + $src = Read-SourceObject "CommonPicture" $picName + $borrowedXml = Build-BorrowedObjectXml "CommonPicture" $picName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "CommonPictures" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${picName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "CommonPicture" $picName + $autoBorrowedPics += $picName + $borrowedFiles += $targetFile + Info " Auto-borrowed: CommonPicture.${picName}" + } else { + Warn " CommonPicture.${picName} not found in source config — will strip from form" + } + } + } + + # Collect all borrowed CommonPictures (including previously borrowed) + $borrowedPicSet = @{} + $nsMgr2 = New-Object System.Xml.XmlNamespaceManager($script:xmlDoc.NameTable) + $nsMgr2.AddNamespace("md", $script:mdNs) + $picNodes = $script:xmlDoc.SelectNodes("//md:ChildObjects/md:CommonPicture", $nsMgr2) + foreach ($pn in $picNodes) { $borrowedPicSet[$pn.InnerText] = $true } + + # Strip blocks referencing non-borrowed CommonPictures + $picBlockPattern = '(?s)\s*\s*CommonPicture\.(\w+).*?' + $picMatches = [regex]::Matches($childItemsXml, $picBlockPattern) + # Process in reverse order to preserve positions + for ($mi = $picMatches.Count - 1; $mi -ge 0; $mi--) { + $pm = $picMatches[$mi] + $cpName = $pm.Groups[1].Value + if (-not $borrowedPicSet.ContainsKey($cpName)) { + $childItemsXml = $childItemsXml.Remove($pm.Index, $pm.Length) + } + } + # Strip StdPicture blocks (except Print) + $childItemsXml = [regex]::Replace($childItemsXml, '(?s)\s*\s*StdPicture\.(?!Print\b)\w+.*?', '') + + # Auto-borrow StyleItems referenced in ChildItems + # Pattern 1: , + # Pattern 2: style:XXX, style:XXX, etc. + $referencedStyles = @{} + $styleRefs1 = [regex]::Matches($childItemsXml, 'ref="style:(\w+)"[^>]*kind="StyleItem"') + foreach ($m in $styleRefs1) { $referencedStyles[$m.Groups[1].Value] = $true } + $styleRefs2 = [regex]::Matches($childItemsXml, '>style:(\w+)') + foreach ($m in $styleRefs2) { $referencedStyles[$m.Groups[1].Value] = $true } + + foreach ($styleName in $referencedStyles.Keys) { + if (-not (Test-ObjectBorrowed "StyleItem" $styleName)) { + $styleSrcFile = Join-Path (Join-Path $cfgDir "StyleItems") "${styleName}.xml" + if (Test-Path $styleSrcFile) { + $src = Read-SourceObject "StyleItem" $styleName + $borrowedXml = Build-BorrowedObjectXml "StyleItem" $styleName $src.Uuid $src.Properties + $targetDir = Join-Path $extDir "StyleItems" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${styleName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "StyleItem" $styleName + $borrowedFiles += $targetFile + Info " Auto-borrowed: StyleItem.${styleName}" + } else { + Warn " StyleItem.${styleName} not found in source config" + } + } + } + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef in ChoiceParameters + # Collect Enum -> [EnumValue names] map + $dtRefs = [regex]::Matches($childItemsXml, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)') + $referencedEnumValues = @{} + foreach ($m in $dtRefs) { + $eName = $m.Groups[1].Value + $evName = $m.Groups[2].Value + if (-not $referencedEnumValues.ContainsKey($eName)) { $referencedEnumValues[$eName] = @{} } + $referencedEnumValues[$eName][$evName] = $true + } + + foreach ($enumName in $referencedEnumValues.Keys) { + if (-not (Test-ObjectBorrowed "Enum" $enumName)) { + $enumSrcFile = Join-Path (Join-Path $cfgDir "Enums") "${enumName}.xml" + if (Test-Path $enumSrcFile) { + # Read source Enum to get UUID and EnumValue UUIDs + $srcParser = New-Object System.Xml.XmlDocument + $srcParser.PreserveWhitespace = $true + $srcParser.Load($enumSrcFile) + $srcEnumEl = $null + foreach ($cn in $srcParser.DocumentElement.ChildNodes) { + if ($cn.NodeType -eq 'Element') { $srcEnumEl = $cn; break } + } + $srcEnumUuid = $srcEnumEl.GetAttribute("uuid") + + # Find source EnumValues by name + $enumValueXmls = @() + $neededValues = $referencedEnumValues[$enumName] + $srcNsMgr = New-Object System.Xml.XmlNamespaceManager($srcParser.NameTable) + $srcNsMgr.AddNamespace("md", $script:mdNs) + $srcEvNodes = $srcEnumEl.SelectNodes("md:ChildObjects/md:EnumValue", $srcNsMgr) + foreach ($evNode in $srcEvNodes) { + $evUuid = $evNode.GetAttribute("uuid") + $evNameNode = $evNode.SelectSingleNode("md:Properties/md:Name", $srcNsMgr) + if ($evNameNode -and $neededValues.ContainsKey($evNameNode.InnerText)) { + $newEvUuid = [guid]::NewGuid().ToString() + $enumValueXmls += @" + + + + Adopted + $($evNameNode.InnerText) + + ${evUuid} + + +"@ + } + } + + # Build borrowed Enum with EnumValues in ChildObjects + $src = Read-SourceObject "Enum" $enumName + $borrowedXml = Build-BorrowedObjectXml "Enum" $enumName $src.Uuid $src.Properties + if ($enumValueXmls.Count -gt 0) { + $evBlock = ($enumValueXmls -join "`r`n") + $borrowedXml = $borrowedXml -replace '', "`r`n${evBlock}`r`n`t`t" + } + + $targetDir = Join-Path $extDir "Enums" + if (-not (Test-Path $targetDir)) { + New-Item -ItemType Directory -Path $targetDir -Force | Out-Null + } + $targetFile = Join-Path $targetDir "${enumName}.xml" + $encBom = New-Object System.Text.UTF8Encoding($true) + [System.IO.File]::WriteAllText($targetFile, $borrowedXml, $encBom) + Add-ToChildObjects "Enum" $enumName + $borrowedFiles += $targetFile + Info " Auto-borrowed: Enum.${enumName} (with $($enumValueXmls.Count) EnumValue(s))" + } else { + Warn " Enum.${enumName} not found in source config" + } + } + } } # Extract the opening tag from source text (preserves namespace declarations) @@ -521,22 +694,31 @@ function Borrow-Form { $formXmlSb.Append($formTag) | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # Part 1: visual elements (add leading tab to first line of each block) + # Part 1: form properties + AutoCommandBar + ChildItems + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t$propXml`r`n") | Out-Null + } if ($autoCmdXml) { $formXmlSb.Append("`t$autoCmdXml") | Out-Null $formXmlSb.Append("`r`n") | Out-Null } - $formXmlSb.Append("`t$childItemsXml") | Out-Null - $formXmlSb.Append("`r`n") | Out-Null + if ($childItemsXml) { + $formXmlSb.Append("`t$childItemsXml") | Out-Null + $formXmlSb.Append("`r`n") | Out-Null + } $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null - # BaseForm: same visual elements, indented one more level + # BaseForm: same content, indented one more level $formXmlSb.Append("`t") | Out-Null $formXmlSb.Append("`r`n") | Out-Null + foreach ($propXml in $formProps) { + $propXml = [regex]::Replace($propXml, $nsStripPattern, '') + $formXmlSb.Append("`t`t$propXml`r`n") | Out-Null + } if ($autoCmdXml) { - # Reindent for BaseForm: first line gets 2 tabs, other lines get +1 tab $acLines = $autoCmdXml -split "`r?`n" for ($li = 0; $li -lt $acLines.Count; $li++) { if ($li -eq 0) { $formXmlSb.Append("`t`t$($acLines[$li])") | Out-Null } @@ -544,12 +726,14 @@ function Borrow-Form { $formXmlSb.Append("`r`n") | Out-Null } } - - $ciLines = $childItemsXml -split "`r?`n" - for ($li = 0; $li -lt $ciLines.Count; $li++) { - if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } - else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } - $formXmlSb.Append("`r`n") | Out-Null + if ($childItemsXml) { + # Reindent ChildItems for BaseForm (+1 tab level) + $ciLines = $childItemsXml -split "`r?`n" + for ($li = 0; $li -lt $ciLines.Count; $li++) { + if ($li -eq 0) { $formXmlSb.Append("`t`t$($ciLines[$li])") | Out-Null } + else { $formXmlSb.Append("`t$($ciLines[$li])") | Out-Null } + $formXmlSb.Append("`r`n") | Out-Null + } } $formXmlSb.Append("`t`t") | Out-Null diff --git a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py index 9b00d514..4c082d2e 100644 --- a/.claude/skills/cfe-borrow/scripts/cfe-borrow.py +++ b/.claude/skills/cfe-borrow/scripts/cfe-borrow.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-borrow v1.0 — Borrow objects from configuration into extension (CFE) +# cfe-borrow v1.1 — Borrow objects from configuration into extension (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -652,18 +652,26 @@ def main(): form_version = src_form_el.get("version", "2.17") src_auto_cmd = None - src_child_items = None + form_props = [] + reached_visual = False for fc in src_form_el: if not isinstance(fc.tag, str): continue ln = localname(fc) if ln == "AutoCommandBar" and src_auto_cmd is None: + reached_visual = True src_auto_cmd = fc - elif ln == "ChildItems" and src_child_items is None: - src_child_items = fc + continue + if ln in ("ChildItems", "Events", "Attributes", "Commands", "Parameters"): + reached_visual = True + continue + if not reached_visual: + # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) + form_props.append(etree.tostring(fc, encoding="unicode")) ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') + # AutoCommandBar: keep ChildItems (buttons with CommandName→0), Autofill→false auto_cmd_xml = "" if src_auto_cmd is not None: auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") @@ -671,15 +679,151 @@ def main(): auto_cmd_xml = re.sub(r'[^<]*', '0', auto_cmd_xml) auto_cmd_xml = auto_cmd_xml.replace('true', 'false') + # ChildItems: copy full tree, clean up base-config references child_items_xml = "" + src_child_items = None + for fc in src_form_el: + if isinstance(fc.tag, str) and localname(fc) == "ChildItems": + src_child_items = fc + break + if src_child_items is not None: child_items_xml = etree.tostring(src_child_items, encoding="unicode") child_items_xml = ns_strip_pattern.sub("", child_items_xml) + # Replace all CommandName values with 0 child_items_xml = re.sub(r'[^<]*', '0', child_items_xml) - else: - child_items_xml = "" + # Strip DataPath + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip TitleDataPath + child_items_xml = re.sub(r'\s*[^<]*', '', child_items_xml) + # Strip TypeLink blocks with human-readable DataPath (Items.XXX) + child_items_xml = re.sub(r'\s*\s*Items\.[^<]*.*?', '', child_items_xml, flags=re.DOTALL) + # Strip element-level Events + child_items_xml = re.sub(r'\s*.*?', '', child_items_xml, flags=re.DOTALL) - # Extract source form opening tag + # Auto-borrow referenced CommonPictures + pic_refs = re.findall(r'CommonPicture\.(\w+)', child_items_xml) + referenced_pictures = {name: True for name in pic_refs} + + auto_borrowed_pics = [] + for pic_name in referenced_pictures: + if not test_object_borrowed("CommonPicture", pic_name): + pic_src_file = os.path.join(cfg_dir, "CommonPictures", f"{pic_name}.xml") + if os.path.isfile(pic_src_file): + src = read_source_object("CommonPicture", pic_name) + borrowed_xml = build_borrowed_object_xml("CommonPicture", pic_name, src["Uuid"], src["Properties"]) + target_dir = os.path.join(ext_dir, "CommonPictures") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{pic_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("CommonPicture", pic_name) + auto_borrowed_pics.append(pic_name) + info(f" Auto-borrowed: CommonPicture.{pic_name}") + else: + warn(f" CommonPicture.{pic_name} not found in source config — will strip from form") + + # Collect all borrowed CommonPictures for Picture stripping + borrowed_pic_set = set() + for co_child in child_objs_el: + if isinstance(co_child.tag, str) and localname(co_child) == "CommonPicture": + borrowed_pic_set.add((co_child.text or "").strip()) + + # Strip blocks referencing non-borrowed CommonPictures (reverse order) + pic_block_pattern = re.compile(r'\s*\s*CommonPicture\.(\w+).*?', re.DOTALL) + pic_matches = list(pic_block_pattern.finditer(child_items_xml)) + for pm in reversed(pic_matches): + cp_name = pm.group(1) + if cp_name not in borrowed_pic_set: + child_items_xml = child_items_xml[:pm.start()] + child_items_xml[pm.end():] + # Strip StdPicture blocks (except Print) + child_items_xml = re.sub(r'\s*\s*StdPicture\.(?!Print\b)\w+.*?', '', child_items_xml, flags=re.DOTALL) + + # Auto-borrow StyleItems referenced in ChildItems + referenced_styles = set() + for m in re.finditer(r'ref="style:(\w+)"[^>]*kind="StyleItem"', child_items_xml): + referenced_styles.add(m.group(1)) + for m in re.finditer(r'>style:(\w+)', child_items_xml): + referenced_styles.add(m.group(1)) + + for style_name in referenced_styles: + if not test_object_borrowed("StyleItem", style_name): + style_src_file = os.path.join(cfg_dir, "StyleItems", f"{style_name}.xml") + if os.path.isfile(style_src_file): + src = read_source_object("StyleItem", style_name) + borrowed_xml = build_borrowed_object_xml("StyleItem", style_name, src["Uuid"], src["Properties"]) + target_dir = os.path.join(ext_dir, "StyleItems") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{style_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("StyleItem", style_name) + info(f" Auto-borrowed: StyleItem.{style_name}") + else: + warn(f" StyleItem.{style_name} not found in source config") + + # Auto-borrow Enums + EnumValues referenced via DesignTimeRef + referenced_enum_values = {} # enum_name -> set of value_names + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', child_items_xml): + e_name, ev_name = m.group(1), m.group(2) + if e_name not in referenced_enum_values: + referenced_enum_values[e_name] = set() + referenced_enum_values[e_name].add(ev_name) + + for enum_name, needed_values in referenced_enum_values.items(): + if not test_object_borrowed("Enum", enum_name): + enum_src_file = os.path.join(cfg_dir, "Enums", f"{enum_name}.xml") + if os.path.isfile(enum_src_file): + # Read source Enum to find EnumValue UUIDs + src_enum_tree = etree.parse(enum_src_file, etree.XMLParser(remove_blank_text=False)) + src_enum_root = src_enum_tree.getroot() + src_enum_el = None + for cn in src_enum_root: + if isinstance(cn.tag, str): + src_enum_el = cn + break + + # Find needed EnumValues + ev_xmls = [] + for ev_node in src_enum_el.iter(): + if isinstance(ev_node.tag, str) and localname(ev_node) == "EnumValue": + ev_uuid = ev_node.get("uuid", "") + name_el = None + for props in ev_node: + if isinstance(props.tag, str) and localname(props) == "Properties": + for prop in props: + if isinstance(prop.tag, str) and localname(prop) == "Name": + name_el = prop + break + if name_el is not None and (name_el.text or "").strip() in needed_values: + new_ev_uuid = str(uuid.uuid4()) + ev_xmls.append( + f'\t\t\t\n' + f'\t\t\t\t\n' + f'\t\t\t\t\n' + f'\t\t\t\t\tAdopted\n' + f'\t\t\t\t\t{name_el.text.strip()}\n' + f'\t\t\t\t\t\n' + f'\t\t\t\t\t{ev_uuid}\n' + f'\t\t\t\t\n' + f'\t\t\t' + ) + + # Build borrowed Enum with EnumValues + src_obj = read_source_object("Enum", enum_name) + borrowed_xml = build_borrowed_object_xml("Enum", enum_name, src_obj["Uuid"], src_obj["Properties"]) + if ev_xmls: + ev_block = "\n".join(ev_xmls) + borrowed_xml = borrowed_xml.replace("", f"\n{ev_block}\n\t\t") + + target_dir = os.path.join(ext_dir, "Enums") + os.makedirs(target_dir, exist_ok=True) + target_file = os.path.join(target_dir, f"{enum_name}.xml") + save_text_bom(target_file, borrowed_xml) + add_to_child_objects("Enum", enum_name) + info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))") + else: + warn(f" Enum.{enum_name} not found in source config") + + # Extract the opening tag from source text xml_decl = '' form_tag = f'' m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content) @@ -696,14 +840,22 @@ def main(): parts.append(form_tag) parts.append("\r\n") + # Part 1: form properties + AutoCommandBar + ChildItems + for prop_xml in form_props: + prop_xml_clean = ns_strip_pattern.sub("", prop_xml) + parts.append(f"\t{prop_xml_clean}\r\n") if auto_cmd_xml: parts.append(f"\t{auto_cmd_xml}\r\n") - parts.append(f"\t{child_items_xml}\r\n") + if child_items_xml: + parts.append(f"\t{child_items_xml}\r\n") parts.append("\t\r\n") - # BaseForm + # BaseForm: same content, indented one more level parts.append(f'\t\r\n') + for prop_xml in form_props: + prop_xml_clean = ns_strip_pattern.sub("", prop_xml) + parts.append(f"\t\t{prop_xml_clean}\r\n") if auto_cmd_xml: ac_lines = auto_cmd_xml.split("\n") for li, line in enumerate(ac_lines): @@ -712,14 +864,14 @@ def main(): else: parts.append(f"\t{line}") parts.append("\r\n") - - ci_lines = child_items_xml.split("\n") - for li, line in enumerate(ci_lines): - if li == 0: - parts.append(f"\t\t{line}") - else: - parts.append(f"\t{line}") - parts.append("\r\n") + if child_items_xml: + ci_lines = child_items_xml.split("\n") + for li, line in enumerate(ci_lines): + if li == 0: + parts.append(f"\t\t{line}") + else: + parts.append(f"\t{line}") + parts.append("\r\n") parts.append("\t\t\r\n") parts.append("\t\r\n") diff --git a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py index 5901693e..490fb938 100644 --- a/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py +++ b/.claude/skills/cfe-patch-method/scripts/cfe-patch-method.py @@ -205,7 +205,7 @@ def main(): if os.path.isfile(bsl_file): # Append to existing file - with open(bsl_file, "r", encoding="utf-8-sig") as f: + with open(bsl_file, "r", encoding="utf-8-sig", newline="") as f: existing = f.read() separator = "\r\n" @@ -213,11 +213,11 @@ def main(): separator = "\r\n\r\n" new_content = existing + separator + bsl_text - with open(bsl_file, "w", encoding="utf-8-sig") as f: + with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f: f.write(new_content) print("[OK] \u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043f\u0435\u0440\u0435\u0445\u0432\u0430\u0442\u0447\u0438\u043a \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0444\u0430\u0439\u043b") # Добавлен перехватчик в существующий файл else: - with open(bsl_file, "w", encoding="utf-8-sig") as f: + with open(bsl_file, "w", encoding="utf-8-sig", newline="") as f: f.write(bsl_text) print("[OK] \u0421\u043e\u0437\u0434\u0430\u043d \u0444\u0430\u0439\u043b \u043c\u043e\u0434\u0443\u043b\u044f") # Создан файл модуля diff --git a/.claude/skills/cfe-validate/SKILL.md b/.claude/skills/cfe-validate/SKILL.md index 9bebc2e0..00d1e3b9 100644 --- a/.claude/skills/cfe-validate/SKILL.md +++ b/.claude/skills/cfe-validate/SKILL.md @@ -28,7 +28,7 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate.ps1 -ExtensionPath "src/Configuration.xml" ``` -## Проверки (9 шагов) +## Проверки (13 шагов) | # | Проверка | Уровень | |---|----------|---------| @@ -41,5 +41,9 @@ powershell.exe -NoProfile -File .claude/skills/cfe-validate/scripts/cfe-validate | 7 | Файлы языков существуют | WARN | | 8 | Каталоги объектов существуют | WARN | | 9 | Заимствованные объекты: ObjectBelonging=Adopted, ExtendedConfigurationObject UUID | ERROR/WARN | +| 10 | Sub-items: Attribute, TabularSection (InternalInfo + вложенные), EnumValue, Form-ссылки | ERROR | +| 11 | Заимствованные формы: метаданные, Form.xml, Module.bsl, BaseForm version | ERROR/WARN | +| 12 | Зависимости форм: CommonPicture, StyleItem (с whitelist платформенных), Enum DesignTimeRef | WARN | +| 13 | TypeLink: human-readable Items.* DataPath (должны быть удалены) | WARN | Exit code: 0 = OK, 1 = есть ошибки. По умолчанию краткий вывод. `-Detailed` для поштучной детализации. diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 index 76c44101..62d8c498 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.ps1 @@ -1,4 +1,4 @@ -# cfe-validate v1.1 — Validate 1C configuration extension structure (CFE) +# cfe-validate v1.2 — Validate 1C configuration extension structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -389,7 +389,7 @@ if (-not $childObjNode) { } else { $check5Ok = $true $totalCount = 0 - $typeCounts = @{} + $script:childObjectIndex = @{} $duplicates = @{} $typeFirstIndex = @{} $lastTypeOrder = -1 @@ -415,21 +415,21 @@ if (-not $childObjNode) { } } - if (-not $typeCounts.ContainsKey($typeName)) { $typeCounts[$typeName] = @{} } - if ($typeCounts[$typeName].ContainsKey($objNameVal)) { + if (-not $script:childObjectIndex.ContainsKey($typeName)) { $script:childObjectIndex[$typeName] = @{} } + if ($script:childObjectIndex[$typeName].ContainsKey($objNameVal)) { if (-not $duplicates.ContainsKey("$typeName.$objNameVal")) { Report-Error "5. Duplicate: $typeName.$objNameVal" $duplicates["$typeName.$objNameVal"] = $true $check5Ok = $false } } else { - $typeCounts[$typeName][$objNameVal] = $true + $script:childObjectIndex[$typeName][$objNameVal] = $true } $totalCount++ } - $typeCount = $typeCounts.Count + $typeCount = $script:childObjectIndex.Count if ($check5Ok) { $orderInfo = if ($orderOk) { ", order correct" } else { "" } Report-OK "5. ChildObjects: $typeCount types, $totalCount objects${orderInfo}" @@ -533,11 +533,46 @@ if ($childObjNode) { if ($script:stopped) { & $finalize; exit 1 } -# --- Check 9: Borrowed objects validation --- +# --- Check 9: Borrowed objects validation + Check 10: Sub-items --- +$script:enumValuesIndex = @{} +$script:formList = @() + +# Helper: validate a borrowed Attribute/EnumValue sub-item +function Validate-BorrowedSubItem { + param([string]$checkNum, [string]$context, [string]$subType, $subItem, $nsm) + $subProps = $subItem.SelectSingleNode("md:Properties", $nsm) + if (-not $subProps) { + Report-Error "${checkNum}. ${context}: ${subType} missing Properties" + return $false + } + $ok = $true + $subOb = $subProps.SelectSingleNode("md:ObjectBelonging", $nsm) + if (-not $subOb -or $subOb.InnerText -ne "Adopted") { + Report-Error "${checkNum}. ${context}: ${subType} ObjectBelonging must be 'Adopted'" + $ok = $false + } + $subName = $subProps.SelectSingleNode("md:Name", $nsm) + if (-not $subName -or -not $subName.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType} missing Name" + $ok = $false + } + $subExt = $subProps.SelectSingleNode("md:ExtendedConfigurationObject", $nsm) + if (-not $subExt -or -not $subExt.InnerText) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) missing ExtendedConfigurationObject" + $ok = $false + } elseif ($subExt.InnerText -notmatch $guidPattern) { + Report-Error "${checkNum}. ${context}: ${subType}.$($subName.InnerText) invalid ExtendedConfigurationObject" + $ok = $false + } + return $ok +} + if ($childObjNode) { $borrowedCount = 0 $borrowedOk = 0 $check9Ok = $true + $check10Ok = $true + $subItemCount = 0 foreach ($child in $childObjNode.ChildNodes) { if ($child.NodeType -ne 'Element') { continue } @@ -577,11 +612,11 @@ if ($childObjNode) { $objProps = $objEl.SelectSingleNode("md:Properties", $objNs) if (-not $objProps) { continue } + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- $obNode = $objProps.SelectSingleNode("md:ObjectBelonging", $objNs) if ($obNode -and $obNode.InnerText -eq "Adopted") { $borrowedCount++ - # Check ExtendedConfigurationObject $extObj = $objProps.SelectSingleNode("md:ExtendedConfigurationObject", $objNs) if (-not $extObj -or -not $extObj.InnerText) { Report-Error "9. Borrowed ${typeName}.${childName}: missing ExtendedConfigurationObject" @@ -594,6 +629,90 @@ if ($childObjNode) { } } + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + $objChildObjects = $objEl.SelectSingleNode("md:ChildObjects", $objNs) + if ($objChildObjects) { + $ctx = "${typeName}.${childName}" + foreach ($subItem in $objChildObjects.ChildNodes) { + if ($subItem.NodeType -ne 'Element') { continue } + $subType = $subItem.LocalName + + if ($subType -eq "Attribute") { + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "Attribute" $subItem $objNs)) { + $check10Ok = $false + } + } + elseif ($subType -eq "TabularSection") { + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" $ctx "TabularSection" $subItem $objNs)) { + $check10Ok = $false + } else { + # Check InternalInfo GeneratedTypes + $tsInfo = $subItem.SelectSingleNode("md:InternalInfo", $objNs) + $tsName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + $tsLabel = if ($tsName) { $tsName.InnerText } else { "?" } + if (-not $tsInfo) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing InternalInfo" + $check10Ok = $false + } else { + $gtNodes = $tsInfo.SelectNodes("xr:GeneratedType", $objNs) + $hasTSCat = $false; $hasTSRCat = $false + foreach ($gt in $gtNodes) { + $cat = $gt.GetAttribute("category") + if ($cat -eq "TabularSection") { $hasTSCat = $true } + if ($cat -eq "TabularSectionRow") { $hasTSRCat = $true } + } + if (-not $hasTSCat -or -not $hasTSRCat) { + Report-Error "10. ${ctx}: TabularSection.${tsLabel} missing GeneratedType (need TabularSection + TabularSectionRow)" + $check10Ok = $false + } + } + # Recurse into TS ChildObjects/Attribute + $tsChildObjs = $subItem.SelectSingleNode("md:ChildObjects", $objNs) + if ($tsChildObjs) { + foreach ($tsAttr in $tsChildObjs.ChildNodes) { + if ($tsAttr.NodeType -ne 'Element' -or $tsAttr.LocalName -ne "Attribute") { continue } + $subItemCount++ + if (-not (Validate-BorrowedSubItem "10" "${ctx}.ТЧ.${tsLabel}" "Attribute" $tsAttr $objNs)) { + $check10Ok = $false + } + } + } + } + } + elseif ($subType -eq "EnumValue" -and $typeName -eq "Enum") { + $subItemCount++ + if (Validate-BorrowedSubItem "10" $ctx "EnumValue" $subItem $objNs) { + $evName = $subItem.SelectSingleNode("md:Properties/md:Name", $objNs) + if ($evName -and $evName.InnerText) { + if (-not $script:enumValuesIndex.ContainsKey($childName)) { + $script:enumValuesIndex[$childName] = @{} + } + $script:enumValuesIndex[$childName][$evName.InnerText] = $true + } + } else { + $check10Ok = $false + } + } + elseif ($subType -eq "Form") { + $formName = $subItem.InnerText + if ($formName) { + $formMetaFile = Join-Path (Join-Path (Join-Path (Join-Path $configDir $dirName) $childName) "Forms") "${formName}.xml" + if (-not (Test-Path $formMetaFile)) { + Report-Error "10. ${ctx}: Form.${formName} metadata file missing" + $check10Ok = $false + } + $script:formList += @{ + TypeName = $typeName; ObjName = $childName + FormName = $formName; DirName = $dirName + } + $subItemCount++ + } + } + } + } + if ($script:stopped) { break } } @@ -602,6 +721,197 @@ if ($childObjNode) { } elseif ($check9Ok) { Report-OK "9. Borrowed objects: $borrowedOk/$borrowedCount validated" } + + if ($subItemCount -eq 0) { + Report-OK "10. Sub-items: none found" + } elseif ($check10Ok) { + Report-OK "10. Sub-items: $subItemCount validated (Attributes, TabularSections, EnumValues, Forms)" + } +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 11: Borrowed form structure --- +$script:borrowedFormsWithTree = @() +$check11Ok = $true +$formCount = 0 + +foreach ($fi in $script:formList) { + $formCount++ + $formBase = Join-Path (Join-Path (Join-Path (Join-Path $configDir $fi.DirName) $fi.ObjName) "Forms") $fi.FormName + $formMetaFile = Join-Path (Split-Path $formBase -Parent) "$($fi.FormName).xml" + $formXmlFile = Join-Path (Join-Path $formBase "Ext") "Form.xml" + $moduleBslFile = Join-Path (Join-Path (Join-Path $formBase "Ext") "Form") "Module.bsl" + $ctx = "$($fi.TypeName).$($fi.ObjName).Form.$($fi.FormName)" + + # Validate form metadata XML + if (Test-Path $formMetaFile) { + try { + $fmDoc = New-Object System.Xml.XmlDocument + $fmDoc.PreserveWhitespace = $false + $fmDoc.Load($formMetaFile) + $fmNs = New-Object System.Xml.XmlNamespaceManager($fmDoc.NameTable) + $fmNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") + + $fmEl = $null + foreach ($c in $fmDoc.DocumentElement.ChildNodes) { + if ($c.NodeType -eq 'Element') { $fmEl = $c; break } + } + if ($fmEl) { + $fmProps = $fmEl.SelectSingleNode("md:Properties", $fmNs) + if ($fmProps) { + $fmOb = $fmProps.SelectSingleNode("md:ObjectBelonging", $fmNs) + $isBorrowed = $fmOb -and $fmOb.InnerText -eq "Adopted" + if ($isBorrowed) { + $fmExt = $fmProps.SelectSingleNode("md:ExtendedConfigurationObject", $fmNs) + if (-not $fmExt -or $fmExt.InnerText -notmatch $guidPattern) { + Report-Error "11. ${ctx}: invalid/missing ExtendedConfigurationObject" + $check11Ok = $false + } + } + $fmType = $fmProps.SelectSingleNode("md:FormType", $fmNs) + if ($fmType -and $fmType.InnerText -ne "Managed") { + Report-Error "11. ${ctx}: FormType must be 'Managed', got '$($fmType.InnerText)'" + $check11Ok = $false + } + } + } + } catch { + Report-Warn "11. ${ctx}: Cannot parse metadata: $($_.Exception.Message)" + } + } + + # Form.xml must exist + if (-not (Test-Path $formXmlFile)) { + Report-Error "11. ${ctx}: Ext/Form.xml missing" + $check11Ok = $false + continue + } + + # Module.bsl should exist + if (-not (Test-Path $moduleBslFile)) { + Report-Warn "11. ${ctx}: Ext/Form/Module.bsl missing" + } + + # Read Form.xml as raw text for BaseForm checks + $formRawText = [System.IO.File]::ReadAllText($formXmlFile, [System.Text.Encoding]::UTF8) + + if ($formRawText -match ']+version=') { + Report-Warn "11. ${ctx}: missing version attribute" + } + + $script:borrowedFormsWithTree += @{ + Path = $formXmlFile; RawText = $formRawText; Context = $ctx + } + } +} + +if ($formCount -eq 0) { + Report-OK "11. Borrowed forms: none found" +} elseif ($check11Ok) { + $bfCount = $script:borrowedFormsWithTree.Count + Report-OK "11. Borrowed forms: $formCount validated ($bfCount with BaseForm)" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 12: Form dependency references --- +$platformStyleItems = @{ + "TableHeaderBackColor"=$true; "AccentColor"=$true; "NormalTextFont"=$true + "FormBackColor"=$true; "ToolTipBackColor"=$true; "BorderColor"=$true + "FieldBackColor"=$true; "FieldTextColor"=$true; "ButtonBackColor"=$true + "ButtonTextColor"=$true; "AlternateRowColor"=$true; "SpecialTextColor"=$true + "TextFont"=$true; "ImportantColor"=$true; "FormTextColor"=$true + "SmallTextFont"=$true; "ExtraLargeTextFont"=$true; "LargeTextFont"=$true + "NormalTextColor"=$true; "GroupHeaderBackColor"=$true; "GroupHeaderFont"=$true + "ErrorColor"=$true; "SuccessColor"=$true; "WarningColor"=$true +} +$check12Ok = $true +$depCheckCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $missingItems = @() + + # CommonPicture references + $cpRefs = @{} + foreach ($m in [regex]::Matches($raw, 'CommonPicture\.(\w+)')) { + $cpRefs[$m.Groups[1].Value] = $true + } + $cpIndex = $script:childObjectIndex["CommonPicture"] + foreach ($cpName in $cpRefs.Keys) { + $depCheckCount++ + if (-not $cpIndex -or -not $cpIndex.ContainsKey($cpName)) { + $missingItems += "CommonPicture.${cpName}" + } + } + + # StyleItem references + $siRefs = @{} + foreach ($m in [regex]::Matches($raw, 'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)')) { + $siRefs[$m.Groups[1].Value] = $true + } + $siIndex = $script:childObjectIndex["StyleItem"] + foreach ($siName in $siRefs.Keys) { + $depCheckCount++ + if ($platformStyleItems.ContainsKey($siName)) { continue } + if (-not $siIndex -or -not $siIndex.ContainsKey($siName)) { + $missingItems += "StyleItem.${siName}" + } + } + + # Enum DesignTimeRef references + $enumRefs = @{} + foreach ($m in [regex]::Matches($raw, 'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)')) { + $eKey = "$($m.Groups[1].Value).$($m.Groups[2].Value)" + $enumRefs[$eKey] = @{ Enum = $m.Groups[1].Value; Value = $m.Groups[2].Value } + } + $eIndex = $script:childObjectIndex["Enum"] + foreach ($entry in $enumRefs.Values) { + $depCheckCount++ + if (-not $eIndex -or -not $eIndex.ContainsKey($entry.Enum)) { + $missingItems += "Enum.$($entry.Enum)" + } elseif (-not $script:enumValuesIndex.ContainsKey($entry.Enum) -or -not $script:enumValuesIndex[$entry.Enum].ContainsKey($entry.Value)) { + $missingItems += "Enum.$($entry.Enum).EnumValue.$($entry.Value)" + } + } + + foreach ($mi in $missingItems) { + Report-Warn "12. ${ctx}: references ${mi} not borrowed in extension" + $check12Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "12. Form dependencies: no borrowed forms with tree" +} elseif ($check12Ok) { + Report-OK "12. Form dependencies: $depCheckCount references checked" +} + +if ($script:stopped) { & $finalize; exit 1 } + +# --- Check 13: TypeLink with human-readable paths --- +$check13Ok = $true +$typeLinkCount = 0 + +foreach ($bf in $script:borrowedFormsWithTree) { + $raw = $bf.RawText + $ctx = $bf.Context + $matches = [regex]::Matches($raw, '\s*Items\.[^<]*') + if ($matches.Count -gt 0) { + $typeLinkCount += $matches.Count + Report-Warn "13. ${ctx}: $($matches.Count) TypeLink(s) with human-readable Items.* DataPath (should be stripped)" + $check13Ok = $false + } +} + +if ($script:borrowedFormsWithTree.Count -eq 0) { + Report-OK "13. TypeLink: no borrowed forms with tree" +} elseif ($check13Ok) { + Report-OK "13. TypeLink: clean" } # --- Final output --- diff --git a/.claude/skills/cfe-validate/scripts/cfe-validate.py b/.claude/skills/cfe-validate/scripts/cfe-validate.py index 40d24db0..fad924d0 100644 --- a/.claude/skills/cfe-validate/scripts/cfe-validate.py +++ b/.claude/skills/cfe-validate/scripts/cfe-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cfe-validate v1.1 — Validate 1C configuration extension XML structure (CFE) +# cfe-validate v1.2 — Validate 1C configuration extension XML structure (CFE) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" import sys, os, argparse, re @@ -392,7 +392,7 @@ def main(): else: check5_ok = True total_count = 0 - type_counts = {} + child_object_index = {} duplicates = {} type_first_index = {} last_type_order = -1 @@ -420,20 +420,20 @@ def main(): order_ok = False last_type_order = type_idx - if type_name not in type_counts: - type_counts[type_name] = {} - if obj_name_val in type_counts[type_name]: + if type_name not in child_object_index: + child_object_index[type_name] = {} + if obj_name_val in child_object_index[type_name]: dup_key = f'{type_name}.{obj_name_val}' if dup_key not in duplicates: r.error(f'5. Duplicate: {dup_key}') duplicates[dup_key] = True check5_ok = False else: - type_counts[type_name][obj_name_val] = True + child_object_index[type_name][obj_name_val] = True total_count += 1 - type_count = len(type_counts) + type_count = len(child_object_index) if check5_ok: order_info = ', order correct' if order_ok else '' r.ok(f'5. ChildObjects: {type_count} types, {total_count} objects{order_info}') @@ -529,11 +529,43 @@ def main(): r.finalize(out_file) sys.exit(1) - # --- Check 9: Borrowed objects validation --- + # --- Check 9: Borrowed objects + Check 10: Sub-items --- + MD = NS['md'] + XR = NS['xr'] + enum_values_index = {} + form_list = [] + + def validate_borrowed_sub_item(check_num, context, sub_type, sub_item): + """Validate a borrowed Attribute/EnumValue/TabularSection sub-item.""" + sub_props = sub_item.find(f'{{{MD}}}Properties') + if sub_props is None: + r.error(f'{check_num}. {context}: {sub_type} missing Properties') + return False + ok = True + sub_ob = sub_props.find(f'{{{MD}}}ObjectBelonging') + if sub_ob is None or (sub_ob.text or '') != 'Adopted': + r.error(f"{check_num}. {context}: {sub_type} ObjectBelonging must be 'Adopted'") + ok = False + sub_name = sub_props.find(f'{{{MD}}}Name') + if sub_name is None or not (sub_name.text or ''): + r.error(f'{check_num}. {context}: {sub_type} missing Name') + ok = False + sub_ext = sub_props.find(f'{{{MD}}}ExtendedConfigurationObject') + sub_name_val = (sub_name.text or '') if sub_name is not None else '?' + if sub_ext is None or not (sub_ext.text or ''): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} missing ExtendedConfigurationObject') + ok = False + elif not GUID_PATTERN.match(sub_ext.text): + r.error(f'{check_num}. {context}: {sub_type}.{sub_name_val} invalid ExtendedConfigurationObject') + ok = False + return ok + if child_obj_node is not None: borrowed_count = 0 borrowed_ok_count = 0 check9_ok = True + check10_ok = True + sub_item_count = 0 for child in child_obj_node: if not isinstance(child.tag, str): @@ -552,7 +584,6 @@ def main(): continue # Parse object XML - obj_doc = None try: obj_parser = etree.XMLParser(remove_blank_text=False) obj_doc = etree.parse(obj_file, obj_parser) @@ -571,16 +602,16 @@ def main(): if obj_el is None: continue - obj_props = obj_el.find(f'{{{NS["md"]}}}Properties') + obj_props = obj_el.find(f'{{{MD}}}Properties') if obj_props is None: continue - ob_node = obj_props.find(f'{{{NS["md"]}}}ObjectBelonging') + # --- Check 9: ObjectBelonging + ExtendedConfigurationObject --- + ob_node = obj_props.find(f'{{{MD}}}ObjectBelonging') if ob_node is not None and (ob_node.text or '') == 'Adopted': borrowed_count += 1 - # Check ExtendedConfigurationObject - ext_obj = obj_props.find(f'{{{NS["md"]}}}ExtendedConfigurationObject') + ext_obj = obj_props.find(f'{{{MD}}}ExtendedConfigurationObject') if ext_obj is None or not (ext_obj.text or ''): r.error(f'9. Borrowed {type_name}.{child_name}: missing ExtendedConfigurationObject') check9_ok = False @@ -590,14 +621,248 @@ def main(): else: borrowed_ok_count += 1 + # --- Check 10: Sub-items (Attribute, TabularSection, EnumValue, Form) --- + obj_child_objects = obj_el.find(f'{{{MD}}}ChildObjects') + if obj_child_objects is not None: + ctx = f'{type_name}.{child_name}' + for sub_item in obj_child_objects: + if not isinstance(sub_item.tag, str): + continue + sub_type = etree.QName(sub_item.tag).localname + + if sub_type == 'Attribute': + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'Attribute', sub_item): + check10_ok = False + + elif sub_type == 'TabularSection': + sub_item_count += 1 + if not validate_borrowed_sub_item('10', ctx, 'TabularSection', sub_item): + check10_ok = False + else: + # Check InternalInfo GeneratedTypes + ts_info = sub_item.find(f'{{{MD}}}InternalInfo') + ts_name_el = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + ts_label = (ts_name_el.text or '?') if ts_name_el is not None else '?' + if ts_info is None: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing InternalInfo') + check10_ok = False + else: + gt_nodes = ts_info.findall(f'{{{XR}}}GeneratedType') + has_ts = any(gt.get('category') == 'TabularSection' for gt in gt_nodes) + has_tsr = any(gt.get('category') == 'TabularSectionRow' for gt in gt_nodes) + if not has_ts or not has_tsr: + r.error(f'10. {ctx}: TabularSection.{ts_label} missing GeneratedType (need TabularSection + TabularSectionRow)') + check10_ok = False + # Recurse into TS ChildObjects/Attribute + ts_child_objs = sub_item.find(f'{{{MD}}}ChildObjects') + if ts_child_objs is not None: + for ts_attr in ts_child_objs: + if not isinstance(ts_attr.tag, str): + continue + if etree.QName(ts_attr.tag).localname != 'Attribute': + continue + sub_item_count += 1 + if not validate_borrowed_sub_item('10', f'{ctx}.ТЧ.{ts_label}', 'Attribute', ts_attr): + check10_ok = False + + elif sub_type == 'EnumValue' and type_name == 'Enum': + sub_item_count += 1 + if validate_borrowed_sub_item('10', ctx, 'EnumValue', sub_item): + ev_name = sub_item.find(f'{{{MD}}}Properties/{{{MD}}}Name') + if ev_name is not None and (ev_name.text or ''): + if child_name not in enum_values_index: + enum_values_index[child_name] = {} + enum_values_index[child_name][ev_name.text] = True + else: + check10_ok = False + + elif sub_type == 'Form': + form_name = sub_item.text or '' + if form_name: + form_meta_file = os.path.join(config_dir, dir_name, child_name, 'Forms', form_name + '.xml') + if not os.path.exists(form_meta_file): + r.error(f'10. {ctx}: Form.{form_name} metadata file missing') + check10_ok = False + form_list.append({ + 'TypeName': type_name, 'ObjName': child_name, + 'FormName': form_name, 'DirName': dir_name, + }) + sub_item_count += 1 + if r.stopped: break if borrowed_count == 0: - pass # no borrowed objects + r.ok('9. Borrowed objects: none found') elif check9_ok: r.ok(f'9. Borrowed objects: {borrowed_ok_count}/{borrowed_count} validated') + if sub_item_count == 0: + r.ok('10. Sub-items: none found') + elif check10_ok: + r.ok(f'10. Sub-items: {sub_item_count} validated (Attributes, TabularSections, EnumValues, Forms)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 11: Borrowed form structure --- + borrowed_forms_with_tree = [] + check11_ok = True + form_count = 0 + + for fi in form_list: + form_count += 1 + form_base = os.path.join(config_dir, fi['DirName'], fi['ObjName'], 'Forms', fi['FormName']) + form_meta_file = os.path.join(os.path.dirname(form_base), fi['FormName'] + '.xml') + form_xml_file = os.path.join(form_base, 'Ext', 'Form.xml') + module_bsl_file = os.path.join(form_base, 'Ext', 'Form', 'Module.bsl') + ctx = f"{fi['TypeName']}.{fi['ObjName']}.Form.{fi['FormName']}" + + # Validate form metadata XML + if os.path.exists(form_meta_file): + try: + fm_doc = etree.parse(form_meta_file, etree.XMLParser(remove_blank_text=False)) + fm_root = fm_doc.getroot() + fm_el = None + for c in fm_root: + if isinstance(c.tag, str): + fm_el = c + break + if fm_el is not None: + fm_props = fm_el.find(f'{{{MD}}}Properties') + if fm_props is not None: + fm_ob = fm_props.find(f'{{{MD}}}ObjectBelonging') + is_borrowed = fm_ob is not None and (fm_ob.text or '') == 'Adopted' + if is_borrowed: + fm_ext = fm_props.find(f'{{{MD}}}ExtendedConfigurationObject') + if fm_ext is None or not (fm_ext.text or '') or not GUID_PATTERN.match(fm_ext.text or ''): + r.error(f'11. {ctx}: invalid/missing ExtendedConfigurationObject') + check11_ok = False + fm_type = fm_props.find(f'{{{MD}}}FormType') + if fm_type is not None and (fm_type.text or '') != 'Managed': + r.error(f"11. {ctx}: FormType must be 'Managed', got '{fm_type.text}'") + check11_ok = False + except etree.XMLSyntaxError as e: + r.warn(f'11. {ctx}: Cannot parse metadata: {e}') + + # Form.xml must exist + if not os.path.exists(form_xml_file): + r.error(f'11. {ctx}: Ext/Form.xml missing') + check11_ok = False + continue + + # Module.bsl should exist + if not os.path.exists(module_bsl_file): + r.warn(f'11. {ctx}: Ext/Form/Module.bsl missing') + + # Read Form.xml as raw text for BaseForm checks + with open(form_xml_file, 'r', encoding='utf-8-sig') as f: + form_raw_text = f.read() + + if ']+version=', form_raw_text): + r.warn(f'11. {ctx}: missing version attribute') + borrowed_forms_with_tree.append({ + 'Path': form_xml_file, 'RawText': form_raw_text, 'Context': ctx, + }) + + if form_count == 0: + r.ok('11. Borrowed forms: none found') + elif check11_ok: + bf_count = len(borrowed_forms_with_tree) + r.ok(f'11. Borrowed forms: {form_count} validated ({bf_count} with BaseForm)') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 12: Form dependency references --- + PLATFORM_STYLE_ITEMS = { + 'TableHeaderBackColor', 'AccentColor', 'NormalTextFont', + 'FormBackColor', 'ToolTipBackColor', 'BorderColor', + 'FieldBackColor', 'FieldTextColor', 'ButtonBackColor', + 'ButtonTextColor', 'AlternateRowColor', 'SpecialTextColor', + 'TextFont', 'ImportantColor', 'FormTextColor', + 'SmallTextFont', 'ExtraLargeTextFont', 'LargeTextFont', + 'NormalTextColor', 'GroupHeaderBackColor', 'GroupHeaderFont', + 'ErrorColor', 'SuccessColor', 'WarningColor', + } + check12_ok = True + dep_check_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + missing_items = [] + + # CommonPicture references + cp_refs = {} + for m in re.finditer(r'CommonPicture\.(\w+)', raw): + cp_refs[m.group(1)] = True + cp_index = child_object_index.get('CommonPicture', {}) + for cp_name in cp_refs: + dep_check_count += 1 + if cp_name not in cp_index: + missing_items.append(f'CommonPicture.{cp_name}') + + # StyleItem references + si_refs = {} + for m in re.finditer(r'style:([A-Za-z\u0410-\u044F\u0401\u0451_][A-Za-z0-9\u0410-\u044F\u0401\u0451_]*)', raw): + si_refs[m.group(1)] = True + si_index = child_object_index.get('StyleItem', {}) + for si_name in si_refs: + dep_check_count += 1 + if si_name in PLATFORM_STYLE_ITEMS: + continue + if si_name not in si_index: + missing_items.append(f'StyleItem.{si_name}') + + # Enum DesignTimeRef references + enum_refs = {} + for m in re.finditer(r'xr:DesignTimeRef">Enum\.(\w+)\.EnumValue\.(\w+)', raw): + e_key = f'{m.group(1)}.{m.group(2)}' + enum_refs[e_key] = {'Enum': m.group(1), 'Value': m.group(2)} + e_index = child_object_index.get('Enum', {}) + for entry in enum_refs.values(): + dep_check_count += 1 + if entry['Enum'] not in e_index: + missing_items.append(f"Enum.{entry['Enum']}") + elif entry['Enum'] not in enum_values_index or entry['Value'] not in enum_values_index.get(entry['Enum'], {}): + missing_items.append(f"Enum.{entry['Enum']}.EnumValue.{entry['Value']}") + + for mi in missing_items: + r.warn(f'12. {ctx}: references {mi} not borrowed in extension') + check12_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('12. Form dependencies: no borrowed forms with tree') + elif check12_ok: + r.ok(f'12. Form dependencies: {dep_check_count} references checked') + + if r.stopped: + r.finalize(out_file) + sys.exit(1) + + # --- Check 13: TypeLink with human-readable paths --- + check13_ok = True + type_link_count = 0 + + for bf in borrowed_forms_with_tree: + raw = bf['RawText'] + ctx = bf['Context'] + matches = re.findall(r'\s*Items\.[^<]*', raw) + if matches: + type_link_count += len(matches) + r.warn(f'13. {ctx}: {len(matches)} TypeLink(s) with human-readable Items.* DataPath (should be stripped)') + check13_ok = False + + if len(borrowed_forms_with_tree) == 0: + r.ok('13. TypeLink: no borrowed forms with tree') + elif check13_ok: + r.ok('13. TypeLink: clean') + # --- Final output --- r.finalize(out_file) sys.exit(1 if r.errors > 0 else 0) diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index 12b4d053..e80afe6e 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1078,11 +1078,18 @@ if ($def.formEvents -and $def.formEvents.Count -gt 0) { $eventsSection = $xmlDoc.CreateElement("Events", $formNs) $insertAfter = $root.SelectSingleNode("f:AutoCommandBar", $nsMgr) if ($insertAfter) { - $refNode = $insertAfter - $ws = $xmlDoc.CreateWhitespace("`r`n`t") - # Insert before the AutoCommandBar (Events come before AutoCommandBar in 1C) - $root.InsertBefore($ws, $refNode) | Out-Null - $root.InsertBefore($eventsSection, $refNode) | Out-Null + # Insert after AutoCommandBar (Events come after AutoCommandBar in 1C) + $ws1 = $xmlDoc.CreateWhitespace("`r`n`t") + $ws2 = $xmlDoc.CreateWhitespace("`r`n`t") + if ($insertAfter.NextSibling) { + $root.InsertBefore($ws1, $insertAfter.NextSibling) | Out-Null + $root.InsertBefore($eventsSection, $ws1) | Out-Null + $root.InsertBefore($ws2, $eventsSection) | Out-Null + } else { + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n`t")) | Out-Null + $root.AppendChild($eventsSection) | Out-Null + $root.AppendChild($xmlDoc.CreateWhitespace("`r`n")) | Out-Null + } } else { $firstChild = $root.FirstChild if ($firstChild) { diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py index a9e3cd24..4aced255 100644 --- a/.claude/skills/form-edit/scripts/form-edit.py +++ b/.claude/skills/form-edit/scripts/form-edit.py @@ -1162,7 +1162,15 @@ form_events_list = defn.get("formEvents") or [] if form_events_list: events_section = root.find("f:Events", NS) if events_section is None: - events_section = etree.SubElement(root, f"{{{FORM_NS}}}Events") + events_section = etree.Element(f"{{{FORM_NS}}}Events") + # Insert after AutoCommandBar (Events come after AutoCommandBar in 1C) + acb_node = root.find("f:AutoCommandBar", NS) + if acb_node is not None: + acb_idx = list(root).index(acb_node) + acb_node.tail = (acb_node.tail or "") + "\r\n\t" + root.insert(acb_idx + 1, events_section) + else: + root.append(events_section) evt_child_indent = get_child_indent(events_section) if not evt_child_indent: diff --git a/docs/1c-extension-spec.md b/docs/1c-extension-spec.md index 465ee2f4..aaa2cfb2 100644 --- a/docs/1c-extension-spec.md +++ b/docs/1c-extension-spec.md @@ -356,71 +356,84 @@ Enums/ # Перечисления #### 5.4.2. Структура Form.xml заимствованной формы -Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Обе части содержат **только визуальные элементы** — атрибуты, события, параметры и команды базовой конфигурации **НЕ включаются**. +Form.xml заимствованной формы — **двухчастный файл**: Part 1 (результирующая форма) и BaseForm (исходная форма). Существуют **два варианта** в зависимости от наличия модификаций модуля формы. + +##### Вариант A — Минимальная форма (чистое заимствование) + +Когда форма заимствована без модификации модуля — Form.xml содержит **только свойства формы**, AutoCommandBar без кнопок и пустые Attributes. **Нет ChildItems**. Обе секции (Part 1 и BaseForm) идентичны. ```xml - - + false + CurrentOrLast + - - - - - - + false - - - - - - - Расш1_ПриСозданииПосле - - - - - Расш1_НоваяКомандаВместо - - + + - + false + CurrentOrLast + - - - - + false - - - - - + - ``` -**Ключевые правила:** +##### Вариант B — Полная форма (заимствование процедуры модуля через `ИзменениеИКонтроль`) -1. **Часть 1** (до ``) — **результирующая форма**. Содержит визуальные элементы (AutoCommandBar + ChildItems) из базовой конфигурации плюс элементы расширения. Атрибуты базовой конфигурации (DynamicList, QueryText и др.) **не включаются** — только реквизиты расширения (id ≥ 1000000) или пустой ``. Events и Commands — только добавленные расширением (с `callType`). +Когда в расширении заимствуется процедура из модуля формы, Конфигуратор выгружает **полное дерево ChildItems** с применением правил очистки. -2. **Часть 2** (``) — **визуальный снимок исходной формы**. Содержит только AutoCommandBar + ChildItems + пустой ``. НЕ содержит Events, Commands, Parameters. Все `` в кнопках заменены на `0`. Платформа использует BaseForm для контроля совместимости при обновлении конфигурации. +```xml +
+ false + + + false + + + + + + + -3. **Правило `0`**: во всех кнопках базовой формы (как в Part 1, так и в BaseForm) значение `` заменяется на `0`. Ссылки на команды конфигурации не сохраняются. Только кнопки, добавленные расширением, сохраняют ссылку на команду (напр. `Form.Command.XXX`). + + + + +``` -4. Элемент `` всегда идёт **последним** в `
` и имеет атрибут `version`. +**Ключевые правила (для обоих вариантов):** + +1. **Свойства формы** — элементы между `` и `` (напр. `AutoTitle`, `AutoTime`, `UsePostingMode`, `RepostOnWrite`, `WindowOpeningMode`, `Customizable`, `CommandBarLocation`) копируются из исходной формы в обе секции. + +2. **AutoCommandBar** — присутствует всегда с `id="-1"`, но без `` (кнопки удаляются). `` = `false`. + +3. **Attributes** — пустой `` в обеих секциях. Атрибуты базовой конфигурации **не включаются**. Реквизиты расширения (id ≥ 1000000) добавляются только в Part 1. + +4. **BaseForm** — последний элемент в ``, атрибут `version`. В BaseForm **нет** Events, Commands, Parameters. + +5. **DataPath: удаление** (вариант B) — все `` из базовых элементов удаляются в обеих секциях. DataPath ссылается на реквизиты, не включённые в расширение. + +6. **TitleDataPath: удаление** (вариант B) — все `` удаляются (напр. `Объект.Товары.RowsCount` — путь недействителен без базовых атрибутов). + +7. **TypeLink: удаление** (вариант B) — блоки `` с `Items.*` удаляются (человекочитаемые пути, которые нельзя преобразовать в UUID-формат Конфигуратора). + +8. **Events элементов: удаление** (вариант B) — все `` внутри визуальных элементов удаляются в обеих секциях. Обработчики расширения добавляются через `elementEvents` в Part 1 с `callType`. + +9. **Picture stripping** (вариант B) — блоки `` с `CommonPicture.XXX` удаляются, если `CommonPicture.XXX` **не заимствован** в расширение. Сам элемент PictureDecoration остаётся, только `` убирается. `StdPicture.Print` сохраняется, остальные StdPicture удаляются. + +10. **Авто-заимствование CommonPictures** — при заимствовании формы автоматически заимствуются все CommonPictures, на которые ссылаются элементы формы. + +11. **Авто-заимствование StyleItems** — элементы формы ссылаются на StyleItems через `` и `style:XXX`. Все такие StyleItems должны быть заимствованы. Стандартные стили (NormalTextFont, AccentColor, FormBackColor и др.) не имеют файлов и автоматически пропускаются. + +12. **Авто-заимствование Enums + EnumValues** — `` могут содержать `Enum.XXX.EnumValue.YYY`. Перечисление `Enum.XXX` заимствуется вместе с конкретными `EnumValue` (borrowed с `ExtendedConfigurationObject` указывающим на UUID оригинального значения). #### 5.4.3. Нумерация ID элементов