feat: Add project guidelines support and enhance project overview rendering

This commit is contained in:
catlog22
2025-12-28 14:50:50 +08:00
parent e16950ef1e
commit a2c88ba885
4 changed files with 251 additions and 12 deletions

View File

@@ -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;
}

View File

@@ -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>
`;
}

View File

@@ -41,6 +41,7 @@ src/codexlens/semantic/vector_store.py
src/codexlens/storage/__init__.py
src/codexlens/storage/dir_index.py
src/codexlens/storage/file_cache.py
src/codexlens/storage/global_index.py
src/codexlens/storage/index_tree.py
src/codexlens/storage/migration_manager.py
src/codexlens/storage/path_mapper.py
@@ -64,6 +65,8 @@ tests/test_enrichment.py
tests/test_entities.py
tests/test_errors.py
tests/test_file_cache.py
tests/test_global_index.py
tests/test_global_symbol_index.py
tests/test_hybrid_chunker.py
tests/test_hybrid_search_e2e.py
tests/test_incremental_indexing.py

View File

@@ -1,6 +1,6 @@
{
"name": "claude-code-workflow",
"version": "6.3.9",
"version": "6.3.10",
"description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
"type": "module",
"main": "ccw/src/index.js",