Fix goals audit to preserve prose and support partial goals
GoalsSaver changes: - Now preserves prose content (Vision, Principles, etc.) before goal sections - Only rewrites ## Active, ## Future, ## Non-Goals sections - Extracts and reinserts prose content between frontmatter and goals Audit handler changes: - Supports new status field: "met", "partial", "not_met" - Maintains backward compatibility with old "checked" boolean format - Sets Goal.partial=True for partially achieved goals - Report shows partial count in summary This fixes a bug where the audit button would delete all prose content from goals.md files that had philosophy/context sections. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
69ec7df308
commit
b084e9e30a
|
|
@ -428,7 +428,12 @@ class MilestonesParser(BaseParser):
|
||||||
|
|
||||||
|
|
||||||
class GoalsSaver:
|
class GoalsSaver:
|
||||||
"""Save goals back to goals.md file."""
|
"""Save goals back to goals.md file.
|
||||||
|
|
||||||
|
This saver preserves prose content (Vision, Principles, etc.) that appears
|
||||||
|
before the goal sections. Only the ## Active, ## Future, and ## Non-Goals
|
||||||
|
sections are rewritten with updated checkbox states.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, path: Path, frontmatter: dict):
|
def __init__(self, path: Path, frontmatter: dict):
|
||||||
"""Initialize saver.
|
"""Initialize saver.
|
||||||
|
|
@ -441,11 +446,19 @@ class GoalsSaver:
|
||||||
self.frontmatter = frontmatter
|
self.frontmatter = frontmatter
|
||||||
|
|
||||||
def save(self, goal_list: GoalList):
|
def save(self, goal_list: GoalList):
|
||||||
"""Save goals back to file.
|
"""Save goals back to file, preserving prose content.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
goal_list: GoalList to save
|
goal_list: GoalList to save
|
||||||
"""
|
"""
|
||||||
|
# Read existing file to preserve prose content
|
||||||
|
existing_content = ""
|
||||||
|
if self.path.exists():
|
||||||
|
existing_content = self.path.read_text()
|
||||||
|
|
||||||
|
# Extract prose content (everything before first goal section)
|
||||||
|
prose_content = self._extract_prose_content(existing_content)
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
|
|
||||||
# Write frontmatter
|
# Write frontmatter
|
||||||
|
|
@ -454,7 +467,12 @@ class GoalsSaver:
|
||||||
lines.append(f"{key}: {value}")
|
lines.append(f"{key}: {value}")
|
||||||
lines.append("---")
|
lines.append("---")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("# Goals")
|
|
||||||
|
# Add preserved prose content (without frontmatter, it's already added)
|
||||||
|
if prose_content:
|
||||||
|
lines.append(prose_content)
|
||||||
|
# Ensure there's a blank line before goal sections
|
||||||
|
if not prose_content.endswith("\n\n"):
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
# Active goals
|
# Active goals
|
||||||
|
|
@ -488,6 +506,53 @@ class GoalsSaver:
|
||||||
|
|
||||||
atomic_write(self.path, "\n".join(lines))
|
atomic_write(self.path, "\n".join(lines))
|
||||||
|
|
||||||
|
def _extract_prose_content(self, content: str) -> str:
|
||||||
|
"""Extract prose content before goal sections.
|
||||||
|
|
||||||
|
Preserves everything between the frontmatter and the first goal section
|
||||||
|
(## Active, ## Future, or ## Non-Goals).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
content: Full file content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Prose content without frontmatter, or empty string
|
||||||
|
"""
|
||||||
|
if not content:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Remove frontmatter
|
||||||
|
lines = content.split("\n")
|
||||||
|
start_idx = 0
|
||||||
|
|
||||||
|
# Skip frontmatter (between --- markers)
|
||||||
|
if lines and lines[0].strip() == "---":
|
||||||
|
for i, line in enumerate(lines[1:], 1):
|
||||||
|
if line.strip() == "---":
|
||||||
|
start_idx = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find where goal sections start
|
||||||
|
goal_section_headers = ["## active", "## future", "## non-goal", "## non goal"]
|
||||||
|
end_idx = len(lines)
|
||||||
|
|
||||||
|
for i, line in enumerate(lines[start_idx:], start_idx):
|
||||||
|
line_lower = line.strip().lower()
|
||||||
|
if any(line_lower.startswith(header) for header in goal_section_headers):
|
||||||
|
end_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# Extract prose content
|
||||||
|
prose_lines = lines[start_idx:end_idx]
|
||||||
|
|
||||||
|
# Strip leading/trailing empty lines but preserve internal structure
|
||||||
|
while prose_lines and not prose_lines[0].strip():
|
||||||
|
prose_lines.pop(0)
|
||||||
|
while prose_lines and not prose_lines[-1].strip():
|
||||||
|
prose_lines.pop()
|
||||||
|
|
||||||
|
return "\n".join(prose_lines)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_checkbox(goal: Goal) -> str:
|
def _get_checkbox(goal: Goal) -> str:
|
||||||
"""Get the checkbox marker for a goal's state.
|
"""Get the checkbox marker for a goal's state.
|
||||||
|
|
|
||||||
|
|
@ -2512,29 +2512,53 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
updated=frontmatter.get("updated"),
|
updated=frontmatter.get("updated"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def parse_status(item: dict) -> tuple[bool, bool]:
|
||||||
|
"""Parse status field to (completed, partial) tuple.
|
||||||
|
|
||||||
|
Supports both old format (checked: bool) and new format (status: str).
|
||||||
|
"""
|
||||||
|
# New format: status field
|
||||||
|
status = item.get("status", "").lower()
|
||||||
|
if status == "met":
|
||||||
|
return True, False
|
||||||
|
elif status == "partial":
|
||||||
|
return False, True
|
||||||
|
elif status == "not_met":
|
||||||
|
return False, False
|
||||||
|
|
||||||
|
# Old format fallback: checked field
|
||||||
|
checked = item.get("checked", False)
|
||||||
|
return checked, False
|
||||||
|
|
||||||
# Process active goals
|
# Process active goals
|
||||||
for item in audit_data.get("active", []):
|
for item in audit_data.get("active", []):
|
||||||
|
completed, partial = parse_status(item)
|
||||||
goal = Goal(
|
goal = Goal(
|
||||||
text=item.get("text", ""),
|
text=item.get("text", ""),
|
||||||
completed=item.get("checked", False),
|
completed=completed,
|
||||||
|
partial=partial,
|
||||||
priority=item.get("priority", "medium"),
|
priority=item.get("priority", "medium"),
|
||||||
)
|
)
|
||||||
goal_list.active.append(goal)
|
goal_list.active.append(goal)
|
||||||
|
|
||||||
# Process future goals
|
# Process future goals
|
||||||
for item in audit_data.get("future", []):
|
for item in audit_data.get("future", []):
|
||||||
|
completed, partial = parse_status(item)
|
||||||
goal = Goal(
|
goal = Goal(
|
||||||
text=item.get("text", ""),
|
text=item.get("text", ""),
|
||||||
completed=item.get("checked", False),
|
completed=completed,
|
||||||
|
partial=partial,
|
||||||
priority=item.get("priority", "medium"),
|
priority=item.get("priority", "medium"),
|
||||||
)
|
)
|
||||||
goal_list.future.append(goal)
|
goal_list.future.append(goal)
|
||||||
|
|
||||||
# Process non-goals
|
# Process non-goals
|
||||||
for item in audit_data.get("non_goals", []):
|
for item in audit_data.get("non_goals", []):
|
||||||
|
completed, partial = parse_status(item)
|
||||||
goal = Goal(
|
goal = Goal(
|
||||||
text=item.get("text", ""),
|
text=item.get("text", ""),
|
||||||
completed=item.get("checked", False),
|
completed=completed,
|
||||||
|
partial=partial,
|
||||||
priority=item.get("priority", "medium"),
|
priority=item.get("priority", "medium"),
|
||||||
)
|
)
|
||||||
goal_list.non_goals.append(goal)
|
goal_list.non_goals.append(goal)
|
||||||
|
|
@ -2545,6 +2569,19 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
|
|
||||||
def _build_audit_report(self, audit_data: dict) -> str:
|
def _build_audit_report(self, audit_data: dict) -> str:
|
||||||
"""Build a human-readable report from audit data."""
|
"""Build a human-readable report from audit data."""
|
||||||
|
|
||||||
|
def get_status_marker(item: dict) -> str:
|
||||||
|
"""Get checkbox marker from status or checked field."""
|
||||||
|
status = item.get("status", "").lower()
|
||||||
|
if status == "met":
|
||||||
|
return "[x]"
|
||||||
|
elif status == "partial":
|
||||||
|
return "[~]"
|
||||||
|
elif status == "not_met":
|
||||||
|
return "[ ]"
|
||||||
|
# Fallback to old format
|
||||||
|
return "[x]" if item.get("checked") else "[ ]"
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
lines.append("=" * 60)
|
lines.append("=" * 60)
|
||||||
lines.append("GOALS AUDIT REPORT")
|
lines.append("GOALS AUDIT REPORT")
|
||||||
|
|
@ -2555,7 +2592,7 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
lines.append("ACTIVE GOALS")
|
lines.append("ACTIVE GOALS")
|
||||||
lines.append("-" * 40)
|
lines.append("-" * 40)
|
||||||
for item in audit_data.get("active", []):
|
for item in audit_data.get("active", []):
|
||||||
status = "[x]" if item.get("checked") else "[ ]"
|
status = get_status_marker(item)
|
||||||
lines.append(f"{status} {item.get('text', '')}")
|
lines.append(f"{status} {item.get('text', '')}")
|
||||||
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
@ -2564,7 +2601,7 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
lines.append("FUTURE GOALS")
|
lines.append("FUTURE GOALS")
|
||||||
lines.append("-" * 40)
|
lines.append("-" * 40)
|
||||||
for item in audit_data.get("future", []):
|
for item in audit_data.get("future", []):
|
||||||
status = "[x]" if item.get("checked") else "[ ]"
|
status = get_status_marker(item)
|
||||||
lines.append(f"{status} {item.get('text', '')}")
|
lines.append(f"{status} {item.get('text', '')}")
|
||||||
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
@ -2573,7 +2610,7 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
lines.append("NON-GOALS")
|
lines.append("NON-GOALS")
|
||||||
lines.append("-" * 40)
|
lines.append("-" * 40)
|
||||||
for item in audit_data.get("non_goals", []):
|
for item in audit_data.get("non_goals", []):
|
||||||
status = "[x]" if item.get("checked") else "[ ]"
|
status = get_status_marker(item)
|
||||||
lines.append(f"{status} {item.get('text', '')}")
|
lines.append(f"{status} {item.get('text', '')}")
|
||||||
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
@ -2583,7 +2620,13 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||||
lines.append("=" * 60)
|
lines.append("=" * 60)
|
||||||
lines.append("SUMMARY")
|
lines.append("SUMMARY")
|
||||||
lines.append("=" * 60)
|
lines.append("=" * 60)
|
||||||
lines.append(f"Active Goals: {summary.get('active_met', 0)}/{summary.get('active_total', 0)} met")
|
active_met = summary.get('active_met', 0)
|
||||||
|
active_partial = summary.get('active_partial', 0)
|
||||||
|
active_total = summary.get('active_total', 0)
|
||||||
|
if active_partial > 0:
|
||||||
|
lines.append(f"Active Goals: {active_met} met, {active_partial} partial / {active_total} total")
|
||||||
|
else:
|
||||||
|
lines.append(f"Active Goals: {active_met}/{active_total} met")
|
||||||
lines.append(f"Future Goals: {summary.get('future_started', 0)}/{summary.get('future_total', 0)} started")
|
lines.append(f"Future Goals: {summary.get('future_started', 0)}/{summary.get('future_total', 0)} started")
|
||||||
lines.append(f"Non-Goals: {summary.get('non_goals_confirmed', 0)}/{summary.get('non_goals_total', 0)} confirmed")
|
lines.append(f"Non-Goals: {summary.get('non_goals_confirmed', 0)}/{summary.get('non_goals_total', 0)} confirmed")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue