diff --git a/.claude/skills/skd-info/SKILL.md b/.claude/skills/skd-info/SKILL.md index 98a0f39d..1ab557d4 100644 --- a/.claude/skills/skd-info/SKILL.md +++ b/.claude/skills/skd-info/SKILL.md @@ -1,7 +1,7 @@ --- name: skd-info description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты -argument-hint: [-Mode overview|query|fields|links|totals|params|variant] [-Name ] +argument-hint: [-Mode overview|query|fields|links|totals|params|variant|trace] [-Name ] allowed-tools: - Bash - Read @@ -24,7 +24,7 @@ allowed-tools: | Параметр | Обязательный | По умолчанию | Описание | |--------------|:------------:|--------------|---------------------------------------------------| | TemplatePath | да | — | Путь к Template.xml или каталогу макета | -| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant` | +| Mode | нет | `overview` | Режим: `overview`, `query`, `fields`, `links`, `totals`, `params`, `variant`, `trace` | | Name | нет | — | Имя набора (query/fields), поля (totals) или варианта (variant) | | Batch | нет | `0` | Номер пакета запроса (0 = все). Только для query | | Limit | нет | `150` | Макс. строк вывода (защита от переполнения) | @@ -45,7 +45,10 @@ powershell.exe -NoProfile -File .claude\skills\skd-info\scripts\skd-info.ps1 -Te ... -Mode fields -Name НаборДанных1 ... -Mode links ... -Mode totals +... -Mode totals -Name СуммаНалога ... -Mode params +... -Mode trace -Name КоэффициентКи +... -Mode trace -Name "Коэффициент Ки" ... -Mode variant -Name Основной ... -Mode variant -Name 1 ``` @@ -210,6 +213,27 @@ DataParams: КлючВарианта="НоменклатураИЦены" Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None ``` +### trace — трассировка поля от заголовка до запроса + +Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов: + +``` +=== Trace: КоэффициентКи "Коэффициент Ки" === + +Dataset: (schema-level only, not in dataset fields) + +Calculated: + ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ + Operands: + КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query] + КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query] + +Resource: + [ОсновноеСредство] Сумма(КоэффициентКи) +``` + +Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс. + ## Разрешение пути - Прямой путь: `path/to/Template.xml` @@ -232,6 +256,7 @@ Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=Non - **Формулы и ресурсы**: totals для вычисляемых полей и ресурсов - **Программный вызов**: params для списка параметров - **Изменение вывода**: variant для структуры группировок и фильтров +- **Как считается колонка?**: trace для полной цепочки от заголовка до запроса ## Защита от переполнения diff --git a/.claude/skills/skd-info/scripts/skd-info.ps1 b/.claude/skills/skd-info/scripts/skd-info.ps1 index 26d855ea..91f01fe6 100644 --- a/.claude/skills/skd-info/scripts/skd-info.ps1 +++ b/.claude/skills/skd-info/scripts/skd-info.ps1 @@ -1,7 +1,7 @@ param( [Parameter(Mandatory=$true)] [string]$TemplatePath, - [ValidateSet("overview", "query", "fields", "links", "totals", "params", "variant")] + [ValidateSet("overview", "query", "fields", "links", "totals", "params", "variant", "trace")] [string]$Mode = "overview", [string]$Name, [int]$Batch = 0, @@ -516,6 +516,7 @@ if ($Mode -eq "overview") { } elseif ($variants.Count -gt 1) { $hints += "-Mode variant -Name variant structure (1..$($variants.Count))" } + $hints += "-Mode trace -Name trace field origin (by name or title)" $lines.Add("Next:") foreach ($h in $hints) { $lines.Add(" $h") } } @@ -1138,6 +1139,199 @@ elseif ($Mode -eq "variant") { } } +# ============================================================ +# MODE: trace +# ============================================================ +elseif ($Mode -eq "trace") { + + if (-not $Name) { + Write-Error "Trace mode requires -Name " + exit 1 + } + + # --- Build field index --- + + $dsFields = @{} # dataPath -> @{ datasets=@(); title="" } + $calcFields = @{} # dataPath -> @{ expression=""; title="" } + $resFields = @{} # dataPath -> @(@{ expression=""; group="" }) + $titleMap = @{} # title -> dataPath + + # Scan dataset fields (including nested Union items) + $dataSets = $root.SelectNodes("s:dataSet", $ns) + foreach ($ds in $dataSets) { + $dsName = $ds.SelectSingleNode("s:name", $ns).InnerText + $dsType = Get-DataSetType $ds + + foreach ($f in $ds.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp) { continue } + $dpStr = $dp.InnerText + if (-not $dsFields.ContainsKey($dpStr)) { + $dsFields[$dpStr] = @{ datasets = @(); title = "" } + } + $dsFields[$dpStr].datasets += "$dsName [$dsType]" + $titleNode = $f.SelectSingleNode("s:title", $ns) + if ($titleNode) { + $t = Get-MLText $titleNode + if ($t) { + if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } + if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + } + } + + if ($dsType -eq "Union") { + foreach ($subDs in $ds.SelectNodes("s:item", $ns)) { + $subName = $subDs.SelectSingleNode("s:name", $ns).InnerText + $subType = Get-DataSetType $subDs + foreach ($f in $subDs.SelectNodes("s:field", $ns)) { + $dp = $f.SelectSingleNode("s:dataPath", $ns) + if (-not $dp) { continue } + $dpStr = $dp.InnerText + if (-not $dsFields.ContainsKey($dpStr)) { + $dsFields[$dpStr] = @{ datasets = @(); title = "" } + } + $dsFields[$dpStr].datasets += "$subName [$subType]" + $titleNode = $f.SelectSingleNode("s:title", $ns) + if ($titleNode) { + $t = Get-MLText $titleNode + if ($t) { + if (-not $dsFields[$dpStr].title) { $dsFields[$dpStr].title = $t } + if (-not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + } + } + } + } + } + + # Scan calculated fields + foreach ($cf in $root.SelectNodes("s:calculatedField", $ns)) { + $dpStr = $cf.SelectSingleNode("s:dataPath", $ns).InnerText + $expr = $cf.SelectSingleNode("s:expression", $ns).InnerText + $cfTitle = $cf.SelectSingleNode("s:title", $ns) + $t = "" + if ($cfTitle) { $t = Get-MLText $cfTitle } + $calcFields[$dpStr] = @{ expression = $expr; title = $t } + if ($t -and -not $titleMap.ContainsKey($t)) { $titleMap[$t] = $dpStr } + } + + # Scan resources + foreach ($tf in $root.SelectNodes("s:totalField", $ns)) { + $dpStr = $tf.SelectSingleNode("s:dataPath", $ns).InnerText + $expr = $tf.SelectSingleNode("s:expression", $ns).InnerText + $grp = $tf.SelectSingleNode("s:group", $ns) + $groupStr = "(overall)" + if ($grp) { $groupStr = $grp.InnerText } + if (-not $resFields.ContainsKey($dpStr)) { $resFields[$dpStr] = @() } + $resFields[$dpStr] += @{ expression = $expr; group = $groupStr } + } + + # --- Resolve name: try dataPath, then exact title, then substring title --- + $targetPath = $Name + $knownPaths = @() + $knownPaths += $dsFields.Keys + $knownPaths += $calcFields.Keys + $knownPaths += $resFields.Keys + $isKnown = $knownPaths -contains $Name + + if (-not $isKnown) { + if ($titleMap.ContainsKey($Name)) { + $targetPath = $titleMap[$Name] + } else { + # Substring match in titles + $matchedTitle = $null + foreach ($key in $titleMap.Keys) { + if ($key -like "*$Name*") { + $matchedTitle = $key + break + } + } + if ($matchedTitle) { + $targetPath = $titleMap[$matchedTitle] + } else { + Write-Error "Field '$Name' not found by dataPath or title" + exit 1 + } + } + } + + # --- Build output --- + $title = "" + if ($calcFields.ContainsKey($targetPath) -and $calcFields[$targetPath].title) { + $title = $calcFields[$targetPath].title + } elseif ($dsFields.ContainsKey($targetPath) -and $dsFields[$targetPath].title) { + $title = $dsFields[$targetPath].title + } + $titleStr = if ($title) { " `"$title`"" } else { "" } + + $lines.Add("=== Trace: $targetPath$titleStr ===") + $lines.Add("") + + # Dataset origin + if ($dsFields.ContainsKey($targetPath)) { + $uniqueDs = $dsFields[$targetPath].datasets | Select-Object -Unique + $lines.Add("Dataset: $($uniqueDs -join ', ')") + } else { + $lines.Add("Dataset: (schema-level only, not in dataset fields)") + } + + # Calculated field + if ($calcFields.ContainsKey($targetPath)) { + $cf = $calcFields[$targetPath] + $lines.Add("") + $lines.Add("Calculated:") + foreach ($el in ($cf.expression -split "`n")) { $lines.Add(" $($el.TrimEnd())") } + + # Extract operands: find known field names in expression + $operands = @() + $allKnown = @() + $allKnown += $dsFields.Keys + $allKnown += $calcFields.Keys + $allKnown = $allKnown | Select-Object -Unique | Where-Object { $_ -ne $targetPath } + # Sort by length descending to match longer names first + $allKnown = $allKnown | Sort-Object -Property Length -Descending + + foreach ($fieldName in $allKnown) { + $escaped = [regex]::Escape($fieldName) + if ($cf.expression -match "(? calculated") + } elseif ($dsFields.ContainsKey($op)) { + $opDs = ($dsFields[$op].datasets | Select-Object -Unique) -join ", " + $lines.Add(" $op -> $opDs") + } else { + $lines.Add(" $op") + } + } + } + } + + # Resource + if ($resFields.ContainsKey($targetPath)) { + $lines.Add("") + $lines.Add("Resource:") + foreach ($r in $resFields[$targetPath]) { + $lines.Add(" [$($r.group)] $($r.expression)") + } + } + + # Simple dataset field, no calc/resource + if (-not $calcFields.ContainsKey($targetPath) -and -not $resFields.ContainsKey($targetPath)) { + if ($dsFields.ContainsKey($targetPath)) { + $lines.Add("") + $lines.Add("(direct dataset field, no calculated expression or resource)") + } + } +} + # --- Output --- $result = $lines.ToArray()