fix(skills): внятная диагностика разбора JSON вместо стектрейса (#80)

Неверный входной JSON ронял скрипты необработанным исключением: PS1 отдавал дамп
ConvertFrom-Json с CategoryInfo, py-порт — traceback с внутренностями json/decoder.py.
Имя файла в сообщении не фигурировало, а для полиморфного -Value не было видно, какую
форму ждёт операция.

Общий хелпер ConvertFrom-JsonInput / parse_json_input в 12 навыках × 2 порта (24 места),
зарегистрирован семьёй в check-inline-drift.mjs — копии держит гард. Сообщение в одну
строку: ожидаемая форма, полученное значение, текст парсера в скобках.

Эхо полученного значения нужно потому, что съеденные оболочкой кавычки дают почти тот же
JSON ({group:X} вместо {"group":"X"}), и без него агент считает свой вызов верным. PS 5.1
печатает лишь огрызок и локализованно, Python — только номер колонки.

Возврат в PS1 через Write-Output -NoEnumerate: вынос разбора в функцию добавляет второй
анруллинг, и одноэлементный JSON-массив стал бы скаляром. Импорты внутри тела py-хелпера —
skd-decompile импортирует json локально как _json, а тело семьи обязано быть одинаковым.

В раннер добавлен inputRaw (запись входного файла дословно): через case.input битый JSON
невыразим, JSON.stringify всегда даёт валидный документ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-21 15:25:54 +03:00
co-authored by Claude Opus 5
parent 53fc16d2f1
commit a44a29a7d8
37 changed files with 1037 additions and 101 deletions
@@ -1,4 +1,4 @@
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# form-compile v1.193 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -14,6 +14,25 @@ param(
)
$ErrorActionPreference = "Stop"
# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected) {
try {
$parsed = $text | ConvertFrom-Json
} catch {
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
$got = ($text -replace '\s+', ' ').Trim()
if ($got.Length -gt 60) { $got = $got.Substring(0, 60) + '...' }
[Console]::Error.WriteLine("[ERROR] ${what}, got: ${got} ($($_.Exception.Message))")
exit 1
}
Write-Output -NoEnumerate $parsed
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# ═══════════════════════════════════════════════════════════════════════════
@@ -300,7 +319,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 = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json
$presetJson = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $builtInPath) $builtInPath
# Convert PSCustomObject to hashtable recursively
$toHash = {
param($obj)
@@ -327,7 +346,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 = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json
$projJson = ConvertFrom-JsonInput (Get-Content -Raw -Encoding UTF8 $projPreset) $projPreset
$projHash = & $toHash $projJson
foreach ($k in @($projHash.Keys)) {
$defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k]
@@ -1656,7 +1675,7 @@ if ($FromObject) {
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
$def = ConvertFrom-JsonInput $json $JsonPath
}
# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# form-compile v1.193 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -15,6 +15,29 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def parse_json_input(text, source, expected=None):
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
expected заполняем только для полиморфного входа: у файла подсказка
была бы наполнителем имя файла и текст парсера самодостаточны.
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
"""
import json as _pj
import sys as _psys
try:
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())
if len(got) > 60:
got = got[:60] + "..."
print("[ERROR] %s, got: %s (%s)" % (what, got, exc), file=_psys.stderr)
_psys.exit(1)
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
@@ -543,7 +566,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
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(json.load(f))
preset_data = ci_json(parse_json_input(f.read(), built_in_path))
for k in list(preset_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
@@ -553,7 +576,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
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 = json.load(f)
proj_data = parse_json_input(f.read(), proj_preset)
for k in list(proj_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), proj_data[k])
break
@@ -6456,7 +6479,7 @@ def main():
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = ci_json(json.load(f))
defn = ci_json(parse_json_input(f.read(), json_path))
global QUERY_BASE_DIR
QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path))