feat(skd-compile): расширенный синтаксис role — shorthand + KV без whitelist

- 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
This commit is contained in:
Nick Shirokov
2026-05-21 17:42:52 +03:00
parent 8cf29c601e
commit 009656991f
4 changed files with 168 additions and 64 deletions
+8 -1
View File
@@ -94,7 +94,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -V
Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу.
Роли: `@dimension`, `@account`, `@balance`, `@period`.
Роли (shorthand или объект):
- `@`-флаги: `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`
- KV: `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType`
```
"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
```
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
@@ -1,4 +1,4 @@
# skd-compile v1.27 — Compile 1C DCS from JSON
# skd-compile v1.28 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -340,6 +340,7 @@ function Parse-FieldShorthand {
$result = @{
dataPath = ""; field = ""; title = ""; type = ""
roles = @(); restrict = @(); appearance = [ordered]@{}
roleExtras = [ordered]@{}
}
# Extract @roles
@@ -356,6 +357,11 @@ function Parse-FieldShorthand {
}
$s = [regex]::Replace($s, '\s*#\w+', '')
# Extract role kv=value (e.g. balanceGroupName=Сумма balanceType=OpeningBalance)
$kvMatches = [regex]::Matches($s, '(\w+)=(\S+)')
foreach ($m in $kvMatches) { $result.roleExtras[$m.Groups[1].Value] = $m.Groups[2].Value }
$s = [regex]::Replace($s, '\s*\w+=\S+', '')
# Split name: type
$s = $s.Trim()
if ($s.Contains(':')) {
@@ -370,6 +376,55 @@ function Parse-FieldShorthand {
return $result
}
# Universal role spec parser: string / array / object / null
# Returns @{ tokens = @(...); extras = [ordered]@{...} }
function Parse-RoleSpec {
param($spec)
$tokens = @()
$extras = [ordered]@{}
if ($null -ne $spec) {
if ($spec -is [string]) {
if ($spec -notmatch '\s' -and $spec -notmatch '=') {
$tokens += $spec
} else {
$s = $spec.Trim()
foreach ($m in [regex]::Matches($s, '@(\w+)')) { $tokens += $m.Groups[1].Value }
$s = [regex]::Replace($s, '\s*@\w+', '').Trim()
foreach ($m in [regex]::Matches($s, '(\w+)=(\S+)')) { $extras[$m.Groups[1].Value] = $m.Groups[2].Value }
}
} elseif ($spec -is [array] -or $spec -is [System.Collections.IList]) {
foreach ($t in $spec) { $tokens += "$t" }
} elseif ($spec.PSObject -and $spec.PSObject.Properties) {
foreach ($prop in $spec.PSObject.Properties) {
$val = $prop.Value
if ($val -is [bool]) {
if ($val) { $tokens += $prop.Name }
} elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) {
$extras[$prop.Name] = "$val"
}
}
} elseif ($spec -is [hashtable] -or $spec -is [System.Collections.IDictionary]) {
foreach ($k in $spec.Keys) {
$val = $spec[$k]
if ($val -is [bool]) {
if ($val) { $tokens += "$k" }
} elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) {
$extras["$k"] = "$val"
}
}
}
}
# Deprecated alias: balanceGroup → balanceGroupName (старое имя в коде compile, в реальном XML — Name)
if ($extras.Contains('balanceGroup') -and -not $extras.Contains('balanceGroupName')) {
$extras['balanceGroupName'] = $extras['balanceGroup']
$extras.Remove('balanceGroup')
}
return @{ tokens = $tokens; extras = $extras }
}
# --- 6. Total field shorthand parser ---
function Parse-TotalShorthand {
@@ -687,18 +742,13 @@ function Emit-Field {
roles = @()
restrict = @()
appearance = [ordered]@{}
roleExtras = [ordered]@{}
}
# Parse role
# Parse role (string shorthand / array / object — единый формат с /skd-edit set-field-role)
if ($fieldDef.role) {
if ($fieldDef.role -is [string]) {
$f.roles = @($fieldDef.role)
} else {
# Object form — collect truthy keys
$roleObj = $fieldDef.role
foreach ($prop in $roleObj.PSObject.Properties) {
if ($prop.Value -eq $true) { $f.roles += $prop.Name }
}
}
$parsed = Parse-RoleSpec $fieldDef.role
$f.roles = $parsed.tokens
$f.roleExtras = $parsed.extras
}
# Parse restrictions
if ($fieldDef.restrict) {
@@ -717,10 +767,6 @@ function Emit-Field {
if ($fieldDef.attrRestrict) {
$f["attrRestrict"] = @($fieldDef.attrRestrict)
}
# role object extras
if ($fieldDef.role -and $fieldDef.role -isnot [string]) {
$f["roleObj"] = $fieldDef.role
}
}
X "$indent<field xsi:type=`"DataSetFieldField`">"
@@ -761,24 +807,23 @@ function Emit-Field {
}
# Role
if ($f.roles.Count -gt 0 -or $f["roleObj"]) {
$hasExtras = $f["roleExtras"] -and $f["roleExtras"].Count -gt 0
if ($f.roles.Count -gt 0 -or $hasExtras) {
X "$indent`t<role>"
foreach ($role in $f.roles) {
if ($role -eq "period") {
# @period -> periodNumber + periodType (not <dcscom:period>)
X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>"
X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>"
# @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить.
$pnInExtras = $hasExtras -and $f["roleExtras"].Contains('periodNumber')
$ptInExtras = $hasExtras -and $f["roleExtras"].Contains('periodType')
if (-not $pnInExtras) { X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>" }
if (-not $ptInExtras) { X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>" }
} else {
X "$indent`t`t<dcscom:$role>true</dcscom:$role>"
}
}
if ($f["roleObj"]) {
$ro = $f["roleObj"]
if ($ro.accountTypeExpression) {
X "$indent`t`t<dcscom:accountTypeExpression>$(Esc-Xml "$($ro.accountTypeExpression)")</dcscom:accountTypeExpression>"
}
if ($ro.balanceGroup) {
X "$indent`t`t<dcscom:balanceGroup>$(Esc-Xml "$($ro.balanceGroup)")</dcscom:balanceGroup>"
if ($hasExtras) {
foreach ($k in $f["roleExtras"].Keys) {
X "$indent`t`t<dcscom:$k>$(Esc-Xml "$($f["roleExtras"][$k])")</dcscom:$k>"
}
}
X "$indent`t</role>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-compile v1.26 — Compile 1C DCS from JSON
# skd-compile v1.28 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -218,6 +218,7 @@ def parse_field_shorthand(s):
result = {
'dataPath': '', 'field': '', 'title': '', 'type': '',
'roles': [], 'restrict': [], 'appearance': {},
'roleExtras': {},
}
# Extract @roles
@@ -232,6 +233,11 @@ def parse_field_shorthand(s):
result['restrict'].append(m)
s = re.sub(r'\s*#\w+', '', s)
# Extract role kv=value (e.g. balanceGroupName=Сумма)
for m in re.finditer(r'(\w+)=(\S+)', s):
result['roleExtras'][m.group(1)] = m.group(2)
s = re.sub(r'\s*\w+=\S+', '', s)
# Split name: type
s = s.strip()
if ':' in s:
@@ -245,6 +251,42 @@ def parse_field_shorthand(s):
return result
# Universal role spec parser: string / list / dict / None
# Returns {'tokens': [...], 'extras': {...}}
def parse_role_spec(spec):
tokens = []
extras = {}
if spec is None:
pass
elif isinstance(spec, str):
if ' ' not in spec and '=' not in spec:
tokens.append(spec)
else:
s = spec.strip()
for m in re.finditer(r'@(\w+)', s):
tokens.append(m.group(1))
s = re.sub(r'\s*@\w+', '', s).strip()
for m in re.finditer(r'(\w+)=(\S+)', s):
extras[m.group(1)] = m.group(2)
elif isinstance(spec, list):
for t in spec:
tokens.append(str(t))
elif isinstance(spec, dict):
for k, v in spec.items():
if isinstance(v, bool):
if v:
tokens.append(k)
elif isinstance(v, (int, float, str)):
extras[k] = str(v)
# Deprecated alias: balanceGroup → balanceGroupName
if 'balanceGroup' in extras and 'balanceGroupName' not in extras:
extras['balanceGroupName'] = extras.pop('balanceGroup')
return {'tokens': tokens, 'extras': extras}
# --- Total field shorthand parser ---
def parse_total_shorthand(s):
@@ -523,16 +565,13 @@ def emit_field(lines, field_def, indent):
'roles': [],
'restrict': [],
'appearance': {},
'roleExtras': {},
}
# Parse role
if field_def.get('role'):
if isinstance(field_def['role'], str):
f['roles'] = [field_def['role']]
else:
# Object form -- collect truthy keys
for k, v in field_def['role'].items():
if v is True:
f['roles'].append(k)
# Parse role (string shorthand / list / dict — единый формат с /skd-edit set-field-role)
if field_def.get('role') is not None:
parsed = parse_role_spec(field_def['role'])
f['roles'] = parsed['tokens']
f['roleExtras'] = parsed['extras']
# Parse restrictions
if field_def.get('restrict'):
f['restrict'] = list(field_def['restrict'])
@@ -545,9 +584,6 @@ def emit_field(lines, field_def, indent):
# attrRestrict
if field_def.get('attrRestrict'):
f['attrRestrict'] = list(field_def['attrRestrict'])
# role object extras
if field_def.get('role') and not isinstance(field_def['role'], str):
f['roleObj'] = field_def['role']
lines.append(f'{indent}<field xsi:type="DataSetFieldField">')
lines.append(f'{indent}\t<dataPath>{esc_xml(f["dataPath"])}</dataPath>')
@@ -580,20 +616,21 @@ def emit_field(lines, field_def, indent):
lines.append(f'{indent}\t</attributeUseRestriction>')
# Role
if (f.get('roles') and len(f['roles']) > 0) or f.get('roleObj'):
extras = f.get('roleExtras') or {}
has_extras = len(extras) > 0
if (f.get('roles') and len(f['roles']) > 0) or has_extras:
lines.append(f'{indent}\t<role>')
for role in f.get('roles', []):
if role == 'period':
lines.append(f'{indent}\t\t<dcscom:periodNumber>1</dcscom:periodNumber>')
lines.append(f'{indent}\t\t<dcscom:periodType>Main</dcscom:periodType>')
# @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить.
if 'periodNumber' not in extras:
lines.append(f'{indent}\t\t<dcscom:periodNumber>1</dcscom:periodNumber>')
if 'periodType' not in extras:
lines.append(f'{indent}\t\t<dcscom:periodType>Main</dcscom:periodType>')
else:
lines.append(f'{indent}\t\t<dcscom:{role}>true</dcscom:{role}>')
if f.get('roleObj'):
ro = f['roleObj']
if ro.get('accountTypeExpression'):
lines.append(f'{indent}\t\t<dcscom:accountTypeExpression>{esc_xml(str(ro["accountTypeExpression"]))}</dcscom:accountTypeExpression>')
if ro.get('balanceGroup'):
lines.append(f'{indent}\t\t<dcscom:balanceGroup>{esc_xml(str(ro["balanceGroup"]))}</dcscom:balanceGroup>')
for k, v in extras.items():
lines.append(f'{indent}\t\t<dcscom:{k}>{esc_xml(str(v))}</dcscom:{k}>')
lines.append(f'{indent}\t</role>')
# ValueType
+30 -15
View File
@@ -124,7 +124,8 @@
"Организация: CatalogRef.Организации @dimension",
"Служебное: string #noFilter #noOrder",
"Счёт: CatalogRef.Хозрасчетный @account",
"Сумма: decimal(15,2) @balance"
"Сумма: decimal(15,2) @balance",
"СуммаНач: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
]
```
@@ -146,9 +147,9 @@
### Парсинг shorthand
1. Разделить по пробелам; найти `@`-роли и `#`-ограничения
1. Извлечь `@`-роли (regex `@(\w+)`), `#`-ограничения (`#(\w+)`), KV-пары роли (`(\w+)=(\S+)`)
2. Остаток до первого `:``dataPath``field` по умолчанию)
3. После `:` до `@`/`#` — тип
3. После `:` — тип
### Типы
@@ -192,22 +193,36 @@
### Роли
| DSL shorthand | Объектная форма | XML |
|---------------|----------------|-----|
| `@dimension` | `"role": "dimension"` или `{"dimension": true}` | `<dcscom:dimension>true</dcscom:dimension>` |
| `@account` | `"role": "account"` или `{"account": true}` | `<dcscom:account>true</dcscom:account>` |
| `@balance` | `"role": "balance"` или `{"balance": true}` | `<dcscom:balance>true</dcscom:balance>` |
| `@period` | `"role": "period"` или `{"period": true}` | `<dcscom:periodNumber>1</dcscom:periodNumber>` + `<dcscom:periodType>Main</dcscom:periodType>` |
Принимаются четыре формы:
Объектная форма с доп. полями:
```json
"role": {
"account": true,
"accountTypeExpression": "Счёт.ВидСчёта",
"balanceGroup": "/Остатки"
}
"role": "dimension" // одиночный флаг
"role": ["dimension", "required"] // массив флагов
"role": "balance balanceGroupName=Сумма balanceType=OpeningBalance" // shorthand
"role": { "balance": true, "balanceGroupName": "Сумма", "balanceType": "OpeningBalance" }
```
Shorthand-формат может быть встроен прямо в shorthand поля:
```
"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
```
**Парсинг shorthand**: `@(\w+)` → boolean флаги; `(\w+)=(\S+)` → строковые KV; остаток — `dataPath[: type]`.
**Поддерживаемые ключи**:
| Категория | Ключи |
|-----------|-------|
| `@`-флаги (boolean) | `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues` |
| Строковые KV | `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType` |
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 |