diff --git a/.claude/commands/issue/discover.md b/.claude/commands/issue/discover.md new file mode 100644 index 00000000..e520f49b --- /dev/null +++ b/.claude/commands/issue/discover.md @@ -0,0 +1,703 @@ +--- +name: issue:discover +description: Discover potential issues from multiple perspectives (bug, UX, test, quality, security, performance, maintainability, best-practices) using CLI explore. Supports Exa external research for security and best-practices perspectives. +argument-hint: " [--perspectives=bug,ux,...] [--external]" +allowed-tools: SlashCommand(*), TodoWrite(*), Read(*), Bash(*), Task(*), AskUserQuestion(*), Glob(*), Grep(*) +--- + +# Issue Discovery Command + +## Quick Start + +```bash +# Discover issues in specific module (interactive perspective selection) +/issue:discover src/auth/** + +# Discover with specific perspectives +/issue:discover src/payment/** --perspectives=bug,security,test + +# Discover with external research for all perspectives +/issue:discover src/api/** --external + +# Discover in multiple modules +/issue:discover src/auth/**,src/payment/** +``` + +**Discovery Scope**: Specified modules/files only +**Output Directory**: `.workflow/issues/discoveries/{discovery-id}/` +**Available Perspectives**: bug, ux, test, quality, security, performance, maintainability, best-practices +**Exa Integration**: Auto-enabled for security and best-practices perspectives +**CLI Tools**: Gemini → Qwen → Codex (fallback chain) + +## What & Why + +### Core Concept +Multi-perspective issue discovery orchestrator that explores code from different angles to identify potential bugs, UX improvements, test gaps, and other actionable items. Unlike code review (which assesses existing code quality), discovery focuses on **finding opportunities for improvement and potential problems**. + +**vs Code Review**: +- **Code Review** (`review-module-cycle`): Evaluates code quality against standards +- **Issue Discovery** (`issue:discover`): Finds actionable issues, bugs, and improvement opportunities + +### Value Proposition +1. **Proactive Issue Detection**: Find problems before they become bugs +2. **Multi-Perspective Analysis**: Each perspective surfaces different types of issues +3. **External Benchmarking**: Compare against industry best practices via Exa +4. **Direct Issue Integration**: Discoveries can be exported to issue tracker +5. **Dashboard Management**: View, filter, and export discoveries via CCW dashboard + +## How It Works + +### Execution Flow + +``` +Phase 1: Discovery & Initialization + └─ Parse target pattern, create session, initialize output structure + +Phase 2: Interactive Perspective Selection + └─ AskUserQuestion for perspective selection (or use --perspectives) + +Phase 3: Parallel Perspective Analysis + ├─ Launch N @cli-explore-agent instances (one per perspective) + ├─ Security & Best-Practices auto-trigger Exa research + ├─ Generate perspective JSON + markdown reports + └─ Update discovery-progress.json + +Phase 4: Aggregation & Prioritization + ├─ Load all perspective JSON files + ├─ Merge findings, deduplicate by file+line + ├─ Calculate priority scores based on impact/urgency + └─ Generate candidate issue list + +Phase 5: Issue Generation + ├─ Convert high-priority discoveries to issue format + ├─ Write to discovery-issues.jsonl (preview) + └─ Generate summary report +``` + +## Perspectives + +### Available Perspectives + +| Perspective | Focus | Categories | Exa | +|-------------|-------|------------|-----| +| **bug** | Potential Bugs | edge-case, null-check, resource-leak, race-condition, boundary, exception-handling | - | +| **ux** | User Experience | error-message, loading-state, feedback, accessibility, interaction, consistency | - | +| **test** | Test Coverage | missing-test, edge-case-test, integration-gap, coverage-hole, assertion-quality | - | +| **quality** | Code Quality | complexity, duplication, naming, documentation, code-smell, readability | - | +| **security** | Security Issues | injection, auth, encryption, input-validation, data-exposure, access-control | ✓ | +| **performance** | Performance | n-plus-one, memory-usage, caching, algorithm, blocking-operation, resource | - | +| **maintainability** | Maintainability | coupling, cohesion, tech-debt, extensibility, module-boundary, interface-design | - | +| **best-practices** | Best Practices | convention, pattern, framework-usage, anti-pattern, industry-standard | ✓ | + +### Interactive Perspective Selection + +When no `--perspectives` flag is provided, the command uses AskUserQuestion: + +```javascript +AskUserQuestion({ + questions: [{ + question: "Select discovery perspectives (multi-select)", + header: "Perspectives", + multiSelect: true, + options: [ + { label: "bug", description: "Potential bugs (edge cases, null checks, resource leaks)" }, + { label: "ux", description: "User experience (error messages, loading states, accessibility)" }, + { label: "test", description: "Test coverage (missing tests, edge cases, integration gaps)" }, + { label: "quality", description: "Code quality (complexity, duplication, naming)" }, + { label: "security", description: "Security issues (auto-enables Exa research)" }, + { label: "performance", description: "Performance (N+1 queries, memory, caching)" }, + { label: "maintainability", description: "Maintainability (coupling, tech debt, extensibility)" }, + { label: "best-practices", description: "Best practices (auto-enables Exa research)" } + ] + }] +}) +``` + +**Recommended Combinations**: +- Quick scan: bug, test, quality +- Full analysis: all perspectives +- Security audit: security, bug, quality + +## Core Responsibilities + +### Orchestrator + +**Phase 1: Discovery & Initialization** + +```javascript +// Step 1: Parse target pattern and resolve files +const resolvedFiles = await expandGlobPattern(targetPattern); +if (resolvedFiles.length === 0) { + throw new Error(`No files matched pattern: ${targetPattern}`); +} + +// Step 2: Generate discovery ID +const discoveryId = `DSC-${formatDate(new Date(), 'YYYYMMDD-HHmmss')}`; + +// Step 3: Create output directory +const outputDir = `.workflow/issues/discoveries/${discoveryId}`; +await mkdir(outputDir, { recursive: true }); +await mkdir(`${outputDir}/perspectives`, { recursive: true }); +await mkdir(`${outputDir}/reports`, { recursive: true }); + +// Step 4: Initialize discovery state +await writeJson(`${outputDir}/discovery-state.json`, { + discovery_id: discoveryId, + target_pattern: targetPattern, + metadata: { + created_at: new Date().toISOString(), + resolved_files: resolvedFiles, + perspectives: [], // filled after selection + external_research_enabled: false + }, + phase: "initialization", + perspectives_completed: [], + total_findings: 0, + priority_distribution: { critical: 0, high: 0, medium: 0, low: 0 }, + issues_generated: 0 +}); + +// Step 5: Initialize progress tracking +await writeJson(`${outputDir}/discovery-progress.json`, { + discovery_id: discoveryId, + last_update: new Date().toISOString(), + phase: "initialization", + progress: { + perspective_analysis: { total: 0, completed: 0, in_progress: 0, percent_complete: 0 }, + external_research: { enabled: false, completed: false }, + aggregation: { completed: false }, + issue_generation: { completed: false } + }, + agent_status: [] +}); +``` + +**Phase 2: Perspective Selection** + +```javascript +// Check for --perspectives flag +let selectedPerspectives = []; + +if (args.perspectives) { + selectedPerspectives = args.perspectives.split(',').map(p => p.trim()); +} else { + // Interactive selection via AskUserQuestion + const response = await AskUserQuestion({ + questions: [{ + question: "Select discovery perspectives to analyze:", + header: "Perspectives", + multiSelect: true, + options: PERSPECTIVE_OPTIONS + }] + }); + selectedPerspectives = parseSelectedPerspectives(response); +} + +// Validate perspectives +const validPerspectives = ['bug', 'ux', 'test', 'quality', 'security', 'performance', 'maintainability', 'best-practices']; +for (const p of selectedPerspectives) { + if (!validPerspectives.includes(p)) { + throw new Error(`Invalid perspective: ${p}`); + } +} + +// Determine if Exa is needed +const exaEnabled = selectedPerspectives.includes('security') || + selectedPerspectives.includes('best-practices') || + args.external; + +// Update state +await updateDiscoveryState(outputDir, { + 'metadata.perspectives': selectedPerspectives, + 'metadata.external_research_enabled': exaEnabled, + phase: 'parallel' +}); +``` + +**Phase 3: Parallel Perspective Analysis** + +Launch N agents in parallel (one per selected perspective): + +```javascript +// Launch agents in parallel +const agentPromises = selectedPerspectives.map(perspective => + Task({ + subagent_type: "cli-explore-agent", + run_in_background: false, + description: `Discover ${perspective} issues via Deep Scan`, + prompt: buildPerspectivePrompt(perspective, discoveryId, resolvedFiles, outputDir) + }) +); + +// For perspectives with Exa enabled, add external research +if (exaEnabled) { + for (const perspective of ['security', 'best-practices']) { + if (selectedPerspectives.includes(perspective)) { + agentPromises.push( + Task({ + subagent_type: "cli-explore-agent", + run_in_background: false, + description: `External research for ${perspective} via Exa`, + prompt: buildExaResearchPrompt(perspective, projectTech, outputDir) + }) + ); + } + } +} + +// Wait for all agents +const results = await Promise.all(agentPromises); +``` + +**Phase 4: Aggregation & Prioritization** + +```javascript +// Load all perspective results +const allFindings = []; +for (const perspective of selectedPerspectives) { + const jsonPath = `${outputDir}/perspectives/${perspective}.json`; + if (await fileExists(jsonPath)) { + const data = await readJson(jsonPath); + allFindings.push(...data.findings.map(f => ({ ...f, perspective }))); + } +} + +// Deduplicate by file+line +const uniqueFindings = deduplicateFindings(allFindings); + +// Calculate priority scores +const prioritizedFindings = uniqueFindings.map(finding => ({ + ...finding, + priority_score: calculatePriorityScore(finding) +})).sort((a, b) => b.priority_score - a.priority_score); + +// Update state with aggregation results +await updateDiscoveryState(outputDir, { + phase: 'aggregation', + total_findings: prioritizedFindings.length, + priority_distribution: countByPriority(prioritizedFindings) +}); +``` + +**Phase 5: Issue Generation** + +```javascript +// Filter high-priority findings for issue generation +const issueWorthy = prioritizedFindings.filter(f => + f.priority === 'critical' || f.priority === 'high' || f.priority_score >= 0.7 +); + +// Convert to issue format +const issues = issueWorthy.map((finding, idx) => ({ + id: `DSC-${String(idx + 1).padStart(3, '0')}`, + title: finding.suggested_issue?.title || finding.title, + status: 'discovered', + priority: mapPriorityToNumber(finding.priority), + source: 'discovery', + source_discovery_id: discoveryId, + perspective: finding.perspective, + context: finding.description, + labels: [finding.perspective, ...(finding.labels || [])], + file: finding.file, + line: finding.line, + created_at: new Date().toISOString() +})); + +// Write discovery issues (preview, not committed to main issues.jsonl) +await writeJsonl(`${outputDir}/discovery-issues.jsonl`, issues); + +// Generate summary report +await generateSummaryReport(outputDir, prioritizedFindings, issues); + +// Update final state +await updateDiscoveryState(outputDir, { + phase: 'complete', + issues_generated: issues.length +}); + +// Update index +await updateDiscoveryIndex(outputDir, discoveryId, { + target_pattern: targetPattern, + perspectives: selectedPerspectives, + total_findings: prioritizedFindings.length, + issues_generated: issues.length, + completed_at: new Date().toISOString() +}); +``` + +### Output File Structure + +``` +.workflow/issues/discoveries/ +├── index.json # Discovery session index +└── {discovery-id}/ + ├── discovery-state.json # State machine + ├── discovery-progress.json # Real-time progress (dashboard polling) + ├── perspectives/ + │ ├── bug.json + │ ├── ux.json + │ ├── test.json + │ ├── quality.json + │ ├── security.json + │ ├── performance.json + │ ├── maintainability.json + │ └── best-practices.json + ├── external-research.json # Exa research results + ├── discovery-issues.jsonl # Generated candidate issues + └── reports/ + ├── summary.md + ├── bug-report.md + └── {perspective}-report.md +``` + +### Schema References + +**External Schema Files** (agent MUST read and follow exactly): + +| Schema | Path | Purpose | +|--------|------|---------| +| **Discovery State** | `~/.claude/workflows/cli-templates/schemas/discovery-state-schema.json` | Session state machine | +| **Discovery Finding** | `~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json` | Perspective analysis results | + +**Agent Schema Loading Protocol**: +```bash +# Agent MUST read schema before generating any JSON output +cat ~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +``` + +### Agent Invocation Template + +**Perspective Analysis Agent**: + +```javascript +Task({ + subagent_type: "cli-explore-agent", + run_in_background: false, + description: `Discover ${perspective} issues via Deep Scan`, + prompt: ` + ## Task Objective + Discover potential ${perspective} issues in specified module files using Deep Scan mode (Bash + Gemini dual-source strategy) + + ## Analysis Mode + Use **Deep Scan mode** for this discovery: + - Phase 1: Bash structural scan for standard patterns + - Phase 2: Gemini semantic analysis for ${perspective}-specific concerns + - Phase 3: Synthesis with attribution + + ## MANDATORY FIRST STEPS + 1. Read discovery state: ${discoveryStateJsonPath} + 2. Get target files from discovery-state.json + 3. Validate file access: bash(ls -la ${targetFiles.join(' ')}) + 4. **CRITICAL**: Read schema FIRST: cat ~/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json + 5. Read: .workflow/project-tech.json (technology stack) + + ## Discovery Context + - Discovery ID: ${discoveryId} + - Perspective: ${perspective} + - Target Pattern: ${targetPattern} + - Resolved Files: ${resolvedFiles.length} files + - Output Directory: ${outputDir} + + ## CLI Configuration + - Tool Priority: gemini → qwen → codex + - Mode: analysis (READ-ONLY for code analysis, WRITE for output files) + - Context Pattern: ${targetFiles.map(f => `@${f}`).join(' ')} + + ## ⚠️ CRITICAL OUTPUT GUIDELINES + + **Agent MUST write JSON files directly - DO NOT return JSON to orchestrator**: + + 1. **Schema Compliance**: Read and strictly follow discovery-finding-schema.json + - All required fields MUST be present + - Use exact enum values (lowercase priority: critical/high/medium/low) + - ID format: dsc-{perspective}-{seq}-{uuid8} + + 2. **Direct File Output**: Agent writes files using Write/mcp__ccw-tools__write_file: + - JSON: ${outputDir}/perspectives/${perspective}.json + - Report: ${outputDir}/reports/${perspective}-report.md + - DO NOT return raw JSON in response - write to file + + 3. **Validation Before Write**: + - Validate JSON against schema structure + - Ensure all findings have required fields + - Verify file paths are relative to project root + + 4. **Progress Update**: After writing, update discovery-progress.json: + - Set perspective status to "completed" + - Update findings_count + - Update completed_at timestamp + + ## Expected Deliverables + + 1. Perspective Results JSON: ${outputDir}/perspectives/${perspective}.json + - Follow discovery-finding-schema.json exactly + - Root structure MUST be object with findings array + - Each finding MUST include: id, title, priority, category, description, file, line, snippet, suggested_issue, confidence + + 2. Discovery Report: ${outputDir}/reports/${perspective}-report.md + - Human-readable summary + - Grouped by priority + - Include file:line references + + ## Perspective-Specific Guidance + ${getPerspectiveGuidance(perspective)} + + ## Success Criteria + - [ ] Schema read and understood before analysis + - [ ] All target files analyzed for ${perspective} concerns + - [ ] JSON written directly to ${outputDir}/perspectives/${perspective}.json + - [ ] Report written to ${outputDir}/reports/${perspective}-report.md + - [ ] Each finding includes actionable suggested_issue + - [ ] Priority assessment is accurate (lowercase enum values) + - [ ] Recommendations are specific and implementable + - [ ] discovery-progress.json updated with completion status + ` +}) +``` + +**Exa Research Agent** (for security and best-practices): + +```javascript +Task({ + subagent_type: "cli-explore-agent", + run_in_background: false, + description: `External research for ${perspective} via Exa`, + prompt: ` + ## Task Objective + Research industry best practices and common patterns for ${perspective} using Exa search + + ## MANDATORY FIRST STEPS + 1. Read project tech stack: .workflow/project-tech.json + 2. Read external research schema structure (if exists) + 3. Identify key technologies (e.g., Node.js, React, Express) + + ## Research Steps + 1. Use Exa to search for: + - "${technology} ${perspective} best practices 2025" + - "${technology} common ${perspective} issues" + - "${technology} ${perspective} checklist" + 2. Synthesize findings relevant to this project + + ## ⚠️ CRITICAL OUTPUT GUIDELINES + + **Agent MUST write files directly - DO NOT return content to orchestrator**: + + 1. **Direct File Output**: Agent writes files using Write/mcp__ccw-tools__write_file: + - JSON: ${outputDir}/external-research.json + - Report: ${outputDir}/reports/${perspective}-external.md + - DO NOT return raw content in response - write to file + + 2. **JSON Structure for external-research.json**: + \`\`\`json + { + "discovery_id": "${discoveryId}", + "perspective": "${perspective}", + "research_timestamp": "ISO8601", + "sources": [ + { "title": "...", "url": "...", "relevance": "..." } + ], + "key_findings": [...], + "gap_analysis": [...], + "recommendations": [...] + } + \`\`\` + + 3. **Progress Update**: After writing, update discovery-progress.json: + - Set external_research.completed to true + + ## Expected Deliverables + + 1. External Research JSON: ${outputDir}/external-research.json + - Sources with URLs + - Key findings + - Relevance to current codebase + + 2. Comparison report in ${outputDir}/reports/${perspective}-external.md + - Industry standards vs current implementation + - Gap analysis + - Prioritized recommendations + + ## Success Criteria + - [ ] At least 3 authoritative sources consulted + - [ ] JSON written directly to ${outputDir}/external-research.json + - [ ] Report written to ${outputDir}/reports/${perspective}-external.md + - [ ] Findings are relevant to project's tech stack + - [ ] Recommendations are actionable + - [ ] discovery-progress.json updated + ` +}) +``` + +### Perspective Guidance Reference + +```javascript +function getPerspectiveGuidance(perspective) { + const guidance = { + bug: ` + Focus Areas: + - Null/undefined checks before property access + - Edge cases in conditionals (empty arrays, 0 values, empty strings) + - Resource leaks (unclosed connections, streams, file handles) + - Race conditions in async code + - Boundary conditions (array indices, date ranges) + - Exception handling gaps (missing try-catch, swallowed errors) + + Priority Criteria: + - Critical: Data corruption, security bypass, system crash + - High: Feature malfunction, data loss potential + - Medium: Unexpected behavior in edge cases + - Low: Minor inconsistencies, cosmetic issues + `, + ux: ` + Focus Areas: + - Error messages (are they user-friendly and actionable?) + - Loading states (are long operations indicated?) + - Feedback (do users know their action succeeded?) + - Accessibility (keyboard navigation, screen readers, color contrast) + - Interaction patterns (consistent behavior across the app) + - Form validation (immediate feedback, clear requirements) + + Priority Criteria: + - Critical: Inaccessible features, misleading feedback + - High: Confusing error messages, missing loading states + - Medium: Inconsistent patterns, minor feedback issues + - Low: Cosmetic improvements, nice-to-haves + `, + test: ` + Focus Areas: + - Missing unit tests for public functions + - Edge case coverage (null, empty, boundary values) + - Integration test gaps (API endpoints, database operations) + - Coverage holes in critical paths (auth, payment, data mutation) + - Assertion quality (are tests actually verifying behavior?) + - Test isolation (do tests depend on each other?) + + Priority Criteria: + - Critical: No tests for security-critical code + - High: Missing tests for core business logic + - Medium: Edge cases not covered, weak assertions + - Low: Minor coverage gaps, test organization issues + `, + quality: ` + Focus Areas: + - Cyclomatic complexity (deeply nested conditionals) + - Code duplication (copy-pasted logic) + - Naming (unclear variable/function names) + - Documentation (missing JSDoc for public APIs) + - Code smells (long functions, large files, magic numbers) + - Readability (overly clever code, unclear intent) + + Priority Criteria: + - Critical: Unmaintainable complexity blocking changes + - High: Significant duplication, confusing logic + - Medium: Naming issues, missing documentation + - Low: Minor refactoring opportunities + `, + security: ` + Focus Areas: + - Input validation and sanitization + - Authentication and authorization mechanisms + - SQL/NoSQL injection vulnerabilities + - XSS, CSRF vulnerabilities + - Sensitive data exposure (logs, errors, responses) + - Access control gaps + + Priority Criteria: + - Critical: Authentication bypass, injection, RCE + - High: Missing authorization, exposed secrets + - Medium: Missing input validation, weak encryption + - Low: Security headers, verbose errors + `, + performance: ` + Focus Areas: + - N+1 query problems in ORM usage + - Memory usage patterns (large objects, memory leaks) + - Caching opportunities (repeated computations, API calls) + - Algorithm efficiency (O(n²) where O(n log n) possible) + - Blocking operations on main thread + - Resource usage (CPU, network, disk I/O) + + Priority Criteria: + - Critical: Memory leaks, blocking main thread + - High: N+1 queries, inefficient algorithms in hot paths + - Medium: Missing caching, suboptimal data structures + - Low: Minor optimization opportunities + `, + maintainability: ` + Focus Areas: + - Module coupling (tight dependencies between unrelated modules) + - Interface design (unclear contracts, leaky abstractions) + - Technical debt indicators (TODOs, FIXMEs, temporary solutions) + - Extensibility (hard to add new features without touching core) + - Module boundaries (unclear separation of responsibilities) + - Configuration management (hardcoded values, environment handling) + + Priority Criteria: + - Critical: Changes require touching unrelated code + - High: Unclear module boundaries, significant tech debt + - Medium: Minor coupling issues, configuration problems + - Low: Refactoring opportunities, documentation gaps + `, + 'best-practices': ` + Focus Areas: + - Framework conventions (are we using the framework idiomatically?) + - Language patterns (modern JS/TS features, async/await usage) + - Anti-patterns (god objects, callback hell, mutation of shared state) + - Deprecated API usage (using old APIs when new ones available) + - Industry standards (OWASP for security, WCAG for accessibility) + - Coding standards (consistent style, ESLint/Prettier compliance) + + Priority Criteria: + - Critical: Anti-patterns causing bugs, deprecated security APIs + - High: Major convention violations, poor patterns + - Medium: Minor style issues, suboptimal patterns + - Low: Cosmetic improvements + ` + }; + + return guidance[perspective] || 'General code discovery analysis'; +} +``` + +## Dashboard Integration + +### Viewing Discoveries + +Open CCW dashboard to manage discoveries: + +```bash +ccw view +``` + +Navigate to **Issues > Discovery** to: +- View all discovery sessions +- Filter findings by perspective and priority +- Preview finding details +- Select and export findings as issues +- Dismiss irrelevant findings + +### Exporting to Issues + +From the dashboard, select findings and click "Export as Issues" to: +1. Convert discoveries to standard issue format +2. Append to `.workflow/issues/issues.jsonl` +3. Set status to `registered` +4. Continue with `/issue:plan` workflow + +## Related Commands + +```bash +# After discovery, plan solutions for exported issues +/issue:plan DSC-001,DSC-002,DSC-003 + +# Or use interactive management +/issue:manage +``` + +## Best Practices + +1. **Start Focused**: Begin with specific modules rather than entire codebase +2. **Use Quick Scan First**: Start with bug, test, quality for fast results +3. **Review Before Export**: Not all discoveries warrant issues - use dashboard to filter +4. **Combine Perspectives**: Run related perspectives together (e.g., security + bug) +5. **Enable Exa for New Tech**: When using unfamiliar frameworks, enable external research diff --git a/.claude/skills/software-manual/SKILL.md b/.claude/skills/software-manual/SKILL.md new file mode 100644 index 00000000..38c5d2e1 --- /dev/null +++ b/.claude/skills/software-manual/SKILL.md @@ -0,0 +1,177 @@ +--- +name: software-manual +description: Generate interactive TiddlyWiki-style HTML software manuals with screenshots, API docs, and multi-level code examples. Use when creating user guides, software documentation, or API references. Triggers on "software manual", "user guide", "generate manual", "create docs". +allowed-tools: Task, AskUserQuestion, Read, Bash, Glob, Grep, Write, mcp__chrome__* +--- + +# Software Manual Skill + +Generate comprehensive, interactive software manuals in TiddlyWiki-style single-file HTML format. + +## Architecture Overview + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Context-Optimized Architecture │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ Phase 1: Requirements → manual-config.json │ +│ ↓ │ +│ Phase 2: Exploration → exploration-*.json │ +│ ↓ │ +│ Phase 3: Parallel Agents → sections/section-*.md │ +│ ↓ (6 Agents) │ +│ Phase 3.5: Consolidation → consolidation-summary.md │ +│ ↓ │ +│ Phase 4: Screenshot → screenshots/*.png │ +│ Capture (via Chrome MCP) │ +│ ↓ │ +│ Phase 5: HTML Assembly → {name}-使用手册.html │ +│ ↓ │ +│ Phase 6: Refinement → iterations/ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Key Design Principles + +1. **主 Agent 编排,子 Agent 执行**: 所有繁重计算委托给 `universal-executor` 子 Agent +2. **Brief Returns**: Agents return path + summary, not full content (avoid context overflow) +3. **System Agents**: 使用 `cli-explore-agent` (探索) 和 `universal-executor` (执行) +4. **Chrome MCP Integration**: Batch screenshot capture with Base64 embedding +5. **Single-File HTML**: TiddlyWiki-style interactive document with embedded resources +6. **User-Friendly Writing**: Clear, step-by-step guides with difficulty levels + +## Execution Flow + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Phase 1: Requirements Discovery (主 Agent) │ +│ → AskUserQuestion: 收集软件类型、目标用户、文档范围 │ +│ → Output: manual-config.json │ +├─────────────────────────────────────────────────────────────────┤ +│ Phase 2: Project Exploration (cli-explore-agent × N) │ +│ → 并行探索: architecture, ui-routes, api-endpoints, config │ +│ → Output: exploration-*.json │ +├─────────────────────────────────────────────────────────────────┤ +│ Phase 3: Parallel Analysis (universal-executor × 6) │ +│ → 6 个子 Agent 并行: overview, ui-guide, api-docs, config, │ +│ troubleshooting, code-examples │ +│ → Output: sections/section-*.md │ +├─────────────────────────────────────────────────────────────────┤ +│ Phase 3.5: Consolidation (universal-executor) │ +│ → 质量检查: 一致性、交叉引用、截图标记 │ +│ → Output: consolidation-summary.md, screenshots-list.json │ +├─────────────────────────────────────────────────────────────────┤ +│ Phase 4: Screenshot Capture (主 Agent + Chrome MCP) │ +│ → 批量截图: 根据 screenshots-list.json │ +│ → Output: screenshots/*.png + manifest.json │ +├─────────────────────────────────────────────────────────────────┤ +│ Phase 5: HTML Assembly (universal-executor) │ +│ → 组装 HTML: MD→tiddlers, 嵌入 CSS/JS/图片 │ +│ → Output: {name}-使用手册.html │ +├─────────────────────────────────────────────────────────────────┤ +│ Phase 6: Iterative Refinement (主 Agent) │ +│ → 预览 + 用户反馈 + 迭代修复 │ +│ → Output: iterations/v*.html │ +└─────────────────────────────────────────────────────────────────┘ +``` + +## Agent Configuration + +| Agent | Role | Output File | Focus Areas | +|-------|------|-------------|-------------| +| overview | Product Manager | section-overview.md | Product intro, features, quick start | +| ui-guide | UX Expert | section-ui-guide.md | UI operations, step-by-step guides | +| api-docs | API Architect | section-api-reference.md | REST API, Frontend API | +| config | DevOps Engineer | section-configuration.md | Env vars, deployment, settings | +| troubleshooting | Support Engineer | section-troubleshooting.md | FAQs, error codes, solutions | +| code-examples | Developer Advocate | section-examples.md | Beginner/Intermediate/Advanced examples | + +## Agent Return Format + +```typescript +interface ManualAgentReturn { + status: "completed" | "partial" | "failed"; + output_file: string; + summary: string; // Max 50 chars + screenshots_needed: Array<{ + id: string; // e.g., "ss-login-form" + url: string; // Relative or absolute URL + description: string; // "Login form interface" + selector?: string; // CSS selector for partial screenshot + wait_for?: string; // Element to wait for + }>; + cross_references: string[]; // Other sections referenced + difficulty_level: "beginner" | "intermediate" | "advanced"; +} +``` + +## HTML Features (TiddlyWiki-style) + +1. **Search**: Full-text search with result highlighting +2. **Collapse/Expand**: Per-section collapsible content +3. **Tag Navigation**: Filter by category tags +4. **Theme Toggle**: Light/Dark mode with localStorage persistence +5. **Single File**: All CSS/JS/images embedded as Base64 +6. **Offline**: Works without internet connection +7. **Print-friendly**: Optimized print stylesheet + +## Directory Setup + +```javascript +// Generate timestamp directory name +const timestamp = new Date().toISOString().slice(0,19).replace(/[-:T]/g, ''); +const dir = `.workflow/.scratchpad/manual-${timestamp}`; + +// Windows +Bash(`mkdir "${dir}\\sections" && mkdir "${dir}\\screenshots" && mkdir "${dir}\\api-docs" && mkdir "${dir}\\iterations"`); +``` + +## Output Structure + +``` +.workflow/.scratchpad/manual-{timestamp}/ +├── manual-config.json # Phase 1 +├── exploration/ # Phase 2 +│ ├── exploration-architecture.json +│ ├── exploration-ui-routes.json +│ └── exploration-api-endpoints.json +├── sections/ # Phase 3 +│ ├── section-overview.md +│ ├── section-ui-guide.md +│ ├── section-api-reference.md +│ ├── section-configuration.md +│ ├── section-troubleshooting.md +│ └── section-examples.md +├── consolidation-summary.md # Phase 3.5 +├── api-docs/ # API documentation +│ ├── frontend/ # TypeDoc output +│ └── backend/ # Swagger/OpenAPI output +├── screenshots/ # Phase 4 +│ ├── ss-*.png +│ └── screenshots-manifest.json +├── iterations/ # Phase 6 +│ ├── v1.html +│ └── v2.html +└── {软件名}-使用手册.html # Final Output +``` + +## Reference Documents + +| Document | Purpose | +|----------|---------| +| [phases/01-requirements-discovery.md](phases/01-requirements-discovery.md) | User config collection | +| [phases/02-project-exploration.md](phases/02-project-exploration.md) | Project type detection | +| [phases/03-parallel-analysis.md](phases/03-parallel-analysis.md) | 6 Agent orchestration | +| [phases/03.5-consolidation.md](phases/03.5-consolidation.md) | Cross-section synthesis | +| [phases/04-screenshot-capture.md](phases/04-screenshot-capture.md) | Chrome MCP integration | +| [phases/05-html-assembly.md](phases/05-html-assembly.md) | HTML generation | +| [phases/06-iterative-refinement.md](phases/06-iterative-refinement.md) | Quality iteration | +| [specs/quality-standards.md](specs/quality-standards.md) | Quality gates | +| [specs/writing-style.md](specs/writing-style.md) | User-friendly writing | +| [specs/html-template.md](specs/html-template.md) | HTML template spec | +| [templates/tiddlywiki-shell.html](templates/tiddlywiki-shell.html) | HTML template | +| [scripts/typedoc-runner.md](scripts/typedoc-runner.md) | TypeDoc execution | +| [scripts/swagger-runner.md](scripts/swagger-runner.md) | Swagger/OpenAPI | +| [scripts/screenshot-helper.md](scripts/screenshot-helper.md) | Chrome MCP guide | diff --git a/.claude/skills/software-manual/phases/01-requirements-discovery.md b/.claude/skills/software-manual/phases/01-requirements-discovery.md new file mode 100644 index 00000000..edb4edbe --- /dev/null +++ b/.claude/skills/software-manual/phases/01-requirements-discovery.md @@ -0,0 +1,162 @@ +# Phase 1: Requirements Discovery + +Collect user requirements and generate configuration for the manual generation process. + +## Objective + +Gather essential information about the software project to customize the manual generation: +- Software type and characteristics +- Target user audience +- Documentation scope and depth +- Special requirements + +## Execution Steps + +### Step 1: Software Information Collection + +Use `AskUserQuestion` to collect: + +```javascript +AskUserQuestion({ + questions: [ + { + question: "What type of software is this project?", + header: "Software Type", + options: [ + { label: "Web Application", description: "Frontend + Backend web app with UI" }, + { label: "CLI Tool", description: "Command-line interface tool" }, + { label: "SDK/Library", description: "Developer library or SDK" }, + { label: "Desktop App", description: "Desktop application (Electron, etc.)" } + ], + multiSelect: false + }, + { + question: "Who is the target audience for this manual?", + header: "Target Users", + options: [ + { label: "End Users", description: "Non-technical users who use the product" }, + { label: "Developers", description: "Developers integrating or extending the product" }, + { label: "Administrators", description: "System admins deploying and maintaining" }, + { label: "All Audiences", description: "Mixed audience with different sections" } + ], + multiSelect: false + }, + { + question: "What documentation scope do you need?", + header: "Doc Scope", + options: [ + { label: "Quick Start", description: "Essential getting started guide only" }, + { label: "User Guide", description: "Complete user-facing documentation" }, + { label: "API Reference", description: "Focus on API documentation" }, + { label: "Comprehensive", description: "Full documentation including all sections" } + ], + multiSelect: false + }, + { + question: "What difficulty levels should code examples cover?", + header: "Example Levels", + options: [ + { label: "Beginner Only", description: "Simple, basic examples" }, + { label: "Beginner + Intermediate", description: "Basic to moderate complexity" }, + { label: "All Levels", description: "Beginner, Intermediate, and Advanced" } + ], + multiSelect: false + } + ] +}); +``` + +### Step 2: Auto-Detection (Supplement) + +Automatically detect project characteristics: + +```javascript +// Detect from package.json +const packageJson = Read('package.json'); +const softwareName = packageJson.name; +const version = packageJson.version; +const description = packageJson.description; + +// Detect tech stack +const hasReact = packageJson.dependencies?.react; +const hasVue = packageJson.dependencies?.vue; +const hasExpress = packageJson.dependencies?.express; +const hasNestJS = packageJson.dependencies?.['@nestjs/core']; + +// Detect CLI +const hasBin = !!packageJson.bin; + +// Detect UI +const hasPages = Glob('src/pages/**/*').length > 0 || Glob('pages/**/*').length > 0; +const hasRoutes = Glob('**/routes.*').length > 0; +``` + +### Step 3: Generate Configuration + +Create `manual-config.json`: + +```json +{ + "software": { + "name": "{{detected or user input}}", + "version": "{{from package.json}}", + "description": "{{from package.json}}", + "type": "{{web|cli|sdk|desktop}}" + }, + "target_audience": "{{end_users|developers|admins|all}}", + "doc_scope": "{{quick_start|user_guide|api_reference|comprehensive}}", + "example_levels": ["beginner", "intermediate", "advanced"], + "tech_stack": { + "frontend": "{{react|vue|angular|vanilla}}", + "backend": "{{express|nestjs|fastify|none}}", + "language": "{{typescript|javascript}}", + "ui_framework": "{{tailwind|mui|antd|none}}" + }, + "features": { + "has_ui": true, + "has_api": true, + "has_cli": false, + "has_config": true + }, + "agents_to_run": [ + "overview", + "ui-guide", + "api-docs", + "config", + "troubleshooting", + "code-examples" + ], + "screenshot_config": { + "enabled": true, + "dev_command": "npm run dev", + "dev_url": "http://localhost:3000", + "wait_timeout": 5000 + }, + "output": { + "filename": "{{name}}-使用手册.html", + "theme": "light", + "language": "zh-CN" + }, + "timestamp": "{{ISO8601}}" +} +``` + +## Agent Selection Logic + +Based on `doc_scope`, select agents to run: + +| Scope | Agents | +|-------|--------| +| quick_start | overview | +| user_guide | overview, ui-guide, config, troubleshooting | +| api_reference | overview, api-docs, code-examples | +| comprehensive | ALL 6 agents | + +## Output + +- **File**: `manual-config.json` +- **Location**: `.workflow/.scratchpad/manual-{timestamp}/` + +## Next Phase + +Proceed to [Phase 2: Project Exploration](02-project-exploration.md) with the generated configuration. diff --git a/.claude/skills/software-manual/phases/02-project-exploration.md b/.claude/skills/software-manual/phases/02-project-exploration.md new file mode 100644 index 00000000..dde97777 --- /dev/null +++ b/.claude/skills/software-manual/phases/02-project-exploration.md @@ -0,0 +1,101 @@ +# Phase 2: Project Exploration + +使用 `cli-explore-agent` 探索项目结构,生成文档所需的结构化数据。 + +## 探索角度 + +```javascript +const EXPLORATION_ANGLES = { + web: ['architecture', 'ui-routes', 'api-endpoints', 'config'], + cli: ['architecture', 'commands', 'config'], + sdk: ['architecture', 'public-api', 'types', 'config'], + desktop: ['architecture', 'ui-screens', 'config'] +}; +``` + +## 执行流程 + +```javascript +const config = JSON.parse(Read(`${workDir}/manual-config.json`)); +const angles = EXPLORATION_ANGLES[config.software.type]; + +// 并行探索 +const tasks = angles.map(angle => Task({ + subagent_type: 'cli-explore-agent', + run_in_background: false, + prompt: buildExplorationPrompt(angle, config, workDir) +})); + +const results = await Promise.all(tasks); +``` + +## 探索配置 + +```javascript +const EXPLORATION_CONFIGS = { + architecture: { + task: '分析项目模块结构、入口点、依赖关系', + patterns: ['src/*/', 'package.json', 'tsconfig.json'], + output: 'exploration-architecture.json' + }, + 'ui-routes': { + task: '提取 UI 路由、页面组件、导航结构', + patterns: ['src/pages/**', 'src/views/**', 'app/**/page.*', 'src/router/**'], + output: 'exploration-ui-routes.json' + }, + 'api-endpoints': { + task: '提取 REST API 端点、请求/响应类型', + patterns: ['src/**/*.controller.*', 'src/routes/**', 'openapi.*', 'swagger.*'], + output: 'exploration-api-endpoints.json' + }, + config: { + task: '提取环境变量、配置文件选项', + patterns: ['.env.example', 'config/**', 'docker-compose.yml'], + output: 'exploration-config.json' + }, + commands: { + task: '提取 CLI 命令、选项、示例', + patterns: ['src/cli*', 'bin/*', 'src/commands/**'], + output: 'exploration-commands.json' + } +}; +``` + +## Prompt 构建 + +```javascript +function buildExplorationPrompt(angle, config, workDir) { + const cfg = EXPLORATION_CONFIGS[angle]; + return ` +[TASK] +${cfg.task} + +[SCOPE] +项目类型: ${config.software.type} +扫描模式: deep-scan +文件模式: ${cfg.patterns.join(', ')} + +[OUTPUT] +文件: ${workDir}/exploration/${cfg.output} +格式: JSON (schema-compliant) + +[RETURN] +简要说明发现的内容数量和关键发现 +`; +} +``` + +## 输出结构 + +``` +exploration/ +├── exploration-architecture.json # 模块结构 +├── exploration-ui-routes.json # UI 路由 +├── exploration-api-endpoints.json # API 端点 +├── exploration-config.json # 配置选项 +└── exploration-commands.json # CLI 命令 (if CLI) +``` + +## 下一阶段 + +→ [Phase 3: Parallel Analysis](03-parallel-analysis.md) diff --git a/.claude/skills/software-manual/phases/03-parallel-analysis.md b/.claude/skills/software-manual/phases/03-parallel-analysis.md new file mode 100644 index 00000000..4e86a34f --- /dev/null +++ b/.claude/skills/software-manual/phases/03-parallel-analysis.md @@ -0,0 +1,130 @@ +# Phase 3: Parallel Analysis + +使用 `universal-executor` 并行生成 6 个文档章节。 + +## Agent 配置 + +```javascript +const AGENT_CONFIGS = { + overview: { + role: 'Product Manager', + output: 'section-overview.md', + task: '撰写产品概览、核心功能、快速入门指南', + focus: '产品定位、目标用户、5步快速入门、系统要求', + input: ['exploration-architecture.json', 'README.md', 'package.json'] + }, + 'ui-guide': { + role: 'UX Expert', + output: 'section-ui-guide.md', + task: '撰写界面操作指南,分步骤说明各功能使用方法', + focus: '界面布局、导航流程、功能操作、快捷键', + input: ['exploration-ui-routes.json', 'pages/**', 'views/**'] + }, + 'api-docs': { + role: 'API Architect', + output: 'section-api-reference.md', + task: '撰写 REST API 和前端 API 参考文档', + focus: 'API 概览、端点分类、请求/响应示例、错误码', + input: ['exploration-api-endpoints.json', 'controllers/**', 'routes/**'] + }, + config: { + role: 'DevOps Engineer', + output: 'section-configuration.md', + task: '撰写配置指南,涵盖环境变量、配置文件、部署设置', + focus: '环境变量表格、配置文件格式、部署选项、安全设置', + input: ['exploration-config.json', '.env.example', 'config/**'] + }, + troubleshooting: { + role: 'Support Engineer', + output: 'section-troubleshooting.md', + task: '撰写故障排查指南,涵盖常见问题、错误码、FAQ', + focus: '常见问题与解决方案、错误码参考、FAQ、获取帮助', + input: ['all exploration files', 'error handling code'] + }, + 'code-examples': { + role: 'Developer Advocate', + output: 'section-examples.md', + task: '撰写多难度级别代码示例(入门40%/进阶40%/高级20%)', + focus: '完整可运行代码、分步解释、预期输出、最佳实践', + input: ['all exploration files', 'examples/**', 'tests/**'] + } +}; +``` + +## 执行流程 + +```javascript +const config = JSON.parse(Read(`${workDir}/manual-config.json`)); + +// 并行启动 6 个 universal-executor +const tasks = Object.entries(AGENT_CONFIGS).map(([name, cfg]) => + Task({ + subagent_type: 'universal-executor', + run_in_background: false, + prompt: buildAgentPrompt(name, cfg, config, workDir) + }) +); + +const results = await Promise.all(tasks); +``` + +## Prompt 构建 + +```javascript +function buildAgentPrompt(name, cfg, config, workDir) { + return ` +[ROLE] ${cfg.role} + +[TASK] +${cfg.task} +输出: ${workDir}/sections/${cfg.output} + +[INPUT] +- Read: ${workDir}/manual-config.json +- Read: ${cfg.input.map(f => `${workDir}/exploration/${f}`).join(', ')} + +[STYLE] +- 用户友好语言,避免技术术语 +- 步骤编号清晰 +- 代码块标注语言 +- 截图标记: + +[FOCUS] +${cfg.focus} + +[RETURN JSON] +{ + "status": "completed", + "output_file": "sections/${cfg.output}", + "summary": "<50字>", + "screenshots_needed": [], + "cross_references": [] +} +`; +} +``` + +## 结果收集 + +```javascript +const agentResults = results.map(r => JSON.parse(r)); +const allScreenshots = agentResults.flatMap(r => r.screenshots_needed); + +Write(`${workDir}/agent-results.json`, JSON.stringify({ + results: agentResults, + screenshots_needed: allScreenshots, + timestamp: new Date().toISOString() +}, null, 2)); +``` + +## 质量检查 + +- [ ] Markdown 语法有效 +- [ ] 无占位符文本 +- [ ] 代码块标注语言 +- [ ] 截图标记格式正确 +- [ ] 交叉引用有效 + +## 下一阶段 + +→ [Phase 3.5: Consolidation](03.5-consolidation.md) diff --git a/.claude/skills/software-manual/phases/03.5-consolidation.md b/.claude/skills/software-manual/phases/03.5-consolidation.md new file mode 100644 index 00000000..5b80d91e --- /dev/null +++ b/.claude/skills/software-manual/phases/03.5-consolidation.md @@ -0,0 +1,82 @@ +# Phase 3.5: Consolidation + +使用 `universal-executor` 子 Agent 执行质量检查,避免主 Agent 内存溢出。 + +## 核心原则 + +**主 Agent 负责编排,子 Agent 负责繁重计算。** + +## 执行流程 + +```javascript +const agentResults = JSON.parse(Read(`${workDir}/agent-results.json`)); + +// 委托给 universal-executor 执行整合检查 +const result = Task({ + subagent_type: 'universal-executor', + run_in_background: false, + prompt: buildConsolidationPrompt(workDir) +}); + +const consolidationResult = JSON.parse(result); +``` + +## Prompt 构建 + +```javascript +function buildConsolidationPrompt(workDir) { + return ` +[ROLE] Quality Analyst + +[TASK] +检查所有章节的一致性和完整性 + +[INPUT] +- 章节文件: ${workDir}/sections/section-*.md +- Agent 结果: ${workDir}/agent-results.json + +[CHECKS] +1. Markdown 语法有效性 +2. 截图标记格式 () +3. 交叉引用有效性 +4. 术语一致性 +5. 代码块语言标注 + +[OUTPUT] +1. 写入 ${workDir}/consolidation-summary.md +2. 写入 ${workDir}/screenshots-list.json (截图清单) + +[RETURN JSON] +{ + "status": "completed", + "sections_checked": , + "screenshots_found": , + "issues": { "errors": , "warnings": }, + "quality_score": <0-100> +} +`; +} +``` + +## Agent 职责 + +1. **读取章节** → 逐个检查 section-*.md +2. **提取截图** → 收集所有截图标记 +3. **验证引用** → 检查交叉引用有效性 +4. **评估质量** → 计算综合分数 +5. **输出报告** → consolidation-summary.md + +## 输出 + +- `consolidation-summary.md` - 质量报告 +- `screenshots-list.json` - 截图清单(供 Phase 4 使用) + +## 质量门禁 + +- [ ] 无错误 +- [ ] 总分 >= 60% +- [ ] 交叉引用有效 + +## 下一阶段 + +→ [Phase 4: Screenshot Capture](04-screenshot-capture.md) diff --git a/.claude/skills/software-manual/phases/04-screenshot-capture.md b/.claude/skills/software-manual/phases/04-screenshot-capture.md new file mode 100644 index 00000000..453328d9 --- /dev/null +++ b/.claude/skills/software-manual/phases/04-screenshot-capture.md @@ -0,0 +1,271 @@ +# Phase 4: Screenshot Capture + +Capture screenshots using Chrome MCP for all identified UI elements. + +## Objective + +- Check Chrome MCP availability +- Start development server +- Capture all required screenshots +- Convert to Base64 for embedding + +## Prerequisites + +- Chrome MCP configured and available +- Development server can be started +- All screenshot URLs accessible + +## Execution Steps + +### Step 1: Load Screenshot List + +```javascript +const consolidation = Read(`${workDir}/consolidation-summary.md`); +const config = JSON.parse(Read(`${workDir}/manual-config.json`)); + +// Parse screenshot table from consolidation +const screenshots = parseScreenshotTable(consolidation); +``` + +### Step 2: Check Chrome MCP Availability + +```javascript +async function checkChromeMCP() { + try { + // Attempt to call Chrome MCP + const version = await mcp__chrome__getVersion(); + return { + available: true, + version: version.version, + browser: version.browser + }; + } catch (error) { + return { + available: false, + error: error.message + }; + } +} + +const chromeMCP = await checkChromeMCP(); + +if (!chromeMCP.available) { + // Fallback: generate manual screenshot instructions + generateManualScreenshotGuide(screenshots); + return { + status: 'skipped', + reason: 'Chrome MCP not available', + manual_guide: `${workDir}/screenshots/MANUAL_CAPTURE.md` + }; +} +``` + +### Step 3: Start Development Server + +```javascript +const devConfig = config.screenshot_config; + +// Start dev server in background +const serverTask = Bash({ + command: devConfig.dev_command, + run_in_background: true +}); + +// Wait for server to be ready +async function waitForServer(url, timeout = 30000) { + const start = Date.now(); + while (Date.now() - start < timeout) { + try { + const response = await fetch(url); + if (response.ok) return true; + } catch (e) { + // Server not ready yet + } + await sleep(1000); + } + throw new Error(`Server at ${url} did not start within ${timeout}ms`); +} + +await waitForServer(devConfig.dev_url, devConfig.wait_timeout); +``` + +### Step 4: Batch Screenshot Capture + +```javascript +const capturedScreenshots = []; +const failedScreenshots = []; + +for (const ss of screenshots) { + try { + const fullUrl = new URL(ss.url, devConfig.dev_url).href; + + // Configure capture options + const captureOptions = { + url: fullUrl, + viewport: { width: 1280, height: 800 }, + fullPage: ss.fullPage || false, + waitFor: ss.wait_for || null, + delay: 500 // Wait for animations + }; + + // Add selector for partial screenshot + if (ss.selector) { + captureOptions.selector = ss.selector; + } + + // Capture screenshot + const result = await mcp__chrome__screenshot(captureOptions); + + // Save screenshot + const filename = `${ss.id}.png`; + Write(`${workDir}/screenshots/${filename}`, result.data, { encoding: 'base64' }); + + capturedScreenshots.push({ + ...ss, + file: filename, + base64_size: result.data.length, + captured_at: new Date().toISOString() + }); + + } catch (error) { + failedScreenshots.push({ + ...ss, + error: error.message + }); + } +} +``` + +### Step 5: Generate Screenshot Manifest + +```javascript +const manifest = { + total: screenshots.length, + captured: capturedScreenshots.length, + failed: failedScreenshots.length, + screenshots: capturedScreenshots, + failures: failedScreenshots, + dev_server: { + url: devConfig.dev_url, + command: devConfig.dev_command + }, + capture_config: { + viewport: { width: 1280, height: 800 }, + format: 'png' + }, + timestamp: new Date().toISOString() +}; + +Write(`${workDir}/screenshots/screenshots-manifest.json`, JSON.stringify(manifest, null, 2)); +``` + +### Step 6: Stop Development Server + +```javascript +// Kill the dev server process +KillShell({ shell_id: serverTask.task_id }); +``` + +### Step 7: Handle Failures + +If any screenshots failed: + +```javascript +if (failedScreenshots.length > 0) { + // Generate manual capture instructions + const manualGuide = ` +# Manual Screenshot Capture Required + +The following screenshots could not be captured automatically: + +${failedScreenshots.map(s => ` +## ${s.id} +- **URL**: ${s.url} +- **Description**: ${s.description} +- **Error**: ${s.error} + +**Instructions**: +1. Navigate to ${s.url} +2. Capture screenshot of ${s.selector || 'full page'} +3. Save as \`${s.id}.png\` +4. Place in \`screenshots/\` directory +`).join('\n')} +`; + + Write(`${workDir}/screenshots/MANUAL_CAPTURE.md`, manualGuide); +} +``` + +## Fallback: Manual Screenshot Mode + +When Chrome MCP is not available: + +```javascript +function generateManualScreenshotGuide(screenshots) { + const guide = ` +# Manual Screenshot Capture Guide + +Chrome MCP is not available. Please capture screenshots manually. + +## Setup + +1. Start your development server: + \`\`\`bash + ${config.screenshot_config.dev_command} + \`\`\` + +2. Open browser to: ${config.screenshot_config.dev_url} + +## Screenshots Required + +${screenshots.map((s, i) => ` +### ${i + 1}. ${s.id} +- **URL**: ${s.url} +- **Description**: ${s.description} +- **Save as**: \`screenshots/${s.id}.png\` +${s.selector ? `- **Element**: Capture only \`${s.selector}\`` : '- **Type**: Full page'} +`).join('\n')} + +## After Capturing + +Place all PNG files in the \`screenshots/\` directory, then run Phase 5 to continue. +`; + + Write(`${workDir}/screenshots/MANUAL_CAPTURE.md`, guide); +} +``` + +## Chrome MCP Configuration Reference + +Expected MCP configuration: + +```json +{ + "mcpServers": { + "chrome": { + "command": "npx", + "args": ["@anthropic-ai/mcp-chrome"], + "env": { + "CHROME_PATH": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + } + } + } +} +``` + +## Output + +- **Files**: `screenshots/*.png` +- **Manifest**: `screenshots/screenshots-manifest.json` +- **Fallback**: `screenshots/MANUAL_CAPTURE.md` (if needed) + +## Quality Checks + +- [ ] All high-priority screenshots captured +- [ ] Screenshot dimensions consistent (1280x800) +- [ ] No broken/blank screenshots +- [ ] Manifest file complete + +## Next Phase + +Proceed to [Phase 5: HTML Assembly](05-html-assembly.md) with captured screenshots. diff --git a/.claude/skills/software-manual/phases/05-html-assembly.md b/.claude/skills/software-manual/phases/05-html-assembly.md new file mode 100644 index 00000000..e3a5c372 --- /dev/null +++ b/.claude/skills/software-manual/phases/05-html-assembly.md @@ -0,0 +1,120 @@ +# Phase 5: HTML Assembly + +使用 `universal-executor` 子 Agent 生成最终 HTML,避免主 Agent 内存溢出。 + +## 核心原则 + +**主 Agent 负责编排,子 Agent 负责繁重计算。** + +## 执行流程 + +```javascript +const config = JSON.parse(Read(`${workDir}/manual-config.json`)); + +// 委托给 universal-executor 执行 HTML 组装 +const result = Task({ + subagent_type: 'universal-executor', + run_in_background: false, + prompt: buildAssemblyPrompt(config, workDir) +}); + +const buildResult = JSON.parse(result); +``` + +## Prompt 构建 + +```javascript +function buildAssemblyPrompt(config, workDir) { + return ` +[ROLE] HTML Assembler + +[TASK] +生成 TiddlyWiki 风格的交互式 HTML 手册 + +[INPUT] +- 模板: .claude/skills/software-manual/templates/tiddlywiki-shell.html +- CSS: .claude/skills/software-manual/templates/css/wiki-base.css, wiki-dark.css +- 配置: ${workDir}/manual-config.json +- 章节: ${workDir}/sections/section-*.md +- 截图: ${workDir}/screenshots/ + +[STEPS] +1. 读取 HTML 模板和 CSS +2. 逐个读取 section-*.md,转换为 HTML tiddlers +3. 处理 标记,嵌入 Base64 图片 +4. 生成目录、搜索索引 +5. 组装最终 HTML,写入 ${workDir}/${config.software.name}-使用手册.html +6. 生成构建报告 ${workDir}/build-report.json + +[HTML FEATURES] +- 搜索: 全文检索 + 高亮 +- 折叠: 章节可展开/收起 +- 标签: 分类过滤 +- 主题: 亮/暗模式切换 +- 离线: 所有资源内嵌 + +[RETURN JSON] +{ + "status": "completed", + "output_file": "${config.software.name}-使用手册.html", + "file_size": "", + "sections_count": , + "screenshots_embedded": +} +`; +} +``` + +## Agent 职责 + +1. **读取模板** → HTML + CSS +2. **转换章节** → Markdown → HTML tiddlers +3. **嵌入截图** → Base64 编码 +4. **生成索引** → 搜索数据 +5. **组装输出** → 单文件 HTML + +## Markdown 转换规则 + +Agent 内部实现: + +``` +# H1 →

+## H2 →

+### H3 →

+```code``` →

+**bold** → 
+*italic* → 
+[text](url) → 
+- item → 
  • +
    +``` + +## Tiddler 结构 + +```html +
    +
    +

    {title}

    +
    {badges}
    +
    +
    {html}
    +
    +``` + +## 输出 + +- `{软件名}-使用手册.html` - 最终 HTML +- `build-report.json` - 构建报告 + +## 质量门禁 + +- [ ] HTML 渲染正确 +- [ ] 搜索功能可用 +- [ ] 折叠/展开正常 +- [ ] 主题切换持久化 +- [ ] 截图显示正确 +- [ ] 文件大小 < 10MB + +## 下一阶段 + +→ [Phase 6: Iterative Refinement](06-iterative-refinement.md) diff --git a/.claude/skills/software-manual/phases/06-iterative-refinement.md b/.claude/skills/software-manual/phases/06-iterative-refinement.md new file mode 100644 index 00000000..4afc7408 --- /dev/null +++ b/.claude/skills/software-manual/phases/06-iterative-refinement.md @@ -0,0 +1,259 @@ +# Phase 6: Iterative Refinement + +Preview, collect feedback, and iterate until quality meets standards. + +## Objective + +- Preview generated HTML in browser +- Collect user feedback +- Address issues iteratively +- Finalize documentation + +## Execution Steps + +### Step 1: Preview HTML + +```javascript +const buildReport = JSON.parse(Read(`${workDir}/build-report.json`)); +const outputFile = `${workDir}/${buildReport.output}`; + +// Open in default browser for preview +Bash({ command: `start "${outputFile}"` }); // Windows +// Bash({ command: `open "${outputFile}"` }); // macOS + +// Report to user +console.log(` +📖 Manual Preview + +File: ${buildReport.output} +Size: ${buildReport.size_human} +Sections: ${buildReport.sections} +Screenshots: ${buildReport.screenshots} + +Please review the manual in your browser. +`); +``` + +### Step 2: Collect Feedback + +```javascript +const feedback = await AskUserQuestion({ + questions: [ + { + question: "How does the manual look overall?", + header: "Overall", + options: [ + { label: "Looks great!", description: "Ready to finalize" }, + { label: "Minor issues", description: "Small tweaks needed" }, + { label: "Major issues", description: "Significant changes required" }, + { label: "Missing content", description: "Need to add more sections" } + ], + multiSelect: false + }, + { + question: "Which aspects need improvement? (Select all that apply)", + header: "Improvements", + options: [ + { label: "Content accuracy", description: "Fix incorrect information" }, + { label: "More examples", description: "Add more code examples" }, + { label: "Better screenshots", description: "Retake or add screenshots" }, + { label: "Styling/Layout", description: "Improve visual appearance" } + ], + multiSelect: true + } + ] +}); +``` + +### Step 3: Address Feedback + +Based on feedback, take appropriate action: + +#### Minor Issues + +```javascript +if (feedback.overall === "Minor issues") { + // Prompt for specific changes + const details = await AskUserQuestion({ + questions: [{ + question: "What specific changes are needed?", + header: "Details", + options: [ + { label: "Typo fixes", description: "Fix spelling/grammar" }, + { label: "Reorder sections", description: "Change section order" }, + { label: "Update content", description: "Modify existing text" }, + { label: "Custom changes", description: "I'll describe the changes" } + ], + multiSelect: true + }] + }); + + // Apply changes based on user input + applyMinorChanges(details); +} +``` + +#### Major Issues + +```javascript +if (feedback.overall === "Major issues") { + // Return to relevant phase + console.log(` + Major issues require returning to an earlier phase: + + - Content issues → Phase 3 (Parallel Analysis) + - Screenshot issues → Phase 4 (Screenshot Capture) + - Structure issues → Phase 2 (Project Exploration) + + Which phase should we return to? + `); + + const phase = await selectPhase(); + return { action: 'restart', from_phase: phase }; +} +``` + +#### Missing Content + +```javascript +if (feedback.overall === "Missing content") { + // Identify missing sections + const missing = await AskUserQuestion({ + questions: [{ + question: "What content is missing?", + header: "Missing", + options: [ + { label: "API endpoints", description: "More API documentation" }, + { label: "UI features", description: "Additional UI guides" }, + { label: "Examples", description: "More code examples" }, + { label: "Troubleshooting", description: "More FAQ items" } + ], + multiSelect: true + }] + }); + + // Run additional agent(s) for missing content + await runSupplementaryAgents(missing); +} +``` + +### Step 4: Save Iteration + +```javascript +// Save current version before changes +const iterationNum = getNextIterationNumber(workDir); +const iterationDir = `${workDir}/iterations`; + +// Copy current version +Bash({ command: `copy "${outputFile}" "${iterationDir}\\v${iterationNum}.html"` }); + +// Log iteration +const iterationLog = { + version: iterationNum, + timestamp: new Date().toISOString(), + feedback: feedback, + changes: appliedChanges +}; + +Write(`${iterationDir}/iteration-${iterationNum}.json`, JSON.stringify(iterationLog, null, 2)); +``` + +### Step 5: Regenerate if Needed + +```javascript +if (changesApplied) { + // Re-run HTML assembly with updated sections + await runPhase('05-html-assembly'); + + // Open updated preview + Bash({ command: `start "${outputFile}"` }); +} +``` + +### Step 6: Finalize + +When user approves: + +```javascript +if (feedback.overall === "Looks great!") { + // Final quality check + const finalReport = { + ...buildReport, + iterations: iterationNum, + finalized_at: new Date().toISOString(), + quality_score: calculateFinalQuality() + }; + + Write(`${workDir}/final-report.json`, JSON.stringify(finalReport, null, 2)); + + // Suggest final location + console.log(` + ✅ Manual Finalized! + + Output: ${buildReport.output} + Size: ${buildReport.size_human} + Quality: ${finalReport.quality_score}% + Iterations: ${iterationNum} + + Suggested actions: + 1. Copy to project root: copy "${outputFile}" "docs/" + 2. Add to version control + 3. Publish to documentation site + `); + + return { status: 'completed', output: outputFile }; +} +``` + +## Iteration History + +Each iteration is logged: + +``` +iterations/ +├── v1.html # First version +├── iteration-1.json # Feedback and changes +├── v2.html # After first iteration +├── iteration-2.json # Feedback and changes +└── ... +``` + +## Quality Metrics + +Track improvement across iterations: + +```javascript +const qualityMetrics = { + content_completeness: 0, // All sections present + screenshot_coverage: 0, // Screenshots for all UI + example_diversity: 0, // Different difficulty levels + search_accuracy: 0, // Search returns relevant results + user_satisfaction: 0 // Based on feedback +}; +``` + +## Exit Conditions + +The refinement phase ends when: +1. User explicitly approves ("Looks great!") +2. Maximum iterations reached (configurable, default: 5) +3. Quality score exceeds threshold (default: 90%) + +## Output + +- **Final HTML**: `{软件名}-使用手册.html` +- **Final Report**: `final-report.json` +- **Iteration History**: `iterations/` + +## Completion + +When finalized, the skill is complete. Final output location: + +``` +.workflow/.scratchpad/manual-{timestamp}/ +├── {软件名}-使用手册.html ← Final deliverable +├── final-report.json +└── iterations/ +``` + +Consider copying to a permanent location like `docs/` or project root. diff --git a/.claude/skills/software-manual/scripts/screenshot-helper.md b/.claude/skills/software-manual/scripts/screenshot-helper.md new file mode 100644 index 00000000..63f8acc7 --- /dev/null +++ b/.claude/skills/software-manual/scripts/screenshot-helper.md @@ -0,0 +1,447 @@ +# Screenshot Helper + +Guide for capturing screenshots using Chrome MCP. + +## Overview + +This script helps capture screenshots of web interfaces for the software manual using Chrome MCP or fallback methods. + +## Chrome MCP Prerequisites + +### Check MCP Availability + +```javascript +async function checkChromeMCPAvailability() { + try { + // Attempt to get Chrome version via MCP + const version = await mcp__chrome__getVersion(); + return { + available: true, + browser: version.browser, + version: version.version + }; + } catch (error) { + return { + available: false, + error: error.message + }; + } +} +``` + +### MCP Configuration + +Expected Claude configuration for Chrome MCP: + +```json +{ + "mcpServers": { + "chrome": { + "command": "npx", + "args": ["@anthropic-ai/mcp-chrome"], + "env": { + "CHROME_PATH": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + } + } + } +} +``` + +## Screenshot Workflow + +### Step 1: Prepare Environment + +```javascript +async function prepareScreenshotEnvironment(workDir, config) { + const screenshotDir = `${workDir}/screenshots`; + + // Create directory + Bash({ command: `mkdir -p "${screenshotDir}"` }); + + // Check Chrome MCP + const chromeMCP = await checkChromeMCPAvailability(); + + if (!chromeMCP.available) { + console.log('Chrome MCP not available. Will generate manual guide.'); + return { mode: 'manual' }; + } + + // Start development server if needed + if (config.screenshot_config?.dev_command) { + const server = await startDevServer(config); + return { mode: 'auto', server, screenshotDir }; + } + + return { mode: 'auto', screenshotDir }; +} +``` + +### Step 2: Start Development Server + +```javascript +async function startDevServer(config) { + const devCommand = config.screenshot_config.dev_command; + const devUrl = config.screenshot_config.dev_url; + + // Start server in background + const server = Bash({ + command: devCommand, + run_in_background: true + }); + + console.log(`Starting dev server: ${devCommand}`); + + // Wait for server to be ready + const ready = await waitForServer(devUrl, 30000); + + if (!ready) { + throw new Error(`Server at ${devUrl} did not start in time`); + } + + console.log(`Dev server ready at ${devUrl}`); + + return server; +} + +async function waitForServer(url, timeout = 30000) { + const start = Date.now(); + + while (Date.now() - start < timeout) { + try { + const response = await fetch(url, { method: 'HEAD' }); + if (response.ok) return true; + } catch (e) { + // Server not ready + } + await sleep(1000); + } + + return false; +} +``` + +### Step 3: Capture Screenshots + +```javascript +async function captureScreenshots(screenshots, config, workDir) { + const results = { + captured: [], + failed: [] + }; + + const devUrl = config.screenshot_config.dev_url; + const screenshotDir = `${workDir}/screenshots`; + + for (const ss of screenshots) { + try { + // Build full URL + const fullUrl = new URL(ss.url, devUrl).href; + + console.log(`Capturing: ${ss.id} (${fullUrl})`); + + // Configure capture options + const options = { + url: fullUrl, + viewport: { width: 1280, height: 800 }, + fullPage: ss.fullPage || false + }; + + // Wait for specific element if specified + if (ss.wait_for) { + options.waitFor = ss.wait_for; + } + + // Capture specific element if selector provided + if (ss.selector) { + options.selector = ss.selector; + } + + // Add delay for animations + await sleep(500); + + // Capture via Chrome MCP + const result = await mcp__chrome__screenshot(options); + + // Save as PNG + const filename = `${ss.id}.png`; + Write(`${screenshotDir}/${filename}`, result.data, { encoding: 'base64' }); + + results.captured.push({ + id: ss.id, + file: filename, + url: ss.url, + description: ss.description, + size: result.data.length + }); + + } catch (error) { + console.error(`Failed to capture ${ss.id}:`, error.message); + results.failed.push({ + id: ss.id, + url: ss.url, + error: error.message + }); + } + } + + return results; +} +``` + +### Step 4: Generate Manifest + +```javascript +function generateScreenshotManifest(results, workDir) { + const manifest = { + generated: new Date().toISOString(), + total: results.captured.length + results.failed.length, + captured: results.captured.length, + failed: results.failed.length, + screenshots: results.captured, + failures: results.failed + }; + + Write(`${workDir}/screenshots/screenshots-manifest.json`, + JSON.stringify(manifest, null, 2)); + + return manifest; +} +``` + +### Step 5: Cleanup + +```javascript +async function cleanupScreenshotEnvironment(env) { + if (env.server) { + console.log('Stopping dev server...'); + KillShell({ shell_id: env.server.task_id }); + } +} +``` + +## Main Runner + +```javascript +async function runScreenshotCapture(workDir, screenshots) { + const config = JSON.parse(Read(`${workDir}/manual-config.json`)); + + // Prepare environment + const env = await prepareScreenshotEnvironment(workDir, config); + + if (env.mode === 'manual') { + // Generate manual capture guide + generateManualCaptureGuide(screenshots, workDir); + return { success: false, mode: 'manual' }; + } + + try { + // Capture screenshots + const results = await captureScreenshots(screenshots, config, workDir); + + // Generate manifest + const manifest = generateScreenshotManifest(results, workDir); + + // Generate manual guide for failed captures + if (results.failed.length > 0) { + generateManualCaptureGuide(results.failed, workDir); + } + + return { + success: true, + captured: results.captured.length, + failed: results.failed.length, + manifest + }; + + } finally { + // Cleanup + await cleanupScreenshotEnvironment(env); + } +} +``` + +## Manual Capture Fallback + +When Chrome MCP is unavailable: + +```javascript +function generateManualCaptureGuide(screenshots, workDir) { + const guide = ` +# Manual Screenshot Capture Guide + +Chrome MCP is not available. Please capture screenshots manually. + +## Prerequisites + +1. Start your development server +2. Open a browser +3. Use a screenshot tool (Snipping Tool, Screenshot, etc.) + +## Screenshots Required + +${screenshots.map((ss, i) => ` +### ${i + 1}. ${ss.id} + +- **URL**: ${ss.url} +- **Description**: ${ss.description} +- **Save as**: \`screenshots/${ss.id}.png\` +${ss.selector ? `- **Capture area**: \`${ss.selector}\` element only` : '- **Type**: Full page or viewport'} +${ss.wait_for ? `- **Wait for**: \`${ss.wait_for}\` to be visible` : ''} + +**Steps:** +1. Navigate to ${ss.url} +${ss.wait_for ? `2. Wait for ${ss.wait_for} to appear` : ''} +${ss.selector ? `2. Capture only the ${ss.selector} area` : '2. Capture the full viewport'} +3. Save as \`${ss.id}.png\` +`).join('\n')} + +## After Capturing + +1. Place all PNG files in the \`screenshots/\` directory +2. Ensure filenames match exactly (case-sensitive) +3. Run Phase 5 (HTML Assembly) to continue + +## Screenshot Specifications + +- **Format**: PNG +- **Width**: 1280px recommended +- **Quality**: High +- **Annotations**: None (add in post-processing if needed) +`; + + Write(`${workDir}/screenshots/MANUAL_CAPTURE.md`, guide); +} +``` + +## Advanced Options + +### Viewport Sizes + +```javascript +const viewportPresets = { + desktop: { width: 1280, height: 800 }, + tablet: { width: 768, height: 1024 }, + mobile: { width: 375, height: 667 }, + wide: { width: 1920, height: 1080 } +}; + +async function captureResponsive(ss, config, workDir) { + const results = []; + + for (const [name, viewport] of Object.entries(viewportPresets)) { + const result = await mcp__chrome__screenshot({ + url: ss.url, + viewport + }); + + const filename = `${ss.id}-${name}.png`; + Write(`${workDir}/screenshots/${filename}`, result.data, { encoding: 'base64' }); + + results.push({ viewport: name, file: filename }); + } + + return results; +} +``` + +### Before/After Comparisons + +```javascript +async function captureInteraction(ss, config, workDir) { + const baseUrl = config.screenshot_config.dev_url; + const fullUrl = new URL(ss.url, baseUrl).href; + + // Capture before state + const before = await mcp__chrome__screenshot({ + url: fullUrl, + viewport: { width: 1280, height: 800 } + }); + Write(`${workDir}/screenshots/${ss.id}-before.png`, before.data, { encoding: 'base64' }); + + // Perform interaction (click, type, etc.) + if (ss.interaction) { + await mcp__chrome__click({ selector: ss.interaction.click }); + await sleep(500); + } + + // Capture after state + const after = await mcp__chrome__screenshot({ + url: fullUrl, + viewport: { width: 1280, height: 800 } + }); + Write(`${workDir}/screenshots/${ss.id}-after.png`, after.data, { encoding: 'base64' }); + + return { + before: `${ss.id}-before.png`, + after: `${ss.id}-after.png` + }; +} +``` + +### Screenshot Annotation + +```javascript +function generateAnnotationGuide(screenshots, workDir) { + const guide = ` +# Screenshot Annotation Guide + +For screenshots requiring callouts or highlights: + +## Tools +- macOS: Preview, Skitch +- Windows: Snipping Tool, ShareX +- Cross-platform: Greenshot, Lightshot + +## Annotation Guidelines + +1. **Callouts**: Use numbered circles (1, 2, 3) +2. **Highlights**: Use semi-transparent rectangles +3. **Arrows**: Point from text to element +4. **Text**: Use sans-serif font, 12-14pt + +## Color Scheme + +- Primary: #0d6efd (blue) +- Secondary: #6c757d (gray) +- Success: #198754 (green) +- Warning: #ffc107 (yellow) +- Danger: #dc3545 (red) + +## Screenshots Needing Annotation + +${screenshots.filter(s => s.annotate).map(ss => ` +- **${ss.id}**: ${ss.description} + - Highlight: ${ss.annotate.highlight || 'N/A'} + - Callouts: ${ss.annotate.callouts?.join(', ') || 'N/A'} +`).join('\n')} +`; + + Write(`${workDir}/screenshots/ANNOTATION_GUIDE.md`, guide); +} +``` + +## Troubleshooting + +### Chrome MCP Not Found + +1. Check Claude MCP configuration +2. Verify Chrome is installed +3. Check CHROME_PATH environment variable + +### Screenshots Are Blank + +1. Increase wait time before capture +2. Check if page requires authentication +3. Verify URL is correct + +### Elements Not Visible + +1. Scroll element into view +2. Expand collapsed sections +3. Wait for animations to complete + +### Server Not Starting + +1. Check if port is already in use +2. Verify dev command is correct +3. Check for startup errors in logs diff --git a/.claude/skills/software-manual/scripts/swagger-runner.md b/.claude/skills/software-manual/scripts/swagger-runner.md new file mode 100644 index 00000000..7f6384bc --- /dev/null +++ b/.claude/skills/software-manual/scripts/swagger-runner.md @@ -0,0 +1,419 @@ +# Swagger/OpenAPI Runner + +Guide for generating backend API documentation from OpenAPI/Swagger specifications. + +## Overview + +This script extracts and converts OpenAPI/Swagger specifications to Markdown format for inclusion in the software manual. + +## Detection Strategy + +### Check for Existing Specification + +```javascript +async function detectOpenAPISpec() { + // Check for existing spec files + const specPatterns = [ + 'openapi.json', + 'openapi.yaml', + 'openapi.yml', + 'swagger.json', + 'swagger.yaml', + 'swagger.yml', + '**/openapi*.json', + '**/swagger*.json' + ]; + + for (const pattern of specPatterns) { + const files = Glob(pattern); + if (files.length > 0) { + return { found: true, type: 'file', path: files[0] }; + } + } + + // Check for swagger-jsdoc in dependencies + const packageJson = JSON.parse(Read('package.json')); + if (packageJson.dependencies?.['swagger-jsdoc'] || + packageJson.devDependencies?.['swagger-jsdoc']) { + return { found: true, type: 'jsdoc' }; + } + + // Check for NestJS Swagger + if (packageJson.dependencies?.['@nestjs/swagger']) { + return { found: true, type: 'nestjs' }; + } + + // Check for runtime endpoint + return { found: false, suggestion: 'runtime' }; +} +``` + +## Extraction Methods + +### Method A: From Existing Spec File + +```javascript +async function extractFromFile(specPath, workDir) { + const outputDir = `${workDir}/api-docs/backend`; + Bash({ command: `mkdir -p "${outputDir}"` }); + + // Copy spec to output + Bash({ command: `cp "${specPath}" "${outputDir}/openapi.json"` }); + + // Convert to Markdown using widdershins + const result = Bash({ + command: `npx widdershins "${specPath}" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL'`, + timeout: 60000 + }); + + return { success: result.exitCode === 0, outputDir }; +} +``` + +### Method B: From swagger-jsdoc + +```javascript +async function extractFromJsDoc(workDir) { + const outputDir = `${workDir}/api-docs/backend`; + + // Look for swagger definition file + const defFiles = Glob('**/swagger*.js').concat(Glob('**/openapi*.js')); + if (defFiles.length === 0) { + return { success: false, error: 'No swagger definition found' }; + } + + // Generate spec + const result = Bash({ + command: `npx swagger-jsdoc -d "${defFiles[0]}" -o "${outputDir}/openapi.json"`, + timeout: 60000 + }); + + if (result.exitCode !== 0) { + return { success: false, error: result.stderr }; + } + + // Convert to Markdown + Bash({ + command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'` + }); + + return { success: true, outputDir }; +} +``` + +### Method C: From NestJS Swagger + +```javascript +async function extractFromNestJS(workDir) { + const outputDir = `${workDir}/api-docs/backend`; + + // NestJS typically exposes /api-docs-json at runtime + // We need to start the server temporarily + + // Start server in background + const server = Bash({ + command: 'npm run start:dev', + run_in_background: true, + timeout: 30000 + }); + + // Wait for server to be ready + await waitForServer('http://localhost:3000', 30000); + + // Fetch OpenAPI spec + const spec = await fetch('http://localhost:3000/api-docs-json'); + const specJson = await spec.json(); + + // Save spec + Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2)); + + // Stop server + KillShell({ shell_id: server.task_id }); + + // Convert to Markdown + Bash({ + command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md" --language_tabs 'javascript:JavaScript' 'bash:cURL'` + }); + + return { success: true, outputDir }; +} +``` + +### Method D: From Runtime Endpoint + +```javascript +async function extractFromRuntime(workDir, serverUrl = 'http://localhost:3000') { + const outputDir = `${workDir}/api-docs/backend`; + + // Common OpenAPI endpoint paths + const endpointPaths = [ + '/api-docs-json', + '/swagger.json', + '/openapi.json', + '/docs/json', + '/api/v1/docs.json' + ]; + + let specJson = null; + + for (const path of endpointPaths) { + try { + const response = await fetch(`${serverUrl}${path}`); + if (response.ok) { + specJson = await response.json(); + break; + } + } catch (e) { + continue; + } + } + + if (!specJson) { + return { success: false, error: 'Could not fetch OpenAPI spec from server' }; + } + + // Save and convert + Write(`${outputDir}/openapi.json`, JSON.stringify(specJson, null, 2)); + + Bash({ + command: `npx widdershins "${outputDir}/openapi.json" -o "${outputDir}/api-reference.md"` + }); + + return { success: true, outputDir }; +} +``` + +## Installation + +### Required Tools + +```bash +# For OpenAPI to Markdown conversion +npm install -g widdershins + +# Or as dev dependency +npm install --save-dev widdershins + +# For generating from JSDoc comments +npm install --save-dev swagger-jsdoc +``` + +## Configuration + +### widdershins Options + +```bash +npx widdershins openapi.json \ + -o api-reference.md \ + --language_tabs 'javascript:JavaScript' 'python:Python' 'bash:cURL' \ + --summary \ + --omitHeader \ + --resolve \ + --expandBody +``` + +| Option | Description | +|--------|-------------| +| `--language_tabs` | Code example languages | +| `--summary` | Use summary as operation heading | +| `--omitHeader` | Don't include title header | +| `--resolve` | Resolve $ref references | +| `--expandBody` | Show full request body | + +### swagger-jsdoc Definition + +Example `swagger-def.js`: + +```javascript +module.exports = { + definition: { + openapi: '3.0.0', + info: { + title: 'MyApp API', + version: '1.0.0', + description: 'API documentation for MyApp' + }, + servers: [ + { url: 'http://localhost:3000/api/v1' } + ] + }, + apis: ['./src/routes/*.js', './src/controllers/*.js'] +}; +``` + +## Output Format + +### Generated Markdown Structure + +```markdown +# MyApp API + +## Overview + +Base URL: `http://localhost:3000/api/v1` + +## Authentication + +This API uses Bearer token authentication. + +--- + +## Projects + +### List Projects + +`GET /projects` + +Returns a list of all projects. + +**Parameters** + +| Name | In | Type | Required | Description | +|------|-----|------|----------|-------------| +| status | query | string | false | Filter by status | +| page | query | integer | false | Page number | + +**Responses** + +| Status | Description | +|--------|-------------| +| 200 | Successful response | +| 401 | Unauthorized | + +**Example Request** + +```javascript +fetch('/api/v1/projects?status=active') + .then(res => res.json()) + .then(data => console.log(data)); +``` + +**Example Response** + +```json +{ + "data": [ + { "id": "1", "name": "Project 1" } + ], + "pagination": { + "page": 1, + "total": 10 + } +} +``` +``` + +## Integration + +### Main Runner + +```javascript +async function runSwaggerExtraction(workDir) { + const detection = await detectOpenAPISpec(); + + if (!detection.found) { + console.log('No OpenAPI spec detected. Skipping backend API docs.'); + return { success: false, skipped: true }; + } + + let result; + + switch (detection.type) { + case 'file': + result = await extractFromFile(detection.path, workDir); + break; + case 'jsdoc': + result = await extractFromJsDoc(workDir); + break; + case 'nestjs': + result = await extractFromNestJS(workDir); + break; + default: + result = await extractFromRuntime(workDir); + } + + if (result.success) { + // Post-process the Markdown + await postProcessApiDocs(result.outputDir); + } + + return result; +} + +async function postProcessApiDocs(outputDir) { + const mdFile = `${outputDir}/api-reference.md`; + let content = Read(mdFile); + + // Remove widdershins header + content = content.replace(/^---[\s\S]*?---\n/, ''); + + // Add custom styling hints + content = content.replace(/^(#{1,3} .+)$/gm, '$1\n'); + + Write(mdFile, content); +} +``` + +## Troubleshooting + +### Common Issues + +#### "widdershins: command not found" + +```bash +npm install -g widdershins +# Or use npx +npx widdershins openapi.json -o api.md +``` + +#### "Error parsing OpenAPI spec" + +```bash +# Validate spec first +npx @redocly/cli lint openapi.json + +# Fix common issues +npx @redocly/cli bundle openapi.json -o fixed.json +``` + +#### "Server not responding" + +Ensure the development server is running and accessible: + +```bash +# Check if server is running +curl http://localhost:3000/health + +# Check OpenAPI endpoint +curl http://localhost:3000/api-docs-json +``` + +### Manual Fallback + +If automatic extraction fails, document APIs manually: + +1. List all route files: `Glob('**/routes/*.js')` +2. Extract route definitions using regex +3. Build documentation structure manually + +```javascript +async function manualApiExtraction(workDir) { + const routeFiles = Glob('src/routes/*.js').concat(Glob('src/routes/*.ts')); + const endpoints = []; + + for (const file of routeFiles) { + const content = Read(file); + const routes = content.matchAll(/router\.(get|post|put|delete|patch)\(['"]([^'"]+)['"]/g); + + for (const match of routes) { + endpoints.push({ + method: match[1].toUpperCase(), + path: match[2], + file: file + }); + } + } + + return endpoints; +} +``` diff --git a/.claude/skills/software-manual/scripts/typedoc-runner.md b/.claude/skills/software-manual/scripts/typedoc-runner.md new file mode 100644 index 00000000..b98f0a00 --- /dev/null +++ b/.claude/skills/software-manual/scripts/typedoc-runner.md @@ -0,0 +1,357 @@ +# TypeDoc Runner + +Guide for generating frontend API documentation using TypeDoc. + +## Overview + +TypeDoc generates API documentation from TypeScript source code by analyzing type annotations and JSDoc comments. + +## Prerequisites + +### Check TypeScript Project + +```javascript +// Verify TypeScript is used +const packageJson = JSON.parse(Read('package.json')); +const hasTypeScript = packageJson.devDependencies?.typescript || + packageJson.dependencies?.typescript; + +if (!hasTypeScript) { + console.log('Not a TypeScript project. Skipping TypeDoc.'); + return; +} + +// Check for tsconfig.json +const hasTsConfig = Glob('tsconfig.json').length > 0; +``` + +## Installation + +### Install TypeDoc + +```bash +npm install --save-dev typedoc typedoc-plugin-markdown +``` + +### Optional Plugins + +```bash +# For better Markdown output +npm install --save-dev typedoc-plugin-markdown + +# For README inclusion +npm install --save-dev typedoc-plugin-rename-defaults +``` + +## Configuration + +### typedoc.json + +Create `typedoc.json` in project root: + +```json +{ + "entryPoints": ["./src/index.ts"], + "entryPointStrategy": "expand", + "out": ".workflow/.scratchpad/manual-{timestamp}/api-docs/frontend", + "plugin": ["typedoc-plugin-markdown"], + "exclude": [ + "**/node_modules/**", + "**/*.test.ts", + "**/*.spec.ts", + "**/tests/**" + ], + "excludePrivate": true, + "excludeProtected": true, + "excludeInternal": true, + "hideGenerator": true, + "readme": "none", + "categorizeByGroup": true, + "navigation": { + "includeCategories": true, + "includeGroups": true + } +} +``` + +### Alternative: CLI Options + +```bash +npx typedoc \ + --entryPoints src/index.ts \ + --entryPointStrategy expand \ + --out api-docs/frontend \ + --plugin typedoc-plugin-markdown \ + --exclude "**/node_modules/**" \ + --exclude "**/*.test.ts" \ + --excludePrivate \ + --excludeProtected \ + --readme none +``` + +## Execution + +### Basic Run + +```javascript +async function runTypeDoc(workDir) { + const outputDir = `${workDir}/api-docs/frontend`; + + // Create output directory + Bash({ command: `mkdir -p "${outputDir}"` }); + + // Run TypeDoc + const result = Bash({ + command: `npx typedoc --out "${outputDir}" --plugin typedoc-plugin-markdown src/`, + timeout: 120000 // 2 minutes + }); + + if (result.exitCode !== 0) { + console.error('TypeDoc failed:', result.stderr); + return { success: false, error: result.stderr }; + } + + // List generated files + const files = Glob(`${outputDir}/**/*.md`); + console.log(`Generated ${files.length} documentation files`); + + return { success: true, files }; +} +``` + +### With Custom Entry Points + +```javascript +async function runTypeDocCustom(workDir, entryPoints) { + const outputDir = `${workDir}/api-docs/frontend`; + + // Build entry points string + const entries = entryPoints.map(e => `--entryPoints "${e}"`).join(' '); + + const result = Bash({ + command: `npx typedoc ${entries} --out "${outputDir}" --plugin typedoc-plugin-markdown`, + timeout: 120000 + }); + + return { success: result.exitCode === 0 }; +} + +// Example: Document specific files +await runTypeDocCustom(workDir, [ + 'src/api/index.ts', + 'src/hooks/index.ts', + 'src/utils/index.ts' +]); +``` + +## Output Structure + +``` +api-docs/frontend/ +├── README.md # Index +├── modules.md # Module list +├── modules/ +│ ├── api.md # API module +│ ├── hooks.md # Hooks module +│ └── utils.md # Utils module +├── classes/ +│ ├── ApiClient.md # Class documentation +│ └── ... +├── interfaces/ +│ ├── Config.md # Interface documentation +│ └── ... +└── functions/ + ├── formatDate.md # Function documentation + └── ... +``` + +## Integration with Manual + +### Reading TypeDoc Output + +```javascript +async function integrateTypeDocOutput(workDir) { + const apiDocsDir = `${workDir}/api-docs/frontend`; + const files = Glob(`${apiDocsDir}/**/*.md`); + + // Build API reference content + let content = '## Frontend API Reference\n\n'; + + // Add modules + const modules = Glob(`${apiDocsDir}/modules/*.md`); + for (const mod of modules) { + const modContent = Read(mod); + content += `### ${extractTitle(modContent)}\n\n`; + content += summarizeModule(modContent); + } + + // Add functions + const functions = Glob(`${apiDocsDir}/functions/*.md`); + content += '\n### Functions\n\n'; + for (const fn of functions) { + const fnContent = Read(fn); + content += formatFunctionDoc(fnContent); + } + + // Add hooks + const hooks = Glob(`${apiDocsDir}/functions/*Hook*.md`); + if (hooks.length > 0) { + content += '\n### Hooks\n\n'; + for (const hook of hooks) { + const hookContent = Read(hook); + content += formatHookDoc(hookContent); + } + } + + return content; +} +``` + +### Example Output Format + +```markdown +## Frontend API Reference + +### API Module + +Functions for interacting with the backend API. + +#### fetchProjects + +```typescript +function fetchProjects(options?: FetchOptions): Promise +``` + +Fetches all projects for the current user. + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| options | FetchOptions | Optional fetch configuration | + +**Returns:** Promise + +### Hooks + +#### useProjects + +```typescript +function useProjects(options?: UseProjectsOptions): UseProjectsResult +``` + +React hook for managing project data. + +**Parameters:** + +| Name | Type | Description | +|------|------|-------------| +| options.status | string | Filter by project status | +| options.limit | number | Max projects to fetch | + +**Returns:** + +| Property | Type | Description | +|----------|------|-------------| +| projects | Project[] | Array of projects | +| loading | boolean | Loading state | +| error | Error \| null | Error if failed | +| refetch | () => void | Refresh data | +``` + +## Troubleshooting + +### Common Issues + +#### "Cannot find module 'typescript'" + +```bash +npm install --save-dev typescript +``` + +#### "No entry points found" + +Ensure entry points exist: + +```bash +# Check entry points +ls src/index.ts + +# Or use glob pattern +npx typedoc --entryPoints "src/**/*.ts" +``` + +#### "Unsupported TypeScript version" + +```bash +# Check TypeDoc compatibility +npm info typedoc peerDependencies + +# Install compatible version +npm install --save-dev typedoc@0.25.x +``` + +### Debugging + +```bash +# Verbose output +npx typedoc --logLevel Verbose src/ + +# Show warnings +npx typedoc --treatWarningsAsErrors src/ +``` + +## Best Practices + +### Document Exports Only + +```typescript +// Good: Public API documented +/** + * Fetches projects from the API. + * @param options - Fetch options + * @returns Promise resolving to projects + */ +export function fetchProjects(options?: FetchOptions): Promise { + // ... +} + +// Internal: Not documented +function internalHelper() { + // ... +} +``` + +### Use JSDoc Comments + +```typescript +/** + * User hook for managing authentication state. + * + * @example + * ```tsx + * const { user, login, logout } = useAuth(); + * ``` + * + * @returns Authentication state and methods + */ +export function useAuth(): AuthResult { + // ... +} +``` + +### Define Types Properly + +```typescript +/** + * Configuration for the API client. + */ +export interface ApiConfig { + /** API base URL */ + baseUrl: string; + /** Request timeout in milliseconds */ + timeout?: number; + /** Custom headers to include */ + headers?: Record; +} +``` diff --git a/.claude/skills/software-manual/specs/html-template.md b/.claude/skills/software-manual/specs/html-template.md new file mode 100644 index 00000000..c93e5ac0 --- /dev/null +++ b/.claude/skills/software-manual/specs/html-template.md @@ -0,0 +1,325 @@ +# HTML Template Specification + +Technical specification for the TiddlyWiki-style HTML output. + +## Overview + +The output is a single, self-contained HTML file with: +- All CSS embedded inline +- All JavaScript embedded inline +- All images embedded as Base64 +- Full offline functionality + +## File Structure + +```html + + + + + + {{SOFTWARE_NAME}} - User Manual + + + + +
    ...
    + + + + + +``` + +## Placeholders + +| Placeholder | Description | Source | +|-------------|-------------|--------| +| `{{SOFTWARE_NAME}}` | Software name | manual-config.json | +| `{{VERSION}}` | Version number | manual-config.json | +| `{{EMBEDDED_CSS}}` | All CSS content | wiki-base.css + wiki-dark.css | +| `{{TOC_HTML}}` | Table of contents | Generated from sections | +| `{{TIDDLERS_HTML}}` | All content blocks | Converted from Markdown | +| `{{SEARCH_INDEX_JSON}}` | Search data | Generated from content | +| `{{EMBEDDED_JS}}` | JavaScript code | Inline in template | +| `{{TIMESTAMP}}` | Generation timestamp | ISO 8601 format | +| `{{LOGO_BASE64}}` | Logo image | Project logo or generated | + +## Component Specifications + +### Sidebar (`.wiki-sidebar`) + +``` +Width: 280px (fixed) +Position: Fixed left +Height: 100vh +Components: + - Logo area (.wiki-logo) + - Search box (.wiki-search) + - Tag navigation (.wiki-tags) + - Table of contents (.wiki-toc) +``` + +### Main Content (`.wiki-content`) + +``` +Margin-left: 280px (sidebar width) +Max-width: 900px (content) +Components: + - Header bar (.content-header) + - Tiddler container (.tiddler-container) + - Footer (.wiki-footer) +``` + +### Tiddler (Content Block) + +```html +
    +
    +

    + + {{TITLE}} +

    +
    + {{DIFFICULTY_LABEL}} + {{TAG_BADGES}} +
    +
    +
    + {{CONTENT_HTML}} +
    +
    +``` + +### Search Index Format + +```json +{ + "tiddler-overview": { + "title": "Product Overview", + "body": "Plain text content for searching...", + "tags": ["getting-started", "overview"] + }, + "tiddler-ui-guide": { + "title": "UI Guide", + "body": "Plain text content...", + "tags": ["ui-guide"] + } +} +``` + +## Interactive Features + +### 1. Search + +- Full-text search with result highlighting +- Searches title, body, and tags +- Shows up to 10 results +- Keyboard accessible (Enter to search, Esc to close) + +### 2. Collapse/Expand + +- Per-section toggle via button +- Expand All / Collapse All buttons +- State indicated by ▼ (expanded) or ▶ (collapsed) +- Smooth transition animation + +### 3. Tag Filtering + +- Tags: all, getting-started, ui-guide, api, config, troubleshooting, examples +- Single selection (radio behavior) +- "all" shows everything +- Hidden tiddlers via `display: none` + +### 4. Theme Toggle + +- Light/Dark mode switch +- Persists to localStorage (`wiki-theme`) +- Applies to entire document via `[data-theme="dark"]` +- Toggle button shows sun/moon icon + +### 5. Responsive Design + +``` +Breakpoints: + - Desktop (> 1024px): Sidebar visible + - Tablet (768-1024px): Sidebar collapsible + - Mobile (< 768px): Sidebar hidden, hamburger menu +``` + +### 6. Print Support + +- Hides sidebar, toggles, interactive elements +- Expands all collapsed sections +- Adjusts colors for print +- Page breaks between sections + +## Accessibility + +### Keyboard Navigation + +- Tab through interactive elements +- Enter to activate buttons +- Escape to close search results +- Arrow keys in search results + +### ARIA Attributes + +```html + +
    + + + {{TOC_HTML}} + + + +
    + +
    + +
    + + + +
    +
    + + +
    +{{TIDDLERS_HTML}} +
    + + +
    +

    Generated by software-manual-skill

    +

    Last updated:

    +
    +
    + + + + + + + + + + + + + + diff --git a/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json b/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json new file mode 100644 index 00000000..55b32eb7 --- /dev/null +++ b/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json @@ -0,0 +1,219 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "discovery-finding-schema", + "title": "Discovery Finding Schema", + "description": "Schema for perspective-based issue discovery results", + "type": "object", + "required": ["perspective", "discovery_id", "analysis_timestamp", "cli_tool_used", "summary", "findings"], + "properties": { + "perspective": { + "type": "string", + "enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"], + "description": "Discovery perspective" + }, + "discovery_id": { + "type": "string", + "pattern": "^DSC-\\d{8}-\\d{6}$", + "description": "Parent discovery session ID" + }, + "analysis_timestamp": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of analysis" + }, + "cli_tool_used": { + "type": "string", + "enum": ["gemini", "qwen", "codex"], + "description": "CLI tool that performed the analysis" + }, + "model": { + "type": "string", + "description": "Specific model version used", + "examples": ["gemini-2.5-pro", "qwen-max"] + }, + "analysis_duration_ms": { + "type": "integer", + "minimum": 0, + "description": "Analysis duration in milliseconds" + }, + "summary": { + "type": "object", + "required": ["total_findings"], + "properties": { + "total_findings": { "type": "integer", "minimum": 0 }, + "critical": { "type": "integer", "minimum": 0 }, + "high": { "type": "integer", "minimum": 0 }, + "medium": { "type": "integer", "minimum": 0 }, + "low": { "type": "integer", "minimum": 0 }, + "files_analyzed": { "type": "integer", "minimum": 0 } + }, + "description": "Summary statistics (FLAT structure, NOT nested)" + }, + "findings": { + "type": "array", + "items": { + "type": "object", + "required": ["id", "title", "perspective", "priority", "category", "description", "file", "line"], + "properties": { + "id": { + "type": "string", + "pattern": "^dsc-[a-z]+-\\d{3}-[a-f0-9]{8}$", + "description": "Unique finding ID: dsc-{perspective}-{seq}-{uuid8}", + "examples": ["dsc-bug-001-a1b2c3d4"] + }, + "title": { + "type": "string", + "minLength": 10, + "maxLength": 200, + "description": "Concise finding title" + }, + "perspective": { + "type": "string", + "enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"] + }, + "priority": { + "type": "string", + "enum": ["critical", "high", "medium", "low"], + "description": "Priority level (lowercase only)" + }, + "category": { + "type": "string", + "description": "Perspective-specific category", + "examples": ["null-check", "edge-case", "missing-test", "complexity", "injection"] + }, + "description": { + "type": "string", + "minLength": 20, + "description": "Detailed description of the finding" + }, + "file": { + "type": "string", + "description": "File path relative to project root" + }, + "line": { + "type": "integer", + "minimum": 1, + "description": "Line number of the finding" + }, + "snippet": { + "type": "string", + "description": "Relevant code snippet" + }, + "suggested_issue": { + "type": "object", + "required": ["title", "type", "priority"], + "properties": { + "title": { + "type": "string", + "description": "Suggested issue title for export" + }, + "type": { + "type": "string", + "enum": ["bug", "feature", "enhancement", "refactor", "test", "docs"], + "description": "Issue type" + }, + "priority": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "description": "Priority 1-5 (1=critical, 5=low)" + }, + "labels": { + "type": "array", + "items": { "type": "string" }, + "description": "Suggested labels for the issue" + } + }, + "description": "Pre-filled issue suggestion for export" + }, + "external_reference": { + "type": ["object", "null"], + "properties": { + "source": { "type": "string" }, + "url": { "type": "string", "format": "uri" }, + "relevance": { "type": "string" } + }, + "description": "External reference from Exa research (if applicable)" + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "Confidence score 0.0-1.0" + }, + "impact": { + "type": "string", + "description": "Description of potential impact" + }, + "recommendation": { + "type": "string", + "description": "Specific recommendation to address the finding" + }, + "metadata": { + "type": "object", + "additionalProperties": true, + "description": "Additional metadata (CWE ID, OWASP category, etc.)" + } + } + }, + "description": "Array of discovered findings" + }, + "cross_references": { + "type": "array", + "items": { + "type": "object", + "properties": { + "finding_id": { "type": "string" }, + "related_perspectives": { + "type": "array", + "items": { "type": "string" } + }, + "reason": { "type": "string" } + } + }, + "description": "Cross-references to findings in other perspectives" + } + }, + "examples": [ + { + "perspective": "bug", + "discovery_id": "DSC-20250128-143022", + "analysis_timestamp": "2025-01-28T14:35:00Z", + "cli_tool_used": "gemini", + "model": "gemini-2.5-pro", + "analysis_duration_ms": 45000, + "summary": { + "total_findings": 8, + "critical": 1, + "high": 2, + "medium": 3, + "low": 2, + "files_analyzed": 5 + }, + "findings": [ + { + "id": "dsc-bug-001-a1b2c3d4", + "title": "Missing null check in user validation", + "perspective": "bug", + "priority": "high", + "category": "null-check", + "description": "User object is accessed without null check after database query, which may fail if user doesn't exist", + "file": "src/auth/validator.ts", + "line": 45, + "snippet": "const user = await db.findUser(id);\nreturn user.email; // user may be null", + "suggested_issue": { + "title": "Add null check in user validation", + "type": "bug", + "priority": 2, + "labels": ["bug", "auth"] + }, + "external_reference": null, + "confidence": 0.85, + "impact": "Runtime error when user not found", + "recommendation": "Add null check: if (!user) throw new NotFoundError('User not found');" + } + ], + "cross_references": [] + } + ] +} diff --git a/.claude/workflows/cli-templates/schemas/discovery-state-schema.json b/.claude/workflows/cli-templates/schemas/discovery-state-schema.json new file mode 100644 index 00000000..589bc729 --- /dev/null +++ b/.claude/workflows/cli-templates/schemas/discovery-state-schema.json @@ -0,0 +1,108 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "discovery-state-schema", + "title": "Discovery State Schema", + "description": "Schema for issue discovery session state machine", + "type": "object", + "required": ["discovery_id", "target_pattern", "metadata", "phase"], + "properties": { + "discovery_id": { + "type": "string", + "description": "Unique discovery session ID", + "pattern": "^DSC-\\d{8}-\\d{6}$", + "examples": ["DSC-20250128-143022"] + }, + "target_pattern": { + "type": "string", + "description": "File/directory pattern being analyzed", + "examples": ["src/auth/**", "src/payment/**,src/api/**"] + }, + "metadata": { + "type": "object", + "required": ["created_at", "resolved_files", "perspectives"], + "properties": { + "created_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of discovery creation" + }, + "resolved_files": { + "type": "array", + "items": { "type": "string" }, + "description": "List of resolved file paths from pattern" + }, + "perspectives": { + "type": "array", + "items": { + "type": "string", + "enum": ["bug", "ux", "test", "quality", "security", "performance", "maintainability", "best-practices"] + }, + "description": "Selected discovery perspectives" + }, + "external_research_enabled": { + "type": "boolean", + "default": false, + "description": "Whether Exa research is enabled" + } + } + }, + "phase": { + "type": "string", + "enum": ["initialization", "parallel", "external", "aggregation", "complete"], + "description": "Current execution phase" + }, + "perspectives_completed": { + "type": "array", + "items": { "type": "string" }, + "description": "List of completed perspective analyses" + }, + "total_findings": { + "type": "integer", + "minimum": 0, + "description": "Total number of findings across all perspectives" + }, + "priority_distribution": { + "type": "object", + "properties": { + "critical": { "type": "integer", "minimum": 0 }, + "high": { "type": "integer", "minimum": 0 }, + "medium": { "type": "integer", "minimum": 0 }, + "low": { "type": "integer", "minimum": 0 } + }, + "description": "Count of findings by priority level" + }, + "issues_generated": { + "type": "integer", + "minimum": 0, + "description": "Number of issues generated from discoveries" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp of discovery completion" + } + }, + "examples": [ + { + "discovery_id": "DSC-20250128-143022", + "target_pattern": "src/auth/**", + "metadata": { + "created_at": "2025-01-28T14:30:22Z", + "resolved_files": ["src/auth/service.ts", "src/auth/validator.ts"], + "perspectives": ["bug", "ux", "test", "quality", "security"], + "external_research_enabled": true + }, + "phase": "complete", + "perspectives_completed": ["bug", "ux", "test", "quality", "security"], + "total_findings": 45, + "priority_distribution": { + "critical": 2, + "high": 8, + "medium": 20, + "low": 15 + }, + "issues_generated": 10, + "completed_at": "2025-01-28T14:45:00Z" + } + ] +} diff --git a/ccw/src/core/dashboard-generator.ts b/ccw/src/core/dashboard-generator.ts index d1dfc358..efeb0559 100644 --- a/ccw/src/core/dashboard-generator.ts +++ b/ccw/src/core/dashboard-generator.ts @@ -47,7 +47,8 @@ const MODULE_CSS_FILES = [ '28-mcp-manager.css', '29-help.css', '30-core-memory.css', - '31-api-settings.css' + '31-api-settings.css', + '34-discovery.css' ]; const MODULE_FILES = [ @@ -97,6 +98,8 @@ const MODULE_FILES = [ 'views/rules-manager.js', 'views/claude-manager.js', 'views/api-settings.js', + 'views/issue-manager.js', + 'views/issue-discovery.js', 'views/help.js', 'main.js' ]; diff --git a/ccw/src/core/routes/discovery-routes.ts b/ccw/src/core/routes/discovery-routes.ts new file mode 100644 index 00000000..2382020d --- /dev/null +++ b/ccw/src/core/routes/discovery-routes.ts @@ -0,0 +1,454 @@ +// @ts-nocheck +/** + * Discovery Routes Module + * + * Storage Structure: + * .workflow/issues/discoveries/ + * ├── index.json # Discovery session index + * └── {discovery-id}/ + * ├── discovery-state.json # State machine + * ├── discovery-progress.json # Real-time progress + * ├── perspectives/ # Per-perspective results + * │ ├── bug.json + * │ └── ... + * ├── external-research.json # Exa research results + * ├── discovery-issues.jsonl # Generated candidate issues + * └── reports/ + * + * API Endpoints: + * - GET /api/discoveries - List all discovery sessions + * - GET /api/discoveries/:id - Get discovery session detail + * - GET /api/discoveries/:id/findings - Get all findings + * - GET /api/discoveries/:id/progress - Get real-time progress + * - POST /api/discoveries/:id/export - Export findings as issues + * - PATCH /api/discoveries/:id/findings/:fid - Update finding status + * - DELETE /api/discoveries/:id - Delete discovery session + */ +import type { IncomingMessage, ServerResponse } from 'http'; +import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, rmSync } from 'fs'; +import { join } from 'path'; + +export interface RouteContext { + pathname: string; + url: URL; + req: IncomingMessage; + res: ServerResponse; + initialPath: string; + handlePostRequest: (req: IncomingMessage, res: ServerResponse, handler: (body: unknown) => Promise) => void; + broadcastToClients: (data: unknown) => void; +} + +// ========== Helper Functions ========== + +function getDiscoveriesDir(projectPath: string): string { + return join(projectPath, '.workflow', 'issues', 'discoveries'); +} + +function readDiscoveryIndex(discoveriesDir: string): { discoveries: any[]; total: number } { + const indexPath = join(discoveriesDir, 'index.json'); + if (!existsSync(indexPath)) { + return { discoveries: [], total: 0 }; + } + try { + return JSON.parse(readFileSync(indexPath, 'utf8')); + } catch { + return { discoveries: [], total: 0 }; + } +} + +function writeDiscoveryIndex(discoveriesDir: string, index: any) { + if (!existsSync(discoveriesDir)) { + mkdirSync(discoveriesDir, { recursive: true }); + } + writeFileSync(join(discoveriesDir, 'index.json'), JSON.stringify(index, null, 2)); +} + +function readDiscoveryState(discoveriesDir: string, discoveryId: string): any | null { + const statePath = join(discoveriesDir, discoveryId, 'discovery-state.json'); + if (!existsSync(statePath)) return null; + try { + return JSON.parse(readFileSync(statePath, 'utf8')); + } catch { + return null; + } +} + +function readDiscoveryProgress(discoveriesDir: string, discoveryId: string): any | null { + const progressPath = join(discoveriesDir, discoveryId, 'discovery-progress.json'); + if (!existsSync(progressPath)) return null; + try { + return JSON.parse(readFileSync(progressPath, 'utf8')); + } catch { + return null; + } +} + +function readPerspectiveFindings(discoveriesDir: string, discoveryId: string): any[] { + const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives'); + if (!existsSync(perspectivesDir)) return []; + + const allFindings: any[] = []; + const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json')); + + for (const file of files) { + try { + const content = JSON.parse(readFileSync(join(perspectivesDir, file), 'utf8')); + const perspective = file.replace('.json', ''); + + if (content.findings && Array.isArray(content.findings)) { + allFindings.push({ + perspective, + summary: content.summary || {}, + findings: content.findings.map((f: any) => ({ + ...f, + perspective: f.perspective || perspective + })) + }); + } + } catch { + // Skip invalid files + } + } + + return allFindings; +} + +function readDiscoveryIssues(discoveriesDir: string, discoveryId: string): any[] { + const issuesPath = join(discoveriesDir, discoveryId, 'discovery-issues.jsonl'); + if (!existsSync(issuesPath)) return []; + try { + const content = readFileSync(issuesPath, 'utf8'); + return content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line)); + } catch { + return []; + } +} + +function writeDiscoveryIssues(discoveriesDir: string, discoveryId: string, issues: any[]) { + const issuesPath = join(discoveriesDir, discoveryId, 'discovery-issues.jsonl'); + writeFileSync(issuesPath, issues.map(i => JSON.stringify(i)).join('\n')); +} + +function flattenFindings(perspectiveResults: any[]): any[] { + const allFindings: any[] = []; + for (const result of perspectiveResults) { + if (result.findings) { + allFindings.push(...result.findings); + } + } + return allFindings; +} + +function appendToIssuesJsonl(projectPath: string, issues: any[]) { + const issuesDir = join(projectPath, '.workflow', 'issues'); + const issuesPath = join(issuesDir, 'issues.jsonl'); + + if (!existsSync(issuesDir)) { + mkdirSync(issuesDir, { recursive: true }); + } + + // Read existing issues + let existingIssues: any[] = []; + if (existsSync(issuesPath)) { + try { + const content = readFileSync(issuesPath, 'utf8'); + existingIssues = content.split('\n').filter(line => line.trim()).map(line => JSON.parse(line)); + } catch { + // Start fresh + } + } + + // Convert discovery issues to standard format and append + const newIssues = issues.map(di => ({ + id: di.id, + title: di.title, + status: 'registered', + priority: di.priority || 3, + context: di.context || di.description || '', + source: 'discovery', + source_discovery_id: di.source_discovery_id, + labels: di.labels || [], + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + })); + + const allIssues = [...existingIssues, ...newIssues]; + writeFileSync(issuesPath, allIssues.map(i => JSON.stringify(i)).join('\n')); + + return newIssues.length; +} + +// ========== Route Handler ========== + +export async function handleDiscoveryRoutes(ctx: RouteContext): Promise { + const { pathname, url, req, res, initialPath, handlePostRequest } = ctx; + const projectPath = url.searchParams.get('path') || initialPath; + const discoveriesDir = getDiscoveriesDir(projectPath); + + // GET /api/discoveries - List all discovery sessions + if (pathname === '/api/discoveries' && req.method === 'GET') { + const index = readDiscoveryIndex(discoveriesDir); + + // Enrich with state info + const enrichedDiscoveries = index.discoveries.map((d: any) => { + const state = readDiscoveryState(discoveriesDir, d.discovery_id); + const progress = readDiscoveryProgress(discoveriesDir, d.discovery_id); + return { + ...d, + phase: state?.phase || 'unknown', + total_findings: state?.total_findings || 0, + issues_generated: state?.issues_generated || 0, + priority_distribution: state?.priority_distribution || {}, + progress: progress?.progress || null + }; + }); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + discoveries: enrichedDiscoveries, + total: enrichedDiscoveries.length, + _metadata: { updated_at: new Date().toISOString() } + })); + return true; + } + + // GET /api/discoveries/:id - Get discovery detail + const detailMatch = pathname.match(/^\/api\/discoveries\/([^/]+)$/); + if (detailMatch && req.method === 'GET') { + const discoveryId = detailMatch[1]; + const state = readDiscoveryState(discoveriesDir, discoveryId); + + if (!state) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` })); + return true; + } + + const progress = readDiscoveryProgress(discoveriesDir, discoveryId); + const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId); + const discoveryIssues = readDiscoveryIssues(discoveriesDir, discoveryId); + + // Read external research if exists + let externalResearch = null; + const externalPath = join(discoveriesDir, discoveryId, 'external-research.json'); + if (existsSync(externalPath)) { + try { + externalResearch = JSON.parse(readFileSync(externalPath, 'utf8')); + } catch { + // Ignore + } + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + ...state, + progress: progress?.progress || null, + perspectives: perspectiveResults, + external_research: externalResearch, + discovery_issues: discoveryIssues + })); + return true; + } + + // GET /api/discoveries/:id/findings - Get all findings + const findingsMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/findings$/); + if (findingsMatch && req.method === 'GET') { + const discoveryId = findingsMatch[1]; + + if (!existsSync(join(discoveriesDir, discoveryId))) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` })); + return true; + } + + const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId); + const allFindings = flattenFindings(perspectiveResults); + + // Support filtering + const perspectiveFilter = url.searchParams.get('perspective'); + const priorityFilter = url.searchParams.get('priority'); + + let filtered = allFindings; + if (perspectiveFilter) { + filtered = filtered.filter(f => f.perspective === perspectiveFilter); + } + if (priorityFilter) { + filtered = filtered.filter(f => f.priority === priorityFilter); + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + findings: filtered, + total: filtered.length, + perspectives: [...new Set(allFindings.map(f => f.perspective))], + _metadata: { discovery_id: discoveryId } + })); + return true; + } + + // GET /api/discoveries/:id/progress - Get real-time progress + const progressMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/progress$/); + if (progressMatch && req.method === 'GET') { + const discoveryId = progressMatch[1]; + const progress = readDiscoveryProgress(discoveriesDir, discoveryId); + + if (!progress) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Progress for ${discoveryId} not found` })); + return true; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(progress)); + return true; + } + + // POST /api/discoveries/:id/export - Export findings as issues + const exportMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/export$/); + if (exportMatch && req.method === 'POST') { + handlePostRequest(req, res, async (body: any) => { + const discoveryId = exportMatch[1]; + const { finding_ids, export_all } = body as { finding_ids?: string[]; export_all?: boolean }; + + if (!existsSync(join(discoveriesDir, discoveryId))) { + return { error: `Discovery ${discoveryId} not found` }; + } + + const perspectiveResults = readPerspectiveFindings(discoveriesDir, discoveryId); + const allFindings = flattenFindings(perspectiveResults); + + let toExport: any[]; + if (export_all) { + toExport = allFindings; + } else if (finding_ids && finding_ids.length > 0) { + toExport = allFindings.filter(f => finding_ids.includes(f.id)); + } else { + return { error: 'Either finding_ids or export_all required' }; + } + + if (toExport.length === 0) { + return { error: 'No findings to export' }; + } + + // Convert findings to issue format + const issuesToExport = toExport.map((f, idx) => { + const suggestedIssue = f.suggested_issue || {}; + return { + id: `ISS-${Date.now()}-${idx}`, + title: suggestedIssue.title || f.title, + priority: suggestedIssue.priority || 3, + context: f.description || '', + source: 'discovery', + source_discovery_id: discoveryId, + perspective: f.perspective, + file: f.file, + line: f.line, + labels: suggestedIssue.labels || [f.perspective] + }; + }); + + // Append to main issues.jsonl + const exportedCount = appendToIssuesJsonl(projectPath, issuesToExport); + + // Update discovery state + const state = readDiscoveryState(discoveriesDir, discoveryId); + if (state) { + state.issues_generated = (state.issues_generated || 0) + exportedCount; + writeFileSync( + join(discoveriesDir, discoveryId, 'discovery-state.json'), + JSON.stringify(state, null, 2) + ); + } + + return { + success: true, + exported_count: exportedCount, + issue_ids: issuesToExport.map(i => i.id) + }; + }); + return true; + } + + // PATCH /api/discoveries/:id/findings/:fid - Update finding status + const updateFindingMatch = pathname.match(/^\/api\/discoveries\/([^/]+)\/findings\/([^/]+)$/); + if (updateFindingMatch && req.method === 'PATCH') { + handlePostRequest(req, res, async (body: any) => { + const [, discoveryId, findingId] = updateFindingMatch; + const { status, dismissed } = body as { status?: string; dismissed?: boolean }; + + const perspectivesDir = join(discoveriesDir, discoveryId, 'perspectives'); + if (!existsSync(perspectivesDir)) { + return { error: `Discovery ${discoveryId} not found` }; + } + + // Find and update the finding + const files = readdirSync(perspectivesDir).filter(f => f.endsWith('.json')); + let updated = false; + + for (const file of files) { + const filePath = join(perspectivesDir, file); + try { + const content = JSON.parse(readFileSync(filePath, 'utf8')); + if (content.findings) { + const findingIndex = content.findings.findIndex((f: any) => f.id === findingId); + if (findingIndex !== -1) { + if (status !== undefined) { + content.findings[findingIndex].status = status; + } + if (dismissed !== undefined) { + content.findings[findingIndex].dismissed = dismissed; + } + content.findings[findingIndex].updated_at = new Date().toISOString(); + writeFileSync(filePath, JSON.stringify(content, null, 2)); + updated = true; + break; + } + } + } catch { + // Skip invalid files + } + } + + if (!updated) { + return { error: `Finding ${findingId} not found` }; + } + + return { success: true, finding_id: findingId }; + }); + return true; + } + + // DELETE /api/discoveries/:id - Delete discovery session + const deleteMatch = pathname.match(/^\/api\/discoveries\/([^/]+)$/); + if (deleteMatch && req.method === 'DELETE') { + const discoveryId = deleteMatch[1]; + const discoveryPath = join(discoveriesDir, discoveryId); + + if (!existsSync(discoveryPath)) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Discovery ${discoveryId} not found` })); + return true; + } + + try { + // Remove directory + rmSync(discoveryPath, { recursive: true, force: true }); + + // Update index + const index = readDiscoveryIndex(discoveriesDir); + index.discoveries = index.discoveries.filter((d: any) => d.discovery_id !== discoveryId); + index.total = index.discoveries.length; + writeDiscoveryIndex(discoveriesDir, index); + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: true, deleted: discoveryId })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Failed to delete discovery' })); + } + return true; + } + + // Not handled + return false; +} diff --git a/ccw/src/core/server.ts b/ccw/src/core/server.ts index afcb2a0a..e5994294 100644 --- a/ccw/src/core/server.ts +++ b/ccw/src/core/server.ts @@ -18,6 +18,7 @@ import { handleSystemRoutes } from './routes/system-routes.js'; import { handleFilesRoutes } from './routes/files-routes.js'; import { handleSkillsRoutes } from './routes/skills-routes.js'; import { handleIssueRoutes } from './routes/issue-routes.js'; +import { handleDiscoveryRoutes } from './routes/discovery-routes.js'; import { handleRulesRoutes } from './routes/rules-routes.js'; import { handleSessionRoutes } from './routes/session-routes.js'; import { handleCcwRoutes } from './routes/ccw-routes.js'; @@ -89,7 +90,8 @@ const MODULE_CSS_FILES = [ '30-core-memory.css', '31-api-settings.css', '32-issue-manager.css', - '33-cli-stream-viewer.css' + '33-cli-stream-viewer.css', + '34-discovery.css' ]; // Modular JS files in dependency order @@ -147,6 +149,7 @@ const MODULE_FILES = [ 'views/api-settings.js', 'views/help.js', 'views/issue-manager.js', + 'views/issue-discovery.js', 'main.js' ]; @@ -355,6 +358,11 @@ export async function startServer(options: ServerOptions = {}): Promise' + + '

    ' + t('common.loading') + '

    ' + + ''; + lucide.createIcons(); + + // Load data + await loadDiscoveryData(); + + // Render the main view + renderDiscoveryView(); +} + +// ========== Data Loading ========== +async function loadDiscoveryData() { + discoveryLoading = true; + try { + const response = await fetch('/api/discoveries?path=' + encodeURIComponent(projectPath)); + if (!response.ok) throw new Error('Failed to load discoveries'); + const data = await response.json(); + discoveryData.discoveries = data.discoveries || []; + updateDiscoveryBadge(); + } catch (err) { + console.error('Failed to load discoveries:', err); + discoveryData.discoveries = []; + } finally { + discoveryLoading = false; + } +} + +async function loadDiscoveryDetail(discoveryId) { + try { + const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath)); + if (!response.ok) throw new Error('Failed to load discovery detail'); + return await response.json(); + } catch (err) { + console.error('Failed to load discovery detail:', err); + return null; + } +} + +async function loadDiscoveryFindings(discoveryId) { + try { + let url = '/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings?path=' + encodeURIComponent(projectPath); + if (discoveryData.perspectiveFilter !== 'all') { + url += '&perspective=' + encodeURIComponent(discoveryData.perspectiveFilter); + } + if (discoveryData.priorityFilter !== 'all') { + url += '&priority=' + encodeURIComponent(discoveryData.priorityFilter); + } + const response = await fetch(url); + if (!response.ok) throw new Error('Failed to load findings'); + const data = await response.json(); + return data.findings || []; + } catch (err) { + console.error('Failed to load findings:', err); + return []; + } +} + +async function loadDiscoveryProgress(discoveryId) { + try { + const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/progress?path=' + encodeURIComponent(projectPath)); + if (!response.ok) return null; + return await response.json(); + } catch (err) { + return null; + } +} + +function updateDiscoveryBadge() { + const badge = document.getElementById('badgeDiscovery'); + if (badge) { + badge.textContent = discoveryData.discoveries.length; + } +} + +// ========== Main View Render ========== +function renderDiscoveryView() { + const container = document.getElementById('mainContent'); + if (!container) return; + + container.innerHTML = ` +
    + +
    +
    +
    +
    + +
    +
    +

    ${t('discovery.title') || 'Issue Discovery'}

    +

    ${t('discovery.description') || 'Discover potential issues from multiple perspectives'}

    +
    +
    + +
    + ${discoveryData.viewMode === 'detail' ? ` + + ` : ''} +
    +
    +
    + + ${discoveryData.viewMode === 'list' ? renderDiscoveryListSection() : renderDiscoveryDetailSection()} +
    + `; + + lucide.createIcons(); +} + +// ========== Discovery List Section ========== +function renderDiscoveryListSection() { + const discoveries = discoveryData.discoveries || []; + + if (discoveries.length === 0) { + return ` +
    +
    + +
    +

    ${t('discovery.noDiscoveries') || 'No discoveries yet'}

    +

    ${t('discovery.runCommand') || 'Run /issue:discover to start discovering issues'}

    +
    + /issue:discover src/auth/** +
    +
    + `; + } + + return ` +
    + ${discoveries.map(d => renderDiscoveryCard(d)).join('')} +
    + `; +} + +function renderDiscoveryCard(discovery) { + const { discovery_id, target_pattern, perspectives, phase, total_findings, issues_generated, priority_distribution, progress } = discovery; + + const isComplete = phase === 'complete'; + const isRunning = phase && phase !== 'complete' && phase !== 'failed'; + + // Calculate progress percentage + let progressPercent = 0; + if (progress && progress.perspective_analysis) { + progressPercent = progress.perspective_analysis.percent_complete || 0; + } else if (isComplete) { + progressPercent = 100; + } + + // Priority distribution bar + const critical = priority_distribution?.critical || 0; + const high = priority_distribution?.high || 0; + const medium = priority_distribution?.medium || 0; + const low = priority_distribution?.low || 0; + const total = critical + high + medium + low || 1; + + return ` +
    +
    +
    + + ${discovery_id} +
    + ${phase || 'unknown'} +
    + +
    +
    + + ${target_pattern || 'N/A'} +
    + + ${perspectives && perspectives.length > 0 ? ` +
    + ${perspectives.slice(0, 5).map(p => `${p}`).join('')} + ${perspectives.length > 5 ? `+${perspectives.length - 5}` : ''} +
    + ` : ''} + + ${isRunning ? ` +
    +
    +
    +
    ${progressPercent}% complete
    + ` : ''} + +
    +
    + ${total_findings || 0} + ${t('discovery.findings') || 'Findings'} +
    +
    + ${issues_generated || 0} + ${t('discovery.exported') || 'Exported'} +
    +
    + + ${total_findings > 0 ? ` +
    +
    +
    +
    +
    +
    + ` : ''} +
    + + +
    + `; +} + +// ========== Discovery Detail Section ========== +function renderDiscoveryDetailSection() { + const discovery = discoveryData.selectedDiscovery; + if (!discovery) { + return '
    '; + } + + const findings = discoveryData.findings || []; + const perspectives = [...new Set(findings.map(f => f.perspective))]; + + // Filter findings + let filteredFindings = findings; + if (discoveryData.perspectiveFilter !== 'all') { + filteredFindings = filteredFindings.filter(f => f.perspective === discoveryData.perspectiveFilter); + } + if (discoveryData.priorityFilter !== 'all') { + filteredFindings = filteredFindings.filter(f => f.priority === discoveryData.priorityFilter); + } + if (discoveryData.searchQuery) { + const q = discoveryData.searchQuery.toLowerCase(); + filteredFindings = filteredFindings.filter(f => + (f.title && f.title.toLowerCase().includes(q)) || + (f.file && f.file.toLowerCase().includes(q)) || + (f.description && f.description.toLowerCase().includes(q)) + ); + } + + return ` +
    + +
    + +
    +
    + + +
    + +
    + + +
    + ${filteredFindings.length} ${t('discovery.findings') || 'findings'} + ${discoveryData.selectedFindings.size > 0 ? ` + (${discoveryData.selectedFindings.size} selected) + ` : ''} +
    + + +
    + ${filteredFindings.length === 0 ? ` +
    + +

    ${t('discovery.noFindings') || 'No findings match your filters'}

    +
    + ` : filteredFindings.map(f => renderFindingItem(f)).join('')} +
    + + + ${discoveryData.selectedFindings.size > 0 ? ` +
    + ${discoveryData.selectedFindings.size} selected + + +
    + ` : ''} +
    + + +
    + ${discoveryData.selectedFinding ? renderFindingPreview(discoveryData.selectedFinding) : ` +
    + +

    ${t('discovery.selectFinding') || 'Select a finding to preview'}

    +
    + `} +
    +
    + `; +} + +function renderFindingItem(finding) { + const isSelected = discoveryData.selectedFindings.has(finding.id); + const isActive = discoveryData.selectedFinding?.id === finding.id; + + return ` +
    +
    + +
    +
    +
    + ${finding.perspective} + ${finding.priority} +
    +
    ${finding.title || 'Untitled'}
    +
    + + ${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''} +
    +
    +
    + `; +} + +function renderFindingPreview(finding) { + return ` +
    +
    +
    + ${finding.perspective} + ${finding.priority} + ${finding.confidence ? `${Math.round(finding.confidence * 100)}% confidence` : ''} +
    +

    ${finding.title || 'Untitled'}

    +
    + +
    +

    ${t('discovery.location') || 'Location'}

    +
    + ${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''} +
    +
    + + ${finding.snippet ? ` +
    +

    ${t('discovery.code') || 'Code'}

    +
    ${escapeHtml(finding.snippet)}
    +
    + ` : ''} + +
    +

    ${t('discovery.description') || 'Description'}

    +

    ${finding.description || 'No description'}

    +
    + + ${finding.impact ? ` +
    +

    ${t('discovery.impact') || 'Impact'}

    +

    ${finding.impact}

    +
    + ` : ''} + + ${finding.recommendation ? ` +
    +

    ${t('discovery.recommendation') || 'Recommendation'}

    +

    ${finding.recommendation}

    +
    + ` : ''} + + ${finding.suggested_issue ? ` +
    +

    ${t('discovery.suggestedIssue') || 'Suggested Issue'}

    +
    +
    ${finding.suggested_issue.title || finding.title}
    +
    + ${finding.suggested_issue.type || 'bug'} + P${finding.suggested_issue.priority || 3} + ${finding.suggested_issue.labels ? finding.suggested_issue.labels.map(l => `${l}`).join('') : ''} +
    +
    +
    + ` : ''} + +
    + + +
    +
    + `; +} + +// ========== Actions ========== +async function viewDiscoveryDetail(discoveryId) { + discoveryData.viewMode = 'detail'; + discoveryData.selectedFinding = null; + discoveryData.selectedFindings.clear(); + discoveryData.perspectiveFilter = 'all'; + discoveryData.priorityFilter = 'all'; + discoveryData.searchQuery = ''; + + // Show loading + renderDiscoveryView(); + + // Load detail + const detail = await loadDiscoveryDetail(discoveryId); + if (detail) { + discoveryData.selectedDiscovery = detail; + // Flatten findings from perspectives + const allFindings = []; + if (detail.perspectives) { + for (const p of detail.perspectives) { + if (p.findings) { + allFindings.push(...p.findings); + } + } + } + discoveryData.findings = allFindings; + } + + // Start polling if running + if (detail && detail.phase && detail.phase !== 'complete' && detail.phase !== 'failed') { + startDiscoveryPolling(discoveryId); + } + + renderDiscoveryView(); +} + +function backToDiscoveryList() { + stopDiscoveryPolling(); + discoveryData.viewMode = 'list'; + discoveryData.selectedDiscovery = null; + discoveryData.selectedFinding = null; + discoveryData.findings = []; + discoveryData.selectedFindings.clear(); + renderDiscoveryView(); +} + +function selectFinding(findingId) { + const finding = discoveryData.findings.find(f => f.id === findingId); + discoveryData.selectedFinding = finding || null; + renderDiscoveryView(); +} + +function toggleFindingSelection(findingId) { + if (discoveryData.selectedFindings.has(findingId)) { + discoveryData.selectedFindings.delete(findingId); + } else { + discoveryData.selectedFindings.add(findingId); + } + renderDiscoveryView(); +} + +function filterDiscoveryByPerspective(perspective) { + discoveryData.perspectiveFilter = perspective; + renderDiscoveryView(); +} + +function filterDiscoveryByPriority(priority) { + discoveryData.priorityFilter = priority; + renderDiscoveryView(); +} + +function searchDiscoveryFindings(query) { + discoveryData.searchQuery = query; + renderDiscoveryView(); +} + +async function exportSelectedFindings() { + if (discoveryData.selectedFindings.size === 0) return; + + const discoveryId = discoveryData.selectedDiscovery?.discovery_id; + if (!discoveryId) return; + + try { + const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ finding_ids: Array.from(discoveryData.selectedFindings) }) + }); + + const result = await response.json(); + if (result.success) { + showNotification('success', `Exported ${result.exported_count} issues`); + discoveryData.selectedFindings.clear(); + // Reload discovery data + await loadDiscoveryData(); + renderDiscoveryView(); + } else { + showNotification('error', result.error || 'Export failed'); + } + } catch (err) { + console.error('Export failed:', err); + showNotification('error', 'Export failed'); + } +} + +async function exportSingleFinding(findingId) { + const discoveryId = discoveryData.selectedDiscovery?.discovery_id; + if (!discoveryId) return; + + try { + const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/export?path=' + encodeURIComponent(projectPath), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ finding_ids: [findingId] }) + }); + + const result = await response.json(); + if (result.success) { + showNotification('success', 'Exported 1 issue'); + // Reload discovery data + await loadDiscoveryData(); + renderDiscoveryView(); + } else { + showNotification('error', result.error || 'Export failed'); + } + } catch (err) { + console.error('Export failed:', err); + showNotification('error', 'Export failed'); + } +} + +async function dismissFinding(findingId) { + const discoveryId = discoveryData.selectedDiscovery?.discovery_id; + if (!discoveryId) return; + + try { + const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '/findings/' + encodeURIComponent(findingId) + '?path=' + encodeURIComponent(projectPath), { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dismissed: true }) + }); + + const result = await response.json(); + if (result.success) { + // Update local state + const finding = discoveryData.findings.find(f => f.id === findingId); + if (finding) { + finding.dismissed = true; + } + if (discoveryData.selectedFinding?.id === findingId) { + discoveryData.selectedFinding = null; + } + renderDiscoveryView(); + } + } catch (err) { + console.error('Dismiss failed:', err); + } +} + +async function dismissSelectedFindings() { + for (const findingId of discoveryData.selectedFindings) { + await dismissFinding(findingId); + } + discoveryData.selectedFindings.clear(); + renderDiscoveryView(); +} + +async function deleteDiscovery(discoveryId) { + if (!confirm(`Delete discovery ${discoveryId}? This cannot be undone.`)) return; + + try { + const response = await fetch('/api/discoveries/' + encodeURIComponent(discoveryId) + '?path=' + encodeURIComponent(projectPath), { + method: 'DELETE' + }); + + const result = await response.json(); + if (result.success) { + showNotification('success', 'Discovery deleted'); + await loadDiscoveryData(); + renderDiscoveryView(); + } else { + showNotification('error', result.error || 'Delete failed'); + } + } catch (err) { + console.error('Delete failed:', err); + showNotification('error', 'Delete failed'); + } +} + +// ========== Progress Polling ========== +function startDiscoveryPolling(discoveryId) { + stopDiscoveryPolling(); + + discoveryPollingInterval = setInterval(async () => { + const progress = await loadDiscoveryProgress(discoveryId); + if (progress) { + // Update progress in UI + if (discoveryData.selectedDiscovery) { + discoveryData.selectedDiscovery.progress = progress.progress; + discoveryData.selectedDiscovery.phase = progress.phase; + } + + // Stop polling if complete + if (progress.phase === 'complete' || progress.phase === 'failed') { + stopDiscoveryPolling(); + // Reload full detail + viewDiscoveryDetail(discoveryId); + } + } + }, 3000); // Poll every 3 seconds +} + +function stopDiscoveryPolling() { + if (discoveryPollingInterval) { + clearInterval(discoveryPollingInterval); + discoveryPollingInterval = null; + } +} + +// ========== Utilities ========== +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// ========== Cleanup ========== +function cleanupDiscoveryView() { + stopDiscoveryPolling(); + discoveryData.selectedDiscovery = null; + discoveryData.selectedFinding = null; + discoveryData.findings = []; + discoveryData.selectedFindings.clear(); + discoveryData.viewMode = 'list'; +} diff --git a/ccw/src/templates/dashboard.html b/ccw/src/templates/dashboard.html index 45ae639e..22e7671c 100644 --- a/ccw/src/templates/dashboard.html +++ b/ccw/src/templates/dashboard.html @@ -418,6 +418,11 @@ Manager 0
  • +