forked from Rockachopa/Timmy-time-dashboard
## FastAPI Morrowind Harness (#821) - GET /api/v1/morrowind/perception — reads perception.json, validates against PerceptionOutput schema - POST /api/v1/morrowind/command — validates CommandInput, logs via CommandLogger, stubs bridge forwarding - GET /api/v1/morrowind/status — connection state, last perception, queue depth, agent vitals - Router registered in dashboard app alongside existing routers - 12 tests with FastAPI TestClient ## SOUL.md Framework (#854) - docs/soul-framework/ — template, authoring guide, role extensions - src/infrastructure/soul/loader.py — parse SOUL.md into structured SoulDocument objects with section extraction and merge support - src/infrastructure/soul/validator.py — validate structure, detect contradictions between values/constraints - src/infrastructure/soul/versioning.py — hash-based version tracking with JSON persistence - 27 tests covering loader, validator, versioning, and Timmy's soul Builds on PR #864 (protocol spec + command log). Closes #821 Closes #854
227 lines
7.4 KiB
Python
227 lines
7.4 KiB
Python
"""Validate SOUL.md structure and check for contradictions.
|
|
|
|
Usage::
|
|
|
|
from infrastructure.soul.loader import SoulLoader
|
|
from infrastructure.soul.validator import SoulValidator
|
|
|
|
soul = SoulLoader.from_file("memory/self/soul.md")
|
|
issues = SoulValidator.validate(soul)
|
|
for issue in issues:
|
|
print(f"[{issue.severity}] {issue.section}: {issue.message}")
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
from .loader import SoulDocument
|
|
|
|
|
|
class Severity(StrEnum):
|
|
"""Validation issue severity."""
|
|
|
|
ERROR = "error"
|
|
WARNING = "warning"
|
|
|
|
|
|
@dataclass
|
|
class ValidationIssue:
|
|
"""A single validation finding."""
|
|
|
|
severity: Severity
|
|
section: str
|
|
message: str
|
|
|
|
|
|
# Pairs of words that indicate potential contradiction when both appear
|
|
# in values or constraints.
|
|
_CONTRADICTION_PAIRS: list[tuple[str, str]] = [
|
|
("obey", "disobey"),
|
|
("always", "never"),
|
|
("silence", "verbose"),
|
|
("hide", "transparent"),
|
|
("deceive", "honest"),
|
|
("refuse", "comply"),
|
|
]
|
|
|
|
|
|
class SoulValidator:
|
|
"""Static validator for :class:`SoulDocument` instances."""
|
|
|
|
@classmethod
|
|
def validate(
|
|
cls,
|
|
soul: SoulDocument,
|
|
*,
|
|
base_soul: SoulDocument | None = None,
|
|
) -> list[ValidationIssue]:
|
|
"""Run all validation checks.
|
|
|
|
Returns a list of :class:`ValidationIssue` items. An empty list
|
|
means the document passed all checks.
|
|
|
|
If *base_soul* is provided, additional checks verify that the
|
|
extension does not contradict the base constraints.
|
|
"""
|
|
issues: list[ValidationIssue] = []
|
|
|
|
if not soul.is_extension:
|
|
issues.extend(cls._check_required_sections(soul))
|
|
|
|
issues.extend(cls._check_values(soul))
|
|
issues.extend(cls._check_constraints(soul))
|
|
issues.extend(cls._check_prime_directive(soul))
|
|
issues.extend(cls._check_contradictions(soul))
|
|
|
|
if base_soul is not None:
|
|
issues.extend(cls._check_extension_compatibility(soul, base_soul))
|
|
|
|
return issues
|
|
|
|
# -- Individual checks ---------------------------------------------------
|
|
|
|
@classmethod
|
|
def _check_required_sections(cls, soul: SoulDocument) -> list[ValidationIssue]:
|
|
"""Verify all five required sections are present."""
|
|
issues: list[ValidationIssue] = []
|
|
if not soul.identity:
|
|
issues.append(
|
|
ValidationIssue(Severity.ERROR, "identity", "Identity section is missing or empty")
|
|
)
|
|
if not soul.values:
|
|
issues.append(
|
|
ValidationIssue(Severity.ERROR, "values", "Values section is missing or has no entries")
|
|
)
|
|
if not soul.prime_directive:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.ERROR,
|
|
"prime_directive",
|
|
"Prime Directive section is missing or empty",
|
|
)
|
|
)
|
|
if not soul.audience_awareness:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.WARNING,
|
|
"audience_awareness",
|
|
"Audience Awareness section is missing or empty",
|
|
)
|
|
)
|
|
if not soul.constraints:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.ERROR,
|
|
"constraints",
|
|
"Constraints section is missing or has no entries",
|
|
)
|
|
)
|
|
return issues
|
|
|
|
@classmethod
|
|
def _check_values(cls, soul: SoulDocument) -> list[ValidationIssue]:
|
|
"""Check values section quality."""
|
|
issues: list[ValidationIssue] = []
|
|
if len(soul.values) > 10:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.WARNING,
|
|
"values",
|
|
f"Too many values ({len(soul.values)}). Consider trimming to 3-7.",
|
|
)
|
|
)
|
|
return issues
|
|
|
|
@classmethod
|
|
def _check_constraints(cls, soul: SoulDocument) -> list[ValidationIssue]:
|
|
"""Check constraints section quality."""
|
|
issues: list[ValidationIssue] = []
|
|
if len(soul.constraints) > 10:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.WARNING,
|
|
"constraints",
|
|
f"Too many constraints ({len(soul.constraints)}). "
|
|
"Consider consolidating to avoid paralysis.",
|
|
)
|
|
)
|
|
return issues
|
|
|
|
@classmethod
|
|
def _check_prime_directive(cls, soul: SoulDocument) -> list[ValidationIssue]:
|
|
"""Prime directive should be a single sentence."""
|
|
issues: list[ValidationIssue] = []
|
|
if not soul.prime_directive:
|
|
return issues
|
|
|
|
# Check for multiple sentences (crude: look for sentence-ending punctuation
|
|
# followed by a capital letter).
|
|
sentences = re.split(r"[.!?]\s+(?=[A-Z])", soul.prime_directive)
|
|
if len(sentences) > 1:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.WARNING,
|
|
"prime_directive",
|
|
"Prime directive appears to contain multiple sentences. "
|
|
"Consider reducing to exactly one.",
|
|
)
|
|
)
|
|
return issues
|
|
|
|
@classmethod
|
|
def _check_contradictions(cls, soul: SoulDocument) -> list[ValidationIssue]:
|
|
"""Detect potential contradictions within values and constraints."""
|
|
issues: list[ValidationIssue] = []
|
|
|
|
# Collect all text from values and constraints.
|
|
all_text = " ".join(soul.values.values()) + " " + " ".join(soul.constraints)
|
|
all_lower = all_text.lower()
|
|
|
|
for word_a, word_b in _CONTRADICTION_PAIRS:
|
|
if word_a in all_lower and word_b in all_lower:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.WARNING,
|
|
"contradictions",
|
|
f"Potential contradiction detected: '{word_a}' and '{word_b}' "
|
|
"both appear in values/constraints.",
|
|
)
|
|
)
|
|
|
|
return issues
|
|
|
|
@classmethod
|
|
def _check_extension_compatibility(
|
|
cls, extension: SoulDocument, base: SoulDocument
|
|
) -> list[ValidationIssue]:
|
|
"""Check that extension constraints don't contradict base constraints."""
|
|
issues: list[ValidationIssue] = []
|
|
|
|
base_text = " ".join(base.constraints).lower()
|
|
ext_text = " ".join(extension.constraints).lower()
|
|
|
|
for word_a, word_b in _CONTRADICTION_PAIRS:
|
|
if word_a in base_text and word_b in ext_text:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.ERROR,
|
|
"extension_compatibility",
|
|
f"Extension constraint contradicts base: base uses '{word_a}', "
|
|
f"extension uses '{word_b}'.",
|
|
)
|
|
)
|
|
if word_b in base_text and word_a in ext_text:
|
|
issues.append(
|
|
ValidationIssue(
|
|
Severity.ERROR,
|
|
"extension_compatibility",
|
|
f"Extension constraint contradicts base: base uses '{word_b}', "
|
|
f"extension uses '{word_a}'.",
|
|
)
|
|
)
|
|
|
|
return issues
|