From 009656991f49f84223e9dad7465bae62c9b096d3 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Thu, 21 May 2026 17:42:52 +0300 Subject: [PATCH] =?UTF-8?q?feat(skd-compile):=20=D1=80=D0=B0=D1=81=D1=88?= =?UTF-8?q?=D0=B8=D1=80=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20=D1=81=D0=B8=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BA=D1=81=D0=B8=D1=81=20role=20=E2=80=94=20short?= =?UTF-8?q?hand=20+=20KV=20=D0=B1=D0=B5=D0=B7=20whitelist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse-RoleSpec (ps1+py): принимает string ("dim"/"flag1 flag2 K=V") / array / object - Parse-FieldShorthand: извлекает K=V из shorthand поля (regex \w+=\S+) - emit: токены → true; extras → VALUE (без whitelist; раньше принимались только accountTypeExpression и balanceGroup) - @period sugar поддерживает override через periodNumber/periodType KV - Fix имени: balanceGroup в JSON принимается как deprecated alias для balanceGroupName (в реальном XML 1С элемент называется balanceGroupName; старый код compile эмитил несуществующий — ни одного попадания в ERP-корпусе) - SKILL.md, docs/skd-dsl-spec.md: единое описание четырёх форм роли - v1.27 → v1.28 --- .claude/skills/skd-compile/SKILL.md | 9 +- .../skd-compile/scripts/skd-compile.ps1 | 97 ++++++++++++++----- .../skills/skd-compile/scripts/skd-compile.py | 81 +++++++++++----- docs/skd-dsl-spec.md | 45 ++++++--- 4 files changed, 168 insertions(+), 64 deletions(-) diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index 1937ecad..2952747a 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -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`. diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 743addd9..088211e2 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.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" @@ -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" foreach ($role in $f.roles) { if ($role -eq "period") { - # @period -> periodNumber + periodType (not ) - X "$indent`t`t1" - X "$indent`t`tMain" + # @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`t1" } + if (-not $ptInExtras) { X "$indent`t`tMain" } } else { X "$indent`t`ttrue" } } - if ($f["roleObj"]) { - $ro = $f["roleObj"] - if ($ro.accountTypeExpression) { - X "$indent`t`t$(Esc-Xml "$($ro.accountTypeExpression)")" - } - if ($ro.balanceGroup) { - X "$indent`t`t$(Esc-Xml "$($ro.balanceGroup)")" + if ($hasExtras) { + foreach ($k in $f["roleExtras"].Keys) { + X "$indent`t`t$(Esc-Xml "$($f["roleExtras"][$k])")" } } X "$indent`t" diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 63c33b5b..744ce9bc 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.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}') lines.append(f'{indent}\t{esc_xml(f["dataPath"])}') @@ -580,20 +616,21 @@ def emit_field(lines, field_def, indent): lines.append(f'{indent}\t') # 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') for role in f.get('roles', []): if role == 'period': - lines.append(f'{indent}\t\t1') - lines.append(f'{indent}\t\tMain') + # @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить. + if 'periodNumber' not in extras: + lines.append(f'{indent}\t\t1') + if 'periodType' not in extras: + lines.append(f'{indent}\t\tMain') else: lines.append(f'{indent}\t\ttrue') - if f.get('roleObj'): - ro = f['roleObj'] - if ro.get('accountTypeExpression'): - lines.append(f'{indent}\t\t{esc_xml(str(ro["accountTypeExpression"]))}') - if ro.get('balanceGroup'): - lines.append(f'{indent}\t\t{esc_xml(str(ro["balanceGroup"]))}') + for k, v in extras.items(): + lines.append(f'{indent}\t\t{esc_xml(str(v))}') lines.append(f'{indent}\t') # ValueType diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index f2b2aa37..9dd96e03 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -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}` | `true` | -| `@account` | `"role": "account"` или `{"account": true}` | `true` | -| `@balance` | `"role": "balance"` или `{"balance": true}` | `true` | -| `@period` | `"role": "period"` или `{"period": true}` | `1` + `Main` | +Принимаются четыре формы: -Объектная форма с доп. полями: ```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'а нет — любой `` принимается; перечисленные — типичные. `@period` — sugar для `periodNumber=1` + `periodType=Main` (можно переопределить явно). + +**XML-выход**: `true` для флагов; `VALUE` для KV. + +> Устаревший ключ `balanceGroup` в object-форме принимается как alias для `balanceGroupName` (имя элемента в реальном XML — `balanceGroupName`). + ### Ограничения | DSL shorthand | Объектная форма | XML useRestriction |