fix(switch): навыки без привязки к Claude — порт на другие платформы не уносит хвосты

Три файла из 77 навыков нарушали соглашение о переносимости, и сборка
Codex-порта уносила их as is (#75).

Функциональный из трёх один: json-dsl.md писал путь к скрипту литералом
.claude/skills/meta-edit/..., то есть на любой платформе кроме claude-code
команда указывала в несуществующий каталог и молча не находила скрипт.
Заменено на ${CLAUDE_SKILL_DIR}/ — дальше путь разворачивает switch.py.

Ещё два — тексты: проза с названием агента в mxl-compile (заодно избыточная:
у соседних *-compile это одна строка «принимает X → генерирует Y») и
.claude-путь в комментарии state.mjs.

От регресса — check-agent-portability.mjs: вырезает разрешённый
${CLAUDE_SKILL_DIR}/ и падает на любом оставшемся claude. Ловит и плейсхолдер
без завершающего слеша: switch.py разворачивает только форму со слешем,
остальные проехали бы насквозь. Сторонний node_modules исключён — playwright
содержит ClaudeGenerator и .claude/agents, переписывать его нельзя.

switch.py не менялся: ни одного плейсхолдера и ни одного вызова скрипта вне
*.md верхнего уровня навыка нет, рекурсия не нужна.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-19 16:35:07 +03:00
co-authored by Claude Opus 5
parent 9475aa6171
commit 97f1a0d4e1
6 changed files with 95 additions and 4 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
Для сложных и комбинированных операций используйте JSON-файл вместо inline-режима.
```powershell
powershell.exe -NoProfile -File .claude/skills/meta-edit/scripts/meta-edit.ps1 -DefinitionFile "<json>" -ObjectPath "<path>"
powershell.exe -NoProfile -File ${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1 -DefinitionFile "<json>" -ObjectPath "<path>"
```
## add — добавить элементы
+1 -1
View File
@@ -11,7 +11,7 @@ allowed-tools:
# /mxl-compile — Компилятор макета из DSL
Принимает компактное JSON-определение макета и генерирует корректный Template.xml для табличного документа 1С. Claude описывает *что* нужно (области, параметры, стили), скрипт обеспечивает *корректность* XML (палитры, индексы, объединения, namespace).
Принимает компактное JSON-определение макета (области, параметры, стили) → генерирует Template.xml табличного документа 1С: палитры, индексы, объединения, namespace.
## Использование
@@ -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 <skills>/web-test/scripts/engine/core/state.mjs
const __fn_state = fileURLToPath(import.meta.url);
export const projectRoot = pathResolve(dirname(__fn_state), '..', '..', '..', '..', '..', '..');
+7
View File
@@ -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/<имя>.<ext>` — литеральный `.claude/skills/...`
на Codex или Cursor указывает в несуществующий каталог, и команда молча не находит скрипт (issue
#75). Название агента в текстах навыка не упоминается вовсе: на чужой платформе это дезинформация.
Сторонний код (`node_modules`) исключён — переписывать его нельзя, на переносимость он не влияет.
`check-type-maps.mjs` сверяет словари типов метаданных с таблицей «Порядок типов в ChildObjects»
из спецификации: имя типа, каталог и порядок обязаны совпадать, а тип, которого в карте нет
намеренно, объявляется исключением **с причиной**. Так проверка отличает намеренное ограничение
+83
View File
@@ -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/<имя>.<ext>;`);
console.error('название агента в текстах навыка не упоминается.');
process.exit(1);
}
console.log(`OK — ${scanned} текстовых файлов, ${allowedHits} вхождений ${ALLOWED}.`);
console.log('Привязки к конкретному AI-агенту в исходниках навыков нет.');
+1
View File
@@ -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;