feat: Implement Study Interview Mode as default study creation method

Study Interview Mode is now the DEFAULT for all study creation requests.
This intelligent Q&A system guides users through optimization setup with:

- 7-phase interview flow: introspection → objectives → constraints → design_variables → validation → review → complete
- Material-aware validation with 12 materials and fuzzy name matching
- Anti-pattern detection for 12 common mistakes (mass-no-constraint, stress-over-yield, etc.)
- Auto extractor mapping E1-E24 based on goal keywords
- State persistence with JSON serialization and backup rotation
- StudyBlueprint generation with full validation

Triggers: "create a study", "new study", "optimize this", any study creation intent
Skip with: "skip interview", "quick setup", "manual config"

Components:
- StudyInterviewEngine: Main orchestrator
- QuestionEngine: Conditional logic evaluation
- EngineeringValidator: MaterialsDatabase + AntiPatternDetector
- InterviewPresenter: Markdown formatting for Claude
- StudyBlueprint: Validated configuration output
- InterviewState: Persistent state management

All 129 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-03 11:06:07 -05:00
parent b1ffc64407
commit 32caa5d05c
27 changed files with 9737 additions and 11 deletions

View File

@@ -0,0 +1,295 @@
"""Tests for InterviewState and InterviewStateManager."""
import pytest
import json
import tempfile
from pathlib import Path
from datetime import datetime
from optimization_engine.interview.interview_state import (
InterviewState,
InterviewPhase,
InterviewStateManager,
AnsweredQuestion,
LogEntry,
)
class TestInterviewPhase:
"""Tests for InterviewPhase enum."""
def test_from_string(self):
"""Test converting string to enum."""
assert InterviewPhase.from_string("introspection") == InterviewPhase.INTROSPECTION
assert InterviewPhase.from_string("objectives") == InterviewPhase.OBJECTIVES
assert InterviewPhase.from_string("complete") == InterviewPhase.COMPLETE
def test_from_string_invalid(self):
"""Test invalid string raises error."""
with pytest.raises(ValueError):
InterviewPhase.from_string("invalid_phase")
def test_next_phase(self):
"""Test getting next phase."""
assert InterviewPhase.INTROSPECTION.next_phase() == InterviewPhase.PROBLEM_DEFINITION
assert InterviewPhase.OBJECTIVES.next_phase() == InterviewPhase.CONSTRAINTS
assert InterviewPhase.COMPLETE.next_phase() is None
def test_previous_phase(self):
"""Test getting previous phase."""
assert InterviewPhase.OBJECTIVES.previous_phase() == InterviewPhase.PROBLEM_DEFINITION
assert InterviewPhase.INTROSPECTION.previous_phase() is None
class TestAnsweredQuestion:
"""Tests for AnsweredQuestion dataclass."""
def test_to_dict(self):
"""Test conversion to dict."""
aq = AnsweredQuestion(
question_id="obj_01",
answered_at="2026-01-02T10:00:00",
raw_response="minimize mass",
parsed_value="minimize_mass",
inferred={"extractor": "E4"}
)
d = aq.to_dict()
assert d["question_id"] == "obj_01"
assert d["parsed_value"] == "minimize_mass"
assert d["inferred"]["extractor"] == "E4"
def test_from_dict(self):
"""Test creation from dict."""
data = {
"question_id": "obj_01",
"answered_at": "2026-01-02T10:00:00",
"raw_response": "minimize mass",
"parsed_value": "minimize_mass",
}
aq = AnsweredQuestion.from_dict(data)
assert aq.question_id == "obj_01"
assert aq.parsed_value == "minimize_mass"
class TestInterviewState:
"""Tests for InterviewState dataclass."""
def test_default_values(self):
"""Test default initialization."""
state = InterviewState()
assert state.version == "1.0"
assert state.session_id != ""
assert state.current_phase == InterviewPhase.INTROSPECTION.value
assert state.complexity == "simple"
assert state.answers["objectives"] == []
def test_get_phase(self):
"""Test getting phase as enum."""
state = InterviewState(current_phase="objectives")
assert state.get_phase() == InterviewPhase.OBJECTIVES
def test_set_phase(self):
"""Test setting phase."""
state = InterviewState()
state.set_phase(InterviewPhase.CONSTRAINTS)
assert state.current_phase == "constraints"
def test_is_complete(self):
"""Test completion check."""
state = InterviewState(current_phase="review")
assert not state.is_complete()
state.current_phase = "complete"
assert state.is_complete()
def test_progress_percentage(self):
"""Test progress calculation."""
state = InterviewState(current_phase="introspection")
assert state.progress_percentage() == 0.0
state.current_phase = "complete"
assert state.progress_percentage() == 100.0
def test_add_answered_question(self):
"""Test adding answered question."""
state = InterviewState()
aq = AnsweredQuestion(
question_id="pd_01",
answered_at=datetime.now().isoformat(),
raw_response="test",
parsed_value="test"
)
state.add_answered_question(aq)
assert len(state.questions_answered) == 1
def test_add_warning(self):
"""Test adding warnings."""
state = InterviewState()
state.add_warning("Test warning")
assert "Test warning" in state.warnings
# Duplicate should not be added
state.add_warning("Test warning")
assert len(state.warnings) == 1
def test_acknowledge_warning(self):
"""Test acknowledging warnings."""
state = InterviewState()
state.add_warning("Test warning")
state.acknowledge_warning("Test warning")
assert "Test warning" in state.warnings_acknowledged
def test_to_json(self):
"""Test JSON serialization."""
state = InterviewState(study_name="test_study")
json_str = state.to_json()
data = json.loads(json_str)
assert data["study_name"] == "test_study"
assert data["version"] == "1.0"
def test_from_json(self):
"""Test JSON deserialization."""
json_str = '{"version": "1.0", "session_id": "abc", "study_name": "test", "current_phase": "objectives", "answers": {}}'
state = InterviewState.from_json(json_str)
assert state.study_name == "test"
assert state.current_phase == "objectives"
def test_validate(self):
"""Test state validation."""
state = InterviewState()
errors = state.validate()
assert "Missing study_name" in errors
state.study_name = "test"
errors = state.validate()
assert "Missing study_name" not in errors
class TestInterviewStateManager:
"""Tests for InterviewStateManager."""
def test_init_creates_directories(self):
"""Test initialization creates needed directories."""
with tempfile.TemporaryDirectory() as tmpdir:
study_path = Path(tmpdir) / "test_study"
study_path.mkdir()
manager = InterviewStateManager(study_path)
assert (study_path / ".interview").exists()
assert (study_path / ".interview" / "backups").exists()
def test_save_and_load_state(self):
"""Test saving and loading state."""
with tempfile.TemporaryDirectory() as tmpdir:
study_path = Path(tmpdir) / "test_study"
study_path.mkdir()
manager = InterviewStateManager(study_path)
state = InterviewState(
study_name="test_study",
study_path=str(study_path),
current_phase="objectives"
)
manager.save_state(state)
assert manager.exists()
loaded = manager.load_state()
assert loaded is not None
assert loaded.study_name == "test_study"
assert loaded.current_phase == "objectives"
def test_append_log(self):
"""Test appending to log file."""
with tempfile.TemporaryDirectory() as tmpdir:
study_path = Path(tmpdir) / "test_study"
study_path.mkdir()
manager = InterviewStateManager(study_path)
entry = LogEntry(
timestamp=datetime.now(),
question_id="obj_01",
question_text="What is your goal?",
answer_raw="minimize mass",
answer_parsed="minimize_mass"
)
manager.append_log(entry)
assert manager.log_file.exists()
content = manager.log_file.read_text()
assert "obj_01" in content
assert "minimize mass" in content
def test_backup_rotation(self):
"""Test backup rotation keeps only N backups."""
with tempfile.TemporaryDirectory() as tmpdir:
study_path = Path(tmpdir) / "test_study"
study_path.mkdir()
manager = InterviewStateManager(study_path)
manager.MAX_BACKUPS = 3
# Create multiple saves
for i in range(5):
state = InterviewState(
study_name=f"test_{i}",
study_path=str(study_path)
)
manager.save_state(state)
backups = list(manager.backup_dir.glob("state_*.json"))
assert len(backups) <= 3
def test_get_history(self):
"""Test getting modification history."""
with tempfile.TemporaryDirectory() as tmpdir:
study_path = Path(tmpdir) / "test_study"
study_path.mkdir()
manager = InterviewStateManager(study_path)
# Save multiple states
for i in range(3):
state = InterviewState(
study_name=f"test_{i}",
study_path=str(study_path),
current_phase=["objectives", "constraints", "review"][i]
)
manager.save_state(state)
history = manager.get_history()
# Should have 2 backups (first save doesn't create backup)
assert len(history) >= 1
class TestLogEntry:
"""Tests for LogEntry dataclass."""
def test_to_markdown(self):
"""Test markdown generation."""
entry = LogEntry(
timestamp=datetime(2026, 1, 2, 10, 30, 0),
question_id="obj_01",
question_text="What is your primary optimization goal?",
answer_raw="minimize mass",
answer_parsed="minimize_mass",
inferred={"extractor": "E4"},
warnings=["Consider safety factor"]
)
md = entry.to_markdown()
assert "## [2026-01-02 10:30:00]" in md
assert "obj_01" in md
assert "minimize mass" in md
assert "Extractor" in md.lower() or "extractor" in md
assert "Consider safety factor" in md