feat: добавить skill для SLM Design
- добавлена сборка self-contained skill для Claude Code и opencode - добавлен install-ready архив skill в public/slm-design/skill - обновлена карточка SLM Design с меню действий открыть/скачать - добавлен static fallback главной страницы из общего конфига - подключены Mantine Menu и Phosphor Icons для действий карточки
This commit is contained in:
@@ -18,7 +18,7 @@ function collectFiles(dir: string, baseDir = dir, archiveRoot = path.basename(di
|
||||
|
||||
return [
|
||||
{
|
||||
name: `${archiveRoot}/${relativePath}`,
|
||||
name: archiveRoot ? `${archiveRoot}/${relativePath}` : relativePath,
|
||||
content: fs.readFileSync(entryPath),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -18,3 +18,5 @@ if (config.archive) {
|
||||
writeZipFromDirectory(path.join(docsDir, 'content'), zipPath, config.slug);
|
||||
console.log(`Собран ${path.relative(rootDir, zipPath)}`);
|
||||
}
|
||||
|
||||
await import('./scripts/build-skill');
|
||||
|
||||
@@ -17,6 +17,12 @@ Scoped Layered Module Design — модульная архитектура фр
|
||||
|
||||
Рекомендуемый порядок чтения: обзор → слои → модули → сегменты → монорепозитории.
|
||||
|
||||
## AI Skill
|
||||
|
||||
Готовый self-contained skill для Claude Code и opencode можно скачать как архив: [slm-design.skill.zip](/slm-design/skill/slm-design.skill.zip).
|
||||
|
||||
Архив можно распаковать в корень другого проекта. Он добавит рабочие файлы `.claude/skills/slm-design/SKILL.md` и `.opencode/skills/slm-design/SKILL.md`.
|
||||
|
||||
## Преимущества
|
||||
|
||||
### Вертикальная организация домена
|
||||
|
||||
140
projects/slm-design/scripts/build-skill.ts
Normal file
140
projects/slm-design/scripts/build-skill.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { writeZipFromDirectory } from '../../_shared/lib/zip';
|
||||
import config from '../project.config';
|
||||
|
||||
const projectDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const rootDir = path.resolve(projectDir, '../..');
|
||||
const templatePath = path.join(projectDir, 'skill', 'slm-design.skill.md');
|
||||
const outputDir = path.join(rootDir, 'public', config.slug, 'skill');
|
||||
const skillZipPath = path.join(outputDir, 'slm-design.skill.zip');
|
||||
|
||||
const description = 'Use this skill ONLY when working in a project that uses SLM Design (Scoped Layered Module Design) — a frontend architecture with layers app, layouts, screens, widgets, business, infra, ui, shared; modules with public API via index.ts; business-domain factories; and segments like ui/, parts/, hooks/, services/, mappers/. Apply when designing new modules, deciding where to place code, reviewing imports and dependency direction, refactoring existing SLM code, or planning monorepo placement (apps/, packages/ui, packages/infra, packages/shared). Project signals that SLM is active (strongest first): src/screens/ + src/widgets/ combination (highly specific to SLM — FSD uses pages/, Atomic and Clean don\'t have this layer); *.factory.ts files in business modules with index.ts exporting only factory and type-only exports; src/screens/ alongside src/ui/ and src/shared/; in monorepo, the same structure inside apps/{app}/src/. Note: src/business/ is a strong confirmation when present, but small or early-stage SLM projects may not have it yet — absence of business/ does NOT rule out SLM. Conflicting signals indicating other architectures: src/features/ or src/entities/ or src/pages/ (FSD); src/atoms/ or src/molecules/ (Atomic); src/domain/ or src/useCases/ (Clean). User signals (Russian or English): "SLM", "SLM Design", "Scoped Layered Module Design", "куда положить", "where to place", "какой слой", "which layer", "это модуль или компонент", "module or component", "можно ли так импортировать", "can I import", "deep import", "фабрика", "factory", "публичный API", "public API", "parts/", "business-домен", "business domain", "композиция фабрик", "factory composition". Do NOT use this skill for: projects on Feature-Sliced Design (FSD), Atomic Design, Clean Architecture, or any other frontend architecture — SLM has its own rules and is NOT a synonym for these; legacy codebases without explicit SLM structure (do not propose migration unless the user explicitly asks); small isolated tasks like styling, single-file bug fixes, CSS, or build tooling where architectural placement is not the question; backend architecture. When project architecture is ambiguous, ask the user before applying SLM rules.';
|
||||
|
||||
const canonPages = [
|
||||
{
|
||||
anchor: 'canon-architecture-index',
|
||||
source: 'canons/architecture/index.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-architecture-layers',
|
||||
source: 'canons/architecture/layers.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-architecture-modules',
|
||||
source: 'canons/architecture/modules.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-architecture-segments',
|
||||
source: 'canons/architecture/segments.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-architecture-monorepo',
|
||||
source: 'canons/architecture/monorepo.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-examples-react-factory',
|
||||
source: 'canons/examples/react/factory.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-examples-react-factory-composition',
|
||||
source: 'canons/examples/react/factory-composition.md',
|
||||
},
|
||||
{
|
||||
anchor: 'canon-examples-react-composition-provider',
|
||||
source: 'canons/examples/react/composition-provider.md',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const routeAnchors = new Map([
|
||||
['/architecture', 'canon-architecture-index'],
|
||||
['/architecture/', 'canon-architecture-index'],
|
||||
['/architecture/layers', 'canon-architecture-layers'],
|
||||
['/architecture/modules', 'canon-architecture-modules'],
|
||||
['/architecture/segments', 'canon-architecture-segments'],
|
||||
['/architecture/monorepo', 'canon-architecture-monorepo'],
|
||||
['/examples/react/factory', 'canon-examples-react-factory'],
|
||||
['/examples/react/factory-composition', 'canon-examples-react-factory-composition'],
|
||||
['/examples/react/composition-provider', 'canon-examples-react-composition-provider'],
|
||||
]);
|
||||
|
||||
function stripFrontmatter(content: string) {
|
||||
return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
||||
}
|
||||
|
||||
function transformOutsideCode(content: string, transformLine: (line: string) => string) {
|
||||
let inFence = false;
|
||||
|
||||
return content
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
if (/^\s*(```|~~~)/.test(line)) {
|
||||
inFence = !inFence;
|
||||
return line;
|
||||
}
|
||||
|
||||
if (inFence) return line;
|
||||
|
||||
return transformLine(line);
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function shiftHeadings(content: string) {
|
||||
return transformOutsideCode(content, (line) => line.replace(/^(#{1,6})\s/, '##$1 '));
|
||||
}
|
||||
|
||||
function rewriteMarkdownLinks(content: string) {
|
||||
return transformOutsideCode(content, (line) => line.replace(/\]\((\/[^)\s#]+\/?)(#[^)\s]+)?\)/g, (match, route: string, hash = '') => {
|
||||
const normalizedRoute = route.length > 1 && route.endsWith('/') ? route.slice(0, -1) : route;
|
||||
const anchor = routeAnchors.get(route) ?? routeAnchors.get(normalizedRoute);
|
||||
|
||||
if (!anchor) return match;
|
||||
|
||||
return `](${hash || `#${anchor}`})`;
|
||||
}));
|
||||
}
|
||||
|
||||
function prepareCanon(source: string, anchor: string) {
|
||||
const sourcePath = path.join(projectDir, source);
|
||||
const content = fs.readFileSync(sourcePath, 'utf8');
|
||||
const prepared = rewriteMarkdownLinks(shiftHeadings(stripFrontmatter(content).trim()));
|
||||
|
||||
return `<a id="${anchor}"></a>\n\n${prepared}`;
|
||||
}
|
||||
|
||||
function createSkillBody() {
|
||||
const template = fs.readFileSync(templatePath, 'utf8').trim();
|
||||
const canonContent = canonPages.map((page) => prepareCanon(page.source, page.anchor)).join('\n\n---\n\n');
|
||||
|
||||
return template.replace('<!-- SLM_CANON_CONTENT -->', canonContent);
|
||||
}
|
||||
|
||||
function createSkill(frontmatter: string) {
|
||||
return `${frontmatter.trim()}\n\n${createSkillBody()}\n`;
|
||||
}
|
||||
|
||||
function writeFile(filePath: string, content: string) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
function buildSkill() {
|
||||
const frontmatter = `---\nname: slm-design\ndescription: ${description}\n---`;
|
||||
const skillContent = createSkill(frontmatter);
|
||||
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
|
||||
writeFile(path.join(outputDir, '.claude', 'skills', 'slm-design', 'SKILL.md'), skillContent);
|
||||
writeFile(path.join(outputDir, '.opencode', 'skills', 'slm-design', 'SKILL.md'), skillContent);
|
||||
|
||||
fs.rmSync(skillZipPath, { force: true });
|
||||
writeZipFromDirectory(outputDir, skillZipPath, '');
|
||||
|
||||
console.log(`Собран ${path.relative(rootDir, outputDir)}`);
|
||||
console.log(`Собран ${path.relative(rootDir, skillZipPath)}`);
|
||||
}
|
||||
|
||||
buildSkill();
|
||||
98
projects/slm-design/skill/slm-design.skill.md
Normal file
98
projects/slm-design/skill/slm-design.skill.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# SLM Design Skill
|
||||
|
||||
SLM Design — новая архитектура. Не заменяй её правилами Feature-Sliced Design, Atomic Design, Clean Architecture или другими привычными схемами. Если в проекте нет локальной документации SLM, считай этот файл единственным источником правды.
|
||||
|
||||
## Определение Контекста
|
||||
|
||||
Перед применением правил убедись, что проект использует SLM Design.
|
||||
|
||||
1. Проверь характерные слои SLM. Сильные сигналы: одновременное наличие `src/screens/` и `src/widgets/` (комбинация специфична именно для SLM — в FSD это `pages/`, в Atomic и Clean таких слоёв нет); файлы `*.factory.ts` в business-модулях с `index.ts`, экспортирующим только фабрику и type-only. Дополнительные сигналы: `src/ui/`, `src/shared/`, `src/layouts/` как слои. В монорепо проверяй то же внутри `apps/{app}/src/`.
|
||||
ВАЖНО: отсутствие `src/business/` НЕ означает, что проект не на SLM. В маленьких или ранних проектах бизнес-доменов может ещё не быть.
|
||||
2. Проверь, что business-модули (если они есть) имеют `*.factory.ts` в корне и `index.ts` экспортирует только фабрику и type-only экспорты.
|
||||
3. Если видишь признаки другой архитектуры — `features/`, `entities/`, `pages/` (FSD), `atoms/`, `molecules/` (Atomic), `domain/`, `useCases/` (Clean) — это НЕ SLM. Не применяй правила этого skill.
|
||||
4. Если структура неоднозначна или проект новый — спроси пользователя, какая архитектура используется, прежде чем применять правила.
|
||||
5. Если пользователь работает в legacy-коде без SLM и НЕ просит миграцию — не предлагай рефакторинг по SLM. Соблюдай существующие паттерны кода.
|
||||
## Порядок Работы
|
||||
|
||||
1. Найди границу приложения: `src/` или `apps/{app}/src`.
|
||||
2. Определи ответственность изменения: запуск приложения, layout, screen, widget, business-domain, infra-service, UI-kit или shared resource.
|
||||
3. Размещай код на самом низком подходящем уровне и поднимай выше только при реальной потребности переиспользования.
|
||||
4. Проверяй, является сущность модулем или компонентом. Если сущность получает данные, владеет сценарием, композирует зависимости или имеет внутреннюю архитектуру — это модуль, а не компонент в `ui/`.
|
||||
5. Все внешние импорты между модулями делай только через публичный API (`index.ts`). Deep imports запрещены.
|
||||
6. Для `business` runtime-зависимостей между доменами используй фабрики. Не импортируй runtime-код одного business-домена напрямую в другой.
|
||||
7. Перед завершением проверь слой, модульную границу, сегменты, публичный API, направление импортов и monorepo-ограничения.
|
||||
|
||||
## Жёсткие Правила
|
||||
|
||||
- Направление зависимостей внутри приложения: `app → [ layouts | screens ] → widgets → business → infra → ui → shared`.
|
||||
- `layouts` и `screens` параллельны и не импортируют друг друга.
|
||||
- Модули одного слоя в группе «Композиция» изолированы друг от друга.
|
||||
- Runtime-импорты между `business`-доменами запрещены. Cross-domain runtime-зависимости передаются только через аргументы фабрики.
|
||||
- `import type` в группе «Ядро» разрешён в обоих направлениях, потому что не создаёт runtime-зависимость.
|
||||
- Каждый внешний импорт модуля идёт через `index.ts` модуля или публичный API пакета.
|
||||
- `business/{name}/index.ts` экспортирует только фабрику и type-only экспорты.
|
||||
- Компонент в `ui/` родительского модуля не импортирует проектный код за пределами родительского модуля, не получает данные, не вызывает сценарные хуки и не содержит бизнес-логику.
|
||||
- `parts/` содержит только вложенные модули, не произвольные `.tsx`, стили или хуки.
|
||||
- `shared/` не знает о продукте, бизнес-доменах, UI-kit сущностях и runtime-состоянии.
|
||||
- В monorepo SLM применяется внутри каждого `apps/{app}/src`.
|
||||
- В `packages/*` можно выносить только общие `ui`, `infra` и `shared`. `business`, `app`, `layouts`, `screens`, `widgets` не выносятся в пакеты.
|
||||
|
||||
## Выбор Места Для Кода
|
||||
|
||||
- Код нужен одной странице или layout: оставь его внутри `screens/{name}/parts/` или `layouts/{name}/parts/`.
|
||||
- Абстрактный UI без бизнес-логики и сценариев: `ui/{name}/`.
|
||||
- Составной блок интерфейса без принадлежности конкретному домену, используемый в нескольких screens/layouts: `widgets/{name}/`.
|
||||
- Код принадлежит бизнес-домену: `business/{domain}/`, даже если переиспользуется.
|
||||
- Технический сервис приложения: `infra/{service}/`.
|
||||
- Чистая утилита или фундаментальный ресурс без знания о продукте: `shared/`.
|
||||
- В monorepo общий UI/infra/shared код, потенциально нужный двум и более frontend-приложениям: `packages/ui/*`, `packages/infra/*`, `packages/shared`.
|
||||
|
||||
## Шаблон Business-Модуля
|
||||
|
||||
```text
|
||||
business/{name}/
|
||||
├── {name}.factory.ts
|
||||
├── hooks/
|
||||
├── services/
|
||||
├── mappers/
|
||||
├── types/
|
||||
│ ├── {name}-api.type.ts
|
||||
│ ├── {name}-deps.type.ts
|
||||
│ └── {name}-factory.type.ts
|
||||
├── ui/
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
Фабрика лежит в корне business-модуля и возвращает публичный runtime API. Если модулю нужны другие домены, принимай зависимости аргументом фабрики доменными именами.
|
||||
|
||||
```ts
|
||||
export { customerFactory } from './customer.factory'
|
||||
|
||||
export type { Customer } from './types/customer.type'
|
||||
export type { CustomerApi } from './types/customer-api.type'
|
||||
export type { CustomerDeps } from './types/customer-deps.type'
|
||||
export type { CustomerFactory } from './types/customer-factory.type'
|
||||
```
|
||||
|
||||
## Чеклист Ревью
|
||||
|
||||
Проверяй архитектуру в таком порядке:
|
||||
|
||||
1. Правильно ли выбран слой по ответственности?
|
||||
2. Не вынесен ли код выше, чем нужно для текущего использования?
|
||||
3. Не лежит ли модульная сущность в `ui/` как компонент?
|
||||
4. Не содержит ли компонент в `ui/` данные, сценарии, внешние импорты или вложенную архитектуру?
|
||||
5. Есть ли у каждого модуля публичный API и нет ли deep imports?
|
||||
6. Соблюдено ли направление зависимостей?
|
||||
7. Нет ли runtime-импортов между business-доменами?
|
||||
8. Экспортирует ли `business/index.ts` только фабрику и типы?
|
||||
9. Не попали ли продуктовые типы, конфиги или стили в `shared/`?
|
||||
10. В monorepo не вынесены ли `business`, `screens`, `layouts` или `widgets` в `packages/*`?
|
||||
|
||||
При ревью сначала перечисляй нарушения с файлами и причиной. Затем предлагай минимальное исправление.
|
||||
|
||||
## Каноническая Спецификация
|
||||
|
||||
Ниже находится полная спецификация SLM Design из канонов проекта. Не сокращай и не переинтерпретируй эти правила.
|
||||
|
||||
<!-- SLM_CANON_CONTENT -->
|
||||
Reference in New Issue
Block a user