diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index d2bb7caf3..1f351d5fc 100644 --- a/.claude/skills/form-compile/scripts/form-compile.ps1 +++ b/.claude/skills/form-compile/scripts/form-compile.ps1 @@ -1,4 +1,4 @@ -# form-compile v1.196 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) +# form-compile v1.197 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -1726,17 +1726,24 @@ if ($FromObject) { # Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile) $script:queryBaseDir = if ($JsonPath) { [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) } else { (Get-Location).Path } -function Resolve-QueryValue { +function Resolve-TextFromFile { param([string]$val, [string]$baseDir) if (-not $val.StartsWith("@")) { return $val } $filePath = $val.Substring(1) if ([System.IO.Path]::IsPathRooted($filePath)) { $candidates = @($filePath) } else { - $candidates = @((Join-Path $baseDir $filePath), (Join-Path (Get-Location).Path $filePath)) + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) } - foreach ($c in $candidates) { if (Test-Path $c) { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Файл значения не найден: $filePath (искали: $($candidates -join ', '))" exit 1 } @@ -6035,7 +6042,7 @@ function Emit-Attributes { $ddr = if ($st.dynamicDataRead -eq $false) { "false" } else { "true" } X "$si$ddr" if ($hasQuery) { - $qtext = Resolve-QueryValue "$($st.query)" $script:queryBaseDir + $qtext = Resolve-TextFromFile "$($st.query)" $script:queryBaseDir X "$si$(Esc-XmlText $qtext)" } # Явные поля набора (редко): override title/dataPath diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index ed6220ae1..8b1746e77 100644 --- a/.claude/skills/form-compile/scripts/form-compile.py +++ b/.claude/skills/form-compile/scripts/form-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# form-compile v1.196 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) +# form-compile v1.197 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -1571,19 +1571,22 @@ def di_attr(el): QUERY_BASE_DIR = None -def resolve_query_value(val, base_dir): - if not val.startswith('@'): +def resolve_text_from_file(val, base_dir): + if not val.startswith("@"): return val file_path = val[1:] if os.path.isabs(file_path): candidates = [file_path] else: - candidates = [os.path.join(base_dir or os.getcwd(), file_path), os.path.join(os.getcwd(), file_path)] + candidates = [ + os.path.join(base_dir, file_path), + os.path.join(os.getcwd(), file_path), + ] for c in candidates: if os.path.exists(c): with open(c, 'r', encoding='utf-8-sig') as f: return f.read().rstrip() - print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + print(f"Файл значения не найден: {file_path} (искали: {', '.join(candidates)})", file=sys.stderr) sys.exit(1) @@ -5877,7 +5880,7 @@ def emit_attributes(lines, attrs, indent, conditional_appearance=None): ddr = 'false' if s.get('dynamicDataRead') is False else 'true' lines.append(f'{si}{ddr}') if has_query: - qtext = resolve_query_value(str(s['query']), QUERY_BASE_DIR) + qtext = resolve_text_from_file(str(s['query']), QUERY_BASE_DIR) lines.append(f'{si}{esc_xml_text(qtext)}') # Явные поля набора (редко): override title/dataPath if s.get('fields'): diff --git a/.claude/skills/role-compile/SKILL.md b/.claude/skills/role-compile/SKILL.md index da0a9459e..d5da4dab3 100644 --- a/.claude/skills/role-compile/SKILL.md +++ b/.claude/skills/role-compile/SKILL.md @@ -91,7 +91,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" - Ссылка в `rls`: `"#ДляОбъекта(\"\")"`. Символ `&` автоматически экранируется в XML. -Длинное условие держи в файле — в значении пишется `@путь` (путь от текущего каталога): +Длинное условие держи в файле — в значении пишется `@путь`. Относительный путь ищется рядом +с JSON-описанием роли, затем в текущем каталоге: ```json "objects": [{"name": "Document.Продажа", "preset": "view", "rls": {"Read": "@условие.txt"}}], diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1 index c5d6a7aa6..d61319dc6 100644 --- a/.claude/skills/role-compile/scripts/role-compile.ps1 +++ b/.claude/skills/role-compile/scripts/role-compile.ps1 @@ -1,4 +1,4 @@ -# role-compile v1.40 — Compile 1C role from JSON +# role-compile v1.41 — Compile 1C role from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -1073,17 +1073,27 @@ function Validate-RightName { } # "@путь" в значении условия — текст берётся из файла: условия RLS типовых занимают десятки -# строк с кавычками, и внутри JSON-строки это источник ошибок экранирования. Путь относительный — -# от текущего каталога, как в role-edit. -function Resolve-TextValue([string]$text) { - if (-not $text -or -not $text.StartsWith("@")) { return $text } - $valueFile = $text.Substring(1).Trim() - if (-not [System.IO.Path]::IsPathRooted($valueFile)) { $valueFile = Join-Path (Get-Location).Path $valueFile } - if (-not (Test-Path -LiteralPath $valueFile -PathType Leaf)) { - Add-ValidationError "Файл значения не найден: $valueFile" - return $text +# строк с кавычками, и внутри JSON-строки это источник ошибок экранирования. Относительный +# путь ищется рядом с JSON-описанием роли, затем в текущем каталоге. +function Resolve-TextFromFile { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) } - return [System.IO.File]::ReadAllText($valueFile).Trim() + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Файл значения не найден: $filePath (искали: $($candidates -join ', '))" + exit 1 } # --- 5a. Service roots: expand to leaves --- @@ -1304,7 +1314,7 @@ function Parse-ObjectEntry { foreach ($p in $entry.rls.PSObject.Properties) { $rlsRight = Translate-RightName $p.Name if ($rightsMap.Contains($rlsRight)) { - $rightsMap[$rlsRight].Condition = Resolve-TextValue "$($p.Value)" + $rightsMap[$rlsRight].Condition = Resolve-TextFromFile "$($p.Value)" $script:textBaseDir } else { Write-Warning "${objName}: RLS for '$rlsRight' but this right is not in the rights list" } @@ -1332,6 +1342,9 @@ if (-not $def.objects -and $def.rights) { $def | Add-Member -NotePropertyName ob # Путь нужен уже здесь: раскрытие сервисного корня читает метаданные сервиса рядом с ролью. $resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir } +# Относительный путь @файла ищем сначала рядом с JSON-описанием роли. +$script:textBaseDir = [System.IO.Path]::GetDirectoryName((Resolve-Path $JsonPath).Path) + $parsedObjects = @() $seenObjectNames = @{} if ($def.objects) { @@ -1492,7 +1505,7 @@ if ($def.templates) { foreach ($tpl in $def.templates) { X "`t" X "`t`t$(Esc-XmlText "$($tpl.name)")" - X "`t`t$(Esc-XmlText (Resolve-TextValue "$($tpl.condition)"))" + X "`t`t$(Esc-XmlText (Resolve-TextFromFile "$($tpl.condition)" $script:textBaseDir))" X "`t" $templateCount++ } diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py index a67f3ab99..6b5dd5136 100644 --- a/.claude/skills/role-compile/scripts/role-compile.py +++ b/.claude/skills/role-compile/scripts/role-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-compile v1.40 — Compile 1C role from JSON +# role-compile v1.41 — Compile 1C role from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -1166,19 +1166,28 @@ def validate_right_name(object_name, right_name): # "@путь" в значении условия — текст берётся из файла: условия RLS типовых занимают десятки -# строк с кавычками, и внутри JSON-строки это источник ошибок экранирования. Путь относительный — -# от текущего каталога, как в role-edit. -def resolve_text_value(text): - if not text or not text.startswith("@"): - return text - value_file = text[1:].strip() - if not os.path.isabs(value_file): - value_file = os.path.join(os.getcwd(), value_file) - if not os.path.isfile(value_file): - add_validation_error(f"Файл значения не найден: {value_file}") - return text - with open(value_file, encoding="utf-8-sig") as f: - return f.read().strip() +# строк с кавычками, и внутри JSON-строки это источник ошибок экранирования. Относительный +# путь ищется рядом с JSON-описанием роли, затем в текущем каталоге. +def resolve_text_from_file(val, base_dir): + if not val.startswith("@"): + return val + file_path = val[1:] + if os.path.isabs(file_path): + candidates = [file_path] + else: + candidates = [ + os.path.join(base_dir, file_path), + os.path.join(os.getcwd(), file_path), + ] + for c in candidates: + if os.path.exists(c): + with open(c, 'r', encoding='utf-8-sig') as f: + return f.read().rstrip() + print(f"Файл значения не найден: {file_path} (искали: {', '.join(candidates)})", file=sys.stderr) + sys.exit(1) + + +TEXT_BASE_DIR = os.getcwd() MD_NS = 'http://v8.1c.ru/8.3/MDClasses' @@ -1380,7 +1389,7 @@ def parse_object_entry(entry): for p_name, p_value in entry['rls'].items(): rls_right = translate_right_name(p_name) if rls_right in rights_map: - rights_map[rls_right]['Condition'] = resolve_text_value(str(p_value)) + rights_map[rls_right]['Condition'] = resolve_text_from_file(str(p_value), TEXT_BASE_DIR) else: print(f"WARNING: {obj_name}: RLS for '{rls_right}' but this right is not in the rights list", file=sys.stderr) @@ -1628,6 +1637,10 @@ def main(): format_version = detect_format_version(out_dir_resolved) # --- 2. Parse all object entries --- + # Относительный путь @файла ищем сначала рядом с JSON-описанием роли. + global TEXT_BASE_DIR + TEXT_BASE_DIR = os.path.dirname(os.path.abspath(args.JsonPath)) + parsed_objects = [] seen_object_names = set() if defn.get('objects'): @@ -1740,7 +1753,7 @@ def main(): for tpl in defn['templates']: lines.append('\t') lines.append(f'\t\t{esc_xml_text(str(tpl["name"]))}') - lines.append(f'\t\t{esc_xml_text(resolve_text_value(str(tpl["condition"])))}') + lines.append(f'\t\t{esc_xml_text(resolve_text_from_file(str(tpl["condition"]), TEXT_BASE_DIR))}') lines.append('\t') template_count += 1 diff --git a/.claude/skills/role-edit/scripts/role-edit.ps1 b/.claude/skills/role-edit/scripts/role-edit.ps1 index d42250f5d..13f392373 100644 --- a/.claude/skills/role-edit/scripts/role-edit.ps1 +++ b/.claude/skills/role-edit/scripts/role-edit.ps1 @@ -1,4 +1,4 @@ -# role-edit v1.0 — Edit existing 1C role rights in place +# role-edit v1.1 — Edit existing 1C role rights in place # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -1025,6 +1025,27 @@ function Validate-RightName { return $true } +function Resolve-TextFromFile { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) + } + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Файл значения не найден: $filePath (искали: $($candidates -join ', '))" + exit 1 +} + # --- 5a. Service roots: expand to leaves --- # Метаданные сервиса читаются один раз на имя: раскрытие и проверка заимствования @@ -1405,20 +1426,6 @@ $script:rightsPath = $script:paths.RightsPath $script:roleXmlPath = $script:paths.RoleXmlPath $script:configRoot = $script:paths.ConfigRoot -# "@путь" в позиции ТЕКСТА (условие RLS, тело шаблона, синоним) — содержимое берётся из файла: -# многострочное условие инлайном ломается о кавычки и о разделитель пакета. Адрес при этом -# остаётся в команде: "Catalog.Товары.Read: @условие.txt". -function Resolve-TextValue([string]$text) { - if (-not $text -or -not $text.StartsWith("@")) { return $text } - $valueFile = $text.Substring(1).Trim() - if (-not [System.IO.Path]::IsPathRooted($valueFile)) { $valueFile = Join-Path (Get-Location).Path $valueFile } - if (-not (Test-Path -LiteralPath $valueFile -PathType Leaf)) { - Add-ValidationError "Файл значения не найден: $valueFile" - return $text - } - return [System.IO.File]::ReadAllText($valueFile).Trim() -} - if ($DefinitionFile -and $Operation) { [Console]::Error.WriteLine("[role-edit] Укажите либо -DefinitionFile, либо -Operation, но не оба сразу") exit 1 @@ -1428,6 +1435,11 @@ if (-not $DefinitionFile -and -not $Operation) { exit 1 } +# База относительного пути @файла: каталог списка операций, иначе каталог самой роли. +# Текущий каталог функция проверяет вторым кандидатом в любом случае. +$script:textBaseDir = if ($DefinitionFile) { [System.IO.Path]::GetDirectoryName((Resolve-Path $DefinitionFile).Path) } + else { [System.IO.Path]::GetDirectoryName($script:paths.RightsPath) } + $targetForGuard = if (Test-Path -LiteralPath $script:roleXmlPath) { $script:roleXmlPath } else { $script:rightsPath } Assert-EditAllowed $targetForGuard 'editable' @@ -1538,7 +1550,7 @@ function Parse-RlsAddress([string]$text, [switch]$ConditionRequired) { Add-ValidationError "$text : разобрано как объект '$objName' и право '$rightName'" return $null } - return @{ Object = $objName; Right = $rightName; Fields = $fields; Condition = (Resolve-TextValue $condition) } + return @{ Object = $objName; Right = $rightName; Fields = $fields; Condition = (Resolve-TextFromFile $condition $script:textBaseDir) } } # "Имя(Пар1, Пар2): условие" — скобки принадлежат имени шаблона, разделитель ищем вне них. @@ -1549,7 +1561,7 @@ function Parse-TemplateSpec([string]$text, [switch]$NameOnly) { Add-ValidationError "$text : ожидается 'Имя(Параметры): условие'" return $null } - return @{ Name = $split.Left; Condition = (Resolve-TextValue $split.Right) } + return @{ Name = $split.Left; Condition = (Resolve-TextFromFile $split.Right $script:textBaseDir) } } # --- Доступ к дереву прав --- @@ -2221,8 +2233,8 @@ foreach ($op in $operations) { "set-template" { Do-SetTemplate $opValue } "remove-template" { Do-RemoveTemplate $opValue } "modify-property" { Do-ModifyProperty $opValue } - "set-synonym" { $script:pendingMeta += ,@{ Field = 'Synonym'; Text = (Resolve-TextValue $opValue) } } - "set-comment" { $script:pendingMeta += ,@{ Field = 'Comment'; Text = (Resolve-TextValue $opValue) } } + "set-synonym" { $script:pendingMeta += ,@{ Field = 'Synonym'; Text = (Resolve-TextFromFile $opValue $script:textBaseDir) } } + "set-comment" { $script:pendingMeta += ,@{ Field = 'Comment'; Text = (Resolve-TextFromFile $opValue $script:textBaseDir) } } default { Add-ValidationError "Неизвестная операция: $opName" } diff --git a/.claude/skills/role-edit/scripts/role-edit.py b/.claude/skills/role-edit/scripts/role-edit.py index 0f5961602..57f53b606 100644 --- a/.claude/skills/role-edit/scripts/role-edit.py +++ b/.claude/skills/role-edit/scripts/role-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# role-edit v1.0 — Edit existing 1C role rights in place +# role-edit v1.1 — Edit existing 1C role rights in place # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -1120,8 +1120,30 @@ def validate_right_name(object_name, right_name): return True -MD_NS = 'http://v8.1c.ru/8.3/MDClasses' +# "@путь" в значении условия — текст берётся из файла: условия RLS типовых занимают десятки +def resolve_text_from_file(val, base_dir): + if not val.startswith("@"): + return val + file_path = val[1:] + if os.path.isabs(file_path): + candidates = [file_path] + else: + candidates = [ + os.path.join(base_dir, file_path), + os.path.join(os.getcwd(), file_path), + ] + for c in candidates: + if os.path.exists(c): + with open(c, 'r', encoding='utf-8-sig') as f: + return f.read().rstrip() + print(f"Файл значения не найден: {file_path} (искали: {', '.join(candidates)})", file=sys.stderr) + sys.exit(1) + + +TEXT_BASE_DIR = os.getcwd() + +MD_NS = 'http://v8.1c.ru/8.3/MDClasses' # Метаданные сервиса читаются один раз на имя: раскрытие и проверка заимствования # спрашивают один и тот же файл. @@ -1461,8 +1483,11 @@ def resolve_role_paths(input_path): class Editor: """Состояние правки: дерево прав, счётчики, отложенные операции.""" - def __init__(self, paths): + def __init__(self, paths, text_base_dir): self.paths = paths + # База относительного пути @файла: каталог списка операций, иначе каталог самой роли. + # Текущий каталог функция проверяет вторым кандидатом в любом случае. + self.text_base_dir = text_base_dir parser = etree.XMLParser(remove_blank_text=False) self.tree = etree.parse(paths["RightsPath"], parser) self.root = self.tree.getroot() @@ -1486,21 +1511,6 @@ class Editor: # Делим ДО чтения файлов, поэтому ';;' внутри условия из файла разделителем не становится. return [part.strip() for part in value.split(";;") if part.strip()] - @staticmethod - def resolve_text_value(text): - """"@путь" в позиции ТЕКСТА (условие RLS, тело шаблона, синоним) — содержимое из файла: - многострочное условие инлайном ломается о кавычки и о разделитель пакета. Адрес при этом - остаётся в команде: "Catalog.Товары.Read: @условие.txt".""" - if not text or not text.startswith("@"): - return text - value_file = text[1:].strip() - if not os.path.isabs(value_file): - value_file = os.path.join(os.getcwd(), value_file) - if not os.path.isfile(value_file): - add_validation_error(f"Файл значения не найден: {value_file}") - return text - with open(value_file, encoding="utf-8-sig") as f: - return f.read().strip() @staticmethod def split_at_top_level_colon(text, open_char, close_char): @@ -1566,7 +1576,7 @@ class Editor: add_validation_error(f"{text} : разобрано как объект '{obj_name}' и право '{right_name}'") return None return {"Object": obj_name, "Right": right_name, "Fields": fields, - "Condition": self.resolve_text_value(condition)} + "Condition": resolve_text_from_file(condition, self.text_base_dir)} def parse_template_spec(self, text, name_only=False): left, right, found = self.split_at_top_level_colon(text, "(", ")") @@ -1575,7 +1585,7 @@ class Editor: if not found: add_validation_error(f"{text} : ожидается 'Имя(Параметры): условие'") return None - return {"Name": left, "Condition": self.resolve_text_value(right)} + return {"Name": left, "Condition": resolve_text_from_file(right, self.text_base_dir)} # --- Доступ к дереву прав --- @@ -2090,7 +2100,9 @@ def main(): target_for_guard = paths["RoleXmlPath"] if os.path.isfile(paths["RoleXmlPath"]) else paths["RightsPath"] assert_edit_allowed(target_for_guard, "editable") - ed = Editor(paths) + text_base_dir = (os.path.dirname(os.path.abspath(args.DefinitionFile)) if args.DefinitionFile + else os.path.dirname(paths["RightsPath"])) + ed = Editor(paths, text_base_dir) operations = [] if args.DefinitionFile: @@ -2149,9 +2161,9 @@ def main(): continue pending.append((key, {"Name": canonical, "Value": val})) elif key == "set-synonym": - pending.append((key, {"Field": "Synonym", "Text": ed.resolve_text_value(op_value)})) + pending.append((key, {"Field": "Synonym", "Text": resolve_text_from_file(op_value, ed.text_base_dir)})) elif key == "set-comment": - pending.append((key, {"Field": "Comment", "Text": ed.resolve_text_value(op_value)})) + pending.append((key, {"Field": "Comment", "Text": resolve_text_from_file(op_value, ed.text_base_dir)})) else: add_validation_error(f"Неизвестная операция: {op_name}") diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index a39feb82a..7821f77df 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.121 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# skd-compile v1.122 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -305,7 +305,7 @@ function Esc-XmlText { return $s.Replace('&','&').Replace('<','<').Replace('>','>') } -function Resolve-QueryValue { +function Resolve-TextFromFile { param([string]$val, [string]$baseDir) if (-not $val.StartsWith("@")) { return $val } $filePath = $val.Substring(1) @@ -322,7 +322,7 @@ function Resolve-QueryValue { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + Write-Error "Файл значения не найден: $filePath (искали: $($candidates -join ', '))" exit 1 } @@ -1341,7 +1341,7 @@ function Emit-DataSet { # Type-specific content if ($dsType -eq "DataSetQuery") { - $queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir + $queryText = Resolve-TextFromFile "$($ds.query)" $script:queryBaseDir X "$indent`t$(Esc-XmlText $queryText)" if ($ds.autoFillFields -eq $false) { X "$indent`tfalse" diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 8380abc5c..124e826cc 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.121 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# skd-compile v1.122 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -321,7 +321,7 @@ def fmt_dec(v): return str(int(v)) if v == int(v) else str(v) -def resolve_query_value(val, base_dir): +def resolve_text_from_file(val, base_dir): if not val.startswith("@"): return val file_path = val[1:] @@ -336,7 +336,7 @@ def resolve_query_value(val, base_dir): if os.path.exists(c): with open(c, 'r', encoding='utf-8-sig') as f: return f.read().rstrip() - print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + print(f"Файл значения не найден: {file_path} (искали: {', '.join(candidates)})", file=sys.stderr) sys.exit(1) @@ -1190,7 +1190,7 @@ def emit_data_set(lines, ds, indent, default_source, tag_name='dataSet'): # Type-specific content if ds_type == 'DataSetQuery': - query_text = resolve_query_value(str(ds.get("query", "")), query_base_dir) + query_text = resolve_text_from_file(str(ds.get("query", "")), query_base_dir) lines.append(f'{indent}\t{esc_xml_text(query_text)}') if ds.get('autoFillFields') is False: lines.append(f'{indent}\tfalse') diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 90de321fd..64a1523cc 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -1,4 +1,4 @@ -# skd-edit v1.39 — Atomic 1C DCS editor +# skd-edit v1.40 — Atomic 1C DCS editor # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: парный .py собирает выражения автодат вне f-string ради совместимости с python 3.9 (PEP 701). [CmdletBinding(PositionalBinding=$false)] @@ -194,7 +194,7 @@ function Esc-XmlText { return $s.Replace('&','&').Replace('<','<').Replace('>','>') } -function Resolve-QueryValue { +function Resolve-TextFromFile { param([string]$val, [string]$baseDir) if (-not $val.StartsWith("@")) { return $val } $filePath = $val.Substring(1) @@ -211,7 +211,7 @@ function Resolve-QueryValue { return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() } } - Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + Write-Error "Файл значения не найден: $filePath (искали: $($candidates -join ', '))" exit 1 } @@ -3097,7 +3097,7 @@ switch ($Operation) { } # InnerText setter handles XML escaping automatically - $queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir + $queryEl.InnerText = Resolve-TextFromFile $Value $script:queryBaseDir $script:Dirty = $true; Write-Host "[OK] Query replaced in dataset `"$dsName`"" } @@ -3337,7 +3337,7 @@ switch ($Operation) { $childIndent = Get-ChildIndent $root $parsed = Parse-DataSetShorthand $Value - $parsed.query = Resolve-QueryValue $parsed.query $script:queryBaseDir + $parsed.query = Resolve-TextFromFile $parsed.query $script:queryBaseDir # Auto-name if empty if (-not $parsed.name) { diff --git a/.claude/skills/skd-edit/scripts/skd-edit.py b/.claude/skills/skd-edit/scripts/skd-edit.py index d48f941b0..ba20fb754 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.py +++ b/.claude/skills/skd-edit/scripts/skd-edit.py @@ -1,4 +1,4 @@ -# skd-edit v1.39 — Atomic 1C DCS editor (Python port) +# skd-edit v1.40 — Atomic 1C DCS editor (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -111,7 +111,7 @@ def esc_xml_text(s): return s.replace('&', '&').replace('<', '<').replace('>', '>') -def resolve_query_value(val, base_dir): +def resolve_text_from_file(val, base_dir): if not val.startswith("@"): return val file_path = val[1:] @@ -126,7 +126,7 @@ def resolve_query_value(val, base_dir): if os.path.exists(c): with open(c, 'r', encoding='utf-8-sig') as f: return f.read().rstrip() - print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + print(f"Файл значения не найден: {file_path} (искали: {', '.join(candidates)})", file=sys.stderr) sys.exit(1) @@ -2689,7 +2689,7 @@ elif operation == "set-query": if query_el is None: print(f"No element found in dataset '{ds_name}'", file=sys.stderr) sys.exit(1) - query_el.text = resolve_query_value(value_arg, query_base_dir) + query_el.text = resolve_text_from_file(value_arg, query_base_dir) dirty = True; print(f'[OK] Query replaced in dataset "{ds_name}"') elif operation == "patch-query": @@ -2877,7 +2877,7 @@ elif operation == "add-dataSetLink": elif operation == "add-dataSet": child_indent = get_child_indent(xml_doc) parsed = parse_data_set_shorthand(value_arg) - parsed["query"] = resolve_query_value(parsed["query"], query_base_dir) + parsed["query"] = resolve_text_from_file(parsed["query"], query_base_dir) if not parsed["name"]: count = sum(1 for ch in xml_doc if isinstance(ch.tag, str) and local_name(ch) == "dataSet" and etree.QName(ch.tag).namespace == SCH_NS) diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index 97d9664e6..4c60e6e71 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -409,6 +409,16 @@ const FAMILIES = [ // Существует только в PY: PowerShell регистронезависим сам по себе (свойства PSObject, ключи // Hashtable, -eq/-contains, имена параметров, ValidateSet), поэтому в .ps1 копии нет и быть // не должно — ps1: null. + // Значение операции может быть "@путь" — текст читается из файла. Правило поиска общее: + // абсолютный путь как есть, относительный — рядом с DSL (или с редактируемым объектом), + // затем в текущем каталоге. Разъедется — и один навык начнёт искать не там, где другой. + { + name: 'значение из файла: resolve_text_from_file', py: 'resolve_text_from_file', ps1: 'Resolve-TextFromFile', + variants: [ + { id: 'base', authority: 'skd-edit', + consumers: ['form-compile', 'role-compile', 'role-edit', 'skd-compile'] }, + ], + }, { name: 'case-insensitive input: CIDict', py: 'CIDict', ps1: null, variants: [