55 lines
2.0 KiB
Python
55 lines
2.0 KiB
Python
from .models import RequirementNode, NodeType
|
|
from .schemas import QualityReport
|
|
|
|
|
|
def validate_requirement(node: RequirementNode, children: list[RequirementNode]) -> QualityReport:
|
|
"""Run quality gates on a requirement node.
|
|
|
|
Quality gates:
|
|
1. Traceable — auto-calculated: has parent (non-pillar) + description
|
|
2. Measurable — user toggle (manually set by the user)
|
|
3. Consistent — user toggle (manually set by the user)
|
|
4. Single-purpose — user toggle (manually set by the user)
|
|
"""
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
|
|
# 1. Traceable — auto-calculated
|
|
traceable = True
|
|
if node.node_type != NodeType.pillar:
|
|
if not node.parent_id:
|
|
errors.append("Non-pillar node must have a parent (traceability)")
|
|
traceable = False
|
|
if not node.description:
|
|
warnings.append("Missing description — hard to trace how this serves parent")
|
|
else:
|
|
if not node.description:
|
|
warnings.append("Pillar should have a description explaining its purpose")
|
|
|
|
# 2. Measurable — user toggle, but forced false without test_spec
|
|
measurable = bool(node.measurable)
|
|
if not node.acceptance_criteria:
|
|
warnings.append("Missing acceptance criteria")
|
|
if not node.test_spec:
|
|
errors.append("Missing test specification (GIVEN/WHEN/THEN) — cannot be measurable")
|
|
measurable = False
|
|
|
|
# 3. Consistent — user toggle
|
|
consistent = bool(node.consistent)
|
|
|
|
# 4. Single-purpose — user toggle, but warn on heuristic
|
|
single_purpose = bool(node.single_purpose)
|
|
title_lower = node.title.lower()
|
|
if " and " in title_lower and " and " not in title_lower.split("(")[0]:
|
|
warnings.append("Title contains 'and' — may serve multiple purposes")
|
|
|
|
return QualityReport(
|
|
traceable=traceable,
|
|
measurable=measurable,
|
|
consistent=consistent,
|
|
single_purpose=single_purpose,
|
|
errors=errors,
|
|
warnings=warnings,
|
|
valid=len(errors) == 0,
|
|
)
|