129 lines
4.4 KiB
Python
129 lines
4.4 KiB
Python
from dataclasses import dataclass
|
|
|
|
from app.modules.design.domain.entities import (
|
|
Capability,
|
|
Entity,
|
|
Module,
|
|
TraceabilityLink,
|
|
)
|
|
from app.modules.design.domain.value_objects import FileStatus
|
|
|
|
TEMPLATE_MARKERS = ["TODO", "EXAMPLE", "<replace", "{{", "Lorem ipsum"]
|
|
|
|
|
|
@dataclass
|
|
class ConstraintViolation:
|
|
rule: str
|
|
entity_id: str
|
|
message: str
|
|
|
|
|
|
class DesignValidationService:
|
|
@staticmethod
|
|
def determine_file_status(content: str, file_path: str) -> FileStatus:
|
|
if not content or not content.strip():
|
|
return FileStatus.MISSING
|
|
|
|
lines = [l for l in content.strip().splitlines() if l.strip()]
|
|
is_csv = file_path.endswith(".csv")
|
|
|
|
# Sparse check
|
|
if is_csv and len(lines) < 2:
|
|
return FileStatus.SPARSE
|
|
if not is_csv and len(lines) < 5:
|
|
return FileStatus.SPARSE
|
|
|
|
# Count placeholder tokens
|
|
total_cells = 0
|
|
placeholder_cells = 0
|
|
for line in lines:
|
|
cells = line.split(",") if is_csv else [line]
|
|
for cell in cells:
|
|
total_cells += 1
|
|
if any(m.lower() in cell.lower() for m in TEMPLATE_MARKERS):
|
|
placeholder_cells += 1
|
|
|
|
# Placeholder heavy: >50%
|
|
if total_cells > 0 and placeholder_cells / total_cells > 0.5:
|
|
return FileStatus.PLACEHOLDER_HEAVY
|
|
|
|
# Template residue: any marker present but <=50%
|
|
if placeholder_cells > 0:
|
|
return FileStatus.TEMPLATE_RESIDUE
|
|
|
|
return FileStatus.OK
|
|
|
|
@staticmethod
|
|
def check_capability_module_linkage(
|
|
capabilities: list[Capability],
|
|
traceability_links: list[TraceabilityLink],
|
|
) -> list[ConstraintViolation]:
|
|
linked_caps = {link.capability_id for link in traceability_links}
|
|
return [
|
|
ConstraintViolation(
|
|
rule="capability_module_linkage",
|
|
entity_id=cap.capability_id,
|
|
message=f"Capability {cap.capability_id} has no TraceabilityLink to any module",
|
|
)
|
|
for cap in capabilities
|
|
if cap.capability_id not in linked_caps
|
|
]
|
|
|
|
@staticmethod
|
|
def check_entity_owner(entities: list[Entity]) -> list[ConstraintViolation]:
|
|
return [
|
|
ConstraintViolation(
|
|
rule="entity_owner",
|
|
entity_id=ent.entity_id,
|
|
message=f"Entity {ent.entity_id} has no owner module",
|
|
)
|
|
for ent in entities
|
|
if not ent.owner_module
|
|
]
|
|
|
|
@staticmethod
|
|
def check_traceability_references(
|
|
links: list[TraceabilityLink],
|
|
capabilities: list[Capability],
|
|
modules: list[Module],
|
|
entities: list[Entity],
|
|
) -> list[ConstraintViolation]:
|
|
cap_ids = {c.capability_id for c in capabilities}
|
|
mod_ids = {m.module_id for m in modules}
|
|
ent_ids = {e.entity_id for e in entities}
|
|
violations: list[ConstraintViolation] = []
|
|
for link in links:
|
|
if link.capability_id not in cap_ids:
|
|
violations.append(ConstraintViolation(
|
|
"traceability_ref", link.trace_id,
|
|
f"References unknown capability {link.capability_id}",
|
|
))
|
|
if link.module_id not in mod_ids:
|
|
violations.append(ConstraintViolation(
|
|
"traceability_ref", link.trace_id,
|
|
f"References unknown module {link.module_id}",
|
|
))
|
|
for eid in link.entity_ids:
|
|
if eid not in ent_ids:
|
|
violations.append(ConstraintViolation(
|
|
"traceability_ref", link.trace_id,
|
|
f"References unknown entity {eid}",
|
|
))
|
|
return violations
|
|
|
|
@classmethod
|
|
def validate_all(
|
|
cls,
|
|
capabilities: list[Capability],
|
|
modules: list[Module],
|
|
entities: list[Entity],
|
|
traceability_links: list[TraceabilityLink],
|
|
) -> list[ConstraintViolation]:
|
|
violations: list[ConstraintViolation] = []
|
|
violations.extend(cls.check_capability_module_linkage(capabilities, traceability_links))
|
|
violations.extend(cls.check_entity_owner(entities))
|
|
violations.extend(cls.check_traceability_references(
|
|
traceability_links, capabilities, modules, entities,
|
|
))
|
|
return violations
|