We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/tokusumi/wassden-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Test scenario coverage validation rules.
This module contains rules that validate test scenario coverage in tasks documents.
"""
import re
from wassden.language_types import Language
from .blocks import BlockType, DocumentBlock, ListItemBlock, TaskBlock
from .id_extractor import IDExtractor
from .validation_rules import (
BlockLocation,
TraceabilityValidationRule,
ValidationContext,
ValidationError,
ValidationResult,
)
class TestScenarioCoverageRule(TraceabilityValidationRule):
"""Validates that all test scenarios from design are referenced in tasks."""
def __init__(self, language: Language = Language.JAPANESE) -> None:
"""Initialize test scenario coverage rule."""
super().__init__(language)
@property
def rule_id(self) -> str:
"""Unique identifier for this rule."""
return "TRACE-TASKS-003"
@property
def rule_name(self) -> str:
"""Human-readable name for this rule."""
return "Test Scenario Coverage"
@property
def description(self) -> str:
"""Description of what this rule validates."""
return "Validates that all test scenarios from design document are referenced in tasks"
def validate(self, document: DocumentBlock, context: ValidationContext) -> ValidationResult:
"""Validate test scenario coverage.
Args:
document: Tasks document to validate
context: Validation context with design document
Returns:
Validation result with errors for unreferenced test scenarios
"""
errors: list[ValidationError] = []
# Only validate if design document exists
if not context.design_doc:
return self._create_result(errors)
# Extract all test scenarios from design document
design_test_scenarios = self._extract_test_scenarios(context.design_doc)
# Extract referenced design components from tasks
referenced_components = self._extract_referenced_components(document)
# Find unreferenced test scenarios
unreferenced_scenarios = design_test_scenarios - referenced_components
if unreferenced_scenarios:
errors.extend(
ValidationError(
message=f"Test scenario not referenced in tasks: {scenario}",
location=BlockLocation(line_start=1, line_end=1, section_path=[]),
)
for scenario in sorted(unreferenced_scenarios)
)
return self._create_result(errors)
def _extract_test_scenarios(self, design_doc: DocumentBlock) -> set[str]:
"""Extract test scenarios from design document.
Args:
design_doc: Design document
Returns:
Set of test scenario identifiers (e.g., "test-input-processing")
"""
test_scenarios = set()
# Extract from ListItemBlocks in test strategy section
list_item_blocks = design_doc.get_blocks_by_type(BlockType.LIST_ITEM)
for block in list_item_blocks:
if isinstance(block, ListItemBlock) and block.content:
# Try extracting design component references using pattern **component-name**
dc_refs = list(IDExtractor.extract_all_dc_refs(block.content))
test_scenarios.update(dc_refs)
# Also extract test scenario names from format "**test-xxx**: description" or "test-xxx: description"
# This handles cases where markdown bold markers are already stripped
# Pattern: test-xxx (kebab-case identifier starting with "test-")
test_pattern = r"\b(test-[a-z0-9]+(?:-[a-z0-9]+)*)\b"
matches = re.findall(test_pattern, block.content)
test_scenarios.update(matches)
return test_scenarios
def _extract_referenced_components(self, tasks_doc: DocumentBlock) -> set[str]:
"""Extract referenced design components from tasks document.
Args:
tasks_doc: Tasks document
Returns:
Set of referenced component identifiers
"""
referenced: set[str] = set()
# Extract from TaskBlocks
task_blocks = tasks_doc.get_blocks_by_type(BlockType.TASK)
for block in task_blocks:
if isinstance(block, TaskBlock):
# Add formal DC references (DC-01, DC-02, etc.)
if block.design_refs:
referenced.update(block.design_refs)
# Also scan raw content for test scenario references (test-xxx format)
# This handles cases where test scenarios are referenced in task content
if block.raw_content:
test_pattern = r"(test-[a-z0-9]+(?:-[a-z0-9]+)*)"
matches = re.findall(test_pattern, block.raw_content)
referenced.update(matches)
return referenced
class DesignComponentCoverageRule(TraceabilityValidationRule):
"""Validates that all design components are referenced in tasks."""
def __init__(self, language: Language = Language.JAPANESE) -> None:
"""Initialize design component coverage rule."""
super().__init__(language)
@property
def rule_id(self) -> str:
"""Unique identifier for this rule."""
return "TRACE-DESIGN-001"
@property
def rule_name(self) -> str:
"""Human-readable name for this rule."""
return "Design Component Coverage"
@property
def description(self) -> str:
"""Description of what this rule validates."""
return "Validates that all design components are referenced in tasks"
def validate(self, document: DocumentBlock, context: ValidationContext) -> ValidationResult:
"""Validate design component coverage.
Args:
document: Tasks document to validate
context: Validation context with design document
Returns:
Validation result with errors for unreferenced components
"""
errors: list[ValidationError] = []
# Only validate if design document exists
if not context.design_doc:
return self._create_result(errors)
# Extract all design components from design document
design_components = self._extract_design_components(context.design_doc)
# Extract referenced components from tasks (pass design_components for substring matching)
referenced_components = self._extract_referenced_components(document, design_components)
# Find unreferenced components
unreferenced = design_components - referenced_components
if unreferenced:
# Format as single message with all missing components (legacy compatibility)
sorted_unreferenced = sorted(unreferenced)
max_display = 10
display_refs = sorted_unreferenced[:max_display]
suffix = "..." if len(sorted_unreferenced) > max_display else ""
components_str = ", ".join(display_refs) + suffix
errors.append(
ValidationError(
message=f"Design components not referenced in tasks: {components_str}",
location=BlockLocation(line_start=1, line_end=1, section_path=[]),
)
)
return self._create_result(errors)
def _extract_design_components(self, design_doc: DocumentBlock) -> set[str]:
"""Extract design components from design document.
Args:
design_doc: Design document
Returns:
Set of component identifiers
"""
components: set[str] = set()
# Extract from ListItemBlocks
list_item_blocks = design_doc.get_blocks_by_type(BlockType.LIST_ITEM)
for block in list_item_blocks:
if isinstance(block, ListItemBlock) and block.content:
# Pattern 1: **component-name** (with bold markers)
bold_pattern = r"\*\*([a-z][a-z0-9]*(?:[-_][a-z0-9]+)+)\*\*"
bold_matches = re.findall(bold_pattern, block.content)
components.update(m for m in bold_matches if not m.startswith("test-"))
# Pattern 2: component-name: (without bold, at start of line)
# This handles cases where markdown parser already stripped bold markers
plain_pattern = r"^([a-z][a-z0-9]*(?:[-_][a-z0-9]+)+):"
plain_matches = re.findall(plain_pattern, block.content)
components.update(m for m in plain_matches if not m.startswith("test-"))
return components
def _extract_referenced_components(self, tasks_doc: DocumentBlock, design_components: set[str]) -> set[str]:
"""Extract referenced design components from tasks document.
Args:
tasks_doc: Tasks document
design_components: Set of design components to look for
Returns:
Set of referenced component identifiers
"""
referenced: set[str] = set()
# Extract from TaskBlocks
task_blocks = tasks_doc.get_blocks_by_type(BlockType.TASK)
for block in task_blocks:
if isinstance(block, TaskBlock):
# Add from design_refs field
if block.design_refs:
# Filter out test scenarios
referenced.update(ref for ref in block.design_refs if not ref.startswith("test-"))
# Also check if any design component name appears in task content (legacy compatibility)
if block.raw_content or block.task_text:
content = block.raw_content or block.task_text
for component in design_components:
if component in content:
referenced.add(component)
return referenced