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
@@ -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<restrictionTemplate>"
X "`t`t<name>$(Esc-XmlText "$($tpl.name)")</name>"
X "`t`t<condition>$(Esc-XmlText (Resolve-TextValue "$($tpl.condition)"))</condition>"
X "`t`t<condition>$(Esc-XmlText (Resolve-TextFromFile "$($tpl.condition)" $script:textBaseDir))</condition>"
X "`t</restrictionTemplate>"
$templateCount++
}
@@ -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<restrictionTemplate>')
lines.append(f'\t\t<name>{esc_xml_text(str(tpl["name"]))}</name>')
lines.append(f'\t\t<condition>{esc_xml_text(resolve_text_value(str(tpl["condition"])))}</condition>')
lines.append(f'\t\t<condition>{esc_xml_text(resolve_text_from_file(str(tpl["condition"]), TEXT_BASE_DIR))}</condition>')
lines.append('\t</restrictionTemplate>')
template_count += 1