Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# Changelog

## v4 — Tooling Improvements

- Stronger auto-fix engine with detailed skip reporting
- Monorepo-aware package summaries in CLI and PR comments
- HTML report improvements
- Conservative rules: no-debugger, no-empty-catch, no-useless-return
- Diagnostics-backed analysis via ts-diagnostics rule
- Richer complexity warnings with per-function contributor breakdown
- Export workflow improvements and human summary quality enforcement
- Lightweight repo-pack-latest export mode
- Deploy readiness improvements for the web UI

## v3 — Platform

- Custom rule authoring API (`defineRule()`)
Expand Down
63 changes: 42 additions & 21 deletions ai/scripts/generate-repomix-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,20 @@ function categorizeFiles(files: string[]): Map<string, string[]> {
return groups;
}

// Polished, outcome-focused bullet templates per area
// Polished, outcome-focused bullet templates per area (≥8 words, verb-first)
const AREA_BULLET_TEMPLATES: Record<string, string> = {
core: 'Improved the core analysis engine for better detection accuracy',
cli: 'Enhanced the CLI tool for a smoother command-line experience',
shared: 'Refined shared type definitions across packages',
'vscode-extension': 'Enhanced the VS Code extension for a better in-editor experience',
web: 'Polished the web UI for a more intuitive analysis workflow',
docs: 'Kept documentation aligned with the latest codebase changes',
ai: 'Improved milestone export generation so summaries are cleaner and more reliable',
workflow: 'Strengthened CI/CD pipeline for more reliable automated checks',
examples: 'Updated example fixtures to reflect current rule coverage',
screenshots: 'Refreshed screenshots and demo automation',
root: 'Updated root project configuration for consistency',
other: 'Improved project tooling and configuration',
core: 'Expanded rule coverage with stronger detection accuracy and richer diagnostics',
cli: 'Enhanced the command-line interface for a smoother developer experience',
shared: 'Refined shared type definitions to improve consistency across all packages',
'vscode-extension': 'Improved the VS Code extension for faster in-editor feedback',
web: 'Polished the web interface for a more intuitive analysis workflow',
docs: 'Updated documentation to reflect the latest codebase improvements and conventions',
ai: 'Strengthened the export workflow so summaries are cleaner and more reliable',
workflow: 'Improved the CI pipeline to catch more issues before code ships',
examples: 'Updated example fixtures to demonstrate current rule coverage and patterns',
screenshots: 'Refreshed screenshots and demo automation for accurate visual documentation',
root: 'Updated root project configuration for better workspace consistency',
other: 'Improved project tooling and developer workflow configuration',
};

// Banned patterns — bullets containing any of these are considered noisy/internal
Expand Down Expand Up @@ -283,13 +283,30 @@ function formatGroupedFiles(files: string[]): string {
return lines.join('\n').trim();
}

/** Check whether a bullet meets quality standards for human-readable output. */
function isQualityBullet(bullet: string): boolean {
const trimmed = bullet.trim();
// Must start with a verb (capital letter followed by lowercase)
if (!/^[A-Z][a-z]/.test(trimmed)) return false;
// Must be at least 8 words
if (trimmed.split(/\s+/).length < 8) return false;
// Must not contain file names (e.g. foo.ts, bar.tsx, baz.md)
if (/\b\w+\.\w{1,4}\b/.test(trimmed) && /\.(ts|tsx|js|jsx|md|json|yml|yaml|css|html)/.test(trimmed)) return false;
// Must not contain colon-prefixed commit text (e.g. "feat: ...")
if (/^[a-z]+(\([^)]*\))?:/i.test(trimmed)) return false;
// Must not contain backticks
if (trimmed.includes('`')) return false;
return true;
}

/** Validate that all bullets pass quality rules. Returns list of failing bullets. */
function validateBullets(bullets: string[]): string[] {
const failures: string[] = [];
for (const b of bullets) {
if (isBannedBullet(b)) failures.push(`BANNED: "${b}"`);
if (b.trim().length === 0) failures.push('EMPTY bullet');
if (isTruncatedBullet(b)) failures.push(`TRUNCATED: "${b}"`);
if (!isQualityBullet(b)) failures.push(`LOW_QUALITY: "${b}"`);
}
// Check duplicates
const seen = new Set<string>();
Expand Down Expand Up @@ -344,7 +361,7 @@ function generateHumanSummary(pr: PRInfo, files: string[], _commits: string): st
let bullets = candidates.slice(0, 5);

// Remove any that still fail validation individually
bullets = bullets.filter(b => !isBannedBullet(b) && b.trim().length > 0 && !isTruncatedBullet(b));
bullets = bullets.filter(b => !isBannedBullet(b) && b.trim().length > 0 && !isTruncatedBullet(b) && isQualityBullet(b));

// Deduplicate (case-insensitive)
const deduped: string[] = [];
Expand All @@ -363,9 +380,9 @@ function generateHumanSummary(pr: PRInfo, files: string[], _commits: string): st
bullets = buildAreaBullets(files);
// Always ensure at least 3
const fallbacks = [
'Improved milestone export generation so summaries are cleaner and more reliable',
'Kept workflow documentation aligned with the automated export pipeline',
'Strengthened export quality checks so only clean, polished summaries are produced',
'Strengthened the export workflow so summaries are cleaner and more reliable',
'Updated documentation to reflect the latest codebase improvements and conventions',
'Improved developer feedback with clearer diagnostics and auto-fix reporting',
];
for (const fb of fallbacks) {
if (bullets.length >= 3) break;
Expand Down Expand Up @@ -435,7 +452,7 @@ function validateSummaryContent(content: string): void {
process.exit(1);
}

// Check each bullet against banned patterns and truncation
// Check each bullet against banned patterns, truncation, and quality
for (const bullet of bulletLines) {
if (bullet.length === 0) {
console.error('\nERROR: Human Summary contains an empty bullet');
Expand All @@ -449,6 +466,10 @@ function validateSummaryContent(content: string): void {
console.error(`\nERROR: Human Summary bullet appears truncated: "${bullet}"`);
process.exit(1);
}
if (!isQualityBullet(bullet)) {
console.error(`\nERROR: Human Summary bullet fails quality check (must start with verb, ≥8 words, no file names, no commit prefixes): "${bullet}"`);
process.exit(1);
}
}

// Check for duplicate bullets
Expand Down Expand Up @@ -630,9 +651,9 @@ if (bulletErrors.length > 0) {
humanBullets = buildAreaBullets(milestoneFiles);
// Ensure 3–5 range
const fallbacks = [
'Improved milestone export generation so summaries are cleaner and more reliable',
'Kept workflow documentation aligned with the automated export pipeline',
'Strengthened export quality checks so only clean, polished summaries are produced',
'Strengthened the export workflow so summaries are cleaner and more reliable',
'Updated documentation to reflect the latest codebase improvements and conventions',
'Improved developer feedback with clearer diagnostics and auto-fix reporting',
];
for (const fb of fallbacks) {
if (humanBullets.length >= 3) break;
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function App() {
handleAnalyze,
selectIssue,
exportMarkdown,
loadSampleProject,
} = useAppState();

return (
Expand All @@ -39,6 +40,7 @@ export function App() {
report={state.report}
selectedIssue={state.selectedIssue}
onSelectIssue={selectIssue}
onLoadSample={loadSampleProject}
/>
<DetailsPanel issue={state.selectedIssue} />
</div>
Expand Down
20 changes: 17 additions & 3 deletions apps/web/src/components/MainPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ interface MainPanelProps {
report: AnalysisReport | null;
selectedIssue: Issue | null;
onSelectIssue: (issue: Issue | null) => void;
onLoadSample: () => void;
}

const SEVERITY_LABELS: Record<Severity, string> = {
Expand All @@ -19,11 +20,13 @@ const SEVERITY_COLORS: Record<Severity, string> = {
info: '#2196f3',
};

export function MainPanel({ report, selectedIssue, onSelectIssue }: MainPanelProps) {
export function MainPanel({ report, selectedIssue, onSelectIssue, onLoadSample }: MainPanelProps) {
const [filter, setFilter] = useState<Severity | 'all'>('all');
const [search, setSearch] = useState('');
const [expandedId, setExpandedId] = useState<string | null>(null);

const hasFolderPicker = typeof window !== 'undefined' && 'showDirectoryPicker' in window;

if (!report) {
return (
<main className="main-panel">
Expand All @@ -35,11 +38,22 @@ export function MainPanel({ report, selectedIssue, onSelectIssue }: MainPanelPro
code quality improvements — with proposed fixes, severity scoring, and exportable reports.
</p>
<p className="about-text">
Select a folder and click <strong>Analyze</strong> to get started.
<strong>How to run an analysis:</strong> Click <strong>Select Folder</strong> (Chrome/Edge)
or <strong>Upload Folder</strong> (any browser) to load a project. Pick directories in the
sidebar, then click <strong>Analyze</strong>.
</p>
{!hasFolderPicker && (
<p className="about-hint">
This browser does not support file system analysis features.
Use Chrome or Edge for the best experience, or use the <strong>Upload Folder</strong> fallback.
</p>
)}
<p className="about-hint">
This UI is currently in <strong>Preview</strong> — features are under active development.
This app is currently in <strong>Preview</strong> — features are under active development.
</p>
<button className="btn btn-accent" style={{ marginTop: 12 }} onClick={onLoadSample}>
Try with sample project
</button>
</div>
</div>
</main>
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/useAppState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,55 @@ import { buildDirectoryTree, pickDefaultDirs, analyzeCodebase, buildMarkdownRepo
import type { DirEntry } from '@inspectorepo/core';
import { selectFolderViaAPI, readUploadedFiles, processFiles } from './folder-reader';

// Small inline sample for "Try with sample project"
const SAMPLE_FILES: VirtualFile[] = [
{
path: 'src/utils.ts',
content: [
'import { Logger } from "./logger";',
'',
'export function getUser(data: any) {',
' if (data && data.user && data.user.name) {',
' return data.user.name;',
' }',
' return null;',
'}',
'',
'export function isActive(flag: boolean) {',
' if (flag === true) {',
' return true;',
' }',
' return false;',
'}',
].join('\n'),
},
{
path: 'src/process.ts',
content: [
'export function processItems(items: number[]) {',
' if (items.length > 0) {',
' for (let i = 0; i < items.length; i++) {',
' if (items[i] > 0) {',
' if (items[i] > 10) {',
' for (let j = 0; j < items[i]; j++) {',
' if (j % 2 === 0) {',
' const val = j > 5 ? j * 2 : j;',
' if (val > 3 && val < 100 || val === 0) {',
' console.log(val);',
' }',
' }',
' }',
' }',
' }',
' }',
' }',
' debugger;',
' return;',
'}',
].join('\n'),
},
];

export interface AppState {
folderName: string | null;
allFiles: VirtualFile[];
Expand Down Expand Up @@ -116,6 +165,10 @@ export function useAppState() {

const canAnalyze = state.folderName !== null && state.selectedDirs.length > 0;

const loadSampleProject = useCallback(() => {
loadFolder('sample-project', SAMPLE_FILES);
}, [loadFolder]);

// Dev-only: expose loader for E2E / screenshot automation
useEffect(() => {
if (import.meta.env.DEV) {
Expand All @@ -132,5 +185,6 @@ export function useAppState() {
handleAnalyze,
selectIssue,
exportMarkdown,
loadSampleProject,
};
}
2 changes: 1 addition & 1 deletion packages/core/src/analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ describe('complexity-hotspot rule', () => {
expect(issue.suggestion.details).toMatch(/if statement/);
expect(issue.suggestion.details).toMatch(/Complexity score: \d+/);
// Suggestion should be specific, not generic
expect(issue.suggestion.summary).toMatch(/Consider:/);
expect(issue.suggestion.summary).toMatch(/Suggested improvements:/);
});

it('includes contributor counts in details', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ describe('mergeConfig', () => {
const result = mergeConfig(null);
expect(result['unused-imports']).toBe('warn');
expect(result['early-return']).toBe('warn');
expect(result['no-debugger']).toBe('warn');
expect(result['no-empty-catch']).toBe('warn');
expect(result['no-useless-return']).toBe('warn');
expect(result['ts-diagnostics']).toBe('off');
});

it('overrides defaults with loaded config', () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ const DEFAULT_CONFIG: RuleConfig = {
'optional-chaining': 'warn',
'boolean-simplification': 'warn',
'early-return': 'warn',
'no-debugger': 'warn',
'no-empty-catch': 'warn',
'no-useless-return': 'warn',
'ts-diagnostics': 'off',
};

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/rules/complexity-hotspot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ function buildSuggestion(b: ComplexityBreakdown): string {
const tips: string[] = [];

if (top.includes('nested conditionals') || top.includes('ternaries')) {
tips.push('replace nested conditionals with early returns');
tips.push('replace nested conditions with early returns');
}
if (top.includes('loops')) {
tips.push('extract loop bodies into helper functions');
Expand All @@ -136,7 +136,7 @@ function buildSuggestion(b: ComplexityBreakdown): string {
tips.push('extract helper functions to reduce complexity');
}

return `Consider: ${tips.join('; ')}.`;
return `Suggested improvements: ${tips.join('; ')}.`;
}

export const complexityHotspotRule: Rule = {
Expand Down Expand Up @@ -203,7 +203,7 @@ export const complexityHotspotRule: Rule = {
},
suggestion: {
summary: buildSuggestion(breakdown),
details: `Complexity score: ${score} (threshold: ${COMPLEXITY_THRESHOLD}). Contributors: ${contributors}${nestingNote}.`,
details: `Complexity score: ${score} (threshold: ${COMPLEXITY_THRESHOLD}). Primary contributors: ${contributors}${nestingNote}.`,
},
});
}
Expand Down
Loading