From 05fc7eba27e6bab2e25244e151f476a0ed591388 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Tue, 17 Mar 2026 20:06:02 +0300 Subject: [PATCH] feat(skd): support @file references for query text in skd-compile and skd-edit Allows using "@path/to/file.sql" instead of inline query text. Path resolved relative to definition file, then CWD; absolute paths supported. Closes #9 Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/skd-compile/SKILL.md | 13 +++++++++ .../skd-compile/scripts/skd-compile.ps1 | 29 +++++++++++++++++-- .../skills/skd-compile/scripts/skd-compile.py | 28 ++++++++++++++++-- .claude/skills/skd-edit/SKILL.md | 3 +- .claude/skills/skd-edit/scripts/skd-edit.ps1 | 28 ++++++++++++++++-- .claude/skills/skd-edit/scripts/skd-edit.py | 25 ++++++++++++++-- docs/skd-dsl-spec.md | 16 +++++++++- docs/skd-guide.md | 2 +- 8 files changed, 133 insertions(+), 11 deletions(-) diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index c677f2d1..2da95218 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -56,6 +56,8 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p { "name": "Продажи", "query": "ВЫБРАТЬ ...", "fields": [...] } ``` +Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD. + ### Поля — shorthand ``` @@ -199,6 +201,17 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p } ``` +### С запросом из внешнего файла (@file) + +```json +{ + "dataSets": [{ + "query": "@queries/sales.sql", + "fields": ["Номенклатура: СправочникСсылка.Номенклатура @dimension", "Количество: число(15,3)", "Сумма: число(15,2)"] + }] +} +``` + ### С ресурсами, параметрами и @autoDates ```json diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index 0b7f12f0..bb02e1ae 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.0 — Compile 1C DCS from JSON +# skd-compile v1.1 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -41,6 +41,9 @@ if (-not $def.dataSets -or $def.dataSets.Count -eq 0) { exit 1 } +# Base directory for resolving @file references in query +$script:queryBaseDir = if ($DefinitionFile) { [System.IO.Path]::GetDirectoryName($DefinitionFile) } else { (Get-Location).Path } + # --- 2. XML helpers --- $script:xml = New-Object System.Text.StringBuilder 16384 @@ -55,6 +58,27 @@ function Esc-Xml { return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') } +function Resolve-QueryValue { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) + } + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + exit 1 +} + function Emit-MLText { param([string]$tag, [string]$text, [string]$indent) X "$indent<$tag xsi:type=`"v8:LocalStringType`">" @@ -672,7 +696,8 @@ function Emit-DataSet { # Type-specific content if ($dsType -eq "DataSetQuery") { - X "$indent`t$(Esc-Xml "$($ds.query)")" + $queryText = Resolve-QueryValue "$($ds.query)" $script:queryBaseDir + X "$indent`t$(Esc-Xml $queryText)" if ($ds.autoFillFields -eq $false) { X "$indent`tfalse" } diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 73052656..551f152e 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.0 — Compile 1C DCS from JSON +# skd-compile v1.1 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -13,6 +13,25 @@ def esc_xml(s): return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') +def resolve_query_value(val, base_dir): + if not val.startswith("@"): + return val + file_path = val[1:] + if os.path.isabs(file_path): + candidates = [file_path] + else: + candidates = [ + os.path.join(base_dir, file_path), + os.path.join(os.getcwd(), file_path), + ] + for c in candidates: + if os.path.exists(c): + with open(c, 'r', encoding='utf-8-sig') as f: + return f.read().rstrip() + print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + sys.exit(1) + + def emit_mltext(lines, indent, tag, text): if not text: lines.append(f"{indent}<{tag}/>") @@ -530,7 +549,8 @@ def emit_data_set(lines, ds, indent, default_source): # Type-specific content if ds_type == 'DataSetQuery': - lines.append(f'{indent}\t{esc_xml(str(ds.get("query", "")))}') + query_text = resolve_query_value(str(ds.get("query", "")), query_base_dir) + lines.append(f'{indent}\t{esc_xml(query_text)}') if ds.get('autoFillFields') is False: lines.append(f'{indent}\tfalse') elif ds_type == 'DataSetObject': @@ -1351,6 +1371,10 @@ def main(): print("JSON must have at least one entry in 'dataSets'", file=sys.stderr) sys.exit(1) + # Base directory for resolving @file references in query + global query_base_dir + query_base_dir = os.path.dirname(def_file) if args.DefinitionFile else os.getcwd() + # --- 2. Resolve defaults --- # DataSources diff --git a/.claude/skills/skd-edit/SKILL.md b/.claude/skills/skd-edit/SKILL.md index 98a9f3dd..62e30aa7 100644 --- a/.claude/skills/skd-edit/SKILL.md +++ b/.claude/skills/skd-edit/SKILL.md @@ -130,6 +130,7 @@ Shorthand: `"Имя: ТЕКСТ_ЗАПРОСА"` или `"ТЕКСТ_ЗАПРО ``` "Доп: ВЫБРАТЬ 1 КАК Тест" "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура" +"Продажи: @queries/sales.sql" ``` `dataSource` берётся из первого существующего. Дубликат имени — предупреждение, пропуск. Не поддерживает пакетный режим (запрос может содержать `;;`). @@ -160,7 +161,7 @@ Shorthand: `"Параметр = значение [when условие] [for По ### set-query — заменить текст запроса -Не поддерживает пакетный режим. Value — полный текст запроса. +Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD. ### set-outputParameter — установить параметр вывода diff --git a/.claude/skills/skd-edit/scripts/skd-edit.ps1 b/.claude/skills/skd-edit/scripts/skd-edit.ps1 index 6d92c3e3..6aa6afe4 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.ps1 +++ b/.claude/skills/skd-edit/scripts/skd-edit.ps1 @@ -1,4 +1,4 @@ -# skd-edit v1.1 — Atomic 1C DCS editor +# skd-edit v1.2 — Atomic 1C DCS editor # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -47,6 +47,29 @@ function Esc-Xml { return $s.Replace('&','&').Replace('<','<').Replace('>','>').Replace('"','"') } +function Resolve-QueryValue { + param([string]$val, [string]$baseDir) + if (-not $val.StartsWith("@")) { return $val } + $filePath = $val.Substring(1) + if ([System.IO.Path]::IsPathRooted($filePath)) { + $candidates = @($filePath) + } else { + $candidates = @( + (Join-Path $baseDir $filePath), + (Join-Path (Get-Location).Path $filePath) + ) + } + foreach ($c in $candidates) { + if (Test-Path $c) { + return (Get-Content -Raw -Encoding UTF8 $c).TrimEnd() + } + } + Write-Error "Query file not found: $filePath (searched: $($candidates -join ', '))" + exit 1 +} + +$script:queryBaseDir = [System.IO.Path]::GetDirectoryName($resolvedPath) + # --- 2. Type system (copied from skd-compile) --- $script:typeSynonyms = New-Object System.Collections.Hashtable @@ -1712,7 +1735,7 @@ switch ($Operation) { } # InnerText setter handles XML escaping automatically - $queryEl.InnerText = $Value + $queryEl.InnerText = Resolve-QueryValue $Value $script:queryBaseDir Write-Host "[OK] Query replaced in dataset `"$dsName`"" } @@ -1813,6 +1836,7 @@ switch ($Operation) { $childIndent = Get-ChildIndent $root $parsed = Parse-DataSetShorthand $Value + $parsed.query = Resolve-QueryValue $parsed.query $script:queryBaseDir # Auto-name if empty if (-not $parsed.name) { diff --git a/.claude/skills/skd-edit/scripts/skd-edit.py b/.claude/skills/skd-edit/scripts/skd-edit.py index 85502ba7..d82586c2 100644 --- a/.claude/skills/skd-edit/scripts/skd-edit.py +++ b/.claude/skills/skd-edit/scripts/skd-edit.py @@ -1,4 +1,4 @@ -# skd-edit v1.1 — Atomic 1C DCS editor (Python port) +# skd-edit v1.2 — Atomic 1C DCS editor (Python port) # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import os @@ -78,6 +78,25 @@ def esc_xml(s): return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') +def resolve_query_value(val, base_dir): + if not val.startswith("@"): + return val + file_path = val[1:] + if os.path.isabs(file_path): + candidates = [file_path] + else: + candidates = [ + os.path.join(base_dir, file_path), + os.path.join(os.getcwd(), file_path), + ] + for c in candidates: + if os.path.exists(c): + with open(c, 'r', encoding='utf-8-sig') as f: + return f.read().rstrip() + print(f"Query file not found: {file_path} (searched: {', '.join(candidates)})", file=sys.stderr) + sys.exit(1) + + def new_uuid(): return str(uuid.uuid4()) @@ -94,6 +113,7 @@ if not os.path.exists(template_path): sys.exit(1) resolved_path = os.path.abspath(template_path) +query_base_dir = os.path.dirname(resolved_path) # ── 2. Type system ────────────────────────────────────────── @@ -1465,7 +1485,7 @@ elif operation == "set-query": if query_el is None: print(f"No element found in dataset '{ds_name}'", file=sys.stderr) sys.exit(1) - query_el.text = value_arg + query_el.text = resolve_query_value(value_arg, query_base_dir) print(f'[OK] Query replaced in dataset "{ds_name}"') elif operation == "set-outputParameter": @@ -1541,6 +1561,7 @@ elif operation == "add-dataSetLink": elif operation == "add-dataSet": child_indent = get_child_indent(xml_doc) parsed = parse_data_set_shorthand(value_arg) + parsed["query"] = resolve_query_value(parsed["query"], query_base_dir) if not parsed["name"]: count = sum(1 for ch in xml_doc if isinstance(ch.tag, str) and local_name(ch) == "dataSet" and etree.QName(ch.tag).namespace == SCH_NS) diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index 9a970d1d..4e419667 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -85,12 +85,26 @@ |------|---------|----------| | `name` | нет | Авто: "НаборДанных1"... | | `source` | нет | Имя dataSource (авто: первый) | -| `query` | да* | Текст запроса (DataSetQuery) | +| `query` | да* | Текст запроса (DataSetQuery). Поддерживает `@file` — см. ниже | | `objectName` | да* | Имя объекта (DataSetObject) | | `items` | да* | Вложенные наборы (DataSetUnion) | | `fields` | нет | Массив полей | | `autoFillFields` | нет | `false` — отключить автозаполнение (по умолчанию не выводится = true) | +### Ссылка на внешний файл запроса (@file) + +Вместо inline-текста запроса можно указать путь к внешнему файлу с префиксом `@`: + +```json +{ "query": "@queries/sales.sql" } +``` + +Порядок разрешения пути: +1. Абсолютный путь — используется как есть +2. Относительно директории JSON-файла определения +3. Относительно текущей рабочей директории (CWD) +4. Если файл не найден — ошибка компиляции + --- ## 4. Поля — shorthand и объектная форма diff --git a/docs/skd-guide.md b/docs/skd-guide.md index 656d121d..7b559490 100644 --- a/docs/skd-guide.md +++ b/docs/skd-guide.md @@ -125,7 +125,7 @@ Claude вызовет `/skd-info` (overview → trace → query → variant) и > поля Номенклатура, Количество, Сумма. Период — параметр. ``` -Claude сформирует JSON: +Claude сформирует JSON (запрос можно вынести в файл: `"query": "@queries/sales.sql"`): ```json { "dataSets": [{