Add trace mode for field origin analysis

New mode traces a field from title/name to its full origin:
dataset fields, calculated expression with operands, resource
formulas. Searches by dataPath, exact title, or title substring.
Collapses 5-7 manual calls into one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-10 18:34:12 +03:00
parent 941fa73803
commit 21dded4d1c
2 changed files with 222 additions and 3 deletions
+27 -2
View File
@@ -1,7 +1,7 @@
---
name: skd-info
description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|totals|params|variant] [-Name <dataset|variant>]
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|totals|params|variant|trace] [-Name <dataset|variant|field>]
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 для полной цепочки от заголовка до запроса
## Защита от переполнения
+195 -1
View File
@@ -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 <N> variant structure (1..$($variants.Count))"
}
$hints += "-Mode trace -Name <f> 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 <field_name_or_title>"
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 "(?<![а-яА-ЯёЁa-zA-Z0-9_.])$escaped(?![а-яА-ЯёЁa-zA-Z0-9_.])") {
$operands += $fieldName
}
}
if ($operands.Count -gt 0) {
$lines.Add(" Operands:")
foreach ($op in $operands) {
if ($calcFields.ContainsKey($op)) {
$lines.Add(" $op -> 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()