mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: Add project guidelines support and enhance project overview rendering
This commit is contained in:
@@ -110,6 +110,34 @@ interface SessionReviewData {
|
||||
findings: Array<Finding & { dimension: string }>;
|
||||
}
|
||||
|
||||
interface ProjectGuidelines {
|
||||
conventions: {
|
||||
coding_style: string[];
|
||||
naming_patterns: string[];
|
||||
file_structure: string[];
|
||||
documentation: string[];
|
||||
};
|
||||
constraints: {
|
||||
architecture: string[];
|
||||
tech_stack: string[];
|
||||
performance: string[];
|
||||
security: string[];
|
||||
};
|
||||
quality_rules: Array<{ rule: string; scope: string; enforced_by?: string }>;
|
||||
learnings: Array<{
|
||||
date: string;
|
||||
session_id?: string;
|
||||
insight: string;
|
||||
context?: string;
|
||||
category?: string;
|
||||
}>;
|
||||
_metadata?: {
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectOverview {
|
||||
projectName: string;
|
||||
description: string;
|
||||
@@ -144,6 +172,7 @@ interface ProjectOverview {
|
||||
analysis_timestamp: string | null;
|
||||
analysis_mode: string;
|
||||
};
|
||||
guidelines: ProjectGuidelines | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,11 +185,13 @@ export async function aggregateData(sessions: ScanSessionsResult, workflowDir: s
|
||||
// Initialize cache manager
|
||||
const cache = createDashboardCache(workflowDir);
|
||||
|
||||
// Prepare paths to watch for changes
|
||||
// Prepare paths to watch for changes (includes both new dual files and legacy)
|
||||
const watchPaths = [
|
||||
join(workflowDir, 'active'),
|
||||
join(workflowDir, 'archives'),
|
||||
join(workflowDir, 'project.json'),
|
||||
join(workflowDir, 'project-tech.json'),
|
||||
join(workflowDir, 'project-guidelines.json'),
|
||||
join(workflowDir, 'project.json'), // Legacy support
|
||||
...sessions.active.map(s => s.path),
|
||||
...sessions.archived.map(s => s.path)
|
||||
];
|
||||
@@ -516,12 +547,19 @@ function sortTaskIds(a: string, b: string): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load project overview from project.json
|
||||
* Load project overview from project-tech.json and project-guidelines.json
|
||||
* Supports dual file structure with backward compatibility for legacy project.json
|
||||
* @param workflowDir - Path to .workflow directory
|
||||
* @returns Project overview data or null if not found
|
||||
*/
|
||||
function loadProjectOverview(workflowDir: string): ProjectOverview | null {
|
||||
const projectFile = join(workflowDir, 'project.json');
|
||||
const techFile = join(workflowDir, 'project-tech.json');
|
||||
const guidelinesFile = join(workflowDir, 'project-guidelines.json');
|
||||
const legacyFile = join(workflowDir, 'project.json');
|
||||
|
||||
// Check for new dual file structure first, fallback to legacy
|
||||
const useLegacy = !existsSync(techFile) && existsSync(legacyFile);
|
||||
const projectFile = useLegacy ? legacyFile : techFile;
|
||||
|
||||
if (!existsSync(projectFile)) {
|
||||
console.log(`Project file not found at: ${projectFile}`);
|
||||
@@ -532,15 +570,59 @@ function loadProjectOverview(workflowDir: string): ProjectOverview | null {
|
||||
const fileContent = readFileSync(projectFile, 'utf8');
|
||||
const projectData = JSON.parse(fileContent) as Record<string, unknown>;
|
||||
|
||||
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'}`);
|
||||
console.log(`Successfully loaded project overview: ${projectData.project_name || 'Unknown'} (${useLegacy ? 'legacy' : 'tech'})`);
|
||||
|
||||
// Parse tech data (compatible with both legacy and new structure)
|
||||
const overview = projectData.overview as Record<string, unknown> | undefined;
|
||||
const technologyStack = overview?.technology_stack as Record<string, unknown[]> | undefined;
|
||||
const architecture = overview?.architecture as Record<string, unknown> | undefined;
|
||||
const developmentIndex = projectData.development_index as Record<string, unknown[]> | undefined;
|
||||
const statistics = projectData.statistics as Record<string, unknown> | undefined;
|
||||
const technologyAnalysis = projectData.technology_analysis as Record<string, unknown> | undefined;
|
||||
const developmentStatus = projectData.development_status as Record<string, unknown> | undefined;
|
||||
|
||||
// Support both old and new schema field names
|
||||
const technologyStack = (overview?.technology_stack || technologyAnalysis?.technology_stack) as Record<string, unknown[]> | undefined;
|
||||
const architecture = (overview?.architecture || technologyAnalysis?.architecture) as Record<string, unknown> | undefined;
|
||||
const developmentIndex = (projectData.development_index || developmentStatus?.development_index) as Record<string, unknown[]> | undefined;
|
||||
const statistics = (projectData.statistics || developmentStatus?.statistics) as Record<string, unknown> | undefined;
|
||||
const metadata = projectData._metadata as Record<string, unknown> | undefined;
|
||||
|
||||
// Load guidelines from separate file if exists
|
||||
let guidelines: ProjectGuidelines | null = null;
|
||||
if (existsSync(guidelinesFile)) {
|
||||
try {
|
||||
const guidelinesContent = readFileSync(guidelinesFile, 'utf8');
|
||||
const guidelinesData = JSON.parse(guidelinesContent) as Record<string, unknown>;
|
||||
|
||||
const conventions = guidelinesData.conventions as Record<string, string[]> | undefined;
|
||||
const constraints = guidelinesData.constraints as Record<string, string[]> | undefined;
|
||||
|
||||
guidelines = {
|
||||
conventions: {
|
||||
coding_style: conventions?.coding_style || [],
|
||||
naming_patterns: conventions?.naming_patterns || [],
|
||||
file_structure: conventions?.file_structure || [],
|
||||
documentation: conventions?.documentation || []
|
||||
},
|
||||
constraints: {
|
||||
architecture: constraints?.architecture || [],
|
||||
tech_stack: constraints?.tech_stack || [],
|
||||
performance: constraints?.performance || [],
|
||||
security: constraints?.security || []
|
||||
},
|
||||
quality_rules: (guidelinesData.quality_rules as Array<{ rule: string; scope: string; enforced_by?: string }>) || [],
|
||||
learnings: (guidelinesData.learnings as Array<{
|
||||
date: string;
|
||||
session_id?: string;
|
||||
insight: string;
|
||||
context?: string;
|
||||
category?: string;
|
||||
}>) || [],
|
||||
_metadata: guidelinesData._metadata as ProjectGuidelines['_metadata'] | undefined
|
||||
};
|
||||
console.log(`Successfully loaded project guidelines`);
|
||||
} catch (guidelinesErr) {
|
||||
console.error(`Failed to parse project-guidelines.json:`, (guidelinesErr as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectName: (projectData.project_name as string) || 'Unknown',
|
||||
description: (overview?.description as string) || '',
|
||||
@@ -574,10 +656,11 @@ function loadProjectOverview(workflowDir: string): ProjectOverview | null {
|
||||
initialized_by: (metadata?.initialized_by as string) || 'unknown',
|
||||
analysis_timestamp: (metadata?.analysis_timestamp as string) || null,
|
||||
analysis_mode: (metadata?.analysis_mode as string) || 'unknown'
|
||||
}
|
||||
},
|
||||
guidelines
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse project.json at ${projectFile}:`, (err as Error).message);
|
||||
console.error(`Failed to parse project file at ${projectFile}:`, (err as Error).message);
|
||||
console.error('Error stack:', (err as Error).stack);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ function renderProjectOverview() {
|
||||
${renderDevelopmentIndex(project.developmentIndex)}
|
||||
</div>
|
||||
|
||||
<!-- Project Guidelines -->
|
||||
${renderProjectGuidelines(project.guidelines)}
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="bg-card border border-border rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
@@ -248,3 +251,153 @@ function renderDevelopmentIndex(devIndex) {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderProjectGuidelines(guidelines) {
|
||||
if (!guidelines) {
|
||||
return `
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
No guidelines configured. Run <code class="px-2 py-1 bg-muted rounded text-xs font-mono">/session:solidify</code> to add project constraints and conventions.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Count total items
|
||||
const conventionCount = Object.values(guidelines.conventions || {}).flat().length;
|
||||
const constraintCount = Object.values(guidelines.constraints || {}).flat().length;
|
||||
const rulesCount = (guidelines.quality_rules || []).length;
|
||||
const learningsCount = (guidelines.learnings || []).length;
|
||||
const totalCount = conventionCount + constraintCount + rulesCount + learningsCount;
|
||||
|
||||
if (totalCount === 0) {
|
||||
return `
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
|
||||
</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Guidelines file exists but is empty. Run <code class="px-2 py-1 bg-muted rounded text-xs font-mono">/session:solidify</code> to add project constraints and conventions.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="bg-card border border-border rounded-lg p-6 mb-6">
|
||||
<h3 class="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
|
||||
<i data-lucide="scroll-text" class="w-5 h-5"></i> Project Guidelines
|
||||
<span class="text-xs px-2 py-0.5 bg-primary-light text-primary rounded-full">${totalCount} items</span>
|
||||
</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Conventions -->
|
||||
${renderGuidelinesSection('Conventions', guidelines.conventions, 'book-marked', 'bg-success-light text-success', [
|
||||
{ key: 'coding_style', label: 'Coding Style' },
|
||||
{ key: 'naming_patterns', label: 'Naming Patterns' },
|
||||
{ key: 'file_structure', label: 'File Structure' },
|
||||
{ key: 'documentation', label: 'Documentation' }
|
||||
])}
|
||||
|
||||
<!-- Constraints -->
|
||||
${renderGuidelinesSection('Constraints', guidelines.constraints, 'shield-alert', 'bg-destructive/10 text-destructive', [
|
||||
{ key: 'architecture', label: 'Architecture' },
|
||||
{ key: 'tech_stack', label: 'Tech Stack' },
|
||||
{ key: 'performance', label: 'Performance' },
|
||||
{ key: 'security', label: 'Security' }
|
||||
])}
|
||||
|
||||
<!-- Quality Rules -->
|
||||
${renderQualityRules(guidelines.quality_rules)}
|
||||
|
||||
<!-- Learnings -->
|
||||
${renderLearnings(guidelines.learnings)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderGuidelinesSection(title, data, icon, badgeClass, categories) {
|
||||
if (!data) return '';
|
||||
|
||||
const items = categories.flatMap(cat => (data[cat.key] || []).map(item => ({ category: cat.label, value: item })));
|
||||
if (items.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<i data-lucide="${icon}" class="w-4 h-4"></i>
|
||||
<span>${title}</span>
|
||||
<span class="text-xs px-2 py-0.5 ${badgeClass} rounded-full">${items.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${items.slice(0, 8).map(item => `
|
||||
<div class="flex items-start gap-3 p-3 bg-background border border-border rounded-lg">
|
||||
<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded whitespace-nowrap">${escapeHtml(item.category)}</span>
|
||||
<span class="text-sm text-foreground">${escapeHtml(item.value)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
${items.length > 8 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${items.length - 8} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderQualityRules(rules) {
|
||||
if (!rules || rules.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<i data-lucide="check-square" class="w-4 h-4"></i>
|
||||
<span>Quality Rules</span>
|
||||
<span class="text-xs px-2 py-0.5 bg-warning-light text-warning rounded-full">${rules.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${rules.slice(0, 6).map(rule => `
|
||||
<div class="p-3 bg-background border border-border rounded-lg">
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<span class="text-sm text-foreground font-medium">${escapeHtml(rule.rule)}</span>
|
||||
${rule.enforced_by ? `<span class="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">${escapeHtml(rule.enforced_by)}</span>` : ''}
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">Scope: ${escapeHtml(rule.scope)}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
${rules.length > 6 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${rules.length - 6} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLearnings(learnings) {
|
||||
if (!learnings || learnings.length === 0) return '';
|
||||
|
||||
return `
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<i data-lucide="lightbulb" class="w-4 h-4"></i>
|
||||
<span>Session Learnings</span>
|
||||
<span class="text-xs px-2 py-0.5 bg-accent text-accent-foreground rounded-full">${learnings.length}</span>
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
${learnings.slice(0, 5).map(learning => `
|
||||
<div class="p-3 bg-background border border-border rounded-lg border-l-4 border-l-primary">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<span class="text-sm text-foreground">${escapeHtml(learning.insight)}</span>
|
||||
<span class="text-xs text-muted-foreground whitespace-nowrap ml-2">${formatDate(learning.date)}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
${learning.category ? `<span class="px-2 py-0.5 bg-muted text-muted-foreground rounded">${escapeHtml(learning.category)}</span>` : ''}
|
||||
${learning.session_id ? `<span class="px-2 py-0.5 bg-primary-light text-primary rounded font-mono">${escapeHtml(learning.session_id)}</span>` : ''}
|
||||
</div>
|
||||
${learning.context ? `<p class="text-xs text-muted-foreground mt-2">${escapeHtml(learning.context)}</p>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
${learnings.length > 5 ? `<div class="text-sm text-muted-foreground text-center py-2">... and ${learnings.length - 5} more</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user