Skip to content

Commit 0d7da25

Browse files
feat: Add advanced Decision Engine and unit tests
1 parent bc8e0c4 commit 0d7da25

2 files changed

Lines changed: 226 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
"""
3+
test_logic.py — CONTINUITY LEGACY Pro Tests
4+
===========================================
5+
Advanced Unit Tests for the Continuity Legacy Framework.
6+
7+
Covers:
8+
- Decision Engine momentum parsing
9+
- Continuity Cycle edge cases
10+
- State JSON validations
11+
12+
Uses Python's standard `unittest` framework.
13+
"""
14+
15+
import unittest
16+
import json
17+
import tempfile
18+
import os
19+
import datetime
20+
from pathlib import Path
21+
22+
# Import the modules to test (assuming they are in path or relative)
23+
import sys
24+
# Add tools/continuity_legacy to python path for testing
25+
current_dir = Path(__file__).parent
26+
tools_dir = current_dir.parent / "tools" / "continuity_legacy"
27+
sys.path.append(str(tools_dir))
28+
29+
# Attempt to import decision_engine. If it fails, tests will error appropriately.
30+
try:
31+
import decision_engine
32+
except ImportError:
33+
decision_engine = None
34+
35+
class TestDecisionEngine(unittest.TestCase):
36+
37+
def setUp(self):
38+
"""Create a temporary directory simulating a project root."""
39+
self.test_dir = tempfile.TemporaryDirectory()
40+
self.root_path = Path(self.test_dir.name)
41+
42+
def tearDown(self):
43+
self.test_dir.cleanup()
44+
45+
def test_momentum_high(self):
46+
"""Test high momentum scoring for recent updates."""
47+
if not decision_engine:
48+
self.skipTest("decision_engine.py module not found.")
49+
50+
recent_time = datetime.datetime.now(datetime.timezone.utc).isoformat()
51+
state_data = {"last_update": recent_time}
52+
53+
result = decision_engine.evaluate_momentum(state_data)
54+
self.assertEqual(result["score"], 95, "Recent update should score 95.")
55+
56+
def test_momentum_stale(self):
57+
"""Test stale momentum scoring for 30+ day old updates."""
58+
if not decision_engine:
59+
self.skipTest("decision_engine.py module not found.")
60+
61+
old_time = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=35)).isoformat()
62+
state_data = {"last_update": old_time}
63+
64+
result = decision_engine.evaluate_momentum(state_data)
65+
self.assertEqual(result["score"], 20, "Stale update should score 20.")
66+
67+
def test_tactical_directive_extraction(self):
68+
"""Test parsing of the LIVE_HANDOFF.md file for next actions."""
69+
if not decision_engine:
70+
self.skipTest("decision_engine.py module not found.")
71+
72+
continuity_dir = self.root_path / ".continuity"
73+
continuity_dir.mkdir(parents=True)
74+
75+
handoff_file = continuity_dir / "LIVE_HANDOFF.md"
76+
handoff_content = """# LIVE HANDOFF
77+
78+
## Next Exact Action
79+
Fix the unit tests in the tests folder.
80+
"""
81+
handoff_file.write_text(handoff_content, encoding="utf-8")
82+
83+
result = decision_engine.extract_tactical_directive(self.root_path)
84+
self.assertIn("Fix the unit tests", result, "Engine failed to extract the exact action.")
85+
86+
def test_missing_files_handling(self):
87+
"""Test graceful degradation when canonical files are missing."""
88+
if not decision_engine:
89+
self.skipTest("decision_engine.py module not found.")
90+
91+
# Empty repo root, STATE.json is missing
92+
state = decision_engine.parse_state(self.root_path)
93+
self.assertEqual(state["status"], "error")
94+
self.assertIn("not found", state["message"])
95+
96+
# LIVE_HANDOFF missing
97+
tactical = decision_engine.extract_tactical_directive(self.root_path)
98+
self.assertIn("CRITICAL", tactical)
99+
100+
if __name__ == "__main__":
101+
unittest.main()
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env python3
2+
"""
3+
decision_engine.py — CONTINUITY LEGACY Pro
4+
==========================================
5+
Advanced Strategic Decision Engine (Alpha).
6+
7+
This module analyzes the canonical memory files (STATE.json, ROADMAP.md, LIVE_HANDOFF.md)
8+
to identify technical debt, project momentum stalls, and missing contexts.
9+
It outputs actionable strategic recommendations for the human / AI developer team.
10+
11+
Features:
12+
- State staleness detection
13+
- Roadmap alignment validation
14+
- Tactical extraction from LIVE_HANDOFF.md
15+
"""
16+
17+
import json
18+
import datetime
19+
from pathlib import Path
20+
21+
def parse_state(repo_root: Path) -> dict:
22+
"""Safely loads and parses STATE.json."""
23+
state_file = repo_root / "STATE.json"
24+
if not state_file.exists():
25+
return {"status": "error", "message": "STATE.json not found."}
26+
try:
27+
with open(state_file, "r", encoding="utf-8") as f:
28+
return json.load(f)
29+
except Exception as e:
30+
return {"status": "error", "message": f"Malformed STATE.json: {e}"}
31+
32+
def evaluate_momentum(state_data: dict) -> dict:
33+
"""Evaluates project momentum based on the last update timestamp."""
34+
try:
35+
last_update_str = state_data.get("last_update", state_data.get("generated_at", ""))
36+
if not last_update_str:
37+
return {"score": 50, "insight": "No timestamp found in STATE.json."}
38+
39+
# Parse ISO format handling potential timezone strings (basic implementation)
40+
clean_str = last_update_str.replace("Z", "+00:00")
41+
last_update = datetime.datetime.fromisoformat(clean_str)
42+
43+
# Calculate delta
44+
now = datetime.datetime.now(datetime.timezone.utc)
45+
delta = now - last_update
46+
47+
if delta.days > 30:
48+
return {"score": 20, "insight": "Project stale. Over 30 days without updates."}
49+
elif delta.days > 7:
50+
return {"score": 60, "insight": "Momentum slowing. Over a week since last update."}
51+
else:
52+
return {"score": 95, "insight": "High momentum. Active development detected."}
53+
except Exception:
54+
return {"score": 50, "insight": "Could not parse last update time."}
55+
56+
def extract_tactical_directive(repo_root: Path) -> str:
57+
"""Extracts the immediate next action from LIVE_HANDOFF.md."""
58+
handoff_file = repo_root / ".continuity" / "LIVE_HANDOFF.md"
59+
if not handoff_file.exists():
60+
return "CRITICAL: LIVE_HANDOFF.md missing. Next exact action is undefined."
61+
62+
try:
63+
content = handoff_file.read_text(encoding="utf-8")
64+
lines = content.splitlines()
65+
for idx, line in enumerate(lines):
66+
if "Next Exact Action" in line:
67+
# Capture the next non-empty line as the directive
68+
for next_line in lines[idx+1:idx+5]:
69+
stripped = next_line.strip()
70+
if stripped and not stripped.startswith("#"):
71+
return f"Directive found: {stripped}"
72+
return "Warning: Next Exact Action header found but no actionable step defined."
73+
except Exception:
74+
return "Error reading LIVE_HANDOFF.md."
75+
76+
def generate_strategy(repo_root: Path) -> dict:
77+
"""Orchestrates the decision engine analysis."""
78+
print("[*] Engine Initializing: Strategic Analysis...")
79+
80+
state = parse_state(repo_root)
81+
if state.get("status") == "error":
82+
print(f"[!] {state['message']}")
83+
return state
84+
85+
momentum = evaluate_momentum(state)
86+
tactical = extract_tactical_directive(repo_root)
87+
88+
# Formulate recommendation
89+
recommendation = "Continue execution of the roadmap."
90+
if momentum["score"] < 50:
91+
recommendation = "PRIORITY: Review STATE.json. Update current context to regain momentum."
92+
elif "missing" in tactical.lower() or "error" in tactical.lower():
93+
recommendation = "PRIORITY: Re-establish the LIVE_HANDOFF.md to define the next exact action for the AI agent."
94+
95+
report = {
96+
"status": "success",
97+
"generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
98+
"momentum_score": momentum["score"],
99+
"momentum_insight": momentum["insight"],
100+
"tactical_directive": tactical,
101+
"strategic_recommendation": recommendation
102+
}
103+
104+
# Write output
105+
out_dir = repo_root / "outputs" / "continuity"
106+
out_dir.mkdir(parents=True, exist_ok=True)
107+
out_file = out_dir / "strategic_decision.json"
108+
109+
with open(out_file, "w", encoding="utf-8") as f:
110+
json.dump(report, f, indent=2)
111+
112+
print(f"[+] Advanced Strategy Report generated at: {out_file.relative_to(repo_root)}")
113+
print(f" -> Momentum: {momentum['score']}/100")
114+
print(f" -> Strategy: {recommendation}")
115+
116+
return report
117+
118+
if __name__ == "__main__":
119+
import argparse
120+
parser = argparse.ArgumentParser(description="CONTINUITY LEGACY Pro - Decision Engine")
121+
parser.add_argument("--repo-root", default=".", help="Root directory of the project")
122+
args = parser.parse_args()
123+
124+
root_path = Path(args.repo_root).resolve()
125+
generate_strategy(root_path)

0 commit comments

Comments
 (0)