refactor(skd-compile,skd-edit,form-compile,role-*): одно правило поиска @файла

Чтение текста из файла жило тремя копиями под именем Resolve-QueryValue
и вне реестра гарда, а role-* завели четвёртое правило — только от
рабочего каталога. Теперь функция одна на пять навыков:
Resolve-TextFromFile / resolve_text_from_file, заведена семья в
check-inline-drift, эталон — skd-edit.

Правило поиска общее: абсолютный путь как есть, относительный — рядом с
DSL (у edit-навыков — рядом с редактируемым объектом), затем в текущем
каталоге, не нашли — ошибка со списком проверенных мест. Имя без "query":
в ролях из файла приходит условие RLS, а не запрос. Текст ошибки
приведён к языку остальных сообщений.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGkXwoXTuafcu1SXMsauFq
This commit is contained in:
Nick Shirokov
2026-09-13 18:23:06 +03:00
co-authored by Claude Opus 5
parent 121c9c8ad3
commit d425833709
12 changed files with 173 additions and 102 deletions
+31 -19
View File
@@ -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"
}
+35 -23
View File
@@ -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}")