From 229e66b90739fc622707b90bfc4405d9a3452142 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Fri, 21 Aug 2026 16:41:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(skills):=20=D1=87=D1=82=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=D1=85=D0=BE=D0=B4=D0=BD=D0=BE=D0=B3=D0=BE=20JSO?= =?UTF-8?q?N=20=E2=80=94=20=D0=BA=D0=BE=D0=B4=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=BA=D0=B0=20=D0=B8=D0=B7=20BOM,=20=D1=8D=D1=85=D0=BE=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D0=B4=D0=BB=D1=8F=20inl?= =?UTF-8?q?ine-=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20(#80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Этажом ниже разбора, на чтении файла, жил тот же класс дефектов в худшей форме. Файл в cp1251 с кириллицей: PS1 `Get-Content -Encoding UTF8` менял имя на 12 символов U+FFFD, JSON после этого разбирался УСПЕШНО, и навык создавал объект с именем из «замен» — молча. Py-порт на том же файле падал traceback-ом. Файл в UTF-16 давал ту же пару: traceback против ложного «JSON must have 'type' field». Новая семья read_json_file / Read-JsonInputFile: кодировка берётся из BOM (UTF-8, UTF-16 LE/BE), без BOM — строгий UTF-8, при провале сообщение называет файл и байт. Кодовую страницу не подбираем: угаданное имя уехало бы в метаданные так же молча. Эхо полученного значения печатается теперь только для inline-входа. Для файла оно показывало первые 60 символов первой строки независимо от того, что ошибка на 120-й, и спорило с позицией от парсера. Заодно уравнен пустой вход: PS 5.1 на пустой строке отдаёт $null, а не ошибку, и навык уходил дальше с $null, тогда как py-порт падал. Раннеру добавлен ключ inputEncoding (utf-16le / utf-16be / cp1251) — иначе кейс про кодировку не выразить, writeFileSync пишет только UTF-8. Co-Authored-By: Claude Opus 5 --- .claude/skills/cf-edit/scripts/cf-edit.ps1 | 53 +++- .claude/skills/cf-edit/scripts/cf-edit.py | 55 +++- .../form-compile/scripts/form-compile.ps1 | 53 +++- .../form-compile/scripts/form-compile.py | 57 ++-- .../skills/form-edit/scripts/form-edit.ps1 | 49 +++- .claude/skills/form-edit/scripts/form-edit.py | 51 +++- .../interface-edit/scripts/interface-edit.ps1 | 59 +++- .../interface-edit/scripts/interface-edit.py | 61 +++-- .../meta-compile/scripts/meta-compile.ps1 | 49 +++- .../meta-compile/scripts/meta-compile.py | 51 +++- .../skills/meta-edit/scripts/meta-edit.ps1 | 49 +++- .claude/skills/meta-edit/scripts/meta-edit.py | 51 +++- .../mxl-compile/scripts/mxl-compile.ps1 | 49 +++- .../skills/mxl-compile/scripts/mxl-compile.py | 51 +++- .../role-compile/scripts/role-compile.ps1 | 49 +++- .../role-compile/scripts/role-compile.py | 51 +++- .../skd-compile/scripts/skd-compile.ps1 | 55 +++- .../skills/skd-compile/scripts/skd-compile.py | 58 +++- .../skd-decompile/scripts/skd-decompile.ps1 | 49 +++- .../skd-decompile/scripts/skd-decompile.py | 51 +++- .../scripts/subsystem-compile.ps1 | 53 +++- .../scripts/subsystem-compile.py | 55 +++- .../subsystem-edit/scripts/subsystem-edit.ps1 | 53 +++- .../subsystem-edit/scripts/subsystem-edit.py | 55 +++- tests/skills/README.md | 1 + .../cases/interface-edit/empty-value.json | 25 ++ .../meta-compile/bad-encoding-cp1251.json | 11 + .../meta-compile/encoding-utf16-bom.json | 13 + .../Catalogs/Номенклатура.xml | 91 +++++++ .../Номенклатура/Ext/ObjectModule.bsl | 0 .../encoding-utf16-bom/Configuration.xml | 252 ++++++++++++++++++ .../Ext/ClientApplicationInterface.xml | 18 ++ .../encoding-utf16-bom/Languages/Русский.xml | 16 ++ tests/skills/check-inline-drift.mjs | 8 + tests/skills/runner.mjs | 36 ++- 35 files changed, 1475 insertions(+), 263 deletions(-) create mode 100644 tests/skills/cases/interface-edit/empty-value.json create mode 100644 tests/skills/cases/meta-compile/bad-encoding-cp1251.json create mode 100644 tests/skills/cases/meta-compile/encoding-utf16-bom.json create mode 100644 tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура/Ext/ObjectModule.bsl create mode 100644 tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Configuration.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Ext/ClientApplicationInterface.xml create mode 100644 tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Languages/Русский.xml diff --git a/.claude/skills/cf-edit/scripts/cf-edit.ps1 b/.claude/skills/cf-edit/scripts/cf-edit.ps1 index 60ddc4b02..6c376d319 100644 --- a/.claude/skills/cf-edit/scripts/cf-edit.ps1 +++ b/.claude/skills/cf-edit/scripts/cf-edit.ps1 @@ -1,4 +1,4 @@ -# cf-edit v1.20 — Edit 1C configuration root (Configuration.xml) +# cf-edit v1.21 — Edit 1C configuration root (Configuration.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][Alias('Path')][string]$ConfigPath, @@ -14,22 +14,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Mode validation --- @@ -659,7 +692,7 @@ function Do-SetPanels($valArg) { # Accept string (JSON), PSCustomObject, or hashtable $layout = $valArg if ($layout -is [string]) { - $layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-panels'" "a JSON object with panel layout" + $layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-panels'" "a JSON object with panel layout" -Inline } if (-not $layout) { Write-Error "set-panels value is empty" @@ -843,7 +876,7 @@ $indent function Do-SetHomePage($valArg) { $layout = $valArg if ($layout -is [string]) { - $layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-home-page'" "a JSON object with home page layout" + $layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-home-page'" "a JSON object with home page layout" -Inline } if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 } @@ -957,7 +990,7 @@ if ($DefinitionFile) { if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $jsonText = Read-JsonInputFile $DefinitionFile $ops = ConvertFrom-JsonInput $jsonText $DefinitionFile if ($ops -is [System.Array]) { foreach ($op in $ops) { $operations += $op } diff --git a/.claude/skills/cf-edit/scripts/cf-edit.py b/.claude/skills/cf-edit/scripts/cf-edit.py index def75d863..2c813bfba 100644 --- a/.claude/skills/cf-edit/scripts/cf-edit.py +++ b/.claude/skills/cf-edit/scripts/cf-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# cf-edit v1.20 — Edit 1C configuration root (Configuration.xml) +# cf-edit v1.21 — Edit 1C configuration root (Configuration.xml) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -15,11 +15,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -27,15 +28,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -847,7 +877,7 @@ def main(): layout = value if isinstance(layout, str): layout = ci_json(parse_json_input( - layout, "-Value for operation 'set-panels'", "a JSON object with panel layout")) + layout, "-Value for operation 'set-panels'", "a JSON object with panel layout", inline=True)) if not isinstance(layout, dict) or not layout: print("set-panels value must be non-empty object", file=sys.stderr) sys.exit(1) @@ -999,7 +1029,7 @@ def main(): layout = value if isinstance(layout, str): layout = ci_json(parse_json_input( - layout, "-Value for operation 'set-home-page'", "a JSON object with home page layout")) + layout, "-Value for operation 'set-home-page'", "a JSON object with home page layout", inline=True)) if not isinstance(layout, dict) or not layout: print("set-home-page value must be non-empty object", file=sys.stderr) sys.exit(1) @@ -1063,8 +1093,7 @@ def main(): def_file = args.DefinitionFile if not os.path.isabs(def_file): def_file = os.path.join(os.getcwd(), def_file) - with open(def_file, "r", encoding="utf-8-sig") as fh: - ops = ci_json(parse_json_input(fh.read(), def_file)) + ops = ci_json(parse_json_input(read_json_file(def_file), def_file)) if isinstance(ops, list): operations = ops else: diff --git a/.claude/skills/form-compile/scripts/form-compile.ps1 b/.claude/skills/form-compile/scripts/form-compile.ps1 index 1c30ddceb..4181de028 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.193 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) +# form-compile v1.194 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$JsonPath, @@ -18,22 +18,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # ═══════════════════════════════════════════════════════════════════════════ @@ -320,7 +353,7 @@ function Load-Preset([string]$PresetName, [string]$ScriptDir) { $presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets" $builtInPath = Join-Path $presetDir "$PresetName.json" if (Test-Path $builtInPath) { - $presetJson = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $builtInPath) $builtInPath + $presetJson = ConvertFrom-JsonInput (Read-JsonInputFile $builtInPath) $builtInPath # Convert PSCustomObject to hashtable recursively $toHash = { param($obj) @@ -347,7 +380,7 @@ function Load-Preset([string]$PresetName, [string]$ScriptDir) { while ($scanDir) { $projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json" if (Test-Path $projPreset) { - $projJson = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $projPreset) $projPreset + $projJson = ConvertFrom-JsonInput (Read-JsonInputFile $projPreset) $projPreset $projHash = & $toHash $projJson foreach ($k in @($projHash.Keys)) { $defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k] @@ -1675,7 +1708,7 @@ if ($FromObject) { exit 1 } - $json = Get-Content -Raw -Encoding UTF8 $JsonPath + $json = Read-JsonInputFile $JsonPath $def = ConvertFrom-JsonInput $json $JsonPath } diff --git a/.claude/skills/form-compile/scripts/form-compile.py b/.claude/skills/form-compile/scripts/form-compile.py index 6361e1c64..e30d8394c 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.193 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) +# form-compile v1.194 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import copy @@ -16,11 +16,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -28,15 +29,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -567,8 +597,7 @@ def load_preset(preset_name, script_dir, out_path_resolved): preset_dir = os.path.join(os.path.dirname(script_dir), 'presets') built_in_path = os.path.join(preset_dir, f'{preset_name}.json') if os.path.isfile(built_in_path): - with open(built_in_path, 'r', encoding='utf-8-sig') as f: - preset_data = ci_json(parse_json_input(f.read(), built_in_path)) + preset_data = ci_json(parse_json_input(read_json_file(built_in_path), built_in_path)) for k in list(preset_data.keys()): defaults[k] = _deep_merge(defaults.get(k), preset_data[k]) @@ -577,8 +606,7 @@ def load_preset(preset_name, script_dir, out_path_resolved): while scan_dir: proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json') if os.path.isfile(proj_preset): - with open(proj_preset, 'r', encoding='utf-8-sig') as f: - proj_data = parse_json_input(f.read(), proj_preset) + proj_data = parse_json_input(read_json_file(proj_preset), proj_preset) for k in list(proj_data.keys()): defaults[k] = _deep_merge(defaults.get(k), proj_data[k]) break @@ -6480,8 +6508,7 @@ def main(): print(f"File not found: {json_path}", file=sys.stderr) sys.exit(1) - with open(json_path, 'r', encoding='utf-8-sig') as f: - defn = ci_json(parse_json_input(f.read(), json_path)) + defn = ci_json(parse_json_input(read_json_file(json_path), json_path)) global QUERY_BASE_DIR QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path)) diff --git a/.claude/skills/form-edit/scripts/form-edit.ps1 b/.claude/skills/form-edit/scripts/form-edit.ps1 index 702ebfb66..6954f3642 100644 --- a/.claude/skills/form-edit/scripts/form-edit.ps1 +++ b/.claude/skills/form-edit/scripts/form-edit.ps1 @@ -1,4 +1,4 @@ -# form-edit v1.15 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста) +# form-edit v1.16 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -14,22 +14,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Support guard (Ext/ParentConfigurations.bin) --- @@ -195,7 +228,7 @@ $root = $xmlDoc.DocumentElement # === 2. Load JSON === -$def = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $JsonPath) $JsonPath +$def = ConvertFrom-JsonInput (Read-JsonInputFile $JsonPath) $JsonPath # === 3. Form name + header === diff --git a/.claude/skills/form-edit/scripts/form-edit.py b/.claude/skills/form-edit/scripts/form-edit.py index f697e9c2b..b0a0980a8 100644 --- a/.claude/skills/form-edit/scripts/form-edit.py +++ b/.claude/skills/form-edit/scripts/form-edit.py @@ -1,4 +1,4 @@ -# form-edit v1.15 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста) +# form-edit v1.16 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -14,11 +14,12 @@ sys.stderr.reconfigure(encoding="utf-8") # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -26,15 +27,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -342,8 +372,7 @@ root = tree.getroot() # ── 2. Load JSON ──────────────────────────────────────────── -with open(json_path, "r", encoding="utf-8-sig") as f: - defn = ci_json(parse_json_input(f.read(), json_path)) +defn = ci_json(parse_json_input(read_json_file(json_path), json_path)) # ── 3. Form name + header ─────────────────────────────────── diff --git a/.claude/skills/interface-edit/scripts/interface-edit.ps1 b/.claude/skills/interface-edit/scripts/interface-edit.ps1 index 77c4b8227..2478e245f 100644 --- a/.claude/skills/interface-edit/scripts/interface-edit.ps1 +++ b/.claude/skills/interface-edit/scripts/interface-edit.ps1 @@ -1,4 +1,4 @@ -# interface-edit v1.19 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без) +# interface-edit v1.20 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][Alias('Path')][string]$CIPath, @@ -20,23 +20,56 @@ if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -Definition # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} + # --- Resolve path --- if (-not [System.IO.Path]::IsPathRooted($CIPath)) { $CIPath = Join-Path (Get-Location).Path $CIPath @@ -374,7 +407,7 @@ function Ensure-Section([string]$sectionName) { function Parse-ValueList([string]$val, [string]$opName) { $val = $val.Trim() if ($val.StartsWith("[")) { - $arr = ConvertFrom-JsonInput $val "-Value for operation '$opName'" "a JSON array of command names" + $arr = ConvertFrom-JsonInput $val "-Value for operation '$opName'" "a JSON array of command names" -Inline $result = @(); foreach ($item in $arr) { $result += "$item" } return ,$result } @@ -539,7 +572,7 @@ function Do-Show([string[]]$commands) { } function Do-Place([string]$jsonVal) { - $def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'place'" "a JSON object {command, group}" + $def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'place'" "a JSON object {command, group}" -Inline $cmdName = Normalize-CmdName "$($def.command)" $groupName = "$($def.group)" if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 } @@ -572,7 +605,7 @@ function Do-Place([string]$jsonVal) { } function Do-Order([string]$jsonVal) { - $def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'order'" "a JSON object {group, commands:[...]}" + $def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'order'" "a JSON object {group, commands:[...]}" -Inline $groupName = "$($def.group)" $commands = @($def.commands | ForEach-Object { Normalize-CmdName "$_" }) if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 } @@ -610,7 +643,7 @@ function Do-Order([string]$jsonVal) { } function Do-SubsystemOrder([string]$jsonVal) { - $parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'subsystem-order'" "a JSON array of subsystem paths" + $parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'subsystem-order'" "a JSON array of subsystem paths" -Inline $subsystems = @(); foreach ($s in $parsed) { $subsystems += "$s" } if ($subsystems.Count -eq 0) { Write-Error "subsystem-order requires array of subsystem paths"; exit 1 } @@ -638,7 +671,7 @@ function Do-SubsystemOrder([string]$jsonVal) { } function Do-GroupOrder([string]$jsonVal) { - $parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'group-order'" "a JSON array of group names" + $parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'group-order'" "a JSON array of group names" -Inline $groups = @(); foreach ($g in $parsed) { $groups += "$g" } if ($groups.Count -eq 0) { Write-Error "group-order requires array of group names"; exit 1 } @@ -671,7 +704,7 @@ if ($DefinitionFile) { if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $jsonText = Read-JsonInputFile $DefinitionFile $ops = ConvertFrom-JsonInput $jsonText $DefinitionFile if ($ops -is [System.Array]) { foreach ($op in $ops) { $operations += $op } diff --git a/.claude/skills/interface-edit/scripts/interface-edit.py b/.claude/skills/interface-edit/scripts/interface-edit.py index dadbdaebe..c0dfa6128 100644 --- a/.claude/skills/interface-edit/scripts/interface-edit.py +++ b/.claude/skills/interface-edit/scripts/interface-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# interface-edit v1.19 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без) +# interface-edit v1.20 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -348,11 +348,12 @@ def import_ci_fragment(xml_string): return nodes -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -360,22 +361,51 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) def parse_value_list(val, op_name): val = val.strip() if val.startswith("["): - arr = ci_json(parse_json_input(val, "-Value for operation '%s'" % op_name, "a JSON array of command names")) + arr = ci_json(parse_json_input(val, "-Value for operation '%s'" % op_name, "a JSON array of command names", inline=True)) return [str(item) for item in arr] return [val] @@ -672,7 +702,7 @@ def main(): def do_place(json_val): nonlocal add_count, modify_count defn = ci_json(json_val if isinstance(json_val, dict) else parse_json_input( - json_val, "-Value for operation 'place'", "a JSON object {command, group}")) + json_val, "-Value for operation 'place'", "a JSON object {command, group}", inline=True)) cmd_name = normalize_cmd_name(str(defn["command"])) group_name = str(defn["group"]) if not cmd_name or not group_name: @@ -701,7 +731,7 @@ def main(): def do_order(json_val): nonlocal add_count, remove_count defn = ci_json(json_val if isinstance(json_val, dict) else parse_json_input( - json_val, "-Value for operation 'order'", "a JSON object {group, commands:[...]}")) + json_val, "-Value for operation 'order'", "a JSON object {group, commands:[...]}", inline=True)) group_name = str(defn["group"]) commands = [normalize_cmd_name(str(c)) for c in defn["commands"]] if not group_name or not commands: @@ -736,7 +766,7 @@ def main(): def do_subsystem_order(json_val): nonlocal add_count, remove_count parsed = ci_json(json_val if isinstance(json_val, list) else parse_json_input( - json_val, "-Value for operation 'subsystem-order'", "a JSON array of subsystem paths")) + json_val, "-Value for operation 'subsystem-order'", "a JSON array of subsystem paths", inline=True)) subsystems = [str(s) for s in parsed] if not subsystems: print("subsystem-order requires array of subsystem paths", file=sys.stderr) @@ -762,7 +792,7 @@ def main(): def do_group_order(json_val): nonlocal add_count, remove_count parsed = ci_json(json_val if isinstance(json_val, list) else parse_json_input( - json_val, "-Value for operation 'group-order'", "a JSON array of group names")) + json_val, "-Value for operation 'group-order'", "a JSON array of group names", inline=True)) groups = [str(g) for g in parsed] if not groups: print("group-order requires array of group names", file=sys.stderr) @@ -791,8 +821,7 @@ def main(): def_file = args.DefinitionFile if not os.path.isabs(def_file): def_file = os.path.join(os.getcwd(), def_file) - with open(def_file, "r", encoding="utf-8-sig") as fh: - ops = ci_json(parse_json_input(fh.read(), def_file)) + ops = ci_json(parse_json_input(read_json_file(def_file), def_file)) if isinstance(ops, list): operations = ops else: diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index 10b3fe7a9..9a053793a 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,4 +1,4 @@ -# meta-compile v1.96 — Compile 1C metadata object from JSON +# meta-compile v1.97 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -13,22 +13,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- 1. Load and validate JSON --- @@ -38,7 +71,7 @@ if (-not (Test-Path $JsonPath)) { exit 1 } -$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$json = Read-JsonInputFile $JsonPath $def = ConvertFrom-JsonInput $json $JsonPath # --- Support guard (Ext/ParentConfigurations.bin) --- diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index f452df6b9..f0624863f 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-compile v1.96 — Compile 1C metadata object from JSON +# meta-compile v1.97 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -24,11 +24,12 @@ sys.stderr.reconfigure(encoding="utf-8") # ============================================================ -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -36,15 +37,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -399,8 +429,7 @@ if not os.path.isfile(json_path): print(f'File not found: {json_path}', file=sys.stderr) sys.exit(1) -with open(json_path, 'r', encoding='utf-8-sig') as f: - json_text = f.read() +json_text = read_json_file(json_path) defn = ci_json(parse_json_input(json_text, json_path)) diff --git a/.claude/skills/meta-edit/scripts/meta-edit.ps1 b/.claude/skills/meta-edit/scripts/meta-edit.ps1 index 46feb7b0d..133875188 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.ps1 +++ b/.claude/skills/meta-edit/scripts/meta-edit.ps1 @@ -1,4 +1,4 @@ -# meta-edit v1.39 — Edit existing 1C metadata object XML +# meta-edit v1.40 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -35,22 +35,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # ============================================================ @@ -141,7 +174,7 @@ if ($DefinitionFile) { Write-Error "Definition file not found: $DefinitionFile" exit 1 } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $jsonText = Read-JsonInputFile $DefinitionFile $def = ConvertFrom-JsonInput $jsonText $DefinitionFile } diff --git a/.claude/skills/meta-edit/scripts/meta-edit.py b/.claude/skills/meta-edit/scripts/meta-edit.py index af0855fb3..411f7be30 100644 --- a/.claude/skills/meta-edit/scripts/meta-edit.py +++ b/.claude/skills/meta-edit/scripts/meta-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-edit v1.39 — Edit existing 1C metadata object XML +# meta-edit v1.40 — Edit existing 1C metadata object XML # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -14,11 +14,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -26,15 +27,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -3279,8 +3309,7 @@ def main(): if args.DefinitionFile: if not os.path.exists(args.DefinitionFile): die(f"Definition file not found: {args.DefinitionFile}") - with open(args.DefinitionFile, "r", encoding="utf-8-sig") as f: - definition = ci_json(parse_json_input(f.read(), args.DefinitionFile)) + definition = ci_json(parse_json_input(read_json_file(args.DefinitionFile), args.DefinitionFile)) # --- Resolve object path --- object_path = args.ObjectPath diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 3c6f0846c..f79d75ab7 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -1,4 +1,4 @@ -# mxl-compile v1.52 — Compile 1C spreadsheet from JSON +# mxl-compile v1.53 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -13,22 +13,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Support guard (Ext/ParentConfigurations.bin) --- @@ -209,7 +242,7 @@ if (-not (Test-Path $JsonPath)) { exit 1 } -$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$json = Read-JsonInputFile $JsonPath $def = ConvertFrom-JsonInput $json $JsonPath # Проверяем НАЛИЧИЕ ключа, а не истинность значения: `columns: 0` — осмысленная величина diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 83d91f16d..b29af4119 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-compile v1.52 — Compile 1C spreadsheet from JSON +# mxl-compile v1.53 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -14,11 +14,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -26,15 +27,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -829,8 +859,7 @@ def main(): print(f"File not found: {json_path}", file=sys.stderr) sys.exit(1) - with open(json_path, 'r', encoding='utf-8-sig') as f: - defn = ci_json(parse_json_input(f.read(), json_path)) + defn = ci_json(parse_json_input(read_json_file(json_path), json_path)) # Проверяем НАЛИЧИЕ ключа, а не истинность значения: `columns: 0` — осмысленная величина # (раскладка по умолчанию пустая, все строки живут в именованных раскладках), а пустой diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1 index 9d3895176..d911c0bcd 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.29 — Compile 1C role from JSON +# role-compile v1.30 — Compile 1C role from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -13,22 +13,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Support guard (Ext/ParentConfigurations.bin) --- @@ -169,7 +202,7 @@ if (-not (Test-Path $JsonPath)) { exit 1 } -$json = Get-Content -Raw -Encoding UTF8 $JsonPath +$json = Read-JsonInputFile $JsonPath $def = ConvertFrom-JsonInput $json $JsonPath if (-not $def.name) { diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py index fcf1cc0b3..22b972503 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.29 — Compile 1C role from JSON +# role-compile v1.30 — Compile 1C role from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -14,11 +14,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -26,15 +27,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -1106,8 +1136,7 @@ def main(): print(f"File not found: {json_path}", file=sys.stderr) sys.exit(1) - with open(json_path, 'r', encoding='utf-8-sig') as f: - defn = ci_json(parse_json_input(f.read(), json_path)) + defn = ci_json(parse_json_input(read_json_file(json_path), json_path)) if not defn.get('name'): print("JSON must have 'name' field (role programmatic name)", file=sys.stderr) diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 903b57623..cba29be76 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.118 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# skd-compile v1.119 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -12,22 +12,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Support guard (Ext/ParentConfigurations.bin) --- @@ -180,14 +213,16 @@ if ($DefinitionFile) { Write-Error "Definition file not found: $DefinitionFile" exit 1 } - $json = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $json = Read-JsonInputFile $DefinitionFile $jsonSource = $DefinitionFile + $jsonInline = $false } else { $json = $Value $jsonSource = "-Value" + $jsonInline = $true } -$def = ConvertFrom-JsonInput $json $jsonSource +$def = ConvertFrom-JsonInput $json $jsonSource -Inline:$jsonInline # --- Sentinel check: refuse to compile if JSON contains skd-decompile sentinels --- # These mark places the decompiler couldn't reverse cleanly; user must resolve @@ -1828,7 +1863,7 @@ while ($scanDir) { } foreach ($stylesFile in $searchPaths) { if (Test-Path $stylesFile) { - $userStyles = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $stylesFile) $stylesFile + $userStyles = ConvertFrom-JsonInput (Read-JsonInputFile $stylesFile) $stylesFile foreach ($prop in $userStyles.PSObject.Properties) { $preset = @{} # Start from 'data' defaults diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 321fd187b..c8c47948e 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.118 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) +# skd-compile v1.119 — Compile 1C DCS from JSON (+write_xml_file/write_utf8_bom: общий эталон записи) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -13,11 +13,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -25,15 +26,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -1653,8 +1683,7 @@ def load_user_styles(base_dir, output_path=None): scan_dir = parent_dir for p in search_paths: if os.path.isfile(p): - with open(p, 'r', encoding='utf-8-sig') as f: - user_styles = ci_json(parse_json_input(f.read(), p)) + user_styles = ci_json(parse_json_input(read_json_file(p), p)) for name, overrides in user_styles.items(): base = dict(AREA_STYLE_PRESETS.get(name, AREA_STYLE_PRESETS['data'])) base.update(overrides) @@ -3086,14 +3115,15 @@ def main(): if not os.path.exists(def_file): print(f"Definition file not found: {def_file}", file=sys.stderr) sys.exit(1) - with open(def_file, 'r', encoding='utf-8-sig') as f: - json_text = f.read() + json_text = read_json_file(def_file) json_source = def_file + json_inline = False else: json_text = args.Value json_source = "-Value" + json_inline = True - defn = ci_json(parse_json_input(json_text, json_source)) + defn = ci_json(parse_json_input(json_text, json_source, inline=json_inline)) if not defn.get('dataSets') or len(defn['dataSets']) == 0: print("JSON must have at least one entry in 'dataSets'", file=sys.stderr) diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 index 0eaa19418..58872a4da 100644 --- a/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 +++ b/.claude/skills/skd-decompile/scripts/skd-decompile.ps1 @@ -1,4 +1,4 @@ -# skd-decompile v0.93 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# skd-decompile v0.94 — Decompile 1C DCS Template.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -13,22 +13,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- 0. Resolve and validate input --- @@ -1160,7 +1193,7 @@ function Load-UserStyles { if (-not $dirPath) { return } $stylesPath = Join-Path $dirPath 'skd-styles.json' if (-not (Test-Path $stylesPath)) { return } - $raw = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $stylesPath) $stylesPath + $raw = ConvertFrom-JsonInput (Read-JsonInputFile $stylesPath) $stylesPath $script:existingUserPresetsRaw = $raw foreach ($prop in $raw.PSObject.Properties) { # Compile-логика: data defaults → built-in if name match → user keys diff --git a/.claude/skills/skd-decompile/scripts/skd-decompile.py b/.claude/skills/skd-decompile/scripts/skd-decompile.py index d3a64ef7a..fe6014e00 100644 --- a/.claude/skills/skd-decompile/scripts/skd-decompile.py +++ b/.claude/skills/skd-decompile/scripts/skd-decompile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-decompile v0.93 — Decompile 1C DCS Template.xml to JSON DSL (draft) +# skd-decompile v0.94 — Decompile 1C DCS Template.xml to JSON DSL (draft) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -10,11 +10,12 @@ import xml.etree.ElementTree as ET # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -22,15 +23,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -1387,8 +1417,7 @@ def load_user_styles(dir_path): styles_path = os.path.join(dir_path, 'skd-styles.json') if not os.path.exists(styles_path): return - with open(styles_path, 'r', encoding='utf-8-sig') as f: - raw = parse_json_input(f.read(), styles_path) + raw = parse_json_input(read_json_file(styles_path), styles_path) existing_user_presets_raw = raw for prop_name, prop_value in raw.items(): preset = {} diff --git a/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 b/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 index 86ac4cb91..c34db6656 100644 --- a/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 +++ b/.claude/skills/subsystem-compile/scripts/subsystem-compile.ps1 @@ -1,4 +1,4 @@ -# subsystem-compile v1.27 — Create 1C subsystem from JSON definition +# subsystem-compile v1.28 — Create 1C subsystem from JSON definition # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -13,22 +13,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- 1. Load JSON --- @@ -49,14 +82,16 @@ if ($DefinitionFile) { Write-Error "Definition file not found: $DefinitionFile" exit 1 } - $json = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $json = Read-JsonInputFile $DefinitionFile $jsonSource = $DefinitionFile + $jsonInline = $false } else { $json = $Value $jsonSource = "-Value" + $jsonInline = $true } -$def = ConvertFrom-JsonInput $json $jsonSource +$def = ConvertFrom-JsonInput $json $jsonSource -Inline:$jsonInline if (-not $def.name) { Write-Error "JSON must have 'name' field" diff --git a/.claude/skills/subsystem-compile/scripts/subsystem-compile.py b/.claude/skills/subsystem-compile/scripts/subsystem-compile.py index b09a6e610..90ccdcf53 100644 --- a/.claude/skills/subsystem-compile/scripts/subsystem-compile.py +++ b/.claude/skills/subsystem-compile/scripts/subsystem-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# subsystem-compile v1.27 — Create 1C subsystem from JSON definition +# subsystem-compile v1.28 — Create 1C subsystem from JSON definition # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -14,11 +14,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -26,15 +27,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -493,14 +523,15 @@ def main(): if not os.path.exists(def_file): print(f"Definition file not found: {def_file}", file=sys.stderr) sys.exit(1) - with open(def_file, 'r', encoding='utf-8-sig') as f: - json_text = f.read() + json_text = read_json_file(def_file) json_source = def_file + json_inline = False else: json_text = args.Value json_source = "-Value" + json_inline = True - defn = ci_json(parse_json_input(json_text, json_source)) + defn = ci_json(parse_json_input(json_text, json_source, inline=json_inline)) if not defn.get('name'): print("JSON must have 'name' field", file=sys.stderr) diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 index f10e3c816..7561cb33c 100644 --- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 +++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.ps1 @@ -1,4 +1,4 @@ -# subsystem-edit v1.22 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) +# subsystem-edit v1.23 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)][Alias('Path')][string]$SubsystemPath, @@ -14,22 +14,55 @@ $ErrorActionPreference = "Stop" # --- Разбор пользовательского JSON --- # Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу # идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только -# для полиморфного входа: у файла подсказка была бы наполнителем. +# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то, +# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске. # Возврат через -NoEnumerate: без него одноэлементный # JSON-массив разворачивался бы в скаляр вторым анруллингом. -function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) { +function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) { try { + # PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null, + # тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково. + if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' } $parsed = $text | ConvertFrom-Json } catch { $what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" } - $got = ($text -replace '\s+', ' ').Trim() - $label = 'got' - if ($got.Length -gt 60) { $label = 'got (first 60 chars, whitespace collapsed)'; $got = $got.Substring(0, 60) } - [Console]::Error.WriteLine("[ERROR] ${what}, ${label}: ${got} ($($_.Exception.Message))") + if ($Inline) { + $got = ($text -replace '\s+', ' ').Trim() + $label = 'got' + if (-not $got) { $got = '(empty)' } + elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) } + $what = "${what}, ${label}: ${got}" + } + [Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))") exit 1 } Write-Output -NoEnumerate $parsed } + +# --- Чтение входного JSON-файла --- +# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8: +# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого +# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем: +# угаданное имя уйдёт в метаданные так же молча. +function Read-JsonInputFile([string]$path) { + $bytes = [System.IO.File]::ReadAllBytes($path) + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) { + return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2) + } + if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) { + return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2) + } + try { + return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes) + } catch { + $detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message } + [Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16") + exit 1 + } +} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # --- Content type normalization (plural→singular, Russian→English) --- @@ -452,7 +485,7 @@ function Expand-SelfClosingElement($container, $parentIndent) { function Parse-ValueList([string]$val, [string]$opName) { $val = $val.Trim() if ($val.StartsWith("[")) { - $arr = ConvertFrom-JsonInput $val "-Value for operation '$opName'" "a JSON array of object names" + $arr = ConvertFrom-JsonInput $val "-Value for operation '$opName'" "a JSON array of object names" -Inline $result = @(); foreach ($item in $arr) { $result += "$item" } return ,$result } @@ -585,7 +618,7 @@ function Do-RemoveChild([string]$childName) { } function Do-SetProperty([string]$jsonVal) { - $propDef = ConvertFrom-JsonInput $jsonVal "-Value for operation 'set-property'" "a JSON object {name, value}" + $propDef = ConvertFrom-JsonInput $jsonVal "-Value for operation 'set-property'" "a JSON object {name, value}" -Inline $propName = "$($propDef.name)" $propValue = "$($propDef.value)" @@ -658,7 +691,7 @@ if ($DefinitionFile) { if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) { $DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile } - $jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile + $jsonText = Read-JsonInputFile $DefinitionFile $ops = ConvertFrom-JsonInput $jsonText $DefinitionFile if ($ops -is [System.Array]) { foreach ($op in $ops) { $operations += $op } diff --git a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py index 6d7e1df69..2fc257115 100644 --- a/.claude/skills/subsystem-edit/scripts/subsystem-edit.py +++ b/.claude/skills/subsystem-edit/scripts/subsystem-edit.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# subsystem-edit v1.22 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) +# subsystem-edit v1.23 — Edit existing 1C subsystem XML (+тип Bot; cfe-diff/cfe-borrow: недостающие типы) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -14,11 +14,12 @@ from lxml import etree # Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet] # регистр не различают, в argparse совпадение точное. -def parse_json_input(text, source, expected=None): +def parse_json_input(text, source, expected=None, inline=False): """Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80). expected заполняем только для полиморфного входа: у файла подсказка - была бы наполнителем — имя файла и текст парсера самодостаточны. + была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то, + что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком. Импорты внутри тела: копия функции живёт в навыках с разными именами модулей (skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым. @@ -26,15 +27,44 @@ def parse_json_input(text, source, expected=None): import json as _pj import sys as _psys try: + if not str(text).strip(): + raise ValueError("input is empty") return _pj.loads(text) except ValueError as exc: what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source - got = " ".join(str(text).split()) - label = "got" - if len(got) > 60: - label = "got (first 60 chars, whitespace collapsed)" - got = got[:60] - print("[ERROR] %s, %s: %s (%s)" % (what, label, got, exc), file=_psys.stderr) + if inline: + got = " ".join(str(text).split()) + label = "got" + if not got: + got = "(empty)" + elif len(got) > 60: + label = "got (first 60 chars)" + got = got[:60] + what = "%s, %s: %s" % (what, label, got) + print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr) + _psys.exit(1) + + +def read_json_file(path): + """Чтение входного JSON-файла с кодировкой из BOM (issue #80). + + BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую + страницу не подбираем: угаданное имя уехало бы в метаданные молча. + """ + import sys as _psys + with open(path, "rb") as _fh: + data = _fh.read() + if data[:3] == b"\xef\xbb\xbf": + return data[3:].decode("utf-8") + if data[:2] == b"\xff\xfe": + return data[2:].decode("utf-16-le") + if data[:2] == b"\xfe\xff": + return data[2:].decode("utf-16-be") + try: + return data.decode("utf-8") + except UnicodeDecodeError as exc: + print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16" + % (path, exc), file=_psys.stderr) _psys.exit(1) @@ -544,7 +574,7 @@ def parse_value_list(val, op_name): """Parse a string or JSON array into a list of strings.""" val = val.strip() if val.startswith("["): - arr = ci_json(parse_json_input(val, "-Value for operation '%s'" % op_name, "a JSON array of object names")) + arr = ci_json(parse_json_input(val, "-Value for operation '%s'" % op_name, "a JSON array of object names", inline=True)) return [str(item) for item in arr] return [val] @@ -803,7 +833,7 @@ def main(): def do_set_property(json_val): nonlocal modify_count prop_def = ci_json(parse_json_input( - json_val, "-Value for operation 'set-property'", "a JSON object {name, value}")) + json_val, "-Value for operation 'set-property'", "a JSON object {name, value}", inline=True)) prop_name = str(prop_def["name"]) prop_value = str(prop_def.get("value", "")) @@ -899,8 +929,7 @@ def main(): def_file = args.DefinitionFile if not os.path.isabs(def_file): def_file = os.path.join(os.getcwd(), def_file) - with open(def_file, "r", encoding="utf-8-sig") as fh: - ops = ci_json(parse_json_input(fh.read(), def_file)) + ops = ci_json(parse_json_input(read_json_file(def_file), def_file)) if isinstance(ops, list): operations = ops else: diff --git a/tests/skills/README.md b/tests/skills/README.md index ab91cfbd6..ed6284266 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -296,6 +296,7 @@ ibcmd-проход автоматически `○ skipped`, если рядом | `name` | да | Название теста (отображается в отчёте) | | `input` | нет | JSON-объект, передаётся навыку через temp-файл | | `inputRaw` | нет | Строка, пишется во входной файл дословно. Нужна для негативных кейсов про битый JSON: через `input` такой вход невыразим — `JSON.stringify` всегда даёт валидный документ. Приоритетнее `input` | +| `inputEncoding` | нет | Кодировка входного файла: `utf-16le` / `utf-16be` (пишутся с BOM) / `cp1251`. Без ключа — UTF-8. Нужна для кейсов, где навык обязан принять кодировку, объявленную BOM, и отвергнуть не-UTF-8 без BOM. Работает и с `input`, и с `inputRaw` | | `params` | нет | Параметры для `case.` и `workPath` маппинга | | `setup` | нет | Переопределение setup из `_skill.json` | | `outputPath` | нет | Относительный путь для навыков с `-OutputPath` | diff --git a/tests/skills/cases/interface-edit/empty-value.json b/tests/skills/cases/interface-edit/empty-value.json new file mode 100644 index 000000000..eff57d7d9 --- /dev/null +++ b/tests/skills/cases/interface-edit/empty-value.json @@ -0,0 +1,25 @@ +{ + "name": "Пустое значение операции: в сообщении (empty), а не висячее двоеточие", + "preRun": [ + { + "script": "subsystem-compile/scripts/subsystem-compile", + "input": { + "name": "Продажи" + }, + "args": { + "-DefinitionFile": "{inputFile}", + "-OutputDir": "{workDir}" + } + } + ], + "params": { + "ciPath": "Subsystems/Продажи/CommandInterface" + }, + "input": [ + { + "operation": "place", + "value": "" + } + ], + "expectError": "got: (empty)" +} diff --git a/tests/skills/cases/meta-compile/bad-encoding-cp1251.json b/tests/skills/cases/meta-compile/bad-encoding-cp1251.json new file mode 100644 index 000000000..af67661b5 --- /dev/null +++ b/tests/skills/cases/meta-compile/bad-encoding-cp1251.json @@ -0,0 +1,11 @@ +{ + "name": "Файл в cp1251 отвергается, а не читается как U+FFFD (issue #80)", + "inputRaw": "{\"type\": \"Catalog\", \"name\": \"Номенклатура\"}", + "inputEncoding": "cp1251", + "expectError": "is not valid UTF-8", + "expect": { + "filesAbsent": [ + "Catalogs" + ] + } +} diff --git a/tests/skills/cases/meta-compile/encoding-utf16-bom.json b/tests/skills/cases/meta-compile/encoding-utf16-bom.json new file mode 100644 index 000000000..a7d638468 --- /dev/null +++ b/tests/skills/cases/meta-compile/encoding-utf16-bom.json @@ -0,0 +1,13 @@ +{ + "name": "Файл в UTF-16 с BOM читается: кодировка объявлена самим файлом", + "inputRaw": "{\"type\": \"Catalog\", \"name\": \"Номенклатура\"}", + "inputEncoding": "utf-16le", + "params": { + "validatePath": "Catalogs/Номенклатура.xml" + }, + "expect": { + "files": [ + "Catalogs/Номенклатура.xml" + ] + } +} diff --git a/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура.xml b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура.xml new file mode 100644 index 000000000..0daed60e9 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура.xml @@ -0,0 +1,91 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + + Номенклатура + + + ru + Номенклатура + + + + false + HierarchyFoldersAndItems + false + 2 + true + true + + ToItems + 9 + 25 + String + Variable + WholeCatalog + false + true + AsDescription + + Auto + InDialog + false + BothWays + + Catalog.Номенклатура.StandardAttribute.Description + Catalog.Номенклатура.StandardAttribute.Code + + Begin + DontUse + Directly + + + + + + + + + + + false + + + Managed + Use + + + + + + Use + Auto + DontUse + false + false + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура/Ext/ObjectModule.bsl b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Catalogs/Номенклатура/Ext/ObjectModule.bsl new file mode 100644 index 000000000..e69de29bb diff --git a/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Configuration.xml b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Configuration.xml new file mode 100644 index 000000000..698db0242 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Configuration.xml @@ -0,0 +1,252 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + + TestConfig + + + ru + TestConfig + + + + + Version8_3_24 + ManagedApplication + + PlatformApplication + + Russian + + + + + false + false + false + + + + + + + + + + + + + + + + + + + + + + Biometrics + true + + + Location + false + + + BackgroundLocation + false + + + BluetoothPrinters + false + + + WiFiPrinters + false + + + Contacts + false + + + Calendars + false + + + PushNotifications + false + + + LocalNotifications + false + + + InAppPurchases + false + + + PersonalComputerFileExchange + false + + + Ads + false + + + NumberDialing + false + + + CallProcessing + false + + + CallLog + false + + + AutoSendSMS + false + + + ReceiveSMS + false + + + SMSLog + false + + + Camera + false + + + Microphone + false + + + MusicLibrary + false + + + PictureAndVideoLibraries + false + + + AudioPlaybackAndVibration + false + + + BackgroundAudioPlaybackAndVibration + false + + + InstallPackages + false + + + OSBackup + true + + + ApplicationUsageStatistics + false + + + BarcodeScanning + false + + + BackgroundAudioRecording + false + + + AllFilesAccess + false + + + Videoconferences + false + + + NFC + false + + + DocumentScanning + false + + + SpeechToText + false + + + Geofences + false + + + IncomingShareRequests + false + + + AllIncomingShareRequestsTypesProcessing + false + + + + + + Normal + + + Language.Русский + + + + + + Managed + NotAutoFree + DontUse + DontUse + TaxiEnableVersion8_2 + DontUse + Version8_3_24 + + + + Русский + Номенклатура + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Ext/ClientApplicationInterface.xml b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Ext/ClientApplicationInterface.xml new file mode 100644 index 000000000..3c1161b2d --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Ext/ClientApplicationInterface.xml @@ -0,0 +1,18 @@ + + + + + UUID-002 + + + + + UUID-004 + + + + + + + + \ No newline at end of file diff --git a/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Languages/Русский.xml b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Languages/Русский.xml new file mode 100644 index 000000000..37c60d786 --- /dev/null +++ b/tests/skills/cases/meta-compile/snapshots/encoding-utf16-bom/Languages/Русский.xml @@ -0,0 +1,16 @@ + + + + + Русский + + + ru + Русский + + + + ru + + + \ No newline at end of file diff --git a/tests/skills/check-inline-drift.mjs b/tests/skills/check-inline-drift.mjs index f9c9b4216..0b6f13cfc 100644 --- a/tests/skills/check-inline-drift.mjs +++ b/tests/skills/check-inline-drift.mjs @@ -398,6 +398,14 @@ const FAMILIES = [ // Сообщение об ошибке разбора одинаково во всех навыках (issue #80): стектрейс парсера // агент читает как «скрипт сломан» и идёт чинить не то место. Вся специфика навыка — // в аргументах source/expected на месте вызова, тело функции общее. + { + name: 'read_json_file', py: 'read_json_file', ps1: 'Read-JsonInputFile', + variants: [ + { id: 'base', authority: 'interface-edit', + consumers: ['cf-edit', 'form-compile', 'form-edit', 'meta-compile', 'meta-edit', 'mxl-compile', + 'role-compile', 'skd-compile', 'skd-decompile', 'subsystem-compile', 'subsystem-edit'] }, + ], + }, { name: 'parse_json_input', py: 'parse_json_input', ps1: 'ConvertFrom-JsonInput', variants: [ diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs index 29f7d549d..af01efb08 100644 --- a/tests/skills/runner.mjs +++ b/tests/skills/runner.mjs @@ -268,6 +268,34 @@ function cleanupWorkspace(ws) { // ─── Arg building ─────────────────────────────────────────────────────────── +// Байты входного файла в заданной кодировке. Нужно для кейсов про кодировку: writeFileSync +// пишет только UTF-8, а навык обязан одинаково вести себя на UTF-16 с BOM (принять) и на +// cp1251 (отвергнуть, а не молча подменить кириллицу на U+FFFD). cp1251 в Node нет — кодируем +// формулой по диапазонам, которые встречаются в кейсах (ASCII + кириллица); прочее — ошибка кейса. +function encodeInput(text, encoding) { + if (!encoding || encoding === 'utf-8' || encoding === 'utf8') return Buffer.from(text, 'utf8'); + if (encoding === 'utf-16le') return Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from(text, 'utf16le')]); + if (encoding === 'utf-16be') { + const le = Buffer.from(text, 'utf16le'); + const be = Buffer.alloc(le.length); + for (let i = 0; i < le.length; i += 2) { be[i] = le[i + 1]; be[i + 1] = le[i]; } + return Buffer.concat([Buffer.from([0xfe, 0xff]), be]); + } + if (encoding === 'cp1251') { + const out = Buffer.alloc(text.length); + for (let i = 0; i < text.length; i++) { + const c = text.codePointAt(i); + if (c < 0x80) out[i] = c; + else if (c >= 0x410 && c <= 0x44f) out[i] = c - 0x350; + else if (c === 0x401) out[i] = 0xa8; + else if (c === 0x451) out[i] = 0xb8; + else throw new Error(`inputEncoding cp1251: символ U+${c.toString(16)} вне поддержанного набора (ASCII + кириллица)`); + } + return out; + } + throw new Error(`inputEncoding: неизвестная кодировка "${encoding}"`); +} + function buildArgs(skillConfig, caseData, workDir, inputFilePath, runtime) { const args = []; const scriptPath = resolveScript(skillConfig.script, runtime); @@ -803,10 +831,10 @@ async function runCaseAsync(testCase, opts) { // JSON.stringify всегда даёт валидный документ. if (caseData.inputRaw !== undefined) { inputFile = join(workDir, '__input.json'); - writeFileSync(inputFile, caseData.inputRaw, 'utf8'); + writeFileSync(inputFile, encodeInput(caseData.inputRaw, caseData.inputEncoding)); } else if (caseData.input !== undefined) { inputFile = join(workDir, '__input.json'); - writeFileSync(inputFile, JSON.stringify(caseData.input, null, 2), 'utf8'); + writeFileSync(inputFile, encodeInput(JSON.stringify(caseData.input, null, 2), caseData.inputEncoding)); } // Execute @@ -1028,10 +1056,10 @@ function runCase(testCase, opts) { // 3. Write input JSON if needed (inputRaw — дословно, см. выше) if (caseData.inputRaw !== undefined) { inputFile = join(workDir, '__input.json'); - writeFileSync(inputFile, caseData.inputRaw, 'utf8'); + writeFileSync(inputFile, encodeInput(caseData.inputRaw, caseData.inputEncoding)); } else if (caseData.input !== undefined) { inputFile = join(workDir, '__input.json'); - writeFileSync(inputFile, JSON.stringify(caseData.input, null, 2), 'utf8'); + writeFileSync(inputFile, encodeInput(JSON.stringify(caseData.input, null, 2), caseData.inputEncoding)); } // 4. Build CLI args and execute