diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 index 3ea0fb637..946f146ee 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.ps1 @@ -1,4 +1,4 @@ -# db-load-cf v1.18 — Load 1C configuration from CF file +# db-load-cf v1.19 — Load 1C configuration from CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -303,6 +303,17 @@ Assert-InfoBaseExists $InfoBasePath # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём, # что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} function Test-SamePath { param([string]$A, [string]$B) if (-not $A -or -not $B) { return $false } @@ -446,18 +457,6 @@ function Write-PlatformOutput { } -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} - # Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое # не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации. # diff --git a/.claude/skills/db-load-cf/scripts/db-load-cf.py b/.claude/skills/db-load-cf/scripts/db-load-cf.py index 069aa8efd..c3f14d579 100644 --- a/.claude/skills/db-load-cf/scripts/db-load-cf.py +++ b/.claude/skills/db-load-cf/scripts/db-load-cf.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-load-cf v1.18 — Load 1C configuration from CF file +# db-load-cf v1.19 — Load 1C configuration from CF file # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse diff --git a/.claude/skills/db-repo/scripts/db-repo.ps1 b/.claude/skills/db-repo/scripts/db-repo.ps1 index 8c04efe95..99166c5b7 100644 --- a/.claude/skills/db-repo/scripts/db-repo.ps1 +++ b/.claude/skills/db-repo/scripts/db-repo.ps1 @@ -1,4 +1,4 @@ -# db-repo v1.15 — 1C configuration repository operations +# db-repo v1.16 — 1C configuration repository operations # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима). <# @@ -320,6 +320,46 @@ function Assert-InfoBaseExists { Assert-InfoBaseExists $InfoBasePath +# --- Запись базы в .v8-project.json --- +# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт +# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём, +# что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} +function Test-SamePath { + param([string]$A, [string]$B) + if (-not $A -or -not $B) { return $false } + try { + $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') + $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') + return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) + } catch { return $false } +} +function Find-ProjectDatabase { + # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. + $pf = Find-V8Project (Get-Location).Path + if (-not $pf) { return $null } + try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } + if (-not $proj.databases) { return $null } + foreach ($db in $proj.databases) { + if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } + if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { + if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and + $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } + } + } + return $null +} + # --- Resolve V8Path --- function Find-ProjectV8Path { # v8path записи базы сильнее корневого: в одном проекте базы живут на разных версиях @@ -503,47 +543,6 @@ function Invoke-PlatformProcess { return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode } } -# --- Запись базы в .v8-project.json --- -# Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт -# сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём, -# что в cf-edit.ps1 (сопоставление по configSrc). -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} -function Test-SamePath { - param([string]$A, [string]$B) - if (-not $A -or -not $B) { return $false } - try { - $na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/') - $nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/') - return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase) - } catch { return $false } -} - -function Find-ProjectDatabase { - # Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена. - $pf = Find-V8Project (Get-Location).Path - if (-not $pf) { return $null } - try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null } - if (-not $proj.databases) { return $null } - foreach ($db in $proj.databases) { - if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db } - if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) { - if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and - $db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db } - } - } - return $null -} - function Resolve-RepositorySettings { # Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра. $dbRec = Find-ProjectDatabase diff --git a/.claude/skills/db-repo/scripts/db-repo.py b/.claude/skills/db-repo/scripts/db-repo.py index 58bd50b8c..91ed4b196 100644 --- a/.claude/skills/db-repo/scripts/db-repo.py +++ b/.claude/skills/db-repo/scripts/db-repo.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# db-repo v1.15 — 1C configuration repository operations +# db-repo v1.16 — 1C configuration repository operations # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: движок только 1cv8 — ibcmd работу с хранилищем не поддерживает (нет такого режима). """Работа с хранилищем конфигурации 1С. diff --git a/.claude/skills/epf-build/scripts/epf-build.ps1 b/.claude/skills/epf-build/scripts/epf-build.ps1 index d9a83db2d..6fe8a3d35 100644 --- a/.claude/skills/epf-build/scripts/epf-build.ps1 +++ b/.claude/skills/epf-build/scripts/epf-build.ps1 @@ -1,4 +1,4 @@ -# epf-build v1.18 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.19 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # NB: *nix-раскладку платформы (/opt/1cv8//1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется. <# @@ -280,6 +280,17 @@ Assert-InfoBaseExists $InfoBasePath # Модель не передаёт ни путь к платформе конкретной базы, ни реквизиты хранилища: скрипт # сопоставляет параметры соединения с записью в databases[] и берёт их оттуда. Тот же приём, # что в cf-edit.ps1 (сопоставление по configSrc). +function Find-V8Project([string]$startDir) { + $d = $startDir + for ($i = 0; $i -lt 20 -and $d; $i++) { + $pj = Join-Path $d ".v8-project.json" + if (Test-Path $pj) { return $pj } + $parent = [System.IO.Path]::GetDirectoryName($d) + if ($parent -eq $d) { break } + $d = $parent + } + return $null +} function Test-SamePath { param([string]$A, [string]$B) if (-not $A -or -not $B) { return $false } @@ -430,18 +441,6 @@ function Test-OutputNonEmpty { return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0) } -function Find-V8Project([string]$startDir) { - $d = $startDir - for ($i = 0; $i -lt 20 -and $d; $i++) { - $pj = Join-Path $d ".v8-project.json" - if (Test-Path $pj) { return $pj } - $parent = [System.IO.Path]::GetDirectoryName($d) - if ($parent -eq $d) { break } - $d = $parent - } - return $null -} - # --- Проверка исходников платформой --- # Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML # и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии diff --git a/.claude/skills/epf-build/scripts/epf-build.py b/.claude/skills/epf-build/scripts/epf-build.py index 3f363588f..c91937495 100644 --- a/.claude/skills/epf-build/scripts/epf-build.py +++ b/.claude/skills/epf-build/scripts/epf-build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# epf-build v1.18 — Build external data processor or report (EPF/ERF) from XML sources +# epf-build v1.19 — Build external data processor or report (EPF/ERF) from XML sources # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse diff --git a/tests/skills/cases/db-repo/v8path-from-database.json b/tests/skills/cases/db-repo/v8path-from-database.json new file mode 100644 index 000000000..cba5c74aa --- /dev/null +++ b/tests/skills/cases/db-repo/v8path-from-database.json @@ -0,0 +1,33 @@ +{ + "name": "Платформа берётся из v8path записи базы", + "fakePlatform": { + "log": "---- Начало операции с хранилищем конфигурации ----\r\nОбъект не был изменен: Справочник.Номенклатура\r\n---- Операция с хранилищем конфигурации завершена ----\r\nПомещение изменений объектов в хранилище успешно завершено\r\n" + }, + "preRun": [ + { + "writeFile": { + "path": ".v8-project.json", + "content": { + "v8path": "{workDir}/nowhere/1cv8.exe", + "databases": [ + { "id": "dev", "name": "Dev", "type": "file", "path": "{workDir}/ib", "v8path": "{fakePlatform}" } + ] + } + } + } + ], + "args_extra": [ + "-InfoBasePath", "{workDir}/ib", + "-RepositoryPath", "{workDir}/repo", + "-RepositoryUser", "Admin", + "-Command", "commit", + "-Objects", "Справочник.Номенклатура", + "-Comment", "x" + ], + "cwd": "workDir", + "expect": { + "stdoutContains": "Новая версия в хранилище НЕ создана", + "stdoutNotContains": "nowhere" + }, + "noSnapshot": "проверяется выбор платформы по stdout, файлов на выходе нет" +} diff --git a/tests/skills/check-all.mjs b/tests/skills/check-all.mjs index 0d4279d94..29d6b1a26 100644 --- a/tests/skills/check-all.mjs +++ b/tests/skills/check-all.mjs @@ -14,6 +14,7 @@ const GUARDS = [ ['check-uuid-invariant.mjs', 'сохранение uuid объекта и сущностей при правке'], ['check-minimal-diff.mjs', 'правка роли: диф только там, где просили'], ['check-inline-drift.mjs', 'общие inline-реализации: копии совпадают с эталонами'], + ['check-ps-define-before-call.mjs', 'порядок в .ps1: функция объявлена выше точки вызова'], ['check-type-maps.mjs', 'карты типов метаданных: согласованы со спецификацией'], ['check-format-versions.mjs', 'проверенный диапазон версий формата: согласован со спецификацией'], ['check-form-purposes.mjs', 'назначения форм в form-add: согласованы со спецификацией и между портами'], diff --git a/tests/skills/check-ps-define-before-call.mjs b/tests/skills/check-ps-define-before-call.mjs new file mode 100644 index 000000000..2b5147ece --- /dev/null +++ b/tests/skills/check-ps-define-before-call.mjs @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// PowerShell ищет функцию в момент ВЫЗОВА и видит только те определения, которые к этому +// моменту уже выполнились. Объявление ниже точки вызова отказывает молчаливо-наполовину: +// CommandNotFoundException уходит в stderr, вызывающая функция получает $null и спокойно +// идёт по ветке «не нашли». Так per-database v8path оказался no-op сразу в трёх навыках +// (#93) — тесты этого не заметили, потому что откат на корневой v8path выглядит штатно. +// +// В .py-порте такого класса ошибок нет: Python связывает имя при вызове, порядок определений +// в модуле не важен. Поэтому гард только для .ps1 — и поэтому расхождение портов молчаливое. +// +// Что проверяем: для каждой функции из списка — если навык её вызывает на верхнем уровне +// (или из функции, которая вызывается на верхнем уровне), определение обязано стоять выше +// первой строки, где начинается цепочка. +// +// Запуск: node tests/skills/check-ps-define-before-call.mjs +import { readFileSync, readdirSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const SKILLS = join(ROOT, '.claude', 'skills'); + +// Цепочка выбора платформы: кто кого зовёт. Точка входа вызывается с верхнего уровня. +const CHAINS = [ + { + entry: 'Find-ProjectV8Path', + // вызов точки входа на верхнем уровне скрипта + entryCall: /^\s*\$V8Path\s*=\s*Find-ProjectV8Path\s*$/, + callees: ['Find-ProjectDatabase', 'Test-SamePath', 'Find-V8Project'], + }, +]; + +const errors = []; +let checked = 0; + +for (const skill of readdirSync(SKILLS)) { + const scriptsDir = join(SKILLS, skill, 'scripts'); + if (!existsSync(scriptsDir)) continue; + for (const file of readdirSync(scriptsDir)) { + if (!file.endsWith('.ps1')) continue; + const path = join(scriptsDir, file); + const lines = readFileSync(path, 'utf8').split('\n'); + + for (const chain of CHAINS) { + const callLine = lines.findIndex(l => chain.entryCall.test(l)); + if (callLine < 0) continue; + checked++; + const defLine = name => lines.findIndex(l => l.trimStart().startsWith(`function ${name}`)); + for (const name of [chain.entry, ...chain.callees]) { + const def = defLine(name); + if (def < 0) { + errors.push(`${skill}/${file}: ${chain.entry} зовёт ${name}, но в навыке её нет`); + continue; + } + if (def > callLine) { + errors.push(`${skill}/${file}: function ${name} объявлена на строке ${def + 1}, ` + + `а цепочка ${chain.entry} выполняется на строке ${callLine + 1} — ` + + `PowerShell её не увидит (отказ уйдёт в stderr, результат станет $null)`); + } + } + } + } +} + +if (errors.length) { + console.log(`${errors.length} РАСХОЖДЕНИЙ:`); + for (const e of errors) console.log(` [ERROR] ${e}`); + console.log('\nПоднимите определения выше точки вызова — порядок в .ps1 значим.'); + process.exit(1); +} + +console.log(`Проверено цепочек: ${checked}`); +console.log('OK — все функции объявлены выше точки вызова.');