mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-08-21 02:29:42 +03:00
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:
co-authored by
Claude Opus 5
parent
9475aa6171
commit
97f1a0d4e1
@@ -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 — добавить элементы
|
||||
|
||||
@@ -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), '..', '..', '..', '..', '..', '..');
|
||||
|
||||
|
||||
@@ -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»
|
||||
из спецификации: имя типа, каталог и порядок обязаны совпадать, а тип, которого в карте нет
|
||||
намеренно, объявляется исключением **с причиной**. Так проверка отличает намеренное ограничение
|
||||
|
||||
@@ -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-агенту в исходниках навыков нет.');
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user