mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 07:30:16 +03:00
chore: init
This commit is contained in:
70
scripts/build-skill.mjs
Normal file
70
scripts/build-skill.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import skillConfig from '../src-skills/slm-design/skill.config.mjs';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const sourceDir = path.join(repoRoot, 'src-skills', skillConfig.name);
|
||||
const sourcePath = path.join(sourceDir, skillConfig.source);
|
||||
const docsDir = path.join(repoRoot, 'docs');
|
||||
const outputDir = path.join(repoRoot, 'skills', skillConfig.name);
|
||||
const includePattern = /<!--\s*include:\s*(.*?)\s*-->/g;
|
||||
|
||||
const isWithin = (filePath, parentPath) => {
|
||||
const relativePath = path.relative(parentPath, filePath);
|
||||
|
||||
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
||||
};
|
||||
|
||||
const removeFrontmatter = (content) => {
|
||||
return content.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
|
||||
};
|
||||
|
||||
const shiftHeadings = (content) => {
|
||||
return content.replace(/^(#{1,6})(?=\s)/gm, (heading) => '#'.repeat(Math.min(heading.length + 1, 6)));
|
||||
};
|
||||
|
||||
const resolveIncludes = (content, baseDir) => {
|
||||
return content.replace(includePattern, (match, includePath) => {
|
||||
const resolvedPath = path.resolve(baseDir, includePath);
|
||||
|
||||
if (!isWithin(resolvedPath, repoRoot) || !fs.existsSync(resolvedPath)) {
|
||||
throw new Error(`Include file not found: ${includePath}`);
|
||||
}
|
||||
|
||||
return shiftHeadings(removeFrontmatter(fs.readFileSync(resolvedPath, 'utf8')).trim());
|
||||
});
|
||||
};
|
||||
|
||||
const rewriteLinks = (content) => {
|
||||
return skillConfig.linkRewrites.reduce((result, { from, to }) => {
|
||||
return result.split(`](${from})`).join(`](${to})`);
|
||||
}, content);
|
||||
};
|
||||
|
||||
const createFrontmatter = () => {
|
||||
return `---\nname: ${skillConfig.name}\ndescription: ${JSON.stringify(skillConfig.description)}\n---`;
|
||||
};
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
throw new Error(`Skill source not found: ${path.relative(repoRoot, sourcePath)}`);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
throw new Error('Documentation directory not found: docs');
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(sourcePath, 'utf8');
|
||||
const content = rewriteLinks(resolveIncludes(source, path.dirname(sourcePath))).trim();
|
||||
const output = [
|
||||
createFrontmatter(),
|
||||
'<!-- Generated from src-skills/slm-design/SKILL.md. Do not edit manually. -->',
|
||||
content,
|
||||
].join('\n\n');
|
||||
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(outputDir, 'SKILL.md'), `${output}\n`);
|
||||
fs.cpSync(docsDir, path.join(outputDir, 'reference'), { recursive: true });
|
||||
|
||||
console.log(path.relative(repoRoot, path.join(outputDir, 'SKILL.md')));
|
||||
99
scripts/check-skill.mjs
Normal file
99
scripts/check-skill.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import skillConfig from '../src-skills/slm-design/skill.config.mjs';
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const skillDir = path.join(repoRoot, 'skills', skillConfig.name);
|
||||
const skillPath = path.join(skillDir, 'SKILL.md');
|
||||
const referenceFiles = [
|
||||
'README.md',
|
||||
'canons/business-factory.md',
|
||||
'canons/business-runtime-boundary.md',
|
||||
'canons/decision-process.md',
|
||||
'canons/file-atlas.md',
|
||||
'canons/index.md',
|
||||
'canons/layers.md',
|
||||
'canons/modules.md',
|
||||
'canons/monorepo.md',
|
||||
'canons/segments.md',
|
||||
'canons/validation.md',
|
||||
'examples/business-composition.md',
|
||||
'examples/business-testing.md',
|
||||
'examples/react/composition-provider.md',
|
||||
'examples/react/composition-structures.md',
|
||||
];
|
||||
|
||||
const assert = (condition, message) => {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const listMarkdownFiles = (directoryPath) => {
|
||||
return fs.readdirSync(directoryPath, { withFileTypes: true }).flatMap((entry) => {
|
||||
const entryPath = path.join(directoryPath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return listMarkdownFiles(entryPath);
|
||||
}
|
||||
|
||||
return entry.isFile() && path.extname(entry.name) === '.md' ? [entryPath] : [];
|
||||
});
|
||||
};
|
||||
|
||||
const assertLocalLinksExist = (filePath) => {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const links = [...content.matchAll(/]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)];
|
||||
|
||||
for (const [, rawTarget] of links) {
|
||||
if (rawTarget.startsWith('#') || /^[a-z][a-z+.-]*:/i.test(rawTarget) || rawTarget.startsWith('//')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [targetPath] = rawTarget.split('#');
|
||||
const resolvedPath = path.resolve(path.dirname(filePath), targetPath);
|
||||
|
||||
assert(fs.existsSync(resolvedPath), `Broken link in ${path.relative(repoRoot, filePath)}: ${rawTarget}`);
|
||||
}
|
||||
};
|
||||
|
||||
assert(fs.existsSync(skillPath), 'Run npm run build before checking the skill.');
|
||||
|
||||
const skillContent = fs.readFileSync(skillPath, 'utf8');
|
||||
|
||||
assert(skillContent.startsWith(`---\nname: ${skillConfig.name}\n`), 'SKILL.md must contain the skill name in frontmatter.');
|
||||
assert(skillContent.includes('description: '), 'SKILL.md must contain a description in frontmatter.');
|
||||
assert(!skillContent.includes('<!-- include:'), 'SKILL.md contains an unresolved include.');
|
||||
assert(!skillContent.includes('/home/gromov/'), 'SKILL.md must not contain an absolute workspace path.');
|
||||
assert(!skillContent.includes('reference/slm-design'), 'SKILL.md contains an old documentation path.');
|
||||
assert(skillContent.includes('## Процесс архитектурного решения'), 'SKILL.md does not include the decision process.');
|
||||
assert(skillContent.includes('## Runtime-граница business'), 'SKILL.md does not include the business runtime boundary.');
|
||||
assert(skillContent.includes('## Архитектурная проверка'), 'SKILL.md does not include the validation rules.');
|
||||
assert(
|
||||
skillContent.includes('./reference/canons/business-factory.md'),
|
||||
'The business factory link must point to local reference documentation.',
|
||||
);
|
||||
|
||||
for (const referenceFile of referenceFiles) {
|
||||
assert(
|
||||
fs.existsSync(path.join(skillDir, 'reference', referenceFile)),
|
||||
`Missing copied reference file: ${referenceFile}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const filePath of listMarkdownFiles(skillDir)) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
assert(
|
||||
!content.includes('/home/gromov/'),
|
||||
`${path.relative(repoRoot, filePath)} contains an absolute workspace path.`,
|
||||
);
|
||||
assert(
|
||||
!content.includes('reference/slm-design'),
|
||||
`${path.relative(repoRoot, filePath)} contains an old documentation path.`,
|
||||
);
|
||||
assertLocalLinksExist(filePath);
|
||||
}
|
||||
|
||||
console.log(`Validated ${path.relative(repoRoot, skillPath)}`);
|
||||
Reference in New Issue
Block a user