fix(role-edit): @файл читается в позиции значения, а не вместо всей строки

Было неинтуитивно и вдобавок молча портило файл: `@условие.txt` целиком
заменял -Value, поэтому в файле должен был лежать ещё и адрес, а
естественная форма "Catalog.Товары.Read: @условие.txt" записывала в
условие литерал "@условие.txt" — без единого предупреждения.

Теперь файл читается там, где стоит текст: условие 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 17:56:39 +03:00
co-authored by Claude Opus 5
parent 2bd624a66f
commit 9c14715359
21 changed files with 1014 additions and 41 deletions
+12 -3
View File
@@ -20,7 +20,7 @@ allowed-tools:
|----------|----------|
| `RolePath` | Каталог роли, `Roles/Имя.xml` или `Roles/Имя/Ext/Rights.xml` |
| `Operation` | Операция из таблицы ниже (альтернатива `DefinitionFile`) |
| `Value` | Значение операции. Пакет через `;;`. `@путь` — взять значение из файла |
| `Value` | Значение операции. Пакет через `;;`. Текст можно взять из файла: `@путь` в позиции значения |
| `DefinitionFile` | JSON-массив операций `[{ "operation": "...", "value": "..." }]` |
| `NoValidate` | Не запускать `role-validate` после правки |
@@ -86,10 +86,19 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-edit.ps1" -Rol
... -Operation set-rls -Value "Catalog.Товары.Read[Ссылка, Код, Наименование]: "
```
Строка с тем же набором полей заменяется, с другим — добавляется. Многострочное условие передавай
файлом: `-Value "@условие.txt"` (в этом режиме `;;` считается текстом, а не разделителем пакета).
Строка с тем же набором полей заменяется, с другим — добавляется.
Ограничение на невыданное право — ошибка: платформа такое молча игнорирует.
Длинное условие держи в файле — адрес остаётся в команде, из файла приходит только текст:
```powershell
... -Operation set-rls -Value "Catalog.Товары.Read: @условие.txt"
... -Operation add-template -Value "ПоОрганизации(Мод): @условие.txt"
```
Так же берётся текст у `set-synonym` и `set-comment`: `-Value "@текст.txt"`. Разделитель пакета
`;;` внутри файла остаётся обычным текстом.
Ссылка на шаблон — `#ИмяШаблона("")`; `&` в условиях экранируется автоматически.
## Верификация
+14 -15
View File
@@ -1405,18 +1405,18 @@ $script:rightsPath = $script:paths.RightsPath
$script:roleXmlPath = $script:paths.RoleXmlPath
$script:configRoot = $script:paths.ConfigRoot
# -Value "@путь" — содержимое берётся из файла: так передают многострочное условие RLS,
# которое инлайном ломается о кавычки и о разделитель пакета.
$script:valueFromFile = $false
if ($Value -and $Value.StartsWith("@")) {
$valueFile = $Value.Substring(1)
# "@путь" в позиции ТЕКСТА (условие 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)) {
[Console]::Error.WriteLine("[role-edit] Файл значения не найден: $valueFile")
exit 1
Add-ValidationError "Файл значения не найден: $valueFile"
return $text
}
$Value = [System.IO.File]::ReadAllText($valueFile).Trim()
$script:valueFromFile = $true
return [System.IO.File]::ReadAllText($valueFile).Trim()
}
if ($DefinitionFile -and $Operation) {
@@ -1453,8 +1453,7 @@ function Add-Note([string]$text) { $script:notes += $text }
# --- Разбор значений операций ---
function Parse-BatchValue([string]$val) {
# Значение из файла не режем: там живут многострочные условия, в которых ';;' — просто текст.
if ($script:valueFromFile) { return ,@($val) }
# Делим ДО чтения файлов, поэтому ';;' внутри условия из файла разделителем не становится.
return @($val -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
@@ -1539,7 +1538,7 @@ function Parse-RlsAddress([string]$text, [switch]$ConditionRequired) {
Add-ValidationError "$text : разобрано как объект '$objName' и право '$rightName'"
return $null
}
return @{ Object = $objName; Right = $rightName; Fields = $fields; Condition = $condition }
return @{ Object = $objName; Right = $rightName; Fields = $fields; Condition = (Resolve-TextValue $condition) }
}
# "Имя(Пар1, Пар2): условие" — скобки принадлежат имени шаблона, разделитель ищем вне них.
@@ -1550,7 +1549,7 @@ function Parse-TemplateSpec([string]$text, [switch]$NameOnly) {
Add-ValidationError "$text : ожидается 'Имя(Параметры): условие'"
return $null
}
return @{ Name = $split.Left; Condition = $split.Right }
return @{ Name = $split.Left; Condition = (Resolve-TextValue $split.Right) }
}
# --- Доступ к дереву прав ---
@@ -2222,8 +2221,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 = $opValue } }
"set-comment" { $script:pendingMeta += ,@{ Field = 'Comment'; Text = $opValue } }
"set-synonym" { $script:pendingMeta += ,@{ Field = 'Synonym'; Text = (Resolve-TextValue $opValue) } }
"set-comment" { $script:pendingMeta += ,@{ Field = 'Comment'; Text = (Resolve-TextValue $opValue) } }
default {
Add-ValidationError "Неизвестная операция: $opName"
}
+24 -23
View File
@@ -1461,9 +1461,8 @@ def resolve_role_paths(input_path):
class Editor:
"""Состояние правки: дерево прав, счётчики, отложенные операции."""
def __init__(self, paths, value_from_file):
def __init__(self, paths):
self.paths = paths
self.value_from_file = value_from_file
parser = etree.XMLParser(remove_blank_text=False)
self.tree = etree.parse(paths["RightsPath"], parser)
self.root = self.tree.getroot()
@@ -1484,11 +1483,25 @@ class Editor:
# --- Разбор значений операций ---
def parse_batch(self, value):
# Значение из файла не режем: там живут многострочные условия, в которых ';;' — просто текст.
if self.value_from_file:
return [value]
# Делим ДО чтения файлов, поэтому ';;' внутри условия из файла разделителем не становится.
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):
depth = 0
@@ -1552,7 +1565,8 @@ class Editor:
# Показываем разбор: иначе непонятно, что навык откусил не тот сегмент.
add_validation_error(f"{text} : разобрано как объект '{obj_name}' и право '{right_name}'")
return None
return {"Object": obj_name, "Right": right_name, "Fields": fields, "Condition": condition}
return {"Object": obj_name, "Right": right_name, "Fields": fields,
"Condition": self.resolve_text_value(condition)}
def parse_template_spec(self, text, name_only=False):
left, right, found = self.split_at_top_level_colon(text, "(", ")")
@@ -1561,7 +1575,7 @@ class Editor:
if not found:
add_validation_error(f"{text} : ожидается 'Имя(Параметры): условие'")
return None
return {"Name": left, "Condition": right}
return {"Name": left, "Condition": self.resolve_text_value(right)}
# --- Доступ к дереву прав ---
@@ -2064,20 +2078,7 @@ def main():
paths = resolve_role_paths(args.RolePath)
# -Value "@путь" — содержимое берётся из файла: так передают многострочное условие RLS,
# которое инлайном ломается о кавычки и о разделитель пакета.
value = args.Value
value_from_file = False
if value and value.startswith("@"):
value_file = value[1:]
if not os.path.isabs(value_file):
value_file = os.path.join(os.getcwd(), value_file)
if not os.path.isfile(value_file):
print(f"[role-edit] Файл значения не найден: {value_file}", file=sys.stderr)
sys.exit(1)
with open(value_file, encoding="utf-8-sig") as f:
value = f.read().strip()
value_from_file = True
if args.DefinitionFile and args.Operation:
print("[role-edit] Укажите либо -DefinitionFile, либо -Operation, но не оба сразу", file=sys.stderr)
@@ -2089,7 +2090,7 @@ 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, value_from_file)
ed = Editor(paths)
operations = []
if args.DefinitionFile:
@@ -2148,9 +2149,9 @@ def main():
continue
pending.append((key, {"Name": canonical, "Value": val}))
elif key == "set-synonym":
pending.append((key, {"Field": "Synonym", "Text": op_value}))
pending.append((key, {"Field": "Synonym", "Text": ed.resolve_text_value(op_value)}))
elif key == "set-comment":
pending.append((key, {"Field": "Comment", "Text": op_value}))
pending.append((key, {"Field": "Comment", "Text": ed.resolve_text_value(op_value)}))
else:
add_validation_error(f"Неизвестная операция: {op_name}")