mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Add templates for epics, product brief, and requirements PRD
- Introduced a comprehensive template for generating epics and stories, including an index and individual epic files. - Created a product brief template to outline product vision, problem statements, and target users. - Developed a requirements PRD template to structure functional and non-functional requirements, including traceability and prioritization. - Implemented ast-grep processors for JavaScript and TypeScript to extract relationships such as imports and inheritance. - Added corresponding patterns for JavaScript and TypeScript to support relationship extraction. - Established comparison tests to validate the accuracy of relationship extraction between tree-sitter and ast-grep methods.
This commit is contained in:
@@ -0,0 +1,845 @@
|
||||
# Spec Quality Command
|
||||
|
||||
## Purpose
|
||||
5-dimension spec quality check with readiness report generation and quality gate determination.
|
||||
|
||||
## Quality Dimensions
|
||||
|
||||
### 1. Completeness (Weight: 25%)
|
||||
|
||||
```javascript
|
||||
function scoreCompleteness(specDocs) {
|
||||
const requiredSections = {
|
||||
"product-brief": [
|
||||
"Vision Statement",
|
||||
"Problem Statement",
|
||||
"Target Audience",
|
||||
"Success Metrics",
|
||||
"Constraints"
|
||||
],
|
||||
"prd": [
|
||||
"Goals",
|
||||
"Requirements",
|
||||
"User Stories",
|
||||
"Acceptance Criteria",
|
||||
"Non-Functional Requirements"
|
||||
],
|
||||
"architecture": [
|
||||
"System Overview",
|
||||
"Component Design",
|
||||
"Data Models",
|
||||
"API Specifications",
|
||||
"Technology Stack"
|
||||
],
|
||||
"user-stories": [
|
||||
"Story List",
|
||||
"Acceptance Criteria",
|
||||
"Priority",
|
||||
"Estimation"
|
||||
],
|
||||
"implementation-plan": [
|
||||
"Task Breakdown",
|
||||
"Dependencies",
|
||||
"Timeline",
|
||||
"Resource Allocation"
|
||||
],
|
||||
"test-strategy": [
|
||||
"Test Scope",
|
||||
"Test Cases",
|
||||
"Coverage Goals",
|
||||
"Test Environment"
|
||||
]
|
||||
}
|
||||
|
||||
let totalScore = 0
|
||||
let totalWeight = 0
|
||||
const details = []
|
||||
|
||||
for (const doc of specDocs) {
|
||||
const phase = doc.phase
|
||||
const expectedSections = requiredSections[phase] || []
|
||||
|
||||
if (expectedSections.length === 0) continue
|
||||
|
||||
let presentCount = 0
|
||||
let substantialCount = 0
|
||||
|
||||
for (const section of expectedSections) {
|
||||
const sectionRegex = new RegExp(`##\\s+${section}`, "i")
|
||||
const sectionMatch = doc.content.match(sectionRegex)
|
||||
|
||||
if (sectionMatch) {
|
||||
presentCount++
|
||||
|
||||
// Check if section has substantial content (not just header)
|
||||
const sectionIndex = doc.content.indexOf(sectionMatch[0])
|
||||
const nextSectionIndex = doc.content.indexOf("\n##", sectionIndex + 1)
|
||||
const sectionContent = nextSectionIndex > -1
|
||||
? doc.content.substring(sectionIndex, nextSectionIndex)
|
||||
: doc.content.substring(sectionIndex)
|
||||
|
||||
// Substantial = more than 100 chars excluding header
|
||||
const contentWithoutHeader = sectionContent.replace(sectionRegex, "").trim()
|
||||
if (contentWithoutHeader.length > 100) {
|
||||
substantialCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const presentRatio = presentCount / expectedSections.length
|
||||
const substantialRatio = substantialCount / expectedSections.length
|
||||
|
||||
// Score: 50% for presence, 50% for substance
|
||||
const docScore = (presentRatio * 50) + (substantialRatio * 50)
|
||||
|
||||
totalScore += docScore
|
||||
totalWeight += 100
|
||||
|
||||
details.push({
|
||||
phase: phase,
|
||||
score: docScore,
|
||||
present: presentCount,
|
||||
substantial: substantialCount,
|
||||
expected: expectedSections.length,
|
||||
missing: expectedSections.filter(s => !doc.content.match(new RegExp(`##\\s+${s}`, "i")))
|
||||
})
|
||||
}
|
||||
|
||||
const overallScore = totalWeight > 0 ? (totalScore / totalWeight) * 100 : 0
|
||||
|
||||
return {
|
||||
score: overallScore,
|
||||
weight: 25,
|
||||
weighted_score: overallScore * 0.25,
|
||||
details: details
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Consistency (Weight: 20%)
|
||||
|
||||
```javascript
|
||||
function scoreConsistency(specDocs) {
|
||||
const issues = []
|
||||
|
||||
// 1. Terminology consistency
|
||||
const terminologyMap = new Map()
|
||||
|
||||
for (const doc of specDocs) {
|
||||
// Extract key terms (capitalized phrases, technical terms)
|
||||
const terms = doc.content.match(/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g) || []
|
||||
|
||||
terms.forEach(term => {
|
||||
const normalized = term.toLowerCase()
|
||||
if (!terminologyMap.has(normalized)) {
|
||||
terminologyMap.set(normalized, new Set())
|
||||
}
|
||||
terminologyMap.get(normalized).add(term)
|
||||
})
|
||||
}
|
||||
|
||||
// Find inconsistent terminology (same concept, different casing/spelling)
|
||||
terminologyMap.forEach((variants, normalized) => {
|
||||
if (variants.size > 1) {
|
||||
issues.push({
|
||||
type: "terminology",
|
||||
severity: "medium",
|
||||
message: `Inconsistent terminology: ${[...variants].join(", ")}`,
|
||||
suggestion: `Standardize to one variant`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Format consistency
|
||||
const headerStyles = new Map()
|
||||
for (const doc of specDocs) {
|
||||
const headers = doc.content.match(/^#{1,6}\s+.+$/gm) || []
|
||||
headers.forEach(header => {
|
||||
const level = header.match(/^#+/)[0].length
|
||||
const style = header.includes("**") ? "bold" : "plain"
|
||||
const key = `level-${level}`
|
||||
|
||||
if (!headerStyles.has(key)) {
|
||||
headerStyles.set(key, new Set())
|
||||
}
|
||||
headerStyles.get(key).add(style)
|
||||
})
|
||||
}
|
||||
|
||||
headerStyles.forEach((styles, level) => {
|
||||
if (styles.size > 1) {
|
||||
issues.push({
|
||||
type: "format",
|
||||
severity: "low",
|
||||
message: `Inconsistent header style at ${level}: ${[...styles].join(", ")}`,
|
||||
suggestion: "Use consistent header formatting"
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 3. Reference consistency
|
||||
const references = new Map()
|
||||
for (const doc of specDocs) {
|
||||
// Extract references to other documents/sections
|
||||
const refs = doc.content.match(/\[.*?\]\(.*?\)/g) || []
|
||||
refs.forEach(ref => {
|
||||
const linkMatch = ref.match(/\((.*?)\)/)
|
||||
if (linkMatch) {
|
||||
const link = linkMatch[1]
|
||||
if (!references.has(link)) {
|
||||
references.set(link, [])
|
||||
}
|
||||
references.get(link).push(doc.phase)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Check for broken references
|
||||
references.forEach((sources, link) => {
|
||||
if (link.startsWith("./") || link.startsWith("../")) {
|
||||
// Check if file exists
|
||||
const exists = Bash(`test -f ${link}`).exitCode === 0
|
||||
if (!exists) {
|
||||
issues.push({
|
||||
type: "reference",
|
||||
severity: "high",
|
||||
message: `Broken reference: ${link} (referenced in ${sources.join(", ")})`,
|
||||
suggestion: "Fix or remove broken reference"
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 4. Naming convention consistency
|
||||
const namingPatterns = {
|
||||
camelCase: /\b[a-z]+(?:[A-Z][a-z]+)+\b/g,
|
||||
PascalCase: /\b[A-Z][a-z]+(?:[A-Z][a-z]+)+\b/g,
|
||||
snake_case: /\b[a-z]+(?:_[a-z]+)+\b/g,
|
||||
kebab_case: /\b[a-z]+(?:-[a-z]+)+\b/g
|
||||
}
|
||||
|
||||
const namingCounts = {}
|
||||
for (const doc of specDocs) {
|
||||
Object.entries(namingPatterns).forEach(([pattern, regex]) => {
|
||||
const matches = doc.content.match(regex) || []
|
||||
namingCounts[pattern] = (namingCounts[pattern] || 0) + matches.length
|
||||
})
|
||||
}
|
||||
|
||||
const dominantPattern = Object.entries(namingCounts)
|
||||
.sort((a, b) => b[1] - a[1])[0]?.[0]
|
||||
|
||||
Object.entries(namingCounts).forEach(([pattern, count]) => {
|
||||
if (pattern !== dominantPattern && count > 10) {
|
||||
issues.push({
|
||||
type: "naming",
|
||||
severity: "low",
|
||||
message: `Mixed naming conventions: ${pattern} (${count} occurrences) vs ${dominantPattern}`,
|
||||
suggestion: `Standardize to ${dominantPattern}`
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate score based on issues
|
||||
const severityWeights = { high: 10, medium: 5, low: 2 }
|
||||
const totalPenalty = issues.reduce((sum, issue) => sum + severityWeights[issue.severity], 0)
|
||||
const maxPenalty = 100 // Arbitrary max for normalization
|
||||
|
||||
const score = Math.max(0, 100 - (totalPenalty / maxPenalty) * 100)
|
||||
|
||||
return {
|
||||
score: score,
|
||||
weight: 20,
|
||||
weighted_score: score * 0.20,
|
||||
issues: issues,
|
||||
details: {
|
||||
terminology_issues: issues.filter(i => i.type === "terminology").length,
|
||||
format_issues: issues.filter(i => i.type === "format").length,
|
||||
reference_issues: issues.filter(i => i.type === "reference").length,
|
||||
naming_issues: issues.filter(i => i.type === "naming").length
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Traceability (Weight: 25%)
|
||||
|
||||
```javascript
|
||||
function scoreTraceability(specDocs) {
|
||||
const chains = []
|
||||
|
||||
// Extract traceability elements
|
||||
const goals = extractElements(specDocs, "product-brief", /^[-*]\s+Goal:\s*(.+)$/gm)
|
||||
const requirements = extractElements(specDocs, "prd", /^[-*]\s+(?:REQ-\d+|Requirement):\s*(.+)$/gm)
|
||||
const components = extractElements(specDocs, "architecture", /^[-*]\s+(?:Component|Module):\s*(.+)$/gm)
|
||||
const stories = extractElements(specDocs, "user-stories", /^[-*]\s+(?:US-\d+|Story):\s*(.+)$/gm)
|
||||
|
||||
// Build traceability chains: Goals → Requirements → Components → Stories
|
||||
for (const goal of goals) {
|
||||
const chain = {
|
||||
goal: goal.text,
|
||||
requirements: [],
|
||||
components: [],
|
||||
stories: [],
|
||||
complete: false
|
||||
}
|
||||
|
||||
// Find requirements that reference this goal
|
||||
const goalKeywords = extractKeywords(goal.text)
|
||||
for (const req of requirements) {
|
||||
if (hasKeywordOverlap(req.text, goalKeywords, 0.3)) {
|
||||
chain.requirements.push(req.text)
|
||||
|
||||
// Find components that implement this requirement
|
||||
const reqKeywords = extractKeywords(req.text)
|
||||
for (const comp of components) {
|
||||
if (hasKeywordOverlap(comp.text, reqKeywords, 0.3)) {
|
||||
chain.components.push(comp.text)
|
||||
}
|
||||
}
|
||||
|
||||
// Find stories that implement this requirement
|
||||
for (const story of stories) {
|
||||
if (hasKeywordOverlap(story.text, reqKeywords, 0.3)) {
|
||||
chain.stories.push(story.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if chain is complete
|
||||
chain.complete = chain.requirements.length > 0 &&
|
||||
chain.components.length > 0 &&
|
||||
chain.stories.length > 0
|
||||
|
||||
chains.push(chain)
|
||||
}
|
||||
|
||||
// Calculate score
|
||||
const completeChains = chains.filter(c => c.complete).length
|
||||
const totalChains = chains.length
|
||||
const score = totalChains > 0 ? (completeChains / totalChains) * 100 : 0
|
||||
|
||||
// Identify weak links
|
||||
const weakLinks = []
|
||||
chains.forEach((chain, idx) => {
|
||||
if (!chain.complete) {
|
||||
if (chain.requirements.length === 0) {
|
||||
weakLinks.push(`Goal ${idx + 1} has no linked requirements`)
|
||||
}
|
||||
if (chain.components.length === 0) {
|
||||
weakLinks.push(`Goal ${idx + 1} has no linked components`)
|
||||
}
|
||||
if (chain.stories.length === 0) {
|
||||
weakLinks.push(`Goal ${idx + 1} has no linked stories`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
score: score,
|
||||
weight: 25,
|
||||
weighted_score: score * 0.25,
|
||||
details: {
|
||||
total_chains: totalChains,
|
||||
complete_chains: completeChains,
|
||||
weak_links: weakLinks
|
||||
},
|
||||
chains: chains
|
||||
}
|
||||
}
|
||||
|
||||
function extractElements(specDocs, phase, regex) {
|
||||
const elements = []
|
||||
const doc = specDocs.find(d => d.phase === phase)
|
||||
|
||||
if (doc) {
|
||||
let match
|
||||
while ((match = regex.exec(doc.content)) !== null) {
|
||||
elements.push({
|
||||
text: match[1].trim(),
|
||||
phase: phase
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
function extractKeywords(text) {
|
||||
// Extract meaningful words (4+ chars, not common words)
|
||||
const commonWords = new Set(["that", "this", "with", "from", "have", "will", "should", "must", "can"])
|
||||
const words = text.toLowerCase().match(/\b\w{4,}\b/g) || []
|
||||
return words.filter(w => !commonWords.has(w))
|
||||
}
|
||||
|
||||
function hasKeywordOverlap(text, keywords, threshold) {
|
||||
const textLower = text.toLowerCase()
|
||||
const matchCount = keywords.filter(kw => textLower.includes(kw)).length
|
||||
return matchCount / keywords.length >= threshold
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Depth (Weight: 20%)
|
||||
|
||||
```javascript
|
||||
function scoreDepth(specDocs) {
|
||||
const dimensions = []
|
||||
|
||||
// 1. Acceptance Criteria Testability
|
||||
const acDoc = specDocs.find(d => d.phase === "prd" || d.phase === "user-stories")
|
||||
if (acDoc) {
|
||||
const acMatches = acDoc.content.match(/Acceptance Criteria:[\s\S]*?(?=\n##|\n\n[-*]|$)/gi) || []
|
||||
let testableCount = 0
|
||||
let totalCount = 0
|
||||
|
||||
acMatches.forEach(section => {
|
||||
const criteria = section.match(/^[-*]\s+(.+)$/gm) || []
|
||||
totalCount += criteria.length
|
||||
|
||||
criteria.forEach(criterion => {
|
||||
// Testable if contains measurable verbs or specific conditions
|
||||
const testablePatterns = [
|
||||
/\b(should|must|will)\s+(display|show|return|validate|check|verify|calculate|send|receive)\b/i,
|
||||
/\b(when|if|given)\b.*\b(then|should|must)\b/i,
|
||||
/\b\d+\b/, // Contains numbers (measurable)
|
||||
/\b(success|error|fail|pass)\b/i
|
||||
]
|
||||
|
||||
const isTestable = testablePatterns.some(pattern => pattern.test(criterion))
|
||||
if (isTestable) testableCount++
|
||||
})
|
||||
})
|
||||
|
||||
const acScore = totalCount > 0 ? (testableCount / totalCount) * 100 : 0
|
||||
dimensions.push({
|
||||
name: "Acceptance Criteria Testability",
|
||||
score: acScore,
|
||||
testable: testableCount,
|
||||
total: totalCount
|
||||
})
|
||||
}
|
||||
|
||||
// 2. ADR Justification
|
||||
const archDoc = specDocs.find(d => d.phase === "architecture")
|
||||
if (archDoc) {
|
||||
const adrMatches = archDoc.content.match(/##\s+(?:ADR|Decision)[\s\S]*?(?=\n##|$)/gi) || []
|
||||
let justifiedCount = 0
|
||||
let totalCount = adrMatches.length
|
||||
|
||||
adrMatches.forEach(adr => {
|
||||
// Justified if contains rationale, alternatives, or consequences
|
||||
const hasJustification = adr.match(/\b(rationale|reason|because|alternative|consequence|trade-?off)\b/i)
|
||||
if (hasJustification) justifiedCount++
|
||||
})
|
||||
|
||||
const adrScore = totalCount > 0 ? (justifiedCount / totalCount) * 100 : 100 // Default 100 if no ADRs
|
||||
dimensions.push({
|
||||
name: "ADR Justification",
|
||||
score: adrScore,
|
||||
justified: justifiedCount,
|
||||
total: totalCount
|
||||
})
|
||||
}
|
||||
|
||||
// 3. User Stories Estimability
|
||||
const storiesDoc = specDocs.find(d => d.phase === "user-stories")
|
||||
if (storiesDoc) {
|
||||
const storyMatches = storiesDoc.content.match(/^[-*]\s+(?:US-\d+|Story)[\s\S]*?(?=\n[-*]|$)/gim) || []
|
||||
let estimableCount = 0
|
||||
let totalCount = storyMatches.length
|
||||
|
||||
storyMatches.forEach(story => {
|
||||
// Estimable if has clear scope, AC, and no ambiguity
|
||||
const hasScope = story.match(/\b(as a|I want|so that)\b/i)
|
||||
const hasAC = story.match(/acceptance criteria/i)
|
||||
const hasEstimate = story.match(/\b(points?|hours?|days?|estimate)\b/i)
|
||||
|
||||
if ((hasScope && hasAC) || hasEstimate) estimableCount++
|
||||
})
|
||||
|
||||
const storiesScore = totalCount > 0 ? (estimableCount / totalCount) * 100 : 0
|
||||
dimensions.push({
|
||||
name: "User Stories Estimability",
|
||||
score: storiesScore,
|
||||
estimable: estimableCount,
|
||||
total: totalCount
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Technical Detail Sufficiency
|
||||
const techDocs = specDocs.filter(d => d.phase === "architecture" || d.phase === "implementation-plan")
|
||||
let detailScore = 0
|
||||
|
||||
if (techDocs.length > 0) {
|
||||
const detailIndicators = [
|
||||
/```[\s\S]*?```/, // Code blocks
|
||||
/\b(API|endpoint|schema|model|interface|class|function)\b/i,
|
||||
/\b(GET|POST|PUT|DELETE|PATCH)\b/, // HTTP methods
|
||||
/\b(database|table|collection|index)\b/i,
|
||||
/\b(authentication|authorization|security)\b/i
|
||||
]
|
||||
|
||||
let indicatorCount = 0
|
||||
techDocs.forEach(doc => {
|
||||
detailIndicators.forEach(pattern => {
|
||||
if (pattern.test(doc.content)) indicatorCount++
|
||||
})
|
||||
})
|
||||
|
||||
detailScore = Math.min(100, (indicatorCount / (detailIndicators.length * techDocs.length)) * 100)
|
||||
dimensions.push({
|
||||
name: "Technical Detail Sufficiency",
|
||||
score: detailScore,
|
||||
indicators_found: indicatorCount,
|
||||
indicators_expected: detailIndicators.length * techDocs.length
|
||||
})
|
||||
}
|
||||
|
||||
// Calculate overall depth score
|
||||
const overallScore = dimensions.reduce((sum, d) => sum + d.score, 0) / dimensions.length
|
||||
|
||||
return {
|
||||
score: overallScore,
|
||||
weight: 20,
|
||||
weighted_score: overallScore * 0.20,
|
||||
dimensions: dimensions
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Requirement Coverage (Weight: 10%)
|
||||
|
||||
```javascript
|
||||
function scoreRequirementCoverage(specDocs, originalRequirements) {
|
||||
// Extract original requirements from task description or initial brief
|
||||
const originalReqs = originalRequirements || extractOriginalRequirements(specDocs)
|
||||
|
||||
if (originalReqs.length === 0) {
|
||||
return {
|
||||
score: 100, // No requirements to cover
|
||||
weight: 10,
|
||||
weighted_score: 10,
|
||||
details: {
|
||||
total: 0,
|
||||
covered: 0,
|
||||
uncovered: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract all requirements from spec documents
|
||||
const specReqs = []
|
||||
for (const doc of specDocs) {
|
||||
const reqMatches = doc.content.match(/^[-*]\s+(?:REQ-\d+|Requirement|Feature):\s*(.+)$/gm) || []
|
||||
reqMatches.forEach(match => {
|
||||
specReqs.push(match.replace(/^[-*]\s+(?:REQ-\d+|Requirement|Feature):\s*/, "").trim())
|
||||
})
|
||||
}
|
||||
|
||||
// Map original requirements to spec requirements
|
||||
const coverage = []
|
||||
for (const origReq of originalReqs) {
|
||||
const keywords = extractKeywords(origReq)
|
||||
const covered = specReqs.some(specReq => hasKeywordOverlap(specReq, keywords, 0.4))
|
||||
|
||||
coverage.push({
|
||||
requirement: origReq,
|
||||
covered: covered
|
||||
})
|
||||
}
|
||||
|
||||
const coveredCount = coverage.filter(c => c.covered).length
|
||||
const score = (coveredCount / originalReqs.length) * 100
|
||||
|
||||
return {
|
||||
score: score,
|
||||
weight: 10,
|
||||
weighted_score: score * 0.10,
|
||||
details: {
|
||||
total: originalReqs.length,
|
||||
covered: coveredCount,
|
||||
uncovered: coverage.filter(c => !c.covered).map(c => c.requirement)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractOriginalRequirements(specDocs) {
|
||||
// Try to find original requirements in product brief
|
||||
const briefDoc = specDocs.find(d => d.phase === "product-brief")
|
||||
if (!briefDoc) return []
|
||||
|
||||
const reqSection = briefDoc.content.match(/##\s+(?:Requirements|Objectives)[\s\S]*?(?=\n##|$)/i)
|
||||
if (!reqSection) return []
|
||||
|
||||
const reqs = reqSection[0].match(/^[-*]\s+(.+)$/gm) || []
|
||||
return reqs.map(r => r.replace(/^[-*]\s+/, "").trim())
|
||||
}
|
||||
```
|
||||
|
||||
## Quality Gate Determination
|
||||
|
||||
```javascript
|
||||
function determineQualityGate(overallScore, coverageScore) {
|
||||
// PASS: Score ≥80% AND coverage ≥70%
|
||||
if (overallScore >= 80 && coverageScore >= 70) {
|
||||
return {
|
||||
gate: "PASS",
|
||||
message: "Specification meets quality standards and is ready for implementation",
|
||||
action: "Proceed to implementation phase"
|
||||
}
|
||||
}
|
||||
|
||||
// FAIL: Score <60% OR coverage <50%
|
||||
if (overallScore < 60 || coverageScore < 50) {
|
||||
return {
|
||||
gate: "FAIL",
|
||||
message: "Specification requires major revisions before implementation",
|
||||
action: "Address critical gaps and resubmit for review"
|
||||
}
|
||||
}
|
||||
|
||||
// REVIEW: Between PASS and FAIL
|
||||
return {
|
||||
gate: "REVIEW",
|
||||
message: "Specification needs improvements but may proceed with caution",
|
||||
action: "Address recommendations and consider re-review"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Readiness Report Generation
|
||||
|
||||
```javascript
|
||||
function formatReadinessReport(report, specDocs) {
|
||||
const { overall_score, quality_gate, dimensions, phase_gates } = report
|
||||
|
||||
let markdown = `# Specification Readiness Report\n\n`
|
||||
markdown += `**Generated**: ${new Date().toISOString()}\n\n`
|
||||
markdown += `**Overall Score**: ${overall_score.toFixed(1)}%\n\n`
|
||||
markdown += `**Quality Gate**: ${quality_gate.gate} - ${quality_gate.message}\n\n`
|
||||
markdown += `**Recommended Action**: ${quality_gate.action}\n\n`
|
||||
|
||||
markdown += `---\n\n`
|
||||
|
||||
markdown += `## Dimension Scores\n\n`
|
||||
markdown += `| Dimension | Score | Weight | Weighted Score |\n`
|
||||
markdown += `|-----------|-------|--------|----------------|\n`
|
||||
|
||||
Object.entries(dimensions).forEach(([name, data]) => {
|
||||
markdown += `| ${name} | ${data.score.toFixed(1)}% | ${data.weight}% | ${data.weighted_score.toFixed(1)}% |\n`
|
||||
})
|
||||
|
||||
markdown += `\n---\n\n`
|
||||
|
||||
// Completeness Details
|
||||
markdown += `## Completeness Analysis\n\n`
|
||||
dimensions.completeness.details.forEach(detail => {
|
||||
markdown += `### ${detail.phase}\n`
|
||||
markdown += `- Score: ${detail.score.toFixed(1)}%\n`
|
||||
markdown += `- Sections Present: ${detail.present}/${detail.expected}\n`
|
||||
markdown += `- Substantial Content: ${detail.substantial}/${detail.expected}\n`
|
||||
if (detail.missing.length > 0) {
|
||||
markdown += `- Missing: ${detail.missing.join(", ")}\n`
|
||||
}
|
||||
markdown += `\n`
|
||||
})
|
||||
|
||||
// Consistency Details
|
||||
markdown += `## Consistency Analysis\n\n`
|
||||
if (dimensions.consistency.issues.length > 0) {
|
||||
markdown += `**Issues Found**: ${dimensions.consistency.issues.length}\n\n`
|
||||
dimensions.consistency.issues.forEach(issue => {
|
||||
markdown += `- **${issue.severity.toUpperCase()}**: ${issue.message}\n`
|
||||
markdown += ` *Suggestion*: ${issue.suggestion}\n\n`
|
||||
})
|
||||
} else {
|
||||
markdown += `No consistency issues found.\n\n`
|
||||
}
|
||||
|
||||
// Traceability Details
|
||||
markdown += `## Traceability Analysis\n\n`
|
||||
markdown += `- Complete Chains: ${dimensions.traceability.details.complete_chains}/${dimensions.traceability.details.total_chains}\n\n`
|
||||
if (dimensions.traceability.details.weak_links.length > 0) {
|
||||
markdown += `**Weak Links**:\n`
|
||||
dimensions.traceability.details.weak_links.forEach(link => {
|
||||
markdown += `- ${link}\n`
|
||||
})
|
||||
markdown += `\n`
|
||||
}
|
||||
|
||||
// Depth Details
|
||||
markdown += `## Depth Analysis\n\n`
|
||||
dimensions.depth.dimensions.forEach(dim => {
|
||||
markdown += `### ${dim.name}\n`
|
||||
markdown += `- Score: ${dim.score.toFixed(1)}%\n`
|
||||
if (dim.testable !== undefined) {
|
||||
markdown += `- Testable: ${dim.testable}/${dim.total}\n`
|
||||
}
|
||||
if (dim.justified !== undefined) {
|
||||
markdown += `- Justified: ${dim.justified}/${dim.total}\n`
|
||||
}
|
||||
if (dim.estimable !== undefined) {
|
||||
markdown += `- Estimable: ${dim.estimable}/${dim.total}\n`
|
||||
}
|
||||
markdown += `\n`
|
||||
})
|
||||
|
||||
// Coverage Details
|
||||
markdown += `## Requirement Coverage\n\n`
|
||||
markdown += `- Covered: ${dimensions.coverage.details.covered}/${dimensions.coverage.details.total}\n`
|
||||
if (dimensions.coverage.details.uncovered.length > 0) {
|
||||
markdown += `\n**Uncovered Requirements**:\n`
|
||||
dimensions.coverage.details.uncovered.forEach(req => {
|
||||
markdown += `- ${req}\n`
|
||||
})
|
||||
}
|
||||
markdown += `\n`
|
||||
|
||||
// Phase Gates
|
||||
if (phase_gates) {
|
||||
markdown += `---\n\n`
|
||||
markdown += `## Phase-Level Quality Gates\n\n`
|
||||
Object.entries(phase_gates).forEach(([phase, gate]) => {
|
||||
markdown += `### ${phase}\n`
|
||||
markdown += `- Gate: ${gate.status}\n`
|
||||
markdown += `- Score: ${gate.score.toFixed(1)}%\n`
|
||||
if (gate.issues.length > 0) {
|
||||
markdown += `- Issues: ${gate.issues.join(", ")}\n`
|
||||
}
|
||||
markdown += `\n`
|
||||
})
|
||||
}
|
||||
|
||||
return markdown
|
||||
}
|
||||
```
|
||||
|
||||
## Spec Summary Generation
|
||||
|
||||
```javascript
|
||||
function formatSpecSummary(specDocs, report) {
|
||||
let markdown = `# Specification Summary\n\n`
|
||||
|
||||
markdown += `**Overall Quality Score**: ${report.overall_score.toFixed(1)}%\n`
|
||||
markdown += `**Quality Gate**: ${report.quality_gate.gate}\n\n`
|
||||
|
||||
markdown += `---\n\n`
|
||||
|
||||
// Document Overview
|
||||
markdown += `## Documents Reviewed\n\n`
|
||||
specDocs.forEach(doc => {
|
||||
markdown += `### ${doc.phase}\n`
|
||||
markdown += `- Path: ${doc.path}\n`
|
||||
markdown += `- Size: ${doc.content.length} characters\n`
|
||||
|
||||
// Extract key sections
|
||||
const sections = doc.content.match(/^##\s+(.+)$/gm) || []
|
||||
if (sections.length > 0) {
|
||||
markdown += `- Sections: ${sections.map(s => s.replace(/^##\s+/, "")).join(", ")}\n`
|
||||
}
|
||||
markdown += `\n`
|
||||
})
|
||||
|
||||
markdown += `---\n\n`
|
||||
|
||||
// Key Findings
|
||||
markdown += `## Key Findings\n\n`
|
||||
|
||||
// Strengths
|
||||
const strengths = []
|
||||
Object.entries(report.dimensions).forEach(([name, data]) => {
|
||||
if (data.score >= 80) {
|
||||
strengths.push(`${name}: ${data.score.toFixed(1)}%`)
|
||||
}
|
||||
})
|
||||
|
||||
if (strengths.length > 0) {
|
||||
markdown += `### Strengths\n`
|
||||
strengths.forEach(s => markdown += `- ${s}\n`)
|
||||
markdown += `\n`
|
||||
}
|
||||
|
||||
// Areas for Improvement
|
||||
const improvements = []
|
||||
Object.entries(report.dimensions).forEach(([name, data]) => {
|
||||
if (data.score < 70) {
|
||||
improvements.push(`${name}: ${data.score.toFixed(1)}%`)
|
||||
}
|
||||
})
|
||||
|
||||
if (improvements.length > 0) {
|
||||
markdown += `### Areas for Improvement\n`
|
||||
improvements.forEach(i => markdown += `- ${i}\n`)
|
||||
markdown += `\n`
|
||||
}
|
||||
|
||||
// Recommendations
|
||||
if (report.recommendations && report.recommendations.length > 0) {
|
||||
markdown += `### Recommendations\n`
|
||||
report.recommendations.forEach((rec, i) => {
|
||||
markdown += `${i + 1}. ${rec}\n`
|
||||
})
|
||||
markdown += `\n`
|
||||
}
|
||||
|
||||
return markdown
|
||||
}
|
||||
```
|
||||
|
||||
## Phase-Level Quality Gates
|
||||
|
||||
```javascript
|
||||
function calculatePhaseGates(specDocs) {
|
||||
const gates = {}
|
||||
|
||||
for (const doc of specDocs) {
|
||||
const phase = doc.phase
|
||||
const issues = []
|
||||
let score = 100
|
||||
|
||||
// Check minimum content threshold
|
||||
if (doc.content.length < 500) {
|
||||
issues.push("Insufficient content")
|
||||
score -= 30
|
||||
}
|
||||
|
||||
// Check for required sections (phase-specific)
|
||||
const requiredSections = getRequiredSections(phase)
|
||||
const missingSections = requiredSections.filter(section =>
|
||||
!doc.content.match(new RegExp(`##\\s+${section}`, "i"))
|
||||
)
|
||||
|
||||
if (missingSections.length > 0) {
|
||||
issues.push(`Missing sections: ${missingSections.join(", ")}`)
|
||||
score -= missingSections.length * 15
|
||||
}
|
||||
|
||||
// Determine gate status
|
||||
let status = "PASS"
|
||||
if (score < 60) status = "FAIL"
|
||||
else if (score < 80) status = "REVIEW"
|
||||
|
||||
gates[phase] = {
|
||||
status: status,
|
||||
score: Math.max(0, score),
|
||||
issues: issues
|
||||
}
|
||||
}
|
||||
|
||||
return gates
|
||||
}
|
||||
|
||||
function getRequiredSections(phase) {
|
||||
const sectionMap = {
|
||||
"product-brief": ["Vision", "Problem", "Target Audience"],
|
||||
"prd": ["Goals", "Requirements", "User Stories"],
|
||||
"architecture": ["Overview", "Components", "Data Models"],
|
||||
"user-stories": ["Stories", "Acceptance Criteria"],
|
||||
"implementation-plan": ["Tasks", "Dependencies"],
|
||||
"test-strategy": ["Test Cases", "Coverage"]
|
||||
}
|
||||
|
||||
return sectionMap[phase] || []
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user