Дополняем гайд группы skd-*:
- В таблицу навыков добавлена строка /skd-decompile с пометкой об
отключённом автоподборе моделью.
- В блок «Рабочий цикл» нарисована обратная стрелка Template.xml →
/skd-decompile → JSON DSL.
- Новый под-раздел «Когда /skd-decompile, а когда /skd-edit» с явным
предупреждением о неполноте преобразования и тихих потерях.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- description короче и с явным «не для точечных правок»
- унифицируем терминологию (sentinel-узлы)
- расширяем «Когда не использовать» — почему /skd-edit лучше для адресных правок
- в критичные конструкции добавляем вложенные схемы
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Два бага PS-версии, незаметно роняли 9/30 отчётов sample30 в decompile-fail:
1. Get-Text без xpath → SelectSingleNode("", $ns) с .NET XPathException
"Результатом выражения должен быть NodeSet". Шесть call-sites в
dataSetLinks (sourceDataSet/destinationDataSet/sourceExpression/...)
передавали уже-выбранный узел без второго аргумента; [string]$xpath
дефолтился в "". Фикс: Get-Text возвращает $node.InnerText, если
xpath пустой.
2. $paramByName[$startMatch] при $startMatch=$null → "индекс массива
вычислен как NULL". Возникает на StandardPeriod-параметре, для
которого в отчёте нет companion expressions. Фикс: guard через if.
Python-порт #2 уже был защищён .get(); по #1 в py был обходной костыль
inline-через-inner_text — заменён на единый get_text(node) после
обновления сигнатуры до get_text(node, xpath=None).
verify-roundtrip sample30: 9 bit-perfect + 20 with-diff + 1 ring3 = 30
(до фикса 9 silently падали как decompile-fail, сумма не сходилась).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- value параметра может быть массивом (для valueListAllowed)
- расшифровка namespace-ов цветов: style: (палитра темы), web: (web-имена),
win: (Windows-системные)
Compile имеет map outputParamTypes — для известных ключей (Заголовок,
ВыводитьПараметрыДанных, РасположениеИтогов и др.) тип эмитится
автоматически по имени параметра. Decompile же всегда оборачивал в
wrapper {value, valueType} для не-xs:* типов — лишний шум.
Зеркалирован тот же map в decompile (15 keys), при чтении проверяем
совпадение fullType с map → если auto-detect compile вернёт тот же
тип, валидируем как простое значение.
JSON outputParameters стал заметно компактнее. Эффект на diff
нейтральный (compile эмитит то же).
Для filter right values с типом dcscor:DesignTimeValue, чьё значение
соответствует ref-pattern (Перечисление.X.Y / Справочник.X.Y / ПланСчетов.X.Y
и др.), compile auto-detect-ит DesignTimeValue из значения. Раньше
decompile всё равно сохранял valueType явно — лишний шум в DSL.
Применено к single-right и multi-right. Эффект на diff нулевой (compile
эмитит то же самое), но decompiled JSON становится компактнее и читабельнее.
Раньше эмитили шапку схемы в 8 строк (каждый xmlns на отдельной строке).
Оригинал платформы пишет всё в одну строку. Был с самого начала
существования skd-compile — не регрессия, но косметика на каждый отчёт
давала 9 строк diff (1 LOST + 8 ADDED).
Sample30 total: 458 → 238 строк diff.
Раньше decompile Normalize-Color сворачивал любой dN: префикс в style:
независимо от URI. Compile Emit-ColorValue знал только style: → один xmlns
(http://v8.1c.ru/8.1/data/ui/style). Цвета web:GhostWhite и win:Highlight
терялись/искажались.
URI mapping:
- http://v8.1c.ru/8.1/data/ui/style → style:
- http://v8.1c.ru/8.1/data/ui/colors/web → web:
- http://v8.1c.ru/8.1/data/ui/colors/windows → win:
decompile: резолвим prefix через GetNamespaceOfPrefix.
compile (PS+Py): Emit-ColorValue для template cells — long form с
локальным xmlns. Emit-AppearanceValue (внутри settings) — short form,
т.к. <dcsset:settings> уже объявляет xmlns:style/web/win/sys на корне.
См. upload/erf/ЦветаВМакете — образец с 4 формами цвета.
Параметр с valueListAllowed=true может иметь несколько <value> элементов
подряд — список значений по умолчанию (например список счетов плана).
Раньше decompile читал только первый (SelectSingleNode), compile эмитил
один <value> → терялись все остальные значения.
DSL: value в параметре может быть массивом строк (или native bool/int/double).
decompile: SelectNodes("r:value") → если >1 элемент, value = array.
compile (PS+Py): если value — array, эмитим каждый отдельным <value>
через Emit-ParamValue.
Альт-indent корпус: 18009 → 17946 строк diff.
Companions НачалоПериода/КонецПериода имеют canonical имена/expression в
некоторых корпусах, но дополнительно несут availableAsField=false
(поле-ограничение, не участвует в выборе). Наш @autoDates auto-generator
не передаёт этот атрибут — для совместимости с другими корпусами без него.
Решение в decompile: не сворачивать в @autoDates если хотя бы один
companion имеет availableAsField=false → companions остаются явными
параметрами, compile эмитит их со всеми атрибутами через стандартный
path.
Sample30: 607 → 458 строк diff (-149).
Раньше параметр с useRestriction=true (без hidden/autoDates/expression/
других object-form-триггеров) сохранялся как shorthand string —
useRestriction в shorthand не выражается, и compile эмитил default
useRestriction=false, теряя ограничение.
Render-Parameter: добавлен тригер object form для случая
useRestriction=true. Объект уже корректно эмитил это поле.
Служебные отчёты-обёртки содержат только settingsVariant с outputParameters
(МакетОформления и подобное) и используются для динамического заполнения
из кода. Compile требует ≥1 dataSet, и весь DSL заточен под data-driven
отчёты — раньше decompile проходил, но compile падал с exit 1.
Теперь decompile fail-fast (exit 3, ring3 skip) — это правильнее
классифицирует такие отчёты в verify-roundtrip как "не поддерживается"
вместо "compile сломался".
См. C:/WS/projects/titan/src/cfe/Титан2/Reports/
ккСправкаРасчетРезервыПоСомнительнымДолгам.
Параметры типа «исключаемые документы» имеют valueType с
<v8:TypeSet xmlns:dN="...">dN:DocumentRef</v8:TypeSet> — указывает на
все ссылки указанного класса конфигурации, а не на конкретный объект.
Раньше теряли целиком: decompile читал только <v8:Type>, compile
эмитил голое имя как <v8:Type>DocumentRef</v8:Type> (что не валидно).
DSL — голое имя ref-класса без точки (CatalogRef, DocumentRef, EnumRef,
ChartOfAccountsRef, ChartOfCharacteristicTypesRef, ChartOfCalculationTypesRef,
BusinessProcessRef, TaskRef, ExchangePlanRef, InformationRegisterRef,
AnyRef) → TypeSet. С точкой (DocumentRef.X) — конкретный Ref как было.
decompile: Get-ValueTypeShorthand читает v8:TypeSet и сохраняет
local-name (после prefix:).
compile (PS+Py): Emit-SingleValueType распознаёт голое имя из набора и
эмитит <v8:TypeSet xmlns:d5p1=...>d5p1:NAME</v8:TypeSet>.
Sample30 total: 618 → 607 строк diff.
Параметр типа StandardPeriod с variant=Custom может иметь явные даты,
отличающиеся от default 0001-01-01T00:00:00 (например endDate=23:59:59).
См. АнализПричинБлокировкиВычетаНДС @941.
Раньше decompile сохранял variant только если ≠Custom (для Custom —
ничего), и compile эмитил захардкоженные 0001-01-01T00:00:00 для обеих
дат. Теряли явное время.
decompile: для StandardPeriod с явными датами (любая ≠00:00:00) →
object form {variant: Custom, startDate, endDate}.
compile (PS+Py): Emit-ParamValue принимает dict val с variant/startDate/
endDate; для Custom без явных дат сохраняется текущий fallback на
0001-01-01T00:00:00.
Sample30 total: 620 → 618 строк diff.
Раньше для StructureItemTable читали только viewMode/userSettingID/
userSettingPresentation/itemsViewMode, а для StructureItemChart — вовсе
ничего из user-settings. Также не поддерживали axis-level режим
доступности секций (columnsViewMode/rowsViewMode/pointsViewMode/
seriesViewMode) и use=false на самих table/chart.
Расширено:
- table: + use=false, + columnsViewMode, + rowsViewMode
- chart: + use=false, + viewMode, + userSettingID, + userSettingPresentation,
+ itemsViewMode, + pointsViewMode, + seriesViewMode
Все эти атрибуты эмитятся платформой как direct children самой item-ноды
после rows/columns (table) или points/series (chart). DSL — простые поля
прямо на table/chart-объекте (как у table уже было для viewMode/etc).
Sample30 total: 729 → 620 строк diff (-109).
conditionalAppearance может содержать СтильГраницы со сложным value:
<dcscor:value xsi:type="v8ui:Line" width="0" gap="false">
<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">None</v8ui:style>
</dcscor:value>
+ nested <dcscor:item> для side-стилей (СтильГраницы.Сверху/.Снизу/.Слева/.Справа),
каждый со своим v8ui:Line value и опц. <dcscor:use>false</dcscor:use>.
Раньше теряли всю структуру и эмитили <value xsi:type="xs:string">None</value>.
DSL form B (выбранный пользователем) — Line как top-level плоский объект:
"СтильГраницы": {
"@type": "Line", "width": 0, "gap": false, "style": "None",
"items": {
"СтильГраницы.Сверху": {
"value": { "@type": "Line", "width": 1, "gap": false, "style": "Solid" },
"use": false
}
}
}
Nested items — универсальный wrapper {value, use?, items?} (как у outputParameters).
Эмитятся как siblings <dcscor:item> внутри родительского <dcscor:item> (после
закрытия родительского <dcscor:value>).
decompile: Read-AppearanceValueNode распознаёт Line и возвращает inline объект;
Get-SettingsAppearance читает nested dcscor:item children и собирает их в items.
compile (PS+Py): emit_appearance_value расширен — Line ветка + рекурсивный
вызов для items siblings.
Sample30 total: 767 → 729 строк diff (-38).
Раньше пропускали top-level selection в chart если она содержит только
SelectedItemAuto (считая дефолтом). Но оригинал платформы всегда эмитит
блок явно — для bit-perfect нужно сохранять presence (по аналогии с
table axis и structure-group, где selection presence уже сохраняется).
Sample30 total: 782 → 767 строк diff.
Cells со style "none" (без цветов/шрифтов/границ) и без width/merge
получали пустой блок <dcsat:appearance></dcsat:appearance>. Оригинал
платформы для таких cells appearance не пишет вовсе.
Добавлен ранний return в Emit-CellAppearance если все атрибуты пустые.
PS + Py синхронизированы. Sample30 total: 794 → 782 строки.
В chart-параметрах типа ТипДиаграммы.СоединениеЗначенийПоСериям platform
эмитит nested SettingsParameterValue с:
- <dcscor:use>false</dcscor:use> (sub-параметр отключён)
- <dcscor:value xmlns:dN="http://v8.1c.ru/8.2/data/chart" xsi:type="dN:X">
(локальный xmlns на value — префикс не объявлен в корне схемы)
Раньше теряли оба:
- decompile не читал sub.use и не резолвил xmlns кастомного xsi:type
- compile эмитил value без xmlns и без use=false
decompile: читаем dcscor:use на sub-item, резолвим prefix→URI через
GetNamespaceOfPrefix; если URI не из стандартных корневых xmlns —
сохраняем valueType как объект {uri, name}.
compile (PS+Py): новый helper Emit-OutputParametersSubItem; если
valueType — объект, эмитим xmlns:dN=<uri> + xsi:type=dN:<name>;
+ <dcscor:use>false</dcscor:use> если sub.use === false.
Sample30 total: 809 → 794 строк diff.
Расширение DSL для area-templates под отчёты с расшифровкой через
data-параметр (типа АнализВыполненияМаршрутныхЛистов в ERP).
Раньше шорткат `drilldown: "X"` всегда генерировал
`Расшифровка_X` + `ИмяРесурса`/`DrillDown`. Появилось три формы:
- A: { name, expression } — без drilldown
- B: { name, expression, drilldown: "X" } — текущий shortcut (без изменений)
- C: { name, drilldown: { field, expression, action } } — самостоятельный
DetailsAreaTemplateParameter
Per-cell override: ячейка может быть object `{ value, drilldown }` —
тогда appearance ссылается на указанный параметр расшифровки (без
префикса `Расшифровка_`). Используется когда несколько ячеек должны
указывать на один details-параметр (МаршрутныйЛист).
Новая корневая секция:
"fieldTemplates": [{ "field": "X", "template": "Макет1" }]
— XML <fieldTemplate><field/><template/></fieldTemplate>.
compile (PS+Py): новый helper Emit-AreaTemplateParameter, ветка form C
в Emit-AreaTemplateDSL/Emit-Templates raw-mode, Emit-FieldTemplates.
Cell rendering: проверка cell.drilldown override перед drilldownMap.
decompile (PS): Build-Template переписан — собирает все
DetailsAreaTemplateParameter в map; распознаёт shortcut form B по
canonical shape (Расшифровка_<Y>/ИмяРесурса/"<Y>"/DrillDown) И наличию
cell appearance ref на этот target; не-shortcut → form C entry.
Cell wrapping: если drilldownTarget ≠ shortcut Расшифровка_Y, ячейка
оборачивается в { value, drilldown }. Plus Build-FieldTemplates.
Размер diff на sample30: 1045 → 809 строк (-22.5% за сессию).
Диаграмма может содержать несколько <dcsset:series> (и аналогично
<dcsset:point>) — каждая со своим groupItems, filter, order, selection,
viewMode, userSettingID и userSettingPresentation. Используется для
multi-series графиков с группировкой по фильтру (например,
АнализЛояльностиКлиентовXYZ — series по СтадияОтношений: Постоянный,
Разовый, Потенциальный, Потерянный клиент).
Раньше декомпилировали только первый блок (SelectSingleNode), теряя
остальные → −113 строк diff на одном этом отчёте (134 → 21).
decompile: SelectNodes → если >1, сохраняем как массив; single → object
(backward-compat).
compile: проверяем тип points/series — array → цикл, иначе single
блок (backward-compat для существующего DSL).
PS и Py compile синхронизированы.
GroupItemField может иметь periodAdditionBegin/End:
- xsi:type="xs:dateTime" с конкретной датой
- xsi:type="dcscor:Field" с путём к параметру/полю (например
"ПараметрыДанных.ДатаНачала"). См. АнализДоходовРасходов @8933.
Раньше эмитили захардкоженный default xs:dateTime 0001-01-01T00:00:00,
теряя любые non-default значения.
decompile: читаем оба элемента, сохраняем как string поля
periodAdditionBegin/End в object form group field (если non-default).
compile: auto-detect типа по pattern (ISO date → xs:dateTime,
иначе → dcscor:Field).
PS и Py compile синхронизированы.
SelectedItemFolder (группа полей с заголовком в selection) имеет
<dcsset:placement> — может быть Auto/Horizontally/Vertically/Special.
Мы захардкоженно эмитили Auto, теряя non-default значения.
decompile: читаем placement, сохраняем если ≠ Auto.
compile: эмитим из item.placement (default Auto для bit-perfect-default).
PS и Py compile синхронизированы.
Параметр типа DateTime в dataParameters внутри settings с use=false и
без значения сохраняется оригиналом как <dcscor:value xsi:type="xs:string"/>
(пустой placeholder), а не xsi:nil. См. АнализПлановыхНачислений @1506
для ОкончаниеПериода.
decompile: detect-ит empty xs:string + use=false → object form
{parameter, use:false, value:'', valueType:'xs:string'}.
compile: Emit-EmptyValue нормализует префикс xs: (xs:string → ^string),
теперь корректно эмитит пустой xs:string вместо fallback xsi:nil.
PS и Py синхронизированы.
При чтении FilterItemComparison с несколькими <right> элементами теряли
xsi:type — все значения уходили как строки, и compile эмитил DesignTimeValue
по auto-detect для строк вида "Перечисление.*". Но оригинал может хранить
эти значения как xs:string (см. АнализНачисленийИУдержаний @22718).
Теперь читаем xsi:type каждого <right>; если все одинаковые — сохраняем
в valueType. Compile уже умеет использовать переданный valueType вместо
auto-detect.
Раньше все values в <dcscor:value xsi:type="dcscor:ChoiceParameters">
эмитились как DesignTimeValue. Оригинал использует xs:boolean для bool-флагов
(пример: Отбор.ОбособленноеПодразделение=false в ChoiceParameters элементе
inputParameters поля ДанныеОрганизации).
decompile: читает тип xsi из <dcscor:value> и конвертит в JSON-native
(true/false для boolean, int/double для decimal, иначе строка).
compile: при эмите inputParameters → choiceParameters → values детектирует
тип значения и эмитит соответствующий xsi:type.
PS и Py синхронизированы.
Раньше StandardBeginningDate (используется когда top-level param имеет
тип xs:dateTime + UI выбор "Начало дня"/"Custom") декомпилировалась
через InnerText: variant+date конкатенировались в "Custom2022-10-25..."
и compile эмитил как xs:string.
Cross-reference на корпусе (671 отчёт):
608 v8:StandardPeriod → v8:StandardPeriod
180 xs:dateTime → v8:StandardBeginningDate
120 xs:dateTime → xs:dateTime (raw)
Decompile теперь сохраняет SBD как объект {variant, date} (без @type marker).
Compile различает SP/SBD по форме value:
{variant, date} → SBD
{variant, startDate, endDate} → SP с датами
{variant} only → инференс по имени (BeginningOf* → SBD)
В корпусе ни одного ambiguous (variant=Custom без полей) не существует:
все 33 SBD/Custom имеют date, все 93 SP/Custom — startDate/endDate.
sample30: −22 строки (932 → 910).
Platform может содержать SelectedItemAuto/OrderItemAuto в top-level
<dcsset:selection>/<dcsset:order> (рядом с явными полями) — это валидно.
Раньше compile использовал -skipAuto на top-level, теряя эти items.
Снапшоты регенерированы.
sample30: −10 строк (942 → 932).
Раньше Emit-Selection/Filter/Order/ConditionalAppearance early-return при
пустых items, даже если decompile сохранил blockViewMode/blockUserSettingID.
Caller (settingsVariant) тоже не вызывал эти функции на пустых items.
Теперь блоки эмитятся когда есть items ИЛИ block-level meta. Реальный кейс
из ERP: <dcsset:conditionalAppearance><dcsset:viewMode>Normal</dcsset:viewMode>
</dcsset:conditionalAppearance> — пустой CA блок только с viewMode.
sample30: −84 строки (1026 → 942).
Анализ корпуса ERP (671 отчёт): items внутри dcsset:row/column/points/series
ВСЕГДА используют короткую форму <dcsset:item> без xsi:type (531 случай,
0 explicit). В остальных контекстах — explicit <dcsset:item xsi:type="dcsset:StructureItemGroup">.
Emit-StructureItem получил switch -shortGroup, который Emit-TableAxisBlock
передаёт для nested children. Флаг наследуется recursive в детей через
Emit-StructureItem рекурсию.
Снапшоты регенерированы.
sample30: −166 строк (1192 → 1026).
Анализ корпуса ERP/БСП (671 отчёт): из 635 StandardPeriod values только
93 (все Custom) имели <v8:startDate>/<v8:endDate>. Остальные 542 варианта
(ThisMonth, LastYear, Today, ThisQuarter, FromBeginningOfThisYear и т.д.)
эмитятся БЕЗ дат — это canonical platform-pattern.
Раньше compile добавлял boilerplate 0001-01-01 даты ко всем вариантам
независимо от типа. Снапшоты регенерированы.
sample30: −138 строк (1330 → 1192).
Platform эмитит StructureItemGroup как короткую форму <dcsset:item> без
xsi:type (это дефолтный тип). Раньше валидатор выдавал error на любой
реальный XML с такой формой. Теперь — отсутствие xsi:type трактуется
как dcsset:StructureItemGroup (с warning только для нестандартных типов).
Когда dataParameter имеет viewMode/userSettingPresentation и значение —
StandardPeriod без явных дат (просто вариант "Custom"/"ThisMonth"),
decompile сохранял value как плоскую строку "Custom" + valueType=v8:StandardPeriod.
Compile, видя valueType с префиксом, эмитил <value xsi:type="v8:StandardPeriod">Custom</value>
— плоский xs:string-like тег вместо StandardPeriod-блока с <v8:variant>.
Теперь когда object-form нужен по другим причинам (viewMode/USP),
StandardPeriod-вариант сохраняется как {variant: ...}, и compile
правильно генерирует <v8:variant xsi:type="v8:StandardPeriodVariant">.
sample30: −66 строк (1396 → 1330).
Раньше @autoDates генерировал НачалоПериода/КонецПериода с type=date →
DateFractions=Date. Реальный БСП паттерн использует DateTime (платформа сама
приводит конец периода к 23:59:59 для дат без времени, но DateTime более
явное и матчит шаблоны БСП).
Снапшоты регенерированы.
Раньше любая пара companion-параметров с expression "&P.ДатаНачала"/
"&P.ДатаОкончания" сворачивалась в @autoDates, независимо от их имён.
Compile же всегда генерирует строго "НачалоПериода"/"КонецПериода" +
type=date + DateFractions=Date. Для отчётов с шаблоном "Период<X>" →
"НачалоПериода<X>"/"КонецПериода<X>" + DateFractions=DateTime
(типовой паттерн БСП — ПериодКонтрольСроков, ПериодОбязательств и т.п.)
это давало некорректный round-trip с потерей суффикса и формата дат.
Теперь fold срабатывает ТОЛЬКО для канонической пары — остальные
companion'ы остаются явными параметрами с полным сохранением имени,
type=dateTime, DateFractions=DateTime и expression.
sample30: −152 строки (1548 → 1396).
Параметр ВыборГруппИЭлементов имеет значение типа FoldersAndItemsUse:
<dcscor:value xmlns:d6p1="http://v8.1c.ru/8.1/data/enterprise"
xsi:type="d6p1:FoldersAndItemsUse">Items</dcscor:value>
Decompile теряла xsi:type → compile эмитил xs:string. Теперь сохраняем
{uri, name} в valueType wrapper'е, compile воспроизводит локальный
xmlns:dN="..." prefix и правильный xsi:type. GetNamespaceOfPrefix
извлекает URI из контекста элемента, что работает для всех 7 известных
xmlns URI (enterprise, current-config, ui/style, chart, types и т.п.).
sample30: −8 строк (1556 → 1548).
Раньше теряли userSettingID, который platform пишет:
1. Прямо в блоки selection/filter/order/conditionalAppearance под
<dcsset:settings> (рядом с block-level viewMode). Расширена цепочка
Get-BlockUSID + новый -blockUserSettingID параметр в Emit-*.
2. На самой <dcsset:item xsi:type="dcsset:StructureItemTable"> (рядом
с viewMode/userSettingPresentation/itemsViewMode). Build-Structure
читает их, Emit-StructureItem (type=table) эмитит.
sample30: −46 строк (1602 → 1556).
Параметры с явно заданным <useRestriction>true</useRestriction>, которые
не покрываются auto-emit от @hidden (где compile сам ставит true) и не
участвуют в @autoDates fold (где compile также ставит true companion'ам),
теряли это свойство. Типичный кейс: параметры-выражения вида
ПериодНачало = &Период.ДатаНачала с useRestriction=true.
Render-Parameter теперь явно эмитит useRestriction:true в object form
с защитой от двойного перекрытия hidden/autoDates.
sample30: −60 строк (1710 → 1650).
Параметры типа ВариантИспользованияГруппировки с xsi:type=
dcsset:DataCompositionGroupUseVariant теряли тип, потому что wrapper
создавался только при наличии viewMode/userSettingID/use=false. Теперь
кастомный xsi:type (не xs:* / LocalStringType / Font) сам по себе
триггерит wrapping — сохраняем valueType для bit-perfect эмиссии.
sample30: −28 строк (1738 → 1710).
Get-FilterValueWithType теряла тип значения — "true"/"false" приходило как
string, и compile эмитил xs:string вместо xs:boolean. Decompile теперь
конвертирует по xsi:type → реальный [bool]/[int]/[double].
Shorthand эмиссия фильтра также фиксирована: bool теперь рендерится как
"true"/"false" (lowercase), не "True"/"False" (PS-style).
Аналогично availableValues в параметрах: bool/decimal значения теперь
сохраняются с правильным типом, а не downgrade в xs:string.
sample30: −30 строк (1768 → 1738).
Раньше теряли viewMode/userSettingID, которые platform пишет прямо в блок order
(не в item). Build-Structure теперь читает их как orderViewMode/orderUserSettingID,
Emit-Order принимает -blockUserSettingID параметр.
sample30: −10 строк (1778 → 1768).
Build-DataParameters не читал viewMode и userSettingPresentation на отдельных
параметрах данных — теряли 74 viewMode references только в одном крупном отчёте.
Теперь object form {parameter, value, valueType?, viewMode?, userSettingID?, ...}
с auto-конверсией bool/decimal по xsi:type.
Compile добавлен early-branch на полный xsi:type (xs:boolean, dcscor:DesignTimeValue
и т.п.) — раньше string "true" эмитился как xs:string вместо xs:boolean.
Расследование: viewmode-trace.py показал ровно 66 LOST viewMode=Normal
+ 8 Inaccessible на пути '/r:DataCompositionSchema/r:settingsVariant/
dcsset:settings/dcsset:dataParameters/dcscor:item[SettingsParameterValue]'.
sample30: −204 строки (1982 → 1778).
Build-TableAxisBlock не читал itemsViewMode на самой оси (только на
StructureItemGroup). Теперь сохраняется и эмитится — bit-perfect для
шаблонов отчётов с явно заданным itemsViewMode=Normal/Inaccessible
на колонках/строках таблицы.
sample30: −30 строк (2012 → 1982).
Параметры со скалярным типом (decimal/string/dateTime) и <value xsi:nil="true"/>
теперь сохраняют значение nil через object form {nilValue:true}. Раньше compile
эмитил типизированный default (xs:decimal>0, xs:string/>, xs:dateTime>0001-01-01)
вместо nil — мismatch на bit-perfect round-trip.
sample30: −22 строки (2034 → 2012).
Build-ConditionalAppearance не читал userSettingPresentation
(read только viewMode/userSettingID), теперь сохраняет multilang
презентацию в JSON, а Emit-ConditionalAppearance эмитит её обратно.
sample30: −80 строк (2114 → 2034).
Две bug-фиксы для шаблонов:
1. PS-quirk: \$widths = if (\$t.widths) { @(\$t.widths) } else { @() }
разворачивал одно-элементный массив в строку, после чего \$widths[0]
возвращал первый Char (например '1' для "15.625"), а [double][Char]'1'=49.
Заменил if-expression на обычный if-statement.
2. Indent в <dcsat:appearance>: компайлер ставил 4 таба вместо 5
(и 5 вместо 6 у items внутри). Поправлено в обоих рантаймах.
sample30: −552 строки (2666 → 2114).
Get-CellValue теперь пропускает dict без ключа value (multilang dict
{ru, en, ...}), а главный цикл Emit-Templates эмитит для таких ячеек
<dcsat:item xsi:type="dcsat:Field"><dcsat:value xsi:type="v8:LocalStringType">
с lwsTitle-структурой. Раньше multilang-ячейки терялись (Get-CellValue
возвращал null → cell не эмитился).
sample30: −180 строк (2870 → 2690).
Build-DataParameters раньше пропускал variant=Custom в шорткоде, теряя как
сам StandardPeriod-маркер, так и use=false (читался из dcsset:use вместо
правильного dcscor:use). Теперь Custom попадает в shorthand как "=Custom",
compile воспроизводит StandardPeriod tag + boilerplate dates корректно.
sample30: −166 строк (3036 → 2870).
Wrapper {value, ...} расширен: помимо use=false поддерживает viewMode,
userSettingID, userSettingPresentation на каждом item внутри
<dcsset:outputParameters>. Также value=Font dict теперь работает в wrapper.
sample30: −92 строки (3322 → 3230).
В UserFieldExpression XML присутствуют все 4 элемента (detailExpression,
detailExpressionPresentation, totalExpression, totalExpressionPresentation),
даже когда они пустые (<dcsset:totalExpression/>). Раньше пустые опускались.
decompile теперь читает по присутствию узла, compile эмитит self-closing
форму для пустых строк.
sample30: −106 строк (3743 → 3637).
На одном поле может быть несколько <orderExpression> (multi-sort fallback).
decompile сохраняет массив (single → object back-compat), compile принимает оба.
sample30: −30 строк, +2 отчёта в bit-perfect (27 with-diff).
skd-decompile теперь извлекает блоки <dataSetLink> на уровне схемы,
skd-compile поддерживает поля parameterListAllowed/startExpression/
linkConditionExpression (раньше был только parameter).
На sample30 это даёт −1100 строк diff (4873 → 3773), один отчёт
(АнализНачисленийНДССАвансовПолученных) переходит в bit-perfect.
- order item: use=false в object form
- outputParameters: wrapper {value, use: false} для отключённого параметра
- table: top-level selection/conditionalAppearance/outputParameters
(отдельно от column/row)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-Structure для table теперь читает selection / outputParameters /
conditionalAppearance прямо на самой <dcsset:item xsi:type="StructureItemTable">,
не только внутри row/column.
Эффект на sample30: −906 строк diff (большой эффект — многие отчёты
с таблицами имеют top-level selection и outputParameters для названия
таблицы и формата вывода).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
StructureItemTable может иметь свои selection / conditionalAppearance /
outputParameters прямо на уровне таблицы (отдельно от row/column).
Раньше Emit-StructureItem для table эмитил только columns и rows; теперь
после rows эмитятся top-level блоки.
Аналогично сделано для chart (там было раньше).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Платформа эмитит <dcsset:filter/> (self-closing, без условий) на
каждом condApp item, где фильтр не задан — это нормальная форма
"правило применяется ко всем строкам без дополнительных условий".
Compile теперь эмитит пустой тег если filter отсутствует/пуст.
Decompile-side уже корректно игнорировал пустой filter (Build-CondApp
читает items только если они есть).
Эффект на sample30: −252 строки diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-OutputParameters теперь читает <dcscor:use>false</dcscor:use> на
item и сохраняет значение в форме {value, use: false}.
Эффект на sample30: −198 строк diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
outputParameters item тоже может иметь <dcscor:use>false</dcscor:use>
(например — отключённый «Заголовок» в варианте). Emit-OutputParameters
теперь распознаёт wrapper {value, use: false} и эмитит <dcscor:use>
в начале item, как уже делал Emit-AppearanceValue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
OrderItemField в settings может иметь <dcsset:use>false</dcsset:use>
(отключённая сортировка-пункт пользовательских настроек). Build-Order
теперь читает use=false и переводит item в object form
{field, direction, use, viewMode}. Compile эмитит <dcsset:use> в
начале item, перед <dcsset:field>.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Get-AppearanceDict теперь читает <dcscor:use>false</dcscor:use> на
appearance items и возвращает значение в форме {value, use: false}.
Compile-side уже принимал этот wrapper.
Эффект на sample30: −21 строка diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-Structure для group теперь читает userSettingID и
userSettingPresentation (multilang dict) наряду с viewMode/itemsViewMode.
Try-StructureShorthand расширена — структура не сворачивается в
shorthand при наличии любого из новых полей (use, conditionalAppearance,
outputParameters, userSettingID, userSettingPresentation).
Эффект на sample30: −462 строки diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
StructureItemGroup может быть зарегистрирована как пункт пользовательских
настроек (например, "По сотрудникам" — позволяет включить/выключить
группировку через UI). Поля userSettingID и userSettingPresentation
эмитятся после viewMode, перед itemsViewMode (платформенный порядок).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
При парсинге shorthand "Поле = Перечисление.X.Y" Parse-FilterShorthand
уже распознавал тип dcscor:DesignTimeValue. Но в auto-detect веток
Emit-FilterItem (single-right и multi-right) этой проверки не было,
поэтому ссылочные значения из object form (где valueType не сохранён)
эмитились как xs:string.
Добавлено в обе ветки: проверка regex ^(Перечисление|Справочник|...
|Catalog|Enum|...)\. → dcscor:DesignTimeValue.
Эффект на sample30: −326 строк diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-TableAxisBlock теперь читает <dcsset:conditionalAppearance>
блока column/row/point/series. Это типовая категория для table
с условным оформлением колонок (например, разный текст для разных
групп начислений в строке таблицы).
Эффект на sample30: −1026 строк diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Колонки/строки таблицы и оси диаграммы (column/row/point/series)
могут содержать собственный <dcsset:conditionalAppearance> — правила
оформления специфичные для этой оси. Emit-TableAxisBlock теперь его
эмитит между outputParameters и nested children.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DataSet field может иметь <attributeUseRestriction> наравне с
<useRestriction> — те же 4 подэлемента (field/condition/group/order),
но ограничения применяются к атрибутам ссылочного поля (например,
"запретить выбирать атрибуты Контрагента в фильтре").
Compile-side уже принимал attrRestrict в JSON; decompile теперь его
заполняет. Item переходит в object form при наличии attrRestrict.
Эффект на sample30: −257 строк diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
<dcsset:additionalProperties> → dict в settings.additionalProperties.
Все значения xs:string, простые key→value пары.
Эффект на sample30: −121 строка diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
<dcsset:additionalProperties> — список <v8:Property name="X">
<v8:Value xsi:type="xs:string">Y</v8:Value></v8:Property>. Используется
платформой для служебных свойств варианта (ВариантНаименование,
КлючВарианта, Адрес — URL tempstorage).
DSL: settings.additionalProperties = { "имя": "значение", ... }
Эмит — после itemsViewMode, перед закрытием settings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Три связанных закрытия:
- Build-Parameter вызывает Read-InputParameters (раньше только Field)
- Read-InputParameters читает LocalStringType value как multilang dict
(ФорматРедактирования и т.п.)
- Build-FilterItem (FilterItemComparison) читает <dcsset:presentation>
с поддержкой multilang (Get-MLText + fallback InnerText); item
переходит в object form при наличии presentation.
Эффект на sample30: −533 строки diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Параметр (parameters[]) может иметь свой inputParameters блок —
например <ФорматРедактирования> со значением xs:LocalStringType.
Раньше Emit-InputParameters использовался только для DataSet field;
теперь подключён и к Emit-Parameter (вывод после <use>).
emit_input_parameters value: добавлена поддержка multilang dict
({ru, en, ...} → LocalStringType). Раньше падал в xs:string.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-ConditionalAppearance и FilterItemGroup читали presentation через
Get-Text (теряли multilang). Теперь читают через Get-MLText с fallback
на InnerText — multilang dict {ru, en, ...} сохраняется в JSON.
Эффект на sample30: −946 строк diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
При значении-словаре {ru, en, ...} эмитим <dcsset:presentation> как
LocalStringType с <v8:item>/<v8:lang>/<v8:content>; при строке —
по-прежнему xs:string. Раньше всегда жёстко xs:string, что давало
LOST для multilang.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-ConditionalAppearance теперь читает:
- <dcsset:use>false</...> → use: false
- любые <dcsset:useInXxx>DontUse</...> → элемент в массиве useInDontUse
(имена тегов: useInGroup → "group", useInFieldsHeader → "fieldsHeader",
и т.п.)
Эффект на sample30: −187 строк diff. Существенная часть LOST <use> и
LOST <content>/<lang> (внутри useInXxx-окружения) закрыта.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Расширение DSL для бит-перфект roundtrip на условном оформлении:
- use: false — отключённое правило (эмитится в начале item)
- useInDontUse: array — список областей где правило НЕ применяется
(\"group\", \"hierarchicalGroup\", \"overall\", \"fieldsHeader\",
\"header\", \"parameters\", \"filter\", \"resourceFieldsHeader\",
\"overallHeader\", \"overallResourceFieldsHeader\")
Compile эмитит <dcsset:useInGroup>DontUse</...> и т.п. в платформенном
порядке.
Семантика: \"useIn\" в платформе — это белый список применения правила;
DSL хранит инверсный список (что отключено) — короче для редких
ограничений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Платформа эмитит <useRestriction>true|false</useRestriction> у каждого
параметра безусловно. Раньше compile эмитил только если =true, что
приводило к LOST <useRestriction>false</useRestriction> в roundtrip.
Эффект на sample30: −84 строки diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Три связанных изменения:
- Build-TableAxisBlock читает вложенные <dcsset:item> как children
(StructureItemGroup внутри row/column/point/series)
- Build-Structure принимает <dcsset:item> без явного xsi:type как
StructureItemGroup (реальные XML используют такую default-форму
для вложенных групп — раньше попадало в sentinel)
- Чтение use=false на StructureItemGroup
Эффект на sample30: −3253 строки diff (массовая категория —
table row almost always содержит nested grouping).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В реальных отчётах внутри table row / column / chart axis (point/series)
часто живут вложенные группы — StructureItemGroup в children, со своими
groupItems / filter / order / selection / outputParameters / nested
children глубже. До этого Emit-TableAxisBlock эмитил только axis-level
поля, без children.
Также: на самой StructureItemGroup может быть use=false (отключённая
ветка структуры в settings) — добавлено в DSL и в эмит.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Раньше при наличии явного <viewMode>Normal</viewMode> decompile
переводил filter item в полноценный object form. Это раздувало JSON
без причины — @normal в shorthand функционально эквивалентен
"viewMode": "Normal" в object form, и compile уже его парсит.
Теперь: object form триггерится только реальными причинами
(userSettingPresentation, value-массив, dcscor:Field валуетайп);
явный Normal сохраняется как @normal в shorthand. Object form
по-прежнему может содержать "viewMode": "Normal" — это равнозначно.
Compile-side изменений не требуется. Spec обновлён.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- selection items: use=false (на field и Auto), пример обновлён
- filter:
- примеры с valueType: dcscor:Field (field-to-field comparison),
value: [a,b,c] (multi-right InList), value: [] (ValueListType placeholder)
- явное описание форм value (скаляр / массив / пустой массив)
- FilterItemGroup принимает user-settings (viewMode/userSettingID/...)
- table column/row + chart points/series: name на всех осях (раньше
только row), плюс user-settings поля
- секция «Стратегия сохранения viewMode» — описана модель explicit-only
(decompile сохраняет точное присутствие, compile эмитит только заданное)
- @normal убран из перечня shorthand-флагов (Normal — default, не
эмитится shorthand'ом; явный Normal переводит в object form)
В SKILL.md изменения не вносятся — фичи редкие, нужны для bit-perfect
round-trip с реальных схем.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Get-FilterValueWithType возвращает xsi:type вместе со значением.
Build-FilterItem теперь сохраняет valueType в object form, если тип
не xs:* (auto-detect compile обрабатывает xs:* сам). Это закрывает
field-to-field comparison: <right xsi:type=\"dcscor:Field\">FieldB</right>
теперь корректно эмитится обратно через valueType=\"dcscor:Field\".
Item переходит в object form при наличии valueType (shorthand не выразим).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Для filter right value compile уже различал bool / native-number /
dateTime, но не различал числовые строки. Реальные отчёты часто хранят
сравнения как числа: <right xsi:type=\"xs:decimal\">5</right>.
Decompile при чтении видит "5" как строку (через InnerText), и без
этого фикса compile эмитил xs:string. Теперь добавлена проверка
по regex ^-?\d+(\.\d+)?$ → xs:decimal.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-FilterItem теперь читает все <dcsset:right> элементы (раньше
только первый — терялись значения для InList с несколькими values).
Первый <right> типа v8:ValueListType трактуется как пустой list-placeholder
(`value: []` в JSON).
Item переходит в object form если value — массив (shorthand не выразим
для multi-value/empty-list).
Shorthand fallback для null/empty value теперь снова `_` (placeholder).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DSL: value на filter item может быть массив:
- value: [] — пустой ValueListType placeholder (для InList с
пользовательскими настройками — пользователь
заполнит значения через UI)
- value: [3, 4, 5] — InList с несколькими конкретными значениями
(compile эмитит несколько <right> подряд)
- value: 3 — single value (как раньше)
Compile автоопределяет тип каждого значения (bool/decimal/dateTime/string).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SelectedItemField и SelectedItemAuto могут иметь <dcsset:use>false</dcsset:use>
— отключённое поле выборки. Раньше игнорировалось при roundtrip.
DSL расширения:
- selection item object form: { field, use: false, title?, viewMode? }
- новый объект для отключённого Auto: { auto: true, use: false }
Decompile переходит в object form если есть use=false (помимо title и
viewMode); compile эмитит <use>false</use> в начале item (XML-порядок).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реальные отчёты непоследовательны: одни filter/item имеют
<viewMode>Normal</viewMode> с userSettingID, другие — нет (зависит от
момента редактирования через UI). Стратегия "compile добавляет implicit
Normal когда есть userSettingID" даёт ложные ADDED строки в bit-perfect.
Меняю на корректную модель:
- decompile сохраняет viewMode даже = 'Normal' если node физически
присутствует в XML (object form переходит автоматически)
- compile эмитит viewMode только если явно задан в JSON
Применено к: filter (item + group), dataParameters, conditionalAppearance,
selection items, order items.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-TableAxisBlock теперь читает name на любой оси (раньше только
для row), плюс viewMode (если non-Normal), userSettingID и
userSettingPresentation на самом блоке column/row/point/series.
Build-FilterItem для FilterItemGroup теперь читает presentation,
viewMode (non-Normal), userSettingID, userSettingPresentation —
раньше группа сохраняла только items.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Платформа эмитит <viewMode>Normal</viewMode> автоматически когда у
элемента есть <userSettingID> (это сигнал пользовательской настройки).
Теперь compile делает то же:
- filter item, dataParameters item, conditionalAppearance item, table
axis (column/row/point/series) — все эмитят Normal если userSettingID
задан и явный viewMode не указан
Кроме того: FilterItemGroup теперь поддерживает свой viewMode /
userSettingID / presentation / userSettingPresentation (наравне с
обычными filter items).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Догнал spec за последние коммиты — описаны availableValues на DataSet
fields (по аналогии с parameters) и conditionalAppearance как
доступное поле структурного элемента group.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-Structure для StructureItemGroup теперь читает локальные
conditionalAppearance и outputParameters — раньше они терялись для
вложенных групп (только для top-level settings работало).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Реальные отчёты задают conditionalAppearance прямо на вложенной
StructureItemGroup (например — особое оформление шапки группировки).
Compile теперь эмитит её сразу после filter, перед outputParameters,
если задана в JSON.
outputParameters на StructureItemGroup уже эмитился — без изменений.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-Field теперь читает <availableValue> на DataSetFieldField,
типизирует value по xsi:type (boolean/decimal/string/dateTime),
сохраняет presentation как multilang dict если возможно.
Поле переходит в object form если есть availableValues.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Раньше availableValues эмитились только для parameters. Реальные
отчёты также задают availableValues на полях dataSet (например
ТипЗаписи=1..5 со ссылочными значениями), что давало отсутствие
важной семантики при roundtrip.
DSL: `field.availableValues: [{value, presentation, valueType?}]` —
типы значений автоопределяются (bool/decimal/dateTime/string),
presentation поддерживает multilang.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
В PS-версии накопилось три блока изменений за сессию, которые не были
отражены в Python-порте — синхронизирую:
- Emit-TableAxisBlock (filter/order/selection/outputParameters на
column/row/point/series)
- Emit-UserFields (UserFieldExpression / UserFieldCase в settings)
DSL spec обновлён: добавлены разделы userFields, расширены примеры
table column/row и chart points/series.
В SKILL.md изменения не вносятся — фичи редкие, описаны только в spec.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build userFields array в settings из <dcsset:userFields>. Поддержаны
оба подтипа (Expression с detail+total / Case с cases). Multilang title
и presentation корректно читаются как объекты.
Эффект на sample30: -5500 строк diff (целая ветка пользовательских
полей со всеми expression/presentation/case-структурами).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Пользовательские вычисляемые поля — отдельный блок <dcsset:userFields>
в начале settings. Два подтипа:
- UserFieldExpression: dataPath + title + detail{expression,presentation}
+ total{expression,presentation}
- UserFieldCase: dataPath + title + cases[{filter, value, presentation}]
DSL: тип определяется наличием 'cases' (case-форма) vs detail/total
(expression-форма) — без явного 'type'.
В SKILL.md не упоминаем (редкая фича — обычно настраивается пользователем
через UI «Изменить вариант»). Описано в spec.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-TableAxisBlock теперь читает filter и outputParameters блока,
order/selection сохраняются с точным присутствием (даже [Auto]) для
bit-perfect round-trip.
Дополнительно: nestedSchema (вложенные DCS-подсхемы) добавлены в Ring 3
fail-fast — фича редкая (15/490 в ERP), требует значительного
расширения DSL (рекурсивная DCS внутри JSON). Поддержку можно
вернуть позднее как nestedSchemas массив в корне JSON.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Колонки и строки таблицы (StructureItemTable.column/row) и оси диаграммы
(StructureItemChart.point/series) могут иметь свои filter, order,
selection, outputParameters — реальные отчёты активно это используют
для отбора и оформления внутри каждой оси.
Compile теперь:
- эмитит filter и outputParameters на column/row/point/series
- order/selection эмитятся только если заданы в JSON (раньше дефолтили
[Auto], что иногда расходилось с оригиналом)
Логика вынесена в общий helper Emit-TableAxisBlock.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build-DataSet для типа DataSetUnion теперь читает <item xsi:type="...">
(платформенный формат), сохранена обратная совместимость с <dataSet>
для XML, сгенерированных предыдущими версиями skd-compile.
Эффект на sample30: -12000 строк diff (LOST <v8:item>/<lang>/<content>
в полях inner-Union datasets).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Платформенный 1С пишет вложенные dataSets внутри DataSetUnion как
<item xsi:type="DataSetQuery">, а наш compile эмитил <dataSet xsi:type=...>.
Это вело к двум проблемам:
- сгенерированный XML отличался от платформенного (косметика для bit-perfect)
- skd-decompile симметрично искал <dataSet> и пропускал inner items
при чтении реальных схем — теряя все вложенные fields/titles
Эталон: upload/erf/ПроверкаЭкранирования/.../Templates/СКД_Объединение
показывает что Designer всегда пишет <item xsi:type="..."> внутри Union.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Decompile теперь читает viewMode/itemsViewMode из XML и сохраняет в JSON
точно как было — даже Normal-значения (платформа эмитит эти теги
контекстно, и для bit-perfect нам важно наличие, а не сам режим).
Чтение:
- item-level: selection item, order item (новая object form)
- block-level: selection/filter/order/conditionalAppearance →
XViewMode на settings
- structure group: viewMode + itemsViewMode на самом item
- settings: itemsViewMode
Дополнительно:
- Убран shorthand @normal из filter/condApp/dataParam (Normal — default,
шум в JSON)
- Структурный shorthand "A > B > details" не сворачивается если есть
viewMode/itemsViewMode на элементе
- Selection/order на structure-item сохраняются даже = [Auto] —
compile теперь не дефолтит, поэтому наличие важно для bit-perfect
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DSL расширения (item-level — паттерн object form расширен):
- selection: {field, viewMode}
- order: {field, direction, viewMode} (новая object form)
- structure group: {type:group, viewMode, itemsViewMode}
DSL расширения (block-level на settings):
- selectionViewMode, filterViewMode, orderViewMode
- conditionalAppearanceViewMode
- itemsViewMode (на самих settings)
Compile эмитит viewMode/itemsViewMode только если явно задано в JSON —
это позволяет decompile сохранить точное наличие/отсутствие из XML и
получить bit-perfect round-trip (платформа эмитит эти теги
контекстно — на ABCXYZ-стиле для каждого блока, а в простых отчётах
без пользовательских настроек — не эмитит).
Дополнительно:
- Пустой LocalStringType теперь эмитится как self-closing (как платформа)
- Убран default order/selection=["Auto"] на StructureItemGroup
(раньше compile дефолтил, теперь эмитит только если задано)
В SKILL.md не упоминаем — фича редкая. Полное описание в spec.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Emit-OutputParameters принудительно использовал str(value), теряя
multilang dict {ru, en} → эмитил как "@{ru=...; en=...}". Теперь
auto-promote ptype=mltext если значение — PSCustomObject/dict.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Найдено через новый debug-tool debug/skd-decompile/verify-roundtrip.ps1
(XML→decompile→compile→diff на сэмпле 30 ERP).
1. Emit-CalcFields appearance: третий дубликат-emitter с тем же
multilang-багом как был в Emit-AppearanceValue для field/cond. Унифи-
цирован через Emit-AppearanceValue. (compile.ps1+py)
2. Emit-SelectionItem: lwsTitle для folder + field-with-title
эмитили "$($item.folder)" — для hashtable получали "@{ru=X; en=Y}".
Унифицирован через Emit-MLText с новой опцией -NoXsiType
(lwsTitle в оригинале без xsi:type, в отличие от <title> в fields).
(compile.ps1+py)
3. Build-Parameter hidden detection: combo availableAsField=false +
useRestriction=true. Только availableAsField=false (без
useRestriction) → object form `availableAsField: false`.
На сэмпле 30 ERP roundtrip-diff: ADDED <content> 667 → 129
(multilang-потери в selection закрыты). Остаточные LOST <item> ~10k —
другие потери (attributeUseRestriction и проч.) — отдельная задача.
Versions: compile v1.35→v1.36, decompile v0.20→v0.21.
1. Внешний .sql теперь именуется <outputBasename>-<datasetName>.sql
(раньше просто <datasetName>.sql). Защищает от коллизий при
batch-decompile нескольких отчётов в одну папку: имена dataset'ов
часто совпадают ("НаборДанных1" — почти везде).
В JSON: "query": "@<outputBasename>-<datasetName>.sql".
2. ConvertTo-CompactJson: Try-InlineJson — пытается сериализовать
container на одну строку. Если результат + текущий indent ≤120
chars → inline; иначе multi-line. Применяется и к объектам и к
массивам (включая массивы из примитивов — раньше они всегда были
inline, что давало гигантские строки на длинных fields).
Примеры inline (объекты ≤120 chars):
- { "value": "B", "style": "header" }
- { "name": "Имя", "expression": "Имя" }
Длинные объекты и массивы — multi-line как раньше.
v0.19 → v0.20.
Заменил ConvertTo-Json (PS5.1) на собственный ConvertTo-CompactJson:
- 2-пробельный indent (вместо 4 + выравнивание keys по длине)
- Массивы примитивов (string/number/bool/null) — inline `[a, b, c]`
- Массивы с объектами/nested arrays — multi-line как раньше
- Кириллица в UTF-8 (без \uXXXX-escapes)
- Корректный escape строк (\\", \\, \n, \r, \t, \uXXXX для control chars)
Build-TotalField: shorthand "name: expr" для любого однострочного
expression. Раньше object form применялась когда expression не Func(arg).
Теперь — только когда есть group или expression многострочный.
Compile принимает любой shorthand вида "dataPath: expression" (Parse-
TotalShorthand делает split по первому ":").
Save-UserStyles тоже использует новый сериализатор.
Все 16 декомпиляционных snapshot'ов обновлены (косметика — JSON
структурно тот же, тесты round-trip проходят).
На реальном отчёте (целевой корпус): 405 → 264 строк (-35%).
v0.18 → v0.19.
Если query ≥3 строк и указан -OutputPath, decompile выносит SQL в
<datasetName>.sql рядом с decompiled.json. В JSON эмитится "@<name>.sql"
вместо inline-строки.
- Имя файла: dataset name (sanitized — non-word chars → _), коллизии
разрешаются суффиксом _2/_3/...
- compile уже поддерживает синтаксис @file.sql (Resolve-QueryValue)
— round-trip симметричен.
- Тесты не изменились: все тестовые queries по 1 строке (порог не
срабатывает).
- На реальных отчётах (ERP/ACC main DCS — 10-50 строк query типично)
даёт значительно более компактный JSON + читаемый .sql с подсветкой
синтаксиса в IDE.
Новый тест dataset-query-multiline (round-trip с внешним .sql).
v0.17 → v0.18.
Закрывает три gap'a, выявленных при полном прогоне ERP+ACC:
1. DataSetFieldFolder — поле-папка для UI-группировки полей в композиторе
настроек. Только dataPath + title, без valueType/role.
- DSL: object form поля с `folder: true`.
- Compile: при folder=true → <field xsi:type="DataSetFieldFolder">.
- Decompile: распознать xsi:type, эмитить object form.
2. GroupItemAuto — пустой <item xsi:type="GroupItemAuto"/> в groupItems
(auto-grouping, аналогично "Auto" в selection).
- DSL: строка "Auto" в groupFields.
- Compile/decompile: round-trip.
3. Empty <field/> в conditionalAppearance/selection (wildcard — apply
to all). Раньше — SelectionItem: sentinel. Теперь эмитим как "Auto"
(семантический эквивалент через SelectedItemAuto).
Новый тест dataset-folder-and-auto-group (round-trip).
Versions: compile v1.34→v1.35, decompile v0.16→v0.17.
На предыдущем прогоне ERP+ACC: 227 sentinel'ов (218 DataSetFieldFolder
+ 7 GroupItemAuto + 2 SelectionItem). После — 0.
Emit-AppearanceValue / emit_appearance_value: hashtable/PSCustomObject/dict
значение → LocalStringType независимо от ключа. Раньше для значения
{ru: "ДЛФ=D", en: "DLF=D"} compile эмитил xs:string "@{ru=ДЛФ=D; en=DLF=D}"
(строковое представление PS hashtable) — потеря структуры и неверный XML.
Wrapper {use: false, value: ...} распознаётся точечно (требуются оба ключа,
чтобы не путать с multilang dict без 'use').
Унификация field-level appearance: parse сохраняет значение как есть
(а не str(v)), emit использует Emit-AppearanceValue вместо дублированной
mini-логики. Side-effect: "true"/"false" в field appearance теперь эмитятся
как xs:boolean (раньше — xs:string). Корректнее для 1С; обновлён один
snapshot теста compile.
Новый тест appearance-multilang-value (поле + conditionalAppearance с
multilang Формат — round-trip bit-perfect).
Versions: compile v1.33→v1.34.
Закрывает п.2 из handoff («известный баг с multilang appearance values»).
Cell в rows теперь может быть либо string ("text"/"{param}"/"|"/">"/null),
либо объектом {value, style: "presetName"}. Object form применяется когда
стиль ячейки отличается от template default.
compile (ps1+py): helpers _get_cell_value / _get_cell_style_or_default.
Emit-AreaTemplateDSL / _emit_area_template_dsl используют per-cell style
для appearance вместо единого template style.
decompile: refactor Build-Template. Первый pass — собрать style name per
cell в cellStyleMap. Второй pass — выбрать template default как most
frequent style. Третий pass — обернуть в {value, style} ячейки, чьи
стили отличаются от default. TemplateStyleMismatch sentinel удалён —
теперь все случаи покрываются через inline override.
Дедуп при обоих pass'ах (Match-PresetByShape) работает через
effectivePresets (built-in + user + ранее аллоцированные customN), так
что одинаковые shape'ы получают одно имя.
Новый тест template-inline-cell-style (round-trip bit-perfect).
Versions: compile v1.32→v1.33, decompile v0.15→v0.16.
Метрики на момент коммита:
- ERP-сэмпл 30: 30/30 clean, 0 sentinel'ов
- Корпус из 40 отчётов целевого класса: 40/40 clean, 0 sentinel'ов
Закрывает категории A, B, C полностью на обоих корпусах.
Категория C — закрыта для однородных шаблонов с custom appearance.
Refactor fingerprint → preset shape (11 полей: font/fontSize/bold/italic/
hAlign/vAlign/wrap/bgColor/textColor/borderColor/borders). vAlign теперь
учитывается в matching (раньше игнорировался).
Алгоритм:
1. При -OutputPath загружается existing skd-styles.json рядом (если есть);
user presets накладываются на built-in по той же логике что и compile.
2. Каждая ячейка → Extract-CellPreset → Match-PresetByShape против
effectivePresets (built-in + user).
3. Если не match — Allocate-CustomStyle: новый customN, регистрируется
в effectivePresets и accumulator.
4. По окончании Save-UserStyles пишет skd-styles.json рядом с outputPath
(preserved existing + новые customN).
5. Compile подхватит файл по своим search-путям (cwd/dirname/scan-up).
В SKILL.md не добавляем (custom стили — для round-trip, не для написания
модель с нуля; built-in `data/header/subheader/total/none` остаются
основным интерфейсом для модели).
- runner.mjs: новый preRun step `writeFile` для подготовки fixture-файлов
в workDir (нужен для теста с предзаписанным skd-styles.json).
- Новый тест template-custom-style: preRun пишет myHeader preset,
скомпилирует темплейт, decompile reverse'ит → переиспользует имя
myHeader (не создаёт customN).
- v0.14 → v0.15.
Метрики:
- ERP-сэмпл 30: 24 → 0 sentinel'ов, clean 26 → 30/30
- Целевой корпус 40 отчётов: 39 → 25 sentinel'ов (часть закрыта), clean
19 → 20/40. Остаточные — шаблоны с разными стилями в разных ячейках
одного шаблона (нужно per-cell style override — отдельная задача).
Закрывает простую часть категории C: шаблоны где у ячеек appearance
содержит только per-cell атрибуты (МинимальнаяШирина и др.) без font/
borders/colors. Раньше такие шаблоны попадали в TemplateStyleMismatch.
- skd-compile (ps1+py): новый preset 'none' со всеми стилевыми полями
null/false. Emit-CellAppearance / _emit_cell_appearance пропускают
Font-элемент когда style.font=null.
- skd-decompile: пустой fingerprint (после отсева per-cell ключей) не
считается за стиль ячейки; если все non-merge ячейки шаблона имели
пустой fp — эмитим style="none" вместо sentinel.
- Новый тест template-no-style (round-trip bit-perfect).
- Versions: compile v1.31→v1.32, decompile v0.13→v0.14.
Метрики:
- ERP-сэмпл 30: 32 → 24 sentinel'ов, clean 24→26/30
- Корпус из 40 отчётов целевого класса: 45 → 39 sentinel'ов, 19/40 clean
Остаточные sentinel'ы — реальный custom appearance (нестандартный шрифт/
выравнивание/цвет вне built-in пресетов). Требует расширения DSL под
hashtable-style — отдельная задача.
Закрывает категорию B полностью на ERP-корпусе:
- selection.folder теперь рекурсивный: внутри items могут быть string,
{field, title}, или ещё одна {folder, items: [...]}. Compile/decompile
обходят дерево рекурсивно (Emit-SelectionItem / Build-SelectionItem).
- structure: новая ветка type=nestedObject с {objectID, settings:
{selection, filter, order, conditionalAppearance, outputParameters}}.
- groupFields теперь объектная форма {field, groupType?, periodAdditionType?}
когда не дефолт (Items / None). Compile уже принимал; decompile перестаёт
ставить warning GroupItemDetails. Try-StructureShorthand игнорирует
object-form поля при сворачивании в строку.
- Refactor: Build-Structure для StructureItemGroup теперь использует
общий Get-GroupFields вместо дублированного inline-кода.
В SKILL.md не добавляем (формы редкие/сложные, модель не пишет с нуля).
Новый тест structure-nested-and-folder покрывает все три случая bit-perfect.
Versions: compile v1.30→v1.31, decompile v0.12→v0.13.
На сэмпле 30 ERP-отчётов: 754 → 32 sentinel'ов (-96%), clean 4 → 24/30.
Остаточные 32 — все TemplateStyleMismatch (категория C, диагностика).
DSL: object-form ключ inputParameters — массив элементов, каждый типизирован
по форме value:
- choiceParameters: [{name, values: [...]}] — параметры выбора (DesignTimeValue)
- choiceParameterLinks: [{name, value, mode}] — связи параметров выбора
- value (+ optional use=false) — простое типизированное значение (bool/string/number)
Compile: Emit-InputParameters / emit_input_parameters → <r:inputParameters>...
Decompile: Read-InputParameters читает любой xsi:type, без SilentDrop warnings.
Build-Parameter — убран вызов несуществующего Check-InputParameters.
В SKILL.md не добавляем (форма сложная — модель не пишет с нуля, но при
декомпиляции из реального отчёта получает корректно и compile примет назад).
Новый тест field-input-parameters (3 типа элементов, bit-perfect round-trip).
Versions: compile v1.29→v1.30, decompile v0.11→v0.12.
На сэмпле 30 ERP-отчётов: SilentDrop:ChoiceParameters/Links 51 → 0,
clean reports 8 → 21, total sentinel'ы 109 → 58.
- Parse-RoleSpec (ps1+py): принимает string ("dim"/"flag1 flag2 K=V") / array / object
- Parse-FieldShorthand: извлекает K=V из shorthand поля (regex \w+=\S+)
- emit: токены → <dcscom:KEY>true</dcscom:KEY>; extras → <dcscom:KEY>VALUE</dcscom:KEY>
(без whitelist; раньше принимались только accountTypeExpression и balanceGroup)
- @period sugar поддерживает override через periodNumber/periodType KV
- Fix имени: balanceGroup в JSON принимается как deprecated alias для balanceGroupName
(в реальном XML 1С элемент называется balanceGroupName; старый код compile эмитил
несуществующий <dcscom:balanceGroup> — ни одного попадания в ERP-корпусе)
- SKILL.md, docs/skd-dsl-spec.md: единое описание четырёх форм роли
- v1.27 → v1.28
- Get-RoleInfo (вместо Get-RoleTokens): любой dcscom:KEY=true → @KEY;
accountTypeExpression/balanceGroup → extras → object form role
(compile уже поддерживает object form через {key: true})
- Build-Field: object form role при наличии extras
- Silent-drop → warnings (без ломания round-trip):
* Check-InputParameters — ChoiceParameters/ChoiceParameterLinks (non-empty)
* orderExpression на field
* scope в conditionalAppearance item
На сэмпле 30 ERP-отчётов: 754 → 147 sentinel'ов (-80%), 8/30 clean.
При наличии __unsupported__ маркеров в JSON (от skd-decompile, Кольцо 2)
compile завершается с exit 4 и понятным сообщением: id/kind/loc каждого
sentinel и подсказка про .warnings.md рядом.
Рекурсивный walk JSON покрывает hashtable/PSCustomObject/array. Sentinel
в любом месте дерева — фейл.
Bump skd-compile v1.26 → v1.27.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sentinel/warnings уже работали по ходу разработки слоёв. Добавил
явные Ring 3 проверки до основного pipeline:
- Picture cells в шаблонах (<dcsat:item xsi:type=Picture>) → exit 3
- Параметры типа ХранилищеЗначения (v8:ValueStorage) → exit 3
- templateCondition (вариативные шаблоны) → exit 3
- Не-DCS корневой элемент → exit 2 (теперь через [Console]::Error,
без обвязки Write-Error и с понятным сообщением по-русски).
Сообщения fail-fast включают рекомендацию использовать /skd-edit
для точечной работы.
Регрессий нет — все 7 синтетических тестов остаются 0 diff.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- DataSetObject: уже была базовая поддержка objectName, теперь fields
тоже извлекаются (отвязали от switch специально для query).
- DataSetUnion: рекурсивный walk вложенных <dataSet> элементов через
Build-DataSet → items[] с полными nested-dataset объектами.
- Вынес логику в Build-DataSet функцию.
Round-trip clean (GUID-normalized) на всех 7 синтетических тестах,
включая ds-types-test с Query + Object + Union в одной схеме.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Парсинг <template>/<template xsi:type=AreaTemplate>/<dcsat:TableRow>/
<dcsat:tableCell> в rows[][].
- Распознавание содержимого ячейки: dcsat:Text → строка, dcsat:Field с
dcscor:Parameter → "{Имя}", dcsat:Field с LocalStringType → строка/multilang,
пустая → null.
- Merge через appearance-флаги ОбъединятьПоВертикали/ОбъединятьПоГоризонтали
на пустых ячейках → "|"/">".
- Детект built-in стилей (header/data/subheader/total) через нормализованный
fingerprint appearance — без сравнения per-cell ширин/высот/merge-флагов.
При несовпадении или неоднородности — sentinel TemplateStyleMismatch.
- Извлечение widths из appearance первого row и minHeight из первой ячейки.
- Drilldown-свёртка: для cells с appearance Расшифровка=Расшифровка_X
и template-параметром DetailsAreaTemplateParameter Расшифровка_X →
свертываем в `{name, expression, drilldown: X}`.
- Сохранение порядка template parameters через [ordered]@{}.
- Fix namespace URI для areatemplate (`area-template` с дефисом).
Bit-perfect round-trip 55924→55924 и 28590→28590 на синтетике с header/data
стилями, merge, drilldown, шаблонными параметрами.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- calculatedFields: shorthand с [title], type, expression и #restrict-флагами;
object form при appearance или multilang title.
- totalFields: детект Func(name) и Func(expr) → shorthand "name: Func"/"name: Func(expr)";
object form при привязке к группе.
- parameters:
- shorthand с [title], type, value, @-флагами;
- распознавание StandardPeriod variants → значение в shorthand;
- @valueList, @hidden флаги;
- availableValues с presentation;
- object form для availableValues/multilang/composite type/expression.
- autoDates-сворачивание: для каждого StandardPeriod-параметра ищем пару
dependent с expression `&P.ДатаНачала`/`&P.ДатаОкончания` (распознаём по
expression, не по имени) и сворачиваем в @autoDates на родителе.
- decimal-тип всегда эмитится с явными (D,F) — JSON читаемее.
- useRestriction суппрессим в параметрах (auto-generated для @hidden).
Bit-perfect round-trip 7468→7468 байт на синтетике
(3 calc + 2 total + 5 параметров включая @autoDates).
Реальный ERP «АнализИзмененийЛичныхДанныхСотрудников» (1035 строк) —
0 warnings при декомпиляции.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Распознавание типов (string/decimal/boolean/date/dateTime/time/CatalogRef.X
и пр.) с qualifiers (decimal-точность/знак, string Length/AllowedLength,
date fractions) → shorthand или composite-массив.
- Роли (@dimension/@account/@balance/@period) с детектом сложных roleAttributes
как sentinel.
- Restrictions (#noField/#noFilter/#noGroup/#noOrder) из useRestriction.
- Multilingual title с авто-сворачиванием {ru:"..."} в строку.
- appearance с поддержкой LocalStringType значений (например, Формат).
- presentationExpression.
- Свёртка дефолтного dataSource (ИсточникДанных1/Local) в умолчание.
- Автодетект object vs shorthand формы поля.
Bit-perfect round-trip на синтетике из 11 разнотипных полей.
Реальный ERP-отчёт АнализВерсийОбъектов декомпилируется с 0 warnings.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Убрал ложное обещание «структурной эквивалентности» (DSL покрывает
подмножество СКД). Слил «Гарантии» и «Не поддерживается» в раздел
«Что получаешь» с тремя категориями (покрытое / sentinel / fail-fast).
Добавил Workflow — декомпил это начало процесса, а не финал.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Layer 1 of the skd-decompile plan: SKILL.md with disable-model-invocation,
ps1 skeleton with XML→JSON pipeline, namespace probe for non-DCS root,
sentinel/warnings accumulator, and DataSetQuery extraction (query only).
Test case minimal-query demonstrates round-trip via skd-compile preRun.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Добавляет одну строку-подсказку в add-field: для попадания в Selection
конкретной группировки (а не variant) — связка -NoSelection + add-selection
с @group=. Это уже работало, но не было явно зафиксировано в SKILL.md.
Расширять сам add-field параметром -Group/@group= не стали — текущий
двухкомандный идиом более атомарен и не создаёт edge cases вроде
взаимодействия @group= и -NoSelection.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Раньше PS1-порт делал `Join-Path (Get-Location) $OutFile` без проверки,
что приводило к невалидным склейкам типа `C:\cwd\C:\abs\path.txt`, и
запись падала с «The given path's format is not supported».
Теперь: если путь абсолютный — нормализуется через `Path::GetFullPath`,
если относительный — резолвится против CWD. Python-порт уже был корректен,
только version bump.
Дополнительно: `args_extra` в runner.mjs теперь поддерживает подстановку
`{workDir}` — нужно для тестов с абсолютными путями внутри workspace.
Тесты: `skd-info/outfile-absolute-cyrillic` (PS + Python).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
patch-query теперь нормализует CRLF/CR → LF в old/new/query перед поиском,
поэтому многострочные шаблоны с любым стилем переводов строк находятся
корректно (XmlDocument декодирует text-узлы как LF).
При not-found вместо сухого сообщения выводится воронка диагностики:
1) cross-dataset probe — «Found in dataset 'Y' instead — wrong -DataSet?»
2) tolerant probe (collapse whitespace + NBSP) — «would match with
whitespace normalized» + точка расхождения
3) prefix divergence — «matched N of M chars, expected 'X' (U+...) but
got 'Y' (U+...)» + короткий контекст
Тесты: 4 новых кейса (positive CRLF-tolerant + 3 диагностических negative).
Регрессия 45/45 PS + 45/45 Python.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Calibrated against live Designer output in upload/erf/ПроверкаЭкранирования.
- New type 'time' (synonym 'время'): xs:dateTime with DateFractions=Time
for time-of-day values. Designer uses the same xs:dateTime XSD type as
date/dateTime — only DateFractions differs. Empty value: typed-zero
0001-01-01T00:00:00 (same as dateTime).
- Extended string regex to accept (N,fix) → AllowedLength=Fixed (was
Variable-only). Non-empty fixed-string values are emitted as-given
without space-padding to Length — the platform handles padding on save.
- Composite types in parameters (array of types in object form, e.g.
["string(10,fix)", "CatalogRef.X"]) now work end-to-end: valueType
emits each type with its qualifiers, and empty composite values
serialize as <value xsi:nil="true"/> matching Designer.
Test case empty-param-values extended with 5 new params covering all
three additions. Snapshot validated by skd-validate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calibrated against 1106 vendor reports (ERP 8.3.24 + БП 8.3.27).
Three categories of false positive removed:
- CalculatedField with empty <expression/> demoted error→warning.
Three legitimate vendor patterns surfaced:
* sibling totalField with same dataPath provides the formula
(used in cancellation-rate and share-percentage reports)
* groupTemplate references the field as group name
* field exists only as a declarative anchor for settingsVariants
Warning preserved so genuinely-missing formulas still surface.
- Duplicate template name demoted error→warning. Vendor configs ship
reports (БазаНормируемыхРасходов/Выручка) with three <template> blocks
named Макет1 — the platform identifies templates by position, not by
<name>. Warning still flags the collision without failing validation.
- comparisonType whitelist extended with NotInHierarchy and
NotInListByHierarchy. Existing list was missing the negated
hierarchy operators used in 20 of the 1106 reports.
Result: 0 false positives across the corpus, all genuine errors still
caught (verified separately against intentionally-broken fixtures).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Calibrated against ~868 real ERP/БП reports — three false positives caught:
1. Composite types: <v8:Type>xs:string</v8:Type> followed by
<v8:Type>d4p1:CatalogRef.X</v8:Type> with a single trailing
<v8:StringQualifiers> is a legitimate pattern. Rewritten check to
collect all <v8:Type> and qualifier blocks per <valueType>, then
verify each qualifier has a matching scalar type anywhere in the
block — not necessarily right before it.
2. System types: AccumulationRecordType (and similar enum-like system
types) use the http://v8.1c.ru/8.1/data/enterprise namespace
(without /current-config) and a plain TypeName local name with no
dot. Whitelisted as a second valid namespace for ref-like types.
3. v8: scalar types extended: v8:Null, v8:Type, v8:ValueStorage —
present in real configs as type-less placeholders.
Also reverted SKILL.md change from previous commit (validator details
don't belong in user-facing docs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
skd-validate was purely structural (names/refs/duplicates) and missed an
entire class of bugs that XDTO rejects at db-load-xml — exactly the
kinds of mistakes the LLM (or hand-edits) commonly introduce.
New section 16: valueType structural — each <v8:Type> must have a known
prefix (xs:/v8: or any prefix bound to enterprise/current-config),
qualifier blocks must match their preceding type, and qualifier
internals (Digits/FractionDigits/AllowedSign, Length/AllowedLength,
DateFractions) must use legal tokens.
New section 17: value content — <value xsi:type="dcscor:DesignTimeValue">
rejects literal placeholders ('_') and empty strings, since these are
the exact symptom of the titan team's BUG-2.
5 new fixtures cover: bare-decimal, missing-qualifiers,
qualifier/type mismatch, ref-literal '_', bad AllowedSign token.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Emit-SingleValueType / emit_single_value_type previously required full
decimal(D,F) — anything else fell through to a fallback that produced
invalid <v8:Type>decimal</v8:Type> (no xs: prefix, no qualifiers).
New regex `^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$` accepts:
- decimal → 10,2,Any (money default — most common 1C intent)
- decimal(N) → N,0,Any (integer)
- decimal(N,nonneg) → N,0,Nonnegative
- decimal(N,M) → as before
- decimal(N,M,nonneg) → as before
Synonyms (число, число(N), etc.) inherit the same forms via Resolve-TypeStr.
Shared Emit-ValueType is called from fields, parameters, and output
parameters — one fix covers all three paths. 3 existing snapshots
regenerated with proper xs:decimal + qualifiers, plus new
decimal-qualifier-defaults test case covering all 5 forms × synonyms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Зеркалим решение из skd-compile: убираем .Replace('"','"') из Esc-Xml
и удаляем post-process, который принудительно ставил " внутри
<query>/<expression>. Реальный Конфигуратор так не пишет — экранирование
было анти-1С-стилем и портило round-trip diff.
Снимок add-calculated-field-restrict обновлён под новый формат.
Кейс preserve-entities-modify-parameter-title удалён: его смысл
инвертировался (теперь проверял бы нормализацию, а не сохранение),
а часть про многострочный xmlns уже покрыта preserve-xmlns-multiline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Конфигуратор внутри текстового контента <query>/<expression> оставляет " сырыми
(проверено на ERP DCS: 1504 raw " против 0 "). Убираем .Replace('"','"')
из esc_xml — теперь round-trip diff против типовых остаётся чистым.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
multilang-base/Template.xml содержал <editFormat xsi:type="v8:LocalStringType">
на <field xsi:type="DataSetFieldField">, что нелегально по XDTO-схеме DCS —
1С Designer падал с "Исключение XDTO" при загрузке через
LoadExternalDataProcessorOrReportFromFiles. Snapshot-тесты этого не ловили
(только byte-equality), а platform-verify (tests/skills/verify-snapshots.mjs)
ронялся на трёх кейсах с этой фикстурой.
Заменил <editFormat> на реалистичный <appearance> блок с вложенным
<dcscor:item xsi:type="dcsset:SettingsParameterValue"> и многоязычным
<dcscor:value> (ru + en) — структура взята из типовой ERP-выгрузки. Это
даёт более правильный test для preserve-unknown-children: <appearance>
содержит вложенный multi-lang xsi:type-узел, который точно прошёл бы
через DOM round-trip с искажениями, если бы _unknownChildren не работал.
preserve-unknown-children-modify-field: shorthand изменён с
"@ignoreNullsInGroups" на "@dimension" (no-op по составу role, но
триггерит rebuild). Прежний @ignoreNullsInGroups без @dimension давал
комбинацию, которую Designer отвергает (ignoreNullsInGroups валиден
только в контексте resource-роли).
39/39 snapshot suite (PS+PY) + 39/39 platform verify через erf-build →
Designer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В типовых конфигурациях (ERP, БП, ЗУП и т.д.) у полей и параметров обычно
есть мульти-язык title (ru + en, иногда + локализация). До этого modify-field /
modify-parameter / modify-dataParameter, перестраивая элемент через
Build-MLTextXml, оставляли только последнее найденное <v8:content> в ru —
en/uk/kk siblings молча терялись, и при следующей выгрузке Designer
ломал миграцию.
Read-FieldProperties сохраняет полный OuterXml <title> в _rawTitle и
коллекционирует OuterXml неизвестных дочерних элементов
(<editFormat>, <appearance>, кастомные расширения) в _unknownChildren.
Build-FieldFragment эмитит:
* _rawTitle как есть, если user не задал новый title;
* Patch-MLTextRu(_rawTitle, newRu) если user задал ru-override — патчит
только <v8:content> в <v8:lang>ru</v8:lang>, остальные языки сохраняет;
* _unknownChildren в конце поля (после valueType).
modify-parameter аналогично: при title-override проверяет multi-lang
(>1 <v8:item>) и патчит ru через Patch-MLTextRu, иначе ребилдит ru-only.
set-field-role сохраняет нестандартные подэлементы <role> (например
<dcscom:addition>, <dcscom:groupFields>), не входящие в фиксированный
known-children set и не указанные через kv в shorthand.
xmlns-стрип на захваченных OuterXml — лишние декларации (которые сериализаторы
добавляют для standalone-фрагментов) убираются.
PY: lxml etree.tostring по умолчанию включает .tail (whitespace после
закрывающего тега), что приводило к non-idempotent ростy whitespace при
повторных прогонах. Везде добавлен with_tail=False.
Новые тесты с idempotent: true:
* preserve-multilang-modify-field (ru-override на multi-lang title);
* preserve-multilang-modify-parameter (то же для параметра);
* preserve-unknown-children-modify-field (role flag, проверяем что
<editFormat> и en title не теряются).
Общая fixture: multilang-base/Template.xml с полем и параметром,
у каждого ru + en title; поле также имеет <editFormat>.
39/39 PS + 39/39 PY. skd-edit v1.20 -> v1.21.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Targeted follow-ups к round-trip фиксу:
* modify-field больше не теряет <valueType> при перестройке поля —
Read-FieldProperties сохраняет полный OuterXml элемента (StringQualifiers,
NumberQualifiers, DateQualifiers и т.п.), Build-FieldFragment отдаёт его
обратно. Лишние xmlns-декларации, добавляемые сериализатором при
выгрузке поддерева, стрипаются регексом.
* Line-ending convention теперь определяется при load (CRLF vs LF) и
единообразно применяется в финале save. Раньше CreateWhitespace и
Build-*Fragment везде использовали CRLF, что приводило к смешанным
переносам в LF-исходниках (и наоборот) и к non-idempotent выходу
modify-parameter title (run 1 → \n\t\t<title>\r\n... → run 2 →
\r\n\t\t<title>\r\n...).
* PS Insert-BeforeElement переведён на LF; все -join "`r`n" → "`n";
py "\r\n".join → "\n". Конечная нормализация переносов делается в
save в соответствии со script:LineEnding.
* preserve-entities-modify-parameter-title.json теперь idempotent: true
(после фикса CRLF leak'а двойной прогон byte-identical).
На реальной схеме diff после modify-field составил 30 строк: целевая
вставка title плюс полезная одноразовая коррекция ранее повреждённых
" в text-content <dcsat:expression>. modify-field идемпотентен.
skd-edit v1.19 -> v1.20.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
XmlDocument round-trip искажал Template.xml даже при отсутствии правок:
декодировал " в <query>/<expression>, схлопывал многострочный xmlns
корня, добавлял пробел перед /> и записывал файл при [WARN] not found.
Дирти-флаг ($script:Dirty / dirty) ставится только на успешной мутации;
финальный save пропускается с [INFO] No changes -- file untouched, если
ни одна операция в batch ничего не изменила. Post-process после OuterXml
восстанавливает raw-форматирование корневого xmlns из исходного файла,
re-escape `"` в текстах <query>/<expression> с anchored regex (не задевая
xsi:type="..."), и нормализует <foo .../> к <foo.../>.
Замеры на реальной схеме после modify-field: diff упал с 423 строк до 37
(94% шума устранено), повторный прогон byte-identical.
В runner.mjs добавлен caseData.idempotent: re-run + byte-equality на всех
файлах workDir. Три новых кейса (NO-OP, entity-preserve, xmlns-multiline)
+ общий fixture roundtrip-base. Все 33 ранее существовавших snapshot
перегенерированы под корректное форматирование (восстанавливают то, что
старый skd-edit ломал).
skd-edit v1.18 -> v1.19. PS и PY порты синхронизированы.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Новый канонический документ docs/web-test-regression-spec.md —
техническое описание движка регрессионных тестов: CLI, формат
тест-модулей, ctx-контракт, утверждения, три уровня хуков
(инфра/тест/контекст), конфиг, контексты Playwright и режимы
изоляции, форматы отчётов (JSON/Allure/JUnit), обнаружение тестов,
ошибки/таймауты/повторы, анализ результатов, глоссарий.
Документ предназначен для CI-интеграторов, ручного редактирования
сгенерированных тестов и сопровождения самого движка. Без дорожной
карты и внутренних self-тестов — только публичный контракт.
regress.md в скилле почищен: добавлены контракт ctx и список
утверждений (раньше модели приходилось читать исходники), срезаны
дубликаты с SKILL.md (live recon, паттерны catalog/document),
переформулированы анти-паттерны под специфику регресс-движка.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В тест-обёртке ACTION_FN при 1С-исключении на throw-ed Error
вешалась полная структура (step, args, errors, formState, stack,
screenshot), но при сборке отчёта движок брал из неё только
{message, step, screenshot} — остальные поля терялись. Платформенный
стек 1С, ради которого делается fetchErrorStack, в JSON-отчёт не
попадал; в Allure statusDetails.trace писался только log()-вывод
теста.
Что поменялось:
- errInfo собирается один раз после teardown (раньше был дубликат на
732 и 745), используется и для ctx.testResult (afterEach), и для
lastError, и для итоговой записи в results[].
- В errInfo добавлено поле onecError: e.onecError — структура с
stack.entries[{location, code}], formState, args, errors доезжает
до JSON-отчёта без обрезания.
- writeAllure склеивает statusDetails.trace из tr.output + (если есть)
onecError.stack.raw под разделителем "--- 1C stack ---". В Allure UI
платформенный стек теперь виден прямо в карточке упавшего теста.
Обратная совместимость: для падений без 1С-исключения (assertion,
навигация и т.п.) e.onecError === undefined → JSON.stringify его
выкидывает, форма записи { message, screenshot } сохраняется в
точности.
Проверено вручную на стенде tests/web-test/ — падающий тест с
ВызватьИсключение, JSON и Allure оба содержат полный stack.entries и
формированный trace.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Новая операция clear-conditionalAppearance в стиле clear-selection/
order/filter. Закрывает потребность "заменить набор правил оформления"
через clear + re-add.
- patch-query: многострочные подстроки уже работали (string.Replace
корректно обрабатывает \n). Зафиксировано в SKILL.md.
- add-total: shorthand-шаблон с тремя случаями (Func, Func(expr),
identity-выражение) — после fix Bug 6 поведение нужно явно объяснить.
- Косметика: убрана утечка XML-внутренностей в комментарии примера
set-field-role @period.
- Пример patch-query @once заменён на более типовой случай уникальной
подстроки (КАК ВТ_СтароеИмя вместо ЛЕВОЕ СОЕДИНЕНИЕ).
Регресс: 33/33 PS, 33/33 PY, 33/33 платформенный verify.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Раньше "DataPath: X" всегда заворачивалось в X(DataPath). Если X не
аггрегатная функция (например, имя другого ресурса или сам DataPath),
получалось некорректное выражение типа Проверка(Проверка).
Зеркалю логику из skd-compile: whitelist аггрегатных функций
(Сумма, Количество, Минимум, Максимум, Среднее + EN-варианты).
Для остального — identity (использовать funcPart как есть).
Сообщение [OK] теперь показывает фактически записанный expression.
Регресс: 32/32 PS, 32/32 PY, 32/32 платформенный verify.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Защищает от случайных замен в комментариях/совпадениях имён:
"ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ @once"
# fail, если в запросе 0 или 2+ вхождений
Без флага default — replace-all (как раньше, обратная совместимость).
При успехе сообщение содержит фактическое число вхождений
"(N occurrence(s))", помогает заметить неожиданную множественность
без явного @once.
Регресс: 31/31 PS, 31/31 PY, 31/31 платформенный verify.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Единый list-синтаксис: availableValue=v1[: p1], v2[: p2], ...
Элементы через запятую, представление после двоеточия.
- Запятые/двоеточия внутри значений и представлений — в одинарных кавычках:
availableValue=Окр1: 'руб., коп.', Окр1000: руб.
- add-parameter теперь принимает availableValue= и создаёт начальный список
в одном вызове (раньше требовался последующий modify-parameter).
- modify-parameter availableValue=... ЗАМЕНЯЕТ весь список (раньше
append). Согласуется с остальными modify-* для одиночных свойств.
- SKILL.md: добавлен shorthand-шаблон для modify-parameter,
расширен для add-parameter [availableValue=список].
Существующие тесты мигрированы со старого ;;-batch на новый list-синтаксис.
Снапшоты сохранились (тесты стартовали с пустого списка — semantics
совпадает для greenfield).
Регресс: 29/29 PS, 29/29 PY, 29/29 платформенный verify.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В -Mode fields -Name <field> к сводке Role добавляются не-bool
параметры роли (balanceGroupName, balanceType, parentDimension,
accountTypeExpression и т.д.) в формате name=value.
Bool-флаги (@balance, @dimension, ...) отображаются как раньше.
False-значения по-прежнему скрыты.
Регресс: 6/6 PS, 6/6 PY (существующие snapshots не задеты).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- @hidden — скрывает параметр от пользовательских настроек
(useRestriction=true + availableAsField=false). Для констант-параметров.
- @always — параметр всегда подставляется в запрос (use=Always).
Используется самостоятельно для видимых обязательных параметров.
- Композируются: @hidden @always одной строкой даёт типовой паттерн
"скрытая константа всегда применяется".
- Поддержка в add-parameter и modify-parameter, идемпотентны.
Регресс: 25/25 PS, 25/25 PY, 25/25 платформенный verify.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В реальных выгрузках ERP/БП встречаются непрозрачные платформенные
DataPath, которые невозможно проверить из одного Form.xml:
- bare numeric ("10", "1000003") — внутренние индексы платформы
- "N/M:<uuid>" — ссылка на метаданные по UUID
Раньше Check 5 ругался на них "attribute not found". Теперь такие
пути пропускаются без счёта в paths checked и без ошибки.
Реалистичные пользовательские опечатки (кириллица в имени атрибута)
продолжают ловиться обычной проверкой attrMap.
Добавлен тест-кейс datapath-opaque-refs, версия v1.5 → v1.6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
В Check 5 раньше брался первый сегмент DataPath и искался в attrMap,
из-за чего ложно ругались реальные формы ERP/БП с путями вида
Items.<TableName>.CurrentData.<Field> (подвалы, инфо-панели) и
~<DynamicListAttr>.<Field> (текущая строка списка).
Теперь:
- ведущий ~ стрипается перед разбором сегментов;
- для Items.<Table>.CurrentData.* находим элемент-таблицу по name,
берём её <DataPath> (атрибут DynamicList/TableSection) и проверяем
его в attrMap. Если таблицы нет — Error; если третий сегмент не
CurrentData — Warn.
Добавлен тест-кейс datapath-currentdata, версия скриптов v1.4 → v1.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Добавлена строка в таблицу навыков и в перечень docs/ для нового
docs/web-test-regression-guide.md (был забыт при первичном коммите
регресс-гайда).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docs/web-test-regression-guide.md — пользовательские сценарии работы
с моделью для покрытия прикладного решения регрессом (русский, по
аналогии с web-test-recording-guide.md): структура tests/<app-name>/,
диалоги с моделью, пример организации покрытия, отчёты Allure +
categories.json.
- .claude/skills/web-test/regress.md — инструкция модели по написанию
регрессионного набора: разведка (метаданные + живой проход через exec),
layout по фичам, готовые шаблоны (CRUD/document/DCS/multi-user/repro),
severity, anti-patterns, failure triage, _allure/ конвенция.
- SKILL.md — указатель на regress.md в конце файла (рядом с recording).
- docs/web-test-runner-spec.md → upload/ (был внутренним планом
разработки, не пользовательской документацией).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run.mjs:
- syncAllureExtras(testDir, reportDir) копирует все файлы из
<testDir>/_allure/ в reportDir перед генерацией отчёта. Underscore
в имени параллелен _hooks.mjs (инфра, не тест) — discovery его
пропускает.
- Вызов после writeAllure при --format=allure.
tests/web-test/_allure/categories.json — 7 правил классификации падений
по нашему 1С-домену:
1. License pool exhausted (1C) — известный multi-context flake.
2. 1C application error (modal) — exception modal через fetchErrorStack.
3. Section panel icon-only — деградация состояния стенда.
4. Navigation lookup miss — navigateSection/openCommand/navigateLink/switchTab.
5. Element not found — clickElement/fillField/selectValue/closeForm/fillTableRow/deleteTableRow.
6. Test timeout — Timeout (Nms) от раннера.
7. Assertion failure — наши createAssertions + 1С-specific (formHasField/tableHasRow/noErrors).
spec §9: раздел «Доп. файлы Allure через <testDir>/_allure/» с таблицей
поддерживаемых типов (categories.json / environment.properties /
executor.json) и минимальным примером.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run.mjs:
- buildSeverityIndex(config) — валидация config.severity (inverted map
«уровень → [теги]») при загрузке: ключи только из blocker|critical|
normal|minor|trivial, теги не дублируются между bucket'ами,
defaultSeverity тоже валидируется. fail-fast через die.
- resolveSeverity(t, severityIndex):
1. mod.severity если задан и валидный — выигрывает.
2. max-rank среди тегов (стандартные имена severity или маппинг).
3. config.defaultSeverity или 'normal'.
Rank: blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1).
Max-wins инвариантен к порядку тегов.
- writeAllure: добавлены labels suite (= dirname(t.file) или 'root') +
severity. Тег `tag` остался как раньше.
- testResult пробрасывает t.severity (для passed/failed веток).
- SEVERITY_RANK/LEVELS объявлены в модульной шапке (top-level await на
cmdTest начинается до конца тела модуля, TDZ-аккуратность).
webtest.config.mjs: severity policy для нашего сьюта (smoke +
multi-context → critical, recording → minor, defaultSeverity = normal).
spec.md §7: раздел про severity-policy в конфиге с валидацией.
spec.md §9: «Авто-эмиссия label-ов» — tag/suite/severity + правила резолва.
Регресс 19/19 ✓ (9m 7.6s). Распределение по уровням после исправления
'record' → 'recording' в маппинге: 13 critical / 5 normal / 1 minor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Зафиксирована конвенция:
- Discovery рекурсивный, путь попадает в отчёт.
- Per-folder hooks/config/context-default НЕ поддерживаются (by design).
- Группировку в отчётах делать через tags, не через путь.
- Сортировка по полному пути (`warehouse/01-x` после `sales/02-y`) —
для глобального порядка нужны 3-значные префиксы или теги-фазы.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
browser.mjs:
- + closeContext(name): logout slot + close page (tab) или context (window),
удаление из реестра. Throw если name неактивен (рулило: nicht den aktiven
closen, recorder always attached к active → invariant простой).
- _logoutSlot(slot, waitMs) — извлечён из disconnect, переиспользуется в
closeContext.
run.mjs:
- ensureContext() после createContext вызывает hooks.afterOpenContext(ctx, name, spec).
- wrapCloseContextHook() оборачивает ctx.closeContext (и каждую scoped-обёртку)
чтобы перед browser.closeContext fir'ить hooks.beforeCloseContext.
- Финальный teardown в finally: для всех живых контекстов кроме первого
(survivor) — beforeCloseContext + closeContext; для survivor только хук,
его закрывает disconnect().
_hooks.mjs v0.5:
- afterOpenContext инжектит persistent DOM-badge с displayName в правый
верхний угол page — в записанном видео всегда видно, какой контекст.
- beforeCloseContext counter-only.
- _state расширен полями afterOpenContext / beforeCloseContext.
15-multi-context-handover.test.mjs:
- +2 шага: closeContext('b') после handover, попытка closeContext(active)
ловится throw'ом с проверкой message.
00-hooks.test.mjs:
- +1 ассерт: afterOpenContext >= 1 (default уже создан), beforeCloseContext === 0
в теле первого теста.
spec §6:
- Раздел «Контекстный уровень» (afterOpenContext / beforeCloseContext + правила closeContext).
- ASCII-диаграмма порядка хуков обновлена с per-context lifecycle.
Регресс 19/19 ✓ (9m 16.8s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
_hooks.mjs v0.4: beforeEach под условием ctx.isRecording() показывает
title slide с testInfo.name + displayName первичного контекста как
subtitle, ждёт 1.5с через ctx.wait() и убирает.
В обычном регрессе (без --record) — ветка скипается, overhead ноль.
Под --record: 01-navigation 12.1s → 13.9s (+1.8с на слайд).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13-misc.test.mjs: setup-шаг упрощён до `assert.ok(existsSync(epfPath))`.
EPF-сборку (epf-init → form-add → form-compile → epf-build) забрал
_hooks.mjs.prepare() — здесь только проверка артефакта с понятной
ошибкой при отсутствии: «запустите раннер с `-- --rebuild-epf`».
webtest.config.mjs: URL обоих контекстов переключён на
`/webtest-runner/ru_RU` — отдельная публикация автономного стенда,
не конфликтует с интерактивной разведкой через `/webtest` на 8081.
Новый tests/web-test/_hooks.mjs v0.2 с prepare()/cleanup().
prepare() поднимает изолированный стенд:
- Hash-locks `tests/skills/.cache/webtest-stand/{config,epf}.lock`
на sha256 от build-steps и EPF_SPEC — автоматический skip
пересборки при отсутствии изменений.
- Слои конфиг XML / БД / EPF пересобираются независимо. Триггер
ручной — флаги `--rebuild-config`/`--reload-data`/`--rebuild-epf`/
`--rebuild-stand` (через `-- ...` после CLI раннера).
- Smart Apache: web-stop+web-publish выполняются только когда
пересоздаём БД (нужно освободить блокировку). Иначе probe-first:
жив (200) → no-op; мёртв → publish + probeReady. На warm-старте
prepare сводится к чтению локов и одному probe (~200ms).
- web-publish на собственном AppName `webtest-runner` :9191 — не
пересекается с интерактивной публикацией `webtest`.
- Кросс-платформенно: env WEBTEST_HOOKS_RUNTIME=python переключает
на зеркальные py-порты скиллов (для не-Windows стендов).
cleanup() пока stub — оставляем стенд поднятым между прогонами,
для full-shutdown ручной /web-stop или `-- --rebuild-stand`.
E2E-проверено: cold-start `--rebuild-stand` поднимает стенд за
~38s; warm-старт prepare = 0.0s; полный регресс 18/18 зелёный
за 9m 7.1s (включая оба multi-context-теста, которые исторически
флапали).
В CLI раннера всё после `--` собирается в массив hookArgs и
передаётся в инфра-хуки prepare/cleanup без интерпретации со
стороны раннера. Сигнатура расширена до { hookArgs, log, config }:
log — структурированный вывод раннера, config — разобранный
webtest.config.mjs. Шаблон «всё после `--` принадлежит вложенному
инструменту» — стандартная shell-конвенция (npm, cargo, pytest).
Спека §6 обновлена под новую сигнатуру, §6.1 закрепляет контракт
`--` ↔ hookArgs с примером. Help-строка раннера упоминает
разделитель.
Извлечены exports: getProjectInfo, resolveScript, execSkill,
replacePlaceholders, runSteps, platformLoadSteps, loadBuildSteps.
CLI-режим сохранён через import.meta.url-guard. Подготовка к
переиспользованию из tests/web-test/_hooks.mjs без дублирования
exec-логики и pipeline-шагов.
Раньше шаг был deferred с комментарием «требует список с видимой
filter-панелью». На самом деле существующая абстракция работает:
два advanced filterList на разных колонках Контрагентов создают
два badge'а в state.filters[], а unfilterList({field}) снимает
конкретный — оставляя остальные.
Новый шаг 09-filter/unfilter-specific (~14s):
- filterList('ООО', {field:'Наименование'}) + filterList('123', {field:'ИНН'})
→ state.filters = [{field:'Наименование',value:'ООО'}, {field:'ИНН',value:'123'}]
- unfilterList({field:'ИНН'}) → остался только Наименование badge
- unfilterList() → пусто
Старый комментарий «defer to filter-panel synthetic» удалён —
оказался устаревшим (видимо unfilterList({field}) уже умел работать
с advanced-filter badge'ами на синтетических списках).
timeout 09-filter поднят с 60000 → 120000ms (8 шагов теперь, +14s
для unfilter-specific).
Регресс: 16/18 зелёных. Два multi-context-теста (14/15) упали на
лицензионном пределе 1С — known environmental issue, не связано с
этим коммитом.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Реквизит шапки ПриходнаяНакладная.Менеджер типа CatalogRef.Контрагенты
с дефолтным choiceHistoryOnInput=Auto. Существующий Контрагент в той же
шапке имеет DontUse, что даёт парный контраст для тестирования влияния
флага на selectValue.
Новый шаг 04-selectvalue/auto-history:
- selectValue('Менеджер', 'ООО Юг') → method='dropdown' (typeahead активен,
префиксный поиск по Description находит «ООО Юг» в catalogue).
- Парный 04-selectvalue/direct-form (existing): selectValue('Контрагент',
'Север') → method='form' (typeahead подавлен DontUse → форма выбора).
Тест покрывает существующее ветвление selectValue по флагу
choiceHistoryOnInput без engine-доработок. Истории на сервере писать
заранее не нужно: typeahead использует prefix-match по Description,
а не статистику истории.
Полный регресс **18/18 зелёный** (8m 47.3s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширение синтетики: реквизит Поставщик типа CatalogRef.Контрагенты
добавлен в шапку ПриходнаяНакладная. Элемент формы Поставщик скомпилирован
с textEdit:false (новый DSL ключ form-compile v1.21 из коммита 32bf9c1):
ручной ввод запрещён, селект-кнопки нет, выбор только через форму выбора
по pick-кнопке.
Новый шаг 03-fillfields/direct-edit-form (~7s) — fillFields на Поставщик
('ООО Юг') возвращает method:'form', минуя обычные paste/typeahead/dropdown
ветки. fillFields внутренне детектит textEdit:false и сразу идёт через
форму выбора (selectValue path).
Полный регресс **18/18 зелёный** (8m 40.6s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширение синтетики: реквизит Источник составного типа
(CatalogRef.Контрагенты + CatalogRef.Номенклатура + CatalogRef.Организации)
добавлен в шапку ПриходнаяНакладная и в ТЧ Товары. meta-compile принимает
составной тип через строковый синтаксис `A + B + C` (см. SKILL.md:56) —
эмитит три `<v8:Type>` элемента с правильным `d5p1:` префиксом.
Элемент ТЧ-колонки переименован в ИсточникТЧ (path/title оставлены
оригинальные) — иначе form-compile генерирует одинаковые companion-имена
(`ИсточникКонтекстноеМеню`) для шапки и ТЧ, и платформа отказывает в
открытии формы документа: "К сожалению, возникла непредвиденная ошибка"
(server-side, без полезного stack). TODO в form-compile-bugs.md: учитывать
путь поля при генерации companion-имён, чтобы избежать конфликта.
Новый шаг 03-fillfields/composite (~25s) — покрывает selectValue с
параметром `{type}` на составном поле:
- Шапка: selectValue('Источник', 'ООО Север', {type:'Контрагенты'})
→ method:'form', type:'Контрагенты', выбор через каталог-форму.
- ТЧ: fillTableRow({Источник: {value:'Альфа', type:'Организации'}},
{row:0}) → method:'form', type:'Организации' (quickChoice=true →
без формы выбора, прямой dropdown).
fillFields на composite без type выбрасывает понятную ошибку
с инструкцией «specify the type: selectValue(...,{type:'ИмяТипа'})» —
поведение API стабильно.
timeout 03-fillfields поднят с 60000 → 120000ms (6 шагов суммарно
~63s, новый composite step добавляет ~25s).
Полный регресс **18/18 зелёный** (8m 28.7s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширение синтетики: новая обработка ДеревоНоменклатуры с реквизитом
формы Дерево типа ДеревоЗначений и колонками Номенклатура (ссылка,
read-only) + Цена (Number, editable). ПриСозданииНаСервере рекурсивно
обходит Справочник.Номенклатура и заполняет дерево, отражая иерархию
групп/элементов из справочника.
Обработка зарегистрирована в подсистеме Администрирование и в роли
Администратор (Use+View).
Новый smoke 16-tree-form.test.mjs (5 шагов, 17.1s) — покрывает
05-table/edit-form (fillTableRow method:'direct' на FormDataTree-колонке)
и 08-hierarchy/tree-edit (expand узла + правка Цены через index-row):
- setup: navigateLink('Обработка.ДеревоНоменклатуры'), таблица Дерево
- read-roots: 2 корневые группы (_kind:'group'), columns=Номенклатура,Цена
- expand: clickElement('Товары',{expand:true}) → 16 строк (1 + 15)
- tree-edit: fillTableRow({Цена:1500},{row:1}) → method:'direct',
Цена становится '1 500,00' (с non-breaking space 1С)
- cleanup: closeForm
Гэп: fillTableRow с row-by-name ('Товар 01') ловит SyntaxError в JS
eval. Использую row-by-index (TODO в web-test-bugs).
Полный регресс **18/18 зелёный** (8m 9.8s) на порту 9191.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Чтобы не конфликтовать с интерактивной разработкой на основном
Apache (8081, занят сторонним проектом), регрессионный регресс
теперь использует отдельный httpd-процесс на порту 9191. Тот же
httpd запускает /web-publish webtest -Port 9191 -V8Path 8.3.24.
Один процесс Apache → собственный пул лицензий 1С. На 8081 другие
проекты — наши тесты их не блокируют и наоборот.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.20 → v1.21 (ps1 + py).
Добавлен ключ DSL `textEdit` для элемента input. Эмитит
`<TextEdit>false</TextEdit>` после AutoMarkIncomplete (значение
true — дефолт платформы, не эмитируется). Закрывает блокер для
03-fillfields/direct-edit-form в синтетике web-test: поле с
запрещённым ручным вводом → выбор только через pick-кнопку/F4.
Snapshot-тест: tests/skills/cases/form-compile/text-edit-flag.json
(2 поля, проверяет наличие TextEdit только на втором). 30/30
form-compile зелёные обоих runtime'ов.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Новый 13-misc.test.mjs (3 шага, 11s) — покрытие openFile() для
внешних обработок с автоматической обработкой security confirmation.
- setup: автономный билд EPF (идемпотентный) через epf-init →
form-add → form-compile (с текстовой декорацией) → epf-build.
child_process.spawnSync для вызова PowerShell скриптов.
- openFile: проверки state.form, activeTab='Тест открытия',
state.texts[] содержит декорацию с ожидаемым value,
opened.attempt>=1, security confirm modal не пробивается.
- cleanup: closeForm + soft-проверка activeTab (между тестами в
desktop могут оставаться формы от других тестов — не настаиваем
на formCount=0).
Артефакты в test-tmp/13-openfile/ (.gitignore). Полный регресс
17/17 зелёный (8m 8s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Новый 08-hierarchy.test.mjs (7 шагов, 24s) — покрывает группы и
tree-grid режима «Дерево» на форме списка Номенклатуры через UI
переключение viewMode. Без расширения синтетики.
- setup: явное переключение в «Иерархический список» через Ещё →
Режим просмотра (viewMode сохраняется между сессиями и НЕ
сбрасывается «Установить стандартные настройки»).
- read-groups (P1): readTable возвращает 2 группы (_kind=group).
- group-expand (P1): clickElement({expand:true}) развёртывает группу,
внутри 15 элементов.
- switch-tree: «Ещё → Режим просмотра → Дерево» → viewMode='tree'.
- read-tree (P2): readTable.rows[]._tree (collapsed|expanded) — проверка
только наличия поля (состояние сохраняется между сессиями).
- tree-expand (P1): defensive свёртка через {expand:false} если узел
expanded, затем {expand:true} → kind='gridTreeNode' toggled=true,
видны 15 элементов под Товарами.
- cleanup: восстановить иерархический список.
Замечание: clickElement({expand:true}) — только развернуть (no-op для
expanded), {expand:false} — только свернуть, {toggle:true} —
безусловно переключить.
05-table/direct-edit-form, edit-dblclick остаются deferred — нужен
документ с иерархической ТЧ. Полный регресс 16/16 зелёный (7m 53s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11-report/drill-down: dblclick по ячейке Номенклатуры сформированного
DCS-отчёта открывает форму элемента (DCS auto-drill). После Сформировать
ищется первая строка с заполненной номенклатурой, проверяется что после
clickElement({row,column},{dblclick:true}) form изменился и есть кнопка
«Записать».
02-crud/more-menu усилен под P2 submenu-read: добавлены явные проверки
clicked.kind='submenu', наличия типовых пунктов «Создать», «Изменить»,
«Расширенный поиск» (length>=5).
Покрыто 2 P2-кейса coverage matrix (11-report/drill-down,
02-crud/submenu-read). Полный регресс 14/14 зелёный (7m 1.6s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10-validation.test.mjs (3 шага): Сообщить() → state.errors.messages,
ВызватьИсключение → onecError.errors.modal с автоматическим закрытием
fetchErrorStack.
14-errors-stack.test.mjs (3 шага): Path 1 OpenReport автоматически фетчит
стек для серверных исключений (entries[] содержит кадр ОбщиеФункции);
оставленная error modal через raw page.click закрывается closeForm;
платформенный диалог «О программе» виден в state.platformDialogs и
закрывается closeForm.
Покрыто 4 P2-кейса coverage matrix: 10-validation/messages,
10-validation/exception-modal, 14-errors/path1, 14-errors/dismiss-platform
+ бонус dismiss-modal. Открытие обработки ТестовыеОшибки через
navigateLink('Обработка.ТестовыеОшибки') — стандартные команды у
DataProcessor отключены.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
meta-compile v1.12 (ps1 + py): Parse-AttributeShorthand принимает поле
choiceHistoryOnInput в object-форме аттрибута, Emit-Attribute эмитит его
вместо хардкода Auto. Покрывает атрибуты Catalog/Document/TabularSection
(Emit-Attribute, единственная точка эмиссии в работе). Другие контексты
(register dimensions, resources, etc.) пока эмитят Auto — расширим
при необходимости.
build-webtest-config: реквизит Документ.ПриходнаяНакладная.Контрагент
получил choiceHistoryOnInput='DontUse'. Это убирает 1С-историю выбора
для поля и фиксит pre-existing flake 04-selectvalue/direct-form:
после 03 значение «ООО Север» оставалось в истории и selectValue
выбирал его через dropdown вместо ожидаемой формы выбора.
Live: полный регресс 12/12 впервые зелёный (5m 28s).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run.mjs:
1. resetState проверял `if (!state.form) break`. form === 0 (фоновая
форма 1С, которую detectForm может вернуть) рассматривался как
"форм нет" → cleanup прерывался, форма оставалась → следующий тест
получал грязное состояние. Замена на `state.form == null` корректно
различает null (desktop) и 0 (реальная фоновая форма).
2. Error screenshot в catch-блоке cmdTest снимался ПОСЛЕ resetState,
который уже закрывал все формы → скрин показывал пустой рабочий
стол вместо места падения. Перенёс снимок в начало catch (до
teardown/afterEach/resetState).
Эффекты:
- 15-multi-context-handover теперь стабильно проходит в полном прогоне
(раньше падал когда предыдущий тест оставлял form=0).
- 04-selectvalue/direct-form остался pre-existing flake (история
выбора 1С после 03 — отдельная задача в синтетике).
- Скриншоты падения теперь показывают реальный UI на момент исключения.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
run.mjs v1.10: cmdTest больше не передаёт noRecord:true в buildContext.
Тестам доступен полный API browser.mjs (showCaption, hideCaption,
startRecording, stopRecording, addNarration).
Изначальный стуб с noRecord:true прятал showCaption/hideCaption тестов
вместе с recording-функциями. Это блокировало визуальные оверлеи в
мульти-контекстных тестах: a.showCaption() тихо превращался в no-op,
баннер никогда не отображался даже под --record.
Smart wait внутри showCaption и так гейтится на наличие recorder
(`if (recorder && ...)`), поэтому без --record тесты остаются быстрыми
(никаких 2-секундных пауз на каждый вызов).
startRecording/stopRecording/addNarration теперь тоже доступны тестам.
При попытке вызвать startRecording в момент активной runner-записи
browser.startRecording бросает "Already recording" — loud failure
лучше silent no-op.
Регресс: 15-multi-context-handover один проходит за 19.9s. Полный
прогон 10/12 (04 и 15 флапают независимо в последовательности).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
browser.mjs v1.12 + run.mjs v1.9: createContext принимает isolation параметр.
По умолчанию 'tab' — все контексты живут в одном launchPersistentContext, каждый
слот получает свою Page (вкладку). Преимущества: 1С extension грузится
надёжно (через --load-extension в persistent profile), один процесс Chromium,
дешёвая память. Cookies делятся между вкладками, но скоупятся по URL-path —
для модели «разные пользователи через разные vrd-публикации» это естественно
и достаточно.
isolation: 'window' (opt-in) — старый путь chromium.launch() + newContext():
полная изоляция cookies, отдельный BrowserContext (и окно) на каждый слот,
но extension может не подняться. Использовать когда нужна изоляция auth
внутри одного URL.
Смешивать режимы в одном прогоне нельзя — createContext бросает явную
ошибку (первый createContext устанавливает activeMode, остальные обязаны
совпадать).
Конфиг tests/web-test/webtest.config.mjs: добавлен комментарий с описанием
обоих режимов. По умолчанию tab — синтетика и наши smoke-тесты идут им.
Live: 11/12 в полном прогоне (default tab) + 3/3 sanity-check в window mode
(01-navigation + 14 + 15). Видеозапись из T4.5 работает в обоих режимах.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
browser.mjs v1.11: recorder стал глобальным (не per-slot) — один ffmpeg,
один mp4 на тест с любым числом переключений контекста.
Frame state (lastFrameBuf/lastFrameTime/handler) переехал в поля recorder.
Добавлен recorder._attachPage(targetPage) — стопает старый CDP screencast,
заводит новый на нужной странице, route'ит фреймы в тот же ffmpeg pipe.
setActiveContext: при активной записи делает _flushFrames (замораживает
хвост уходящего окна), затем _attachPage(page) после _activateSlot. Видео
получается непрерывным с плавным сюжетом — пока активен a, видно a; пока
активен b, видно b.
_saveActiveSlot/_activateSlot больше не трогают recorder/lastCaptions/
lastRecordingDuration — recorder следует за активной страницей через
_attachPage, не через slot mirror.
disconnect: убрал leftover из T4.1, который пытался итерировать slot.recorder.
Live: 15-multi-context-handover с --record → 17.84s mp4, 446 кадров @ 25fps,
извлечённые кадры показывают переключение между окнами a (1920x1042) и
b (982x546). Полный регресс 11/12 (04-selectvalue — pre-existing flake).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
browser.mjs v1.10: createContext/setActiveContext/listContexts/getActiveContext/
hasContext. Несколько изолированных BrowserContext в одном Chromium-процессе через
chromium.launch() + newContext(). Module-level page/sessionPrefix/seanceId/recorder
зеркалят активный слот (атомарный своп через _saveActiveSlot/_activateSlot).
connect() оставлен для exec/run/start без изменений (launchPersistentContext).
run.mjs v1.8: ensureContext(name) + ленивое создание. Single-routing через
export const context = 'name'. Multi через export const contexts = ['a','b'] +
buildScopedContext(name) строит ctx.a/ctx.b — каждое действие префиксится
setActiveContext. Reset state после теста по всем активным контекстам.
Конфиг tests/web-test/webtest.config.mjs: два контекста a/b на одну webtest
публикацию (изолированные cookies через newContext).
Smoke-тесты:
- 14-multi-context-routing.test.mjs — single routing в b (2.6s)
- 15-multi-context-handover.test.mjs — ctx.a создаёт Контрагента, ctx.b в
независимой сессии видит запись через filterList, ctx.a cleanup (14.5s, 4/4)
Live: 11/12 в полном прогоне. 04-selectvalue/direct-form флапает —
pre-existing, воспроизводится на baseline 95e4674 (03→04 sequence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
section-error / command-error / switchTab error: проверка throw для
несуществующих имён.
navigateLink: link-type (Catalog.Контрагенты) + e1cib URL (с soft-skip
для платформ без поддержки e1cib через Shift+F11).
Live на webtest: 10/10 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
exact: filterList exact:true строго 1 совпадение.
hidden-field: filterList по неотображённому реквизиту через FieldSelector
DLB (КодКПП в синтетике нет — soft-skip).
date: filterList по колонке Дата поступления (синтетика выводит её в форму
списка Номенклатуры).
reference: filterList по ссылочной колонке Контрагент (форма списка ПН).
unfilter-all: unfilterList() полностью восстанавливает список.
unfilter-specific отложен — требует списка с видимой filter-панелью,
synthetic списки фильтруют без создания badge.
cancel-search/clear-input семантически дубликаты unfilter-all через
публичный API.
show-all-form требует quickChoice=true каталога с количеством > порога
(в синтетике нет).
Live на webtest: все 7 шагов passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
modal: F4 на ref-поле открывает модальную форму выбора Контрагентов,
state.modal=true, formCount=2.
tabs: форма элемента Номенклатуры с двумя табами (Основное/Дополнительно)
возвращает state.tabs[].
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
02-crud: confirm-save-no (rollback при save:false), confirm-pending
(closeForm() без решения возвращает confirmation), more-menu (clickElement
'Ещё' возвращает submenu).
03-fillfields: clear (Shift+F4 через пустое значение), reference-non-quickchoice
(fillFields на quickChoice=false поле — method=dropdown через DLB; чистый
form-path требует hasPick && !hasSelect, такого поля в синтетике нет).
04-selectvalue: clear (selectValue '' → Shift+F4). show-all-form отложен —
требует quickChoice=true каталога с количеством > порога dropdown
(в синтетике нет).
05-table: checkbox (fillTableRow с Boolean), clear (Shift+F4 на ref-ячейке +
восстановление для последующего delete).
Live на webtest: все шаги проходят.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fillTableRow({Количество, Цена}, {row:1}) — purpose-built проверка inEdit
multi-cell tab-loop. method='direct' для обоих полей, значения
подставляются корректно (live на webtest).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Кейс ссылался на CommonForm.* и DataProcessor.Поиск.Form.ФормаПоиска,
которых не было в workDir — verify-snapshots падал на db-load-xml,
а cf-validate Check 9 (валидация HP form refs) ловил это только под
--with-validation, поэтому в обычной регрессии проблема была не видна.
Поскольку CommonForm пока не умеет создавать ни meta-compile, ни form-add
(он привязывается к существующему объекту-владельцу), заменил ссылки
на формы Catalog/DataProcessor — DSL-покрытие сохранено целиком:
template, string-ref, height, visibility, русский синтаксис типа,
короткая и полная форма roles. Добавлен preRun из 10 шагов
(meta-compile + form-add + role-compile) для синтеза всех ссылок.
Поддержка CommonForm в meta-compile/form-add — отдельная задача.
verify-snapshots: 1/1 ✓ (db-load-xml, db-update проходят).
Полная регрессия: 349/349 ✓.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- QuickChoice<true→false> для Catalog (07b2ec3 «дефолты QuickChoice по реальным
конфигам» — часть снапшотов уже обновили в dc0382c, эти были пропущены)
- автоген <Title>/<AutoTitle>/<TitleLocation>/<SavedData> в формах
(76800fc «автоген Title из имени» и серия фич form-compile)
- column-group/ — новый снапшот для кейса из 4f9d9ae (был не закоммичен)
Все 349 unit-тестов и 6 integration-тестов зелёные. verify-snapshots
(платформенная загрузка) — 201/202; отдельный pre-existing fail
cf-edit/set-home-page (Check 9 не валидирует HP form refs) разбираем
отдельно, в дифф этой регенерации не входит.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex on Windows launches powershell.exe as a login-shell that loads
the user profile despite -NoProfile in our SKILL.md. With Restricted
ExecutionPolicy this spams "выполнение сценариев отключено". Add
-ExecutionPolicy Bypass for codex; keep canonical -NoProfile -File for
all other platforms.
Round-trip safe: cmd_install always copies fresh from .claude/skills/,
so switching codex→cursor strips the EP flag. cmd_switch_runtime
re-emits PS commands via normalize_ps_invocation each pass, so
in-place py↔ps in .codex/skills/ keeps the flag.
Also fix a pre-existing bug in cmd_switch_runtime: file-existence
check used repo_root() instead of project_dir, so in-place runtime
switch in a foreign project always tripped skip_runtime=True and
became a no-op. The bug was masked when project_dir == repo_root
(source-repo workflow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex runner on Windows launches PowerShell as login-shell and decodes
stdout/stderr without UTF-8, garbling Cyrillic output. The other 51 ps1
scripts already set `[Console]::OutputEncoding = UTF8`; bring these 7
in line and add `InputEncoding = UTF8` for symmetry.
Touched: epf-init, erf-init, form-add, form-remove, help-add,
template-add, template-remove. Versions bumped in both ps1 and py
headers to keep the pair in sync.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace overreaching "полный цикл разработки... до записи видеоинструкций" pitch with grounded one-liner matching the GitHub repo description (XML/CLI abstractions + eyes & hands for web-client testing).
- Drop non-standard per-plugin `interface.shortDescription` from .agents/plugins/marketplace.json — MarketplaceInterface only describes `displayName`, openai/plugins keeps per-plugin entries minimal (name/source/policy/category).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BOM before `---` makes Codex's strict frontmatter parser reject every
SKILL.md ("Skipped loading 66 skill(s)"). Without BOM the skills load
and execute correctly; the only remaining issue is mojibake display of
Cyrillic in SKILL.md previews — that's a Codex rendering bug, not ours.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex on Windows opens SKILL.md without a declared encoding and
defaults to CP1252, mangling Cyrillic. Adding the BOM lets the loader
auto-detect UTF-8.
Applied only on codex/codex-py builds to leave other ports untouched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex schema only accepts `ON_INSTALL` or `ON_USE`; the field is
optional, so omit it for an unauthenticated plugin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex plugin browser truncates the description, so the runtime tag
needs to appear at the start. Apply the same prefix to Claude
marketplace.json and plugin.json for consistency.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add 1c-skills-py to .claude-plugin/marketplace.json (→ port-claude-code-py)
- New .agents/plugins/marketplace.json — Codex marketplace with PS + Py plugins
- Templates .github/templates/{codex,claude}-plugin.json.tmpl rendered by CI
- build-ports.yml: generate plugin manifests on port-codex/port-codex-py/port-claude-code-py; Codex version YYYY.M.D+sha7 auto-bumped per push
- README: install instructions for Codex + Py variant for Claude
- .gitignore: narrow .agents/ → .agents/skills/ so the marketplace is tracked
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tumbler-представление RadioButtonField не парсится fillFields, но варианты
видны в state.buttons[] и кликаются через clickElement. Уточнили шаг radio:
- RadioButtons (КатегорияЦены) → fillFields с method=radio
- Tumbler (СпособУчёта) → проверка наличия в buttons[] + clickElement('ФИФО')
Семантика Tumbler через fillFields остаётся как баг web-test/browser.mjs
(см. upload/web-test-bugs.md пункт 5), но рабочий путь интеракции есть.
10/10 smoke зелёные после рестарта Apache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
03-fillfields:
- reference-dropdown: переведён с Контрагент на Организация
(после смены quickChoice Контрагенты идут через форму выбора)
- новый шаг radio: КатегорияЦены через method=radio (RadioButtons)
04-selectvalue:
- dropdown: переведён на Организация (quickChoice=true)
- новый шаг direct-form: Контрагент (quickChoice=false), method=form
Закрывает selectValue#3 dropdown (P0), selectValue#6 direct-form (P1),
fillFields#3 radio (P1) из coverage matrix.
Tumbler-представление радио (СпособУчёта) пока не покрыто — getFormState
не возвращает Tumbler в fields[]. Зафиксировано в upload/web-test-bugs.md
пункт 5.
10/10 smoke зелёные на webtest базе.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширение синтетики под новые возможности meta-compile/form-compile,
закрывает три ветки coverage matrix:
- Catalog.Организации (quickChoice: true) → selectValue#3 dropdown (P0)
- Catalog.Контрагенты (дефолт quickChoice: false) → selectValue#6 direct-form (P1)
- form-compile radio с видами RadioButtons (КатегорияЦены) и Tumbler
(СпособУчёта) → fillFields#3 radio (P1)
В шапку ПриходнаяНакладная добавлен реквизит Организация (dropdown ветка),
Контрагент остаётся на форме выбора. Фикстура ЗаполнитьОрганизации создаёт
2 организации (Альфа, Бета); первая подставляется в документы.
Платформенная верификация: build-webtest-db (45 шагов, 30.3s) зелёная,
db-create + db-load-xml + db-update проходят. Функциональный прогон
runner.mjs integration/build-webtest — 42 шага зелёные.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Раннер v1.7.
T5 --record: startRecording перед каждым тестом, stopRecording
после (и в passed, и в failed ветке). Файл
{reportDir}/{testIdx}-{slug}.mp4. testResult.video содержит путь.
В Allure — attachment типа video/mp4. config.record читается
тоже. Использует существующую инфраструктуру browser.mjs.
T6 export const params: материализация в N тестов на этапе
discovery. Имя через {key}-шаблон в mod.name (например
'demo {type}'); если шаблона нет — суффикс [index]. Тест-функция
получает param как второй аргумент: default(ctx, param).
В отчёте каждый набор — отдельная test entry с собственным uuid
в Allure / testcase в JUnit.
Live-проверка:
- params: 2 теста с именами demo A / demo B из шаблона.
- record: mp4 91KB на 6-секундном тесте, путь в JSON и
Allure attachment video/mp4.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Раннер v1.6. Реализованы оба формата отчётов из spec §9.
allure: {reportDir}/{uuid}-result.json на каждый тест. uuid через
randomUUID, labels из tags, steps рекурсивно с attachments из
step.screenshot, statusDetails для упавших шагов и тестов.
Пропускает skipped (нет start/stop).
junit: один XML в --report=path.xml. Валидация: --format=junit
требует --report=. xmlEscape для name/message/trace. <failure>
для упавших, <skipped/> для пропущенных, <system-out> со ссылкой
на screenshot.
Валидация формата (json|allure|junit) на старте cmdTest.
testResult теперь хранит start/stop в мс — нужно для Allure
и полезно в JSON-отчёте.
Live-проверка: 01-navigation в Allure (5 шагов с attachments,
все ссылки на существующие PNG); JUnit с passed и forced-fail
(спецсимволы корректно экранированы).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Раннер v1.5. Парсит --screenshot и --report-dir, мерж с config.screenshot.
- every-step: после успешного step() пишет {reportDir}/{testIdx}-{stepIdx}-{slug}.png,
путь в step.screenshot.
- off: ни пошаговых, ни error-shot.
- on-failure (default): error-shot уехал из .claude/skills/web-test/
в {reportDir}/error-{testIdx}-{slug}.png.
reportDir фоллбэчит: --report-dir → dirname(--report) → testDir.
Известная нестыковка: error-shot из buildContext/executeScript остаётся в
.claude/skills/web-test/error-shot.png — затронем при T2 (Allure).
Live-проверка: 01-navigation с every-step (5 PNG), off (пусто),
default on-failure на стуб-failing тесте (error-shot в reportDir).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Синтетика: добавлен template-add ОсновнаяСхемаКомпоновкиДанных к отчёту
(без него skd-compile писал Template.xml в незарегистрированный путь),
переписан DSL skd-compile — fields внутри dataSets, типы полей, totalFields,
явный settingsVariants со structure и быстрым отбором по Номенклатуре
(@off @user @quickAccess).
Тест 11-report покрывает: регистрацию команды в подсистеме, открытие формы
отчёта с дефолтной кнопкой Сформировать, видимость и структуру быстрого
DCS-фильтра, формирование отчёта, применение фильтра через selectValue
(auto-enable чекбокса + значение), пересчёт с фильтром, снятие фильтра
через fillFields toggle off с восстановлением исходных данных.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Покрывает форму списка (form, formCount, openForms, tables, buttons)
и форму элемента (fields с label и value, проверка по конкретному
полю Наименование).
Покрывает:
- filterList('Север') — поиск по всем колонкам списка Контрагенты
- filterList('Север', { field: 'Наименование' }) — фильтр по
конкретной колонке через расширенный поиск
- unfilterList — восстановление исходного набора
Третий запланированный кейс (text-field filter) семантически совпадает
с advanced-column когда колонка строкового типа — оставлен на регресс P1.
Покрывает clickElement по имени страницы как механизм переключения
вкладок формы. Используем форму элемента Номенклатура: page1
показывает шапку (Артикул, ВидНоменклатуры, ...), page2 — Дополнительно
(ЕдиницаИзмерения, Комментарий). Verify: набор state.fields различен
после переключения и совпадает после возврата.
Раньше verify-list брал первый попавшийся проведённый документ Север —
если в базе уже лежал проведённый Север из прошлого прогона, тест
проходил даже если текущий не сохранился. Теперь среди кандидатов
открываем каждый и сверяем Комментарий с уникальным docId текущего
прогона; ассерт срабатывает только при совпадении.
Раньше использовалось отсутствие поля Контрагент после Провести и закрыть
как косвенный признак закрытия — это работало, но было привязано к
конкретному реквизиту накладной. Заменил на сравнение state.form до и
после: номер активной формы меняется (11 → 5), это прямой и общий
признак, что мы переключились с формы документа на другую.
Создание, заполнение шапки и табличной части, Провести и закрыть,
проверка появления документа в списке с Проведён=Да.
Проверка закрытия формы документа: в синтетике web-test форма списка и
форма документа делят один слот (formCount=1 в обоих состояниях),
поэтому используем признак отсутствия поля Контрагент в текущем
state.fields после Провести и закрыть — если поле есть, мы остались
на форме документа.
Покрывает работу с табличной частью Товары документа Приходная накладная:
- fillTableRow с add:true добавляет строки последовательно
- fillTableRow с row:N редактирует существующую строку (Tab-навигация)
- deleteTableRow удаляет строку по индексу
Закрытие формы без сохранения (save:false) — соответствует новой
семантике после фикса form-compile (SavedData).
01-navigation: первое открытое окно 1С имеет form=0 (number), и
assert.ok(state.form, ...) валился на falsy при первом запуске сессии.
Сменил на state.form != null.
04-selectvalue: явный save:false при закрытии модифицированной формы
накладной — после фикса SavedData=true главного реквизита платформа
требует решения по confirmation dialog.
После фикса form-compile (kind=check для Boolean + SavedData=true для
главного реквизита) Активен передаётся как настоящий boolean (toggle),
getFormState возвращает value:true/false. Закрытие модифицированных форм
теперь требует явного save:false — иначе платформа показывает
confirmation dialog «Записать?».
После фикса form-compile (a59be4b SavedData=true для главного реквизита)
canonical confirm-save-yes flow работает без ручного патча Form.xml —
предупреждение в шаге неактуально.
После фикса form-compile (Дефект 2: kind=check → CheckBoxField) булевый
реквизит Активен в форме элемента и форме списка Номенклатуры теперь
описывается как check — рендерится настоящим чекбоксом.
Один P0 кейс из coverage matrix:
- dropdown: selectValue('Контрагент', 'ООО Север') → method='dropdown'
на форме новой ПриходнойНакладной (CatalogRef + малый список)
API возвращает form state с .selected = {field, search, method}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 шага, 5 типов полей зелёные на синтетике webtest:
- text (paste): Артикул на форме Номенклатура
- dropdown (Да/Нет): Активен — Boolean рендерится как Да/Нет селектор
- dropdown (EnumRef): ВидНоменклатуры
- date (paste): ДатаПоступления
- reference (dropdown CatalogRef): Контрагент в новой ПриходнаяНакладная
NB: 1C рендерит Boolean-атрибут не как чекбокс, а как dropdown «Да/Нет»
(actions: ["select"]) — fillFields правильно определяет это.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Гипотеза о баге fillField paste была ошибочной — реальная причина в form-compile
который не эмитит <SavedData>true</SavedData> для MainAttribute главной формы.
Платформа без SavedData не трекает modified-state, confirmation dialog не
появляется.
Платформенная верификация на патченной Form.xml: closeForm({save:true})
после fillField корректно ловит confirmation, жмёт «Да», изменения
сохраняются. См. T11 в upload/web-test-runner-tasks.md.
ВНИМАНИЕ: тест зависит от ручного патча Form.xml. После прогона
build-webtest-db.mjs тест упадёт до фикса form-compile (T11).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 шага зелёные на синтетике webtest:
- read: список Контрагентов отдаёт колонки/строки/total
- open-item: dblclick открывает форму элемента
- close-clean: Escape без изменений закрывает форму без диалога
- save-via-button: fillField + «Записать и закрыть» → значение сохраняется
confirm-save-yes (P0 из coverage matrix) отложен — fillField через paste не
выставляет 1C "modified" флаг, confirmation dialog не появляется. Зафиксировано
в upload/web-test-runner-tasks.md как T11.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Заменяет одноразовый platform-webtest-config.test.mjs на скрипт сборки в
постоянные пути из .v8-project.json (tests/skills/.cache/webtest-config
+ C:\edt\IB\webtest). Переиспользует steps из build-webtest-config.test.mjs.
Generic platform-config.test.mjs уже покрывает regression «платформа принимает
сборку» — отдельный синтетический тест дублировал.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Файл теперь генерируется самим cf-init с ERP-дефолтом (см. предыдущий
коммит на dev), отдельный writeFile в build-webtest-config больше не нужен.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Page элементы в DSL получали name (через ключ 'page'), но не получали
title, поэтому вкладки рендерились пустыми квадратиками. Также Pages
без явного pagesRepresentation отображались в режиме None (без табов).
- Добавил title к каждой Page (Основное, Дополнительно)
- pagesRepresentation: 'TabsOnTop' на Pages
После: getFormState().tabs возвращает [{name:'Основное'},{name:'Дополнительно'}]
вместо пустого массива.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Два бага, найденные при попытке запустить синтетическую ИБ через
web-publish + web-test:
1. ОбщиеФункции без ServerCall=true — ManagedApplicationModule (клиент)
не мог звать процедуры серверного модуля напрямую. ПриНачалеРаботыСистемы
падал с ошибкой компиляции в runtime, страница не догружалась. Добавил
serverCall: true в DSL meta-compile.
2. Без Ext/ClientApplicationInterface.xml панель разделов рендерилась
icon-only (без подписей), web-test navigateSection не находил секции.
Добавил writeFile-шаг с раскладкой панелей как в acc/erp:
- top: панель разделов (8e10648b...) + панель информации (cbab57f2...)
- left: панель функций текущего раздела (b553047f...)
Проверено end-to-end: после пересборки runner-ом + web-publish + start
работают navigateSection, openCommand, readTable. Фикстуры (4 контрагента,
25 номенклатуры в группах, 3 документа) автоматически заполняются при
первом старте через ManagedApplicationModule → ОбщиеФункции.ЗаполнитьФикстурыЕслиНужно.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Покрытие matrix #9 — данные для smoke-тестов:
- Константа ДанныеЗаполнены (Boolean) — флаг идемпотентности
- ОбщиеФункции.ЗаполнитьФикстурыЕслиНужно() — транзакционно создаёт:
* 4 контрагента (ООО Север/Юг/Восток, АО Запад)
* 25 номенклатуры в группах Товары (15) и Услуги (10)
* 3 приходных накладных по 3 строки
- Ext/ManagedApplicationModule.bsl с ПриНачалеРаботыСистемы — вызывает
заполнение при первом старте тонкого клиента
Платформенная верификация компилирует BSL (43 шага, 23.7s). Реальное
выполнение заполнения произойдёт при первом подключении web-test
runner-а к синтетической базе.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
meta-compile/subsystem-compile/role-compile сами добавляют записи в
Configuration.xml. cf-edit в каждом прогоне рапортовал Added: 0 — был
no-op + дублировал список объектов, который надо было синхронизировать
руками при каждом изменении.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Расширение build-webtest-config под coverage matrix (пункты 1, 2, 7
из upload/web-test-runner-tasks.md M1):
- Перечисление КатегорииЦен (для будущего radio-button теста)
- Номенклатура.ВидНоменклатуры → EnumRef.ВидыНоменклатуры
- Номенклатура.КатегорияЦены → EnumRef.КатегорииЦен
- ПриходнаяНакладная.Контрагент: String → CatalogRef.Контрагенты
- ПриходнаяНакладная.Товары.Номенклатура: String → CatalogRef.Номенклатура
- ПриходнаяНакладная.Товары.Согласовано: новый Boolean (для checkbox
в grid, fillTableRow ветка #6)
- Формы Номенклатура и Документ обновлены под новые поля
- Subsystem.Склад: добавлены Enum.* в content
- Configuration.xml регистрирует Enum.КатегорииЦен
Платформенная верификация (platform-webtest-config.test.mjs) зелёная,
25 шагов 16.7s.
Гэп: form-compile не умеет рендерить RadioButtonField — представление
КатегорияЦены остаётся обычным input. Будет отдельной задачей перед
тестами P1 fillFields/radio.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
После платформенных тестов (db-create/db-load-xml/db-update) Windows
держит файловые хэндлы 1cv8 ещё несколько сотен миллисекунд. rmSync без
ретраев падал EBUSY на Roles/.../Rights.xml, и uncaught-ошибка в finally
рушила весь node-процесс — теряли результат теста.
Теперь rmSync с maxRetries: 10, retryDelay: 200 (≈2с буфер) и try/catch
вокруг — в худшем случае warning + лишняя tmp-папка вместо краша.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-webtest-config упал после ужесточения form-compile (запрет runtime-типа
FormDataStructure для главного реквизита). Перевёл типы на конкретные
CatalogObject.X / DocumentObject.X без cfg:-префикса. Добавил
platform-webtest-config.test.mjs — переиспользует шаги сборки и в хвосте
делает db-create + db-load-xml + db-update. Зелёный, 24 шага.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix: discoverTests падал с ENOTDIR при передаче .test.mjs файла
- Добавлен 01-navigation.test.mjs — навигация по разделам, открытие
списков через navigateLink, переключение между подсистемами
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Подсистемы: singular формы в Content (Catalog вместо Catalogs)
- КурсыВалют: Independent вместо RecorderSubordinate
- Убран AccumulationRegister (требует регистратор, не нужен для UI)
- Отчёт: запрос из ТЧ документа вместо регистра
Формы загружаются без Form.xml (автогенерация платформой) —
баг form-compile (XDTO exception) требует отдельного исследования.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Один процесс браузера, несколько изолированных BrowserContext'ов.
Мгновенное переключение между пользователями, состояние каждой
сессии сохраняется. Не требует полного рефакторинга createContext().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Единый механизм для внутреннего регресса browser.mjs API и
пользовательского регресса 1С-приложений. Паттерны Playwright Test.
Содержание: CLI, формат тестов, контексты, хуки, assertions, step(),
отчёты (JSON/Allure/JUnit), синтетическая конфигурация, дорожная карта.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:37:47 +03:00
357 changed files with 29663 additions and 3200 deletions
"description":"Полный цикл разработки на платформе 1С:Предприятие 8.3 — от создания конфигураций, расширений, внешних обработок и отчётов до загрузки изменений в информационную базу, обновления, запуска, публикации на веб-сервере, тестирования через веб-клиент и записи видеоинструкций."
"description":"[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент."
},
{
"name":"1c-skills-py",
"source":{
"source":"github",
"repo":"Nikolay-Shirokov/cc-1c-skills",
"ref":"port-claude-code-py"
},
"description":"[Python] То же — для Linux/Mac или когда PowerShell недоступен."
"description":"Полный цикл разработки на платформе 1С:Предприятие 8.3 — от создания конфигураций, расширений, внешних обработок и отчётов до загрузки изменений в информационную базу, обновления, запуска, публикации на веб-сервере, тестирования через веб-клиент и записи видеоинструкций.",
"description":"[PowerShell] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
Типы: `string`, `string(N)`,`decimal`, `decimal(D)`,`decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
`decimal` без скобок = `10,2` (деньги по умолчанию), `decimal(N)` = `N,0` (целое); `,nonneg` в конце скобок → AllowedSign=Nonnegative.
Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу.
description: Декомпиляция схемы компоновки данных 1С (СКД) в JSON-черновик в формате skd-compile. Используй для scaffold нового отчёта по образцу или структурного рефакторинга. Не для точечных правок
- **Точечные правки готового отчёта** (добавить поле, фильтр, итог, переименовать) → `/skd-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает шаблон целиком, может терять непокрытые конструкции и даёт большой diff в исходниках. `/skd-edit` правит адресно, без полной реконструкции.
## Параметры
| Параметр | Описание |
|----------|----------|
| `TemplatePath` | Путь к Template.xml (обязательный) |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
JSON-черновик в формате `/skd-compile` — **не полное обратимое представление СКД**. На вход компилятору такой JSON напрямую может не пойти: в нём встречаются sentinel-узлы (маркер `__unsupported__`).
- **Готовые узлы** — большая часть СКД (поля, параметры, шаблоны, варианты со structure/filter/order/conditionalAppearance и т.п.) ложится в JSON как обычные узлы DSL.
- **Sentinel-узлы** — места, где встретилась конструкция, которую декомпилятор не умеет выразить в DSL. JSON остаётся валидным, но компилятор откажется его собирать, пока sentinel не **заменён ручной реализацией** (явный raw `template`, прописанный appearance и т.п.) **или не удалён**, если в новом отчёте конструкция не нужна. Это намеренный барьер — чтобы непокрытое не уехало в финальный отчёт незамеченным.
- **`<basename>.warnings.md`** рядом с`OutputPath` — список всех sentinel-узлов с координатами в исходнике, по нему удобно обходить места под ручную доработку.
- **Критичные конструкции** (Picture cells, ХранилищеЗначения, вложенные схемы, не-СКД root) — скрипт падает с ненулевым кодом и сообщением в stderr; такой Template как образец не годится.
## Workflow
1. `/skd-decompile <Template.xml> -OutputPath draft.json` — получить черновик.
2. Открыть `draft.warnings.md`, посмотреть, что не покрылось.
3. Поправить JSON под задачу. Sentinel-узлы — заменить на ручную реализацию (через явный raw `template`, через ручное описание appearance и т.п.) либо удалить, если конструкция в новом отчёте не нужна.
@@ -52,11 +52,16 @@ Shorthand: `"Имя [Заголовок]: тип @роль #ограничени
Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск.
Чтобы поле попало в selection не варианта, а конкретной группировки структуры — используй `-NoSelection` и затем `add-selection "Имя @group=ИмяГруппы"`.
### add-total — добавить итог
Shorthand: `"<dataPath>: <выражение>"`. Если выражение — известная аггрегатная функция без скобок (`Сумма`, `Количество`, `Минимум`, `Максимум`, `Среднее`), оно автоматически оборачивается в `Func(dataPath)`. Если функция со скобками или произвольное выражение — используется как есть.
```
"Цена: Среднее"
"Стоимость: Сумма(Кол * Цена)"
"Цена: Среднее" # → Среднее(Цена)
"Стоимость: Сумма(Кол * Цена)" # → как есть
"Проверка: Проверка" # identity: выражение = Проверка
```
### add-calculated-field — добавить вычисляемое поле
@@ -80,24 +85,46 @@ Shorthand: `"Имя [Заголовок]: тип = Выражение #noFilter
"Организация: CatalogRef.Организации"
```
Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `<title>`.
Shorthand: `"Имя [Заголовок]: тип = значение [availableValue=список] [@флаги]"`. `[Заголовок]` опциональный — добавляет `<title>`.
`@autoDates` генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра — для БСП-отчётов, чтобы получить пару полей «Начало/Конец» в панели быстрых настроек.
Флаги:
- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра.
- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе).
- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода).
`availableValue=` задаёт начальный список допустимых значений. Формат списка: `v1[: p1], v2[: p2], ...` — элементы через `,`, представление после `:`. Если в значении или представлении встречается `,` или `:` — оборачивай в одинарные кавычки `'...'`:
`[Заголовок]` опциональный — устанавливает или заменяет `<title>`. Можно вызывать без других kv-пар, чтобы только обновить title.
`availableValue=`добавляет один элемент списка допустимых значений (можно несколько через `;;`). Тип значения определяется автоматически (DesignTimeValue для ссылок).
`availableValue=`**заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`.
`value=` заменяет значение параметра (тип значения подбирается автоматически по объявленному типу параметра).
Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны.
### rename-parameter — переименовать параметр
@@ -231,13 +258,17 @@ Value — имена ресурсов (как в полях/вычисляемы
### patch-query — точечная замена в тексте запроса
Shorthand: `"старое => новое"`. Заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
```
"СубконтоДт1) В => СубконтоКт1) В"
"ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ"
"КАК ВТ_СтароеИмя => КАК ВТ_НовоеИмя @once"
```
`@once` — упасть с ошибкой, если в запросе не **ровно одно** вхождение. Защищает от случайных замен в комментариях и однотипных идентификаторах.
Многострочные подстроки поддерживаются.
### set-outputParameter — установить параметр вывода
Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
Shorthand: `"Поле1 > Поле2 > details"`. `>` — вложенный уровень группировки, `,` — несколько полей в одном уровне, `details` — детальные записи. **Заменяет всю структуру полностью** (включая Selection/order/filter/conditionalAppearance каждой группы). Для точечной модификации полей группировки с сохранением настроек — используй `modify-structure`. Не поддерживает пакетный режим.
```
"Организация > Номенклатура > details"
"Валюта, НаименованиеБанка, ИНН"
"details"
"СчетМеждународногоУчета @name=ДанныеОтчета"
```
`@name=Имя` — присваивает имя группировке (`<dcsset:name>`). Используется для привязки шаблонов через `groupName`.
### modify-structure — изменить поля группировки существующей группы
Тот же shorthand что и `set-structure`. Находит группу по `@name=`, заменяет только `<groupItems>` (поля группировки). Selection/order/filter/conditionalAppearance/outputParameters группы сохраняются. Без `@name=` — ошибка.
```
"Валюта @name=ДанныеОтчета"
"Валюта, НаименованиеБанка @name=ДанныеОтчета"
"details @name=ДанныеОтчета"
```
### modify-field — изменить существующее поле
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
report_error(f"<value xsi:type=\"dcscor:DesignTimeValue\">{text}</value> — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '{text}'")
@@ -529,3 +529,7 @@ On error (auto-screenshot taken):
- **Cyrillic in bash** — use `cat <<'SCRIPT' | node $RUN exec -` to avoid escaping issues
- **Non-breaking spaces** — 1C uses `\u00a0` instead of regular spaces. All matching is normalized internally
- **Section panel display** — `navigateSection()` works with any panel position (side, top) but requires "Picture and text" or "Text" display mode. Icon-only mode is not supported — API cannot read section names from icons alone
## Regression suites
When the user asks to cover a 1C solution with automated regression — multi-file test suites with assertions, hooks, tags, retries, Allure/JUnit reports, multi-user process tests — switch to the `test` mode. See [regress.md](regress.md) for authoring discipline, recon flow (metadata + live walkthrough via `exec`), per-application folder layout, ready-to-paste templates, and failure triage. Default to ad-hoc `run`/`exec` for single-script automation — `test` is the specialised mode for project-wide coverage.
Use this when the user asks to cover a 1C solution with automated regression tests, build out a test suite, or run an existing suite and analyse failures. For ad-hoc single-script automation, stay with the `run`/`exec` modes from SKILL.md instead.
The runner is the same `run.mjs`. The mode is `test`:
```bash
node $RUN test [url] <dir|file> [flags]
```
Tests live next to the project they cover (not inside the skill). Convention: `tests/` at the project root, with `_hooks.mjs` and `webtest.config.mjs` at the suite root. Tests are ES modules with `*.test.mjs` suffix.
## When to choose `test` over `exec`
| Goal | Mode |
|------|------|
| Explore a form, prototype a single step, debug one selector | `exec` (interactive session) |
| Reproduce a bug as a failing test before fixing it | `test` |
| Cover a feature so future changes are checked automatically | `test` |
| Run the project's regression on a new build | `test` |
| Generate a screencast walkthrough | `exec` with `startRecording` |
Don't write a `.test.mjs` for a one-shot user request. Don't drive a regression suite through chained `exec` calls.
## Before writing tests — recon
Two layers, in order.
**1. Static recon — metadata.** Never invent identifiers. For every metadata object the user mentions, run the matching info skill first: `/meta-info` (attributes/tabular sections), `/form-info` (form layout), `/skd-info` (DCS), `/mxl-info` (templates), `/role-info` (rights), `/subsystem-info` (composition / command interface). If the user names objects you can't find — stop and ask.
**2. Live recon — interactive walkthrough.** For any non-trivial scenario, walk the path live in `exec` mode before transcribing it. Metadata tells you what exists; the live walkthrough tells you what actually happens. Capture from `getFormState()`: exact button names (`'Провести и закрыть'`, not `'Сохранить'`), table section names for multi-grid forms, required fields, places where a real async wait is needed. Then transcribe the working sequence into `*.test.mjs`, wrapping logical chunks in `step('...', async () => { ... })`.
The mechanics of `exec` / `getFormState` / `fillFields` / `clickElement` are in [SKILL.md](SKILL.md) — read it before recon if you haven't already.
When live recon is overkill: trivial reads (`navigateSection` + `readTable` + assert non-empty), or scenarios you've already proven once in this session. When it's essential: confirmation dialogs, posting/cancellation flows, reports with custom filters, multi-grid forms, user-customised forms.
## Suite layout
**Each application gets its own subfolder under `tests/`.** A single repo may host several independent suites side by side — they must not share `_hooks.mjs` or `webtest.config.mjs`, because each suite restores a different DB, publishes to a different URL, and ships its own test data.
```
tests/
<app-name>/ # application regression — one per solution
_hooks.mjs
webtest.config.mjs
_allure/ # optional static Allure config
01-login/
02-counterparties/
...
<another-app>/ # second solution, fully isolated
```
Inside the application subfolder, organize by **feature**, not by metadata kind. Numeric prefixes on both folder and file enforce run order — discovery walks recursively and sorts files by full relative path; entries starting with `_` or `.` are skipped (so `_hooks.mjs`, `_allure/` won't be picked up as tests).
```
tests/<app-name>/
01-login/
01-open-base.test.mjs
02-section-navigation.test.mjs
02-counterparties/
01-create.test.mjs
02-edit-phone.test.mjs
03-goods-receipt/
01-fill.test.mjs
02-post.test.mjs
05-approval-process/
01-end-to-end.test.mjs # multi-user
```
Per-folder `_hooks.mjs` / `webtest.config.mjs` inside the application subfolder are NOT supported — only the application-root copies are loaded.
## Test file anatomy
```js
export const name = 'Создание контрагента'; // required
export const tags = ['catalog', 'create']; // optional, used for filtering + Allure
await step('Убедиться, что элемент появился в списке', async () => {
const t = await readTable();
assert.tableHasRow(t, r => r['Наименование']?.startsWith('Тест '));
});
}
```
**Step names — in Russian, descriptive.** Step labels surface in the console output, in JSON/JUnit, and as Allure step nodes. Russian-speaking QA reads them. Use a full action phrase (`'Создать нового контрагента'`), not a tag (`'create'`) and not a transliteration. Same applies to `export const name` and `displayName` in `webtest.config.mjs`.
## `ctx` contract
The runner injects every `browser.mjs` export into `ctx` (all 1C action functions auto-detect platform errors — see SKILL.md), plus the test utilities below.
### Test utilities
```js
step(name, fn) // async wrapper. Records start/stop. Nested calls supported.
// On throw: marks the step failed, re-throws.
// On screenshot='every-step': captures after fn().
log(...args) // adds a line to ctx.testInfo's output (goes into JSON / Allure
// attachment). Use instead of console.log inside tests.
assert.* // see "Assertions" below
```
### `ctx.testInfo` (always set, read-only)
```js
{
name, // 'Навигация по разделам' (with params substituted)
file, // '01-navigation.test.mjs' (basename)
filePath, // relative path inside testDir
tags, // ['nav', 'smoke']
timeout, // ms
attempt, // 1..maxAttempts (1-based)
maxAttempts, // 1 + retry
param, // { ... } | undefined (only when export const params is set)
contexts: { // mirrors config.contexts; includes custom fields like displayName
clerk: { url, isolation, displayName, ... },
manager: { ... },
},
primaryContext, // 'clerk' — name of the context active at test entry
// (= t.context for single, t.contexts[0] for multi)
- **Single-context (default or `export const context = 'manager'`):** all API on `ctx` top-level — `ctx.clickElement(...)`, `ctx.getFormState()`, etc.
- **Multi-context (`export const contexts = ['clerk', 'manager']`):** each name is its own scoped namespace — `ctx.clerk.clickElement(...)`, `ctx.manager.fillFields(...)`. `step`, `assert`, `log`, `testInfo` stay top-level. Scoped methods auto-switch the active page before each call.
## Assertions
All on `ctx.assert`. Throw `AssertionError` with `.message`, `.actual`, `.expected`. No dependencies.
Beyond these, just use plain JS (`throw new Error(...)`) — there's no custom matcher extension API. The 1C-specific helpers are the ones worth preferring over hand-rolled equivalents because their error messages name the actual fields/rows present, which speeds up triage.
// Severity → tags mapping for Allure. Each tag at most one bucket.
severity: {
critical: ['smoke', 'crud'],
minor: ['recording'],
},
defaultSeverity: 'normal',
};
```
CLI flags override config. Use latin context IDs + Russian `displayName` for ergonomics — `ctx.testInfo.contexts.clerk.displayName` is friendlier than mixed-case Cyrillic keys.
## _hooks.mjs
Two layers. Infra hooks run without a browser; testlevel hooks receive `ctx`.
```js
import { execSync } from 'child_process';
// Infra — runs once around the whole suite.
export async function prepare({ hookArgs, log, config }) {
// hookArgs: everything after `--` on the CLI, as a string[]. Parse yourself.
const force = hookArgs.includes('--rebuild-stand');
export async function beforeCloseContext(ctx, name, spec) { }
```
Built-in state reset (`dismissPendingErrors` + close all forms) runs after `afterEach` automatically. Don't reimplement it in `afterEach`.
Pass hook args after `--`:
```bash
node $RUN test tests/<app-name>/ --bail -- --rebuild-stand --data=demo
└─runner─┘ └────── hookArgs ─────────┘
```
**Where to put data setup:**
- DB restore, publication, EPF build → `prepare()`. Make it idempotent (hash-locks).
- Test-specific seed data → per-test `setup`.
- Shared session-wide warmup → `beforeAll`.
## Ready-to-paste patterns
A minimal CRUD shape is in *Test file anatomy* above — use it as the rhythm for catalog/document tests, swapping in the right section/command/fields. The patterns below cover what's specific to the regression engine, not the browser API (those live in SKILL.md).
### DCS report
```js
await openCommand('Остатки товаров');
// Reset user settings — 1C persists them between sessions.
await manager.closeContext('clerk'); // free a 1C license for the next test
});
}
```
Close contexts you no longer need (`manager.closeContext('clerk')`) before the next multi-user test starts — frees a 1C web-client license and stops the previous role from holding state.
### Failing-test repro
```js
export const name = 'Bug #123: накладная без контрагента не должна проводиться';
Each `params` entry becomes its own test in the report. `{key}` placeholders in `name` get substituted; without placeholders, a `[index]` suffix is added. `ctx.testInfo.param` carries the current row.
## Running
```bash
node $RUN test tests/<app-name>/ # full app suite
node $RUN test tests/<app-name>/03-goods-receipt/ # one feature folder
node $RUN test tests/<app-name>/02-counterparties/01-create.test.mjs # one file
node $RUN test tests/<app-name>/ --tags=smoke # by tag (intersection)
node $RUN test tests/<app-name>/ --grep='накладн' # by name regex
node $RUN test tests/<app-name>/ --bail --retry=1 # stop on first fail, allow 1 retry
node $RUN test tests/<app-name>/ --report=allure-results --format=allure --report-dir=allure-results
node $RUN test tests/<app-name>/ -- --rebuild-stand # after `--` → hookArgs
```
Default report is JSON when `--report=…` is given. Allure needs `--format=allure` + a directory. JUnit similarly with `--format=junit`.
### Allure static config — `_allure/`
The runner copies `<testDir>/_allure/` into the report directory before generating Allure output. Drop in `categories.json` (regex-based failure classification — useful for 1C-specific buckets: license pool exhaustion, platform exceptions, runner timeouts, assertion failures), `environment.properties` (optional, often emitted dynamically by `prepare()`), `executor.json` (CI metadata, skip locally). The underscore prefix keeps the directory out of test discovery.
## Severity guidance
When the user doesn't dictate, default to:
| Test kind | Severity |
|-----------|----------|
| Login + section navigation, basic CRUD on covered entities | `critical` (also tag `smoke`) |
| Reserved for show-stopper protections | `blocker` (use sparingly) |
Don't promote everything to `critical` — it loses signal in the Allure dashboard.
## Anti-patterns
- **Sleeps as a substitute for assertions.**`wait(5)` after `openCommand` is fine; `wait(30)` because something flakes is a bug — wait on `getFormState` instead.
- **Retry as a substitute for understanding.** "Not found" twice means the data isn't there or the label is wrong. Don't loop.
- **Position-based row identification** (`rows[0]`) when the DB has shared seed data. Filter by a unique marker (`Date.now()` suffix) instead.
- **Hand-writing reset code in `afterEach`.** The runner already closes forms and dismisses errors after the hook.
- **Cross-test state assumptions.** Each test must start from the desktop and seed its own data. Order-of-execution coupling is a regression-suite trap.
- **`tags: ['smoke']` on a 90-second test.** Smoke means fast.
- **Skipping recon** because "I know what this catalog looks like." The project's customisation almost certainly differs from stock.
(General browser-API anti-patterns — raw DOM, `clickElement('Закрыть')` instead of `closeForm()` — live in SKILL.md.)
## After a run — failure triage
1. Scan the JSON or Allure summary for `failed`.
2. For each failure, read `error.message` + `error.step` + screenshot.
3. If `error.onecError.stack` is present — it's a 1C exception, look at the platform trace.
4. Classify:
- **Test bug** — selector wrong, expectation wrong, race with no anchor → fix the test.
- **Application bug** — actual misbehaviour reproduced → report to the user with the failing step name and the platform stack.
- **Stand flake** — Apache timeout, login form not loading, license shortage → fix the hook idempotency or session-cleanup logic, not the test.
5. After fixes, re-run only the affected files before the full suite.
Report back to the user with the classification, not raw failure dumps.
## Reference
- Browser API: [SKILL.md](SKILL.md)
- Video and narration: [recording.md](recording.md)
thrownewError(`createContext: invalid isolation "${isolation}", expected 'tab' or 'window'`);
}
if(activeMode&&activeMode!==isolation){
thrownewError(`createContext: cannot mix isolation modes — first context used "${activeMode}", "${name}" requested "${isolation}". Use the same mode for all contexts in one run.`);
}
// First context: launch browser. Subsequent: reuse existing.
"description": "[Python] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент. Linux/Mac или когда PowerShell недоступен.",
"description": "[{{RUNTIME_LABEL}}] Навыки для разработки на 1С:Предприятие 8.3 — абстракции над XML-форматами и CLI конфигуратора, плюс глаза и руки для тестирования через веб-клиент.",
`availableValues` — список допустимых значений поля с (опциональной multilang) подписью. Типы значений автоопределяются (`bool`/`decimal`/`dateTime`/`string`); можно указать `valueType` явно. Аналогичное поле существует на `parameters` — см. раздел 6.
### Парсинг shorthand
1. Разделить по пробелам; найти `@`-роли и `#`-ограничения
1. Извлечь `@`-роли (regex `@(\w+)`), `#`-ограничения (`#(\w+)`), KV-пары роли (`(\w+)=(\S+)`)
2. Остаток до первого `:` — `dataPath` (и `field` по умолчанию)
> **Ссылочные типы** (`CatalogRef.XXX`, `DocumentRef.XXX` и др.) эмитируются с inline namespace declaration: `<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.XXX</v8:Type>`. Использование префикса `cfg:` вместо `d5p1:` с объявлением namespace приводит к ошибке XDTO. Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией (не пустую).
> **TypeSet (тип-набор)** — голое имя без точки (`CatalogRef`, `DocumentRef`, `EnumRef`, `ChartOfAccountsRef`, `ChartOfCharacteristicTypesRef`, `ChartOfCalculationTypesRef`, `BusinessProcessRef`, `TaskRef`, `ExchangePlanRef`, `InformationRegisterRef`, `AnyRef`) — указывает на **все** ссылки этого класса конфигурации (а не на конкретный объект). Эмитится как `<v8:TypeSet>` вместо `<v8:Type>`. Используется в параметрах типа «исключаемые документы» и подобных.
### Синонимы типов
Все имена типов регистронезависимые. Поддерживаются русские и альтернативные имена:
Whitelist'а нет — любой `<dcscom:KEY>` принимается; перечисленные — типичные. `@period` — sugar для `periodNumber=1` + `periodType=Main` (можно переопределить явно).
**XML-выход**: `<dcscom:KEY>true</dcscom:KEY>` для флагов; `<dcscom:KEY>VALUE</dcscom:KEY>` для KV.
> Устаревший ключ `balanceGroup` в object-форме принимается как alias для `balanceGroupName` (имя элемента в реальном XML — `balanceGroupName`).
### Ограничения
| DSL shorthand | Объектная форма | XML useRestriction |
@@ -371,7 +398,7 @@ XML-маппинг — по `<group>` на каждый элемент:
| `name` | Имя параметра |
| `title` | Заголовок (умолч. = name) |
| `type` | Тип (см. таблицу типов) |
| `value` | Значение по умолчанию |
| `value` | Значение по умолчанию (скаляр; для `valueListAllowed=true` — массив значений по умолчанию: `[ "ПланСчетов.Хозрасчетный.X", "...Y", "...Z" ]`) |
| `expression` | Выражение для вычисления |
| `availableAsField` | `false` — скрыть из полей |
| `valueListAllowed` | `true` — разрешить список значений |
@@ -379,7 +406,9 @@ XML-маппинг — по `<group>` на каждый элемент:
| `useRestriction` | `true` — скрыть от пользователя |
| `use` | `"Always"`, `"Auto"` |
| `denyIncompleteValues` | `true` — запретить произвольные значения (только из availableValues) |
| `availableValues` | Массив `[{value, presentation}]` — допустимые значения с представлениями |
| `availableValues` | Массив `[{value, presentation}]` — допустимые значения с представлениями. Типы (bool/int/decimal) сохраняются нативно в JSON |
`additionalProperties` — словарь служебных свойств варианта (`<dcsset:additionalProperties>` в XML), значения сохраняются как `xs:string`. Платформа использует его для типа «имя варианта», URL временного хранилища и т.п. — для bit-perfect round-trip сохраняется как есть, обычно модели заполнять не нужно.
@@ -554,14 +606,19 @@ XML-маппинг — по `<group>` на каждый элемент:
|------|----------|
| `field` | Имя поля |
| `op` | Оператор (см. таблицу) |
| `value` | Правая часть (опц.) |
| `valueType` | xsi:type для значения (опц.) |
| `value` | Правая часть (опц.). См. формы ниже |
| `valueType` | xsi:type для значения (опц.). `"dcscor:Field"` = field-to-field comparison (значение — имя другого поля). Для массива `value: [...]` применяется ко всем элементам — нужен когда auto-detect ошибается (например `Перечисление.X.Y` должно остаться `xs:string`, а не `dcscor:DesignTimeValue`) |
| `userSettingPresentation` | Отображаемое имя настройки (LocalStringType) |
**Формы `value`:**
- Скаляр (`"X"`, `5`, `true`, `"2024-01-01T00:00:00"`) — single `<right>` (стандартный случай). Тип определяется автоматически: bool / число / дата / строка.
- Массив `[a, b, c]` — несколько `<right>` подряд (для `in`/`notIn` с конкретными значениями).
- Пустой массив `[]` — `<right xsi:type="v8:ValueListType">` placeholder (типичный паттерн для `in` с пользовательскими настройками — значения заполнит пользователь через UI).
Операторы:
| DSL | XML comparisonType |
@@ -581,18 +638,24 @@ XML-маппинг — по `<group>` на каждый элемент:
| `filled` | `Filled` |
| `notFilled` | `NotFilled` |
Группа условий: `{ "group": "And"|"Or"|"Not", "items": [...] }` → `FilterItemGroup`с`groupType`.
Группа условий: `{ "group": "And"|"Or"|"Not", "items": [...] }` → `FilterItemGroup`с`groupType`. Группа также принимает item-level поля `presentation`, `viewMode`, `userSettingID`, `userSettingPresentation` — для регистрации группы как пункта пользовательских настроек.
- `"Auto"` → `OrderItemAuto` (только на уровне группировок; на верхнем уровне settings игнорируется)
- Объект `{ field, direction?, viewMode?, use? }` — нужен, когда требуется задать `viewMode`, или отключить сортировку через `use: false` (см. [viewMode](#viewmode-режим-доступности))
### conditionalAppearance
@@ -604,14 +667,16 @@ XML-маппинг — по `<group>` на каждый элемент:
- Multilang dict `{ru, en}` для любого ключа → `v8:LocalStringType`
- Прочие → `xs:string`
Значение можно обернуть в `{ "value": ..., "use": false }` — отключённый параметр (платформа эмитит `<dcscor:use>false</dcscor:use>`). Такая же форма доступна в `appearance` items (см. раздел conditionalAppearance).
Top-level Line хранится **плоско** (`@type`/`width`/`gap`/`style` + `use?`/`items?` на одном уровне). Nested items используют универсальный wrapper `{ value, use? }` — у`value` тип любой (Line/Font/color/text). Значения `style`: `None`, `Solid`, `Double`, `LargeDashed`, `SmallDashed`, `Dotted` и т.п. (значения `v8ui:SpreadsheetDocumentCellLineType`).
### dataParameters
#### Автогенерация
@@ -696,11 +830,26 @@ XML-маппинг — по `<group>` на каждый элемент:
|------|----------|
| `parameter` | Имя параметра |
| `value` | Значение (объект `{ "variant": "LastMonth" }` для StandardPeriod, или скаляр) |
| `valueType` | Полный xsi:type если кастомный (например `dcsset:DataCompositionGroupUseVariant`). Для пустого значения с `use: false` — `"xs:string"` эмитит `<value xsi:type="xs:string"/>` (placeholder отключённого параметра типа DateTime, бит-перфектный аналог `xsi:nil`) |
| `{variant: "BeginningOf*"}` | `v8:StandardBeginningDate` | без даты — variant'ы начинаются с `BeginningOf` |
| `"2024-01-15T10:00:00"` (string) | `xs:dateTime` | raw datetime без обёртки |
Platform-pattern: `startDate`/`endDate`/`date` эмитятся ТОЛЬКО для `variant=Custom`. Для `ThisMonth`/`LastYear`/`BeginningOfThisDay`/... — только `<v8:variant>`.
### structure
#### String shorthand (рекомендуется для типичных случаев)
@@ -735,16 +884,30 @@ XML-маппинг — по `<group>` на каждый элемент:
|------|----------|
| `type` | `"group"` |
| `name` | Имя группировки (опц.) |
| `groupBy` | Массив полей. Пусто/опущено = детальные записи |
| `groupBy` | Массив полей. Каждый элемент — строка (имя поля) или объект `{ field, groupType?, periodAdditionType?, periodAdditionBegin?, periodAdditionEnd? }`. Пусто/опущено = детальные записи. Object-форма нужна когда `groupType ≠ "Items"`, `periodAdditionType ≠ "None"` или задана `periodAdditionBegin/End` (см. ниже) |
| `outputParameters` | Параметры вывода (как в settings) |
| `conditionalAppearance` | Условное оформление группы (как в settings) |
| `use` | `false` = ветка структуры отключена (на самой группе) |
| `viewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` — режим доступности группы в пользовательских настройках |
| `itemsViewMode` | `"Normal"`, `"QuickAccess"`, `"Inaccessible"` — режим доступности подэлементов группы |
| `userSettingID` | `"auto"` → генерировать GUID. Регистрирует группу как пункт пользовательских настроек |
| `userSettingPresentation` | Имя в пользовательских настройках (string или multilang dict) |
| `children` | Вложенные элементы структуры |
Пустой `groupBy` (или `[]`) = детальные записи (без `groupItems` в XML).
**`periodAdditionBegin` / `periodAdditionEnd`** на field-объекте — даты добавочного периода (`<dcsset:periodAdditionBegin>`/`<dcsset:periodAdditionEnd>`). Compile auto-определяет xsi:type значения: строка вида `2025-01-01T00:00:00` → `xs:dateTime`, иначе (путь к параметру, например `ПараметрыДанных.ДатаНачала`) → `dcscor:Field`.
"userSettingPresentation": { "ru": "Колонка с периодом" }
}
]
}
```
Каждая `column`/`row` принимает те же поля что и `group`: `name`, `groupBy`/`groupFields`, `filter`, `order`, `selection`, `outputParameters`, `conditionalAppearance`, `children` (вложенные `StructureItemGroup`), плюс user-settings — `viewMode`, `userSettingID`, `userSettingPresentation`, `itemsViewMode` (регистрация column/row как пункта «Изменить вариант»).
На самой `table` (отдельно от column/row) также допустимы `selection`, `conditionalAppearance`, `outputParameters`, плюс user-settings: `viewMode`, `userSettingID`, `userSettingPresentation`, `itemsViewMode`, `columnsViewMode`, `rowsViewMode`, `use` (`false` = таблица отключена).
- `columnsViewMode` / `rowsViewMode` — режим доступности секции колонок / строк в пользовательских настройках (значения: `Normal` / `QuickAccess` / `Inaccessible`).
> **Внутренний паттерн**: `<dcsset:item xsi:type="dcsset:StructureItemGroup">` внутри `<dcsset:row>`/`<dcsset:column>`/`<dcsset:points>`/`<dcsset:series>` платформа всегда сериализует в **короткой форме**`<dcsset:item>` без `xsi:type`. Compile эмитит этот вариант автоматически для `children` table axis.
`points` и `series` принимают те же поля что table column/row (включая `name` и user-settings).
На самой chart-item: `viewMode`, `userSettingID`, `userSettingPresentation`, `itemsViewMode`, `pointsViewMode`, `seriesViewMode`, `use: false` (диаграмма отключена). `pointsViewMode`/`seriesViewMode` — аналоги `columnsViewMode`/`rowsViewMode`у таблицы.
**Multi-series / multi-points** — `points` и `series` могут быть массивом объектов, тогда генерируется несколько `<dcsset:point>` или `<dcsset:series>` подряд (каждый со своими `groupBy`, `filter`, user-settings). Используется например для разделения данных диаграммы на несколько серий по разным фильтрам:
Дополнительные поля, которые пользователь может задать в режиме «Изменить вариант» через UI. Хранятся в settings варианта. Два подтипа определяются по содержимому объекта:
**Expression-форма** — поле вычисляется выражением (опционально с разделением для детальных строк и для итогов):
"expression": "Выбор Когда Группа = ... Тогда ОтработаноДней Иначе 0 Конец",
"presentation": "Выбор Когда Группа = ... Тогда [Отработано дней] Иначе 0 Конец"
},
"total": {
"expression": "Сумма(Выбор Когда Группа = ... Тогда ОтработаноДней Иначе 0 Конец)",
"presentation": "Сумма(Выбор Когда Группа = ... Тогда [Отработано дней] Иначе 0 Конец)"
}
}
]
```
| Поле | Описание |
|------|----------|
| `dataPath` | Путь поля в формате `ПользовательскиеПоля.ПолеN` |
| `title` | Заголовок (строка или multilang dict) |
| `detail.expression` | Выражение для детальных записей |
| `detail.presentation` | Тот же expression с подстановкой `[Имя поля]` (для UI) |
| `total.expression` | Выражение для итоговой строки |
| `total.presentation` | Same для UI |
> **Пустые значения**: XML всегда содержит все четыре элемента (`detailExpression`, `detailExpressionPresentation`, `totalExpression`, `totalExpressionPresentation`) — даже если без значения (`<dcsset:totalExpression/>`). decompile сохраняет ключ с пустой строкой, compile эмитит self-closing тег для пустых строк. Это нужно для bit-perfect round-trip.
**Case-форма** — поле принимает разные значения в зависимости от условий:
Shorthand-флаги `@inaccessible`, `@quickAccess` доступны для `filter` и `dataParameters` строковых форм.
**2. Block-level** — на самом блоке (внутри `settings`). Управляет доступностью всей группы как пункта пользовательских настроек:
```json
"settings": {
"selectionViewMode": "Inaccessible",
"filterViewMode": "Inaccessible",
"orderViewMode": "Inaccessible",
"conditionalAppearanceViewMode": "Inaccessible",
"itemsViewMode": "Inaccessible",
"selectionUserSettingID": "auto",
"filterUserSettingID": "auto",
"orderUserSettingID": "auto",
"conditionalAppearanceUserSettingID": "auto",
"selection": [...],
"filter": [...]
}
```
`itemsViewMode` на settings — общий режим для всех подэлементов варианта (`<dcsset:itemsViewMode>` в XML). `XxxUserSettingID` парят с`XxxViewMode` — platform пишет их в block-level пользовательских настроек. Пустые блоки (без items) тоже эмитятся, если есть block-level meta — например `<dcsset:conditionalAppearance><dcsset:viewMode>Normal</dcsset:viewMode></dcsset:conditionalAppearance>`.
Также `orderViewMode`/`orderUserSettingID` поддержаны на StructureItemGroup для случаев когда block-level meta лежит на nested `<dcsset:order>`.
**3. Structure item** — на элементе структуры (`group`):
**4. Table axis / chart axis** — на самой `column`/`row`/`points`/`series`. Через те же поля `viewMode`, `userSettingID`, `userSettingPresentation` (см. раздел Таблица).
#### Стратегия сохранения
Платформа эмитит `viewMode` непоследовательно: в одних местах `<viewMode>Normal</viewMode>` присутствует явно (когда элемент — пункт пользовательских настроек), в других — нет. Для bit-perfect round-trip:
- `skd-decompile` сохраняет `viewMode` в JSON **точно как было в XML**, включая явный `"Normal"` если он физически присутствовал.
- `skd-compile` эмитит `<viewMode>` только если значение задано в JSON (без `implicit Normal`-подстановки).
При компиляции JSON, написанного с нуля моделью, `viewMode` опускается → платформа применит default `Normal` при загрузке схемы.
---
## 10. Макеты и привязки (templates, groupTemplates)
@@ -886,13 +1215,45 @@ XML-маппинг — по `<group>` на каждый элемент:
#### Расшифровка (drilldown) в параметрах шаблона
Ключ `drilldown` в параметре шаблона автоматически генерирует:
1. `DetailsAreaTemplateParameter`с именем `Расшифровка_<значение>`, `fieldExpression` по полю `ИмяРесурса`, `mainAction=DrillDown`
2. Привязку `Расшифровка` в appearance ячеек, ссылающихся на этот параметр через `{Имя}`
Ключ `drilldown` в параметре шаблона — три формы по типу значения:
**Форма A (без drilldown)** — обычный `ExpressionAreaTemplateParameter`:
```json
{ "name": "Дата", "expression": "Документ.Дата" }
```
**Форма B (строка, shortcut)** — `ExpressionAreaTemplateParameter` + автоматический `DetailsAreaTemplateParameter`с именем `Расшифровка_<value>`, `fieldExpression` по полю `ИмяРесурса` (`expression="<value>"`), `mainAction=DrillDown`. Ячейки `{name}` получают appearance `Расшифровка → Расшифровка_<value>` автоматически:
**Форма C (объект)** — самостоятельный `DetailsAreaTemplateParameter`с именем `name`, без `ExpressionAreaTemplateParameter`. Используется когда расшифровка ссылается на data-параметр (а не на ИмяРесурса) и/или нужен другой `mainAction` (например `OpenValue`):
```json
{ "name": "МаршрутныйЛист",
"drilldown": { "field": "МаршрутныйЛист",
"expression": "МаршрутныйЛист",
"action": "OpenValue" } }
```
`action` по умолчанию `DrillDown`.
**Override на уровне ячейки** — object-форма `{ value, drilldown }`. Используется когда несколько ячеек должны указывать на один и тот же параметр-расшифровку (объявленный формой C):
Значение `drilldown` в ячейке — это полное имя параметра-расшифровки (как объявлено в `parameters`). Для shortcut form B override не нужен — appearance подставляется автоматически.
### fieldTemplates
Привязка именованного area-template к полю — `<fieldTemplate><field>X</field><template>Y</template></fieldTemplate>`. Когда платформа выводит значение поля `X`, используется макет `Y`:
| `/skd-decompile` | `<TemplatePath> [-OutputPath <out.json>]` | Преобразование Template.xml в JSON-черновик в формате `/skd-compile` — для нового отчёта по образцу или структурной переработки существующего. Из соображений предосторожности исключён из автоматического подбора моделью — вызывается только явной командой |
1. Claude формирует JSON-определение СКД (shorthand-поля, параметры, итоги, варианты)
1. Claude формирует JSON-определение СКД (shorthand-поля, параметры, итоги, варианты) — либо с нуля по описанию, либо `/skd-decompile` готовит черновик по существующему Template.xml
2. `/skd-compile` генерирует Template.xml с корректными namespace, типами, группировками
3. `/skd-edit` вносит точечные изменения: добавление полей, фильтров, наборов данных, вариантов, условного оформления и т.д.
4. `/skd-validate` проверяет корректность XML
5. `/skd-info` выводит компактную сводку для визуальной проверки
### Когда `/skd-decompile`, а когда `/skd-edit`
- **`/skd-edit`** — точечные правки готового отчёта (добавить поле, фильтр, итог, переименовать параметр). Меняет XML адресно, без полной реконструкции, не задевает непокрытые конструкции.
- **`/skd-decompile` → правка JSON → `/skd-compile`** — сценарии, где правки структурны: новый отчёт по образцу существующего, переработка варианта, перерисовка макета, перебор набора полей. Цикл переписывает Template.xml целиком.
**Полнота не гарантируется.** Известные декомпилятору непокрытые конструкции явно отмечаются маркерами в JSON и собираются в файл предупреждений — компилятор откажется собирать такой черновик, пока маркеры не разрешены вручную или не удалены. Но возможны и **тихие потери** — мелкое оформление, редкие настройки, незнакомые декомпилятору расширения. Это даёт валидный XML без части функциональности, и Конфигуратор такой результат не отбракует. Именно поэтому навык не предназначен для точечных правок (для них есть `/skd-edit`) и исключён из автоматического подбора моделью — вызывается только явной командой пользователя. Решение использовать пересобранный Template.xml — на стороне пользователя, и сверка с оригиналом перед коммитом остаётся его ответственностью.
## JSON DSL — компактный формат
СКД описываются в JSON с двумя уровнями детализации для каждой секции:
Навык `/web-test` умеет не только разово выполнить сценарий в браузере, но и сопровождать прикладное решение полноценным набором автотестов: каждый тест — отдельный файл, с шагами, проверками, тегами, отчётом и видеозаписью падений. После каждой правки конфигурации модель прогоняет весь набор и показывает, что ожидаемо ведёт себя как раньше, а что сломалось.
Это про прикладное решение в целом, не про разовую проверку одной формы. Для разовых сценариев («открой накладную, проверь сумму») по-прежнему удобнее интерактивный режим из [web-test-guide.md](web-test-guide.md).
## Предусловия
- База опубликована через Apache (`/web-publish`).
- Установлен Node.js 18+, зависимости подняты: `cd .claude/skills/web-test/scripts && npm install`.
- ffmpeg — нужен только если хотите видеозапись прогона как доказательство падения. Без него падения фиксируются скриншотами. Установка описана в [web-test-recording-guide.md](web-test-recording-guide.md).
## Как это устроено
Набор тестов живёт в каталоге `tests/` вашего проекта. Каждое прикладное решение — отдельная подпапка. Внутри подпапки:
- `_hooks.mjs` — подготовка стенда (восстановление базы, публикация) и общая очистка после прогона. Необязателен.
- `webtest.config.mjs` — адрес базы и набор пользователей (например, кладовщик и менеджер для процессов согласования). Необязателен — если в проекте один пользователь и один URL, можно обойтись без него.
- Сами тесты — файлы `*.test.mjs`, сгруппированные по функциональным папкам.
```
tests/
моя-конфигурация/
_hooks.mjs
webtest.config.mjs
01-вход/
01-открытие-базы.test.mjs
02-контрагенты/
01-создание.test.mjs
02-правка-телефона.test.mjs
03-поступление-товаров/
01-оформление.test.mjs
02-проведение.test.mjs
04-отчёт-остатки/
01-формирование.test.mjs
05-согласование/
01-полный-цикл.test.mjs
```
Порядок выполнения — по алфавиту, поэтому удобно префиксовать папки и файлы номерами. Это даёт предсказуемый сценарий: сначала вход, потом справочники, потом документы, потом отчёты, в конце — процессы с несколькими пользователями.
## Быстрый старт
Самый короткий путь от нуля до зелёного теста — попросить модель пройти ваш сценарий руками и зафиксировать его как тест:
```
> Покрой регрессом справочник Контрагенты в моей конфигурации.
1. Соберёт информацию о справочнике через `/meta-info` и `/form-info` — посмотрит реквизиты и форму элемента, чтобы знать правильные имена полей.
2. Подключится к опубликованной базе в интерактивном режиме и **руками пройдёт** каждый сценарий — создание, правка, удаление. Это нужно, чтобы зафиксировать настоящие имена кнопок, увидеть, какие диалоги показывает 1С, понять, требуется ли подтверждение сохранения.
3. Зафиксирует пройденный сценарий как файл `tests/<ваша-конфигурация>/02-контрагенты/01-создание.test.mjs`.
4. Запустит его и покажет результат.
При следующих прогонах ничего этого делать не нужно — модель просто запустит готовый набор.
## Сценарии работы с моделью
### Покрытие регрессом доработанного объекта
```
> Я добавил в справочник Номенклатура реквизит "Цена" и "Активен".
> Покрой это регрессом — создание, редактирование, фильтрация по активности
```
Модель:
- посмотрит структуру справочника и формы (через `/meta-info`, `/form-info`);
- интерактивно проверит, как ведут себя новые поля в браузере;
- сгенерирует 2-3 тестовых файла под папкой `02-номенклатура/`;
- прогонит — покажет, что зелёное, что красное.
### Тест процесса с несколькими пользователями
```
> Сделай тест для процесса согласования приходных накладных.
> Кладовщик создаёт накладную, менеджер утверждает,
> кладовщик видит обновлённый статус
```
Модель настроит в `webtest.config.mjs` двух пользователей (с разными URL базы — например, `app-clerk` и `app-manager`), напишет тест, который оркестрирует переключение между ними, и положит его в `05-согласование/`.
Учтите ограничение по лицензиям 1С: каждый одновременно открытый пользователь — это занятая клиентская лицензия. Если в наборе много многопользовательских тестов, а на стенде лицензий впритык, прогоны начнут спотыкаться на «свободных лицензий не осталось». Модель освобождает сессии между тестами автоматически (закрывает контексты после процессного теста), но если стенд ограничен — закладывайте это в планирование набора: один-два многопользовательских сценария вместо десяти.
### Воспроизведение ошибки тестом
```
> При проведении накладной без заполненного контрагента у нас не появляется
> ошибка валидации, документ просто проводится с пустым контрагентом — это баг.
> Зафиксируй это падающим тестом
```
Модель воспроизведёт сценарий, напишет тест с проверкой «должна быть ошибка», получит красный — потом, когда вы поправите конфигурацию и попросите перепрогнать, тест станет зелёным. Это документирует ожидаемое поведение в виде кода.
### Прогон регресса после изменений
```
> Я обновил расширение, накатил в базу. Прогони регресс
```
Модель запустит весь набор, дождётся завершения и расскажет:
- сколько тестов прошло, сколько упало, сколько пропущено;
- по каждому упавшему — что именно сломалось (название шага, сообщение об ошибке, ссылка на скриншот);
- классифицирует падения: это ошибка в самом тесте (нужно поправить тест), ошибка в приложении (баг, который вы внесли изменением), или нестабильность стенда (Apache не ответил вовремя, лицензия не освободилась).
```
> Прогони только тесты по контрагентам с подробным отчётом
```
Запустит подмножество — фильтр по тегу или папке, с записью JSON-отчёта.
### Подготовка автономного стенда
Если вы хотите, чтобы регресс можно было запустить «с нуля» — даже на чистой машине без подготовленной базы, — модель настроит автоматическую подготовку стенда:
```
> Сделай, чтобы перед прогоном тестов база восстанавливалась из эталона,
> а после прогона публикация снималась
```
Это пишется один раз в файле `_hooks.mjs`: при запуске тестов запускается подготовка (через навыки `/db-create`, `/db-load-xml`, `/web-publish`), а после — очистка. Внутри предусмотрено кэширование: если ничего не менялось со прошлого прогона, повторная подготовка занимает доли секунды.
## Пример организации покрытия
Допустим, у нас условное прикладное решение «Учёт поступлений товаров» — справочники контрагентов и номенклатуры, документ приходной накладной, отчёт остатков, процесс согласования с двумя пользователями. Логично организовать набор так:
```
tests/учёт-поступлений/
_hooks.mjs # подготовка: восстановление базы + публикация
webtest.config.mjs # URL базы, контексты кладовщика и менеджера
03-расшифровка.test.mjs # переход из ячейки отчёта в исходный документ
06-согласование/
01-полный-цикл.test.mjs # многопользовательский тест
```
Принципы:
- **Папки — по бизнес-функции**, не по типу метаданных. Лучше `04-поступление-товаров/` (что делает пользователь), чем `документы/` (что лежит в дереве конфигурации).
- **Цифровые префиксы** — на папке и на файле. Гарантируют, что сначала отработают базовые проверки (вход, справочники), потом сложные (документы, отчёты, процессы). При падении базы остальное и так не пройдёт — нет смысла занимать стенд получасом.
- **Один файл — одна логически связанная история.** Не «всё про контрагентов в одном файле», а «отдельно создание, отдельно правка, отдельно удаление». Когда падает — сразу видно, какой именно сценарий сломан.
- **Негативные тесты тоже есть.** «Документ без контрагента не проводится» — такой же важный регресс, как и позитивный сценарий, особенно после правок в обработчиках проверки заполнения.
- **Процессные тесты — в конце.** Они самые хрупкие (зависят от двух сессий, лицензий, синхронизации) и самые длинные. Если упадут — у вас уже есть данные от предыдущих тестов.
## Анатомия одного теста
Пользователь, как правило, тест не пишет — генерирует модель. Но прочитать и поправить полезно уметь. Стандартный файл выглядит так:
await step('Убедиться, что элемент появился в списке', async () => {
const t = await readTable();
assert.tableHasRow(t, r => r['Наименование'] === 'ТД Тест');
});
}
```
Что здесь есть:
- **`name`** — человекочитаемое имя теста. Появится в отчёте.
- **`tags`** — теги для фильтрации. Можно прогонять не весь набор, а только нужные: `--tags=контрагенты`.
- **`timeout`** — сколько максимум тест может идти. По умолчанию 30 секунд, для длинных сценариев увеличиваем.
- **Тело теста** — функция, которая получает API браузера (см. [SKILL.md](../.claude/skills/web-test/SKILL.md)) плюс `assert` и `step`.
- **`step('имя', async () => {...})`** — обёртка шага. Имена шагов попадают в отчёт, при падении видно, какой именно шаг сломался.
- **`assert.*`** — проверки. `assert.tableHasRow`, `assert.equal`, `assert.ok` и т.д. Если проверка не выполнилась — тест считается упавшим.
Имена шагов и теста — по-русски, описательные. Они показываются и в консоли, и в отчётах.
## Запуск и отчёты
### Простой прогон
```
> Прогони регресс
```
Модель запустит весь набор, дождётся, покажет сводку:
```
✓ Открытие базы (2.1s)
✓ Создание контрагента (8.4s)
✗ Проведение приходной накладной (12.7s)
└ Заполнить табличную часть (5.2s)
Не найден столбец "Цена" в табличной части "Товары"
скриншот: tests/учёт-поступлений/error-shot.png
23 пройдено, 1 упал, 0 пропущено (3 мин 42 с)
```
### Подробный отчёт
```
> Прогони регресс и сохрани подробный отчёт
```
Модель добавит флаг записи отчёта (JSON или Allure) — потом по нему можно листать историю прогонов, видеть длительности шагов, открывать прикреплённые скриншоты.
Allure — стандартный визуальный отчёт с категориями падений, графиками, таймлайном. Чтобы посмотреть отчёт после прогона:
```bash
# Allure CLI устанавливается отдельно (npm install -g allure-commandline)
allure serve allure-results
```
### Категории падений в Allure
Без дополнительной настройки Allure складывает все упавшие тесты в один общий список «Defects». Если в прогоне упало 15 тестов, не сразу понятно, что из этого — пятнадцать разных проблем или одна и та же ошибка (например, нехватка лицензии на стенде), которая зацепила пятнадцать тестов подряд.
Чтобы Allure группировал падения по причинам, рядом с тестами кладётся каталог `_allure/`с файлом `categories.json`. Подчёркивание в имени каталога — чтобы он не воспринимался как папка с тестами; раннер копирует его содержимое в отчёт.
```
tests/моя-конфигурация/
_allure/
categories.json # классификация падений
environment.properties # необязательно: URL, версия 1С, ветка git
executor.json # необязательно: метаданные сборки CI
_hooks.mjs
01-вход/
...
```
`categories.json` — это список регулярных выражений, по которым ошибка теста относится к той или иной группе:
Когда вы попросите модель в первый раз настроить регресс, она положит шаблонный `categories.json` со стандартными классами. По мере того как вы будете находить новые типичные причины падений (например, специфичные для вашего расширения тексты ошибок), категории дополняются.
В виджете «Categories» итогового отчёта вы увидите примерно так:
```
Нехватка лицензий 1С — 12 падений
Ошибка приложения 1С — 2 падения
Несовпадение ожидания и факта — 1 падение
```
— и сразу понятно, что 12 падений — это один стенд-баг, а двумя «ошибками приложения» нужно разобраться по существу.
Помимо `categories.json` в каталог `_allure/` можно положить ещё два стандартных файла:
- **`environment.properties`** — список `ключ=значение` (URL базы, версия платформы 1С, имя ветки git, номер сборки). Покажется в отчёте в виджете «Environment». Полезно, когда регресс гоняется на нескольких стендах или после каждого билда — видно, на чём именно был получен результат. Этот файл удобно генерировать прямо в подготовке стенда (`_hooks.mjs`), а не держать статичной копией.
- **`executor.json`** — метаданные системы сборки: ссылка на Jenkins-задачу, идентификатор запуска GitHub Actions и т.д. Нужен только если регресс запускается на сервере сборки. При локальном прогоне ничего класть не надо.
### Прогон части набора
```
> Прогони только тесты по поступлениям товаров
> Прогони только базовые проверки
> Прогони только упавший вчера тест с проведением накладной
```
Модель выберет нужное подмножество — по папке, по тегу или по имени теста.
### Принудительная пересборка стенда
Если хотите, чтобы перед прогоном база восстановилась с нуля:
```
> Прогони регресс с полной пересборкой стенда
```
Это передаст в подготовку флаг типа `--rebuild-stand` — `_hooks.mjs` пересоздаст базу из эталона. Полезно после крупных правок или если подозреваете, что предыдущие прогоны загрязнили данные.
## Что делать, когда тест упал
Модель проанализирует падение и отнесёт его к одной из трёх категорий:
1. **Ошибка в самом тесте.** Например, переименовали реквизит — тест ищет старое имя поля. Решение: модель обновит тест.
2. **Ошибка в приложении.** Это и есть то, ради чего регресс существует: что-то поменялось в конфигурации, и сценарий, который раньше работал, теперь не отрабатывает. Модель опишет, что именно произошло, со скриншотом и трассировкой стека 1С, если ошибка была серверной.
3. **Нестабильность стенда.** Apache не ответил, не освободилась лицензия, база отвалилась. Это лечится не правкой теста, а починкой подготовки стенда в `_hooks.mjs` или, реже, повторным прогоном с одним повтором.
Просите модель не «исправь упавший тест», а «разберись с падением» — иначе она может молча подкрутить ожидание под текущее поведение, замаскировав настоящий баг.
## Полезные подробности
### Тестовые данные
В прикладном решении обычно нужны какие-то стартовые данные: пара контрагентов, номенклатура, заведённые организации. Их кладём не в каждый тест, а один раз в подготовку стенда (`_hooks.mjs`) — после восстановления базы загружаются эталонные данные, на которых работают все тесты.
Если конкретному тесту нужны свои данные (например, документ, который мы будем редактировать), он создаёт их сам в начале и убирает в конце.
### Имена документов и уникальность
Тесты прогоняются многократно. Если тест создаёт документ «Накладная-Тест», следующий прогон может натолкнуться на старую запись. Решение — добавлять к имени метку времени:
```js
const метка = 'Тест-' + Date.now();
await fillFields({ 'Комментарий': метка });
// ...
const t = await readTable();
assert.tableHasRow(t, r => r['Комментарий'] === метка);
```
Модель это делает автоматически, но если правите тест руками — держите в голове.
### Видео при падении
Можно включить запись видео всех тестов — тогда при падении прикладывается не только скриншот, но и MP4 со всей сессией:
```
> Прогони регресс с записью видео
```
Размер прогона при этом растёт (на 2-3 минутах теста выходит 5-10 МБ), но при отладке сложного падения видео экономит кучу времени.
### Многоязычные конфигурации
Если у вас есть конфигурация с командами и реквизитами на нескольких языках, тесты пишутся под один язык (как правило, тот, в котором ведётся работа в проде). При смене языка интерфейса в браузере тесты не пройдут — модель видит другие подписи кнопок.
## Где смотреть дальше
- API браузера, которое вызывают тесты — [SKILL.md](../.claude/skills/web-test/SKILL.md).
- Подробная инструкция для модели по написанию тестов (на английском, технический документ) — [.claude/skills/web-test/regress.md](../.claude/skills/web-test/regress.md).
- Интерактивный режим без тестов — [web-test-guide.md](web-test-guide.md).
- Запись видеоинструкций — [web-test-recording-guide.md](web-test-recording-guide.md).
Техническое описание движка регрессионных тестов: инструмент исполняет описанные кодом пользовательские сценарии в веб-клиенте прикладного решения на платформе 1С и сверяет результат с ожиданиями.
Смежные документы:
- [web-test-regression-guide.md](web-test-regression-guide.md) — пользовательский гайд с быстрым стартом.
- [web-test-guide.md](web-test-guide.md) — справочник по browser-API (`clickElement`, `getFormState`, `readTable`, …), который используется внутри тестов.
| `--record` | false | Записывать видео для каждого теста (mp4 в `--report-dir`) |
| `-- <hookArgs…>` | — | Всё после `--` пробрасывается в `_hooks.mjs` как `hookArgs` (см. §6.1) |
URL необязателен, если в каталоге тестов есть `webtest.config.mjs`. CLI URL переопределяет URL дефолтного контекста.
### Валидация CLI
- `--screenshot=<v>` принимается только `on-failure | every-step | off`; при невалидном значении движок выводит ошибку и завершается с ненулевым кодом до старта прогона.
- `--format=<v>` принимается только `json | allure | junit`; иначе — завершение с ошибкой.
- `--format=junit` требует `--report=<path>` (иначе некуда писать XML); иначе — завершение с ошибкой.
> `dismissPendingErrors` — внутренняя функция (browser.mjs), на `ctx` не публикуется. Тест её не вызывает напрямую: она срабатывает автоматически перед каждым ACTION_FN и внутри встроенного сброса.
### Тестовые утилиты
- `step(name, fn)` — обёртка шага (см. §4)
- `assert.*` — хелперы утверждений (см. §5)
- `log(...args)` — добавить строку в вывод теста. Строки накапливаются в массив, склеиваются и попадают в JSON `tests[].output`. В Allure-отчёте `output` пишется в `statusDetails.trace`**только для упавших тестов**; для успешных теряется (отдельного вложения не создаётся).
### Метаданные теста (`ctx.testInfo`)
Декларативная информация о текущем тесте. Движок выставляет `ctx.testInfo` перед каждой попыткой (до `beforeEach`); хук и тело теста могут читать. Изменять не следует — объект используется самим движком при сборке отчёта.
```js
ctx.testInfo = {
name, // 'Навигация по разделам' (с подставленными params)
file, // '01-navigation.test.mjs' (basename)
filePath, // '01-navigation.test.mjs' (relative к testDir, разделитель '/')
contexts: { // объект, всегда 1+ ключей; зеркалит config.contexts
a: { url, isolation, ...customFields },
b: { ... },
},
primaryContext, // 'a' — имя контекста, активного на входе в тест
// (= t.context для single, t.contexts[0] для multi)
}
```
Доступ к специфике контекста: `testInfo.contexts[testInfo.primaryContext].displayName`. `primaryContext` — декларация теста; не зависит от текущего значения `getActiveContext()` (которое может меняться внутри теста).
### Результат теста в afterEach (`ctx.testResult`)
Только в `afterEach`. До запуска теста — `null`. После — заполняется движком перед вызовом хука:
```js
ctx.testResult = {
status, // 'passed' | 'failed'
duration, // ms
attempts, // фактически выполнено попыток (1..maxAttempts)
steps, // массив step-результатов (структура — см. §4)
}
```
В итоговый JSON-отчёт (`tests[]`) добавляются ещё `name`, `file`, `tags`, `contexts`, `severity`, `start`, `stop`, `output`, `screenshot`, `video` (см. §9). В`afterEach` они недоступны — движок собирает финальную запись после хука.
### Мульти-контекст
При `export const contexts = ['a', 'b']`:
- `ctx.a` и `ctx.b` — отдельные scoped-объекты, каждый с полным API браузера. Перед каждым вызовом scoped-обёртка переключает активный контекст через `setActiveContext`.
- `ctx.step`, `ctx.assert`, `ctx.log`, `ctx.testInfo`, `ctx.testResult` остаются на верхнем уровне.
При single-context (`export const context = 'X'` или дефолт) API публикуется плоско на `ctx`.
---
## 4. step(name, fn) — обёртка шага
```js
await step('Имя шага', async () => {
// тело шага
});
```
Поведение:
- Записывает метку `start` перед `fn()`.
- Записывает метку `stop` после `fn()` (успех или ошибка).
- При ошибке: устанавливает `status: 'failed'`, прикрепляет сообщение, пробрасывает исключение.
- При успехе: устанавливает `status: 'passed'`.
- Если стратегия скриншотов `every-step` — делает скриншот после `fn()`.
- Вложенные шаги поддерживаются (шаг внутри шага).
- Напрямую маппится на шаги Allure.
Структура данных шага (для отчётов):
```js
{
name: 'Имя шага',
start: 1712345678000, // мс от эпохи
stop: 1712345679200,
status: 'passed' | 'failed',
error: 'сообщение' | undefined,
screenshot: 'путь' | undefined,
steps: [] // вложенные шаги
}
```
---
## 5. Утверждения (assertions)
Простые хелперы без зависимостей. Бросают `AssertionError` со свойствами `.message`, `.actual`, `.expected`.
### Общие
```js
assert.ok(value, msg?) // истинность
assert.equal(actual, expected, msg?) // ===
assert.notEqual(actual, expected, msg?) // !==
assert.deepEqual(actual, expected, msg?) // сравнение через JSON
- `hookArgs: string[]` — всё, что в командной строке передано после разделителя `--`, без интерпретации со стороны движка. Хук парсит сам (см. §6.1).
- `log: (...args) => void` — функция логирования движка (структурированный вывод с префиксом `[hooks]`). Использовать вместо `console.log`, чтобы не ломать формат отчёта.
- `config: object` — разобранный `webtest.config.mjs` (URL контекстов, режим изоляции, правила severity и т.д.).
**Тестовый уровень** (с контекстом браузера):
- `beforeAll(ctx)` — после подключения, перед первым тестом.
- `afterAll(ctx)` — после последнего теста, до отключения.
- `beforeEach(ctx)` — перед каждым тестом. На входе уже доступен `ctx.testInfo` (см. §3).
- `afterEach(ctx)` — после каждого теста. Дополнительно доступен `ctx.testResult` с результатом завершившегося теста.
**Контекстный уровень** (на каждый browser-контекст, жизненный цикл = создан → удалён):
- `afterOpenContext(ctx, name, spec)` — сразу после успешного `createContext`. `spec` — запись из `config.contexts[name]` со всеми пользовательскими полями (`displayName`, `url`, `isolation`, …). Полезно: вставка постоянного DOM-оверлея/бейджа, предварительная навигация в контексте, регистрация телеметрии.
- `beforeCloseContext(ctx, name, spec)` — перед `closeContext` (контекст ещё активен и работает). Полезно: сохранение остатков буферов, сбор метрик, последний скриншот. Срабатывает и при явном `ctx.closeContext(name)` из теста, и в финальной очистке движка перед `disconnect`.
`closeContext(name)` валиден только когда `name !== getActiveContext()` — иначе бросается исключение. В scoped API (`ctx.a.closeContext('b')`) это естественно: scoped-обёртка сначала вызывает `setActiveContext('a')`, потом закрывает `'b'` — целевой контекст всегда неактивен.
### Подавление ошибок в хуках
Ошибки в `afterEach`, `teardown`, `afterAll` и `cleanup` ловятся и логируются движком, но не прерывают прогон и не помечают тест/прогон как failed. Логика: пост-хуки очистки должны быть устойчивы к собственным сбоям, чтобы один сломанный `teardown` не приводил к падению остальных тестов по цепочке. Если в этих хуках произошла фатальная для регресса проблема — бросайте отдельный `Error` в `beforeAll`/`beforeEach`, чтобы он прервал прогон, либо проверяйте состояние в самом тесте.
### Порядок выполнения
```
prepare() // без браузера (восстановление БД, публикация)
browser.launch() // запуск процесса браузера
createContext(default) // первый контекст создан
afterOpenContext(ctx, default) // hook: контекст готов
beforeAll(ctx) // браузер готов, default-контекст создан
[lazy ensureContext(name)] // для multi-context тестов
afterOpenContext(ctx, name)
beforeEach(ctx)
test.setup(ctx) // подготовка теста
test.default(ctx) // тело теста (может вызвать ctx.closeContext)
Гарантирует, что каждый тест стартует с чистого рабочего стола, независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения). Реимплементировать это в пользовательском `afterEach` не нужно.
### Пример _hooks.mjs
```js
import { execSync } from 'child_process';
export async function prepare({ hookArgs, log, config }) {
const force = hookArgs.includes('--rebuild-stand');
// Встроенный сброс состояния выполняется ПОСЛЕ afterEach автоматически.
}
export async function afterOpenContext(ctx, name, spec) {
// Удобно для persistent DOM-overlay'я с displayName (видно в видео,
// какая вкладка к какому пользователю относится).
}
export async function beforeCloseContext(ctx, name, spec) {
// Срабатывает и при ctx.closeContext из теста, и в финальной очистке.
}
```
### 6.1. Проброс пользовательских флагов через `--`
Движок не знает о пользовательских флагах хуков. Чтобы хуки получили разовые параметры без правки `webtest.config.mjs` или окружения, используется стандартная shell-конвенция `--` (как у`npm`, `cargo`, `pytest`): всё, что идёт после `--` в CLI движка, передаётся в `prepare` / `cleanup` через поле `hookArgs: string[]` без интерпретации.
```
node run.mjs test tests/myapp/ --bail -- --rebuild-stand --reload-data
└─ runner ─┘ └────── hookArgs ────────┘
```
В этом примере движок получает `--bail`, а`hookArgs` хуков становится `['--rebuild-stand', '--reload-data']`. Парсинг этого массива — ответственность хуков.
Если разделитель `--` не указан, `hookArgs` — пустой массив. Это позволяет движку и хукам развиваться независимо: новый встроенный флаг движка никогда не пересечётся с пользовательским.
---
## 7. Файл конфигурации
`webtest.config.mjs` в корне каталога тестов. Необязателен — если отсутствует, URL должен быть передан через CLI.
```js
export default {
// Контексты: именованные URL для разных пользователей/ролей.
// Рекомендация: латинский ID контекста (`clerk`, `manager`) + кириллический
// `displayName` для UI/слайдов. Любые пользовательские поля пробрасываются как есть
// и доступны хукам через `ctx.testInfo.contexts[name]` (см. §3).
// Значения по умолчанию (переопределяются флагами CLI)
timeout: 30000,
retries: 0,
screenshot: 'on-failure', // 'every-step' | 'off'
record: false,
// Дефолтный тег-фильтр. Применяется только если CLI не передал --tags.
// Удобно для сценариев «прогон по умолчанию = smoke», при этом --tags=full
// (или --tags=) с CLI прозрачно перекрывает.
tags: ['smoke'],
// Дефолтный режим изоляции для контекстов, которые сами его не указали
// (config.contexts.<name>.isolation). См. §8.
isolation: 'tab', // 'tab' | 'window'
// Allure severity policy (опционально). Маппинг наоборот: уровень → [теги].
// Резолв см. §9 «Авто-эмиссия label-ов».
severity: {
critical: ['smoke', 'multi-context'],
minor: ['recording'],
// blocker / trivial — необязательны, можно опустить
},
defaultSeverity: 'normal',
};
```
**Упрощённая форма** (один контекст, без именованных):
```js
export default {
url: 'http://localhost/app/ru_RU',
timeout: 30000,
};
```
### Валидация файла конфигурации
`severity` валидируется при загрузке:
- ключи — только из `blocker | critical | normal | minor | trivial`;
- значение каждого ключа — массив тегов;
- тег не может одновременно состоять в двух уровнях severity (явная ошибка с указанием конфликта);
- `defaultSeverity` — из стандартного набора.
При нарушении любого правила движок выводит сообщение с указанием конфликта и завершается с ненулевым кодом до запуска тестов.
Кириллица в ID контекстов работает, но смешанный регистр снижает читаемость кода (`testInfo.contexts.кладовщик.displayName` рядом с`testInfo.contexts.clerk.displayName`). Рекомендуем разделять технический ID и человекочитаемое имя.
Флаги CLI всегда переопределяют значения из файла конфигурации.
---
## 8. Контексты
### Механизм: Playwright BrowserContext
Один процесс браузера (`chromium.launch()`), несколько изолированных контекстов. Каждый контекст — отдельная сессия (куки, авторизация, состояние страницы).
Движок НЕ группирует тесты по контексту. Порядок выполнения — алфавитный по полному относительному пути файла (плюс порядок экспорта внутри файла). Для каждого теста:
1. Через `ensureContext(name)` создаются BrowserContext-ы, упомянутые в `t.context` / `t.contexts` (если ещё не созданы).
2. `setActiveContext(primaryContext)` — активный контекст = первый объявленный (для single — `t.context || defaultContext`, для multi — `t.contexts[0]`).
3. После теста встроенный сброс пробегает по всем использованным контекстам.
Контексты живут между тестами: переключение через `setActiveContext` — дешёвое, повторный вход в 1С не требуется. Закрываются явно (`closeContext`) или финальной очисткой движка перед закрытием браузера.
Каждый именованный контекст — полноценный scoped-объект API со своим `page`. Тест оркестрирует переключение между пользователями. Состояние каждого пользователя сохраняется между переключениями:
```js
await step('Кладовщик создаёт документ', async () => {
| `createContext(name, url, { isolation, extensionPath })` | Создаёт BrowserContext и переходит по URL. |
| `setActiveContext(name)` | Переключает активный слот; при активной записи дописывает последние кадры старой страницы и переподключает screencast. |
| `closeContext(name)` | Выход из 1С + закрытие (`page` для `tab`, `BrowserContext` для `window`), удаляет из реестра. Бросает исключение, если `name === active`. |
| `listContexts()` / `hasContext(name)` / `getActiveContext()` | Только для чтения. |
### Режимы изоляции
Поле `isolation` задаётся в двух местах:
- **На уровне контекста:**`config.contexts.<name>.isolation` — приоритет 1.
- **На уровне файла конфигурации:**`config.isolation` — применяется к контекстам, у которых своего значения нет. По умолчанию `'tab'`.
| Режим | Реализация | Окна | Cookies | 1С-расширение |
| `'tab'` (default) | `launchPersistentContext` + `newPage()` per context | 1 окно, N вкладок | общие по path | загружается надёжно |
| `'window'` | `chromium.launch()` + `newContext()` per context | N окон | полная изоляция | может не загружаться |
Смешивать режимы в одном прогоне нельзя — `createContext` бросает явную ошибку. То есть `config.isolation` фактически становится режимом всего прогона, если хотя бы один контекст явно не переопределил его на тот же режим.
### Закрытие неактивных контекстов
`closeContext(name)` нельзя вызвать на активном контексте — будет исключение. В scoped API это естественно: вызывать `manager.closeContext('clerk')` (scoped-обёртка сначала переключает активный на `manager`, потом закрывает `clerk`). Если контекст лишний (роль больше не нужна в рамках теста / прогона) — закрывайте его сразу: освобождает лицензию платформы и снимает нагрузку со следующих тестов.
---
## 9. Отчёты
### JSON (нативный, по умолчанию)
```json
{
"runner": "web-test",
"url": "http://localhost/app/ru_RU",
"startedAt": "2026-04-05T10:00:00.000Z",
"finishedAt": "2026-04-05T10:05:30.000Z",
"duration": 330.0,
"summary": {
"total": 25,
"passed": 23,
"failed": 1,
"skipped": 1
},
"tests": [
{
"name": "CRUD справочника Контрагенты",
"file": "02-catalog-crud.test.mjs",
"tags": ["smoke", "crud"],
"contexts": ["clerk"],
"severity": "critical",
"status": "passed",
"start": 1712345678000,
"stop": 1712345690300,
"duration": 12.3,
"attempts": 1,
"steps": [
{
"name": "Открыть список",
"start": 1712345678000,
"stop": 1712345679200,
"status": "passed",
"steps": []
}
],
"output": "Элемент найден в списке",
"error": null,
"screenshot": null,
"video": null
},
{
"name": "Обязательное поле",
"file": "10-validation.test.mjs",
"tags": ["validation"],
"contexts": ["clerk"],
"status": "failed",
"duration": 8.1,
"attempts": 2,
"steps": [
{
"name": "Сохранить пустую форму",
"start": 1712345700000,
"stop": 1712345708100,
"status": "failed",
"error": "Ожидалось модальное окно ошибки, но форма сохранилась"
}
],
"output": "",
"error": {
"message": "Ожидалось модальное окно ошибки, но форма сохранилась",
Отдельные JSON-файлы для каждого теста в каталоге `allure-results/`:
```json
{
"uuid": "сгенерированный-uuid",
"name": "CRUD справочника",
"fullName": "02-catalog-crud.test.mjs",
"status": "passed",
"stage": "finished",
"start": 1712345678000,
"stop": 1712345690300,
"labels": [
{ "name": "tag", "value": "smoke" },
{ "name": "tag", "value": "crud" },
{ "name": "suite", "value": "root" },
{ "name": "severity", "value": "critical" }
],
"steps": [
{
"name": "Открыть список",
"status": "passed",
"start": 1712345678000,
"stop": 1712345679200,
"steps": []
}
],
"attachments": [
{
"name": "Скриншот при падении",
"source": "uuid-attachment.png",
"type": "image/png"
}
]
}
```
Скриншоты/видео копируются в `allure-results/` с уникальными именами.
#### Авто-эмиссия меток
Движок всегда заполняет следующие метки (`labels`):
- **`tag`** — по одному на каждый элемент `mod.tags[]`. Готовая фильтрация в Allure-отчёте без дополнительной разметки.
- **`suite`** — `dirname(t.filePath)`. Тесты в корне `testDir` идут под `'root'`, тесты в подкаталоге `sales/` — под `'sales'`. Это даёт левую группировку отчёта без ручной разметки.
- **`severity`** — резолв в порядке приоритета:
1. `export const severity = 'critical'` в самом тесте, **если значение валидное** (одно из `blocker | critical | normal | minor | trivial`). Если экспорт задан, но значение невалидное — пункт пропускается и идём в (3); резолв через теги (пункт 2) при этом **не выполняется** (хотел бы автор иначе — он бы не объявлял `severity`).
2. Иначе **максимальный ранг** среди тегов теста (стандартные имена `blocker | critical | normal | minor | trivial` напрямую, либо через `config.severity`-маппинг).
3. Иначе `config.defaultSeverity` или `'normal'`.
Ранги: `blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1)`. Выбор по максимуму не зависит от порядка тегов в `mod.tags`.
Движок ищет каталог `_allure/` рядом с тестами и копирует все его файлы в `reportDir` перед генерацией отчёта. Конвенция для статичной настройки Allure, для которой нет места внутри JSON-файла теста:
| Файл | Назначение |
|------|-----------|
| `categories.json` | Классификация падений по regex (группировка failed-тестов в виджете Categories — «timeout», «license-flake», «1C modal» и т.п.). |
| `environment.properties` | `key=value` строки в виджет Environment (URL, версия 1С, ветка git, номер сборки). Часто формируется динамически из `prepare()`. |
Подчёркивание в имени — параллель `_hooks.mjs` (инфраструктура, не тест). Сборщик тестов пропускает каталог `_allure/` по общему правилу (`startsWith('_')`). Если каталога нет — ничего не происходит, отчёт собирается обычным образом.
Ожидалось модальное окно ошибки, но форма сохранилась
скриншот: error-shot-10.png
○ Составной тип (skip: не реализовано)
23 passed, 1 failed, 1 skipped (2m 0.5s)
```
Для passed-тестов выводится одна строка `✓ name (duration)`. Шаги печатаются только для упавших — после строки `✗`, с отступом, плюс сообщение ошибки и путь к скриншоту. Полная картина по шагам — в JSON-отчёте (`--report=…`).
---
## 11. Скриншоты и видео
### Стратегия скриншотов
| Стратегия | Поведение |
|-----------|----------|
| `on-failure` (по умолчанию) | Скриншот при падении теста, прикрепляется к ошибке. |
| `every-step` | Скриншот в конце каждого `step()`, плюс при падении. |
| `off` | Без автоматических скриншотов. |
Скриншоты сохраняются в каталог отчёта по шаблону `{индекс-теста}-{имя-шага}.png`. В JSON-отчёте — путь относительно каталога отчёта.
### Видеозапись
При включённом `--record`:
- `startRecording()` перед каждым тестом.
- `stopRecording()` после каждого теста.
- Видео сохраняется как `{индекс-теста}-{имя-теста}.mp4`.
- Прикрепляется к отчёту (Allure: вложение видео).
Подробности по записи (overlays, captions, narration) — см. [web-test-recording-guide.md](web-test-recording-guide.md).
---
## 12. Сброс состояния
Встроенный механизм, выполняется после `afterEach` (и `teardown`) каждого теста. Псевдокод и условие выхода — в §6 «Встроенный сброс состояния».
Для мульти-контекстных тестов сброс пробегает по всем живым контекстам, использованным тестом.
Гарантирует, что каждый тест стартует с чистого рабочего стола, независимо от того, как завершился предыдущий (падение, таймаут, ошибка утверждения).
Параметры разворачиваются в отдельные тесты на этапе discovery:
- Имя теста формируется подстановкой через шаблон `{key}` в `mod.name`; если шаблона нет — суффикс `[index]`.
- Тест получает `param` вторым аргументом (`default(ctx, param)`).
- В отчётах каждый набор — отдельная запись со своим `name` и `param` в `testInfo`.
- `ctx.testInfo.param` доступен в теле теста и хуках.
---
## 14. Обнаружение тестов
`testDir` (первый позиционный аргумент после URL) — каталог, в котором живут тесты. Сборщик рекурсивно обходит дерево и собирает файлы по правилам ниже.
```
tests/myapp/
_hooks.mjs # пропускается (префикс '_')
_allure/ # пропускается (префикс '_')
webtest.config.mjs # пропускается (не *.test.mjs)
sales/
01-order-create.test.mjs
02-order-post.test.mjs
warehouse/
01-receipt.test.mjs
```
### Правила
| Аспект | Поведение |
|--------|-----------|
| Обход | Рекурсивный; файлы и каталоги, имя которых начинается на `_` или `.`, пропускаются |
| Шаблон имени | Только `*.test.mjs` |
| Порядок | Сортировка по полному относительному пути (`sales/01` идёт до `warehouse/01`) |
| `file` в отчёте | `relative(testDir, file)`с разделителем `/`, например `sales/01-order-create.test.mjs` |
| Фильтр по пути с CLI | `node run.mjs test tests/myapp/sales/` запустит только подкаталог |
| Конкретный файл | `node run.mjs test tests/myapp/sales/01-order-create.test.mjs` |
### Чего НЕТ (сознательное упрощение)
- **`_hooks.mjs` на уровне подкаталога.** Движок ищет `_hooks.mjs` только в корне `testDir`. Подкаталоги свои хуки не получают.
- **`webtest.config.mjs` на уровне подкаталога.** Тоже только в корне.
- **Многоуровневой Suite-разметки из дерева каталогов.** Allure-метка `suite` строится только по первому уровню (`dirname(filePath)`); более глубокую группировку делайте через `tags`.
- **Контекста по умолчанию на уровне подкаталога.** Каждый тест объявляет `context` / `contexts` сам; от пути контексты не наследуются.
### Конвенции
1. **Папки — для организации**, не для механики. Общая подготовка — в глобальном `_hooks.mjs.beforeAll` или в `setup` / `teardown` конкретного теста.
2. **Группировку в отчётах** делайте через `tags: ['sales']`, не через путь. Это даёт фильтрацию (`--tags=sales`) и работает в Allure/JUnit без дополнительной разметки.
3. **«Запустить только sales»** — двумя путями: `tests/myapp/sales/` (по каталогу) или `--tags=sales` (по тегу).
4. **Сортировка по полному пути** означает, что `warehouse/01-x` запустится ПОСЛЕ `sales/02-y`. Для строгого глобального порядка используйте 3-значные префиксы (`010-`/`020-`/…) либо явные теги-фазы.
---
## 15. Ошибки и трассировка
### Авто-обнаружение 1С-ошибок
Все ACTION_FNS (`clickElement`, `fillFields`, `fillField`, `selectValue`, `fillTableRow`, `deleteTableRow`, `openCommand`, `navigateSection`, `navigateLink`, `openFile`, `closeForm`, `filterList`, `unfilterList`) обёрнуты. После каждого вызова:
1. Проверяется `state.errors.modal` / `balloon`.
2. Если есть — делается скриншот (до того, как `fetchErrorStack` закроет модалку).
3. Для модальных ошибок вызывается `fetchErrorStack` (две стратегии — Path 1 для платформенных исключений с кнопкой «Открыть отчёт», Path 2 для `ВызватьИсключение` через гамбургер-меню → О программе → Информация для тех. поддержки; см. [web-test-guide.md](web-test-guide.md)).
4. Бросается исключение со структурированным `err.onecError`:
В отчёте это превращается в `error.onecError.stack` для упавшего теста. Разбор причин падения и категории — см. §16.
### Платформенные модальные диалоги
`getFormState()` возвращает `platformDialogs` — массив платформенных диалогов (About, Support Info, Error Report). `closeForm()` закрывает их. `dismissPendingErrors()` чистит ожидающие модалки автоматически (вызывается перед каждым ACTION_FN, плюс в встроенном сбросе после теста).
Модальное окно платформенной ошибки сначала рендерится в переходном состоянии (~1 с), затем перерисовывается в стабильное. `fetchErrorStack` ждёт 1.5 с и перепроверяет `hasReport` перед выбором стратегии.
### Таймауты
- Глобальный таймаут теста: `mod.timeout` или `config.timeout` или CLI `--timeout=ms`.
- Таймаут срабатывает на уровне теста (`testFn()` + `setup` + `teardown`), не на уровне отдельного `step` или action.
- При таймауте: текущий step помечается failed, бросается ошибка с сообщением `Timeout (<N>ms)`, далее запускается `afterEach` и встроенный сброс.
### Повторы
При `--retry=N` (или `config.retries`) упавший тест повторяется до `1 + N` раз. Для каждой попытки:
- В отчёте фиксируется `attempts` — фактически выполнено попыток.
- Считается passed, если последняя попытка зелёная; иначе failed.
`beforeAll` / `afterAll` / `prepare` / `cleanup` / `afterOpenContext` / `beforeCloseContext` не повторяются (это жизненный цикл всего прогона или контекста, не теста).
---
## 16. Анализ результатов
### Что лежит в записи об упавшем тесте
JSON-отчёт (`tests[]`, полная структура — §9) для каждого падения содержит:
- `error.message` — текст исключения.
- `error.step` — имя шага, на котором упало.
- `error.screenshot` — путь к скриншоту падения (если стратегия скриншотов не `off`).
- `error.onecError` (только для 1С-исключений) — структура с полями: `step` (имя действия, например `clickElement`), `args` (аргументы вызова), `errors` (модальное окно или balloon), `formState` (снимок формы на момент ошибки), `stack` — платформенный стек вызовов 1С с `entries[{location, code}]`.
- `steps[]` — пошаговая разбивка с метками времени, у каждого шага свой `status` и `error`.
В Allure-отчёте те же данные лежат в `statusDetails` (текст ошибки и трассировка), скриншоты и видео — во вложениях, автоматическая группировка по причинам — через `categories.json` (§9).
### Типовые причины падений
Большинство падений на 1С-стенде сводится к трём причинам, и их полезно различать при разборе отчёта:
- **Ошибка в тесте** — селектор не нашёл элемент, ожидание не сошлось, гонка без точки синхронизации. Признаки: падение стабильно повторяется на одном и том же шаге; после правки теста воспроизводимость исчезает. Действие — изменить тест.
- **Ошибка в прикладном решении** — реально воспроизведённое некорректное поведение конфигурации. Признаки: упал шаг, имитирующий пользовательскую операцию; в `error.onecError.stack` есть платформенный стек вызовов 1С с указанием на код решения. Действие — передать разработчику конфигурации, тест править не нужно.
- **Сбой стенда** — таймаут Apache, форма входа не загрузилась, не хватило веб-лицензий. Признаки: падение на навигации или входе; от прогона к прогону падает «то одно, то другое», без связи с содержанием теста. Действие — править инфраструктуру (`prepare()`, очистка сессий, идемпотентность хуков), не тесты.
`categories.json` Allure (§9) удобно настраивать именно под эти три категории — regex по `error.message` уже даёт первичную классификацию в виджете Categories.
| **Context (BrowserContext)** | Изолированная сессия Playwright. Куки/состояние/страница независимы. В рамках одного теста используется один или несколько контекстов. |
| **Active context** | Контекст, на котором сейчас оперируют функции browser-API. Переключается `setActiveContext`. |
| **Primary context** | Контекст, активный на входе в тест. Декларация (`mod.context` или `mod.contexts[0]`). Зафиксирован в `testInfo.primaryContext`. |
| **Default context** | Контекст из `config.defaultContext` (или единственный URL в упрощённой конфигурации). Используется, если тест не указал `context` / `contexts`. |
| **Scoped API** | Объект на `ctx.<name>` в мульти-контекстных тестах — обёртки browser-функций, авто-переключающие контекст перед каждым вызовом. |
| **Action function (ACTION_FN)** | Browser-функция, обёрнутая авто-обнаружением 1С-ошибок. Список — в §3. |
| **Step** | Логический блок внутри теста, обёрнутый `step(name, fn)`. Маппится на Allure-step, попадает в `report.tests[].steps[]`. |
| **Reset state** | Встроенная пост-тестовая очистка: `dismissPendingErrors` + закрытие всех открытых форм до рабочего стола. Выполняется после `afterEach`. |
| **hookArgs** | Массив строк, переданных в `prepare` / `cleanup` после CLI-разделителя `--`. Движком не интерпретируются. |
| **Severity** | Уровень критичности теста (`blocker / critical / normal / minor / trivial`) для Allure. Резолвится из `mod.severity`, тегов, `config.severity`, `config.defaultSeverity`. |
---
## См. также
- [web-test-guide.md](web-test-guide.md) — browser API (`clickElement`, `getFormState`, `readTable`, …) и интерактивный режим.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.