mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
feat: add issue discovery view for managing discovery sessions and findings
- Implemented main render function for the issue discovery view. - Added data loading functions to fetch discoveries, details, findings, and progress. - Created rendering functions for discovery list and detail sections. - Introduced filtering and searching capabilities for findings. - Implemented actions for exporting and dismissing findings. - Added polling mechanism to track discovery progress. - Included utility functions for HTML escaping and cleanup.
This commit is contained in:
703
.claude/commands/issue/discover.md
Normal file
703
.claude/commands/issue/discover.md
Normal file
@@ -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: "<path-pattern> [--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
|
||||
177
.claude/skills/software-manual/SKILL.md
Normal file
177
.claude/skills/software-manual/SKILL.md
Normal file
@@ -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 |
|
||||
@@ -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.
|
||||
101
.claude/skills/software-manual/phases/02-project-exploration.md
Normal file
101
.claude/skills/software-manual/phases/02-project-exploration.md
Normal file
@@ -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)
|
||||
130
.claude/skills/software-manual/phases/03-parallel-analysis.md
Normal file
130
.claude/skills/software-manual/phases/03-parallel-analysis.md
Normal file
@@ -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]
|
||||
- 用户友好语言,避免技术术语
|
||||
- 步骤编号清晰
|
||||
- 代码块标注语言
|
||||
- 截图标记: <!-- SCREENSHOT: id="ss-xxx" url="/path" description="xxx" -->
|
||||
|
||||
[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)
|
||||
82
.claude/skills/software-manual/phases/03.5-consolidation.md
Normal file
82
.claude/skills/software-manual/phases/03.5-consolidation.md
Normal file
@@ -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. 截图标记格式 (<!-- SCREENSHOT: id="..." -->)
|
||||
3. 交叉引用有效性
|
||||
4. 术语一致性
|
||||
5. 代码块语言标注
|
||||
|
||||
[OUTPUT]
|
||||
1. 写入 ${workDir}/consolidation-summary.md
|
||||
2. 写入 ${workDir}/screenshots-list.json (截图清单)
|
||||
|
||||
[RETURN JSON]
|
||||
{
|
||||
"status": "completed",
|
||||
"sections_checked": <n>,
|
||||
"screenshots_found": <n>,
|
||||
"issues": { "errors": <n>, "warnings": <n> },
|
||||
"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)
|
||||
271
.claude/skills/software-manual/phases/04-screenshot-capture.md
Normal file
271
.claude/skills/software-manual/phases/04-screenshot-capture.md
Normal file
@@ -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.
|
||||
120
.claude/skills/software-manual/phases/05-html-assembly.md
Normal file
120
.claude/skills/software-manual/phases/05-html-assembly.md
Normal file
@@ -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. 处理 <!-- SCREENSHOT: id="..." --> 标记,嵌入 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": "<size>",
|
||||
"sections_count": <n>,
|
||||
"screenshots_embedded": <n>
|
||||
}
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
## Agent 职责
|
||||
|
||||
1. **读取模板** → HTML + CSS
|
||||
2. **转换章节** → Markdown → HTML tiddlers
|
||||
3. **嵌入截图** → Base64 编码
|
||||
4. **生成索引** → 搜索数据
|
||||
5. **组装输出** → 单文件 HTML
|
||||
|
||||
## Markdown 转换规则
|
||||
|
||||
Agent 内部实现:
|
||||
|
||||
```
|
||||
# H1 → <h1>
|
||||
## H2 → <h2>
|
||||
### H3 → <h3>
|
||||
```code``` → <pre><code>
|
||||
**bold** → <strong>
|
||||
*italic* → <em>
|
||||
[text](url) → <a href>
|
||||
- item → <li>
|
||||
<!-- SCREENSHOT: id="xxx" --> → <figure><img src="data:..."></figure>
|
||||
```
|
||||
|
||||
## Tiddler 结构
|
||||
|
||||
```html
|
||||
<article class="tiddler" id="tiddler-{name}" data-tags="..." data-difficulty="...">
|
||||
<header class="tiddler-header">
|
||||
<h2><button class="collapse-toggle">▼</button> {title}</h2>
|
||||
<div class="tiddler-meta">{badges}</div>
|
||||
</header>
|
||||
<div class="tiddler-content">{html}</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
## 输出
|
||||
|
||||
- `{软件名}-使用手册.html` - 最终 HTML
|
||||
- `build-report.json` - 构建报告
|
||||
|
||||
## 质量门禁
|
||||
|
||||
- [ ] HTML 渲染正确
|
||||
- [ ] 搜索功能可用
|
||||
- [ ] 折叠/展开正常
|
||||
- [ ] 主题切换持久化
|
||||
- [ ] 截图显示正确
|
||||
- [ ] 文件大小 < 10MB
|
||||
|
||||
## 下一阶段
|
||||
|
||||
→ [Phase 6: Iterative Refinement](06-iterative-refinement.md)
|
||||
259
.claude/skills/software-manual/phases/06-iterative-refinement.md
Normal file
259
.claude/skills/software-manual/phases/06-iterative-refinement.md
Normal file
@@ -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.
|
||||
447
.claude/skills/software-manual/scripts/screenshot-helper.md
Normal file
447
.claude/skills/software-manual/scripts/screenshot-helper.md
Normal file
@@ -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
|
||||
419
.claude/skills/software-manual/scripts/swagger-runner.md
Normal file
419
.claude/skills/software-manual/scripts/swagger-runner.md
Normal file
@@ -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;
|
||||
}
|
||||
```
|
||||
357
.claude/skills/software-manual/scripts/typedoc-runner.md
Normal file
357
.claude/skills/software-manual/scripts/typedoc-runner.md
Normal file
@@ -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<Project[]>
|
||||
```
|
||||
|
||||
Fetches all projects for the current user.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| options | FetchOptions | Optional fetch configuration |
|
||||
|
||||
**Returns:** Promise<Project[]>
|
||||
|
||||
### 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<Project[]> {
|
||||
// ...
|
||||
}
|
||||
|
||||
// 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<string, string>;
|
||||
}
|
||||
```
|
||||
325
.claude/skills/software-manual/specs/html-template.md
Normal file
325
.claude/skills/software-manual/specs/html-template.md
Normal file
@@ -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
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{SOFTWARE_NAME}} - User Manual</title>
|
||||
<style>{{EMBEDDED_CSS}}</style>
|
||||
</head>
|
||||
<body class="wiki-container" data-theme="light">
|
||||
<aside class="wiki-sidebar">...</aside>
|
||||
<main class="wiki-content">...</main>
|
||||
<button class="theme-toggle">...</button>
|
||||
<script id="search-index" type="application/json">{{SEARCH_INDEX}}</script>
|
||||
<script>{{EMBEDDED_JS}}</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## 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
|
||||
<article class="tiddler"
|
||||
id="tiddler-{{ID}}"
|
||||
data-tags="{{TAGS}}"
|
||||
data-difficulty="{{DIFFICULTY}}">
|
||||
<header class="tiddler-header">
|
||||
<h2 class="tiddler-title">
|
||||
<button class="collapse-toggle">▼</button>
|
||||
{{TITLE}}
|
||||
</h2>
|
||||
<div class="tiddler-meta">
|
||||
<span class="difficulty-badge {{DIFFICULTY}}">{{DIFFICULTY_LABEL}}</span>
|
||||
{{TAG_BADGES}}
|
||||
</div>
|
||||
</header>
|
||||
<div class="tiddler-content">
|
||||
{{CONTENT_HTML}}
|
||||
</div>
|
||||
</article>
|
||||
```
|
||||
|
||||
### 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
|
||||
<input aria-label="Search">
|
||||
<nav aria-label="Table of Contents">
|
||||
<button aria-label="Toggle theme">
|
||||
<div aria-live="polite"> <!-- For search results -->
|
||||
```
|
||||
|
||||
### Color Contrast
|
||||
|
||||
- Text/background ratio ≥ 4.5:1
|
||||
- Interactive elements clearly visible
|
||||
- Focus indicators visible
|
||||
|
||||
## Performance
|
||||
|
||||
### Target Metrics
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Total file size | < 10MB |
|
||||
| Time to interactive | < 2s |
|
||||
| Search latency | < 100ms |
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Lazy loading for images**: `loading="lazy"`
|
||||
2. **Efficient search**: In-memory index, no external requests
|
||||
3. **CSS containment**: Scope styles to components
|
||||
4. **Minimal JavaScript**: Vanilla JS, no libraries
|
||||
|
||||
## CSS Variables
|
||||
|
||||
### Light Theme
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--accent-color: #0d6efd;
|
||||
--border-color: #dee2e6;
|
||||
}
|
||||
```
|
||||
|
||||
### Dark Theme
|
||||
|
||||
```css
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #b8b8b8;
|
||||
--accent-color: #4dabf7;
|
||||
--border-color: #2d3748;
|
||||
}
|
||||
```
|
||||
|
||||
## Markdown to HTML Mapping
|
||||
|
||||
| Markdown | HTML |
|
||||
|----------|------|
|
||||
| `# Heading` | `<h1>` |
|
||||
| `## Heading` | `<h2>` |
|
||||
| `**bold**` | `<strong>` |
|
||||
| `*italic*` | `<em>` |
|
||||
| `` `code` `` | `<code>` |
|
||||
| `[link](url)` | `<a href="url">` |
|
||||
| `- item` | `<ul><li>` |
|
||||
| `1. item` | `<ol><li>` |
|
||||
| ``` ```js ``` | `<pre><code class="language-js">` |
|
||||
| `> quote` | `<blockquote>` |
|
||||
| `---` | `<hr>` |
|
||||
|
||||
## Screenshot Embedding
|
||||
|
||||
### Marker Format
|
||||
|
||||
```markdown
|
||||
<!-- SCREENSHOT: id="ss-login" url="/login" description="Login form" -->
|
||||
```
|
||||
|
||||
### Embedded Format
|
||||
|
||||
```html
|
||||
<figure class="screenshot">
|
||||
<img src="data:image/png;base64,{{BASE64_DATA}}"
|
||||
alt="Login form"
|
||||
loading="lazy">
|
||||
<figcaption>Login form</figcaption>
|
||||
</figure>
|
||||
```
|
||||
|
||||
### Placeholder (if missing)
|
||||
|
||||
```html
|
||||
<div class="screenshot-placeholder">
|
||||
[Screenshot: ss-login - Login form]
|
||||
</div>
|
||||
```
|
||||
|
||||
## File Size Optimization
|
||||
|
||||
### CSS
|
||||
|
||||
- Minify before embedding
|
||||
- Remove unused styles
|
||||
- Combine duplicate rules
|
||||
|
||||
### JavaScript
|
||||
|
||||
- Minify before embedding
|
||||
- Remove console.log statements
|
||||
- Use IIFE for scoping
|
||||
|
||||
### Images
|
||||
|
||||
- Compress before Base64 encoding
|
||||
- Use appropriate dimensions (max 1280px width)
|
||||
- Consider WebP format if browser support is acceptable
|
||||
|
||||
## Validation
|
||||
|
||||
### HTML Validation
|
||||
|
||||
- W3C HTML5 compliance
|
||||
- Proper nesting
|
||||
- Required attributes present
|
||||
|
||||
### CSS Validation
|
||||
|
||||
- Valid property values
|
||||
- No deprecated properties
|
||||
- Vendor prefixes where needed
|
||||
|
||||
### JavaScript
|
||||
|
||||
- No syntax errors
|
||||
- All functions defined
|
||||
- Error handling for edge cases
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Opens in Chrome/Firefox/Safari/Edge
|
||||
- [ ] Search works correctly
|
||||
- [ ] Collapse/expand works
|
||||
- [ ] Tag filtering works
|
||||
- [ ] Theme toggle works
|
||||
- [ ] Print preview correct
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Offline functionality
|
||||
- [ ] All links valid
|
||||
- [ ] All images display
|
||||
- [ ] No console errors
|
||||
253
.claude/skills/software-manual/specs/quality-standards.md
Normal file
253
.claude/skills/software-manual/specs/quality-standards.md
Normal file
@@ -0,0 +1,253 @@
|
||||
# Quality Standards
|
||||
|
||||
Quality gates and standards for software manual generation.
|
||||
|
||||
## Quality Dimensions
|
||||
|
||||
### 1. Completeness (25%)
|
||||
|
||||
All required sections present and adequately covered.
|
||||
|
||||
| Requirement | Weight | Criteria |
|
||||
|-------------|--------|----------|
|
||||
| Overview section | 5 | Product intro, features, quick start |
|
||||
| UI Guide | 5 | All major screens documented |
|
||||
| API Reference | 5 | All public APIs documented |
|
||||
| Configuration | 4 | All config options explained |
|
||||
| Troubleshooting | 3 | Common issues addressed |
|
||||
| Examples | 3 | Multi-level examples provided |
|
||||
|
||||
**Scoring**:
|
||||
- 100%: All sections present with adequate depth
|
||||
- 80%: All sections present, some lacking depth
|
||||
- 60%: Missing 1-2 sections
|
||||
- 40%: Missing 3+ sections
|
||||
- 0%: Critical sections missing (overview, UI guide)
|
||||
|
||||
### 2. Consistency (25%)
|
||||
|
||||
Terminology, style, and structure uniform across sections.
|
||||
|
||||
| Aspect | Check |
|
||||
|--------|-------|
|
||||
| Terminology | Same term for same concept throughout |
|
||||
| Formatting | Consistent heading levels, code block styles |
|
||||
| Tone | Consistent formality level |
|
||||
| Cross-references | All internal links valid |
|
||||
| Screenshot naming | Follow `ss-{feature}-{action}` pattern |
|
||||
|
||||
**Scoring**:
|
||||
- 100%: Zero inconsistencies
|
||||
- 80%: 1-3 minor inconsistencies
|
||||
- 60%: 4-6 inconsistencies
|
||||
- 40%: 7-10 inconsistencies
|
||||
- 0%: Pervasive inconsistencies
|
||||
|
||||
### 3. Depth (25%)
|
||||
|
||||
Content provides sufficient detail for target audience.
|
||||
|
||||
| Level | Criteria |
|
||||
|-------|----------|
|
||||
| Shallow | Basic descriptions only |
|
||||
| Standard | Descriptions + usage examples |
|
||||
| Deep | Descriptions + examples + edge cases + best practices |
|
||||
|
||||
**Per-Section Depth Check**:
|
||||
- [ ] Explains "what" (definition)
|
||||
- [ ] Explains "why" (rationale)
|
||||
- [ ] Explains "how" (procedure)
|
||||
- [ ] Provides examples
|
||||
- [ ] Covers edge cases
|
||||
- [ ] Includes tips/best practices
|
||||
|
||||
**Scoring**:
|
||||
- 100%: Deep coverage on all critical sections
|
||||
- 80%: Standard coverage on all sections
|
||||
- 60%: Shallow coverage on some sections
|
||||
- 40%: Missing depth in critical areas
|
||||
- 0%: Superficial throughout
|
||||
|
||||
### 4. Readability (25%)
|
||||
|
||||
Clear, user-friendly writing that's easy to follow.
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Sentence length | Average < 20 words |
|
||||
| Paragraph length | Average < 5 sentences |
|
||||
| Heading hierarchy | Proper H1 > H2 > H3 nesting |
|
||||
| Code blocks | Language specified |
|
||||
| Lists | Used for 3+ items |
|
||||
| Screenshots | Placed near relevant text |
|
||||
|
||||
**Structural Elements**:
|
||||
- [ ] Clear section headers
|
||||
- [ ] Numbered steps for procedures
|
||||
- [ ] Bullet lists for options/features
|
||||
- [ ] Tables for comparisons
|
||||
- [ ] Code blocks with syntax highlighting
|
||||
- [ ] Screenshots with captions
|
||||
|
||||
**Scoring**:
|
||||
- 100%: All readability criteria met
|
||||
- 80%: Minor structural issues
|
||||
- 60%: Some sections hard to follow
|
||||
- 40%: Significant readability problems
|
||||
- 0%: Unclear, poorly structured
|
||||
|
||||
## Overall Quality Score
|
||||
|
||||
```
|
||||
Overall = (Completeness × 0.25) + (Consistency × 0.25) +
|
||||
(Depth × 0.25) + (Readability × 0.25)
|
||||
```
|
||||
|
||||
**Quality Gates**:
|
||||
|
||||
| Gate | Threshold | Action |
|
||||
|------|-----------|--------|
|
||||
| Pass | ≥ 80% | Proceed to HTML generation |
|
||||
| Review | 60-79% | Address warnings, proceed with caution |
|
||||
| Fail | < 60% | Must address errors before continuing |
|
||||
|
||||
## Issue Classification
|
||||
|
||||
### Errors (Must Fix)
|
||||
|
||||
- Missing required sections
|
||||
- Invalid cross-references
|
||||
- Broken screenshot markers
|
||||
- Code blocks without language
|
||||
- Incomplete procedures (missing steps)
|
||||
|
||||
### Warnings (Should Fix)
|
||||
|
||||
- Terminology inconsistencies
|
||||
- Sections lacking depth
|
||||
- Missing examples
|
||||
- Long paragraphs (> 7 sentences)
|
||||
- Screenshots missing captions
|
||||
|
||||
### Info (Nice to Have)
|
||||
|
||||
- Optimization suggestions
|
||||
- Additional example opportunities
|
||||
- Alternative explanations
|
||||
- Enhancement ideas
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
### Pre-Generation
|
||||
|
||||
- [ ] All agents completed successfully
|
||||
- [ ] No errors in consolidation report
|
||||
- [ ] Overall score ≥ 60%
|
||||
|
||||
### Post-Generation
|
||||
|
||||
- [ ] HTML renders correctly
|
||||
- [ ] Search returns relevant results
|
||||
- [ ] All screenshots display
|
||||
- [ ] Theme toggle works
|
||||
- [ ] Print preview looks good
|
||||
|
||||
### Final Review
|
||||
|
||||
- [ ] User previewed and approved
|
||||
- [ ] File size reasonable (< 10MB)
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Accessible (keyboard navigation works)
|
||||
|
||||
## Automated Checks
|
||||
|
||||
```javascript
|
||||
function runQualityChecks(workDir) {
|
||||
const results = {
|
||||
completeness: checkCompleteness(workDir),
|
||||
consistency: checkConsistency(workDir),
|
||||
depth: checkDepth(workDir),
|
||||
readability: checkReadability(workDir)
|
||||
};
|
||||
|
||||
results.overall = (
|
||||
results.completeness * 0.25 +
|
||||
results.consistency * 0.25 +
|
||||
results.depth * 0.25 +
|
||||
results.readability * 0.25
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function checkCompleteness(workDir) {
|
||||
const requiredSections = [
|
||||
'section-overview.md',
|
||||
'section-ui-guide.md',
|
||||
'section-api-reference.md',
|
||||
'section-configuration.md',
|
||||
'section-troubleshooting.md',
|
||||
'section-examples.md'
|
||||
];
|
||||
|
||||
const existing = Glob(`${workDir}/sections/section-*.md`);
|
||||
const found = requiredSections.filter(s =>
|
||||
existing.some(e => e.endsWith(s))
|
||||
);
|
||||
|
||||
return (found.length / requiredSections.length) * 100;
|
||||
}
|
||||
|
||||
function checkConsistency(workDir) {
|
||||
// Check terminology, cross-references, naming conventions
|
||||
const issues = [];
|
||||
|
||||
// ... implementation ...
|
||||
|
||||
return Math.max(0, 100 - issues.length * 10);
|
||||
}
|
||||
|
||||
function checkDepth(workDir) {
|
||||
// Check content length, examples, edge cases
|
||||
const sections = Glob(`${workDir}/sections/section-*.md`);
|
||||
let totalScore = 0;
|
||||
|
||||
for (const section of sections) {
|
||||
const content = Read(section);
|
||||
let sectionScore = 0;
|
||||
|
||||
if (content.length > 500) sectionScore += 20;
|
||||
if (content.includes('```')) sectionScore += 20;
|
||||
if (content.includes('Example')) sectionScore += 20;
|
||||
if (content.match(/\d+\./g)?.length > 3) sectionScore += 20;
|
||||
if (content.includes('Note:') || content.includes('Tip:')) sectionScore += 20;
|
||||
|
||||
totalScore += sectionScore;
|
||||
}
|
||||
|
||||
return totalScore / sections.length;
|
||||
}
|
||||
|
||||
function checkReadability(workDir) {
|
||||
// Check structure, formatting, organization
|
||||
const sections = Glob(`${workDir}/sections/section-*.md`);
|
||||
let issues = 0;
|
||||
|
||||
for (const section of sections) {
|
||||
const content = Read(section);
|
||||
|
||||
// Check heading hierarchy
|
||||
if (!content.startsWith('# ')) issues++;
|
||||
|
||||
// Check code block languages
|
||||
const codeBlocks = content.match(/```\w*/g);
|
||||
if (codeBlocks?.some(b => b === '```')) issues++;
|
||||
|
||||
// Check paragraph length
|
||||
const paragraphs = content.split('\n\n');
|
||||
if (paragraphs.some(p => p.split('. ').length > 7)) issues++;
|
||||
}
|
||||
|
||||
return Math.max(0, 100 - issues * 10);
|
||||
}
|
||||
```
|
||||
298
.claude/skills/software-manual/specs/writing-style.md
Normal file
298
.claude/skills/software-manual/specs/writing-style.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Writing Style Guide
|
||||
|
||||
User-friendly writing standards for software manuals.
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. User-Centered
|
||||
|
||||
Write for the user, not the developer.
|
||||
|
||||
**Do**:
|
||||
- "Click the **Save** button to save your changes"
|
||||
- "Enter your email address in the login form"
|
||||
|
||||
**Don't**:
|
||||
- "The onClick handler triggers the save mutation"
|
||||
- "POST to /api/auth/login with email in body"
|
||||
|
||||
### 2. Action-Oriented
|
||||
|
||||
Focus on what users can **do**, not what the system does.
|
||||
|
||||
**Do**:
|
||||
- "You can export your data as CSV"
|
||||
- "To create a new project, click **New Project**"
|
||||
|
||||
**Don't**:
|
||||
- "The system exports data in CSV format"
|
||||
- "A new project is created when the button is clicked"
|
||||
|
||||
### 3. Clear and Direct
|
||||
|
||||
Use simple, straightforward language.
|
||||
|
||||
**Do**:
|
||||
- "Select a file to upload"
|
||||
- "The maximum file size is 10MB"
|
||||
|
||||
**Don't**:
|
||||
- "Utilize the file selection interface to designate a file for uploading"
|
||||
- "File size constraints limit uploads to 10 megabytes"
|
||||
|
||||
## Tone
|
||||
|
||||
### Friendly but Professional
|
||||
|
||||
- Conversational but not casual
|
||||
- Helpful but not condescending
|
||||
- Confident but not arrogant
|
||||
|
||||
**Examples**:
|
||||
|
||||
| Too Casual | Just Right | Too Formal |
|
||||
|------------|------------|------------|
|
||||
| "Yo, here's how..." | "Here's how to..." | "The following procedure describes..." |
|
||||
| "Easy peasy!" | "That's all you need to do." | "The procedure has been completed." |
|
||||
| "Don't worry about it" | "You don't need to change this" | "This parameter does not require modification" |
|
||||
|
||||
### Second Person
|
||||
|
||||
Address the user directly as "you".
|
||||
|
||||
**Do**: "You can customize your dashboard..."
|
||||
**Don't**: "Users can customize their dashboards..."
|
||||
|
||||
## Structure
|
||||
|
||||
### Headings
|
||||
|
||||
Use clear, descriptive headings that tell users what they'll learn.
|
||||
|
||||
**Good Headings**:
|
||||
- "Getting Started"
|
||||
- "Creating Your First Project"
|
||||
- "Configuring Email Notifications"
|
||||
- "Troubleshooting Login Issues"
|
||||
|
||||
**Weak Headings**:
|
||||
- "Overview"
|
||||
- "Step 1"
|
||||
- "Settings"
|
||||
- "FAQ"
|
||||
|
||||
### Procedures
|
||||
|
||||
Number steps for sequential tasks.
|
||||
|
||||
```markdown
|
||||
## Creating a New User
|
||||
|
||||
1. Navigate to **Settings** > **Users**.
|
||||
2. Click the **Add User** button.
|
||||
3. Enter the user's email address.
|
||||
4. Select a role from the dropdown.
|
||||
5. Click **Save**.
|
||||
|
||||
The new user will receive an invitation email.
|
||||
```
|
||||
|
||||
### Features/Options
|
||||
|
||||
Use bullet lists for non-sequential items.
|
||||
|
||||
```markdown
|
||||
## Export Options
|
||||
|
||||
You can export your data in several formats:
|
||||
|
||||
- **CSV**: Compatible with spreadsheets
|
||||
- **JSON**: Best for developers
|
||||
- **PDF**: Ideal for sharing reports
|
||||
```
|
||||
|
||||
### Comparisons
|
||||
|
||||
Use tables for comparing options.
|
||||
|
||||
```markdown
|
||||
## Plan Comparison
|
||||
|
||||
| Feature | Free | Pro | Enterprise |
|
||||
|---------|------|-----|------------|
|
||||
| Projects | 3 | Unlimited | Unlimited |
|
||||
| Storage | 1GB | 10GB | 100GB |
|
||||
| Support | Community | Email | Dedicated |
|
||||
```
|
||||
|
||||
## Content Types
|
||||
|
||||
### Conceptual (What Is)
|
||||
|
||||
Explain what something is and why it matters.
|
||||
|
||||
```markdown
|
||||
## What is a Workspace?
|
||||
|
||||
A workspace is a container for your projects and team members. Each workspace
|
||||
has its own settings, billing, and permissions. You might create separate
|
||||
workspaces for different clients or departments.
|
||||
```
|
||||
|
||||
### Procedural (How To)
|
||||
|
||||
Step-by-step instructions for completing a task.
|
||||
|
||||
```markdown
|
||||
## How to Create a Workspace
|
||||
|
||||
1. Click your profile icon in the top-right corner.
|
||||
2. Select **Create Workspace**.
|
||||
3. Enter a name for your workspace.
|
||||
4. Choose a plan (you can upgrade later).
|
||||
5. Click **Create**.
|
||||
|
||||
Your new workspace is ready to use.
|
||||
```
|
||||
|
||||
### Reference (API/Config)
|
||||
|
||||
Detailed specifications and parameters.
|
||||
|
||||
```markdown
|
||||
## Configuration Options
|
||||
|
||||
### `DATABASE_URL`
|
||||
|
||||
- **Type**: String (required)
|
||||
- **Format**: `postgresql://user:password@host:port/database`
|
||||
- **Example**: `postgresql://admin:secret@localhost:5432/myapp`
|
||||
|
||||
Database connection string for PostgreSQL.
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
### Bold
|
||||
|
||||
Use for:
|
||||
- UI elements: Click **Save**
|
||||
- First use of key terms: **Workspaces** contain projects
|
||||
- Emphasis: **Never** share your API key
|
||||
|
||||
### Italic
|
||||
|
||||
Use for:
|
||||
- Introducing new terms: A *workspace* is...
|
||||
- Placeholders: Replace *your-api-key* with...
|
||||
- Emphasis (sparingly): This is *really* important
|
||||
|
||||
### Code
|
||||
|
||||
Use for:
|
||||
- Commands: Run `npm install`
|
||||
- File paths: Edit `config/settings.json`
|
||||
- Environment variables: Set `DATABASE_URL`
|
||||
- API endpoints: POST `/api/users`
|
||||
- Code references: The `handleSubmit` function
|
||||
|
||||
### Code Blocks
|
||||
|
||||
Always specify the language.
|
||||
|
||||
```javascript
|
||||
// Example: Fetching user data
|
||||
const response = await fetch('/api/user');
|
||||
const user = await response.json();
|
||||
```
|
||||
|
||||
### Notes and Warnings
|
||||
|
||||
Use for important callouts.
|
||||
|
||||
```markdown
|
||||
> **Note**: This feature requires a Pro plan.
|
||||
|
||||
> **Warning**: Deleting a workspace cannot be undone.
|
||||
|
||||
> **Tip**: Use keyboard shortcuts to work faster.
|
||||
```
|
||||
|
||||
## Screenshots
|
||||
|
||||
### When to Include
|
||||
|
||||
- First time showing a UI element
|
||||
- Complex interfaces
|
||||
- Before/after comparisons
|
||||
- Error states
|
||||
|
||||
### Guidelines
|
||||
|
||||
- Capture just the relevant area
|
||||
- Use consistent dimensions
|
||||
- Highlight important elements
|
||||
- Add descriptive captions
|
||||
|
||||
```markdown
|
||||
<!-- SCREENSHOT: id="ss-dashboard" description="Main dashboard showing project list" -->
|
||||
|
||||
*The dashboard displays all your projects with their status.*
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Section Example
|
||||
|
||||
```markdown
|
||||
## Inviting Team Members
|
||||
|
||||
You can invite colleagues to collaborate on your projects.
|
||||
|
||||
### To invite a team member:
|
||||
|
||||
1. Open **Settings** > **Team**.
|
||||
2. Click **Invite Member**.
|
||||
3. Enter their email address.
|
||||
4. Select their role:
|
||||
- **Admin**: Full access to all settings
|
||||
- **Editor**: Can edit projects
|
||||
- **Viewer**: Read-only access
|
||||
5. Click **Send Invite**.
|
||||
|
||||
The person will receive an email with a link to join your workspace.
|
||||
|
||||
> **Note**: You can have up to 5 team members on the Free plan.
|
||||
|
||||
<!-- SCREENSHOT: id="ss-invite-team" description="Team invitation dialog" -->
|
||||
```
|
||||
|
||||
## Language Guidelines
|
||||
|
||||
### Avoid Jargon
|
||||
|
||||
| Technical | User-Friendly |
|
||||
|-----------|---------------|
|
||||
| Execute | Run |
|
||||
| Terminate | Stop, End |
|
||||
| Instantiate | Create |
|
||||
| Invoke | Call, Use |
|
||||
| Parameterize | Set, Configure |
|
||||
| Persist | Save |
|
||||
|
||||
### Be Specific
|
||||
|
||||
| Vague | Specific |
|
||||
|-------|----------|
|
||||
| "Click the button" | "Click **Save**" |
|
||||
| "Enter information" | "Enter your email address" |
|
||||
| "An error occurred" | "Your password must be at least 8 characters" |
|
||||
| "It takes a moment" | "This typically takes 2-3 seconds" |
|
||||
|
||||
### Use Active Voice
|
||||
|
||||
| Passive | Active |
|
||||
|---------|--------|
|
||||
| "The file is uploaded" | "Upload the file" |
|
||||
| "Settings are saved" | "Click **Save** to keep your changes" |
|
||||
| "Errors are displayed" | "The form shows any errors" |
|
||||
695
.claude/skills/software-manual/templates/css/wiki-base.css
Normal file
695
.claude/skills/software-manual/templates/css/wiki-base.css
Normal file
@@ -0,0 +1,695 @@
|
||||
/* ========================================
|
||||
TiddlyWiki-Style Base CSS
|
||||
Software Manual Skill
|
||||
======================================== */
|
||||
|
||||
/* ========== CSS Variables ========== */
|
||||
:root {
|
||||
/* Light Theme */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--bg-tertiary: #e9ecef;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #6c757d;
|
||||
--border-color: #dee2e6;
|
||||
--accent-color: #0d6efd;
|
||||
--accent-hover: #0b5ed7;
|
||||
--success-color: #198754;
|
||||
--warning-color: #ffc107;
|
||||
--danger-color: #dc3545;
|
||||
--info-color: #0dcaf0;
|
||||
|
||||
/* Layout */
|
||||
--sidebar-width: 280px;
|
||||
--header-height: 60px;
|
||||
--content-max-width: 900px;
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
|
||||
/* Typography */
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
--font-family-mono: 'SF Mono', Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--line-height: 1.6;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 300ms ease;
|
||||
}
|
||||
|
||||
/* ========== Reset & Base ========== */
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* ========== Layout ========== */
|
||||
.wiki-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ========== Sidebar ========== */
|
||||
.wiki-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: var(--sidebar-width);
|
||||
height: 100vh;
|
||||
background-color: var(--bg-primary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform var(--transition-base);
|
||||
}
|
||||
|
||||
/* Logo Area */
|
||||
.wiki-logo {
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.wiki-logo .logo-placeholder {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto var(--spacing-sm);
|
||||
background: linear-gradient(135deg, var(--accent-color), var(--info-color));
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
|
||||
.wiki-logo h1 {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.wiki-logo .version {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.wiki-search {
|
||||
padding: var(--spacing-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wiki-search input {
|
||||
width: 100%;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: var(--font-size-sm);
|
||||
background-color: var(--bg-secondary);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.wiki-search input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.15);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: var(--spacing-md);
|
||||
right: var(--spacing-md);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: block;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
text-decoration: none;
|
||||
color: var(--text-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.result-excerpt {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.result-excerpt mark {
|
||||
background-color: var(--warning-color);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: var(--spacing-md);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.wiki-tags {
|
||||
padding: var(--spacing-md);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-xs);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.wiki-tags .tag {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 20px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.wiki-tags .tag:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.wiki-tags .tag.active {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Table of Contents */
|
||||
.wiki-toc {
|
||||
flex: 1;
|
||||
padding: var(--spacing-md);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.wiki-toc h3 {
|
||||
font-size: var(--font-size-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.wiki-toc ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.wiki-toc li {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.wiki-toc a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
font-size: var(--font-size-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.wiki-toc a:hover {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* ========== Main Content ========== */
|
||||
.wiki-content {
|
||||
flex: 1;
|
||||
margin-left: var(--sidebar-width);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.content-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--bg-primary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: var(--spacing-sm) var(--spacing-lg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: var(--spacing-sm);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sidebar-toggle span {
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: var(--text-primary);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.header-actions button:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
/* Tiddler Container */
|
||||
.tiddler-container {
|
||||
flex: 1;
|
||||
max-width: var(--content-max-width);
|
||||
margin: 0 auto;
|
||||
padding: var(--spacing-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ========== Tiddler (Content Block) ========== */
|
||||
.tiddler {
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.tiddler:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.tiddler-header {
|
||||
padding: var(--spacing-md) var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.tiddler-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.collapse-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: var(--spacing-xs);
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.tiddler.collapsed .collapse-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.tiddler-meta {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.difficulty-badge {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.difficulty-badge.beginner {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.difficulty-badge.intermediate {
|
||||
background-color: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.difficulty-badge.advanced {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.tag-badge {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tiddler-content {
|
||||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.tiddler.collapsed .tiddler-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ========== Content Typography ========== */
|
||||
.tiddler-content h1,
|
||||
.tiddler-content h2,
|
||||
.tiddler-content h3,
|
||||
.tiddler-content h4 {
|
||||
margin-top: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiddler-content h1 { font-size: 1.75rem; }
|
||||
.tiddler-content h2 { font-size: 1.5rem; }
|
||||
.tiddler-content h3 { font-size: 1.25rem; }
|
||||
.tiddler-content h4 { font-size: 1.125rem; }
|
||||
|
||||
.tiddler-content p {
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.tiddler-content ul,
|
||||
.tiddler-content ol {
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding-left: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.tiddler-content li {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.tiddler-content a {
|
||||
color: var(--accent-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tiddler-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
.tiddler-content code {
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: 0.9em;
|
||||
padding: 2px 6px;
|
||||
background-color: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tiddler-content pre {
|
||||
position: relative;
|
||||
margin-bottom: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background-color: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tiddler-content pre code {
|
||||
padding: 0;
|
||||
background: none;
|
||||
color: #d4d4d4;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.copy-code-btn {
|
||||
position: absolute;
|
||||
top: var(--spacing-sm);
|
||||
right: var(--spacing-sm);
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 0.75rem;
|
||||
background-color: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.tiddler-content pre:hover .copy-code-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.tiddler-content table {
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-md);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.tiddler-content th,
|
||||
.tiddler-content td {
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
border: 1px solid var(--border-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.tiddler-content th {
|
||||
background-color: var(--bg-secondary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tiddler-content tr:nth-child(even) {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* Screenshots */
|
||||
.screenshot {
|
||||
margin: var(--spacing-lg) 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.screenshot img {
|
||||
max-width: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.screenshot figcaption {
|
||||
margin-top: var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.screenshot-placeholder {
|
||||
padding: var(--spacing-xl);
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== Footer ========== */
|
||||
.wiki-footer {
|
||||
padding: var(--spacing-lg);
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-sm);
|
||||
border-top: 1px solid var(--border-color);
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* ========== Theme Toggle ========== */
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: var(--spacing-lg);
|
||||
right: var(--spacing-lg);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: var(--bg-primary);
|
||||
box-shadow: var(--shadow-lg);
|
||||
cursor: pointer;
|
||||
font-size: 1.5rem;
|
||||
z-index: 100;
|
||||
transition: transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
[data-theme="light"] .moon-icon { display: inline; }
|
||||
[data-theme="light"] .sun-icon { display: none; }
|
||||
[data-theme="dark"] .moon-icon { display: none; }
|
||||
[data-theme="dark"] .sun-icon { display: inline; }
|
||||
|
||||
/* ========== Back to Top ========== */
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
bottom: calc(var(--spacing-lg) + 60px);
|
||||
right: var(--spacing-lg);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background-color: var(--accent-color);
|
||||
color: white;
|
||||
font-size: 1.25rem;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all var(--transition-fast);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.back-to-top.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.back-to-top:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* ========== Responsive ========== */
|
||||
@media (max-width: 1024px) {
|
||||
.wiki-sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.wiki-sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.wiki-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tiddler-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wiki-tags {
|
||||
overflow-x: auto;
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: var(--spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Print Styles ========== */
|
||||
@media print {
|
||||
.wiki-sidebar,
|
||||
.theme-toggle,
|
||||
.back-to-top,
|
||||
.content-header,
|
||||
.collapse-toggle,
|
||||
.copy-code-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wiki-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tiddler {
|
||||
break-inside: avoid;
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.tiddler.collapsed .tiddler-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tiddler-content pre {
|
||||
background-color: #f5f5f5 !important;
|
||||
color: #333 !important;
|
||||
}
|
||||
}
|
||||
278
.claude/skills/software-manual/templates/css/wiki-dark.css
Normal file
278
.claude/skills/software-manual/templates/css/wiki-dark.css
Normal file
@@ -0,0 +1,278 @@
|
||||
/* ========================================
|
||||
TiddlyWiki-Style Dark Theme
|
||||
Software Manual Skill
|
||||
======================================== */
|
||||
|
||||
[data-theme="dark"] {
|
||||
/* Dark Theme Colors */
|
||||
--bg-primary: #1a1a2e;
|
||||
--bg-secondary: #16213e;
|
||||
--bg-tertiary: #0f3460;
|
||||
--text-primary: #eaeaea;
|
||||
--text-secondary: #b8b8b8;
|
||||
--text-muted: #888888;
|
||||
--border-color: #2d3748;
|
||||
--accent-color: #4dabf7;
|
||||
--accent-hover: #339af0;
|
||||
--success-color: #51cf66;
|
||||
--warning-color: #ffd43b;
|
||||
--danger-color: #ff6b6b;
|
||||
--info-color: #22b8cf;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dark theme specific overrides */
|
||||
[data-theme="dark"] .wiki-logo .logo-placeholder {
|
||||
background: linear-gradient(135deg, var(--accent-color), #6741d9);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-search input {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-search input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-results {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-result-item {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .search-result-item:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .result-excerpt mark {
|
||||
background-color: rgba(255, 212, 59, 0.3);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-tags .tag {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-tags .tag:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-tags .tag.active {
|
||||
background-color: var(--accent-color);
|
||||
border-color: var(--accent-color);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-toc a:hover {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .content-header {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .sidebar-toggle span {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .header-actions button {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .header-actions button:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-header {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .difficulty-badge.beginner {
|
||||
background-color: rgba(81, 207, 102, 0.2);
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .difficulty-badge.intermediate {
|
||||
background-color: rgba(255, 212, 59, 0.2);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .difficulty-badge.advanced {
|
||||
background-color: rgba(255, 107, 107, 0.2);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tag-badge {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content code {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content pre {
|
||||
background-color: #0d1117;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content pre code {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .copy-code-btn {
|
||||
background-color: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content th {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content tr:nth-child(even) {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content th,
|
||||
[data-theme="dark"] .tiddler-content td {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .screenshot img {
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .screenshot-placeholder {
|
||||
background-color: var(--bg-tertiary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-footer {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .theme-toggle {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .back-to-top {
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .back-to-top:hover {
|
||||
background-color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Scrollbar styling for dark theme */
|
||||
[data-theme="dark"] ::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-track {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
/* Selection color */
|
||||
[data-theme="dark"] ::selection {
|
||||
background-color: rgba(77, 171, 247, 0.3);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Focus styles for accessibility */
|
||||
[data-theme="dark"] :focus {
|
||||
outline-color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .wiki-search input:focus {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(77, 171, 247, 0.2);
|
||||
}
|
||||
|
||||
/* Link colors */
|
||||
[data-theme="dark"] .tiddler-content a {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tiddler-content a:hover {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Blockquote styling */
|
||||
[data-theme="dark"] .tiddler-content blockquote {
|
||||
border-left: 4px solid var(--accent-color);
|
||||
background-color: var(--bg-tertiary);
|
||||
padding: var(--spacing-md);
|
||||
margin: var(--spacing-md) 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Horizontal rule */
|
||||
[data-theme="dark"] .tiddler-content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color);
|
||||
margin: var(--spacing-lg) 0;
|
||||
}
|
||||
|
||||
/* Alert/Note boxes */
|
||||
[data-theme="dark"] .note,
|
||||
[data-theme="dark"] .warning,
|
||||
[data-theme="dark"] .tip,
|
||||
[data-theme="dark"] .danger {
|
||||
padding: var(--spacing-md);
|
||||
border-radius: 6px;
|
||||
margin: var(--spacing-md) 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .note {
|
||||
background-color: rgba(34, 184, 207, 0.1);
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .warning {
|
||||
background-color: rgba(255, 212, 59, 0.1);
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .tip {
|
||||
background-color: rgba(81, 207, 102, 0.1);
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .danger {
|
||||
background-color: rgba(255, 107, 107, 0.1);
|
||||
border-left: 4px solid var(--danger-color);
|
||||
}
|
||||
332
.claude/skills/software-manual/templates/tiddlywiki-shell.html
Normal file
332
.claude/skills/software-manual/templates/tiddlywiki-shell.html
Normal file
@@ -0,0 +1,332 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="{{SOFTWARE_NAME}} - Interactive Software Manual">
|
||||
<meta name="generator" content="software-manual-skill">
|
||||
<title>{{SOFTWARE_NAME}} v{{VERSION}} - User Manual</title>
|
||||
<style>
|
||||
{{EMBEDDED_CSS}}
|
||||
</style>
|
||||
</head>
|
||||
<body class="wiki-container" data-theme="light">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="wiki-sidebar">
|
||||
<!-- Logo and Title -->
|
||||
<div class="wiki-logo">
|
||||
<div class="logo-placeholder">{{SOFTWARE_NAME}}</div>
|
||||
<h1>{{SOFTWARE_NAME}}</h1>
|
||||
<span class="version">v{{VERSION}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="wiki-search">
|
||||
<input type="text" id="searchInput" placeholder="Search documentation..." aria-label="Search">
|
||||
<div id="searchResults" class="search-results" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<!-- Tag Navigation -->
|
||||
<nav class="wiki-tags" aria-label="Filter by category">
|
||||
<button class="tag active" data-tag="all">All</button>
|
||||
<button class="tag" data-tag="getting-started">Getting Started</button>
|
||||
<button class="tag" data-tag="ui-guide">UI Guide</button>
|
||||
<button class="tag" data-tag="api">API</button>
|
||||
<button class="tag" data-tag="config">Configuration</button>
|
||||
<button class="tag" data-tag="troubleshooting">Troubleshooting</button>
|
||||
<button class="tag" data-tag="examples">Examples</button>
|
||||
</nav>
|
||||
|
||||
<!-- Table of Contents -->
|
||||
{{TOC_HTML}}
|
||||
</aside>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="wiki-content">
|
||||
<!-- Header Bar -->
|
||||
<header class="content-header">
|
||||
<button class="sidebar-toggle" id="sidebarToggle" aria-label="Toggle sidebar">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button class="expand-all" id="expandAll">Expand All</button>
|
||||
<button class="collapse-all" id="collapseAll">Collapse All</button>
|
||||
<button class="print-btn" id="printBtn">Print</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tiddler Container -->
|
||||
<div class="tiddler-container">
|
||||
{{TIDDLERS_HTML}}
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="wiki-footer">
|
||||
<p>Generated by <strong>software-manual-skill</strong></p>
|
||||
<p>Last updated: <time datetime="{{TIMESTAMP}}">{{TIMESTAMP}}</time></p>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
<!-- Theme Toggle Button -->
|
||||
<button class="theme-toggle" id="themeToggle" aria-label="Toggle theme">
|
||||
<span class="sun-icon">☀</span>
|
||||
<span class="moon-icon">☾</span>
|
||||
</button>
|
||||
|
||||
<!-- Back to Top Button -->
|
||||
<button class="back-to-top" id="backToTop" aria-label="Back to top">↑</button>
|
||||
|
||||
<!-- Search Index Data -->
|
||||
<script id="search-index" type="application/json">
|
||||
{{SEARCH_INDEX_JSON}}
|
||||
</script>
|
||||
|
||||
<!-- Embedded JavaScript -->
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ========== Search Functionality ==========
|
||||
class WikiSearch {
|
||||
constructor(indexData) {
|
||||
this.index = indexData;
|
||||
}
|
||||
|
||||
search(query) {
|
||||
if (!query || query.length < 2) return [];
|
||||
|
||||
const results = [];
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const queryWords = lowerQuery.split(/\s+/);
|
||||
|
||||
for (const [id, content] of Object.entries(this.index)) {
|
||||
let score = 0;
|
||||
|
||||
// Title match (higher weight)
|
||||
const titleLower = content.title.toLowerCase();
|
||||
if (titleLower.includes(lowerQuery)) {
|
||||
score += 10;
|
||||
}
|
||||
queryWords.forEach(word => {
|
||||
if (titleLower.includes(word)) score += 3;
|
||||
});
|
||||
|
||||
// Body match
|
||||
const bodyLower = content.body.toLowerCase();
|
||||
if (bodyLower.includes(lowerQuery)) {
|
||||
score += 5;
|
||||
}
|
||||
queryWords.forEach(word => {
|
||||
if (bodyLower.includes(word)) score += 1;
|
||||
});
|
||||
|
||||
// Tag match
|
||||
if (content.tags) {
|
||||
content.tags.forEach(tag => {
|
||||
if (tag.toLowerCase().includes(lowerQuery)) score += 4;
|
||||
});
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
results.push({
|
||||
id,
|
||||
title: content.title,
|
||||
excerpt: this.highlight(content.body, query),
|
||||
score
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 10);
|
||||
}
|
||||
|
||||
highlight(text, query) {
|
||||
const maxLength = 150;
|
||||
const lowerText = text.toLowerCase();
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const index = lowerText.indexOf(lowerQuery);
|
||||
|
||||
if (index === -1) {
|
||||
return text.substring(0, maxLength) + (text.length > maxLength ? '...' : '');
|
||||
}
|
||||
|
||||
const start = Math.max(0, index - 40);
|
||||
const end = Math.min(text.length, index + query.length + 80);
|
||||
let excerpt = text.substring(start, end);
|
||||
|
||||
if (start > 0) excerpt = '...' + excerpt;
|
||||
if (end < text.length) excerpt += '...';
|
||||
|
||||
// Highlight matches
|
||||
const regex = new RegExp('(' + query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
||||
return excerpt.replace(regex, '<mark>$1</mark>');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize search
|
||||
const indexData = JSON.parse(document.getElementById('search-index').textContent);
|
||||
const search = new WikiSearch(indexData);
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchResults = document.getElementById('searchResults');
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const query = this.value.trim();
|
||||
const results = search.search(query);
|
||||
|
||||
if (results.length === 0) {
|
||||
searchResults.innerHTML = query.length >= 2
|
||||
? '<div class="no-results">No results found</div>'
|
||||
: '';
|
||||
return;
|
||||
}
|
||||
|
||||
searchResults.innerHTML = results.map(r => `
|
||||
<a href="#${r.id}" class="search-result-item" data-tiddler="${r.id}">
|
||||
<div class="result-title">${r.title}</div>
|
||||
<div class="result-excerpt">${r.excerpt}</div>
|
||||
</a>
|
||||
`).join('');
|
||||
});
|
||||
|
||||
// Clear search on result click
|
||||
searchResults.addEventListener('click', function(e) {
|
||||
const item = e.target.closest('.search-result-item');
|
||||
if (item) {
|
||||
searchInput.value = '';
|
||||
searchResults.innerHTML = '';
|
||||
|
||||
// Expand target tiddler
|
||||
const tiddlerId = item.dataset.tiddler;
|
||||
const tiddler = document.getElementById(tiddlerId);
|
||||
if (tiddler) {
|
||||
tiddler.classList.remove('collapsed');
|
||||
const toggle = tiddler.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▼';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ========== Collapse/Expand ==========
|
||||
document.querySelectorAll('.collapse-toggle').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const tiddler = this.closest('.tiddler');
|
||||
tiddler.classList.toggle('collapsed');
|
||||
this.textContent = tiddler.classList.contains('collapsed') ? '▶' : '▼';
|
||||
});
|
||||
});
|
||||
|
||||
// Expand/Collapse All
|
||||
document.getElementById('expandAll').addEventListener('click', function() {
|
||||
document.querySelectorAll('.tiddler').forEach(t => {
|
||||
t.classList.remove('collapsed');
|
||||
const toggle = t.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▼';
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('collapseAll').addEventListener('click', function() {
|
||||
document.querySelectorAll('.tiddler').forEach(t => {
|
||||
t.classList.add('collapsed');
|
||||
const toggle = t.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▶';
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Tag Filtering ==========
|
||||
document.querySelectorAll('.wiki-tags .tag').forEach(tag => {
|
||||
tag.addEventListener('click', function() {
|
||||
const filter = this.dataset.tag;
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.wiki-tags .tag').forEach(t => t.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Filter tiddlers
|
||||
document.querySelectorAll('.tiddler').forEach(tiddler => {
|
||||
if (filter === 'all') {
|
||||
tiddler.style.display = '';
|
||||
} else {
|
||||
const tags = tiddler.dataset.tags || '';
|
||||
tiddler.style.display = tags.includes(filter) ? '' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Theme Toggle ==========
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const savedTheme = localStorage.getItem('wiki-theme');
|
||||
|
||||
if (savedTheme) {
|
||||
document.body.dataset.theme = savedTheme;
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', function() {
|
||||
const isDark = document.body.dataset.theme === 'dark';
|
||||
document.body.dataset.theme = isDark ? 'light' : 'dark';
|
||||
localStorage.setItem('wiki-theme', document.body.dataset.theme);
|
||||
});
|
||||
|
||||
// ========== Sidebar Toggle (Mobile) ==========
|
||||
document.getElementById('sidebarToggle').addEventListener('click', function() {
|
||||
document.querySelector('.wiki-sidebar').classList.toggle('open');
|
||||
});
|
||||
|
||||
// ========== Back to Top ==========
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
backToTop.classList.toggle('visible', window.scrollY > 300);
|
||||
});
|
||||
|
||||
backToTop.addEventListener('click', function() {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// ========== Print ==========
|
||||
document.getElementById('printBtn').addEventListener('click', function() {
|
||||
window.print();
|
||||
});
|
||||
|
||||
// ========== TOC Navigation ==========
|
||||
document.querySelectorAll('.wiki-toc a').forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
const tiddlerId = this.getAttribute('href').substring(1);
|
||||
const tiddler = document.getElementById(tiddlerId);
|
||||
|
||||
if (tiddler) {
|
||||
// Expand if collapsed
|
||||
tiddler.classList.remove('collapsed');
|
||||
const toggle = tiddler.querySelector('.collapse-toggle');
|
||||
if (toggle) toggle.textContent = '▼';
|
||||
|
||||
// Close sidebar on mobile
|
||||
document.querySelector('.wiki-sidebar').classList.remove('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ========== Code Block Copy ==========
|
||||
document.querySelectorAll('pre').forEach(pre => {
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'copy-code-btn';
|
||||
copyBtn.textContent = 'Copy';
|
||||
copyBtn.addEventListener('click', function() {
|
||||
const code = pre.querySelector('code');
|
||||
navigator.clipboard.writeText(code.textContent).then(() => {
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(() => copyBtn.textContent = 'Copy', 2000);
|
||||
});
|
||||
});
|
||||
pre.appendChild(copyBtn);
|
||||
});
|
||||
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
454
ccw/src/core/routes/discovery-routes.ts
Normal file
454
ccw/src/core/routes/discovery-routes.ts
Normal file
@@ -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<any>) => 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<boolean> {
|
||||
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;
|
||||
}
|
||||
@@ -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<http.Ser
|
||||
if (await handleIssueRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Discovery routes (/api/discoveries*)
|
||||
if (pathname.startsWith('/api/discoveries')) {
|
||||
if (await handleDiscoveryRoutes(routeContext)) return;
|
||||
}
|
||||
|
||||
// Rules routes (/api/rules*)
|
||||
if (pathname.startsWith('/api/rules')) {
|
||||
if (await handleRulesRoutes(routeContext)) return;
|
||||
|
||||
719
ccw/src/templates/dashboard-css/34-discovery.css
Normal file
719
ccw/src/templates/dashboard-css/34-discovery.css
Normal file
@@ -0,0 +1,719 @@
|
||||
/* ==========================================
|
||||
ISSUE DISCOVERY STYLES
|
||||
========================================== */
|
||||
|
||||
/* Discovery Manager Container */
|
||||
.discovery-manager {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.discovery-manager.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Discovery Header */
|
||||
.discovery-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.discovery-back-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted));
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.discovery-back-btn:hover {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--hover));
|
||||
}
|
||||
|
||||
/* Discovery List */
|
||||
.discovery-list-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Discovery Card */
|
||||
.discovery-card {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.discovery-card:hover {
|
||||
border-color: hsl(var(--primary) / 0.5);
|
||||
box-shadow: 0 4px 12px hsl(var(--foreground) / 0.05);
|
||||
}
|
||||
|
||||
.discovery-card.running {
|
||||
border-color: hsl(var(--warning) / 0.5);
|
||||
}
|
||||
|
||||
.discovery-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.discovery-phase {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.discovery-phase.complete {
|
||||
background: hsl(var(--success) / 0.1);
|
||||
color: hsl(var(--success));
|
||||
}
|
||||
|
||||
.discovery-phase.parallel,
|
||||
.discovery-phase.external,
|
||||
.discovery-phase.aggregation {
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.discovery-phase.initialization {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.discovery-card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.discovery-target {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Perspective Badges */
|
||||
.discovery-perspectives {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.perspective-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.perspective-badge.bug {
|
||||
background: hsl(0 84% 60% / 0.1);
|
||||
color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.perspective-badge.ux {
|
||||
background: hsl(262 84% 60% / 0.1);
|
||||
color: hsl(262 84% 60%);
|
||||
}
|
||||
|
||||
.perspective-badge.test {
|
||||
background: hsl(200 84% 50% / 0.1);
|
||||
color: hsl(200 84% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.quality {
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.perspective-badge.security {
|
||||
background: hsl(0 84% 50% / 0.1);
|
||||
color: hsl(0 84% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.performance {
|
||||
background: hsl(38 92% 50% / 0.1);
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.maintainability {
|
||||
background: hsl(280 60% 50% / 0.1);
|
||||
color: hsl(280 60% 50%);
|
||||
}
|
||||
|
||||
.perspective-badge.best-practices {
|
||||
background: hsl(170 60% 45% / 0.1);
|
||||
color: hsl(170 60% 45%);
|
||||
}
|
||||
|
||||
.perspective-badge.more {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.discovery-progress-bar {
|
||||
height: 4px;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-progress-bar .progress-fill {
|
||||
height: 100%;
|
||||
background: hsl(var(--primary));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.discovery-stats {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.discovery-stats .stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.discovery-stats .stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.discovery-stats .stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Priority Distribution Bar */
|
||||
.discovery-priority-bar {
|
||||
display: flex;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.critical {
|
||||
background: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.high {
|
||||
background: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.medium {
|
||||
background: hsl(48 96% 53%);
|
||||
}
|
||||
|
||||
.discovery-priority-bar .priority-segment.low {
|
||||
background: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
.discovery-card-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.discovery-action-btn {
|
||||
padding: 0.375rem;
|
||||
border-radius: 0.375rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.discovery-action-btn:hover {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.discovery-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
text-align: center;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.discovery-empty .empty-icon {
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Discovery Detail Container */
|
||||
.discovery-detail-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 1.5rem;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.discovery-detail-container {
|
||||
grid-template-columns: 1fr;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Findings Panel */
|
||||
.discovery-findings-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.discovery-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted));
|
||||
border: 1px solid hsl(var(--border));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
background: hsl(var(--muted));
|
||||
border: 1px solid hsl(var(--border));
|
||||
flex: 1;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.toolbar-search input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--foreground));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.findings-count {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.findings-count .selected-count {
|
||||
color: hsl(var(--primary));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Findings List */
|
||||
.findings-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.findings-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Finding Item */
|
||||
.finding-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.finding-item:hover {
|
||||
background: hsl(var(--muted) / 0.5);
|
||||
}
|
||||
|
||||
.finding-item.active {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
border: 1px solid hsl(var(--primary) / 0.3);
|
||||
}
|
||||
|
||||
.finding-item.selected {
|
||||
background: hsl(var(--primary) / 0.05);
|
||||
}
|
||||
|
||||
.finding-item.dismissed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.finding-checkbox {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding-top: 0.125rem;
|
||||
}
|
||||
|
||||
.finding-checkbox input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.finding-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.finding-header {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.finding-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.3;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.finding-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Priority Badge */
|
||||
.priority-badge {
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.priority-badge.critical {
|
||||
background: hsl(0 84% 60% / 0.1);
|
||||
color: hsl(0 84% 60%);
|
||||
}
|
||||
|
||||
.priority-badge.high {
|
||||
background: hsl(38 92% 50% / 0.1);
|
||||
color: hsl(38 92% 50%);
|
||||
}
|
||||
|
||||
.priority-badge.medium {
|
||||
background: hsl(48 96% 53% / 0.1);
|
||||
color: hsl(48 70% 40%);
|
||||
}
|
||||
|
||||
.priority-badge.low {
|
||||
background: hsl(142 76% 36% / 0.1);
|
||||
color: hsl(142 76% 36%);
|
||||
}
|
||||
|
||||
/* Bulk Actions */
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
|
||||
.bulk-count {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.bulk-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.bulk-action-btn.export {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.bulk-action-btn.export:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.bulk-action-btn.dismiss {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.bulk-action-btn.dismiss:hover {
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
color: hsl(var(--destructive));
|
||||
}
|
||||
|
||||
/* Preview Panel */
|
||||
.discovery-preview-panel {
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
min-height: 300px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Finding Preview */
|
||||
.finding-preview {
|
||||
padding: 1.25rem;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.preview-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.confidence-badge {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.preview-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.preview-location code {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.preview-snippet {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--muted));
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.preview-snippet code {
|
||||
font-size: 0.75rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.preview-description,
|
||||
.preview-impact,
|
||||
.preview-recommendation {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Suggested Issue */
|
||||
.preview-section.suggested-issue {
|
||||
padding: 0.75rem;
|
||||
background: hsl(var(--primary) / 0.05);
|
||||
border: 1px solid hsl(var(--primary) / 0.2);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.suggested-issue-content {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.suggested-issue-content .issue-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.suggested-issue-content .issue-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.issue-type,
|
||||
.issue-priority,
|
||||
.issue-label {
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.issue-type {
|
||||
background: hsl(var(--primary) / 0.1);
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.issue-priority {
|
||||
background: hsl(var(--warning) / 0.1);
|
||||
color: hsl(var(--warning));
|
||||
}
|
||||
|
||||
.issue-label {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Preview Actions */
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.preview-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.preview-action-btn.primary {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.preview-action-btn.primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.preview-action-btn.secondary {
|
||||
background: hsl(var(--muted));
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.preview-action-btn.secondary:hover {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 0.1);
|
||||
}
|
||||
@@ -161,6 +161,12 @@ function initNavigation() {
|
||||
} else {
|
||||
console.error('renderIssueManager not defined - please refresh the page');
|
||||
}
|
||||
} else if (currentView === 'issue-discovery') {
|
||||
if (typeof renderIssueDiscovery === 'function') {
|
||||
renderIssueDiscovery();
|
||||
} else {
|
||||
console.error('renderIssueDiscovery not defined - please refresh the page');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -207,6 +213,8 @@ function updateContentTitle() {
|
||||
titleEl.textContent = t('title.apiSettings');
|
||||
} else if (currentView === 'issue-manager') {
|
||||
titleEl.textContent = t('title.issueManager');
|
||||
} else if (currentView === 'issue-discovery') {
|
||||
titleEl.textContent = t('title.issueDiscovery');
|
||||
} else if (currentView === 'liteTasks') {
|
||||
const names = { 'lite-plan': t('title.litePlanSessions'), 'lite-fix': t('title.liteFixSessions') };
|
||||
titleEl.textContent = names[currentLiteType] || t('title.liteTasks');
|
||||
|
||||
@@ -1726,7 +1726,62 @@ const i18n = {
|
||||
// Issue Manager
|
||||
'nav.issues': 'Issues',
|
||||
'nav.issueManager': 'Manager',
|
||||
'nav.issueDiscovery': 'Discovery',
|
||||
'title.issueManager': 'Issue Manager',
|
||||
'title.issueDiscovery': 'Issue Discovery',
|
||||
|
||||
// Issue Discovery
|
||||
'discovery.title': 'Issue Discovery',
|
||||
'discovery.description': 'Discover potential issues from multiple perspectives',
|
||||
'discovery.noSessions': 'No discovery sessions',
|
||||
'discovery.runHint': 'Run /issue:discover to start discovering issues',
|
||||
'discovery.sessions': 'Sessions',
|
||||
'discovery.findings': 'Findings',
|
||||
'discovery.phase': 'Phase',
|
||||
'discovery.perspectives': 'Perspectives',
|
||||
'discovery.progress': 'Progress',
|
||||
'discovery.total': 'Total',
|
||||
'discovery.exported': 'Exported',
|
||||
'discovery.dismissed': 'Dismissed',
|
||||
'discovery.pending': 'Pending',
|
||||
'discovery.external': 'External Research',
|
||||
'discovery.selectAll': 'Select All',
|
||||
'discovery.deselectAll': 'Deselect All',
|
||||
'discovery.exportSelected': 'Export Selected',
|
||||
'discovery.dismissSelected': 'Dismiss Selected',
|
||||
'discovery.exportAsIssue': 'Export as Issue',
|
||||
'discovery.dismiss': 'Dismiss',
|
||||
'discovery.keep': 'Keep',
|
||||
'discovery.priority.critical': 'Critical',
|
||||
'discovery.priority.high': 'High',
|
||||
'discovery.priority.medium': 'Medium',
|
||||
'discovery.priority.low': 'Low',
|
||||
'discovery.perspective.bug': 'Bug',
|
||||
'discovery.perspective.ux': 'UX',
|
||||
'discovery.perspective.test': 'Test',
|
||||
'discovery.perspective.quality': 'Quality',
|
||||
'discovery.perspective.security': 'Security',
|
||||
'discovery.perspective.performance': 'Performance',
|
||||
'discovery.perspective.maintainability': 'Maintainability',
|
||||
'discovery.perspective.best-practices': 'Best Practices',
|
||||
'discovery.file': 'File',
|
||||
'discovery.line': 'Line',
|
||||
'discovery.confidence': 'Confidence',
|
||||
'discovery.suggestedIssue': 'Suggested Issue',
|
||||
'discovery.externalRef': 'External Reference',
|
||||
'discovery.noFindings': 'No findings in this session',
|
||||
'discovery.filterPerspective': 'Filter by Perspective',
|
||||
'discovery.filterPriority': 'Filter by Priority',
|
||||
'discovery.filterAll': 'All',
|
||||
'discovery.deleteSession': 'Delete Session',
|
||||
'discovery.confirmDelete': 'Are you sure you want to delete this discovery session?',
|
||||
'discovery.deleted': 'Discovery session deleted',
|
||||
'discovery.exportSuccess': 'Findings exported as issues',
|
||||
'discovery.dismissSuccess': 'Findings dismissed',
|
||||
'discovery.backToList': 'Back to Sessions',
|
||||
'discovery.viewDetails': 'View Details',
|
||||
'discovery.inProgress': 'In Progress',
|
||||
'discovery.completed': 'Completed',
|
||||
// issues.* keys (used by issue-manager.js)
|
||||
'issues.title': 'Issue Manager',
|
||||
'issues.description': 'Manage issues, solutions, and execution queue',
|
||||
@@ -3586,7 +3641,62 @@ const i18n = {
|
||||
// Issue Manager
|
||||
'nav.issues': '议题',
|
||||
'nav.issueManager': '管理器',
|
||||
'nav.issueDiscovery': '发现',
|
||||
'title.issueManager': '议题管理器',
|
||||
'title.issueDiscovery': '议题发现',
|
||||
|
||||
// Issue Discovery
|
||||
'discovery.title': '议题发现',
|
||||
'discovery.description': '从多个视角发现潜在问题',
|
||||
'discovery.noSessions': '暂无发现会话',
|
||||
'discovery.runHint': '运行 /issue:discover 开始发现问题',
|
||||
'discovery.sessions': '会话',
|
||||
'discovery.findings': '发现',
|
||||
'discovery.phase': '阶段',
|
||||
'discovery.perspectives': '视角',
|
||||
'discovery.progress': '进度',
|
||||
'discovery.total': '总计',
|
||||
'discovery.exported': '已导出',
|
||||
'discovery.dismissed': '已忽略',
|
||||
'discovery.pending': '待处理',
|
||||
'discovery.external': '外部研究',
|
||||
'discovery.selectAll': '全选',
|
||||
'discovery.deselectAll': '取消全选',
|
||||
'discovery.exportSelected': '导出选中',
|
||||
'discovery.dismissSelected': '忽略选中',
|
||||
'discovery.exportAsIssue': '导出为议题',
|
||||
'discovery.dismiss': '忽略',
|
||||
'discovery.keep': '保留',
|
||||
'discovery.priority.critical': '紧急',
|
||||
'discovery.priority.high': '高',
|
||||
'discovery.priority.medium': '中',
|
||||
'discovery.priority.low': '低',
|
||||
'discovery.perspective.bug': 'Bug',
|
||||
'discovery.perspective.ux': '用户体验',
|
||||
'discovery.perspective.test': '测试',
|
||||
'discovery.perspective.quality': '代码质量',
|
||||
'discovery.perspective.security': '安全',
|
||||
'discovery.perspective.performance': '性能',
|
||||
'discovery.perspective.maintainability': '可维护性',
|
||||
'discovery.perspective.best-practices': '最佳实践',
|
||||
'discovery.file': '文件',
|
||||
'discovery.line': '行号',
|
||||
'discovery.confidence': '置信度',
|
||||
'discovery.suggestedIssue': '建议议题',
|
||||
'discovery.externalRef': '外部参考',
|
||||
'discovery.noFindings': '此会话暂无发现',
|
||||
'discovery.filterPerspective': '按视角筛选',
|
||||
'discovery.filterPriority': '按优先级筛选',
|
||||
'discovery.filterAll': '全部',
|
||||
'discovery.deleteSession': '删除会话',
|
||||
'discovery.confirmDelete': '确定要删除此发现会话吗?',
|
||||
'discovery.deleted': '发现会话已删除',
|
||||
'discovery.exportSuccess': '发现已导出为议题',
|
||||
'discovery.dismissSuccess': '发现已忽略',
|
||||
'discovery.backToList': '返回列表',
|
||||
'discovery.viewDetails': '查看详情',
|
||||
'discovery.inProgress': '进行中',
|
||||
'discovery.completed': '已完成',
|
||||
// issues.* keys (used by issue-manager.js)
|
||||
'issues.title': '议题管理器',
|
||||
'issues.description': '管理议题、解决方案和执行队列',
|
||||
|
||||
685
ccw/src/templates/dashboard-js/views/issue-discovery.js
Normal file
685
ccw/src/templates/dashboard-js/views/issue-discovery.js
Normal file
@@ -0,0 +1,685 @@
|
||||
// ==========================================
|
||||
// ISSUE DISCOVERY VIEW
|
||||
// Manages discovery sessions and findings
|
||||
// ==========================================
|
||||
|
||||
// ========== Discovery State ==========
|
||||
var discoveryData = {
|
||||
discoveries: [],
|
||||
selectedDiscovery: null,
|
||||
selectedFinding: null,
|
||||
findings: [],
|
||||
perspectiveFilter: 'all',
|
||||
priorityFilter: 'all',
|
||||
searchQuery: '',
|
||||
selectedFindings: new Set(),
|
||||
viewMode: 'list' // 'list' | 'detail'
|
||||
};
|
||||
var discoveryLoading = false;
|
||||
var discoveryPollingInterval = null;
|
||||
|
||||
// ========== Main Render Function ==========
|
||||
async function renderIssueDiscovery() {
|
||||
const container = document.getElementById('mainContent');
|
||||
if (!container) return;
|
||||
|
||||
// Hide stats grid and carousel
|
||||
hideStatsAndCarousel();
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = '<div class="discovery-manager loading">' +
|
||||
'<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>' +
|
||||
'<p>' + t('common.loading') + '</p>' +
|
||||
'</div>';
|
||||
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 = `
|
||||
<div class="discovery-manager">
|
||||
<!-- Header -->
|
||||
<div class="discovery-header mb-6">
|
||||
<div class="flex items-center justify-between flex-wrap gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||
<i data-lucide="search-code" class="w-5 h-5 text-primary"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-foreground">${t('discovery.title') || 'Issue Discovery'}</h2>
|
||||
<p class="text-sm text-muted-foreground">${t('discovery.description') || 'Discover potential issues from multiple perspectives'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
${discoveryData.viewMode === 'detail' ? `
|
||||
<button class="discovery-back-btn" onclick="backToDiscoveryList()">
|
||||
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
||||
<span>${t('common.back') || 'Back'}</span>
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${discoveryData.viewMode === 'list' ? renderDiscoveryListSection() : renderDiscoveryDetailSection()}
|
||||
</div>
|
||||
`;
|
||||
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
// ========== Discovery List Section ==========
|
||||
function renderDiscoveryListSection() {
|
||||
const discoveries = discoveryData.discoveries || [];
|
||||
|
||||
if (discoveries.length === 0) {
|
||||
return `
|
||||
<div class="discovery-empty">
|
||||
<div class="empty-icon">
|
||||
<i data-lucide="search-x" class="w-12 h-12 text-muted-foreground"></i>
|
||||
</div>
|
||||
<h3 class="text-lg font-medium text-foreground mt-4">${t('discovery.noDiscoveries') || 'No discoveries yet'}</h3>
|
||||
<p class="text-sm text-muted-foreground mt-2">${t('discovery.runCommand') || 'Run /issue:discover to start discovering issues'}</p>
|
||||
<div class="mt-4 p-3 bg-muted/50 rounded-lg">
|
||||
<code class="text-sm text-primary">/issue:discover src/auth/**</code>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="discovery-list-container">
|
||||
${discoveries.map(d => renderDiscoveryCard(d)).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="discovery-card ${isComplete ? 'complete' : ''} ${isRunning ? 'running' : ''}" onclick="viewDiscoveryDetail('${discovery_id}')">
|
||||
<div class="discovery-card-header">
|
||||
<div class="discovery-id">
|
||||
<i data-lucide="search" class="w-4 h-4"></i>
|
||||
<span>${discovery_id}</span>
|
||||
</div>
|
||||
<span class="discovery-phase ${phase}">${phase || 'unknown'}</span>
|
||||
</div>
|
||||
|
||||
<div class="discovery-card-body">
|
||||
<div class="discovery-target">
|
||||
<i data-lucide="folder" class="w-4 h-4 text-muted-foreground"></i>
|
||||
<span class="text-sm text-foreground">${target_pattern || 'N/A'}</span>
|
||||
</div>
|
||||
|
||||
${perspectives && perspectives.length > 0 ? `
|
||||
<div class="discovery-perspectives">
|
||||
${perspectives.slice(0, 5).map(p => `<span class="perspective-badge ${p}">${p}</span>`).join('')}
|
||||
${perspectives.length > 5 ? `<span class="perspective-badge more">+${perspectives.length - 5}</span>` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${isRunning ? `
|
||||
<div class="discovery-progress-bar">
|
||||
<div class="progress-fill" style="width: ${progressPercent}%"></div>
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground mt-1">${progressPercent}% complete</div>
|
||||
` : ''}
|
||||
|
||||
<div class="discovery-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">${total_findings || 0}</span>
|
||||
<span class="stat-label">${t('discovery.findings') || 'Findings'}</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${issues_generated || 0}</span>
|
||||
<span class="stat-label">${t('discovery.exported') || 'Exported'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${total_findings > 0 ? `
|
||||
<div class="discovery-priority-bar">
|
||||
<div class="priority-segment critical" style="width: ${(critical / total) * 100}%" title="Critical: ${critical}"></div>
|
||||
<div class="priority-segment high" style="width: ${(high / total) * 100}%" title="High: ${high}"></div>
|
||||
<div class="priority-segment medium" style="width: ${(medium / total) * 100}%" title="Medium: ${medium}"></div>
|
||||
<div class="priority-segment low" style="width: ${(low / total) * 100}%" title="Low: ${low}"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="discovery-card-footer">
|
||||
<button class="discovery-action-btn" onclick="event.stopPropagation(); deleteDiscovery('${discovery_id}')">
|
||||
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========== Discovery Detail Section ==========
|
||||
function renderDiscoveryDetailSection() {
|
||||
const discovery = discoveryData.selectedDiscovery;
|
||||
if (!discovery) {
|
||||
return '<div class="loading-spinner"><i data-lucide="loader-2" class="w-8 h-8 animate-spin"></i></div>';
|
||||
}
|
||||
|
||||
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 `
|
||||
<div class="discovery-detail-container">
|
||||
<!-- Left Panel: Findings List -->
|
||||
<div class="discovery-findings-panel">
|
||||
<!-- Toolbar -->
|
||||
<div class="discovery-toolbar">
|
||||
<div class="toolbar-filters">
|
||||
<select class="filter-select" onchange="filterDiscoveryByPerspective(this.value)">
|
||||
<option value="all" ${discoveryData.perspectiveFilter === 'all' ? 'selected' : ''}>${t('discovery.allPerspectives') || 'All Perspectives'}</option>
|
||||
${perspectives.map(p => `<option value="${p}" ${discoveryData.perspectiveFilter === p ? 'selected' : ''}>${p}</option>`).join('')}
|
||||
</select>
|
||||
<select class="filter-select" onchange="filterDiscoveryByPriority(this.value)">
|
||||
<option value="all" ${discoveryData.priorityFilter === 'all' ? 'selected' : ''}>${t('discovery.allPriorities') || 'All Priorities'}</option>
|
||||
<option value="critical" ${discoveryData.priorityFilter === 'critical' ? 'selected' : ''}>Critical</option>
|
||||
<option value="high" ${discoveryData.priorityFilter === 'high' ? 'selected' : ''}>High</option>
|
||||
<option value="medium" ${discoveryData.priorityFilter === 'medium' ? 'selected' : ''}>Medium</option>
|
||||
<option value="low" ${discoveryData.priorityFilter === 'low' ? 'selected' : ''}>Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="toolbar-search">
|
||||
<i data-lucide="search" class="w-4 h-4"></i>
|
||||
<input type="text" placeholder="${t('common.search') || 'Search...'}"
|
||||
value="${discoveryData.searchQuery}"
|
||||
oninput="searchDiscoveryFindings(this.value)">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Findings Count -->
|
||||
<div class="findings-count">
|
||||
<span>${filteredFindings.length} ${t('discovery.findings') || 'findings'}</span>
|
||||
${discoveryData.selectedFindings.size > 0 ? `
|
||||
<span class="selected-count">(${discoveryData.selectedFindings.size} selected)</span>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Findings List -->
|
||||
<div class="findings-list">
|
||||
${filteredFindings.length === 0 ? `
|
||||
<div class="findings-empty">
|
||||
<i data-lucide="inbox" class="w-8 h-8 text-muted-foreground"></i>
|
||||
<p>${t('discovery.noFindings') || 'No findings match your filters'}</p>
|
||||
</div>
|
||||
` : filteredFindings.map(f => renderFindingItem(f)).join('')}
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
${discoveryData.selectedFindings.size > 0 ? `
|
||||
<div class="bulk-actions">
|
||||
<span class="bulk-count">${discoveryData.selectedFindings.size} selected</span>
|
||||
<button class="bulk-action-btn export" onclick="exportSelectedFindings()">
|
||||
<i data-lucide="upload" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.exportAsIssues') || 'Export as Issues'}</span>
|
||||
</button>
|
||||
<button class="bulk-action-btn dismiss" onclick="dismissSelectedFindings()">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.dismiss') || 'Dismiss'}</span>
|
||||
</button>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Finding Preview -->
|
||||
<div class="discovery-preview-panel">
|
||||
${discoveryData.selectedFinding ? renderFindingPreview(discoveryData.selectedFinding) : `
|
||||
<div class="preview-empty">
|
||||
<i data-lucide="mouse-pointer-click" class="w-12 h-12 text-muted-foreground"></i>
|
||||
<p>${t('discovery.selectFinding') || 'Select a finding to preview'}</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFindingItem(finding) {
|
||||
const isSelected = discoveryData.selectedFindings.has(finding.id);
|
||||
const isActive = discoveryData.selectedFinding?.id === finding.id;
|
||||
|
||||
return `
|
||||
<div class="finding-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${finding.dismissed ? 'dismissed' : ''}"
|
||||
onclick="selectFinding('${finding.id}')">
|
||||
<div class="finding-checkbox" onclick="event.stopPropagation(); toggleFindingSelection('${finding.id}')">
|
||||
<input type="checkbox" ${isSelected ? 'checked' : ''}>
|
||||
</div>
|
||||
<div class="finding-content">
|
||||
<div class="finding-header">
|
||||
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
|
||||
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
|
||||
</div>
|
||||
<div class="finding-title">${finding.title || 'Untitled'}</div>
|
||||
<div class="finding-location">
|
||||
<i data-lucide="file" class="w-3 h-3"></i>
|
||||
<span>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFindingPreview(finding) {
|
||||
return `
|
||||
<div class="finding-preview">
|
||||
<div class="preview-header">
|
||||
<div class="preview-badges">
|
||||
<span class="perspective-badge ${finding.perspective}">${finding.perspective}</span>
|
||||
<span class="priority-badge ${finding.priority}">${finding.priority}</span>
|
||||
${finding.confidence ? `<span class="confidence-badge">${Math.round(finding.confidence * 100)}% confidence</span>` : ''}
|
||||
</div>
|
||||
<h3 class="preview-title">${finding.title || 'Untitled'}</h3>
|
||||
</div>
|
||||
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="file-code" class="w-4 h-4"></i> ${t('discovery.location') || 'Location'}</h4>
|
||||
<div class="preview-location">
|
||||
<code>${finding.file || 'Unknown'}${finding.line ? ':' + finding.line : ''}</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${finding.snippet ? `
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="code" class="w-4 h-4"></i> ${t('discovery.code') || 'Code'}</h4>
|
||||
<pre class="preview-snippet"><code>${escapeHtml(finding.snippet)}</code></pre>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="info" class="w-4 h-4"></i> ${t('discovery.description') || 'Description'}</h4>
|
||||
<p class="preview-description">${finding.description || 'No description'}</p>
|
||||
</div>
|
||||
|
||||
${finding.impact ? `
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="alert-triangle" class="w-4 h-4"></i> ${t('discovery.impact') || 'Impact'}</h4>
|
||||
<p class="preview-impact">${finding.impact}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${finding.recommendation ? `
|
||||
<div class="preview-section">
|
||||
<h4><i data-lucide="lightbulb" class="w-4 h-4"></i> ${t('discovery.recommendation') || 'Recommendation'}</h4>
|
||||
<p class="preview-recommendation">${finding.recommendation}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${finding.suggested_issue ? `
|
||||
<div class="preview-section suggested-issue">
|
||||
<h4><i data-lucide="clipboard-list" class="w-4 h-4"></i> ${t('discovery.suggestedIssue') || 'Suggested Issue'}</h4>
|
||||
<div class="suggested-issue-content">
|
||||
<div class="issue-title">${finding.suggested_issue.title || finding.title}</div>
|
||||
<div class="issue-meta">
|
||||
<span class="issue-type">${finding.suggested_issue.type || 'bug'}</span>
|
||||
<span class="issue-priority">P${finding.suggested_issue.priority || 3}</span>
|
||||
${finding.suggested_issue.labels ? finding.suggested_issue.labels.map(l => `<span class="issue-label">${l}</span>`).join('') : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="preview-actions">
|
||||
<button class="preview-action-btn primary" onclick="exportSingleFinding('${finding.id}')">
|
||||
<i data-lucide="upload" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.exportAsIssue') || 'Export as Issue'}</span>
|
||||
</button>
|
||||
<button class="preview-action-btn secondary" onclick="dismissFinding('${finding.id}')">
|
||||
<i data-lucide="x" class="w-4 h-4"></i>
|
||||
<span>${t('discovery.dismiss') || 'Dismiss'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========== 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';
|
||||
}
|
||||
@@ -418,6 +418,11 @@
|
||||
<span class="nav-text flex-1" data-i18n="nav.issueManager">Manager</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeIssues">0</span>
|
||||
</li>
|
||||
<li class="nav-item flex items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground hover:bg-hover hover:text-foreground rounded cursor-pointer transition-colors" data-view="issue-discovery" data-tooltip="Issue Discovery">
|
||||
<i data-lucide="search-code" class="nav-icon"></i>
|
||||
<span class="nav-text flex-1" data-i18n="nav.issueDiscovery">Discovery</span>
|
||||
<span class="badge px-2 py-0.5 text-xs font-semibold rounded-full bg-hover text-muted-foreground" id="badgeDiscovery">0</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user