diff --git a/.claude/skills/role-compile/SKILL.md b/.claude/skills/role-compile/SKILL.md
index d998e1e9..8e7e8758 100644
--- a/.claude/skills/role-compile/SKILL.md
+++ b/.claude/skills/role-compile/SKILL.md
@@ -64,7 +64,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" -
### Русские синонимы
-Поддерживаются русские типы (`Справочник`→Catalog, `Документ`→Document) и права (`Чтение`→Read, `Просмотр`→View). Смешивание допустимо: `"Справочник.Контрагенты: Чтение, View"`.
+Поддерживаются русские типы (`Справочник`→Catalog, `Документ`→Document) и права (`Чтение`→Read, `Просмотр`→View). Каноничная форма — английская.
### Шаблоны RLS
@@ -99,7 +99,13 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-compile.ps1" -
}
```
-Подробные таблицы пресетов, русских синонимов и дополнительные примеры — в `dsl-reference.md`.
+## Что можно писать в `objects`
+
+Права имеют 27 типов объектов; тип или имя права вне списка — ошибка: роль не создаётся, файлы не пишутся, `Configuration.xml` не меняется. Права нельзя назначить на `Enum`, `CommonModule`, `DefinedType`, `CommonPicture`, `CommonTemplate`, `Language`, `FunctionalOption`, `EventSubscription`, `ScheduledJob`, `StyleItem`, `SettingsStorage` и подобные — в дереве редактора ролей их нет.
+
+Права на части объекта задаются точечным путём: `Catalog.Контрагенты.Attribute.ИНН: View, Edit`, `WebService.Обмен.Operation.Загрузить: Use`.
+
+Полные таблицы «тип → права», виды вложенности (включая внешние источники данных), список типов без прав, таблицы пресетов и дополнительные примеры — в `dsl-reference.md`.
## Верификация
diff --git a/.claude/skills/role-compile/dsl-reference.md b/.claude/skills/role-compile/dsl-reference.md
index 0e23a589..fd6f8f83 100644
--- a/.claude/skills/role-compile/dsl-reference.md
+++ b/.claude/skills/role-compile/dsl-reference.md
@@ -2,49 +2,86 @@
Подробная справка по JSON DSL для `/role-compile`. Компактное описание — в [SKILL.md](SKILL.md).
-## Структура верхнего уровня
+## Какие объекты можно писать в `objects`
-```json
-{
- "name": "ИмяРоли",
- "synonym": "Отображаемое имя роли",
- "comment": "",
- "setForNewObjects": false,
- "setForAttributesByDefault": true,
- "independentRightsOfChildObjects": false,
- "objects": [ ... ],
- "templates": [ ... ]
-}
-```
+Права в роли имеют ровно 27 типов. Тип вне этого списка — ошибка компиляции: роль не
+создаётся, файлы не пишутся.
-- `name` — программное имя роли (обязательно)
-- `synonym` — отображаемое имя (по умолчанию = name)
-- `comment` — комментарий (по умолчанию пусто)
-- Глобальные флаги — по умолчанию `false`, `true`, `false`
+| Тип | Права |
+|-----|-------|
+| `Configuration` | Administration, DataAdministration, UpdateDataBaseConfiguration, ConfigurationExtensionsAdministration, ActiveUsers, EventLog, ExclusiveMode, ThinClient, ThickClient, WebClient, MobileClient, ExternalConnection, Automation, Output, SaveUserData, TechnicalSpecialistMode, InteractiveOpenExtDataProcessors, InteractiveOpenExtReports, AnalyticsSystemClient, CollaborationSystemInfoBaseRegistration, MainWindowModeNormal, MainWindowModeWorkplace, MainWindowModeEmbeddedWorkplace, MainWindowModeFullscreenWorkplace, MainWindowModeKiosk |
+| `Catalog`, `ChartOfCharacteristicTypes` | Read, Insert, Update, Delete, View, Edit, InputByString, Interactive* (Insert, SetDeletionMark, ClearDeletionMark, Delete, DeleteMarked), *PredefinedData, *DataHistory* |
+| `Document` | то же + Posting, UndoPosting, InteractivePosting, InteractivePostingRegular, InteractiveUndoPosting, InteractiveChangeOfPosted |
+| `ChartOfAccounts`, `ChartOfCalculationTypes`, `ExchangePlan` | Read, Insert, Update, Delete, View, Edit, InputByString, Interactive*, *DataHistory* (без InteractiveDeleteMarked у ChartOfAccounts) |
+| `BusinessProcess` | Read, Insert, Update, Delete, View, Edit, InputByString, Start, InteractiveInsert, InteractiveSetDeletionMark, InteractiveClearDeletionMark, InteractiveDelete, InteractiveActivate, InteractiveStart |
+| `Task` | то же, но вместо Start/InteractiveStart — Execute, InteractiveExecute |
+| `InformationRegister` | Read, Update, View, Edit, TotalsControl, *DataHistory* |
+| `AccumulationRegister`, `AccountingRegister` | Read, Update, View, Edit, TotalsControl |
+| `CalculationRegister` | Read, View |
+| `Constant` | Read, Update, View, Edit, *DataHistory* |
+| `DocumentJournal` | Read, View |
+| `Sequence` | Read, Update |
+| `DataProcessor`, `Report` | Use, View |
+| `WebService`, `HTTPService`, `IntegrationService` | Use |
+| `CommonForm`, `CommonCommand`, `Subsystem`, `FilterCriterion` | View |
+| `CommonAttribute` | View, Edit |
+| `SessionParameter` | Get, Set |
+| `ExternalDataSource` | Use, Administration, StandardAuthenticationChange, SessionStandardAuthenticationChange, SessionOSAuthenticationChange |
-## Объекты: два формата
+Полные списки прав по каждому типу — в коде навыка (`scripts/role-compile.ps1`,
+таблица `$knownRights`); навык проверяет каждое имя права и отвергает несуществующее.
+
+### Вложенные объекты
+
+Путь длиннее двух сегментов задаёт права на часть объекта. Вид вложенности —
+предпоследний сегмент, он определяет набор прав.
+
+| Вид | Права |
+|-----|-------|
+| `Attribute`, `StandardAttribute`, `TabularSection`, `StandardTabularSection`, `Dimension`, `Resource`, `AccountingFlag`, `ExtDimensionAccountingFlag`, `AddressingAttribute` | View, Edit |
+| `Command`, вложенная `Subsystem` | View |
+| `Operation` (веб-сервис), `Method` (HTTP-сервис, под `URLTemplate`), `IntegrationServiceChannel` | Use |
+| `Recalculation` (регистр расчёта) | Read, Update |
+
+Виды `Operation`, `Method`, `IntegrationServiceChannel`, `Recalculation` и виды внешнего
+источника допустимы только у своего типа-родителя. Графы журнала документов
+(`DocumentJournal.Ж.Column.Г`) собственных прав не имеют.
+
+Примеры: `Catalog.Контрагенты.Attribute.ИНН: View, Edit`,
+`Document.Реализация.TabularSection.Товары.Attribute.Цена: View`,
+`WebService.Обмен.Operation.Загрузить: Use`,
+`HTTPService.API.URLTemplate.Файлы.Method.get: Use`.
+
+### Вложенные объекты внешнего источника данных
+
+Виды `Table`, `Cube`, `Function`, `Field`, `DimensionTable` бывают **только** под
+`ExternalDataSource`.
+
+| Путь | Права |
+|------|-------|
+| `ExternalDataSource.И.Table.Т` | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveDelete |
+| `ExternalDataSource.И.Table.Т.Field.П` | View, Edit |
+| `ExternalDataSource.И.Cube.К` | Read, View |
+| `ExternalDataSource.И.Cube.К.Dimension.И`, `…Resource.Р` | View |
+| `ExternalDataSource.И.Cube.К.DimensionTable.Т` | Read, View |
+| `ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П` | View, Edit |
+| `ExternalDataSource.И.Function.Ф` | Use, View |
+| `…Table.Т.Command.К`, `…Cube.К.Command.К`, `…DimensionTable.Т.Command.К` | View |
+
+### Типы объектов без прав в ролях
+
+Права нельзя назначить на `Enum`, `CommonModule`, `DefinedType`, `CommonPicture`,
+`CommonTemplate`, `Language`, `FunctionalOption`, `FunctionalOptionsParameter`,
+`EventSubscription`, `ScheduledJob`, `StyleItem`, `Style`, `SettingsStorage`,
+`XDTOPackage`, `WSReference`, `DocumentNumerator` — в дереве редактора ролей этих типов нет.
+
+Попытка — ошибка компиляции. Права на перечисления особенно опасны: конфигурация с таким
+блоком не загружается в информационную базу, конфигуратор зависает без сообщений.
+
+## Формат записи объектов
Массив `objects` принимает строки (shorthand) и объекты (полная форма).
-### Строковый shorthand
-
-```
-"ОбъектМетаданных: @пресет"
-"ОбъектМетаданных: Право1, Право2"
-```
-
-Примеры:
-```json
-"objects": [
- "Catalog.Номенклатура: @view",
- "Document.Реализация: @edit",
- "InformationRegister.Цены: Read, Update",
- "DataProcessor.Загрузка: @view"
-]
-```
-
-### Объектная форма (для RLS и переопределений)
-
```json
{
"name": "Document.Реализация",
@@ -58,6 +95,9 @@
- `rights` — переопределения: dict `{"Right": true/false}` или массив `["Right1", "Right2"]`
- `rls` — RLS-ограничения: `{"ИмяПрава": "текст условия"}`
+Русские имена типов и прав тоже принимаются (`Справочник.Контрагенты: Чтение`), каноничная
+форма — английская.
+
## Пресеты — подробные таблицы
Пресеты обозначаются `@` в строковом формате. В объектной форме ключ `preset` без `@`.
@@ -88,127 +128,8 @@
| SessionParameter | Get, Set |
| CommonAttribute | View, Edit |
-Для сервисов (WebService, HTTPService, IntegrationService) пресеты не определены — используй явные права: `"WebService.Имя: Use"`.
-
-Если пресет не определён для типа объекта — предупреждение с подсказкой доступных.
-
-## Русские синонимы
-
-Скрипт автоматически транслирует русские имена в английские. Можно смешивать: `"Справочник.Контрагенты: Чтение, View"` — работает.
-
-### Типы объектов
-
-| Русский | English |
-|---------|---------|
-| `Справочник` | Catalog |
-| `Документ` | Document |
-| `РегистрСведений` | InformationRegister |
-| `РегистрНакопления` | AccumulationRegister |
-| `РегистрБухгалтерии` | AccountingRegister |
-| `РегистрРасчета` | CalculationRegister |
-| `Константа` | Constant |
-| `ПланСчетов` | ChartOfAccounts |
-| `ПланВидовХарактеристик` | ChartOfCharacteristicTypes |
-| `ПланВидовРасчета` | ChartOfCalculationTypes |
-| `ПланОбмена` | ExchangePlan |
-| `БизнесПроцесс` | BusinessProcess |
-| `Задача` | Task |
-| `Обработка` | DataProcessor |
-| `Отчет` | Report |
-| `ОбщаяФорма` | CommonForm |
-| `ОбщаяКоманда` | CommonCommand |
-| `Подсистема` | Subsystem |
-| `КритерийОтбора` | FilterCriterion |
-| `ЖурналДокументов` | DocumentJournal |
-| `Последовательность` | Sequence |
-| `ВебСервис` | WebService |
-| `HTTPСервис` | HTTPService |
-| `СервисИнтеграции` | IntegrationService |
-| `ПараметрСеанса` | SessionParameter |
-| `ОбщийРеквизит` | CommonAttribute |
-| `Конфигурация` | Configuration |
-| `Перечисление` | Enum |
-
-### Вложенные типы
-
-| Русский | English |
-|---------|---------|
-| `Реквизит` | Attribute |
-| `СтандартныйРеквизит` | StandardAttribute |
-| `ТабличнаяЧасть` | TabularSection |
-| `Измерение` | Dimension |
-| `Ресурс` | Resource |
-| `Команда` | Command |
-| `РеквизитАдресации` | AddressingAttribute |
-
-### Права (основные)
-
-| Русский | English |
-|---------|---------|
-| `Чтение` | Read |
-| `Добавление` | Insert |
-| `Изменение` | Update |
-| `Удаление` | Delete |
-| `Просмотр` | View |
-| `Редактирование` | Edit |
-| `ВводПоСтроке` | InputByString |
-| `Проведение` | Posting |
-| `ОтменаПроведения` | UndoPosting |
-| `Использование` | Use |
-| `Получение` | Get |
-| `Установка` | Set |
-| `Старт` | Start |
-| `Выполнение` | Execute |
-| `УправлениеИтогами` | TotalsControl |
-
-### Права (интерактивные)
-
-| Русский | English |
-|---------|---------|
-| `ИнтерактивноеДобавление` | InteractiveInsert |
-| `ИнтерактивнаяПометкаУдаления` | InteractiveSetDeletionMark |
-| `ИнтерактивноеСнятиеПометкиУдаления` | InteractiveClearDeletionMark |
-| `ИнтерактивноеУдаление` | InteractiveDelete |
-| `ИнтерактивноеУдалениеПомеченных` | InteractiveDeleteMarked |
-| `ИнтерактивноеПроведение` | InteractivePosting |
-| `ИнтерактивноеПроведениеНеоперативное` | InteractivePostingRegular |
-| `ИнтерактивнаяОтменаПроведения` | InteractiveUndoPosting |
-| `ИнтерактивноеИзменениеПроведенных` | InteractiveChangeOfPosted |
-| `ИнтерактивныйСтарт` | InteractiveStart |
-| `ИнтерактивнаяАктивация` | InteractiveActivate |
-| `ИнтерактивноеВыполнение` | InteractiveExecute |
-
-### Права (конфигурация)
-
-| Русский | English |
-|---------|---------|
-| `Администрирование` | Administration |
-| `АдминистрированиеДанных` | DataAdministration |
-| `ТонкийКлиент` | ThinClient |
-| `ТолстыйКлиент` | ThickClient |
-| `ВебКлиент` | WebClient |
-| `МобильныйКлиент` | MobileClient |
-| `ВнешнееСоединение` | ExternalConnection |
-| `Вывод` | Output |
-| `СохранениеДанныхПользователя` | SaveUserData |
-
-## Типы объектов без прав в ролях
-
-Следующие типы 1С **не могут** иметь права в ролях (не добавляются в `objects`):
-
-| Тип | Причина |
-|-----|---------|
-| Enum (Перечисление) | Права наследуются от конфигурации, явное назначение невозможно |
-| CommonModule (ОбщийМодуль) | Не имеет собственных прав в роли |
-| DefinedType (ОпределяемыйТип) | Тип данных, не объект прав |
-| CommonPicture (ОбщаяКартинка) | Ресурс, не объект прав |
-| CommonTemplate (ОбщийМакет) | Ресурс, не объект прав |
-| Language (Язык) | Конфигурационный элемент |
-| FunctionalOption (ФункциональнаяОпция) | Не объект прав |
-| FunctionalOptionsParameter | Не объект прав |
-| EventSubscription (ПодпискаНаСобытие) | Не объект прав |
-| ScheduledJob (РегламентноеЗадание) | Не объект прав |
-| StyleItem (ЭлементСтиля) | Ресурс оформления |
+Для сервисов (WebService, HTTPService, IntegrationService) и внешних источников данных
+пресеты не определены — задавай права явно: `"WebService.Имя: Use"`.
## Шаблоны ограничений (RLS templates)
@@ -227,36 +148,7 @@
## Примеры
-### 1. Простая роль (только пресеты)
-
-```json
-{
- "name": "ЧтениеНоменклатуры",
- "synonym": "Чтение номенклатуры",
- "objects": [
- "Catalog.Номенклатура: @view",
- "Catalog.Контрагенты: @view",
- "DataProcessor.Загрузка: @view"
- ]
-}
-```
-
-### 2. Роль для регламентного задания
-
-```json
-{
- "name": "ОбновлениеЦен",
- "synonym": "Обновление цен номенклатуры",
- "objects": [
- "Catalog.Номенклатура: Read",
- "Catalog.Валюты: Read",
- "InformationRegister.ЦеныНоменклатуры: Read, Update",
- "Constant.ОсновнаяВалюта: Read"
- ]
-}
-```
-
-### 3. Роль с RLS
+### Роль с RLS
```json
{
@@ -281,22 +173,7 @@
}
```
-### 4. Роль с русскими синонимами
-
-```json
-{
- "name": "ПросмотрДанных",
- "synonym": "Просмотр данных",
- "objects": [
- "Справочник.Контрагенты: @view",
- "Документ.Реализация: Чтение, Просмотр",
- "РегистрСведений.Цены: @edit",
- "Обработка.ЗагрузкаДанных: @view"
- ]
-}
-```
-
-### 5. Роль с переопределением прав из пресета
+### Роль с переопределением прав из пресета
```json
{
@@ -311,3 +188,16 @@
]
}
```
+
+### Роль с правами на части объекта
+
+```json
+{
+ "name": "МенеджерБезЦен",
+ "synonym": "Менеджер без доступа к ценам",
+ "objects": [
+ "Catalog.Номенклатура: @view",
+ { "name": "Catalog.Номенклатура.Attribute.Цена", "rights": { "View": false, "Edit": false } }
+ ]
+}
+```
diff --git a/.claude/skills/role-compile/scripts/role-compile.ps1 b/.claude/skills/role-compile/scripts/role-compile.ps1
index 189853ac..3aab0568 100644
--- a/.claude/skills/role-compile/scripts/role-compile.ps1
+++ b/.claude/skills/role-compile/scripts/role-compile.ps1
@@ -1,4 +1,4 @@
-# role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
+# role-compile v1.25 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -217,7 +217,24 @@ $script:typeAliases = @{
"ПараметрСеанса" = "SessionParameter"
"ОбщийРеквизит" = "CommonAttribute"
"Конфигурация" = "Configuration"
+ "ВнешнийИсточникДанных" = "ExternalDataSource"
+ # Типы без прав в ролях: алиасы нужны не ради генерации, а ради отказа по делу —
+ # иначе на русскую запись навык ответит «неизвестный тип 'ОбщийМодуль'».
"Перечисление" = "Enum"
+ "ОбщийМодуль" = "CommonModule"
+ "ОпределяемыйТип" = "DefinedType"
+ "ОбщаяКартинка" = "CommonPicture"
+ "ОбщийМакет" = "CommonTemplate"
+ "Язык" = "Language"
+ "ФункциональнаяОпция" = "FunctionalOption"
+ "ПараметрФункциональныхОпций" = "FunctionalOptionsParameter"
+ "ПодпискаНаСобытие" = "EventSubscription"
+ "РегламентноеЗадание" = "ScheduledJob"
+ "ЭлементСтиля" = "StyleItem"
+ "ХранилищеНастроек" = "SettingsStorage"
+ "ПакетXDTO" = "XDTOPackage"
+ "WSСсылка" = "WSReference"
+ "Нумератор" = "DocumentNumerator"
# Nested
"Реквизит" = "Attribute"
"СтандартныйРеквизит" = "StandardAttribute"
@@ -403,11 +420,72 @@ $script:knownRights = @{
"IntegrationService" = @("Use")
"SessionParameter" = @("Get","Set")
"CommonAttribute" = @("View","Edit")
+ "ExternalDataSource" = @(
+ "Use","Administration","StandardAuthenticationChange",
+ "SessionStandardAuthenticationChange","SessionOSAuthenticationChange"
+ )
}
-# Nested objects: Attribute, StandardAttribute, TabularSection, Dimension, Resource, AddressingAttribute
-$script:nestedRights = @("View","Edit")
-$script:commandRights = @("View")
+# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса
+# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора:
+# догадкам тут не место — закрытый список превращает промах в ложный отказ.
+$script:nestedKindRights = @{
+ "Attribute" = @("View","Edit")
+ "StandardAttribute" = @("View","Edit")
+ "TabularSection" = @("View","Edit")
+ "StandardTabularSection" = @("View","Edit")
+ "Dimension" = @("View","Edit")
+ "Resource" = @("View","Edit")
+ "AccountingFlag" = @("View","Edit")
+ "ExtDimensionAccountingFlag" = @("View","Edit")
+ "AddressingAttribute" = @("View","Edit")
+ "Field" = @("View","Edit")
+ "Command" = @("View")
+ "Subsystem" = @("View")
+ "Operation" = @("Use")
+ "Method" = @("Use")
+ "IntegrationServiceChannel" = @("Use")
+ "Recalculation" = @("Read","Update")
+ "Cube" = @("Read","View")
+ "DimensionTable" = @("Read","View")
+ "Function" = @("Use","View")
+ "Table" = @(
+ "Read","Insert","Update","Delete","View","Edit","InputByString",
+ "InteractiveInsert","InteractiveDelete"
+ )
+}
+
+# Виды, существующие только у одного типа-родителя: без этой привязки
+# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект.
+$script:kindOwners = @{
+ "Table" = "ExternalDataSource"
+ "Cube" = "ExternalDataSource"
+ "Function" = "ExternalDataSource"
+ "Field" = "ExternalDataSource"
+ "DimensionTable" = "ExternalDataSource"
+ "Recalculation" = "CalculationRegister"
+ "Operation" = "WebService"
+ "Method" = "HTTPService"
+ "IntegrationServiceChannel" = "IntegrationService"
+}
+
+# Один и тот же вид под разными родителями имеет разный набор: измерение регистра —
+# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя,
+# объединение молча разрешило бы Edit там, где платформа его не даёт.
+$script:nestedKindRightsByType = @{
+ "ExternalDataSource" = @{
+ "Dimension" = @("View")
+ "Resource" = @("View")
+ }
+}
+
+# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением —
+# отказ даёт отсутствие типа в $knownRights; здесь только причина для сообщения.
+$script:noRightsTypes = @(
+ "Enum","CommonModule","DefinedType","CommonPicture","CommonTemplate","Language",
+ "FunctionalOption","FunctionalOptionsParameter","EventSubscription","ScheduledJob",
+ "StyleItem","Style","SettingsStorage","XDTOPackage","WSReference","DocumentNumerator"
+)
# --- 4. Presets (@view, @edit) ---
@@ -472,6 +550,66 @@ function Is-NestedObject {
return ($objectName.Split(".").Count -ge 3)
}
+# Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным
+# (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца.
+function Get-NestedKind {
+ param([string]$objectName)
+ $parts = $objectName.Split(".")
+ if ($parts.Count -lt 3) { return $null }
+ return $parts[$parts.Count - 2]
+}
+
+function Get-NestedRights {
+ param([string]$objectType, [string]$kind)
+ if ($script:nestedKindRightsByType.ContainsKey($objectType) -and
+ $script:nestedKindRightsByType[$objectType].ContainsKey($kind)) {
+ return @($script:nestedKindRightsByType[$objectType][$kind])
+ }
+ if ($script:nestedKindRights.ContainsKey($kind)) { return @($script:nestedKindRights[$kind]) }
+ return $null
+}
+
+# Отказ копится, а не печатается сразу: роль пишется целиком, поэтому единственный
+# безопасный момент отказа — до первой записи, и показать надо все причины сразу.
+$script:validationErrors = @()
+
+function Add-ValidationError {
+ param([string]$message)
+ $script:validationErrors += $message
+}
+
+# Проверка имени объекта: тип по белому списку (всегда, включая вложенные пути) и вид
+# вложенности. Запрещённый и незнакомый тип — разные диагнозы.
+function Validate-ObjectName {
+ param([string]$objectName)
+
+ $objectType = Get-ObjectType $objectName
+ if (-not $script:knownRights.ContainsKey($objectType)) {
+ if ($script:noRightsTypes -contains $objectType) {
+ Add-ValidationError "${objectName}: тип '$objectType' не имеет прав в роли — уберите объект из списка"
+ } else {
+ $similar = @($script:knownRights.Keys | Where-Object { $_ -like "*$objectType*" -or $objectType -like "*$_*" })
+ $sug = if ($similar.Count -gt 0) { " Возможно: $(($similar | Select-Object -First 3) -join ', ')?" } else { "" }
+ Add-ValidationError "${objectName}: неизвестный тип объекта '$objectType'.$sug"
+ }
+ return $false
+ }
+
+ if (Is-NestedObject $objectName) {
+ $kind = Get-NestedKind $objectName
+ if ($script:kindOwners.ContainsKey($kind) -and $objectType -ne $script:kindOwners[$kind]) {
+ Add-ValidationError "${objectName}: вид '$kind' бывает только у $($script:kindOwners[$kind])"
+ return $false
+ }
+ if ($null -eq (Get-NestedRights $objectType $kind)) {
+ Add-ValidationError "${objectName}: неизвестный вид вложенности '$kind'"
+ return $false
+ }
+ }
+
+ return $true
+}
+
function Resolve-Preset {
param([string]$objectType, [string]$presetName)
@@ -503,23 +641,18 @@ function Validate-RightName {
$objectType = Get-ObjectType $objectName
- if (Is-NestedObject $objectName) {
- if ($objectName -match '\.Command\.') {
- if ($rightName -notin $script:commandRights) {
- Write-Warning "${objectName}: '$rightName' not valid for commands (only: View)"
- return $false
- }
- } else {
- if ($rightName -notin $script:nestedRights) {
- Write-Warning "${objectName}: '$rightName' not valid for nested objects (only: View, Edit)"
- return $false
- }
- }
- return $true
- }
+ # Тип уже проверен Validate-ObjectName — здесь только права, иначе про один
+ # запрещённый тип напечатается столько строк, сколько у него перечислено прав.
+ if (-not $script:knownRights.ContainsKey($objectType)) { return $false }
- if (-not $script:knownRights.ContainsKey($objectType)) {
- Write-Warning "${objectName}: unknown object type '$objectType'"
+ if (Is-NestedObject $objectName) {
+ $kind = Get-NestedKind $objectName
+ $validNested = Get-NestedRights $objectType $kind
+ if ($null -eq $validNested) { return $false }
+ if ($rightName -notin $validNested) {
+ Add-ValidationError "${objectName}: право '$rightName' недопустимо для вида '$kind' (допустимо: $($validNested -join ', '))"
+ return $false
+ }
return $true
}
@@ -528,8 +661,8 @@ function Validate-RightName {
$suggestions = @($validRights | Where-Object {
$_ -like "*$rightName*" -or $rightName -like "*$_*"
})
- $sugStr = if ($suggestions.Count -gt 0) { " Did you mean: $($suggestions -join ', ')?" } else { "" }
- Write-Warning "${objectName}: unknown right '$rightName'.$sugStr"
+ $sugStr = if ($suggestions.Count -gt 0) { " Возможно: $(($suggestions | Select-Object -First 3) -join ', ')?" } else { "" }
+ Add-ValidationError "${objectName}: право '$rightName' не существует у типа '$objectType'.$sugStr"
return $false
}
@@ -550,6 +683,9 @@ function Parse-ObjectEntry {
}
$objName = Translate-ObjectName ($entry.Substring(0, $colonIdx).Trim())
$rightsStr = $entry.Substring($colonIdx + 1).Trim()
+ # Объект с непроходным именем дальше не разбираем: пресет для несуществующего типа
+ # добавил бы к отказу ещё и бессмысленное предупреждение.
+ if (-not (Validate-ObjectName $objName)) { return $null }
$objectType = Get-ObjectType $objName
if ($rightsStr.StartsWith('@')) {
@@ -575,6 +711,8 @@ function Parse-ObjectEntry {
return $null
}
+ if (-not (Validate-ObjectName $objName)) { return $null }
+
$objectType = Get-ObjectType $objName
$rightsMap = [ordered]@{}
@@ -648,6 +786,16 @@ if ($def.objects) {
}
}
+# Отказ ДО записи: роль пишется тремя файлами (метаданные, права, регистрация в
+# Configuration.xml), и частично записанная роль хуже отсутствующей. Печатаем все причины
+# разом — иначе пользователь чинит их по одной. Console.Error, а не Write-Error: под
+# ErrorActionPreference=Stop последний бросает исключение (см. комментарий в support-guard).
+if ($script:validationErrors.Count -gt 0) {
+ [Console]::Error.WriteLine("[role-compile] Роль не создана: $($script:validationErrors.Count) ошибок в описании прав.")
+ foreach ($e in $script:validationErrors) { [Console]::Error.WriteLine(" ERROR: $e") }
+ exit 1
+}
+
# --- Detect format version ---
function Detect-FormatVersion([string]$dir) {
diff --git a/.claude/skills/role-compile/scripts/role-compile.py b/.claude/skills/role-compile/scripts/role-compile.py
index c769e54b..fed2b41b 100644
--- a/.claude/skills/role-compile/scripts/role-compile.py
+++ b/.claude/skills/role-compile/scripts/role-compile.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# role-compile v1.24 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
+# role-compile v1.25 — Compile 1C role from JSON (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -354,7 +354,24 @@ TYPE_ALIASES = {
"ПараметрСеанса": "SessionParameter",
"ОбщийРеквизит": "CommonAttribute",
"Конфигурация": "Configuration",
+ "ВнешнийИсточникДанных": "ExternalDataSource",
+ # Типы без прав в ролях: алиасы нужны не ради генерации, а ради отказа по делу —
+ # иначе на русскую запись навык ответит «неизвестный тип 'ОбщийМодуль'».
"Перечисление": "Enum",
+ "ОбщийМодуль": "CommonModule",
+ "ОпределяемыйТип": "DefinedType",
+ "ОбщаяКартинка": "CommonPicture",
+ "ОбщийМакет": "CommonTemplate",
+ "Язык": "Language",
+ "ФункциональнаяОпция": "FunctionalOption",
+ "ПараметрФункциональныхОпций": "FunctionalOptionsParameter",
+ "ПодпискаНаСобытие": "EventSubscription",
+ "РегламентноеЗадание": "ScheduledJob",
+ "ЭлементСтиля": "StyleItem",
+ "ХранилищеНастроек": "SettingsStorage",
+ "ПакетXDTO": "XDTOPackage",
+ "WSСсылка": "WSReference",
+ "Нумератор": "DocumentNumerator",
# Nested
"Реквизит": "Attribute",
"СтандартныйРеквизит": "StandardAttribute",
@@ -516,10 +533,72 @@ KNOWN_RIGHTS = {
"IntegrationService": ["Use"],
"SessionParameter": ["Get", "Set"],
"CommonAttribute": ["View", "Edit"],
+ "ExternalDataSource": [
+ "Use", "Administration", "StandardAuthenticationChange",
+ "SessionStandardAuthenticationChange", "SessionOSAuthenticationChange",
+ ],
}
-NESTED_RIGHTS = ["View", "Edit"]
-COMMAND_RIGHTS = ["View"]
+# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса
+# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора:
+# догадкам тут не место — закрытый список превращает промах в ложный отказ.
+NESTED_KIND_RIGHTS = {
+ "Attribute": ["View", "Edit"],
+ "StandardAttribute": ["View", "Edit"],
+ "TabularSection": ["View", "Edit"],
+ "StandardTabularSection": ["View", "Edit"],
+ "Dimension": ["View", "Edit"],
+ "Resource": ["View", "Edit"],
+ "AccountingFlag": ["View", "Edit"],
+ "ExtDimensionAccountingFlag": ["View", "Edit"],
+ "AddressingAttribute": ["View", "Edit"],
+ "Field": ["View", "Edit"],
+ "Command": ["View"],
+ "Subsystem": ["View"],
+ "Operation": ["Use"],
+ "Method": ["Use"],
+ "IntegrationServiceChannel": ["Use"],
+ "Recalculation": ["Read", "Update"],
+ "Cube": ["Read", "View"],
+ "DimensionTable": ["Read", "View"],
+ "Function": ["Use", "View"],
+ "Table": [
+ "Read", "Insert", "Update", "Delete", "View", "Edit", "InputByString",
+ "InteractiveInsert", "InteractiveDelete",
+ ],
+}
+
+# Виды, существующие только у одного типа-родителя: без этой привязки
+# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект.
+KIND_OWNERS = {
+ 'Table': 'ExternalDataSource',
+ 'Cube': 'ExternalDataSource',
+ 'Function': 'ExternalDataSource',
+ 'Field': 'ExternalDataSource',
+ 'DimensionTable': 'ExternalDataSource',
+ 'Recalculation': 'CalculationRegister',
+ 'Operation': 'WebService',
+ 'Method': 'HTTPService',
+ 'IntegrationServiceChannel': 'IntegrationService',
+}
+
+# Один и тот же вид под разными родителями имеет разный набор: измерение регистра —
+# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя,
+# объединение молча разрешило бы Edit там, где платформа его не даёт.
+NESTED_KIND_RIGHTS_BY_TYPE = {
+ "ExternalDataSource": {
+ "Dimension": ["View"],
+ "Resource": ["View"],
+ },
+}
+
+# Типы без прав в ролях (в дереве редактора ролей их нет). Список НЕ управляет поведением —
+# отказ даёт отсутствие типа в KNOWN_RIGHTS; здесь только выбор формулировки.
+NO_RIGHTS_TYPES = [
+ "Enum", "CommonModule", "DefinedType", "CommonPicture", "CommonTemplate", "Language",
+ "FunctionalOption", "FunctionalOptionsParameter", "EventSubscription", "ScheduledJob",
+ "StyleItem", "Style", "SettingsStorage", "XDTOPackage", "WSReference", "DocumentNumerator",
+]
# --- Presets ---
@@ -594,6 +673,56 @@ def is_nested_object(object_name):
return len(object_name.split('.')) >= 3
+def get_nested_kind(object_name):
+ """Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным
+ (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца."""
+ parts = object_name.split('.')
+ if len(parts) < 3:
+ return None
+ return parts[-2]
+
+
+def get_nested_rights(object_type, kind):
+ by_type = NESTED_KIND_RIGHTS_BY_TYPE.get(object_type)
+ if by_type and kind in by_type:
+ return by_type[kind]
+ return NESTED_KIND_RIGHTS.get(kind)
+
+
+# Отказ копится, а не печатается сразу: роль пишется целиком, поэтому единственный
+# безопасный момент отказа — до первой записи, и показать надо все причины сразу.
+VALIDATION_ERRORS = []
+
+
+def add_validation_error(message):
+ VALIDATION_ERRORS.append(message)
+
+
+def validate_object_name(object_name):
+ """Тип по белому списку (всегда, включая вложенные пути) и вид вложенности.
+ Запрещённый и незнакомый тип — разные диагнозы."""
+ object_type = get_object_type(object_name)
+ if object_type not in KNOWN_RIGHTS:
+ if object_type in NO_RIGHTS_TYPES:
+ add_validation_error(f"{object_name}: тип '{object_type}' не имеет прав в роли — уберите объект из списка")
+ else:
+ similar = [t for t in KNOWN_RIGHTS if object_type in t or t in object_type][:3]
+ sug = f" Возможно: {', '.join(similar)}?" if similar else ''
+ add_validation_error(f"{object_name}: неизвестный тип объекта '{object_type}'.{sug}")
+ return False
+
+ if is_nested_object(object_name):
+ kind = get_nested_kind(object_name)
+ if kind in KIND_OWNERS and object_type != KIND_OWNERS[kind]:
+ add_validation_error(f"{object_name}: вид '{kind}' бывает только у {KIND_OWNERS[kind]}")
+ return False
+ if get_nested_rights(object_type, kind) is None:
+ add_validation_error(f"{object_name}: неизвестный вид вложенности '{kind}'")
+ return False
+
+ return True
+
+
def resolve_preset(object_type, preset_name):
preset = preset_name.lstrip('@')
if preset not in PRESETS:
@@ -614,26 +743,26 @@ def resolve_preset(object_type, preset_name):
def validate_right_name(object_name, right_name):
object_type = get_object_type(object_name)
- if is_nested_object(object_name):
- if '.Command.' in object_name:
- if right_name not in COMMAND_RIGHTS:
- print(f"WARNING: {object_name}: '{right_name}' not valid for commands (only: View)", file=sys.stderr)
- return False
- else:
- if right_name not in NESTED_RIGHTS:
- print(f"WARNING: {object_name}: '{right_name}' not valid for nested objects (only: View, Edit)", file=sys.stderr)
- return False
- return True
-
+ # Тип уже проверен validate_object_name — здесь только права, иначе про один
+ # запрещённый тип напечатается столько строк, сколько у него перечислено прав.
if object_type not in KNOWN_RIGHTS:
- print(f"WARNING: {object_name}: unknown object type '{object_type}'", file=sys.stderr)
+ return False
+
+ if is_nested_object(object_name):
+ kind = get_nested_kind(object_name)
+ valid_nested = get_nested_rights(object_type, kind)
+ if valid_nested is None:
+ return False
+ if right_name not in valid_nested:
+ add_validation_error(f"{object_name}: право '{right_name}' недопустимо для вида '{kind}' (допустимо: {', '.join(valid_nested)})")
+ return False
return True
valid_rights = KNOWN_RIGHTS[object_type]
if right_name not in valid_rights:
- suggestions = [r for r in valid_rights if right_name in r or r in right_name]
- sug_str = f" Did you mean: {', '.join(suggestions)}?" if suggestions else ""
- print(f"WARNING: {object_name}: unknown right '{right_name}'.{sug_str}", file=sys.stderr)
+ suggestions = [r for r in valid_rights if right_name in r or r in right_name][:3]
+ sug_str = f" Возможно: {', '.join(suggestions)}?" if suggestions else ""
+ add_validation_error(f"{object_name}: право '{right_name}' не существует у типа '{object_type}'.{sug_str}")
return False
return True
@@ -648,6 +777,10 @@ def parse_object_entry(entry):
return None
obj_name = translate_object_name(entry[:colon_idx].strip())
rights_str = entry[colon_idx + 1:].strip()
+ # Объект с непроходным именем дальше не разбираем: пресет для несуществующего типа
+ # добавил бы к отказу ещё и бессмысленное предупреждение.
+ if not validate_object_name(obj_name):
+ return None
object_type = get_object_type(obj_name)
if rights_str.startswith('@'):
@@ -668,6 +801,9 @@ def parse_object_entry(entry):
print("WARNING: Object entry missing 'name' field", file=sys.stderr)
return None
+ if not validate_object_name(obj_name):
+ return None
+
object_type = get_object_type(obj_name)
# Use a list of tuples to preserve insertion order
rights_map = {} # name -> {Value, Condition}
@@ -761,6 +897,15 @@ def main():
if parsed:
parsed_objects.append(parsed)
+ # Отказ ДО записи: роль пишется тремя файлами (метаданные, права, регистрация в
+ # Configuration.xml), и частично записанная роль хуже отсутствующей. Печатаем все
+ # причины разом — иначе пользователь чинит их по одной.
+ if VALIDATION_ERRORS:
+ print(f"[role-compile] Роль не создана: {len(VALIDATION_ERRORS)} ошибок в описании прав.", file=sys.stderr)
+ for e in VALIDATION_ERRORS:
+ print(f" ERROR: {e}", file=sys.stderr)
+ sys.exit(1)
+
# --- 3. Generate UUID ---
uid = new_uuid()
diff --git a/.claude/skills/role-validate/SKILL.md b/.claude/skills/role-validate/SKILL.md
index f3dab146..166b8cc0 100644
--- a/.claude/skills/role-validate/SKILL.md
+++ b/.claude/skills/role-validate/SKILL.md
@@ -25,3 +25,14 @@ allowed-tools:
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/role-validate.ps1" -RightsPath "Roles/МояРоль"
```
+
+## Что считается ошибкой
+
+Ошибка (код возврата 1), а не предупреждение:
+
+- тип объекта, не имеющий прав в ролях (`Enum`, `CommonModule`, `CommonPicture`, `ScheduledJob` и т.п.) — конфигурация с блоком прав на перечисление не загружается в базу, конфигуратор зависает без сообщений;
+- незнакомый тип объекта (опечатка);
+- имя права, которого у этого типа нет;
+- вид вложенности, невозможный у данного типа, и недопустимое право у вложенного объекта.
+
+Полные списки «тип → права» и виды вложенности — в `role-compile/dsl-reference.md`.
diff --git a/.claude/skills/role-validate/scripts/role-validate.ps1 b/.claude/skills/role-validate/scripts/role-validate.ps1
index fdac6ebe..c7515bda 100644
--- a/.claude/skills/role-validate/scripts/role-validate.ps1
+++ b/.claude/skills/role-validate/scripts/role-validate.ps1
@@ -1,4 +1,4 @@
-# role-validate v1.2 — Validate 1C role structure
+# role-validate v1.3 — Validate 1C role structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -127,11 +127,72 @@ $script:knownRights = @{
"IntegrationService" = @("Use")
"SessionParameter" = @("Get","Set")
"CommonAttribute" = @("View","Edit")
+ "ExternalDataSource" = @(
+ "Use","Administration","StandardAuthenticationChange",
+ "SessionStandardAuthenticationChange","SessionOSAuthenticationChange"
+ )
}
-$script:nestedRights = @("View","Edit")
-$script:channelRights = @("Use")
-$script:commandRights = @("View")
+# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса
+# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора:
+# догадкам тут не место — закрытый список превращает промах в ложный отказ.
+$script:nestedKindRights = @{
+ "Attribute" = @("View","Edit")
+ "StandardAttribute" = @("View","Edit")
+ "TabularSection" = @("View","Edit")
+ "StandardTabularSection" = @("View","Edit")
+ "Dimension" = @("View","Edit")
+ "Resource" = @("View","Edit")
+ "AccountingFlag" = @("View","Edit")
+ "ExtDimensionAccountingFlag" = @("View","Edit")
+ "AddressingAttribute" = @("View","Edit")
+ "Field" = @("View","Edit")
+ "Command" = @("View")
+ "Subsystem" = @("View")
+ "Operation" = @("Use")
+ "Method" = @("Use")
+ "IntegrationServiceChannel" = @("Use")
+ "Recalculation" = @("Read","Update")
+ "Cube" = @("Read","View")
+ "DimensionTable" = @("Read","View")
+ "Function" = @("Use","View")
+ "Table" = @(
+ "Read","Insert","Update","Delete","View","Edit","InputByString",
+ "InteractiveInsert","InteractiveDelete"
+ )
+}
+
+# Виды, существующие только у одного типа-родителя: без этой привязки
+# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект.
+$script:kindOwners = @{
+ "Table" = "ExternalDataSource"
+ "Cube" = "ExternalDataSource"
+ "Function" = "ExternalDataSource"
+ "Field" = "ExternalDataSource"
+ "DimensionTable" = "ExternalDataSource"
+ "Recalculation" = "CalculationRegister"
+ "Operation" = "WebService"
+ "Method" = "HTTPService"
+ "IntegrationServiceChannel" = "IntegrationService"
+}
+
+# Один и тот же вид под разными родителями имеет разный набор: измерение регистра —
+# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя,
+# объединение молча разрешило бы Edit там, где платформа его не даёт.
+$script:nestedKindRightsByType = @{
+ "ExternalDataSource" = @{
+ "Dimension" = @("View")
+ "Resource" = @("View")
+ }
+}
+
+# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением —
+# отказ даёт отсутствие типа в $knownRights; здесь только причина для сообщения.
+$script:noRightsTypes = @(
+ "Enum","CommonModule","DefinedType","CommonPicture","CommonTemplate","Language",
+ "FunctionalOption","FunctionalOptionsParameter","EventSubscription","ScheduledJob",
+ "StyleItem","Style","SettingsStorage","XDTOPackage","WSReference","DocumentNumerator"
+)
# --- 2. Output helpers ---
@@ -179,6 +240,37 @@ function Is-NestedObject {
return ($name.Split(".").Count -ge 3)
}
+# Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным
+# (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца.
+function Get-NestedKind {
+ param([string]$name)
+ $parts = $name.Split(".")
+ if ($parts.Count -lt 3) { return $null }
+ return $parts[$parts.Count - 2]
+}
+
+function Get-NestedRights {
+ param([string]$objectType, [string]$kind)
+ if ($script:nestedKindRightsByType.ContainsKey($objectType) -and
+ $script:nestedKindRightsByType[$objectType].ContainsKey($kind)) {
+ return @($script:nestedKindRightsByType[$objectType][$kind])
+ }
+ if ($script:nestedKindRights.ContainsKey($kind)) { return @($script:nestedKindRights[$kind]) }
+ return $null
+}
+
+# Запрещённый тип и незнакомый тип — разные диагнозы: первый документирован, второй
+# скорее опечатка или пробел в таблице.
+function Format-TypeError {
+ param([string]$objName, [string]$objectType)
+ if ($script:noRightsTypes -contains $objectType) {
+ return "${objName}: тип '$objectType' не имеет прав в роли — блок нужно удалить"
+ }
+ $similar = Find-Similar -needle $objectType -haystack @($script:knownRights.Keys)
+ $sug = if ($similar.Count -gt 0) { " Возможно: $($similar -join ', ')?" } else { "" }
+ return "${objName}: неизвестный тип объекта '$objectType'.$sug"
+}
+
function Find-Similar {
param([string]$needle, [string[]]$haystack)
$result = @($haystack | Where-Object {
@@ -303,9 +395,16 @@ foreach ($obj in $objects) {
$objectType = Get-ObjectType $objName
$isNested = Is-NestedObject $objName
- # Check object type is known
- if (-not $isNested -and -not $script:knownRights.ContainsKey($objectType)) {
- Report-Warn "${objName}: unknown object type '$objectType'"
+ # Тип проверяется ВСЕГДА, включая вложенные пути: раньше `Enum.Х.Attribute.Y` не проверялся
+ # вообще, потому что ветка «unknown» стояла под `-not $isNested`.
+ $nestedKind = if ($isNested) { Get-NestedKind $objName } else { $null }
+ $typeKnown = $script:knownRights.ContainsKey($objectType)
+ if (-not $typeKnown) {
+ Report-Error (Format-TypeError $objName $objectType)
+ } elseif ($isNested -and $script:kindOwners.ContainsKey($nestedKind) -and $objectType -ne $script:kindOwners[$nestedKind]) {
+ Report-Error "${objName}: вид '$nestedKind' бывает только у $($script:kindOwners[$nestedKind])"
+ } elseif ($isNested -and $null -eq (Get-NestedRights $objectType $nestedKind)) {
+ Report-Error "${objName}: неизвестный вид вложенности '$nestedKind'"
}
# Check rights
@@ -345,27 +444,20 @@ foreach ($obj in $objects) {
$rightCount++
- # Validate right name
- if ($isNested) {
- if ($objName -match '\.Command\.') {
- if ($rName -notin $script:commandRights) {
- Report-Warn "${objName}: '$rName' not valid for commands (only: View)"
- }
- } elseif ($objName -match '\.IntegrationServiceChannel\.') {
- if ($rName -notin $script:channelRights) {
- Report-Warn "${objName}: '$rName' not valid for channels (only: Use)"
- }
- } else {
- if ($rName -notin $script:nestedRights) {
- Report-Warn "${objName}: '$rName' not valid for nested objects (only: View, Edit)"
- }
+ # Validate right name. Тип уже отвергнут выше — второй раз про него не пишем.
+ if (-not $typeKnown) {
+ # уже сообщено
+ } elseif ($isNested) {
+ $validNested = Get-NestedRights $objectType $nestedKind
+ if ($null -ne $validNested -and $rName -notin $validNested) {
+ Report-Error "${objName}: право '$rName' недопустимо для вида '$nestedKind' (допустимо: $($validNested -join ', '))"
}
- } elseif ($script:knownRights.ContainsKey($objectType)) {
+ } else {
$validRights = $script:knownRights[$objectType]
if ($rName -notin $validRights) {
$similar = Find-Similar -needle $rName -haystack $validRights
- $sugStr = if ($similar.Count -gt 0) { " Did you mean: $($similar -join ', ')?" } else { "" }
- Report-Warn "${objName}: unknown right '$rName'.$sugStr"
+ $sugStr = if ($similar.Count -gt 0) { " Возможно: $($similar -join ', ')?" } else { "" }
+ Report-Error "${objName}: право '$rName' не существует у типа '$objectType'.$sugStr"
}
}
}
diff --git a/.claude/skills/role-validate/scripts/role-validate.py b/.claude/skills/role-validate/scripts/role-validate.py
index 2836998a..93fe1915 100644
--- a/.claude/skills/role-validate/scripts/role-validate.py
+++ b/.claude/skills/role-validate/scripts/role-validate.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-# role-validate v1.2 — Validate 1C role Rights.xml structure
+# role-validate v1.3 — Validate 1C role Rights.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates role Rights.xml: root element, global flags, objects, rights, RLS, templates."""
import sys, os, argparse, re
@@ -145,11 +145,72 @@ KNOWN_RIGHTS = {
'IntegrationService': ['Use'],
'SessionParameter': ['Get', 'Set'],
'CommonAttribute': ['View', 'Edit'],
+ 'ExternalDataSource': [
+ 'Use', 'Administration', 'StandardAuthenticationChange',
+ 'SessionStandardAuthenticationChange', 'SessionOSAuthenticationChange',
+ ],
}
-NESTED_RIGHTS = ['View', 'Edit']
-CHANNEL_RIGHTS = ['Use']
-COMMAND_RIGHTS = ['View']
+# Виды вложенности (предпоследний сегмент пути) → допустимые права. Списки сняты с корпуса
+# типовых конфигураций и с выгрузки роли, где права проставлены по всему дереву редактора:
+# догадкам тут не место — закрытый список превращает промах в ложный отказ.
+NESTED_KIND_RIGHTS = {
+ 'Attribute': ['View', 'Edit'],
+ 'StandardAttribute': ['View', 'Edit'],
+ 'TabularSection': ['View', 'Edit'],
+ 'StandardTabularSection': ['View', 'Edit'],
+ 'Dimension': ['View', 'Edit'],
+ 'Resource': ['View', 'Edit'],
+ 'AccountingFlag': ['View', 'Edit'],
+ 'ExtDimensionAccountingFlag': ['View', 'Edit'],
+ 'AddressingAttribute': ['View', 'Edit'],
+ 'Field': ['View', 'Edit'],
+ 'Command': ['View'],
+ 'Subsystem': ['View'],
+ 'Operation': ['Use'],
+ 'Method': ['Use'],
+ 'IntegrationServiceChannel': ['Use'],
+ 'Recalculation': ['Read', 'Update'],
+ 'Cube': ['Read', 'View'],
+ 'DimensionTable': ['Read', 'View'],
+ 'Function': ['Use', 'View'],
+ 'Table': [
+ 'Read', 'Insert', 'Update', 'Delete', 'View', 'Edit', 'InputByString',
+ 'InteractiveInsert', 'InteractiveDelete',
+ ],
+}
+
+# Виды, существующие только у одного типа-родителя: без этой привязки
+# `Catalog.Товары.Field.Цена` прошёл бы как валидный вложенный объект.
+KIND_OWNERS = {
+ 'Table': 'ExternalDataSource',
+ 'Cube': 'ExternalDataSource',
+ 'Function': 'ExternalDataSource',
+ 'Field': 'ExternalDataSource',
+ 'DimensionTable': 'ExternalDataSource',
+ 'Recalculation': 'CalculationRegister',
+ 'Operation': 'WebService',
+ 'Method': 'HTTPService',
+ 'IntegrationServiceChannel': 'IntegrationService',
+}
+
+# Один и тот же вид под разными родителями имеет разный набор: измерение регистра —
+# View + Edit, измерение куба внешнего источника — только View. Объединять нельзя,
+# объединение молча разрешило бы Edit там, где платформа его не даёт.
+NESTED_KIND_RIGHTS_BY_TYPE = {
+ 'ExternalDataSource': {
+ 'Dimension': ['View'],
+ 'Resource': ['View'],
+ },
+}
+
+# Типы без прав в ролях (в дереве редактора ролей их нет). Таблица НЕ управляет поведением —
+# отказ даёт отсутствие типа в KNOWN_RIGHTS; здесь только причина для сообщения.
+NO_RIGHTS_TYPES = [
+ 'Enum', 'CommonModule', 'DefinedType', 'CommonPicture', 'CommonTemplate', 'Language',
+ 'FunctionalOption', 'FunctionalOptionsParameter', 'EventSubscription', 'ScheduledJob',
+ 'StyleItem', 'Style', 'SettingsStorage', 'XDTOPackage', 'WSReference', 'DocumentNumerator',
+]
def get_object_type(name):
@@ -163,6 +224,22 @@ def is_nested_object(name):
return name.count('.') >= 2
+def get_nested_kind(name):
+ """Вид вложенности — предпоследний сегмент: путь бывает и восьмисегментным
+ (ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П), считать от конца."""
+ parts = name.split('.')
+ if len(parts) < 3:
+ return None
+ return parts[-2]
+
+
+def get_nested_rights(object_type, kind):
+ by_type = NESTED_KIND_RIGHTS_BY_TYPE.get(object_type)
+ if by_type and kind in by_type:
+ return by_type[kind]
+ return NESTED_KIND_RIGHTS.get(kind)
+
+
def find_similar(needle, haystack):
result = []
needle_lower = needle.lower()
@@ -175,6 +252,16 @@ def find_similar(needle, haystack):
return result
+def format_type_error(obj_name, object_type):
+ """Запрещённый тип и незнакомый тип — разные диагнозы: первый документирован,
+ второй скорее опечатка или пробел в таблице."""
+ if object_type in NO_RIGHTS_TYPES:
+ return f"{obj_name}: тип '{object_type}' не имеет прав в роли — блок нужно удалить"
+ similar = find_similar(object_type, list(KNOWN_RIGHTS.keys()))
+ sug = f" Возможно: {', '.join(similar)}?" if similar else ''
+ return f"{obj_name}: неизвестный тип объекта '{object_type}'.{sug}"
+
+
def get_child_text(parent, local_name, ns):
"""Get text of first child element with given local name in namespace."""
for child in parent:
@@ -339,9 +426,16 @@ def main():
object_type = get_object_type(obj_name)
is_nested = is_nested_object(obj_name)
- # Check object type is known
- if not is_nested and object_type not in KNOWN_RIGHTS:
- report_warn(f"{obj_name}: unknown object type '{object_type}'")
+ # Тип проверяется ВСЕГДА, включая вложенные пути: раньше `Enum.Х.Attribute.Y`
+ # не проверялся вообще — ветка «unknown» стояла под `not is_nested`.
+ nested_kind = get_nested_kind(obj_name) if is_nested else None
+ type_known = object_type in KNOWN_RIGHTS
+ if not type_known:
+ report_error(format_type_error(obj_name, object_type))
+ elif is_nested and nested_kind in KIND_OWNERS and object_type != KIND_OWNERS[nested_kind]:
+ report_error(f"{obj_name}: вид '{nested_kind}' бывает только у {KIND_OWNERS[nested_kind]}")
+ elif is_nested and get_nested_rights(object_type, nested_kind) is None:
+ report_error(f"{obj_name}: неизвестный вид вложенности '{nested_kind}'")
# Check rights
for child in obj:
@@ -384,23 +478,19 @@ def main():
right_count += 1
- # Validate right name
- if is_nested:
- if '.Command.' in obj_name:
- if r_name not in COMMAND_RIGHTS:
- report_warn(f"{obj_name}: '{r_name}' not valid for commands (only: View)")
- elif '.IntegrationServiceChannel.' in obj_name:
- if r_name not in CHANNEL_RIGHTS:
- report_warn(f"{obj_name}: '{r_name}' not valid for channels (only: Use)")
- else:
- if r_name not in NESTED_RIGHTS:
- report_warn(f"{obj_name}: '{r_name}' not valid for nested objects (only: View, Edit)")
- elif object_type in KNOWN_RIGHTS:
+ # Validate right name. Тип уже отвергнут выше — второй раз про него не пишем.
+ if not type_known:
+ pass
+ elif is_nested:
+ valid_nested = get_nested_rights(object_type, nested_kind)
+ if valid_nested is not None and r_name not in valid_nested:
+ report_error(f"{obj_name}: право '{r_name}' недопустимо для вида '{nested_kind}' (допустимо: {', '.join(valid_nested)})")
+ else:
valid_rights = KNOWN_RIGHTS[object_type]
if r_name not in valid_rights:
similar = find_similar(r_name, valid_rights)
- sug_str = f' Did you mean: {", ".join(similar)}?' if similar else ''
- report_warn(f"{obj_name}: unknown right '{r_name}'.{sug_str}")
+ sug_str = f" Возможно: {', '.join(similar)}?" if similar else ''
+ report_error(f"{obj_name}: право '{r_name}' не существует у типа '{object_type}'.{sug_str}")
report_ok(f'{obj_count} objects, {right_count} rights')
if rls_count > 0:
diff --git a/docs/1c-role-spec.md b/docs/1c-role-spec.md
index 0efd7a6e..309b3bb6 100644
--- a/docs/1c-role-spec.md
+++ b/docs/1c-role-spec.md
@@ -542,18 +542,44 @@ Subsystem.Администрирование.Subsystem.Пользователи
| `SessionParameter` | Get, Set |
| `CommonAttribute` | View, Edit |
+#### ExternalDataSource
+
+Источник ошибки в прежних редакциях спецификации: внешние источники данных права **имеют** —
+узел «Внешние источники данных» есть в дереве редактора ролей. Значения ниже сняты с выгрузки
+роли со всеми проставленными правами (8.3.25).
+
+| Объект | Права |
+|--------|-------|
+| `ExternalDataSource.И` | Use, Administration, StandardAuthenticationChange, SessionStandardAuthenticationChange, SessionOSAuthenticationChange |
+| `ExternalDataSource.И.Table.Т` | Read, Insert, Update, Delete, View, Edit, InputByString, InteractiveInsert, InteractiveDelete |
+| `ExternalDataSource.И.Table.Т.Field.П` | View, Edit |
+| `ExternalDataSource.И.Cube.К` | Read, View |
+| `ExternalDataSource.И.Cube.К.Dimension.И` | View |
+| `ExternalDataSource.И.Cube.К.Resource.Р` | View |
+| `ExternalDataSource.И.Cube.К.DimensionTable.Т` | Read, View |
+| `ExternalDataSource.И.Cube.К.DimensionTable.Т.Field.П` | View, Edit |
+| `ExternalDataSource.И.Function.Ф` | Use, View |
+| `…Table.Т.Command.К`, `…Cube.К.Command.К`, `…DimensionTable.Т.Command.К` | View |
+
#### Типы объектов БЕЗ прав в ролях
-Следующие типы не фигурируют в Rights.xml (права не применимы или управляются иначе):
+Следующие типы не фигурируют в Rights.xml — в дереве редактора ролей их нет:
-- `Enum` (перечисления)
-- `FunctionalOption`
-- `DefinedType`
+- `Enum` (перечисления) — блок прав на перечисление приводит к зависанию загрузки
+ конфигурации в информационную базу: конфигуратор не завершается и не выдаёт сообщений
- `CommonModule`
+- `DefinedType`
- `CommonPicture`
- `CommonTemplate`
+- `Language`
+- `FunctionalOption`, `FunctionalOptionsParameter`
+- `EventSubscription`
+- `ScheduledJob`
+- `StyleItem`, `Style`
- `SettingsStorage`
-- `ExternalDataSource`
+- `XDTOPackage`
+- `WSReference`
+- `DocumentNumerator`
---
@@ -646,8 +672,21 @@ Subsystem.Администрирование.Subsystem.Пользователи
| `TabularSection.*.Attribute` | (все с TabularSection) | View, Edit |
| `Dimension` | InformationRegister, AccumulationRegister, AccountingRegister | View, Edit |
| `Resource` | InformationRegister, AccumulationRegister, AccountingRegister | View, Edit |
-| `Command` | Catalog, Document, DataProcessor, Report, *Register, DocumentJournal, ExchangePlan, BusinessProcess, Task | View |
-| `AddressingAttribute` | Task | View, Edit |
+| `Command` | Catalog, Document, DataProcessor, Report, *Register, DocumentJournal, ExchangePlan, BusinessProcess, Task, ExternalDataSource (Table/Cube/DimensionTable) | View |
+| `AddressingAttribute` | Task, BusinessProcess | View, Edit |
+| `StandardTabularSection` | Catalog, Document, ChartOf* | View, Edit |
+| `AccountingFlag`, `ExtDimensionAccountingFlag` | ChartOfAccounts | View, Edit |
+| `Subsystem` | Subsystem (вложенная) | View |
+| `Operation` | WebService | Use |
+| `URLTemplate.*.Method` | HTTPService | Use |
+| `IntegrationServiceChannel` | IntegrationService | Use |
+| `Recalculation` | CalculationRegister | Read, Update |
+| `Column` (графа журнала) | — | прав не имеет |
+| `Table`, `Cube`, `Function`, `Field`, `DimensionTable` | только ExternalDataSource | см. раздел ExternalDataSource |
+
+Виды, встречающиеся в выгрузках типовых конфигураций, сверены с корпусом
+(`acc`, `erp`, `ut`, `unf` — ~2750 ролей); внешние источники данных — с выгрузкой роли,
+где права проставлены по всему дереву.
---
diff --git a/tests/skills/cases/role-compile/basic-role.json b/tests/skills/cases/role-compile/basic-role.json
index 88463da6..31b39621 100644
--- a/tests/skills/cases/role-compile/basic-role.json
+++ b/tests/skills/cases/role-compile/basic-role.json
@@ -16,7 +16,7 @@
"input": {
"name": "Кладовщик",
"objects": [
- "Catalog.Товары: Read View"
+ "Catalog.Товары: Read, View"
]
},
"validatePath": "Roles/Кладовщик",
diff --git a/tests/skills/cases/role-compile/error-forbidden-type.json b/tests/skills/cases/role-compile/error-forbidden-type.json
new file mode 100644
index 00000000..72a127ae
--- /dev/null
+++ b/tests/skills/cases/role-compile/error-forbidden-type.json
@@ -0,0 +1,19 @@
+{
+ "name": "Отказ: тип без прав в ролях (Enum), файлы не созданы",
+ "input": {
+ "name": "Кладовщик",
+ "objects": [
+ "Enum.ЮрФизЛицо: Read",
+ "Перечисление.ЮрФизЛицо: Чтение"
+ ]
+ },
+ "expectError": "не имеет прав в роли",
+ "noSnapshot": "негативный кейс: навык не должен создать ни одного файла",
+ "expect": {
+ "filesAbsent": [
+ "Roles/Кладовщик.xml",
+ "Roles/Кладовщик/Ext/Rights.xml"
+ ],
+ "fileNotContains": { "file": "Configuration.xml", "text": "Кладовщик" }
+ }
+}
diff --git a/tests/skills/cases/role-compile/error-nested-alien-kind.json b/tests/skills/cases/role-compile/error-nested-alien-kind.json
new file mode 100644
index 00000000..59d971dc
--- /dev/null
+++ b/tests/skills/cases/role-compile/error-nested-alien-kind.json
@@ -0,0 +1,12 @@
+{
+ "name": "Отказ: вид вложенности не от того типа",
+ "input": {
+ "name": "Кладовщик",
+ "objects": ["Catalog.Товары.Field.Цена: View"]
+ },
+ "expectError": "бывает только у ExternalDataSource",
+ "noSnapshot": "негативный кейс: навык не должен создать ни одного файла",
+ "expect": {
+ "filesAbsent": ["Roles/Кладовщик/Ext/Rights.xml"]
+ }
+}
diff --git a/tests/skills/cases/role-compile/error-unknown-right.json b/tests/skills/cases/role-compile/error-unknown-right.json
new file mode 100644
index 00000000..c5a3bc59
--- /dev/null
+++ b/tests/skills/cases/role-compile/error-unknown-right.json
@@ -0,0 +1,12 @@
+{
+ "name": "Отказ: несуществующее право у известного типа",
+ "input": {
+ "name": "Кладовщик",
+ "objects": ["Catalog.Товары: Reed"]
+ },
+ "expectError": "право 'Reed' не существует у типа 'Catalog'",
+ "noSnapshot": "негативный кейс: навык не должен создать ни одного файла",
+ "expect": {
+ "filesAbsent": ["Roles/Кладовщик/Ext/Rights.xml"]
+ }
+}
diff --git a/tests/skills/cases/role-compile/error-unknown-type.json b/tests/skills/cases/role-compile/error-unknown-type.json
new file mode 100644
index 00000000..f54ca766
--- /dev/null
+++ b/tests/skills/cases/role-compile/error-unknown-type.json
@@ -0,0 +1,13 @@
+{
+ "name": "Отказ: незнакомый тип объекта с подсказкой",
+ "input": {
+ "name": "Кладовщик",
+ "objects": ["Catalogg.Товары: Read"]
+ },
+ "expectError": "неизвестный тип объекта 'Catalogg'",
+ "noSnapshot": "негативный кейс: навык не должен создать ни одного файла",
+ "expect": {
+ "stderrContains": "Возможно: Catalog?",
+ "filesAbsent": ["Roles/Кладовщик/Ext/Rights.xml"]
+ }
+}
diff --git a/tests/skills/cases/role-compile/nested-and-services.json b/tests/skills/cases/role-compile/nested-and-services.json
new file mode 100644
index 00000000..340a083b
--- /dev/null
+++ b/tests/skills/cases/role-compile/nested-and-services.json
@@ -0,0 +1,25 @@
+{
+ "name": "Позитив: операции сервисов, вложенные объекты и внешний источник данных",
+ "input": {
+ "name": "Интеграция",
+ "synonym": "Интеграция",
+ "objects": [
+ "WebService.Обмен.Operation.Загрузить: Use",
+ "HTTPService.ПередачаДанных.URLTemplate.Файлы.Method.get: Use",
+ "IntegrationService.Канал.IntegrationServiceChannel.Основной: Use",
+ "Catalog.Товары.Attribute.Цена: View, Edit",
+ "Subsystem.Продажи.Subsystem.Опт: View",
+ "CalculationRegister.Начисления.Recalculation.Перерасчет: Read, Update",
+ "ExternalDataSource.Источник: Use, Administration",
+ "ExternalDataSource.Источник.Table.Заказы: Read, View",
+ "ExternalDataSource.Источник.Table.Заказы.Field.Сумма: View, Edit",
+ "ExternalDataSource.Источник.Cube.Продажи.Dimension.Период: View"
+ ]
+ },
+ "expect": {
+ "files": [
+ "Roles/Интеграция/Ext/Rights.xml"
+ ]
+ },
+ "skipValidation": true
+}
diff --git a/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml
index ce904f5f..f0b9463c 100644
--- a/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml
+++ b/tests/skills/cases/role-compile/snapshots/basic-role/Roles/Кладовщик/Ext/Rights.xml
@@ -6,7 +6,11 @@
diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml
new file mode 100644
index 00000000..b243f6c7
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Configuration.xml
@@ -0,0 +1,252 @@
+
+
+
+
+
+ UUID-002
+ UUID-003
+
+
+ UUID-004
+ UUID-005
+
+
+ UUID-006
+ UUID-007
+
+
+ UUID-008
+ UUID-009
+
+
+ UUID-010
+ UUID-011
+
+
+ UUID-012
+ UUID-013
+
+
+ UUID-014
+ UUID-015
+
+
+
+ TestConfig
+
+
+ ru
+ TestConfig
+
+
+
+
+ Version8_3_24
+ ManagedApplication
+
+ PlatformApplication
+
+ Russian
+
+
+
+
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Biometrics
+ true
+
+
+ Location
+ false
+
+
+ BackgroundLocation
+ false
+
+
+ BluetoothPrinters
+ false
+
+
+ WiFiPrinters
+ false
+
+
+ Contacts
+ false
+
+
+ Calendars
+ false
+
+
+ PushNotifications
+ false
+
+
+ LocalNotifications
+ false
+
+
+ InAppPurchases
+ false
+
+
+ PersonalComputerFileExchange
+ false
+
+
+ Ads
+ false
+
+
+ NumberDialing
+ false
+
+
+ CallProcessing
+ false
+
+
+ CallLog
+ false
+
+
+ AutoSendSMS
+ false
+
+
+ ReceiveSMS
+ false
+
+
+ SMSLog
+ false
+
+
+ Camera
+ false
+
+
+ Microphone
+ false
+
+
+ MusicLibrary
+ false
+
+
+ PictureAndVideoLibraries
+ false
+
+
+ AudioPlaybackAndVibration
+ false
+
+
+ BackgroundAudioPlaybackAndVibration
+ false
+
+
+ InstallPackages
+ false
+
+
+ OSBackup
+ true
+
+
+ ApplicationUsageStatistics
+ false
+
+
+ BarcodeScanning
+ false
+
+
+ BackgroundAudioRecording
+ false
+
+
+ AllFilesAccess
+ false
+
+
+ Videoconferences
+ false
+
+
+ NFC
+ false
+
+
+ DocumentScanning
+ false
+
+
+ SpeechToText
+ false
+
+
+ Geofences
+ false
+
+
+ IncomingShareRequests
+ false
+
+
+ AllIncomingShareRequestsTypesProcessing
+ false
+
+
+
+
+
+ Normal
+
+
+ Language.Русский
+
+
+
+
+
+ Managed
+ NotAutoFree
+ DontUse
+ DontUse
+ TaxiEnableVersion8_2
+ DontUse
+ Version8_3_24
+
+
+
+ Русский
+ Интеграция
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml
new file mode 100644
index 00000000..3c1161b2
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Ext/ClientApplicationInterface.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ UUID-002
+
+
+
+
+ UUID-004
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml
new file mode 100644
index 00000000..37c60d78
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Languages/Русский.xml
@@ -0,0 +1,16 @@
+
+
+
+
+ Русский
+
+
+ ru
+ Русский
+
+
+
+ ru
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml
new file mode 100644
index 00000000..2df0275d
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция.xml
@@ -0,0 +1,15 @@
+
+
+
+
+ Интеграция
+
+
+ ru
+ Интеграция
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml
new file mode 100644
index 00000000..c0b3e410
--- /dev/null
+++ b/tests/skills/cases/role-compile/snapshots/nested-and-services/Roles/Интеграция/Ext/Rights.xml
@@ -0,0 +1,96 @@
+
+
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml b/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml
index ce904f5f..f0b9463c 100644
--- a/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml
+++ b/tests/skills/cases/role-compile/snapshots/synonym-rights/Roles/Кладовщик/Ext/Rights.xml
@@ -6,7 +6,11 @@
diff --git a/tests/skills/cases/role-compile/synonym-rights.json b/tests/skills/cases/role-compile/synonym-rights.json
index 5433c667..f170ed53 100644
--- a/tests/skills/cases/role-compile/synonym-rights.json
+++ b/tests/skills/cases/role-compile/synonym-rights.json
@@ -16,7 +16,7 @@
"input": {
"name": "Кладовщик",
"rights": [
- "Catalog.Товары: Read View"
+ "Catalog.Товары: Read, View"
]
},
"validatePath": "Roles/Кладовщик",
diff --git a/tests/skills/cases/role-validate/error-forbidden-type.json b/tests/skills/cases/role-validate/error-forbidden-type.json
new file mode 100644
index 00000000..ea7376d7
--- /dev/null
+++ b/tests/skills/cases/role-validate/error-forbidden-type.json
@@ -0,0 +1,13 @@
+{
+ "name": "Ошибка: блоки прав на типы без прав в ролях",
+ "setup": "fixture:no-rights-types",
+ "params": { "rightsPath": "Roles/РольВсеТипы/Ext/Rights.xml" },
+ "expectError": true,
+ "noSnapshot": "негативный кейс: навык ничего не пишет, проверяется текст и код возврата",
+ "expect": {
+ "stdoutContains": [
+ "Enum.ЮрФизЛицо: тип 'Enum' не имеет прав в роли",
+ "CommonModule.ОбщегоНазначения: тип 'CommonModule' не имеет прав в роли"
+ ]
+ }
+}
diff --git a/tests/skills/cases/role-validate/external-data-source.json b/tests/skills/cases/role-validate/external-data-source.json
new file mode 100644
index 00000000..12486638
--- /dev/null
+++ b/tests/skills/cases/role-validate/external-data-source.json
@@ -0,0 +1,10 @@
+{
+ "name": "Внешний источник данных: все уровни дерева проходят без замечаний",
+ "setup": "fixture:external-data-source",
+ "params": { "rightsPath": "Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml" },
+ "noSnapshot": "валидатор не пишет файлов, проверяется чистый вывод",
+ "expect": {
+ "stdoutContains": "Validation OK",
+ "stdoutNotContains": ["[WARN]", "[ERROR]"]
+ }
+}
diff --git a/tests/skills/cases/role-validate/fixtures/bad-root/BadRole/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/bad-root/Roles/BadRole/Ext/Rights.xml
similarity index 100%
rename from tests/skills/cases/role-validate/fixtures/bad-root/BadRole/Ext/Rights.xml
rename to tests/skills/cases/role-validate/fixtures/bad-root/Roles/BadRole/Ext/Rights.xml
diff --git a/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml
new file mode 100644
index 00000000..1bfcf022
--- /dev/null
+++ b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаВнешнийИсточник/Ext/Rights.xml
@@ -0,0 +1,205 @@
+
+
+ false
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml
new file mode 100644
index 00000000..ef824fe9
--- /dev/null
+++ b/tests/skills/cases/role-validate/fixtures/external-data-source/Roles/ПраваНаПерерасчет/Ext/Rights.xml
@@ -0,0 +1,64 @@
+
+
+ false
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml b/tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml
new file mode 100644
index 00000000..8ccabf5b
--- /dev/null
+++ b/tests/skills/cases/role-validate/fixtures/no-rights-types/Roles/РольВсеТипы/Ext/Rights.xml
@@ -0,0 +1,83 @@
+
+
+ false
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-validate/recalculation.json b/tests/skills/cases/role-validate/recalculation.json
new file mode 100644
index 00000000..8b7316de
--- /dev/null
+++ b/tests/skills/cases/role-validate/recalculation.json
@@ -0,0 +1,10 @@
+{
+ "name": "Перерасчёт регистра расчёта проходит без замечаний",
+ "setup": "fixture:external-data-source",
+ "params": { "rightsPath": "Roles/ПраваНаПерерасчет/Ext/Rights.xml" },
+ "noSnapshot": "валидатор не пишет файлов, проверяется чистый вывод",
+ "expect": {
+ "stdoutContains": "Validation OK",
+ "stdoutNotContains": ["[WARN]", "[ERROR]"]
+ }
+}
diff --git a/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml b/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml
index 9a1537d2..57bb47e8 100644
--- a/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml
+++ b/tests/skills/cases/role-validate/snapshots/valid-role/Roles/Тест/Ext/Rights.xml
@@ -3,4 +3,11 @@
false
true
false
+
\ No newline at end of file
diff --git a/tests/skills/cases/role-validate/valid-role.json b/tests/skills/cases/role-validate/valid-role.json
index 163bb3b9..7df0aece 100644
--- a/tests/skills/cases/role-validate/valid-role.json
+++ b/tests/skills/cases/role-validate/valid-role.json
@@ -16,13 +16,8 @@
"script": "role-compile/scripts/role-compile",
"input": {
"name": "Тест",
- "rights": [
- {
- "object": "Catalog.Товары",
- "rights": [
- "Read"
- ]
- }
+ "objects": [
+ "Catalog.Товары: Read"
]
},
"args": {
diff --git a/tests/skills/check-type-maps.mjs b/tests/skills/check-type-maps.mjs
index 52e6966b..5a9d57d5 100644
--- a/tests/skills/check-type-maps.mjs
+++ b/tests/skills/check-type-maps.mjs
@@ -60,8 +60,10 @@ const MAPS = [
// вокабуляры: значения — канонические имена, полнота не требуется
{
skill: 'role-compile', file: 'role-compile', kind: 'alias', py: 'TYPE_ALIASES', ps1: null,
- extraTargets: ['Configuration', 'Attribute', 'StandardAttribute', 'TabularSection',
- 'Dimension', 'Resource', 'Command', 'AddressingAttribute'],
+ // ExternalDataSource — объект метаданных с правами в ролях, но в таблице порядка
+ // ChildObjects его нет: позиция в порядке не измерена (в корпусе внешних источников нет).
+ extraTargets: ['Configuration', 'ExternalDataSource', 'Attribute', 'StandardAttribute',
+ 'TabularSection', 'Dimension', 'Resource', 'Command', 'AddressingAttribute'],
},
{ skill: 'interface-edit', file: 'interface-edit', kind: 'alias', py: 'TYPE_NORM_MAP', ps1: null },
{ skill: 'subsystem-edit', file: 'subsystem-edit', kind: 'alias', py: 'CONTENT_TYPE_MAP', ps1: null },
diff --git a/tests/skills/integration/build-config.test.mjs b/tests/skills/integration/build-config.test.mjs
index 81878a1e..258d0e77 100644
--- a/tests/skills/integration/build-config.test.mjs
+++ b/tests/skills/integration/build-config.test.mjs
@@ -238,11 +238,11 @@ export const steps = [
input: {
name: 'Кладовщик',
objects: [
- 'Catalog.Контрагенты: Read View',
- 'Catalog.Номенклатура: Read View',
- 'Document.ПриходнаяНакладная: Read View Add Update',
+ 'Catalog.Контрагенты: Read, View',
+ 'Catalog.Номенклатура: Read, View',
+ 'Document.ПриходнаяНакладная: Read, View, Insert, Update',
'AccumulationRegister.ОстаткиТоваров: Read',
- 'Report.ОстаткиТоваров: Use View',
+ 'Report.ОстаткиТоваров: Use, View',
],
},
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
diff --git a/tests/skills/integration/build-webtest-config.test.mjs b/tests/skills/integration/build-webtest-config.test.mjs
index 3f27e767..144b98dd 100644
--- a/tests/skills/integration/build-webtest-config.test.mjs
+++ b/tests/skills/integration/build-webtest-config.test.mjs
@@ -2027,20 +2027,20 @@ export const steps = [
input: {
name: 'Администратор',
objects: [
- 'Catalog.Организации: Read View Add Update Delete',
- 'Catalog.Контрагенты: Read View Add Update Delete',
- 'Catalog.КонтактныеЛица: Read View Add Update Delete',
- 'Catalog.Номенклатура: Read View Add Update Delete',
- 'Document.ПриходнаяНакладная: Read View Add Update Delete Posting UnPosting',
- 'InformationRegister.КурсыВалют: Read View Add Update Delete',
- 'Report.ОстаткиТоваров: Use View',
- 'DataProcessor.ДеревоНоменклатуры: Use View',
- 'DataProcessor.МножественныйВыбор: Use View',
- 'DataProcessor.БезшапочнаяТаблица: Use View',
- 'DataProcessor.МногострочнаяШапка: Use View',
- 'DataProcessor.ГруппыКолонок: Use View',
- 'DataProcessor.ПроверкаДоступности: Use View',
- 'DataProcessor.СтраницаНастроек: Use View',
+ 'Catalog.Организации: Read, View, Insert, Update, Delete',
+ 'Catalog.Контрагенты: Read, View, Insert, Update, Delete',
+ 'Catalog.КонтактныеЛица: Read, View, Insert, Update, Delete',
+ 'Catalog.Номенклатура: Read, View, Insert, Update, Delete',
+ 'Document.ПриходнаяНакладная: Read, View, Insert, Update, Delete, Posting, UndoPosting',
+ 'InformationRegister.КурсыВалют: Read, View, Update, Edit',
+ 'Report.ОстаткиТоваров: Use, View',
+ 'DataProcessor.ДеревоНоменклатуры: Use, View',
+ 'DataProcessor.МножественныйВыбор: Use, View',
+ 'DataProcessor.БезшапочнаяТаблица: Use, View',
+ 'DataProcessor.МногострочнаяШапка: Use, View',
+ 'DataProcessor.ГруппыКолонок: Use, View',
+ 'DataProcessor.ПроверкаДоступности: Use, View',
+ 'DataProcessor.СтраницаНастроек: Use, View',
],
},
args: { '-JsonPath': '{inputFile}', '-OutputDir': '{workDir}' },
diff --git a/tests/skills/runner.mjs b/tests/skills/runner.mjs
index b1f3ba2b..357dc291 100644
--- a/tests/skills/runner.mjs
+++ b/tests/skills/runner.mjs
@@ -411,7 +411,7 @@ function checkFileContains(workDir, spec, expectPresent) {
// который молча ничего не проверяет (так уже было с 9 кейсами meta-edit) —
// поэтому он ошибка, а не игнор.
const KNOWN_EXPECT_KEYS = new Set([
- 'files', 'stdoutContains', 'stdoutNotContains', 'stderrContains', 'preserves',
+ 'files', 'filesAbsent', 'stdoutContains', 'stdoutNotContains', 'stderrContains', 'preserves',
'fileContains', 'fileNotContains', 'filesEqual',
]);
@@ -841,6 +841,16 @@ async function runCaseAsync(testCase, opts) {
if (!stderr.includes(needle)) errors.push(`stderr does not contain "${needle}"`);
}
}
+ // Отсутствие файла — тоже утверждение, и нужно оно чаще всего НЕГАТИВНОМУ кейсу:
+ // «отказ произошёл до записи». В позитивной ветке (где живёт expect.files) такой
+ // проверки не было бы ровно там, где она единственная содержательная.
+ if (caseData.expect?.filesAbsent) {
+ const paths = Array.isArray(caseData.expect.filesAbsent)
+ ? caseData.expect.filesAbsent : [caseData.expect.filesAbsent];
+ for (const p of paths) {
+ if (existsSync(join(workDir, p))) errors.push(`File must not exist: ${p}`);
+ }
+ }
}
if (!caseData.expectError) {
if (caseData.expect?.preserves) {
@@ -1066,6 +1076,16 @@ function runCase(testCase, opts) {
if (!stderr.includes(needle)) errors.push(`stderr does not contain "${needle}"`);
}
}
+ // Отсутствие файла — тоже утверждение, и нужно оно чаще всего НЕГАТИВНОМУ кейсу:
+ // «отказ произошёл до записи». В позитивной ветке (где живёт expect.files) такой
+ // проверки не было бы ровно там, где она единственная содержательная.
+ if (caseData.expect?.filesAbsent) {
+ const paths = Array.isArray(caseData.expect.filesAbsent)
+ ? caseData.expect.filesAbsent : [caseData.expect.filesAbsent];
+ for (const p of paths) {
+ if (existsSync(join(workDir, p))) errors.push(`File must not exist: ${p}`);
+ }
+ }
}
if (!caseData.expectError) {