diff --git a/.claude/skills/meta-edit/json-dsl.md b/.claude/skills/meta-edit/json-dsl.md index 245197c1..43942522 100644 --- a/.claude/skills/meta-edit/json-dsl.md +++ b/.claude/skills/meta-edit/json-dsl.md @@ -3,7 +3,7 @@ Для сложных и комбинированных операций используйте JSON-файл вместо inline-режима. ```powershell -powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "" -ObjectPath "" +powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -DefinitionFile "" -ObjectPath "" ``` ## add — добавить элементы diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index 38a2150b..31dccb53 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -11,7 +11,7 @@ allowed-tools: # /mxl-compile — Компилятор макета из DSL -Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace). +Принимает компактное JSON-определение макета (области, параметры, стили) → генерирует Template.xml табличного документа 1С: палитры, индексы, объединения, namespace. ## Использование diff --git a/.claude/skills/web-test/scripts/engine/core/state.mjs b/.claude/skills/web-test/scripts/engine/core/state.mjs index b275cfcc..0d5d6e6b 100644 --- a/.claude/skills/web-test/scripts/engine/core/state.mjs +++ b/.claude/skills/web-test/scripts/engine/core/state.mjs @@ -1,4 +1,4 @@ -// web-test core/state v1.18 — module-level state for the web-test engine. +// web-test core/state v1.19 — module-level state for the web-test engine. // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // // Holds the single browser/page/recorder slot plus the multi-context registry, @@ -10,7 +10,7 @@ import { dirname, resolve as pathResolve } from 'path'; import { fileURLToPath } from 'url'; -// Project root: 6 levels up from .claude/skills/web-test/scripts/engine/core/state.mjs +// Project root: 6 levels up from /web-test/scripts/engine/core/state.mjs const __fn_state = fileURLToPath(import.meta.url); export const projectRoot = pathResolve(dirname(__fn_state), '..', '..', '..', '..', '..', '..'); diff --git a/tests/skills/README.md b/tests/skills/README.md index faaa3e76..d99db0c5 100644 --- a/tests/skills/README.md +++ b/tests/skills/README.md @@ -66,6 +66,7 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → | `check-type-maps.mjs` | карты типов метаданных согласованы с таблицей `ChildObjects` из `docs/1c-configuration-spec.md` | | `check-format-versions.mjs` | проверенный диапазон версий формата: границы одинаковы во всех навыках и сходятся с лестницей из `docs/1c-configuration-spec.md` | | `check-positional-binding.mjs` | read-only навыки (`*-info` / `*-validate` / `cfe-diff`): позиционным остаётся только путь ко входу, лишний позиционный аргумент не перезаписывает указанный файл | +| `check-agent-portability.mjs` | исходники навыков не привязаны к конкретному AI-агенту: единственная разрешённая форма — плейсхолдер `${CLAUDE_SKILL_DIR}/`, который разворачивает `scripts/switch.py` | `check-inline-drift.mjs` держит реестр семей внутри себя: у каждой семьи перечислены варианты, у варианта — навык-эталон и список копий. Отклоняющийся вариант обязан иметь обоснование (`why`), @@ -81,6 +82,12 @@ node tests/skills/check-inline-drift.mjs --list # реестр: семья → **замеренная** ступень таблицы «Лестница версий» из спецификации, поэтому расхождение спеки и кода падает здесь, а не на чужой выгрузке. +`check-agent-portability.mjs` держит переносимость текстов навыка между платформами. Путь к скрипту +пишется только как `${CLAUDE_SKILL_DIR}/scripts/<имя>.` — литеральный `.claude/skills/...` +на Codex или Cursor указывает в несуществующий каталог, и команда молча не находит скрипт (issue +#75). Название агента в текстах навыка не упоминается вовсе: на чужой платформе это дезинформация. +Сторонний код (`node_modules`) исключён — переписывать его нельзя, на переносимость он не влияет. + `check-type-maps.mjs` сверяет словари типов метаданных с таблицей «Порядок типов в ChildObjects» из спецификации: имя типа, каталог и порядок обязаны совпадать, а тип, которого в карте нет намеренно, объявляется исключением **с причиной**. Так проверка отличает намеренное ограничение diff --git a/tests/skills/check-agent-portability.mjs b/tests/skills/check-agent-portability.mjs new file mode 100644 index 00000000..3dda6a09 --- /dev/null +++ b/tests/skills/check-agent-portability.mjs @@ -0,0 +1,83 @@ +#!/usr/bin/env node +// Инвариант: исходники навыков не привязаны к конкретному AI-агенту. Единственная разрешённая +// форма упоминания — плейсхолдер `${CLAUDE_SKILL_DIR}/`, который scripts/switch.py разворачивает +// в префикс целевой платформы (.codex/skills, .cursor/skills и т.д.). +// +// Без этого навык уезжает на другую платформу с битым путём: `.claude/skills/meta-edit/...`, +// зашитый литералом в json-dsl.md, указывал в несуществующий каталог, а команда молча не +// находила скрипт. Прозаические «Claude описывает...» туда же — на Codex это дезинформация. +// Снапшот-тесты этого не видят: они сверяют вывод скриптов, а не переносимость текстов. +// +// Проверка: во всех текстовых файлах .claude/skills/** (кроме node_modules и __pycache__) +// после вырезания `${CLAUDE_SKILL_DIR}/` не должно остаться ни одного вхождения `claude` +// в любом регистре. Это ловит и литеральные пути, и прозу, и плейсхолдер без завершающего +// слеша — switch.py разворачивает только форму со слешем, остальные проедут насквозь. +// Выход 1 при нарушении. Запуск: node tests/skills/check-agent-portability.mjs +import { readFileSync, readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join, relative, sep } from 'node:path'; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..'); +const SKILLS_DIR = join(ROOT, '.claude', 'skills'); + +// node_modules — сторонний код (playwright содержит ClaudeGenerator и `.claude/agents`), +// переписывать его нельзя и незачем: на переносимость навыка он не влияет. +const SKIP_DIRS = new Set(['node_modules', '__pycache__', '.git']); + +// Разрешённая форма — ровно та, которую разворачивает rewrite_paths() в switch.py. +const ALLOWED = '${CLAUDE_SKILL_DIR}/'; + +function walk(dir, out = []) { + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + if (!SKIP_DIRS.has(entry.name)) walk(full, out); + } else if (entry.isFile()) { + out.push(full); + } + } + return out; +} + +const files = walk(SKILLS_DIR); +if (files.length === 0) { + console.error(`Не найдено ни одного файла в ${SKILLS_DIR} — гард потерял цель.`); + process.exit(1); +} + +const violations = []; +let scanned = 0; +let allowedHits = 0; + +for (const file of files) { + const buf = readFileSync(file); + // Бинарники (иконки, .bin, .cf) пропускаем: NUL-байт — надёжный признак, ловить в них + // подстроку бессмысленно. + if (buf.includes(0)) continue; + const text = buf.toString('utf8'); + scanned++; + + const before = text.split(ALLOWED).length - 1; + allowedHits += before; + const stripped = text.split(ALLOWED).join(''); + if (!/claude/i.test(stripped)) continue; + + const rel = relative(ROOT, file).split(sep).join('/'); + stripped.split('\n').forEach((line, i) => { + if (!/claude/i.test(line)) return; + // Номер строки считаем по очищенному тексту: вырезается подстрока, а не строка целиком, + // поэтому нумерация совпадает с исходным файлом. + violations.push(`${rel}:${i + 1}: ${line.trim()}`); + }); +} + +if (violations.length) { + console.error(`Нарушений: ${violations.length}\n`); + for (const v of violations) console.error(` ${v}`); + console.error(`\nПуть к скрипту навыка пишется как ${ALLOWED}scripts/<имя>.;`); + console.error('название агента в текстах навыка не упоминается.'); + process.exit(1); +} + +console.log(`OK — ${scanned} текстовых файлов, ${allowedHits} вхождений ${ALLOWED}.`); +console.log('Привязки к конкретному AI-агенту в исходниках навыков нет.'); diff --git a/tests/skills/check-all.mjs b/tests/skills/check-all.mjs index 6eba7117..2a5d823b 100644 --- a/tests/skills/check-all.mjs +++ b/tests/skills/check-all.mjs @@ -15,6 +15,7 @@ const GUARDS = [ ['check-type-maps.mjs', 'карты типов метаданных: согласованы со спецификацией'], ['check-format-versions.mjs', 'проверенный диапазон версий формата: согласован со спецификацией'], ['check-positional-binding.mjs', 'read-only навыки: позиционным остаётся только путь ко входу'], + ['check-agent-portability.mjs', 'исходники навыков: без привязки к конкретному AI-агенту'], ]; let failed = 0;