From 96660f4d9d0ab4b61d2573bf2bbd2b894b9c08cb Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sat, 20 Jun 2026 13:57:31 +0300 Subject: [PATCH] =?UTF-8?q?feat(cf-info,meta-info):=20=D0=B2=D1=8B=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=20=D1=81=D0=BE=D1=81=D1=82=D0=BE=D1=8F=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=B8=D0=B7=20ParentConfigurations.bin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Декодер Ext/ParentConfigurations.bin (нативная копия в каждом скрипте, без общего модуля — по конвенции автономности навыков; single-pass, не падает на битом/пустом файле). cf-info (v1.3): блок «Поддержка:» в overview/full — на поддержке / возможность изменения вкл-выкл / счётчики на замке/редактируется/снято (только при G=0) / снята полностью / расширение (CFE). При G=1 показывает read-only без вводящих в заблуждение счётчиков; тяжёлый скан 7.4МБ пропускается, когда не нужен. При K>1 перечисляет конфигурации поставщика. meta-info (v1.3): строка «Поддержка:» под заголовком объекта — walk-up к корню конфигурации, статус с учётом G-vs-f1 и консервативной свёртки min(f1) по блокам поставщиков. Для на-замке/read-only — короткое последствие+действие (cfe-*), для остальных терсно. Проверено на корпусе и размеченных дампах (acc G=1, erp снято, ЧастьОбъектов, Корень, НесколькоПоддержек K=7 мультивендор, CFE) — оба порта байт-в-байт. Формат: docs/1c-support-state-spec.md. Тесты: cf-info 7/7, meta-info 17/17 на PowerShell и Python. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/cf-info/scripts/cf-info.ps1 | 77 ++++++++++++++++++- .claude/skills/cf-info/scripts/cf-info.py | 73 +++++++++++++++++- .../skills/meta-info/scripts/meta-info.ps1 | 48 +++++++++++- .claude/skills/meta-info/scripts/meta-info.py | 56 +++++++++++++- .../cases/cf-info/real-acc-support.json | 5 ++ .../cases/cf-info/real-erp-support.json | 5 ++ .../cases/meta-info/real-acc-support.json | 6 ++ .../cases/meta-info/real-erp-support.json | 6 ++ 8 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 tests/skills/cases/cf-info/real-acc-support.json create mode 100644 tests/skills/cases/cf-info/real-erp-support.json create mode 100644 tests/skills/cases/meta-info/real-acc-support.json create mode 100644 tests/skills/cases/meta-info/real-erp-support.json diff --git a/.claude/skills/cf-info/scripts/cf-info.ps1 b/.claude/skills/cf-info/scripts/cf-info.ps1 index d85df9aa..c9ce3072 100644 --- a/.claude/skills/cf-info/scripts/cf-info.ps1 +++ b/.claude/skills/cf-info/scripts/cf-info.ps1 @@ -1,4 +1,4 @@ -# cf-info v1.2 — Compact summary of 1C configuration root +# cf-info v1.3 — Compact summary of 1C configuration root # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath, @@ -218,6 +218,78 @@ function Get-HomePageLayout { $script:homePage = Get-HomePageLayout +# --- Support state (Ext/ParentConfigurations.bin) --- +# Decodes the 1C support-state file. See docs/1c-support-state-spec.md. +# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed', +# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}), +# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each +# vendor block separately); only computed when G=0. +function Read-SupportState([string]$binPath) { + try { + if (-not (Test-Path $binPath)) { return @{ State = 'absent' } } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return @{ State = 'removed' } } + $startIdx = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return $null } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return @{ State = 'removed' } } + # Vendor descriptors: ...,"ver","vendor","name",count, + $vendors = @() + $vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,' + foreach ($m in $vRe.Matches($text)) { + $vendors += @{ + Version = ($m.Groups[1].Value -replace '""','"') + Vendor = ($m.Groups[2].Value -replace '""','"') + Name = ($m.Groups[3].Value -replace '""','"') + } + } + # Per-object counts only matter when editing is enabled (G=0); when G=1 the + # whole config is read-only and stored f1 values are the inactive default. + $counts = $null + if ($G -eq 0) { + $counts = @(0, 0, 0) + # Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid. + $rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}' + foreach ($m in $rRe.Matches($text)) { + $counts[[int]$m.Groups[1].Value]++ + } + } + return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts } + } catch { return $null } +} + +function Get-SupportLines { + $configDir = [System.IO.Path]::GetDirectoryName($ConfigPath) + $binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin" + $st = Read-SupportState $binPath + $out = @() + if (-not $st -or $st.State -eq 'absent') { + if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" } + else { $out += "Поддержка: не на поддержке (своя конфигурация)" } + return $out + } + if ($st.State -eq 'removed') { + $out += "Поддержка: снята с поддержки полностью" + return $out + } + $out += "Поддержка: на поддержке" + if ($st.G -eq 0) { + $out += " Возможность изменения: включена" + $out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])" + } else { + $out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)" + } + $out += " Конфигураций поставщика: $($st.K)" + if ($st.K -gt 1) { + foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor) — $($v.Name) $($v.Version)" } + } + return $out +} + function Format-HomePageItem($it, [bool]$detailed) { $badges = @() $badges += "h=$($it.height)" @@ -253,6 +325,7 @@ $cfgVersion = Get-PropText "Version" $cfgVendor = Get-PropText "Vendor" $cfgCompat = Get-PropText "CompatibilityMode" $cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode" +$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose" $cfgDefaultRun = Get-PropText "DefaultRunMode" $cfgScript = Get-PropText "ScriptVariant" $cfgDefaultLang = Get-PropText "DefaultLanguage" @@ -284,6 +357,7 @@ if ($Mode -eq "overview" -and -not $Section) { Out "Формат: $version" if ($cfgVendor) { Out "Поставщик: $cfgVendor" } if ($cfgVersion) { Out "Версия: $cfgVersion" } + foreach ($l in (Get-SupportLines)) { Out $l } Out "Совместимость: $cfgCompat" Out "Режим запуска: $cfgDefaultRun" Out "Язык скриптов: $cfgScript" @@ -386,6 +460,7 @@ if ($Mode -eq "full" -and -not $Section) { if ($cfgPrefix) { Out "Префикс: $cfgPrefix" } if ($cfgVendor) { Out "Поставщик: $cfgVendor" } if ($cfgVersion) { Out "Версия: $cfgVersion" } + foreach ($l in (Get-SupportLines)) { Out $l } $cfgUpdateAddr = Get-PropText "UpdateCatalogAddress" if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" } Out "" diff --git a/.claude/skills/cf-info/scripts/cf-info.py b/.claude/skills/cf-info/scripts/cf-info.py index bd781e4d..4814cc27 100644 --- a/.claude/skills/cf-info/scripts/cf-info.py +++ b/.claude/skills/cf-info/scripts/cf-info.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -# cf-info v1.2 — Compact summary of 1C configuration root +# cf-info v1.3 — Compact summary of 1C configuration root # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os +import re import sys from collections import OrderedDict from lxml import etree @@ -219,6 +220,71 @@ def get_home_page_layout(): home_page = get_home_page_layout() +# --- Support state (Ext/ParentConfigurations.bin) --- +# Decodes the 1C support-state file. See docs/1c-support-state-spec.md. +# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed', +# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}], +# counts [locked, editable, removed] by f1 — record tally (k>1 counts each +# vendor block separately); only computed when g==0. +def read_support_state(bin_path): + try: + if not os.path.isfile(bin_path): + return {"state": "absent"} + data = open(bin_path, "rb").read() + if len(data) <= 32: + return {"state": "removed"} + if data[:3] == b"\xef\xbb\xbf": + data = data[3:] + text = data.decode("utf-8", "replace") + h = re.match(r"\{6,(\d+),(\d+),", text) + if not h: + return None + g = int(h.group(1)) + k = int(h.group(2)) + if k == 0: + return {"state": "removed"} + vendors = [] + for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text): + vendors.append({ + "version": m.group(1).replace('""', '"'), + "vendor": m.group(2).replace('""', '"'), + "name": m.group(3).replace('""', '"'), + }) + counts = None + if g == 0: + counts = [0, 0, 0] + for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text): + counts[int(m.group(1))] += 1 + return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts} + except Exception: + return None + +def get_support_lines(): + config_dir = os.path.dirname(config_path) + bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin") + st = read_support_state(bin_path) + res = [] + if not st or st["state"] == "absent": + if cfg_ext_purpose: + res.append("Поддержка: расширение (CFE), правки свободны") + else: + res.append("Поддержка: не на поддержке (своя конфигурация)") + return res + if st["state"] == "removed": + res.append("Поддержка: снята с поддержки полностью") + return res + res.append("Поддержка: на поддержке") + if st["g"] == 0: + res.append(" Возможность изменения: включена") + res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}") + else: + res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)") + res.append(f" Конфигураций поставщика: {st['k']}") + if st["k"] > 1: + for v in st["vendors"]: + res.append(f" Поставщик: {v['vendor']} — {v['name']} {v['version']}") + return res + def format_home_page_item(it, detailed): badges = [f"h={it['height']}"] if not it["common"]: @@ -249,6 +315,7 @@ cfg_version = get_prop_text("Version") cfg_vendor = get_prop_text("Vendor") cfg_compat = get_prop_text("CompatibilityMode") cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode") +cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose") cfg_default_run = get_prop_text("DefaultRunMode") cfg_script = get_prop_text("ScriptVariant") cfg_default_lang = get_prop_text("DefaultLanguage") @@ -281,6 +348,8 @@ if args.Mode == "overview" and not args.Section: out(f"Поставщик: {cfg_vendor}") if cfg_version: out(f"Версия: {cfg_version}") + for ln in get_support_lines(): + out(ln) out(f"Совместимость: {cfg_compat}") out(f"Режим запуска: {cfg_default_run}") out(f"Язык скриптов: {cfg_script}") @@ -369,6 +438,8 @@ if args.Mode == "full" and not args.Section: out(f"Поставщик: {cfg_vendor}") if cfg_version: out(f"Версия: {cfg_version}") + for ln in get_support_lines(): + out(ln) cfg_update_addr = get_prop_text("UpdateCatalogAddress") if cfg_update_addr: out(f"Каталог обн.: {cfg_update_addr}") diff --git a/.claude/skills/meta-info/scripts/meta-info.ps1 b/.claude/skills/meta-info/scripts/meta-info.ps1 index 8c29bc2b..47c3ea4f 100644 --- a/.claude/skills/meta-info/scripts/meta-info.ps1 +++ b/.claude/skills/meta-info/scripts/meta-info.ps1 @@ -1,4 +1,4 @@ -# meta-info v1.2 — Compact summary of 1C metadata object +# meta-info v1.3 — Compact summary of 1C metadata object # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath, @@ -415,6 +415,51 @@ function Get-WSOperations($childObjs) { return $result } +# --- Support status of this object (Ext/ParentConfigurations.bin) --- +# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the +# object's support rule. Never throws — degrades to "не на поддержке". +function Get-ObjectSupportStatus([string]$objUuid) { + try { + # Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin). + $d = [System.IO.Path]::GetDirectoryName($ObjectPath) + $binPath = $null + for ($i = 0; $i -lt 8 -and $d; $i++) { + $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" + if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand; break } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" } + $bytes = [System.IO.File]::ReadAllBytes($binPath) + if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" } + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) + $h = [regex]::Match($text, '^\{6,(\d+),(\d+),') + if (-not $h.Success) { return "не на поддержке" } + $G = [int]$h.Groups[1].Value + $K = [int]$h.Groups[2].Value + if ($K -eq 0) { return "снято с поддержки (правки свободны)" } + if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" } + if (-not $objUuid) { return "не на поддержке" } + # Conservative fold: locked if f1=0 in ANY vendor block. + $u = [regex]::Escape($objUuid.ToLower()) + $best = $null + foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { + $f1 = [int]$m.Groups[1].Value + if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } + } + if ($null -eq $best) { return "не на поддержке" } + switch ($best) { + 0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" } + 1 { return "редактируется с сохранением поддержки" } + 2 { return "снято с поддержки (правки свободны)" } + } + return "не на поддержке" + } catch { return "не на поддержке" } +} + # --- Extract metadata --- $props = $typeNode.SelectSingleNode("md:Properties", $ns) $childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns) @@ -608,6 +653,7 @@ if (-not $drillDone) { if ($synonym -and $synonym -ne $objName) { $header += " — `"$synonym`"" } $header += " ===" Out $header + Out "Поддержка: $(Get-ObjectSupportStatus $typeNode.GetAttribute('uuid'))" # --- Type presentation (ref objects) --- if ($isRefObject) { diff --git a/.claude/skills/meta-info/scripts/meta-info.py b/.claude/skills/meta-info/scripts/meta-info.py index cee63692..ab00b7b9 100644 --- a/.claude/skills/meta-info/scripts/meta-info.py +++ b/.claude/skills/meta-info/scripts/meta-info.py @@ -1,4 +1,4 @@ -# meta-info v1.2 — Compact summary of 1C metadata object (Python port) +# meta-info v1.3 — Compact summary of 1C metadata object (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -469,6 +469,59 @@ def get_ws_operations(child_objs): return result +# ── Support status of this object (Ext/ParentConfigurations.bin) ── +# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the +# object's support rule. Never throws — degrades to "не на поддержке". +def get_object_support_status(obj_uuid): + try: + d = os.path.dirname(object_path) + bin_path = None + for _ in range(8): + if not d: + break + cand = os.path.join(d, "Ext", "ParentConfigurations.bin") + if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")): + bin_path = cand + break + parent = os.path.dirname(d) + if parent == d: + break + d = parent + if not bin_path or not os.path.exists(bin_path): + return "не на поддержке" + data = open(bin_path, "rb").read() + if len(data) <= 32: + return "снято с поддержки (правки свободны)" + if data[:3] == b"\xef\xbb\xbf": + data = data[3:] + text = data.decode("utf-8", "replace") + h = re.match(r"\{6,(\d+),(\d+),", text) + if not h: + return "не на поддержке" + g = int(h.group(1)) + k = int(h.group(2)) + if k == 0: + return "снято с поддержки (правки свободны)" + if g == 1: + return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" + if not obj_uuid: + return "не на поддержке" + best = None + for m in re.finditer(r"([0-2]),0," + re.escape(obj_uuid.lower()), text): + f1 = int(m.group(1)) + if best is None or f1 < best: + best = f1 + if best is None: + return "не на поддержке" + return { + 0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта", + 1: "редактируется с сохранением поддержки", + 2: "снято с поддержки (правки свободны)", + }.get(best, "не на поддержке") + except Exception: + return "не на поддержке" + + # ── Extract metadata ───────────────────────────────────────── props = find(type_node, "md:Properties") @@ -650,6 +703,7 @@ if not drill_done: header += f' \u2014 "{synonym}"' header += " ===" out(header) + out(f"Поддержка: {get_object_support_status(type_node.get('uuid', ''))}") # Type presentation (ref objects) if is_ref_object: diff --git a/tests/skills/cases/cf-info/real-acc-support.json b/tests/skills/cases/cf-info/real-acc-support.json new file mode 100644 index 00000000..76eb2750 --- /dev/null +++ b/tests/skills/cases/cf-info/real-acc-support.json @@ -0,0 +1,5 @@ +{ + "name": "Поддержка: acc на поддержке, возможность изменения выключена", + "setup": "external:C:/WS/tasks/cfsrc/acc_8.3.24", + "expect": { "stdoutContains": ["Поддержка:", "выключена", "read-only"] } +} diff --git a/tests/skills/cases/cf-info/real-erp-support.json b/tests/skills/cases/cf-info/real-erp-support.json new file mode 100644 index 00000000..41c12b9f --- /dev/null +++ b/tests/skills/cases/cf-info/real-erp-support.json @@ -0,0 +1,5 @@ +{ + "name": "Поддержка: erp снята с поддержки (16-байт bin)", + "setup": "external:C:/WS/tasks/cfsrc/erp_8.3.24", + "expect": { "stdoutContains": "снята с поддержки полностью" } +} diff --git a/tests/skills/cases/meta-info/real-acc-support.json b/tests/skills/cases/meta-info/real-acc-support.json new file mode 100644 index 00000000..386e0d40 --- /dev/null +++ b/tests/skills/cases/meta-info/real-acc-support.json @@ -0,0 +1,6 @@ +{ + "name": "Поддержка объекта: acc Номенклатура — конфигурация read-only (G=1)", + "setup": "external:C:/WS/tasks/cfsrc/acc_8.3.24", + "params": { "objectPath": "Catalogs/Номенклатура" }, + "expect": { "stdoutContains": "Поддержка: конфигурация read-only" } +} diff --git a/tests/skills/cases/meta-info/real-erp-support.json b/tests/skills/cases/meta-info/real-erp-support.json new file mode 100644 index 00000000..e0eb694c --- /dev/null +++ b/tests/skills/cases/meta-info/real-erp-support.json @@ -0,0 +1,6 @@ +{ + "name": "Поддержка объекта: erp Валюты — снято с поддержки", + "setup": "external:C:/WS/tasks/cfsrc/erp_8.3.24", + "params": { "objectPath": "Catalogs/Валюты" }, + "expect": { "stdoutContains": "Поддержка: снято с поддержки" } +}