From e3a6044f44b8aa7529e762bb531a04a85dc79e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cmkczarkowski=E2=80=9D?= Date: Wed, 24 Sep 2025 12:32:46 +0200 Subject: [PATCH] refactor: extract shared markdown builders from rules generation strategies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored SingleFileRulesStrategy and MultiFileRulesStrategy to eliminate code duplication by extracting common markdown generation logic into a shared utility module. ## Problem Both strategy classes duplicated significant amounts of code for: - Project header generation - Empty state messaging - Library content formatting - Section structure rendering ## Solution Created `rulesMarkdownBuilders.ts` with reusable functions: - `createProjectMarkdown()` - Generates project header - `createEmptyStateMarkdown()` - Creates empty state message - `generateLibraryContent()` - Formats individual library rules - `renderLibrarySection()` - Renders complete library section - `generateLibrarySections()` - Builds all library sections - `PROJECT_FILE_CONFIG` - Centralizes default configuration ## Benefits - Eliminates ~60 lines of duplicate code - Ensures consistent formatting across strategies - Simplifies future strategy additions - Makes copy changes easier to manage - Improves maintainability šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../MultiFileRulesStrategy.ts | 48 ++++------ .../SingleFileRulesStrategy.ts | 65 ++++---------- .../shared/rulesMarkdownBuilders.ts | 87 +++++++++++++++++++ 3 files changed, 119 insertions(+), 81 deletions(-) create mode 100644 src/services/rules-builder/rules-generation-strategies/shared/rulesMarkdownBuilders.ts diff --git a/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts b/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts index 124b632..ee7f2dd 100644 --- a/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts +++ b/src/services/rules-builder/rules-generation-strategies/MultiFileRulesStrategy.ts @@ -1,8 +1,14 @@ import type { RulesGenerationStrategy } from '../RulesGenerationStrategy.ts'; import { Layer, type Library, Stack } from '../../../data/dictionaries.ts'; import type { RulesContent } from '../RulesBuilderTypes.ts'; -import { getRulesForLibrary } from '../../../data/rules'; import { slugify } from '../../../utils/slugify.ts'; +import { + createProjectMarkdown, + createEmptyStateMarkdown, + generateLibraryContent, + renderLibrarySection, + PROJECT_FILE_CONFIG, +} from './shared/rulesMarkdownBuilders.ts'; /** * Strategy for multi-file rules generation @@ -15,17 +21,17 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { stacksByLayer: Record, librariesByStack: Record, ): RulesContent[] { - const projectMarkdown = `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`; - const noSelectedLibrariesMarkdown = `---\n\nšŸ‘ˆ Use the Rule Builder on the left or drop dependency file here`; - const projectLabel = 'Project', - projectFileName = 'project.mdc'; - + const projectMarkdown = createProjectMarkdown(projectName, projectDescription); const markdowns: RulesContent[] = []; - markdowns.push({ markdown: projectMarkdown, label: projectLabel, fileName: projectFileName }); + markdowns.push({ + markdown: projectMarkdown, + label: PROJECT_FILE_CONFIG.label, + fileName: PROJECT_FILE_CONFIG.fileName, + }); if (selectedLibraries.length === 0) { - markdowns[0].markdown += noSelectedLibrariesMarkdown; + markdowns[0].markdown += createEmptyStateMarkdown(); return markdowns; } @@ -37,7 +43,6 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { layer, stack, library, - libraryRules: getRulesForLibrary(library), }), ); }); @@ -48,39 +53,18 @@ export class MultiFileRulesStrategy implements RulesGenerationStrategy { } private buildRulesContent({ - libraryRules, layer, stack, library, }: { - libraryRules: string[]; layer: string; stack: string; library: string; }): RulesContent { const label = `${layer} - ${stack} - ${library}`; const fileName: RulesContent['fileName'] = `${slugify(`${layer}-${stack}-${library}`)}.mdc`; - const content = - libraryRules.length > 0 - ? `${libraryRules.map((rule) => `- ${rule}`).join('\n')}` - : `- Use ${library} according to best practices`; - const markdown = this.renderRuleMarkdown({ content, layer, stack, library }); + const content = generateLibraryContent(library as Library); + const markdown = renderLibrarySection({ layer, stack, library, content }); return { markdown, label, fileName }; } - - private renderRuleMarkdown = ({ - content, - layer, - stack, - library, - }: { - content: string; - layer: string; - stack: string; - library: string; - }) => - `## ${layer}\n\n### Guidelines for ${stack}\n\n#### ${library}\n\n{{content}}\n\n`.replace( - '{{content}}', - content, - ); } diff --git a/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts b/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts index 467c9e7..95a98ef 100644 --- a/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts +++ b/src/services/rules-builder/rules-generation-strategies/SingleFileRulesStrategy.ts @@ -1,7 +1,12 @@ import type { RulesGenerationStrategy } from '../RulesGenerationStrategy.ts'; import { Layer, Library, Stack } from '../../../data/dictionaries.ts'; import type { RulesContent } from '../RulesBuilderTypes.ts'; -import { getRulesForLibrary } from '../../../data/rules.ts'; +import { + createProjectMarkdown, + createEmptyStateMarkdown, + generateLibrarySections, + PROJECT_FILE_CONFIG, +} from './shared/rulesMarkdownBuilders.ts'; /** * Strategy for single-file rules generation @@ -14,58 +19,20 @@ export class SingleFileRulesStrategy implements RulesGenerationStrategy { stacksByLayer: Record, librariesByStack: Record, ): RulesContent[] { - const projectMarkdown = `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`; - const noSelectedLibrariesMarkdown = `---\n\nšŸ‘ˆ Use the Rule Builder on the left or drop dependency file here`; - const projectLabel = 'Project', - projectFileName = 'project.mdc'; - - let markdown = projectMarkdown; + let markdown = createProjectMarkdown(projectName, projectDescription); if (selectedLibraries.length === 0) { - markdown += noSelectedLibrariesMarkdown; - return [{ markdown, label: projectLabel, fileName: projectFileName }]; + markdown += createEmptyStateMarkdown(); + return [ + { + markdown, + label: PROJECT_FILE_CONFIG.label, + fileName: PROJECT_FILE_CONFIG.fileName, + }, + ]; } - markdown += this.generateLibraryMarkdown(stacksByLayer, librariesByStack); + markdown += generateLibrarySections(stacksByLayer, librariesByStack); return [{ markdown, label: 'All Rules', fileName: 'rules.mdc' }]; } - - private generateLibraryMarkdown( - stacksByLayer: Record, - librariesByStack: Record, - ): string { - let markdown = ''; - - // Generate content for each layer and its stacks - Object.entries(stacksByLayer).forEach(([layer, stacks]) => { - markdown += `## ${layer}\n\n`; - - stacks.forEach((stack) => { - markdown += `### Guidelines for ${stack}\n\n`; - - const libraries = librariesByStack[stack]; - if (libraries) { - libraries.forEach((library) => { - markdown += `#### ${library}\n\n`; - - // Get specific rules for this library - const libraryRules = getRulesForLibrary(library); - if (libraryRules.length > 0) { - libraryRules.forEach((rule) => { - markdown += `- ${rule}\n`; - }); - } else { - markdown += `- Use ${library} according to best practices\n`; - } - - markdown += '\n'; - }); - } - - markdown += '\n'; - }); - }); - - return markdown; - } } diff --git a/src/services/rules-builder/rules-generation-strategies/shared/rulesMarkdownBuilders.ts b/src/services/rules-builder/rules-generation-strategies/shared/rulesMarkdownBuilders.ts new file mode 100644 index 0000000..0f30afe --- /dev/null +++ b/src/services/rules-builder/rules-generation-strategies/shared/rulesMarkdownBuilders.ts @@ -0,0 +1,87 @@ +import type { Layer, Library, Stack } from '../../../../data/dictionaries.ts'; +import { getRulesForLibrary } from '../../../../data/rules.ts'; + +/** + * Creates the project header markdown + */ +export function createProjectMarkdown(projectName: string, projectDescription: string): string { + return `# AI Rules for ${projectName}\n\n${projectDescription}\n\n`; +} + +/** + * Creates the empty state markdown + */ +export function createEmptyStateMarkdown(): string { + return `---\n\nšŸ‘ˆ Use the Rule Builder on the left or drop dependency file here`; +} + +/** + * Generates markdown for a single library + */ +export function generateLibraryContent(library: Library): string { + const libraryRules = getRulesForLibrary(library); + + if (libraryRules.length > 0) { + return libraryRules.map((rule) => `- ${rule}`).join('\n'); + } + + return `- Use ${library} according to best practices`; +} + +/** + * Renders a complete library section with headers + */ +export function renderLibrarySection({ + layer, + stack, + library, + content, +}: { + layer: string; + stack: string; + library: string; + content?: string; +}): string { + const finalContent = content ?? generateLibraryContent(library as Library); + + return `## ${layer}\n\n### Guidelines for ${stack}\n\n#### ${library}\n\n${finalContent}\n\n`; +} + +/** + * Generates markdown for all libraries organized by layer and stack + */ +export function generateLibrarySections( + stacksByLayer: Record, + librariesByStack: Record, +): string { + let markdown = ''; + + Object.entries(stacksByLayer).forEach(([layer, stacks]) => { + markdown += `## ${layer}\n\n`; + + stacks.forEach((stack) => { + markdown += `### Guidelines for ${stack}\n\n`; + + const libraries = librariesByStack[stack]; + if (libraries) { + libraries.forEach((library) => { + markdown += `#### ${library}\n\n`; + markdown += generateLibraryContent(library); + markdown += '\n\n'; + }); + } + + markdown += '\n'; + }); + }); + + return markdown; +} + +/** + * Default project file configuration + */ +export const PROJECT_FILE_CONFIG = { + label: 'Project', + fileName: 'project.mdc', +} as const;