feat(cf-*): set-home-page + drill-down -Section home-page + form-ref валидация

- cf-edit: новая операция set-home-page перезаписывает Ext/HomePageWorkArea.xml.
  DSL принимает template (OneColumn/TwoColumnsEqualWidth/TwoColumnsVariableWidth),
  left/right с записями форм (строка или объект form/height/visibility/roles).
  Тихая нормализация ссылок: русские типы, 3-сегмент → авто-Form, файловые пути
- cf-info: краткая HP-сводка (template + счётчики) в overview/full, детальный
  вид через -Section home-page (alias -Name) с раскладкой и переопределениями ролей
- cf-validate: Check 9 — валидация ссылок на формы из HomePageWorkArea и
  Default*Form свойств; битая ссылка → error
- reference.md: убран реализационный шум (canonical sort, авто-нормализация форм,
  panelDef detail, секция авто-валидации); путь src/ в примерах вместо репо-специфичных

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-01 19:13:18 +03:00
co-authored by Claude Opus 4.7
parent bd3b40852a
commit 42b96bbd21
20 changed files with 1218 additions and 41 deletions
+109 -4
View File
@@ -1,9 +1,12 @@
# cf-info v1.1 — Compact summary of 1C configuration root
# cf-info v1.2 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[Alias('Name')]
[ValidateSet("home-page")]
[string]$Section,
[int]$Limit = 150,
[int]$Offset = 0,
[string]$OutFile
@@ -171,6 +174,62 @@ function Format-LayoutSlots($slots) {
$script:panelLayout = Get-PanelsLayout
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
function Get-HomePageLayout {
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
$hpPath = Join-Path (Join-Path $configDir "Ext") "HomePageWorkArea.xml"
if (-not (Test-Path $hpPath)) { return $null }
try { [xml]$hpDoc = Get-Content -Path $hpPath -Encoding UTF8 } catch { return $null }
if (-not $hpDoc.DocumentElement) { return $null }
$hpNs = New-Object System.Xml.XmlNamespaceManager($hpDoc.NameTable)
$hpNs.AddNamespace("hp", "http://v8.1c.ru/8.3/xcf/extrnprops")
$hpNs.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$result = [ordered]@{ template = ""; left = @(); right = @() }
$tmplNode = $hpDoc.DocumentElement.SelectSingleNode("hp:WorkingAreaTemplate", $hpNs)
if ($tmplNode) { $result.template = $tmplNode.InnerText.Trim() }
foreach ($colName in @("LeftColumn","RightColumn")) {
$colNode = $hpDoc.DocumentElement.SelectSingleNode("hp:$colName", $hpNs)
if (-not $colNode) { continue }
$items = @()
foreach ($item in $colNode.SelectNodes("hp:Item", $hpNs)) {
$f = $item.SelectSingleNode("hp:Form", $hpNs)
$h = $item.SelectSingleNode("hp:Height", $hpNs)
$visNode = $item.SelectSingleNode("hp:Visibility", $hpNs)
$common = $true
$roles = @()
if ($visNode) {
$cn = $visNode.SelectSingleNode("xr:Common", $hpNs)
if ($cn) { $common = ($cn.InnerText.Trim() -eq "true") }
foreach ($v in $visNode.SelectNodes("xr:Value", $hpNs)) {
$roles += @{ name = $v.GetAttribute("name"); value = ($v.InnerText.Trim() -eq "true") }
}
}
$items += [ordered]@{
form = if ($f) { $f.InnerText.Trim() } else { "" }
height = if ($h) { [int]$h.InnerText.Trim() } else { 10 }
common = $common
roles = $roles
}
}
if ($colName -eq "LeftColumn") { $result.left = $items } else { $result.right = $items }
}
return $result
}
$script:homePage = Get-HomePageLayout
function Format-HomePageItem($it, [bool]$detailed) {
$badges = @()
$badges += "h=$($it.height)"
if (-not $it.common) { $badges += "скрыта" }
if ($it.roles.Count -gt 0) {
if ($detailed) { $badges += "роли: $($it.roles.Count)" }
else { $badges += "+$($it.roles.Count) ролей" }
}
$tail = if ($badges.Count -gt 0) { " (" + ($badges -join ", ") + ")" } else { "" }
return " $($it.form)$tail"
}
# --- Count objects in ChildObjects ---
$objectCounts = [ordered]@{}
$totalObjects = 0
@@ -207,7 +266,7 @@ $cfgDbSpaces = Get-PropText "DatabaseTablespacesUseMode"
$cfgWindowMode = Get-PropText "MainClientApplicationWindowMode"
# --- BRIEF mode ---
if ($Mode -eq "brief") {
if ($Mode -eq "brief" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
$compatPart = if ($cfgCompat) { " | $cfgCompat" } else { "" }
@@ -215,7 +274,7 @@ if ($Mode -eq "brief") {
}
# --- OVERVIEW mode ---
if ($Mode -eq "overview") {
if ($Mode -eq "overview" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
@@ -251,6 +310,16 @@ if ($Mode -eq "overview") {
}
}
# Home page layout (brief summary)
if ($script:homePage) {
$ln = $script:homePage.left.Count
$rn = $script:homePage.right.Count
Out "--- Начальная страница ---"
Out " Шаблон: $($script:homePage.template)"
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
Out ""
}
# Object counts table
Out "--- Состав ($totalObjects объектов) ---"
Out ""
@@ -273,8 +342,34 @@ if ($Mode -eq "overview") {
}
}
# --- Drill-down: -Section home-page ---
if ($Section -eq "home-page") {
if (-not $script:homePage) {
Out "Файл Ext/HomePageWorkArea.xml не найден"
} else {
Out "=== Начальная страница: $cfgName ==="
Out ""
Out "Шаблон: $($script:homePage.template)"
Out ""
foreach ($side in @(@("LeftColumn","left"), @("RightColumn","right"))) {
$items = $script:homePage[$side[1]]
$lbl = $side[0]
if ($items.Count -eq 0) { Out "${lbl}: —"; Out ""; continue }
Out "${lbl} ($($items.Count)):"
foreach ($it in $items) {
Out (Format-HomePageItem $it $true)
foreach ($r in $it.roles) {
$rval = if ($r.value) { "true" } else { "false" }
Out " $($r.name): $rval"
}
}
Out ""
}
}
}
# --- FULL mode ---
if ($Mode -eq "full") {
if ($Mode -eq "full" -and -not $Section) {
$synPart = if ($cfgSynonym) { " $dash `"$cfgSynonym`"" } else { "" }
$verPart = if ($cfgVersion) { " v$cfgVersion" } else { "" }
Out "=== Конфигурация: ${cfgName}${synPart}${verPart} ==="
@@ -362,6 +457,16 @@ if ($Mode -eq "full") {
Out ""
}
# --- Section: Home page (brief summary) ---
if ($script:homePage) {
$ln = $script:homePage.left.Count
$rn = $script:homePage.right.Count
Out "--- Начальная страница ---"
Out " Шаблон: $($script:homePage.template)"
Out " LeftColumn: $ln, RightColumn: $rn (детали: -Section home-page)"
Out ""
}
# --- Section: Storages & default forms ---
Out "--- Хранилища и формы по умолчанию ---"
$storageProps = @("CommonSettingsStorage","ReportsUserSettingsStorage","ReportsVariantsStorage","FormDataSettingsStorage","DynamicListsUserSettingsStorage","URLExternalDataStorage")
+97 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-info v1.1 — Compact summary of 1C configuration root
# cf-info v1.2 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -15,6 +15,7 @@ sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Analyze 1C configuration structure", allow_abbrev=False)
parser.add_argument("-ConfigPath", "-Path", required=True, help="Path to Configuration.xml or directory")
parser.add_argument("-Mode", choices=["overview", "brief", "full"], default="overview", help="Output mode")
parser.add_argument("-Section", "-Name", choices=["home-page"], default=None, help="Drill-down section (alias: -Name)")
parser.add_argument("-Limit", type=int, default=150, help="Max lines to show")
parser.add_argument("-Offset", type=int, default=0, help="Lines to skip")
parser.add_argument("-OutFile", default="", help="Write output to file")
@@ -172,6 +173,61 @@ def format_layout_slots(slots):
panel_layout = get_panels_layout()
# --- Read home page layout (Ext/HomePageWorkArea.xml) ---
HP_NS = "http://v8.1c.ru/8.3/xcf/extrnprops"
XR_NS_HP = "http://v8.1c.ru/8.3/xcf/readable"
def get_home_page_layout():
cfg_dir = os.path.dirname(config_path)
hp_path = os.path.join(cfg_dir, "Ext", "HomePageWorkArea.xml")
if not os.path.isfile(hp_path):
return None
try:
hp_tree = etree.parse(hp_path)
except Exception:
return None
hp_root = hp_tree.getroot()
result = {"template": "", "left": [], "right": []}
tn = hp_root.find(f"{{{HP_NS}}}WorkingAreaTemplate")
if tn is not None and tn.text:
result["template"] = tn.text.strip()
for col_name, key in (("LeftColumn", "left"), ("RightColumn", "right")):
col = hp_root.find(f"{{{HP_NS}}}{col_name}")
if col is None:
continue
items = []
for it in col.findall(f"{{{HP_NS}}}Item"):
f = it.find(f"{{{HP_NS}}}Form")
h = it.find(f"{{{HP_NS}}}Height")
vis = it.find(f"{{{HP_NS}}}Visibility")
common = True
roles = []
if vis is not None:
cn = vis.find(f"{{{XR_NS_HP}}}Common")
if cn is not None and cn.text:
common = cn.text.strip() == "true"
for v in vis.findall(f"{{{XR_NS_HP}}}Value"):
roles.append({"name": v.get("name", ""), "value": (v.text or "").strip() == "true"})
items.append({
"form": (f.text or "").strip() if f is not None else "",
"height": int((h.text or "10").strip()) if h is not None else 10,
"common": common,
"roles": roles,
})
result[key] = items
return result
home_page = get_home_page_layout()
def format_home_page_item(it, detailed):
badges = [f"h={it['height']}"]
if not it["common"]:
badges.append("скрыта")
if it["roles"]:
badges.append(f"роли: {len(it['roles'])}" if detailed else f"+{len(it['roles'])} ролей")
tail = f" ({', '.join(badges)})" if badges else ""
return f" {it['form']}{tail}"
# --- Count objects in ChildObjects ---
object_counts = OrderedDict()
total_objects = 0
@@ -206,14 +262,14 @@ cfg_db_spaces = get_prop_text("DatabaseTablespacesUseMode")
cfg_window_mode = get_prop_text("MainClientApplicationWindowMode")
# --- BRIEF mode ---
if args.Mode == "brief":
if args.Mode == "brief" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
compat_part = f" | {cfg_compat}" if cfg_compat else ""
out(f"Конфигурация: {cfg_name}{syn_part}{ver_part} | {total_objects} объектов{compat_part}")
# --- OVERVIEW mode ---
if args.Mode == "overview":
if args.Mode == "overview" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
@@ -241,6 +297,13 @@ if args.Mode == "overview":
out(f" {s.ljust(7)} {format_layout_slots(panel_layout[s])}")
out()
# Home page (brief summary)
if home_page:
out("--- Начальная страница ---")
out(f" Шаблон: {home_page['template']}")
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
out()
# Object counts table
out(f"--- Состав ({total_objects} объектов) ---")
out()
@@ -261,7 +324,30 @@ if args.Mode == "overview":
out(f" {padded} {count}")
# --- FULL mode ---
if args.Mode == "full":
# --- Drill-down: -Section home-page ---
if args.Section == "home-page":
if not home_page:
out("Файл Ext/HomePageWorkArea.xml не найден")
else:
out(f"=== Начальная страница: {cfg_name} ===")
out()
out(f"Шаблон: {home_page['template']}")
out()
for col_lbl, col_key in (("LeftColumn", "left"), ("RightColumn", "right")):
items = home_page[col_key]
if not items:
out(f"{col_lbl}: —")
out()
continue
out(f"{col_lbl} ({len(items)}):")
for it in items:
out(format_home_page_item(it, True))
for r in it["roles"]:
rval = "true" if r["value"] else "false"
out(f" {r['name']}: {rval}")
out()
if args.Mode == "full" and not args.Section:
syn_part = f' {dash} "{cfg_synonym}"' if cfg_synonym else ""
ver_part = f" v{cfg_version}" if cfg_version else ""
out(f"=== Конфигурация: {cfg_name}{syn_part}{ver_part} ===")
@@ -350,6 +436,13 @@ if args.Mode == "full":
out(f" объявлено: {', '.join(panel_layout['declared'])}")
out()
# --- Section: Home page (brief summary) ---
if home_page:
out("--- Начальная страница ---")
out(f" Шаблон: {home_page['template']}")
out(f" LeftColumn: {len(home_page['left'])}, RightColumn: {len(home_page['right'])} (детали: -Section home-page)")
out()
# --- Section: Storages & default forms ---
out("--- Хранилища и формы по умолчанию ---")
storage_props = [